From 44844f0de7ca37e77f4a46b9066e237cc10550ca Mon Sep 17 00:00:00 2001 From: thepaperpilot Date: Sun, 30 Apr 2023 15:02:55 -0500 Subject: [PATCH] Implement influences --- src/data/planes.tsx | 179 ++++++++++++++++++++++++-------- src/data/projEntry.tsx | 226 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 339 insertions(+), 66 deletions(-) diff --git a/src/data/planes.tsx b/src/data/planes.tsx index 8583dae..300c28e 100644 --- a/src/data/planes.tsx +++ b/src/data/planes.tsx @@ -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(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[] = []; 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, - modifier: WithRequired, - cost: FormulaSource, - previewModifier: WithRequired = modifier - ) { + function prepareFeature({ + feature, + canClick, + modifier, + cost, + previewModifier, + showETA + }: { + feature: VueFeature; + canClick: Computable; + modifier: WithRequired; + cost: FormulaSource; + previewModifier?: WithRequired; + showETA?: Computable; + }) { canClick = convertComputable(canClick); + showETA = convertComputable(showETA); const isHovering = trackHover(feature); previews.push({ shouldShowPreview: computed( () => unref(canClick as ProcessedComputable) && 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) ? 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), + 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 @@ -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; diff --git a/src/data/projEntry.tsx b/src/data/projEntry.tsx index 5e62bda..40b5f6b 100644 --- a/src/data/projEntry.tsx +++ b/src/data/projEntry.tsx @@ -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> = computed(() => ({ + ...board.types.influence.nodes.value.reduce((acc, curr) => { + acc[(curr.state as unknown as InfluenceState).type] = curr; + return acc; + }, {} as Record) + })); + 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 {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++;