From 52f75c74c095b08487d7099b524dc3c33fc5f01e Mon Sep 17 00:00:00 2001 From: "azykov@mail.ru" Date: Sat, 14 Feb 2026 13:37:17 +0300 Subject: [PATCH] full generation and upgrade rules revamp --- engine/src/game/Game.ts | 7 +- engine/src/game/ResourceGenerator.ts | 103 ++++++++++++---------- engine/src/game/ResourceSet.ts | 54 ++++++++---- engine/src/rules/GameRules.ts | 2 + engine/src/rules/ResourceGeneratorRule.ts | 16 ++-- engine/src/rulesets/rules.ts | 50 ++++++++--- engine/src/rulesets/utils.ts | 66 ++++++++++---- engine/src/types/Resource.ts | 6 +- simulator/src/Simulator.ts | 1 + web/src/App.scss | 42 --------- web/src/App.tsx | 16 +--- web/src/Page.tsx | 32 +++---- web/src/ProgressView.tsx | 10 +-- web/src/ResourceGeneratorView.tsx | 52 +++++++++++ web/src/ResourcesView.tsx | 22 ++--- web/src/index.scss | 85 ++++++++++++++++-- web/src/state/Root.ts | 7 ++ web/src/tools/format.ts | 4 +- web/src/tools/time.ts | 3 + 19 files changed, 374 insertions(+), 204 deletions(-) delete mode 100644 web/src/App.scss create mode 100644 web/src/ResourceGeneratorView.tsx create mode 100644 web/src/tools/time.ts diff --git a/engine/src/game/Game.ts b/engine/src/game/Game.ts index cae3fdd..56e8604 100644 --- a/engine/src/game/Game.ts +++ b/engine/src/game/Game.ts @@ -15,10 +15,15 @@ export class Game { ) { this.rules = rules; + this.initResources(); this.initGenerators(); } - private initGenerators(): void { + public initResources(): void { + this.resources.setMany(ResourceSet.from(this.rules.startResources)); + } + + public initGenerators(): void { this.generators.splice(0); this.generators.push(...this.rules.generators.map((rule) => new ResourceGenerator(this, rule))); } diff --git a/engine/src/game/ResourceGenerator.ts b/engine/src/game/ResourceGenerator.ts index 02333ee..2495aa9 100644 --- a/engine/src/game/ResourceGenerator.ts +++ b/engine/src/game/ResourceGenerator.ts @@ -5,8 +5,8 @@ import { ResourceSet } from "./ResourceSet"; export class ResourceGenerator { - private _level: number; - private _progress: number; + private _level!: number; + private _progress!: number; public readonly game: Game; public readonly rule: ResourceGeneratorRule; @@ -21,80 +21,91 @@ export class ResourceGenerator { ) { this.game = game; this.rule = rule; - this._level = this.rule.startingLevel; - this._progress = 0; + this.reset(); } public tick(time: number): void { - if (this.period === 0) { + const period = this.getPeriod(); + + if (period === 0) { this._progress = 0; return; } - if (this.rule.manualProgressReset) { - this._progress = Math.min(this._progress + time, this.period); - } + let progress = this._progress + time; + + if (this.rule.manualGeneration) + this._progress = Math.min(progress, period); else { - this._progress = (this._progress + time) % this.period; - this.game.resources.add(this.generation.times(time / this.period)); + this._progress = progress % period; + const times = Math.floor(progress / period); + if (times > 0) + this.generate(times); } } - public upgrade(): boolean { - if (this.rule.manualProgressReset && this._progress < this.period) { + public reset(): void { + this._level = this.rule.startingLevel; + this._progress = 0; + } + + public manualGenerate(): boolean { + if (this.rule.manualGeneration && !this.isFull) return false; - } - if (this.isOpen && this.canAffordUpgrade) { - console.log('ok to upgrade'); - this.game.resources.remove(this.upgradePrice); - this.increaseLevel(); + this._progress = 0; + this.generate(1); + return true; + } - if (this.rule.manualProgressReset) - this.game.resources.add(this.generation); + private generate(times: number): void { + const gain = this.getGain(); + this.rule.onGenerate(this.level, this.game, gain.mul(times)); + } - if (this.rule.resetProgressOnUpgrade) - this._progress = 0; - return true; - } - - return false; + public upgrade(): void { + this.rule.onUpgrade?.(this.level, this.game); + this.increaseLevel(); } private increaseLevel() { this._level++; } - public get period(): number { - return this.rule.period(this.level); + public get isFull(): boolean { + const period = this.getPeriod(); + return period === 0 || this._progress === period; } - public get generation(): ResourceSet { - return ResourceSet.from(this.rule.generation(this.level)); - } - - public get visibilityPrice(): ResourceSet { - return ResourceSet.from(this.rule.visibilityPrice(this.level)); - } - - public get openPrice(): ResourceSet { - return ResourceSet.from(this.rule.openPrice(this.level)); - } - - public get upgradePrice(): ResourceSet { - return ResourceSet.from(this.rule.upgradePrice(this.level)); + public getPeriod(level?: number): number { + return this.rule.period(level ?? this.level, this.game); } public get isVisible(): boolean { - return this.game.resources.gte(this.visibilityPrice); + return this.rule.isVisible(this.level, this.game); } - public get isOpen(): boolean { - return this.game.resources.gte(this.openPrice); + public getUpgradePrice(level?: number): ResourceSet { + return this.rule.upgradePrice(level ?? this.level, this.game); } - public get canAffordUpgrade(): boolean { - return this.game.resources.gte(this.upgradePrice); + public upgradableToLevel(limit: number = 100000): number { + + for (let idx = 0; idx < limit; idx++) { + const level = this.level + idx; + if (!this.rule.isUpgradable(level, this.game, this.getUpgradePrice(level))) + return level; + } + + return -1; + } + + public get isUpgradable(): boolean { + return this.rule.isUpgradable(this.level, this.game, this.getUpgradePrice()); + } + + public getGain(level?: number): ResourceSet { + return this.rule.generationGain(level ?? this.level, this.game); } public snapshot(): ResourceGeneratorSnapshot { diff --git a/engine/src/game/ResourceSet.ts b/engine/src/game/ResourceSet.ts index 9fcfa19..591e707 100644 --- a/engine/src/game/ResourceSet.ts +++ b/engine/src/game/ResourceSet.ts @@ -1,57 +1,81 @@ import Decimal from "break_eternity.js"; -import { ResourceNames, type PartialResources, type Resource } from "../types"; +import { type PartialResources } from "../types"; export class ResourceSet { - private items = new Map(); + private items = new Map(); + + public static get zero(): ResourceSet { + return new ResourceSet(); + } public static from(source: PartialResources): ResourceSet { const inst = new ResourceSet(); - inst.items = new Map(Object.entries(source) as [Resource, Decimal][]); + inst.items = new Map(Object.entries(source) as [string, Decimal][]); return inst; } + public clear(): void { + this.items.clear(); + } + public clone(): ResourceSet { return ResourceSet.from(this.toObject()); } - public get(key: Resource): Decimal { + public keys(mergeWith?: ResourceSet): string[] { + const keys = Array.from(this.items.keys()); + const mergeWithKeys = mergeWith ? mergeWith.keys() : []; + + return [...keys, ...mergeWithKeys.filter((mk) => !keys.includes(mk))]; + } + + public get(key: string): Decimal { return this.items.get(key) ?? Decimal.dZero; } - public set(key: Resource, value: Decimal): void { + public set(key: string, value: Decimal): void { this.items.set(key, value); } - public predicate(predicate: (res: Resource) => boolean): boolean { - return ResourceNames.every((res) => predicate(res)); + public setMany(values: ResourceSet): void { + this.items.clear(); + [...values.items.entries()].forEach(([k, v]) => this.items.set(k, v)); + } + + public get isZero(): boolean { + return this.predicate(this.keys(), (res) => this.get(res).eq(0)); + } + + public predicate(keys: string[], predicate: (res: string) => boolean): boolean { + return keys.every((res) => predicate(res)); } public gte(arg: ResourceSet): boolean { - return this.predicate((res) => this.get(res).gte(arg.get(res))); + return this.predicate(this.keys(arg), (res) => this.get(res).gte(arg.get(res))); } - public static transform(target: ResourceSet, predicate: (res: Resource) => Decimal): void { - for (const res of ResourceNames) + public static transform(keys: string[], target: ResourceSet, predicate: (res: string) => Decimal): void { + for (const res of keys) target.set(res, predicate(res)); } public sum(arg: ResourceSet): ResourceSet { const inst = new ResourceSet(); - ResourceSet.transform(inst, (res) => this.get(res).plus(arg.get(res))); + ResourceSet.transform(this.keys(arg), inst, (res) => this.get(res).plus(arg.get(res))); return inst; } public add(arg: ResourceSet): void { - ResourceSet.transform(this, (res) => this.get(res).plus(arg.get(res))); + ResourceSet.transform(this.keys(arg), this, (res) => this.get(res).plus(arg.get(res))); } public remove(arg: ResourceSet): void { - ResourceSet.transform(this, (res) => this.get(res).minus(arg.get(res))); + ResourceSet.transform(this.keys(arg), this, (res) => this.get(res).minus(arg.get(res))); } - public times(factor: number): ResourceSet { + public mul(factor: number): ResourceSet { const inst = new ResourceSet(); - ResourceSet.transform(inst, (res) => this.get(res).multiply(factor)); + ResourceSet.transform(this.keys(), inst, (res) => this.get(res).multiply(factor)); return inst; } diff --git a/engine/src/rules/GameRules.ts b/engine/src/rules/GameRules.ts index f9dd187..2feb458 100644 --- a/engine/src/rules/GameRules.ts +++ b/engine/src/rules/GameRules.ts @@ -1,5 +1,7 @@ +import type { Resources } from "../types"; import type { ResourceGeneratorRule } from "./ResourceGeneratorRule"; export type GameRules = { + startResources: Resources, generators: ResourceGeneratorRule[], } diff --git a/engine/src/rules/ResourceGeneratorRule.ts b/engine/src/rules/ResourceGeneratorRule.ts index da8cc42..478a543 100644 --- a/engine/src/rules/ResourceGeneratorRule.ts +++ b/engine/src/rules/ResourceGeneratorRule.ts @@ -1,13 +1,15 @@ -import type { PartialResources } from "../types"; +import type { Game, ResourceSet } from "../game"; export type ResourceGeneratorRule = { name: string, startingLevel: number, // usually zero - period: (level: number) => number, - generation: (level: number) => PartialResources, // per period - visibilityPrice: (level: number) => PartialResources; - openPrice: (level: number) => PartialResources; - upgradePrice: (level: number) => PartialResources; + period: (level: number, game: Game) => number, + isVisible: (level: number, game: Game) => boolean; + upgradePrice: (level: number, game: Game) => ResourceSet; + isUpgradable: (level: number, game: Game, upgradePrice: ResourceSet) => boolean; + generationGain: (level: number, game: Game) => ResourceSet, + onGenerate: (level: number, game: Game, gain: ResourceSet) => void, + onUpgrade?: (level: number, game: Game) => void, resetProgressOnUpgrade: boolean; - manualProgressReset: boolean; + manualGeneration: boolean; }; diff --git a/engine/src/rulesets/rules.ts b/engine/src/rulesets/rules.ts index 975bddb..943bfd5 100644 --- a/engine/src/rulesets/rules.ts +++ b/engine/src/rulesets/rules.ts @@ -1,33 +1,63 @@ +import Decimal from "break_eternity.js"; import type { GameRules } from "../rules/GameRules"; import { simpleResGen, resourceGen } from './utils'; +import { ResourceSet } from "../game"; export const rules: GameRules = { + startResources: { prestigeMul: new Decimal(1) }, generators: [ { name: 'white', startingLevel: 1, resetProgressOnUpgrade: true, - manualProgressReset: true, - period: (level) => level ? 0.5 : 0, - generation: resourceGen({ RED: { offset: 14, power: 2, factor: 1 } }), - visibilityPrice: () => ({}), - openPrice: () => ({}), - upgradePrice: () => ({}), + manualGeneration: true, + period: (level) => level ? 3 : 0, + isVisible: () => true, + upgradePrice: () => ResourceSet.zero, + isUpgradable: () => false, + generationGain: (level, game) => resourceGen({ RED: { offset: 14, power: 2, factor: 1 } }, level), + onGenerate: (level, game, gain) => game.resources.add(gain), }, simpleResGen({ name: 'red', startingLevel: 1, period: 1, - generation: { RED: { offset: 3, power: 1.5, factor: 1 } }, - openPrice: { RED: { offset: 3, power: 2.1, factor: 10 } } + upgradePrice: { RED: { offset: 3, power: 2.1, factor: 10 } }, + generatesResources: { RED: { offset: 3, power: 1.5, factor: 1 } }, }), ...['orange', 'yellow', 'green', 'cyan', 'blue', 'violet'] .map((color, idx) => simpleResGen({ name: color, period: idx + 2, - generation: { RED: { power: 1.5, factor: (idx + 2) ** 2 } }, + generatesResources: { RED: { power: 1.5, factor: (idx + 2) ** 2 } }, // visibilityPrice: { RED: { offset: 5 * 10 ** (idx + 1) } }, - openPrice: { RED: { offset: 10 ** (idx + 2), power: 2.1, factor: 10 * 5 ** (idx + 1) } } + upgradePrice: { RED: { offset: 10 ** (idx + 2), power: 2.1, factor: 10 * 5 ** (idx + 1) } } })), + // { + // name: 'prestige', + // startingLevel: 0, + // period: () => 0, + // // generation: resourceGen({ prestigeMul: { offset: 0, power: 0.2, factor: 1 } }), + // openPrice: (level) => ({ RED: new Decimal(50) }), + // upgradePrice: () => ({}), + // visibilityPrice: () => ({}), + // manualProgressReset: true, + // resetProgressOnUpgrade: true, + // generation: (level, game, dryRun) => { + // if (!dryRun) { + // console.log('prestige ' + level); + // game.generators.filter((gen) => gen.rule.name !== 'prestige').forEach((gen) => gen.reset()); + // ResourceSet.transform( + // game.resources.keys().filter((res) => res != 'prestigeMul'), + // game.resources, + // () => Decimal.dZero, + // ); + // } + + // return resourceGen({ prestigeMul: { offset: 0, power: 1, factor: 1 } })(level, game); + + // // game.resources.clear(); + // } + // } ], } \ No newline at end of file diff --git a/engine/src/rulesets/utils.ts b/engine/src/rulesets/utils.ts index 3b512c4..e78b6eb 100644 --- a/engine/src/rulesets/utils.ts +++ b/engine/src/rulesets/utils.ts @@ -1,6 +1,7 @@ import Decimal from "break_eternity.js"; -import type { PartialResources, Resource, Resources } from "../types"; +import type { PartialResources, Resources } from "../types"; import type { ResourceGeneratorRule } from "../rules/ResourceGeneratorRule"; +import { ResourceSet, type Game } from "../game"; type ResourceGenParams = { offset?: number, @@ -12,23 +13,49 @@ type SimpleResGenParams = { name: string, startingLevel?: number, period: number, - generation: Partial>, - visibilityPrice?: Partial>, - openPrice: Partial>, - upgradePrice?: Partial>, + visibilityPrice?: Partial>, + // openPrice: Partial>, + upgradePrice: Partial>, + generatesResources: Partial>, } -export function resourceGen(p?: Partial>): (lvl: number) => PartialResources { - if (!p) - return () => ({}); +// export function resourceGen(p?: Partial>, multiplyByResource?: string): (lvl: number, game: Game) => PartialResources { +// if (!p) +// return () => ({}); - return (lvl) => Object.fromEntries( +// return (lvl, game) => Object.fromEntries( +// Object.entries(p) +// .map(([res, rp]) => { +// const value = rp +// ? new Decimal((rp.offset ?? 0) + Math.pow(lvl, rp.power ?? 1) * (rp.factor ?? 0)) +// : Decimal.dZero; +// return [ +// res, +// multiplyByResource +// ? value.mul(game.resources.get(multiplyByResource)) +// : value, +// ]; +// }) +// ) as Resources; +// } + +export function resourceGen(p: Partial> | undefined, level: number): ResourceSet { + + if (!p) + return ResourceSet.zero; + + return ResourceSet.from(Object.fromEntries( Object.entries(p) - .map(([res, rp]) => [ - res, - new Decimal((rp.offset ?? 0) + Math.pow(lvl, rp.power ?? 1) * (rp.factor ?? 0)), - ]) - ) as Resources; + .map(([res, rp]) => { + const value = rp + ? new Decimal((rp.offset ?? 0) + Math.pow(level, rp.power ?? 1) * (rp.factor ?? 0)) + : Decimal.dZero; + return [ + res, + value, + ]; + }) + )); } export function simpleResGen(params: SimpleResGenParams): ResourceGeneratorRule { @@ -37,11 +64,12 @@ export function simpleResGen(params: SimpleResGenParams): ResourceGeneratorRule name: params.name, startingLevel: params.startingLevel ?? 0, resetProgressOnUpgrade: true, - manualProgressReset: false, + manualGeneration: false, period: (level) => level ? params.period : 0, - generation: resourceGen(params.generation), - visibilityPrice: resourceGen(params.visibilityPrice), - openPrice: resourceGen(params.openPrice), - upgradePrice: resourceGen(params.upgradePrice), + isVisible: (level, game) => game.resources.gte(resourceGen(params.visibilityPrice, level)), + upgradePrice: (level, game) => resourceGen(params.upgradePrice, level), + isUpgradable: (level, game, upgradePrice) => game.resources.gte(upgradePrice), + generationGain: (level, game) => resourceGen(params.generatesResources, level), + onGenerate: (level, game, gain) => game.resources.add(gain), } } \ No newline at end of file diff --git a/engine/src/types/Resource.ts b/engine/src/types/Resource.ts index cdf0f38..db948e2 100644 --- a/engine/src/types/Resource.ts +++ b/engine/src/types/Resource.ts @@ -1,7 +1,7 @@ import Decimal from "break_eternity.js"; -export const ResourceNames = ['RED'] as const; -export type Resource = (typeof ResourceNames)[number]; +// export const ResourceNames = ['RED'] as const; +// export type Resource = (typeof ResourceNames)[number]; -export type Resources = Record; +export type Resources = Record; export type PartialResources = Partial; diff --git a/simulator/src/Simulator.ts b/simulator/src/Simulator.ts index 5f5661b..f4be6a4 100644 --- a/simulator/src/Simulator.ts +++ b/simulator/src/Simulator.ts @@ -24,6 +24,7 @@ export class Simulator { public simulate(duration: number) { const stats: SimulationStats = { actions: [], + finalTime: 0, finalSnapshot: { resources: {}, generators: [] }, }; diff --git a/web/src/App.scss b/web/src/App.scss deleted file mode 100644 index 21516d6..0000000 --- a/web/src/App.scss +++ /dev/null @@ -1,42 +0,0 @@ -// #root { -// max-width: 1280px; -// margin: 0 auto; -// padding: 2rem; -// text-align: center; -// } - -// .logo { -// height: 6em; -// padding: 1.5em; -// will-change: filter; -// transition: filter 300ms; -// } -// .logo:hover { -// filter: drop-shadow(0 0 2em #646cffaa); -// } -// .logo.react:hover { -// filter: drop-shadow(0 0 2em #61dafbaa); -// } - -// @keyframes logo-spin { -// from { -// transform: rotate(0deg); -// } -// to { -// transform: rotate(360deg); -// } -// } - -// @media (prefers-reduced-motion: no-preference) { -// a:nth-of-type(2) .logo { -// animation: logo-spin infinite 20s linear; -// } -// } - -// .card { -// padding: 2em; -// } - -// .read-the-docs { -// color: #888; -// } diff --git a/web/src/App.tsx b/web/src/App.tsx index 562a737..4d6bf92 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,21 +3,16 @@ import { observer } from 'mobx-react-lite'; import { root } from './state/Root'; import { Page } from './Page'; -import './App.scss' import type { ChangeEvent } from 'react'; +import { now } from './tools/time'; -function now(): number { - return (performance.now() + performance.timeOrigin) / 1000; -} const gameLoop = function () { - let lastTime = now(); - function loop() { const nowTime = now(); - const deltaTime = nowTime - lastTime; - lastTime = now(); + const deltaTime = nowTime - root.lastGameTime; + root.setLastGameTime(now()); // console.log('loop tick ' + deltaTime); root.tick(deltaTime); @@ -33,10 +28,6 @@ gameLoop(); export const App = observer(function () { - function handleTickClick(): void { - root.tick(1 / 60); - } - function handleNumberNotationChange(event: ChangeEvent): void { root.setNumberNotationName(event.target.value); } @@ -47,6 +38,7 @@ export const App = observer(function () { return ( <> +
{root.lastGameTime}
Notation: