Keep money mattering at the top, and fix the instruments that hid it
Both settings now carry a charge levied by standing itself, which no purchase ends. Corporate Ladder's steps on money (1200/-60, 2400/-120, 3600/-240): a flat charge cannot flatten a slope, since late income scales with how well the player plays, and the flat charge needed to stop an optimiser accumulating put ordinary archetypes below zero for 44% of a run. Stepped, the balance settles around $4,250 and ordinary play comes out better than before it existed. The Frontier's is flat at -4 a run, because its late game never accumulated in the first place. Found in the first two human sessions of this work: - A career saved before a pack gained a stat had no path for it, and conditions compare numbers only, so every event, option and upkeep entry gated on a new stat silently did not exist for that run. Loading now fills those in. - 'Wrap up my career' sat immediately after the last option in tab order, so one Tab too many and Enter read as the game ending at day 31. It goes last, and the retrospective says plainly that it is a summary. - A 181-turn session ran against a stale bundle and the change under test never fired. Every logged turn now records app_version and build. - src/version.js had reported 0.3.0 since 0.3.0, while playtesters were told to check the status bar. A test now holds it to package.json. - 'Put money against the book' was shown locked to a player who owed nothing, six times. Options gated on a state the player cannot choose to enter are now hidden rather than greyed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxdJFbLq1rBKsUeticGV1g
This commit is contained in:
co-authored by
Claude Opus 5
parent
5d645e6452
commit
d84ae495f4
+52
-2
@@ -30,6 +30,26 @@ async function boot({ pack = 'corporateladder' } = {}) {
|
||||
const text = (root) => root.textContent;
|
||||
const optionButtons = (root) => byClass(root, 'option');
|
||||
const enabledOptions = (root) => optionButtons(root).filter((b) => !b.disabled);
|
||||
const wrapUp = (root) => byTag(root, 'button').find((b) => /Wrap up/.test(b.textContent));
|
||||
|
||||
test('tabbing past the last option does not land on the button that ends the run', async () => {
|
||||
// Document order is tab order. "Wrap up my career" sat immediately after the
|
||||
// last option, so one Tab too many and Enter showed the retrospective — which
|
||||
// a real player, on the keyboard, read as the game ending at day 31.
|
||||
const dom = await boot();
|
||||
try {
|
||||
const buttons = byTag(dom.root, 'button');
|
||||
const lastOption = buttons.map((b) => /option/.test(b.className ?? '')).lastIndexOf(true);
|
||||
const next = buttons[lastOption + 1];
|
||||
assert.ok(next, 'there is something after the options');
|
||||
assert.doesNotMatch(next.textContent, /Wrap up/,
|
||||
'the next thing Tab reaches should not be the one that leaves the turn');
|
||||
assert.equal(wrapUp(dom.root), buttons[buttons.length - 1],
|
||||
'it belongs last, behind everything harmless');
|
||||
} finally {
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the app boots to a playable turn screen', async () => {
|
||||
const dom = await boot();
|
||||
@@ -213,9 +233,10 @@ test('the retrospective summarises the run and is not an ending', async () => {
|
||||
enabledOptions(dom.root)[0].click();
|
||||
firstByClass(dom.root, 'button--next').click();
|
||||
}
|
||||
firstByClass(dom.root, 'button').click(); // "Wrap up my career"
|
||||
wrapUp(dom.root).click();
|
||||
|
||||
assert.ok(firstByClass(dom.root, 'screen--retrospective'));
|
||||
assert.match(text(dom.root), /not an ending/, 'it says so, rather than implying it');
|
||||
const body = text(dom.root);
|
||||
assert.match(body, /Career retrospective/);
|
||||
assert.match(body, /6 days in the mailroom/);
|
||||
@@ -234,7 +255,7 @@ test('the retrospective summarises the run and is not an ending', async () => {
|
||||
test('a run with nothing notable still gets a retrospective', async () => {
|
||||
const dom = await boot();
|
||||
try {
|
||||
firstByClass(dom.root, 'button').click(); // wrap up on turn one
|
||||
wrapUp(dom.root).click(); // wrap up on turn one
|
||||
assert.ok(firstByClass(dom.root, 'screen--retrospective'));
|
||||
assert.match(text(dom.root), /Nothing that anybody would write down/);
|
||||
} finally {
|
||||
@@ -453,6 +474,35 @@ test('a run saved before settings were selectable is not lost', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('a save written before a stat existed gains it, rather than losing the content', async () => {
|
||||
// The failure this guards against is silent, which is what makes it worth a
|
||||
// test: conditions.js compares numbers only, so with no `lifestyle` path both
|
||||
// `lifestyle < 2` and `lifestyle >= 1` are false. Every event, option and
|
||||
// upkeep entry gated on a stat added since the save was written simply does
|
||||
// not exist for that career, and nothing anywhere says so.
|
||||
const dom = installFakeDom();
|
||||
try {
|
||||
const { startGame } = await import('../src/engine/game.js');
|
||||
const { corporateLadder } = await import('../content/corporateladder/index.js');
|
||||
const before = startGame(corporateLadder, { seed: 'pre-lifestyle' });
|
||||
before.turn = 40;
|
||||
delete before.lifestyle;
|
||||
dom.storage.setItem('theladder.save.v1.corporateladder',
|
||||
JSON.stringify({ format: 1, savedAt: null, state: before }));
|
||||
dom.storage.setItem('theladder.pack.v1', 'corporateladder');
|
||||
|
||||
const { boot: bootApp } = await import(`../src/ui/app.js?t=${Math.random()}`);
|
||||
bootApp(dom.root);
|
||||
|
||||
assert.match(text(dom.root), /Day 40/, 'the career resumed where it was');
|
||||
const saved = JSON.parse(dom.storage.getItem('theladder.save.v1.corporateladder'));
|
||||
assert.equal(saved.state.lifestyle, 0,
|
||||
'the stat the run predates was filled in, and the repair written back');
|
||||
} finally {
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('the header reads the second pack\'s display map, not the first\'s', async () => {
|
||||
// `display` is a per-pack contract with the interface, and a typo in it is
|
||||
// only visible at runtime. Corporate Ladder's version is covered above; this
|
||||
|
||||
Reference in New Issue
Block a user