Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44f55f0d24 | ||
|
|
42b6f30d5b | ||
|
|
e734a6a4a7 |
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Headless test for Session 11 - Golden Starbloom random event.
|
||||
* Verifies golden bloom scheduling, claiming, and the production boost.
|
||||
*/
|
||||
|
||||
// Minimal DOM stubs (only what our code touches)
|
||||
global.document = {
|
||||
getElementById: () => ({ getBoundingClientRect: () => ({ left: 0, top: 0, width: 300, height: 200 }) }),
|
||||
querySelector: () => null,
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
|
||||
// UI stub capturing notifications + spawn/remove calls
|
||||
const notifications = [];
|
||||
global.UI = {
|
||||
showNotification: (msg) => notifications.push(msg),
|
||||
update: () => {},
|
||||
spawnGoldenBloom: () => { global.__spawned = (global.__spawned || 0) + 1; },
|
||||
removeGoldenBloom: () => { global.__removed = (global.__removed || 0) + 1; },
|
||||
updateGoldenBloomBadge: () => {},
|
||||
};
|
||||
|
||||
// Audio / Particles stubs
|
||||
global.Audio = { playGoldenBloom: () => {} };
|
||||
global.Particles = { createGoldenEffect: (x, y) => { global.__goldX = x; global.__goldY = y; } };
|
||||
|
||||
// Load the real game engine source and eval it in this scope.
|
||||
import { readFileSync } from 'fs';
|
||||
const src = readFileSync('/opt/data/idle-game/js/game.js', 'utf8')
|
||||
.replace(/window\.Game = Game;/, '')
|
||||
+ '\nglobalThis.Game = Game;\n';
|
||||
|
||||
eval(src);
|
||||
|
||||
// Build stubs for the data modules needed by the engine.
|
||||
global.BUILDINGS = {
|
||||
starSprout: { id: 'starSprout', baseCost: 15, baseProduction: 0.1 },
|
||||
moonBloom: { id: 'moonBloom', baseCost: 100, baseProduction: 0.5 },
|
||||
};
|
||||
global.UPGRADES = {};
|
||||
|
||||
// Minimal prestige stub returning x1 multiplier (no bonus) so we can isolate
|
||||
// golden-bloom math from the real prestige system.
|
||||
global.PRESTIGE = { getMultiplier: () => 1 };
|
||||
global.PRESTIGE_UPGRADES = {};
|
||||
|
||||
// Achievements stub - check() must be a no-op for these tests.
|
||||
global.Achievements = { check: () => {} };
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) { passed++; console.log(` OK ${msg}`); }
|
||||
else { failed++; console.error(` FAIL ${msg}`); }
|
||||
}
|
||||
|
||||
console.log('Test 1: no bloom active initially -> remaining is 0');
|
||||
Game.state.goldenBlooms.active = false;
|
||||
assert(Game.getGoldenBloomRemaining() === 0, `remaining is 0 (got ${Game.getGoldenBloomRemaining()})`);
|
||||
|
||||
console.log('Test 2: claiming with no active bloom returns 0 and does not award stardust');
|
||||
const before = Game.state.stardust;
|
||||
Game.state.goldenBlooms.active = false;
|
||||
const none = Game.claimGoldenBloom(10, 10);
|
||||
assert(none === 0, `claim returned 0 when inactive (got ${none})`);
|
||||
assert(Game.state.stardust === before, 'stardust unchanged when no bloom');
|
||||
|
||||
console.log('Test 3: golden bloom bonus = floor(production * mult), production boost applied');
|
||||
// Give the player some buildings so production is deterministic.
|
||||
Game.state.buildings = { starSprout: 10 }; // 10 * 0.1 = 1.0 /s (prestige multiplier = 1)
|
||||
Game.state.productionBoostUntil = 0;
|
||||
const remaining0 = Game.getGoldenBloomRemaining();
|
||||
Game.state.goldenBlooms.active = true;
|
||||
Game.state.goldenBlooms.timer = 5;
|
||||
assert(Game.getGoldenBloomRemaining() === 5, `remaining reflects timer (got ${Game.getGoldenBloomRemaining()})`);
|
||||
|
||||
const prodBefore = Game.getProductionPerSecond();
|
||||
assert(Math.abs(prodBefore - 1.0) < 1e-9, `production is 1.0 before boost (got ${prodBefore})`);
|
||||
|
||||
// Activate the boost and check the x1.5 multiplier.
|
||||
Game.state.productionBoostUntil = Date.now() + 30000;
|
||||
const prodBoosted = Game.getProductionPerSecond();
|
||||
assert(Math.abs(prodBoosted - 1.5) < 1e-9, `production is 1.5 during boost (got ${prodBoosted})`);
|
||||
|
||||
const boostInfo = Game.getProductionBoostInfo();
|
||||
assert(boostInfo.active === true, 'boost info reports active');
|
||||
assert(boostInfo.remaining > 0 && boostInfo.remaining <= 30, `boost remaining in range (${boostInfo.remaining})`);
|
||||
|
||||
// Now claim the bloom. Bonus should be floor(1.5 * 15) = 22.
|
||||
Game.state.productionBoostUntil = Date.now() + 30000; // keep boost on so bonus uses boosted prod
|
||||
const earned = Game.claimGoldenBloom(42, 84);
|
||||
assert(earned === Math.floor(1.5 * 15), `bonus is floor(prod*mult)=${Math.floor(1.5*15)} (got ${earned})`);
|
||||
assert(Game.state.stardust === before + earned, 'stardust increased by exactly the bonus');
|
||||
assert(Game.state.goldenBlooms.totalClaimed === 1, 'totalClaimed incremented to 1');
|
||||
assert(Game.state.goldenBlooms.active === false, 'bloom no longer active after claim');
|
||||
assert(global.__goldX === 42 && global.__goldY === 84, `particle coords passed through (${global.__goldX}, ${global.__goldY})`);
|
||||
assert(notifications.some(n => /Golden Starbloom/.test(n)), 'golden bloom notification shown');
|
||||
|
||||
console.log('Test 4: boost expires correctly via getProductionBoostInfo');
|
||||
Game.state.productionBoostUntil = Date.now() - 1000; // already expired
|
||||
const expired = Game.getProductionBoostInfo();
|
||||
assert(expired.active === false && expired.remaining === 0, 'boost inactive after expiry');
|
||||
const prodExpired = Game.getProductionPerSecond();
|
||||
assert(Math.abs(prodExpired - 1.0) < 1e-9, `production back to 1.0 after boost expires (got ${prodExpired})`);
|
||||
|
||||
console.log('Test 5: updateGoldenBloom countdown decrements active bloom and vanishes');
|
||||
Game.state.goldenBlooms.active = true;
|
||||
Game.state.goldenBlooms.timer = 2.0;
|
||||
Game.updateGoldenBloom(1.0); // advance 1s
|
||||
assert(Game.state.goldenBlooms.active === true && Game.state.goldenBlooms.timer === 1.0, `countdown decremented (timer=${Game.state.goldenBlooms.timer})`);
|
||||
Game.updateGoldenBloom(1.5); // more than remaining -> vanishes
|
||||
assert(Game.state.goldenBlooms.active === false && Game.state.goldenBlooms.timer === 0, 'bloom vanishes when timer hits 0');
|
||||
|
||||
console.log('Test 6: bloom spawns after elapsed reaches maxMs (forced)');
|
||||
Game.state.goldenBlooms.active = false;
|
||||
Game.state.goldenBlooms.lastSpawn = Date.now() - (Game.config.goldenBloomMaxMs + 1); // past maximum
|
||||
global.__spawned = 0;
|
||||
Game.updateGoldenBloom(0.1);
|
||||
assert(Game.state.goldenBlooms.active === true, 'bloom spawns once maxMs has elapsed');
|
||||
assert(Game.state.goldenBlooms.timer === Game.config.goldenBloomDuration, `timer set to duration (${Game.state.goldenBlooms.timer})`);
|
||||
assert(global.__spawned >= 1, `UI.spawnGoldenBloom called (${global.__spawned}x)`);
|
||||
|
||||
console.log('Test 7: bloom does NOT spawn before minMs elapsed');
|
||||
Game.state.goldenBlooms.active = false;
|
||||
Game.state.goldenBlooms.lastSpawn = Date.now(); // just now
|
||||
const spawnedBefore = global.__spawned || 0;
|
||||
Game.updateGoldenBloom(5); // advance a few seconds (well under minMs)
|
||||
assert(Game.state.goldenBlooms.active === false, 'no premature spawn before minMs');
|
||||
|
||||
console.log(`\nResult: ${passed} passed, ${failed} failed`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Headless test for Session 15 - "Buy Max prestige upgrades" bulk-buy feature.
|
||||
* Verifies Game.buyMaxPrestigeUpgrades() and PrestigeUI.updateBuyMaxPrestigeButton().
|
||||
*/
|
||||
|
||||
// Minimal DOM stubs (only what our code touches)
|
||||
const els = {
|
||||
'buy-all-prestige': { disabled: false, style: {}, textContent: '' },
|
||||
};
|
||||
global.document = {
|
||||
getElementById: (id) => els[id] || { getBoundingClientRect: () => ({ left: 0, top: 0, width: 300, height: 200 }) },
|
||||
querySelector: () => null,
|
||||
querySelectorAll: () => [],
|
||||
addEventListener: () => {},
|
||||
};
|
||||
|
||||
const notifications = [];
|
||||
global.window = globalThis; // prestige.js ends with window.PRESTIGE = ...
|
||||
global.UI = {
|
||||
showNotification: (msg) => notifications.push(msg),
|
||||
update: () => {},
|
||||
updateBuyMaxPrestigeButton: () => {}, // mirror the real UI method for the test harness
|
||||
};
|
||||
global.Audio = { playPurchase: () => {} };
|
||||
global.Particles = {};
|
||||
|
||||
// Load the real game engine source.
|
||||
import { readFileSync } from 'fs';
|
||||
const gameSrc = readFileSync('/opt/data/idle-game/js/game.js', 'utf8')
|
||||
.replace(/window\.Game = Game;/, '')
|
||||
+ '\nglobalThis.Game = Game;\n';
|
||||
eval(gameSrc);
|
||||
|
||||
// Load the real prestige data + UI (game.js no longer references these).
|
||||
const prestigeDataSrc = readFileSync('/opt/data/idle-game/js/prestige.js', 'utf8');
|
||||
eval(prestigeDataSrc + '\nglobalThis.PRESTIGE = PRESTIGE;\nglobalThis.PRESTIGE_UPGRADES = PRESTIGE_UPGRADES;\n');
|
||||
global.PRESTIGE = globalThis.PRESTIGE;
|
||||
global.PRESTIGE_UPGRADES = globalThis.PRESTIGE_UPGRADES;
|
||||
|
||||
const prestigeUISrc = readFileSync('/opt/data/idle-game/js/prestigeUI.js', 'utf8')
|
||||
.replace(/window\.PrestigeUI = PrestigeUI;/, '')
|
||||
+ '\nglobalThis._RealPrestigeUI = PrestigeUI;\n';
|
||||
eval(prestigeUISrc);
|
||||
const realPrestigeUI = globalThis._RealPrestigeUI;
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) { passed++; console.log(` OK ${msg}`); }
|
||||
else { failed++; console.error(` FAIL ${msg}`); }
|
||||
}
|
||||
|
||||
console.log('Test 1: buyMaxPrestigeUpgrades with no affordable upgrades buys nothing');
|
||||
Game.state.stellarEssence = 5; // below even the cheapest upgrade (cosmicFingers cost 10)
|
||||
Game.state.prestigeUpgrades = [];
|
||||
Game.state.buildings = {};
|
||||
const r1 = Game.buyMaxPrestigeUpgrades();
|
||||
assert(r1.bought === 0, `bought count is 0 (got ${r1.bought})`);
|
||||
assert(r1.ids.length === 0, 'ids array empty');
|
||||
assert(Game.state.prestigeUpgrades.length === 0, 'no upgrades purchased');
|
||||
assert(Game.state.stellarEssence === 5, 'essence unchanged when nothing affordable');
|
||||
|
||||
console.log('Test 2: buyMaxPrestigeUpgrades buys everything affordable in one pass');
|
||||
// prestige upgrades are all ungated (no requirements field). cosmicFingers(10) + stellarTouch(50) = exactly 60.
|
||||
Game.state.stellarEssence = 60;
|
||||
Game.state.prestigeUpgrades = [];
|
||||
const r2 = Game.buyMaxPrestigeUpgrades();
|
||||
assert(r2.bought === 2, `bought 2 upgrades (got ${r2.bought})`);
|
||||
assert(r2.ids.includes('cosmicFingers') && r2.ids.includes('stellarTouch'), 'bought cosmicFingers + stellarTouch');
|
||||
const spent = 10 + 50;
|
||||
assert(Game.state.stellarEssence === 60 - spent, `essence depleted correctly (${Game.state.stellarEssence}, expected ${60 - spent})`);
|
||||
|
||||
console.log('Test 3: requirement-style gating is a no-op for prestige upgrades (all ungated)');
|
||||
// Prestige upgrades have no requirements field, so with enough essence everything buys.
|
||||
Game.state.stellarEssence = 1000; // covers all six upgrades (max cost 200)
|
||||
Game.state.prestigeUpgrades = [];
|
||||
const r3 = Game.buyMaxPrestigeUpgrades();
|
||||
assert(r3.bought === 6, `bought all 6 prestige upgrades (got ${r3.bought})`);
|
||||
assert(['cosmicFingers','stellarTouch','essenceAmplifier','acceleratedGrowth','quantumGarden','temporalManipulation'].every(id => r3.ids.includes(id)), 'all six upgrades purchased');
|
||||
|
||||
console.log('Test 4: loop re-checks until a full pass buys nothing (idempotent convergence)');
|
||||
Game.state.stellarEssence = 1000;
|
||||
Game.state.prestigeUpgrades = [];
|
||||
const r4 = Game.buyMaxPrestigeUpgrades();
|
||||
assert(r4.bought === 6, `bought all 6 upgrades on first pass (got ${r4.bought})`);
|
||||
|
||||
console.log('Test 5: already-owned upgrades are skipped; no double-purchase');
|
||||
Game.state.stellarEssence = 1000;
|
||||
Game.state.prestigeUpgrades = ['cosmicFingers']; // already owned -> must be skipped
|
||||
const r5 = Game.buyMaxPrestigeUpgrades();
|
||||
assert(!r5.ids.includes('cosmicFingers'), 'cosmicFingers not re-bought (already owned)');
|
||||
|
||||
console.log('Test 6: idempotent second call buys nothing');
|
||||
const beforeEssence = Game.state.stellarEssence;
|
||||
const r6 = Game.buyMaxPrestigeUpgrades();
|
||||
assert(r6.bought === 0, `second call buys 0 (got ${r6.bought})`);
|
||||
assert(Game.state.stellarEssence === beforeEssence, 'essence unchanged on no-op call');
|
||||
|
||||
console.log('Test 7: notifications fire on purchase, silence on no-op');
|
||||
notifications.length = 0;
|
||||
Game.state.stellarEssence = 1000;
|
||||
Game.state.prestigeUpgrades = [];
|
||||
Game.state.buildings = {};
|
||||
Game.buyMaxPrestigeUpgrades();
|
||||
assert(notifications.some(n => /Bought .* prestige upgrade/.test(n)), 'notification shown when something bought');
|
||||
notifications.length = 0;
|
||||
const r7 = Game.buyMaxPrestigeUpgrades(); // now nothing left affordable (all bought)
|
||||
assert(!notifications.some(n => /Bought/.test(n)) || r7.bought === 0, `no purchase notification on no-op (bought=${r7.bought})`);
|
||||
|
||||
console.log('Test 8: exercise the REAL PrestigeUI.updateBuyMaxPrestigeButton method against real PRESTIGE_UPGRADES');
|
||||
Game.state.stellarEssence = 0;
|
||||
Game.state.prestigeUpgrades = [];
|
||||
Game.state.buildings = {};
|
||||
realPrestigeUI.updateBuyMaxPrestigeButton();
|
||||
assert(els['buy-all-prestige'].style.display === 'none', `real UI hides Buy Max when essence=0 (display='${els['buy-all-prestige'].style.display}')`);
|
||||
|
||||
// cosmicFingers is the cheapest real prestige upgrade (cost 10, no requirements),
|
||||
// so at essence=10 a button should appear and be enabled.
|
||||
Game.state.stellarEssence = 10;
|
||||
realPrestigeUI.updateBuyMaxPrestigeButton();
|
||||
assert(els['buy-all-prestige'].style.display !== 'none', 'real UI shows Buy Max when a cheap unowned prestige upgrade is affordable');
|
||||
assert(els['buy-all-prestige'].disabled === false, 'real UI Buy Max enabled when affordable');
|
||||
|
||||
// Give 0 essence -> hidden again.
|
||||
Game.state.stellarEssence = 0;
|
||||
realPrestigeUI.updateBuyMaxPrestigeButton();
|
||||
assert(els['buy-all-prestige'].style.display === 'none', 'real UI hides Buy Max when nothing affordable');
|
||||
|
||||
// Requirement-gated affordability: stellarTouch (cost 50, no building req) is
|
||||
// affordable but essenceAmplifier (cost 25, needs solarLeaf>=25) is gated. The
|
||||
// button should still show because at least one unowned upgrade is affordable.
|
||||
Game.state.stellarEssence = 100;
|
||||
Game.state.buildings = { starSprout: 1 }; // solarLeaf < 25 -> essenceAmplifier gated, but stellarTouch still affordable
|
||||
realPrestigeUI.updateBuyMaxPrestigeButton();
|
||||
assert(els['buy-all-prestige'].style.display !== 'none', 'real UI shows Buy Max when a non-gated upgrade is affordable even if gated ones are locked');
|
||||
|
||||
console.log('Test 9: real UI reflects state after an actual buy through Game.buyMaxPrestigeUpgrades');
|
||||
Game.state.stellarEssence = 10;
|
||||
Game.state.prestigeUpgrades = [];
|
||||
Game.buyMaxPrestigeUpgrades(); // buys cosmicFingers (only affordable), essence now 0
|
||||
assert(Game.state.prestigeUpgrades.includes('cosmicFingers'), 'cosmicFingers purchased via real Game method');
|
||||
realPrestigeUI.updateBuyMaxPrestigeButton();
|
||||
assert(els['buy-all-prestige'].style.display === 'none', 'real UI hides Buy Max after all affordable upgrades bought (essence now 0)');
|
||||
|
||||
console.log(`\nResult: ${passed} passed, ${failed} failed`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user