Implement repeatables and fix cost formula

This commit is contained in:
thepaperpilot 2023-04-30 11:42:13 -05:00
parent ea70d3cce2
commit 3092fb91a9
2 changed files with 218 additions and 118 deletions

View file

@ -1,13 +1,20 @@
import ModalVue from "components/Modal.vue";
import SpacerVue from "components/layout/Spacer.vue"; import SpacerVue from "components/layout/Spacer.vue";
import StickyVue from "components/layout/Sticky.vue"; import StickyVue from "components/layout/Sticky.vue";
import { GenericAchievement, createAchievement } from "features/achievements/achievement";
import { BoardNode } from "features/boards/board";
import { jsx } from "features/feature"; import { jsx } from "features/feature";
import { createRepeatable } from "features/repeatable";
import { createResource } from "features/resources/resource"; import { createResource } from "features/resources/resource";
import { addTooltip } from "features/tooltips/tooltip";
import { createUpgrade } from "features/upgrades/upgrade"; import { createUpgrade } from "features/upgrades/upgrade";
import Formula from "game/formulas/formulas"; import Formula, { unrefFormulaSource } from "game/formulas/formulas";
import { FormulaSource, GenericFormula } from "game/formulas/types";
import { BaseLayer, createLayer } from "game/layers"; import { BaseLayer, createLayer } from "game/layers";
import { import {
Modifier, Modifier,
createAdditiveModifier, createAdditiveModifier,
createExponentialModifier,
createMultiplicativeModifier, createMultiplicativeModifier,
createSequentialModifier createSequentialModifier
} from "game/modifiers"; } from "game/modifiers";
@ -17,17 +24,13 @@ import { adjectives, colors, uniqueNamesGenerator } from "unique-names-generator
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import { format } from "util/break_eternity"; import { format } from "util/break_eternity";
import { Direction, WithRequired, camelToTitle } from "util/common"; import { Direction, WithRequired, camelToTitle } from "util/common";
import { Computable, ProcessedComputable, convertComputable } from "util/computed";
import { VueFeature, render, renderRow, trackHover } from "util/vue"; import { VueFeature, render, renderRow, trackHover } from "util/vue";
import { ComputedRef, Ref, computed, ref, unref } from "vue"; import { ComputedRef, Ref, computed, ref, unref } from "vue";
import { createCollapsibleModifierSections, createFormulaPreview, estimateTime } from "./common"; import { createCollapsibleModifierSections, createFormulaPreview, estimateTime } from "./common";
import { main, Resources, resourceNames, mineLootTable, ResourceState } from "./projEntry"; import { main, mineLootTable, resourceNames } from "./projEntry";
import type { ResourceState, Resources, PortalState } from "./projEntry";
import { getColor, getName, sfc32 } from "./utils"; import { getColor, getName, sfc32 } from "./utils";
import ModalVue from "components/Modal.vue";
import { addTooltip } from "features/tooltips/tooltip";
import { GenericAchievement, createAchievement } from "features/achievements/achievement";
import { Computable, ProcessedComputable } from "util/computed";
import { BoardNode } from "features/boards/board";
import { createExponentialModifier } from "game/modifiers";
export type Treasure = GenericAchievement & { export type Treasure = GenericAchievement & {
update?: (diff: number) => void; update?: (diff: number) => void;
@ -56,35 +59,54 @@ export function createPlane(id: string, tier: Resources, seed: number) {
const previews: { const previews: {
shouldShowPreview: Ref<boolean>; shouldShowPreview: Ref<boolean>;
modifier: Modifier; modifier: Modifier;
cost: DecimalSource; cost: FormulaSource;
}[] = []; }[] = [];
function prepareFeature(
feature: VueFeature,
canClick: Computable<boolean>,
modifier: WithRequired<Modifier, "description" | "invert">,
cost: FormulaSource,
previewModifier: WithRequired<Modifier, "invert"> = modifier
) {
canClick = convertComputable(canClick);
const isHovering = trackHover(feature);
previews.push({
shouldShowPreview: computed(
() => unref(canClick as ProcessedComputable<boolean>) && isHovering.value
),
modifier: previewModifier,
cost
});
resourceModifiers.push(modifier);
const eta = estimateTime(resource, computedResourceGain, () =>
unrefFormulaSource(cost)
);
addTooltip(feature, {
display: eta,
direction: Direction.Down
});
}
const features: VueFeature[][] = []; const features: VueFeature[][] = [];
const t = ref<DecimalSource>(0); const n = ref(0);
let costFormula = Formula.variable(t); // Makes cost formula value reactive on n, so nextCost will update as appropriate
let costFormula = Formula.variable(n).times(0);
const cachedGain: Record<number, DecimalSource> = {};
let visibility: Computable<boolean> = true; let visibility: Computable<boolean> = true;
const nextCost = computed(() =>
Decimal.add(difficulty, random() - 0.5)
.pow_base(2)
.times(10)
.times(costFormula.evaluate())
);
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const featureWeights = { const featureWeights = {
upgrades: 16 upgrades: 16,
repeatables: i <= 1 ? 0 : 8
}; };
const sumFeatureWeights = Object.values(featureWeights).reduce((a, b) => a + b); const type = pickRandom(featureWeights, random);
const featureWeightsKeys = Object.keys(
featureWeights
) as (keyof typeof featureWeights)[];
let r = Math.floor(random() * sumFeatureWeights);
let weight = 0;
let type: keyof typeof featureWeights | null = null;
for (let i = 0; i < featureWeightsKeys.length; i++) {
const feature = featureWeightsKeys[i];
weight += featureWeights[feature];
if (r < weight) {
type = feature;
break;
}
}
if (type == null) {
continue; // Should not happen
}
switch (type) { switch (type) {
case "upgrades": case "upgrades":
const upgrades: VueFeature[] = []; const upgrades: VueFeature[] = [];
@ -94,44 +116,16 @@ export function createPlane(id: string, tier: Resources, seed: number) {
mult: i === 0 && j === 0 ? 0 : 1, mult: i === 0 && j === 0 ? 0 : 1,
pow: i === 0 ? 0 : 0.5 pow: i === 0 ? 0 : 0.5
}; };
const sumUpgradeTypeWeights = Object.values(upgradeTypeWeights).reduce( const upgradeType = pickRandom(upgradeTypeWeights, random);
(a, b) => a + b const cost = nextCost.value;
); const title = getRandomUpgrade(random);
const upgradeTypeWeightsKeys = Object.keys(
upgradeTypeWeights
) as (keyof typeof upgradeTypeWeights)[];
let weight = 0;
let upgradeType: keyof typeof upgradeTypeWeights | null = null;
r = random() * sumUpgradeTypeWeights;
for (let i = 0; i < upgradeTypeWeightsKeys.length; i++) {
const type = upgradeTypeWeightsKeys[i];
weight += upgradeTypeWeights[type];
if (r < weight) {
upgradeType = type;
break;
}
}
if (upgradeType == null) {
continue;
}
const cost = Decimal.times(difficulty, random() + 0.5)
.pow_base(2)
.times(10)
.times(costFormula.evaluate());
const title = camelToTitle(
uniqueNamesGenerator({
dictionaries: [colors, adjectives],
seed: random() * 4294967296,
separator: " "
}) + "ity"
);
let description = ""; let description = "";
let modifier: WithRequired<Modifier, "description" | "invert">; let modifier: WithRequired<Modifier, "description" | "invert">;
switch (upgradeType) { switch (upgradeType) {
case "add": { case "add": {
const addend = Decimal.add(cost, 10).pow(random() / 4 + 0.875); const addend = Decimal.add(cost, 10).pow(random() / 4 + 0.875);
description = `Gain ${format(addend)} ${resource.displayName}/s`; description = `Gain ${format(addend)} ${resource.displayName}/s`;
costFormula = costFormula.step(t.value, c => c.add(addend)); costFormula = costFormula.add(addend);
modifier = createAdditiveModifier(() => ({ modifier = createAdditiveModifier(() => ({
addend, addend,
description: title, description: title,
@ -141,13 +135,12 @@ export function createPlane(id: string, tier: Resources, seed: number) {
} }
case "mult": { case "mult": {
const multiplier = random() * 5 + 1; const multiplier = random() * 5 + 1;
description = `Multiply ${resource.displayName} gain by ${format( description = `Multiply previous ${
multiplier resource.displayName
)}.`; } gain by x${format(multiplier)}.`;
costFormula = costFormula.step(t.value, c => { costFormula = costFormula.add(
const beforeStep = Decimal.sub(t.value, c.evaluate()); Decimal.sub(multiplier, 1).times(cachedGain[n.value - 1])
return c.add(beforeStep).times(multiplier).sub(beforeStep); );
});
modifier = createMultiplicativeModifier(() => ({ modifier = createMultiplicativeModifier(() => ({
multiplier, multiplier,
description: title, description: title,
@ -156,14 +149,13 @@ export function createPlane(id: string, tier: Resources, seed: number) {
break; break;
} }
case "pow": { case "pow": {
const exponent = random() / 5 + 1; const exponent = random() / 5 + 1.1;
description = `Raise ${resource.displayName} gain to the ^${format( description = `Raise previous ${
exponent resource.displayName
)}`; } gain to the ^${format(exponent)}`;
costFormula = costFormula.step(t.value, c => { costFormula = costFormula
const beforeStep = Decimal.sub(t.value, c.evaluate()); .add(Decimal.pow(cachedGain[n.value - 1], exponent))
return c.add(beforeStep).pow(exponent).sub(beforeStep); .sub(cachedGain[n.value - 1]);
});
modifier = createExponentialModifier(() => ({ modifier = createExponentialModifier(() => ({
exponent, exponent,
description: title, description: title,
@ -171,9 +163,8 @@ export function createPlane(id: string, tier: Resources, seed: number) {
})); }));
} }
} }
t.value = Decimal.times(difficulty, random() + 0.5) cachedGain[n.value] = costFormula.evaluate();
.pow_base(2) n.value++;
.add(t.value);
const upgradeVisibility = visibility; const upgradeVisibility = visibility;
const upgrade = createUpgrade(() => ({ const upgrade = createUpgrade(() => ({
requirements: createCostRequirement(() => ({ requirements: createCostRequirement(() => ({
@ -186,27 +177,119 @@ export function createPlane(id: string, tier: Resources, seed: number) {
}, },
visibility: upgradeVisibility visibility: upgradeVisibility
})); }));
const isHovering = trackHover(upgrade); prepareFeature(
previews.push({ upgrade,
shouldShowPreview: computed( () => !upgrade.bought.value && upgrade.canPurchase.value,
() =>
!upgrade.bought.value &&
upgrade.canPurchase.value &&
isHovering.value
),
modifier, modifier,
cost cost
}); );
resourceModifiers.push(modifier);
const eta = estimateTime(resource, computedResourceGain, cost);
addTooltip(upgrade, {
display: () => (upgrade.bought.value ? "" : eta.value),
direction: Direction.Down
});
upgrades.push(upgrade); upgrades.push(upgrade);
} }
features.push(upgrades); features.push(upgrades);
break; break;
case "repeatables":
const repeatables: VueFeature[] = [];
for (let j = 0; j < 3; j++) {
const repeatableTypeWeights = {
add: 1.5,
mult: 3
};
const upgradeType = pickRandom(repeatableTypeWeights, random);
// Repeatables will estimate 5 purchases between each increment of `n`
// This will become less accurate the further n gets from when the repeatable showed up, but at that time it should be having an increasingly smaller effect on the overall gain
const currentN = n.value;
const initialCost = nextCost.value;
const title = getRandomUpgrade(random);
let description = "";
let effect: ComputedRef<string>;
let modifier: WithRequired<Modifier, "description" | "invert">;
let previewModifier: WithRequired<Modifier, "invert">;
let cost: GenericFormula;
const costInput = Formula.variable(
computed(() => repeatable.amount.value)
).times(2);
switch (upgradeType) {
case "add": {
const addend = Decimal.add(initialCost, 10).times(random() + 0.5);
description = `Gain ${format(addend)} ${resource.displayName}/s`;
cost = costInput.add(1).times(initialCost);
costFormula = costFormula.add(
computed(() =>
Decimal.sub(n.value, currentN).add(1).times(5).times(addend)
)
);
effect = computed(
() =>
format(Decimal.times(addend, repeatable.amount.value)) +
"/s"
);
modifier = createAdditiveModifier(() => ({
addend: () => Decimal.times(addend, repeatable.amount.value),
description: title,
enabled: () => Decimal.gt(repeatable.amount.value, 0)
}));
previewModifier = createAdditiveModifier(() => ({ addend }));
break;
}
case "mult": {
const multiplier = random() + 1;
description = `Multiply previous ${
resource.displayName
} gain by x${format(multiplier)}.`;
cost = costInput.pow_base(multiplier).times(initialCost);
costFormula = costFormula.add(
computed(() =>
Decimal.sub(n.value, currentN)
.add(1)
.times(5)
.pow_base(multiplier)
.sub(1)
.times(cachedGain[currentN])
)
);
effect = computed(
() =>
"x" +
format(Decimal.pow(multiplier, repeatable.amount.value))
);
modifier = createMultiplicativeModifier(() => ({
multiplier: () =>
Decimal.pow(multiplier, repeatable.amount.value),
description: title,
enabled: () => Decimal.gt(repeatable.amount.value, 0)
}));
previewModifier = createMultiplicativeModifier(() => ({
multiplier
}));
break;
}
}
cachedGain[n.value] = costFormula.evaluate();
n.value++;
const repeatableVisibility = visibility;
const repeatable = createRepeatable(() => ({
requirements: createCostRequirement(() => ({
resource: noPersist(resource),
cost
})),
display: () => ({
title,
description,
effectDisplay: unref(effect)
}),
visibility: repeatableVisibility
}));
prepareFeature(
repeatable,
() => unref(repeatable.canClick),
modifier,
cost,
previewModifier
);
repeatables.push(repeatable);
}
features.push(repeatables);
break;
} }
const treasureWeights = { const treasureWeights = {
cache: 100, cache: 100,
@ -214,24 +297,7 @@ export function createPlane(id: string, tier: Resources, seed: number) {
resourceMulti: 5, resourceMulti: 5,
energyMulti: 5 energyMulti: 5
}; };
const sumTreasureWeights = Object.values(treasureWeights).reduce((a, b) => a + b); const treasureType = pickRandom(treasureWeights, random);
const treasureWeightsKeys = Object.keys(
treasureWeights
) as (keyof typeof treasureWeights)[];
r = Math.floor(random() * sumTreasureWeights);
weight = 0;
let treasureType: keyof typeof treasureWeights | null = null;
for (let i = 0; i < treasureWeightsKeys.length; i++) {
const type = treasureWeightsKeys[i];
weight += treasureWeights[type];
if (r < weight) {
treasureType = type;
break;
}
}
if (treasureType == null) {
continue; // Should not happen
}
let description = ""; let description = "";
let update: (diff: number) => void; let update: (diff: number) => void;
let onComplete: VoidFunction; let onComplete: VoidFunction;
@ -275,11 +341,8 @@ export function createPlane(id: string, tier: Resources, seed: number) {
description = `Gain ${format(resourceMulti)}x energy while plane is active.`; description = `Gain ${format(resourceMulti)}x energy while plane is active.`;
break; break;
} }
const cost = Decimal.times(difficulty, random() + 0.5)
.pow_base(2)
.times(10)
.times(costFormula.evaluate());
const milestoneVisibility = visibility; const milestoneVisibility = visibility;
const cost = nextCost.value;
const milestone = createAchievement(() => ({ const milestone = createAchievement(() => ({
requirements: createCostRequirement(() => ({ requirements: createCostRequirement(() => ({
resource: noPersist(resource), resource: noPersist(resource),
@ -330,6 +393,12 @@ export function createPlane(id: string, tier: Resources, seed: number) {
)); ));
this.on("preUpdate", diff => { this.on("preUpdate", diff => {
if (
!main.activePortals.value.some(n => (n.state as unknown as PortalState).id === id)
) {
return;
}
resource.value = Decimal.times(computedResourceGain.value, diff).add(resource.value); resource.value = Decimal.times(computedResourceGain.value, diff).add(resource.value);
earnedTreasures.value.forEach(treasure => { earnedTreasures.value.forEach(treasure => {
@ -340,7 +409,7 @@ export function createPlane(id: string, tier: Resources, seed: number) {
const resourceChange = computed(() => { const resourceChange = computed(() => {
const preview = previews.find(p => p.shouldShowPreview.value); const preview = previews.find(p => p.shouldShowPreview.value);
if (preview) { if (preview) {
return Decimal.neg(preview.cost); return Decimal.neg(unrefFormulaSource(preview.cost));
} }
return 0; return 0;
}); });
@ -527,4 +596,34 @@ function getRandomResource(random: () => number) {
return resource!; return resource!;
} }
function pickRandom<T extends string>(items: Record<T, number>, random: () => number) {
const sumWeights = (Object.values(items) as number[]).reduce((a, b) => a + b);
const keys = Object.keys(items) as T[];
let weight = 0;
let result: T | null = null;
const r = random() * sumWeights;
for (let i = 0; i < keys.length; i++) {
const type = keys[i];
weight += items[type];
if (r < weight) {
result = type;
break;
}
}
if (result == null) {
throw new Error("Failed to pick random. This should not happen");
}
return result;
}
function getRandomUpgrade(random: () => number) {
return camelToTitle(
uniqueNamesGenerator({
dictionaries: [colors, adjectives],
seed: random() * 4294967296,
separator: " "
}) + "ity"
);
}
export type GenericPlane = ReturnType<typeof createPlane>; export type GenericPlane = ReturnType<typeof createPlane>;

View file

@ -1598,6 +1598,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
resourceNodes, resourceNodes,
toolNodes, toolNodes,
grantResource, grantResource,
activePortals,
display: jsx(() => ( display: jsx(() => (
<> <>
<StickyVue class="nav-container"> <StickyVue class="nav-container">