Implement basic dowsing rod functionality

This commit is contained in:
thepaperpilot 2023-04-23 17:42:22 -05:00
parent d93cc8c3ec
commit 24fa24f86a

View file

@ -8,28 +8,29 @@ import {
createBoard, createBoard,
getUniqueNodeID getUniqueNodeID
} from "features/boards/board"; } from "features/boards/board";
import { jsx } from "features/feature"; import { JSXFunction, jsx } from "features/feature";
import { createResource } from "features/resources/resource"; import { createResource } from "features/resources/resource";
import { createTabFamily } from "features/tabs/tabFamily"; import { createTabFamily } from "features/tabs/tabFamily";
import Formula, { calculateCost } from "game/formulas/formulas"; import Formula, { calculateCost } from "game/formulas/formulas";
import type { BaseLayer, GenericLayer } from "game/layers"; import type { BaseLayer, GenericLayer } from "game/layers";
import { createLayer } from "game/layers"; import { createLayer } from "game/layers";
import { import {
Modifier,
createAdditiveModifier, createAdditiveModifier,
createMultiplicativeModifier, createMultiplicativeModifier,
createSequentialModifier createSequentialModifier
} from "game/modifiers"; } from "game/modifiers";
import { State, persistent } from "game/persistence"; import { Persistent, State, persistent } from "game/persistence";
import type { Player } from "game/player"; import type { Player } from "game/player";
import player from "game/player"; import player from "game/player";
import settings from "game/settings"; import settings from "game/settings";
import Decimal, { DecimalSource } from "lib/break_eternity"; import Decimal, { DecimalSource } from "lib/break_eternity";
import { format, formatWhole } from "util/bignum"; import { format, formatWhole } from "util/bignum";
import { camelToTitle } from "util/common"; import { WithRequired, camelToTitle } from "util/common";
import { render } from "util/vue"; import { render } from "util/vue";
import { ComputedRef, computed, nextTick, reactive, ref, watch } from "vue"; import { ComputedRef, computed, nextTick, reactive, ref, watch } from "vue";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import { createCollapsibleModifierSections, createFormulaPreview } from "./common"; import { Section, createCollapsibleModifierSections, createFormulaPreview } from "./common";
import "./main.css"; import "./main.css";
const toast = useToast(); const toast = useToast();
@ -44,6 +45,12 @@ export interface ResourceState {
amount: DecimalSource; amount: DecimalSource;
} }
export interface DowsingState {
resources: Resources[];
maxConnections: number;
powered: boolean;
}
const mineLootTable = { const mineLootTable = {
dirt: 120, dirt: 120,
sand: 60, sand: 60,
@ -64,6 +71,7 @@ const mineLootTable = {
} as const; } as const;
export type Resources = keyof typeof mineLootTable; export type Resources = keyof typeof mineLootTable;
const resourceNames = Object.keys(mineLootTable) as Resources[];
const tools = { const tools = {
dirt: { dirt: {
@ -75,7 +83,8 @@ const tools = {
sand: { sand: {
cost: 1e4, cost: 1e4,
name: "Dowsing Rod", name: "Dowsing Rod",
type: "dowsing" type: "dowsing",
state: { resources: [], maxConnections: 1, powered: false }
}, },
gravel: { gravel: {
cost: 1e5, cost: 1e5,
@ -193,13 +202,13 @@ export const main = createLayer("main", function (this: BaseLayer) {
}, {} as Record<Resources, BoardNode>) }, {} as Record<Resources, BoardNode>)
); );
const toolNodes: ComputedRef<Record<Resources, BoardNode>> = computed(() => const toolNodes: ComputedRef<Record<Resources, BoardNode>> = computed(() => ({
// TODO add non-passive tools ...board.types.passive.nodes.value.reduce((acc, curr) => {
board.types.passive.nodes.value.reduce((acc, curr) => {
acc[curr.state as Resources] = curr; acc[curr.state as Resources] = curr;
return acc; return acc;
}, {} as Record<Resources, BoardNode>) }, {} as Record<Resources, BoardNode>),
); sand: board.types.dowsing.nodes.value[0]
}));
const resourceLevels = computed(() => const resourceLevels = computed(() =>
resourceNames.reduce((acc, curr) => { resourceNames.reduce((acc, curr) => {
@ -268,17 +277,14 @@ export const main = createLayer("main", function (this: BaseLayer) {
}); });
}); });
const poweredMachines = computed(() => { const poweredMachines: ComputedRef<number> = computed(() => {
let poweredMachines = 0; return [mine, dowsing].filter(node => (node.value?.state as { powered: boolean })?.powered)
if ((mine.value.state as unknown as MineState).powered) { .length;
poweredMachines++;
}
return poweredMachines;
}); });
const nextPowerCost = computed(() => const nextPowerCost = computed(() =>
Decimal.eq(poweredMachines.value, 0) Decimal.eq(poweredMachines.value, 0)
? 10 ? 10
: Decimal.add(poweredMachines.value, 1).pow10().times(0.9) : Decimal.add(poweredMachines.value, 1).pow_base(100).div(10).times(0.99)
); );
const togglePoweredAction = { const togglePoweredAction = {
@ -359,44 +365,36 @@ export const main = createLayer("main", function (this: BaseLayer) {
shape: Shape.Diamond, shape: Shape.Diamond,
size: 50, size: 50,
title: "🛠️", title: "🛠️",
label: node => label: node => {
node === board.selectedNode.value if (node === board.selectedNode.value) {
? { return {
text: text:
node.state == null node.state == null
? hasForged.value ? hasForged.value
? "Forge" ? "Forge"
: "Forge - Drag a material to me!" : "Forge - Drag a resource to me!"
: `Forge - ${tools[node.state as Resources].name} selected` : `Forge - ${tools[node.state as Resources].name} selected`
} };
: (board as GenericBoard).draggingNode.value?.type === "resource" }
? { if ((board as GenericBoard).draggingNode.value?.type === "resource") {
text: tools[ const resource = (
( (board as GenericBoard).draggingNode.value
(board as GenericBoard).draggingNode.value ?.state as unknown as ResourceState
?.state as unknown as ResourceState ).type;
).type const text = node.state === resource ? "Disconnect" : tools[resource].name;
].name, const color =
color: node.state === resource ||
Decimal.gte( (Decimal.gte(energy.value, tools[resource].cost) &&
energy.value, toolNodes.value[resource] == null)
tools[ ? "var(--accent2)"
( : "var(--danger)";
(board as GenericBoard).draggingNode.value return {
?.state as unknown as ResourceState text,
).type color
].cost };
) && }
!( return null;
( },
(board as GenericBoard).draggingNode.value
?.state as unknown as ResourceState
).type in toolNodes.value
)
? "var(--accent2)"
: "var(--danger)"
}
: null,
actionDistance: 100, actionDistance: 100,
actions: [ actions: [
{ {
@ -411,7 +409,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
const tool = tools[node.state as Resources]; const tool = tools[node.state as Resources];
if ( if (
Decimal.gte(energy.value, tool.cost) && Decimal.gte(energy.value, tool.cost) &&
!((node.state as Resources) in toolNodes.value) toolNodes.value[node.state as Resources] == null
) { ) {
energy.value = Decimal.sub(energy.value, tool.cost); energy.value = Decimal.sub(energy.value, tool.cost);
const newNode = { const newNode = {
@ -429,13 +427,13 @@ export const main = createLayer("main", function (this: BaseLayer) {
}, },
fillColor: node => fillColor: node =>
Decimal.gte(energy.value, tools[node.state as Resources].cost) && Decimal.gte(energy.value, tools[node.state as Resources].cost) &&
!((node.state as Resources) in toolNodes.value) toolNodes.value[node.state as Resources] == null
? "var(--accent2)" ? "var(--accent2)"
: "var(--danger)", : "var(--danger)",
visibility: node => node.state != null, visibility: node => node.state != null,
confirmationLabel: node => confirmationLabel: node =>
Decimal.gte(energy.value, tools[node.state as Resources].cost) Decimal.gte(energy.value, tools[node.state as Resources].cost)
? !((node.state as Resources) in toolNodes.value) ? toolNodes.value[node.state as Resources] == null
? { text: "Tap again to confirm" } ? { text: "Tap again to confirm" }
: { text: "Already crafted", color: "var(--danger)" } : { text: "Already crafted", color: "var(--danger)" }
: { text: "Cannot afford", color: "var(--danger)" } : { text: "Cannot afford", color: "var(--danger)" }
@ -443,7 +441,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
{ {
id: "deselect", id: "deselect",
icon: "close", icon: "close",
tooltip: { text: "De-select material" }, tooltip: { text: "Disconnect resource" },
onClick(node) { onClick(node) {
node.state = undefined; node.state = undefined;
board.selectedAction.value = null; board.selectedAction.value = null;
@ -492,6 +490,94 @@ export const main = createLayer("main", function (this: BaseLayer) {
: null, : null,
outlineColor: "var(--bought)", outlineColor: "var(--bought)",
draggable: true draggable: true
},
dowsing: {
shape: Shape.Diamond,
size: 50,
title: "🥢",
label: node => {
if (node === board.selectedNode.value) {
return {
text:
(node.state as unknown as DowsingState).resources.length === 0
? "Dowsing - Drag a resource to me!"
: `Dowsing - Doubling ${
(node.state as { resources: Resources[] }).resources
.length
} materials' odds`
};
}
if ((board as GenericBoard).draggingNode.value?.type === "resource") {
const resource = (
(board as GenericBoard).draggingNode.value
?.state as unknown as ResourceState
).type;
const { maxConnections, resources } = node.state as unknown as DowsingState;
if (resources.includes(resource)) {
return { text: "Disconnect", color: "var(--accent2)" };
}
if (resources.length === maxConnections) {
return { text: "Max connections", color: "var(--danger)" };
}
return {
text: `Double ${resource} odds`,
color: "var(--accent2)"
};
}
return null;
},
actions: [
{
id: "deselect",
icon: "close",
tooltip: { text: "Disconnect resources" },
onClick(node) {
node.state = { ...(node.state as object), resources: [] };
board.selectedAction.value = null;
board.selectedNode.value = null;
},
visibility: node =>
(node.state as unknown as DowsingState).resources.length > 0
},
togglePoweredAction
],
classes: node => ({
running: isPowered(node)
}),
canAccept(node, otherNode) {
if (otherNode.type !== "resource") {
return false;
}
const resource = (otherNode.state as unknown as ResourceState).type;
const { maxConnections, resources } = node.state as unknown as DowsingState;
if (resources.includes(resource)) {
return true;
}
if (resources.length === maxConnections) {
return false;
}
return true;
},
onDrop(node, otherNode) {
if (otherNode.type !== "resource") {
return;
}
const resource = (otherNode.state as unknown as ResourceState).type;
const resources = (node.state as unknown as DowsingState).resources;
if (resources.includes(resource)) {
node.state = {
...(node.state as object),
resources: resources.filter(r => r !== resource)
};
} else {
node.state = {
...(node.state as object),
resources: [...resources, resource]
};
}
board.selectedNode.value = node;
},
draggable: true
} }
}, },
style: { style: {
@ -517,6 +603,17 @@ export const main = createLayer("main", function (this: BaseLayer) {
strokeWidth: 4 strokeWidth: 4
}); });
} }
if (dowsing.value != null) {
(dowsing.value.state as unknown as DowsingState).resources.forEach(resource => {
links.push({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
startNode: dowsing.value!,
endNode: resourceNodes.value[resource],
stroke: isPowered(dowsing.value!) ? "var(--accent1)" : "var(--foreground)",
strokeWidth: 4
});
});
}
return links; return links;
} }
})); }));
@ -529,6 +626,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
() => board.nodes.value.find(n => n.type === "mine") as BoardNode () => board.nodes.value.find(n => n.type === "mine") as BoardNode
); );
const factory = computed(() => board.nodes.value.find(n => n.type === "factory")); const factory = computed(() => board.nodes.value.find(n => n.type === "factory"));
const dowsing = computed(() => board.nodes.value.find(n => n.type === "dowsing"));
function grantResource(type: Resources, amount: DecimalSource) { function grantResource(type: Resources, amount: DecimalSource) {
let node = resourceNodes.value[type]; let node = resourceNodes.value[type];
@ -551,8 +649,12 @@ export const main = createLayer("main", function (this: BaseLayer) {
} }
// Amount of completions that could give you the exact average of each item without any partials // Amount of completions that could give you the exact average of each item without any partials
const sumMineWeights = (Object.values(mineLootTable) as number[]).reduce((a, b) => a + b); const sumMineWeights = computed(() =>
const resourceNames = Object.keys(mineLootTable) as Resources[]; (Object.keys(mineLootTable) as Resources[]).reduce(
(a, b) => a + new Decimal(dropRates[b].computedModifier.value).toNumber(),
0
)
);
const energyModifier = createSequentialModifier(() => [ const energyModifier = createSequentialModifier(() => [
...resourceNames.map(resource => ...resourceNames.map(resource =>
@ -578,11 +680,11 @@ export const main = createLayer("main", function (this: BaseLayer) {
createMultiplicativeModifier(() => ({ createMultiplicativeModifier(() => ({
multiplier: 2, multiplier: 2,
description: tools.stone.name, description: tools.stone.name,
enabled: () => "stone" in toolNodes.value enabled: () => toolNodes.value["stone"] != null
})), })),
createAdditiveModifier(() => ({ createAdditiveModifier(() => ({
addend: () => Decimal.pow10(poweredMachines.value).neg(), addend: () => Decimal.pow(100, poweredMachines.value).div(10).neg(),
description: "Powered Machines (10^n energy/s)", description: "Powered Machines (100^n/10 energy/s)",
enabled: () => Decimal.gt(poweredMachines.value, 0) enabled: () => Decimal.gt(poweredMachines.value, 0)
})) }))
]); ]);
@ -592,7 +694,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
createMultiplicativeModifier(() => ({ createMultiplicativeModifier(() => ({
multiplier: 2, multiplier: 2,
description: tools.dirt.name, description: tools.dirt.name,
enabled: () => "dirt" in toolNodes.value enabled: () => toolNodes.value["dirt"] != null
})) }))
]); ]);
const computedMiningSpeedModifier = computed(() => miningSpeedModifier.apply(1)); const computedMiningSpeedModifier = computed(() => miningSpeedModifier.apply(1));
@ -601,7 +703,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
createMultiplicativeModifier(() => ({ createMultiplicativeModifier(() => ({
multiplier: 2, multiplier: 2,
description: tools.gravel.name, description: tools.gravel.name,
enabled: () => "gravel" in toolNodes.value enabled: () => toolNodes.value["gravel"] != null
})) }))
]); ]);
const computedMaterialGainModifier = computed(() => materialGainModifier.apply(1)); const computedMaterialGainModifier = computed(() => materialGainModifier.apply(1));
@ -610,13 +712,34 @@ export const main = createLayer("main", function (this: BaseLayer) {
createAdditiveModifier(() => ({ createAdditiveModifier(() => ({
addend: 0.001, addend: 0.001,
description: tools.copper.name, description: tools.copper.name,
enabled: () => "copper" in toolNodes.value enabled: () => toolNodes.value["copper"] != null
})) }))
]); ]);
const computedmaterialLevelEffectModifier = computed(() => const computedmaterialLevelEffectModifier = computed(() =>
materialLevelEffectModifier.apply(1.01) materialLevelEffectModifier.apply(1.01)
); );
const dropRates = (Object.keys(mineLootTable) as Resources[]).reduce((acc, resource) => {
const modifier = createSequentialModifier(() => [
createMultiplicativeModifier(() => ({
multiplier: 2,
description: "Dowsing",
enabled: () =>
dowsing.value != null &&
isPowered(dowsing.value) &&
(dowsing.value.state as unknown as DowsingState).resources.includes(resource)
}))
]);
const computedModifier = computed(() => modifier.apply(mineLootTable[resource]));
const section = {
title: `${camelToTitle(resource)} Drop Rate`,
modifier,
base: mineLootTable[resource]
};
acc[resource] = { modifier, computedModifier, section };
return acc;
}, {} as Record<Resources, { modifier: WithRequired<Modifier, "invert" | "description">; computedModifier: ComputedRef<DecimalSource>; section: Section }>);
const [energyTab, energyTabCollapsed] = createCollapsibleModifierSections(() => [ const [energyTab, energyTabCollapsed] = createCollapsibleModifierSections(() => [
{ {
title: "Energy Gain", title: "Energy Gain",
@ -631,21 +754,24 @@ export const main = createLayer("main", function (this: BaseLayer) {
modifier: miningSpeedModifier, modifier: miningSpeedModifier,
base: 1, base: 1,
unit: "/s", unit: "/s",
visible: () => "dirt" in toolNodes.value visible: () => toolNodes.value["dirt"] != null
}, },
{ {
title: "Ore Dropped", title: "Ore Dropped",
modifier: materialGainModifier, modifier: materialGainModifier,
base: 1, base: 1,
visible: () => "gravel" in toolNodes.value visible: () => toolNodes.value["gravel"] != null
}, },
{ {
title: "Material Level Effect", title: "Material Level Effect",
modifier: materialLevelEffectModifier, modifier: materialLevelEffectModifier,
base: 1.01, base: 1.01,
visible: () => "copper" in toolNodes.value visible: () => toolNodes.value["copper"] != null
} }
]); ]);
const [resourcesTab, resourcesCollapsed] = createCollapsibleModifierSections(() =>
Object.values(dropRates).map(d => d.section)
);
const modifierTabs = createTabFamily({ const modifierTabs = createTabFamily({
general: () => ({ general: () => ({
display: "Energy", display: "Energy",
@ -663,6 +789,15 @@ export const main = createLayer("main", function (this: BaseLayer) {
visibility: () => Object.keys(toolNodes.value).length > 0, visibility: () => Object.keys(toolNodes.value).length > 0,
tab: miningTab, tab: miningTab,
miningTabCollapsed miningTabCollapsed
}),
resources: () => ({
display: "Resources",
glowColor(): string {
return modifierTabs.activeTab.value === this.tab ? "white" : "";
},
visibility: () => dowsing.value != null,
tab: resourcesTab,
resourcesCollapsed
}) })
}); });
const showModifiersModal = ref(false); const showModifiersModal = ref(false);
@ -697,27 +832,27 @@ export const main = createLayer("main", function (this: BaseLayer) {
...(mine.value.state as object), ...(mine.value.state as object),
progress: Decimal.sub(progress, completions) progress: Decimal.sub(progress, completions)
}; };
const allResourceCompletions = completions.div(sumMineWeights).floor(); const allResourceCompletions = completions.div(sumMineWeights.value).floor();
if (allResourceCompletions.gt(0)) { if (allResourceCompletions.gt(0)) {
resourceNames.forEach(resource => { resourceNames.forEach(resource => {
grantResource( grantResource(
resource as Resources, resource,
Decimal.times( Decimal.times(
mineLootTable[resource as Resources] as number, new Decimal(dropRates[resource].computedModifier.value).toNumber(),
allResourceCompletions allResourceCompletions
).times(computedMaterialGainModifier.value) ).times(computedMaterialGainModifier.value)
); );
resourceMinedCooldown[resource as Resources] = 0.3; resourceMinedCooldown[resource] = 0.3;
}); });
} }
const remainder = Decimal.sub(completions, allResourceCompletions).toNumber(); const remainder = Decimal.sub(completions, allResourceCompletions).toNumber();
for (let i = 0; i < remainder; i++) { for (let i = 0; i < remainder; i++) {
const random = Math.floor(Math.random() * sumMineWeights); const random = Math.floor(Math.random() * sumMineWeights.value);
let weight = 0; let weight = 0;
for (let i = 0; i < resourceNames.length; i++) { for (let i = 0; i < resourceNames.length; i++) {
const resource = resourceNames[i]; const resource = resourceNames[i];
weight += mineLootTable[resource]; weight += new Decimal(dropRates[resource].computedModifier.value).toNumber();
if (random <= weight) { if (random < weight) {
grantResource(resource, computedMaterialGainModifier.value); grantResource(resource, computedMaterialGainModifier.value);
resourceMinedCooldown[resource] = 0.3; resourceMinedCooldown[resource] = 0.3;
break; break;
@ -764,7 +899,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
return (board.selectedNode.value?.state as { powered: boolean }).powered return (board.selectedNode.value?.state as { powered: boolean }).powered
? Decimal.eq(poweredMachines.value, 1) ? Decimal.eq(poweredMachines.value, 1)
? 10 ? 10
: Decimal.pow10(poweredMachines.value).times(0.9) : Decimal.pow(100, poweredMachines.value).div(10).times(0.99)
: Decimal.neg(nextPowerCost.value); : Decimal.neg(nextPowerCost.value);
} }
return 0; return 0;