Implement influences

This commit is contained in:
thepaperpilot 2023-04-30 15:02:55 -05:00
parent 2a362a95c3
commit 44844f0de7
2 changed files with 339 additions and 66 deletions

View file

@ -2,7 +2,7 @@ import ModalVue from "components/Modal.vue";
import SpacerVue from "components/layout/Spacer.vue";
import StickyVue from "components/layout/Sticky.vue";
import { GenericAchievement, createAchievement } from "features/achievements/achievement";
import { BoardNode } from "features/boards/board";
import { BoardNode, getUniqueNodeID } from "features/boards/board";
import { jsx } from "features/feature";
import { createRepeatable } from "features/repeatable";
import { createResource } from "features/resources/resource";
@ -18,7 +18,7 @@ import {
createMultiplicativeModifier,
createSequentialModifier
} from "game/modifiers";
import { noPersist, persistent } from "game/persistence";
import { State, noPersist, persistent } from "game/persistence";
import { createCostRequirement } from "game/requirements";
import { adjectives, colors, uniqueNamesGenerator } from "unique-names-generator";
import Decimal, { DecimalSource } from "util/bignum";
@ -28,7 +28,14 @@ import { Computable, ProcessedComputable, convertComputable } from "util/compute
import { VueFeature, render, renderRow, trackHover } from "util/vue";
import { ComputedRef, Ref, computed, ref, unref } from "vue";
import { createCollapsibleModifierSections, createFormulaPreview, estimateTime } from "./common";
import { main, mineLootTable, resourceNames } from "./projEntry";
import {
InfluenceState,
Influences,
influences as influenceTypes,
main,
mineLootTable,
resourceNames
} from "./projEntry";
import type { ResourceState, Resources, PortalState } from "./projEntry";
import { getColor, getName, sfc32 } from "./utils";
@ -39,7 +46,12 @@ export type Treasure = GenericAchievement & {
resourceMulti: DecimalSource;
};
export function createPlane(id: string, tier: Resources, seed: number) {
export function createPlane(
id: string,
tier: Resources,
seed: number,
influences: InfluenceState[]
) {
return createLayer(id, function (this: BaseLayer) {
const random = sfc32(0, seed >> 0, seed >> 32, 1);
for (let i = 0; i < 12; i++) random();
@ -49,8 +61,21 @@ export function createPlane(id: string, tier: Resources, seed: number) {
const background = getColor([0.18, 0.2, 0.25], random);
const resource = createResource<DecimalSource>(0, getName(random));
const tierIndex = resourceNames.indexOf(tier);
let difficultyRand = random();
if (influences.some(i => i.type === "increaseDiff")) {
difficultyRand = difficultyRand / 2 + 0.5;
}
if (influences.some(i => i.type === "decreaseDiff")) {
difficultyRand = difficultyRand / 2;
}
const difficulty = random() + tierIndex + 1;
const length = Math.ceil(random() * (tierIndex + 2));
const rewardsLevel = influences.some(i => i.type === "increaseRewards")
? difficulty + 1
: difficulty;
let length = Math.ceil(random() * (tierIndex + 2));
if (influences.some(i => i.type === "increaseLength")) {
length++;
}
const resourceModifiers: WithRequired<Modifier, "description" | "invert">[] = [];
const resourceGainModifier = createSequentialModifier(() => resourceModifiers);
@ -62,21 +87,30 @@ export function createPlane(id: string, tier: Resources, seed: number) {
cost: FormulaSource;
}[] = [];
function prepareFeature(
feature: VueFeature,
canClick: Computable<boolean>,
modifier: WithRequired<Modifier, "description" | "invert">,
cost: FormulaSource,
previewModifier: WithRequired<Modifier, "invert"> = modifier
) {
function prepareFeature({
feature,
canClick,
modifier,
cost,
previewModifier,
showETA
}: {
feature: VueFeature;
canClick: Computable<boolean>;
modifier: WithRequired<Modifier, "description" | "invert">;
cost: FormulaSource;
previewModifier?: WithRequired<Modifier, "invert">;
showETA?: Computable<boolean | undefined>;
}) {
canClick = convertComputable(canClick);
showETA = convertComputable(showETA);
const isHovering = trackHover(feature);
previews.push({
shouldShowPreview: computed(
() => unref(canClick as ProcessedComputable<boolean>) && isHovering.value
),
modifier: previewModifier,
modifier: previewModifier ?? modifier,
cost
});
resourceModifiers.push(modifier);
@ -84,7 +118,10 @@ export function createPlane(id: string, tier: Resources, seed: number) {
unrefFormulaSource(cost)
);
addTooltip(feature, {
display: eta,
display:
showETA == null
? eta
: () => (unref(showETA as ProcessedComputable<boolean>) ? eta.value : ""),
direction: Direction.Down
});
}
@ -101,10 +138,15 @@ export function createPlane(id: string, tier: Resources, seed: number) {
.times(10)
.times(costFormula.evaluate())
);
const influenceTreasures: Influences[] = [];
for (let i = 0; i < length; i++) {
const featureWeights = {
upgrades: 16,
repeatables: i <= 1 ? 0 : 8
upgrades: 32,
repeatables: i <= 1 ? 0 : 16
// conversion: i <= 3 ? 0 : 8,
// xp: i <= 5 ? 0 : 4,
// dimensions: i <= 7 ? 0 : 2,
// prestige: i <= 7 && i < length - 1 ? 0 : 1
};
const type = pickRandom(featureWeights, random);
switch (type) {
@ -177,12 +219,13 @@ export function createPlane(id: string, tier: Resources, seed: number) {
},
visibility: upgradeVisibility
}));
prepareFeature(
upgrade,
() => !upgrade.bought.value && upgrade.canPurchase.value,
prepareFeature({
feature: upgrade,
canClick: () => upgrade.canPurchase.value,
modifier,
cost
);
cost,
showETA: () => !upgrade.bought.value
});
upgrades.push(upgrade);
}
features.push(upgrades);
@ -193,6 +236,7 @@ export function createPlane(id: string, tier: Resources, seed: number) {
const repeatableTypeWeights = {
add: 1.5,
mult: 3
// pow was too hard to implement such that the cost would be invertible
};
const upgradeType = pickRandom(repeatableTypeWeights, random);
// Repeatables will estimate 5 purchases between each increment of `n`
@ -279,23 +323,30 @@ export function createPlane(id: string, tier: Resources, seed: number) {
}),
visibility: repeatableVisibility
}));
prepareFeature(
repeatable,
() => unref(repeatable.canClick),
prepareFeature({
feature: repeatable,
canClick: () => unref(repeatable.canClick),
modifier,
cost,
previewModifier
);
});
repeatables.push(repeatable);
}
features.push(repeatables);
break;
}
const treasureWeights = {
cache: 100,
generation: 10,
resourceMulti: 5,
energyMulti: 5
cache: influences.some(i => i.type === "increaseCaches") ? 10 : 1,
generation: influences.some(i => i.type === "increaseGens") ? 10 : 1,
resourceMulti: influences.some(i => i.type === "increaseResourceMults") ? 10 : 1,
energyMulti: influences.some(i => i.type === "increaseEnergyMults") ? 2.5 : 0.25,
influences:
Object.keys(main.influenceNodes.value).length + influenceTreasures.length ===
Object.keys(influenceTypes).length
? 0
: influences.some(i => i.type === "increaseInfluences")
? 20
: 2
};
const treasureType = pickRandom(treasureWeights, random);
let description = "";
@ -307,8 +358,8 @@ export function createPlane(id: string, tier: Resources, seed: number) {
let resourceMulti: DecimalSource;
switch (treasureType) {
case "cache":
randomResource = getRandomResource(random);
description = `Gain ${format(difficulty)}x your current ${randomResource}.`;
randomResource = getRandomResource(random, influences);
description = `Gain ${format(rewardsLevel)}x your current ${randomResource}.`;
onComplete = () =>
main.grantResource(
randomResource,
@ -317,29 +368,61 @@ export function createPlane(id: string, tier: Resources, seed: number) {
main.resourceNodes.value[randomResource]
?.state as unknown as ResourceState | null
)?.amount ?? 0,
difficulty
rewardsLevel
)
);
break;
case "generation":
randomResource = getRandomResource(random);
const gain = Decimal.div(difficulty, 120).times(mineLootTable[randomResource]);
randomResource = getRandomResource(random, influences);
const gain = Decimal.div(rewardsLevel, 120).times(
mineLootTable[randomResource]
);
description = `Gain ${format(gain)} ${randomResource}/s while plane is active.`;
update = diff => main.grantResource(randomResource, Decimal.times(diff, gain));
link = computed(() => main.resourceNodes.value[randomResource]);
break;
case "resourceMulti":
effectedResource = randomResource = getRandomResource(random);
resourceMulti = Decimal.div(difficulty, 17).pow_base(2);
effectedResource = randomResource = getRandomResource(random, influences);
resourceMulti = Decimal.div(rewardsLevel, 17).pow_base(2);
description = `Gain ${format(
resourceMulti
)}x ${randomResource} while plane is active.`;
break;
case "energyMulti":
effectedResource = "energy";
resourceMulti = Decimal.div(difficulty, 17);
resourceMulti = Decimal.div(rewardsLevel, 17);
description = `Gain ${format(resourceMulti)}x energy while plane is active.`;
break;
case "influences":
const randomInfluence = pickRandom(
(Object.keys(influenceTypes) as Influences[]).reduce((acc, curr) => {
acc[curr] =
curr in main.influenceNodes.value ||
influenceTreasures.includes(curr)
? 0
: 1;
return acc;
}, {} as Record<Influences, number>),
random
);
influenceTreasures.push(randomInfluence);
description = `Gain a new portal influence`;
onComplete = () =>
main.board.placeInAvailableSpace({
id: getUniqueNodeID(main.board),
position: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
...main.board.types.portal.nodes.value.find(
n => (n.state as unknown as PortalState).id === id
)!.position
},
type: "influence",
state: {
type: randomInfluence,
data: influenceTypes[randomInfluence].initialData
}
});
break;
}
const milestoneVisibility = visibility;
const cost = nextCost.value;
@ -510,6 +593,7 @@ export function createPlane(id: string, tier: Resources, seed: number) {
return {
tier: persistent(tier),
seed: persistent(seed),
influences: persistent(influences as unknown as State[]),
name,
color,
resource,
@ -539,7 +623,7 @@ export function createPlane(id: string, tier: Resources, seed: number) {
style="display: inline"
onClick={() => (showModifiersModal.value = true)}
>
open modifiers
modifiers
</button>
</span>
</StickyVue>
@ -578,8 +662,23 @@ export function createPlane(id: string, tier: Resources, seed: number) {
}
// Using separate method from what's used in mining, because planes are influenced by influences and not things like dowsing
function getRandomResource(random: () => number) {
const sumResourceWeights = (Object.values(mineLootTable) as number[]).reduce((a, b) => a + b);
function getRandomResource(random: () => number, influences: InfluenceState[]) {
influences = influences.filter(
i => i.type === "increaseResources" || i.type === "decreaseResources"
);
const sumResourceWeights = (Object.keys(mineLootTable) as Resources[]).reduce((a, b) => {
let weight = mineLootTable[b];
influences
.filter(i => i.data === b)
.forEach(influence => {
if (influence.type === "increaseResources") {
weight *= 1000;
} else {
weight /= 1000;
}
});
return a + weight;
}, 0);
const resourceWeightsKeys = Object.keys(mineLootTable) as Resources[];
const r = Math.floor(random() * sumResourceWeights);
let weight = 0;

View file

@ -66,7 +66,7 @@ export interface EmpowererState {
export interface PortalGeneratorState {
tier: Resources | undefined;
influences: string[];
influences: Influences[];
}
export interface PortalState {
@ -74,6 +74,11 @@ export interface PortalState {
powered: boolean;
}
export interface InfluenceState {
type: Influences;
data: State;
}
export const mineLootTable = {
dirt: 120,
sand: 60,
@ -216,6 +221,95 @@ const passives = {
export type Passives = keyof typeof passives;
export const influences = {
increaseResources: {
description: (state: InfluenceState) => {
const resources = state.data as Resources[];
if (resources.length === 0) {
return "Increase resource odds - Drag a resource to me!";
}
if (resources.length === 1) {
return `Increase ${resources[0]}'s odds`;
}
return `Increase ${resources.length} resources' odds`;
},
cost: 2,
initialData: []
},
decreaseResources: {
description: (state: InfluenceState) => {
const resources = state.data as Resources[];
if (resources.length === 0) {
return "Decrease resource odds - Drag a resource to me!";
}
if (resources.length === 1) {
return `Decrease ${resources[0]}'s odds`;
}
return `Decrease ${resources.length} resources' odds`;
},
cost: 2,
initialData: []
},
increaseLength: {
description: "Increase length",
cost: 100,
initialData: undefined
},
increaseCaches: {
description: "Increase caches odds",
cost: 10,
initialData: undefined
},
increaseGens: {
description: "Increase generators odds",
cost: 10,
initialData: undefined
},
increaseInfluences: {
description: "Increase influences odds",
cost: 10,
initialData: undefined
},
increaseEnergyMults: {
description: "Increase energy mults odds",
cost: 10,
initialData: undefined
},
increaseResourceMults: {
description: "Increase resource mults odds",
cost: 10,
initialData: undefined
},
increaseDiff: {
description: "Increase difficulty/rewards odds",
cost: 10,
initialData: undefined
},
decreaseDiff: {
description: "Decrease difficulty/rewards odds",
cost: 10,
initialData: undefined
},
increaseRewards: {
description: "Increase rewards level",
cost: 1e4,
initialData: undefined
}
// relic: {
// description: "Max length/difficulty, add tier-unique relic",
// cost: 1e6,
// initialData: undefined
// }
} as const satisfies Record<
string,
{
description: string | ((state: InfluenceState) => string);
cost: DecimalSource;
initialData?: State;
}
>;
export type Influences = keyof typeof influences;
/**
* @hidden
*/
@ -242,6 +336,13 @@ export const main = createLayer("main", function (this: BaseLayer) {
iron: board.types.portalGenerator.nodes.value[0]
}));
const influenceNodes: ComputedRef<Record<Influences, BoardNode>> = computed(() => ({
...board.types.influence.nodes.value.reduce((acc, curr) => {
acc[(curr.state as unknown as InfluenceState).type] = curr;
return acc;
}, {} as Record<Influences, BoardNode>)
}));
const resourceLevels = computed(() =>
resourceNames.reduce((acc, curr) => {
const amount =
@ -336,11 +437,12 @@ export const main = createLayer("main", function (this: BaseLayer) {
id: "deselect",
icon: "close",
tooltip: (node: BoardNode) => ({
text:
"resources" in (node.state as object) ? "Disconnect resources" : "Disconnect tools"
text: "tools" in (node.state as object) ? "Disconnect tools" : "Disconnect resources"
}),
onClick(node: BoardNode) {
if ("resources" in (node.state as object)) {
if (Array.isArray(node.state)) {
node.state = [];
} else if ("resources" in (node.state as object)) {
node.state = { ...(node.state as object), resources: [] };
} else if ("tools" in (node.state as object)) {
node.state = { ...(node.state as object), tools: [] };
@ -349,6 +451,9 @@ export const main = createLayer("main", function (this: BaseLayer) {
board.selectedNode.value = null;
},
visibility: (node: BoardNode) => {
if (Array.isArray(node.state)) {
return node.state.length > 0;
}
if ("resources" in (node.state as object)) {
return (node.state as { resources: Resources[] }).resources.length > 0;
}
@ -886,11 +991,9 @@ export const main = createLayer("main", function (this: BaseLayer) {
}-tier portal`
};
}
if ((board as GenericBoard).draggingNode.value?.type === "resource") {
const resource = (
(board as GenericBoard).draggingNode.value
?.state as unknown as ResourceState
).type;
const draggingNode = (board as GenericBoard).draggingNode.value;
if (draggingNode?.type === "resource") {
const resource = (draggingNode.state as unknown as ResourceState).type;
const text =
(node.state as unknown as PortalGeneratorState).tier === resource
? "Disconnect"
@ -899,8 +1002,17 @@ export const main = createLayer("main", function (this: BaseLayer) {
text,
color: "var(--accent2)"
};
} else if (draggingNode?.type === "influence") {
const influence = (draggingNode.state as unknown as InfluenceState).type;
const { influences } = node.state as unknown as PortalGeneratorState;
if (influences.includes(influence)) {
return { text: "Disconnect", color: "var(--accent2)" };
}
return {
text: "Add influence",
color: "var(--accent2)"
};
}
// TODO handle influences
return null;
},
actionDistance: Math.PI / 4,
@ -937,12 +1049,18 @@ export const main = createLayer("main", function (this: BaseLayer) {
while (`portal-${id}` in layers) {
id++;
}
const { tier, influences } =
node.state as unknown as PortalGeneratorState;
addLayer(
createPlane(
`portal-${id}`,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(node.state as unknown as PortalGeneratorState).tier!,
Math.floor(Math.random() * 4294967296)
tier!,
Math.floor(Math.random() * 4294967296),
influences.map(
influence =>
influenceNodes.value[influence]
.state as unknown as InfluenceState
)
),
player
);
@ -974,7 +1092,8 @@ export const main = createLayer("main", function (this: BaseLayer) {
tier: droppedType === currentType ? undefined : droppedType
};
} else if (otherNode.type === "influence") {
const droppedInfluence = otherNode.state as string;
const droppedInfluence = (otherNode.state as unknown as InfluenceState)
.type;
const currentInfluences = (node.state as unknown as PortalGeneratorState)
.influences;
if (currentInfluences.includes(droppedInfluence)) {
@ -1022,6 +1141,42 @@ export const main = createLayer("main", function (this: BaseLayer) {
outlineColor: node =>
(layers[(node.state as unknown as PortalState).id] as GenericPlane).background,
draggable: true
},
influence: {
shape: Shape.Circle,
size: 50,
title: node => (node.state as unknown as InfluenceState).type,
label: node => {
if (node === board.selectedNode.value) {
const state = node.state as unknown as InfluenceState;
const desc = influences[state.type].description;
return { text: typeof desc === "function" ? desc(state) : desc };
}
return null;
},
actionDistance: Math.PI / 4,
actions: [deselectAllAction],
canAccept: (node, otherNode) => {
if (otherNode.type !== "resource") {
return false;
}
return Array.isArray(node.state);
},
onDrop: (node, otherNode) => {
if (otherNode.type !== "resource") {
return;
}
const resource = (otherNode.state as unknown as ResourceState).type;
const resources = node.state as Resources[];
if (resources.includes(resource)) {
node.state = resources.filter(r => r !== resource);
} else {
node.state = [...resources, resource];
}
board.selectedNode.value = node;
},
outlineColor: "var(--danger)",
draggable: true
}
},
style: {
@ -1098,21 +1253,25 @@ export const main = createLayer("main", function (this: BaseLayer) {
});
}
if (portalGenerator.value != null) {
if ((portalGenerator.value.state as unknown as PortalGeneratorState).tier != null) {
const state = portalGenerator.value.state as unknown as PortalGeneratorState;
if (state.tier != null) {
links.push({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
startNode: portalGenerator.value!,
endNode:
resourceNodes.value[
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(portalGenerator.value.state as unknown as PortalGeneratorState)
.tier!
],
startNode: portalGenerator.value,
endNode: resourceNodes.value[state.tier],
stroke: "var(--foreground)",
strokeWidth: 4
});
// TODO link to influences
}
state.influences.forEach(influence => {
console.log(influence);
links.push({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
startNode: portalGenerator.value!,
endNode: influenceNodes.value[influence],
stroke: "var(--foreground)",
strokeWidth: 4
});
});
(board as GenericBoard).types.portal.nodes.value.forEach(node => {
const plane = layers[(node.state as unknown as PortalState).id] as GenericPlane;
plane.links.value.forEach(n => {
@ -1142,6 +1301,19 @@ export const main = createLayer("main", function (this: BaseLayer) {
return links;
});
}
Object.values(influenceNodes.value).forEach(node => {
const state = node.state as unknown as InfluenceState;
if (state.type === "increaseResources" || state.type === "decreaseResources") {
(state.data as Resources[]).forEach(resource => {
links.push({
startNode: node,
endNode: resourceNodes.value[resource],
stroke: "var(--foreground)",
strokeWidth: 4
});
});
}
});
return links;
}
}));
@ -1597,6 +1769,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
passives,
resourceNodes,
toolNodes,
influenceNodes,
grantResource,
activePortals,
display: jsx(() => (
@ -1630,7 +1803,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
style="display: inline"
onClick={() => (showModifiersModal.value = true)}
>
open modifiers
modifiers
</button>
</span>
{player.devSpeed === 0 ? (
@ -1662,7 +1835,8 @@ export const getInitialLayers = (
createPlane(
`portal-${id}`,
layer.tier ?? "dirt",
layer.seed ?? Math.floor(Math.random() * 4294967296)
layer.seed ?? Math.floor(Math.random() * 4294967296),
(layer.influences ?? []) as unknown as InfluenceState[]
)
);
id++;