fix: runestone formula, prestige/transcendence rebalance, exploration fixes, and comprehensive balance audit #135

Merged
naomi merged 53 commits from fix/stones into main 2026-03-31 19:57:53 -07:00
3 changed files with 219 additions and 3 deletions
Showing only changes of commit 65c4a409ca - Show all commits
+192
View File
@@ -1306,6 +1306,54 @@ export const defaultQuests: Array<Quest> = [
status: "locked",
zoneId: "reality_forge",
},
{
combatPowerRequired: 1e59,
description:
"The deep levels of the Forge, where the most experimental realities are stored in a state of near-completion. Your guild discovers that several of these abandoned projects are disturbingly familiar.",
durationSeconds: 24 * 60 * 60,
id: "forge_depths",
name: "The Forge Depths",
prerequisiteIds: [ "forge_chronicle" ],
rewards: [
{ amount: 5e62, type: "gold" },
{ amount: 1.5e59, type: "essence" },
{ amount: 8e54, type: "crystals" },
],
status: "locked",
zoneId: "reality_forge",
},
{
combatPowerRequired: 3e64,
description:
"The underlying structure that defines the laws for every reality the Forge produces — a lattice of constraints so fundamental that violating them would undo everything. Your guild crosses it carefully.",
durationSeconds: 24 * 60 * 60,
id: "prime_matrix",
name: "The Prime Matrix",
prerequisiteIds: [ "forge_depths" ],
rewards: [
{ amount: 2e67, type: "gold" },
{ amount: 6e63, type: "essence" },
{ amount: 3e59, type: "crystals" },
],
status: "locked",
zoneId: "reality_forge",
},
{
combatPowerRequired: 8e69,
description:
"The complete record of every reality the Forge has ever produced, indexed, annotated, and preserved. Your universe has a surprisingly detailed entry with several editorial notes.",
durationSeconds: 24 * 60 * 60,
id: "creation_archive",
name: "The Creation Archive",
prerequisiteIds: [ "prime_matrix" ],
rewards: [
{ amount: 1e72, type: "gold" },
{ amount: 3e68, type: "essence" },
{ amount: 1.5e64, type: "crystals" },
],
status: "locked",
zoneId: "reality_forge",
},
// ── Cosmic Maelstrom ──────────────────────────────────────────────────────
{
combatPowerRequired: 1.8e46,
@@ -1406,6 +1454,54 @@ export const defaultQuests: Array<Quest> = [
status: "locked",
zoneId: "cosmic_maelstrom",
},
{
combatPowerRequired: 2e73,
description:
"The deepest layer of the maelstrom — where the storms have been spiralling for so long they have created something resembling order. Your guild navigates it by learning to read the shape of chaos.",
durationSeconds: 24 * 60 * 60,
id: "maelstrom_deep",
name: "The Deep Maelstrom",
prerequisiteIds: [ "maelstrom_codex" ],
rewards: [
{ amount: 5e76, type: "gold" },
{ amount: 1.5e73, type: "essence" },
{ amount: 8e68, type: "crystals" },
],
status: "locked",
zoneId: "cosmic_maelstrom",
},
{
combatPowerRequired: 5e79,
description:
"The point at which all the storm currents converge — not because they are drawn there, but because there is nowhere else left to go. Your guild stands in the geometric centre of cosmic fury.",
durationSeconds: 24 * 60 * 60,
id: "maelstrom_nexus",
name: "The Storm Nexus",
prerequisiteIds: [ "maelstrom_deep" ],
rewards: [
{ amount: 2e83, type: "gold" },
{ amount: 6e79, type: "essence" },
{ amount: 3e75, type: "crystals" },
],
status: "locked",
zoneId: "cosmic_maelstrom",
},
{
combatPowerRequired: 1e86,
description:
"The record of every storm that has ever been — an archive written in lightning, indexed in thunder, preserved in the kind of silence that only exists at the exact centre of infinite noise.",
durationSeconds: 24 * 60 * 60,
id: "storm_chronicle",
name: "The Storm Chronicle",
prerequisiteIds: [ "maelstrom_nexus" ],
rewards: [
{ amount: 1e90, type: "gold" },
{ amount: 3e86, type: "essence" },
{ amount: 1.5e82, type: "crystals" },
],
status: "locked",
zoneId: "cosmic_maelstrom",
},
// ── Primeval Sanctum ──────────────────────────────────────────────────────
{
combatPowerRequired: 7.2e49,
@@ -1504,6 +1600,54 @@ export const defaultQuests: Array<Quest> = [
status: "locked",
zoneId: "primeval_sanctum",
},
{
combatPowerRequired: 3e92,
description:
"The deepest chambers of the sanctum — where the primordia are not preserved but still occurring, still being, still becoming for the first and only time. The floor hums with unfinished creation.",
durationSeconds: 24 * 60 * 60,
id: "sanctum_deep",
name: "The Deep Sanctum",
prerequisiteIds: [ "sanctum_chronicle" ],
rewards: [
{ amount: 8e95, type: "gold" },
{ amount: 2.5e92, type: "essence" },
{ amount: 1e88, type: "crystals" },
],
status: "locked",
zoneId: "primeval_sanctum",
},
{
combatPowerRequired: 8e98,
description:
"The crossroads between everything primeval and everything that came after — a threshold so old that every subsequent age of the universe is, from its perspective, still ongoing.",
durationSeconds: 24 * 60 * 60,
id: "sanctum_nexus",
name: "The Primeval Nexus",
prerequisiteIds: [ "sanctum_deep" ],
rewards: [
{ amount: 4e102, type: "gold" },
{ amount: 1.2e99, type: "essence" },
{ amount: 5e94, type: "crystals" },
],
status: "locked",
zoneId: "primeval_sanctum",
},
{
combatPowerRequired: 2e105,
description:
"The sanctum's final gift to those who reached its depths: a full accounting of what it means to have existed before time had opinions about how things should go. Your guild is the first to read it.",
durationSeconds: 24 * 60 * 60,
id: "primeval_archive",
name: "The Primeval Archive",
prerequisiteIds: [ "sanctum_nexus" ],
rewards: [
{ amount: 2e109, type: "gold" },
{ amount: 6e105, type: "essence" },
{ amount: 2.5e101, type: "crystals" },
],
status: "locked",
zoneId: "primeval_sanctum",
},
// ── The Absolute ──────────────────────────────────────────────────────────
{
combatPowerRequired: 3e53,
@@ -1601,4 +1745,52 @@ export const defaultQuests: Array<Quest> = [
status: "locked",
zoneId: "the_absolute",
},
{
combatPowerRequired: 5e111,
description:
"Beyond the end of everything, there is more. Not in contradiction — but in the way that answers, once found, reveal the next question. Your guild goes further than the concept of further was designed to accommodate.",
durationSeconds: 24 * 60 * 60,
id: "absolute_beyond",
name: "Beyond the Absolute",
prerequisiteIds: [ "absolute_dominion" ],
rewards: [
{ amount: 1e118, type: "gold" },
{ amount: 3e114, type: "essence" },
{ amount: 1.5e110, type: "crystals" },
],
status: "locked",
zoneId: "the_absolute",
},
{
combatPowerRequired: 1e118,
description:
"The region that exists past the end of existence — a space defined not by what it contains but by being the place where containment no longer applies. Your guild navigates it by not needing it to make sense.",
durationSeconds: 24 * 60 * 60,
id: "absolute_depth",
name: "The Absolute Depth",
prerequisiteIds: [ "absolute_beyond" ],
rewards: [
{ amount: 5e124, type: "gold" },
{ amount: 1.5e121, type: "essence" },
{ amount: 7e116, type: "crystals" },
],
status: "locked",
zoneId: "the_absolute",
},
{
combatPowerRequired: 3e124,
description:
"The final record: not of what happened, but of the fact that it happened at all. A guild from a mortal realm reached the end of all things and chose to keep going. The universe notes this with something that is not quite surprise.",
durationSeconds: 24 * 60 * 60,
id: "absolute_chronicle",
name: "The Absolute Chronicle",
prerequisiteIds: [ "absolute_depth" ],
rewards: [
{ amount: 2e131, type: "gold" },
{ amount: 6e127, type: "essence" },
{ amount: 3e123, type: "crystals" },
],
status: "locked",
zoneId: "the_absolute",
},
];
+24
View File
@@ -881,6 +881,30 @@ describe("debug route", () => {
expect(body.bossesPatched).toBe(1);
});
it("patches boss when only equipmentRewards differ (covers savedRewards branch)", async () => {
const state = makeState({
bosses: [{ id: "troll_king", status: "available", currentHp: 1000, maxHp: 1000, upgradeRewards: ["click_2"], bountyRunestonesClaimed: false, damagePerSecond: 5, goldReward: 10_000, essenceReward: 25, crystalReward: 5, equipmentRewards: [], prestigeRequirement: 0, zoneId: "verdant_vale", bountyRunestones: 1, name: "The Troll King", description: "Gruk the Immovable has terrorised the trade roads for decades. Merchants will pay handsomely for his head." }] as GameState["bosses"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { bossesPatched: number };
expect(body.bossesPatched).toBe(1);
});
it("patches boss when only bountyRunestones differs with all other fields matching", async () => {
const state = makeState({
bosses: [{ id: "troll_king", status: "available", currentHp: 1000, maxHp: 1000, upgradeRewards: ["click_2"], bountyRunestonesClaimed: false, damagePerSecond: 5, goldReward: 10_000, essenceReward: 25, crystalReward: 5, equipmentRewards: ["iron_sword", "chainmail", "mages_focus"], prestigeRequirement: 0, zoneId: "verdant_vale", bountyRunestones: 99, name: "The Troll King", description: "Gruk the Immovable has terrorised the trade roads for decades. Merchants will pay handsomely for his head." }] as GameState["bosses"],
});
vi.mocked(prisma.gameState.findUnique).mockResolvedValueOnce({ state } as never);
vi.mocked(prisma.gameState.update).mockResolvedValueOnce({} as never);
const res = await syncNewContent();
expect(res.status).toBe(200);
const body = await res.json() as { bossesPatched: number };
expect(body.bossesPatched).toBe(1);
});
it("skips boss stat patching for bosses not in defaults", async () => {
const state = makeState({
bosses: [{ id: "nonexistent_boss_xyz", status: "available", currentHp: 100, maxHp: 1, upgradeRewards: [], bountyRunestonesClaimed: false, damagePerSecond: 1, goldReward: 1, essenceReward: 1, crystalReward: 1, equipmentRewards: [], prestigeRequirement: 0, zoneId: "old_zone", bountyRunestones: 0, name: "Ghost", description: "Old" }] as GameState["bosses"],
+3 -3
View File
@@ -302,7 +302,7 @@ export const computeEffectiveAdventurerStats = (
const runestonesIncome = state.prestige.runestonesIncomeMultiplier ?? 1;
const runestonesEssence = state.prestige.runestonesEssenceMultiplier ?? 1;
const prestigeCombatMultiplier = Math.pow(PRESTIGE_COMBAT_BASE, state.prestige.count);
const prestigeCombatMultiplier = PRESTIGE_COMBAT_BASE ** state.prestige.count;
const echoIncome = state.transcendence?.echoIncomeMultiplier ?? 1;
const echoCombatMultiplier = state.transcendence?.echoCombatMultiplier ?? 1;
const craftedGoldMultiplier
@@ -383,7 +383,7 @@ export const computePartyCombatPower = (state: GameState): number => {
}
}
const prestigeMultiplier = Math.pow(PRESTIGE_COMBAT_BASE, state.prestige.count);
const prestigeMultiplier = PRESTIGE_COMBAT_BASE ** state.prestige.count;
const equipmentCombatMultiplier = state.equipment.
filter((item) => {
@@ -477,7 +477,7 @@ export const computeProjectedRunestones = (state: GameState): number => {
: 1;
const runestoneMult = gain1Mult * gain2Mult;
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- optional chained game state field */
const echoMult: number = state.transcendence?.echoRunestoneMultiplier ?? 1;
const echoMult: number = state.transcendence?.echoPrestigeRunestoneMultiplier ?? 1;
return Math.floor(base * runestoneMult * echoMult);
};