Files
TheLadder/content/corporateladder/index.js
T
JesseMarkowitzandClaude Opus 5 76072edf4e Show the build, play from the keyboard, and clean up the ledger
Five things from the second playtest.

A status line at the top of every screen: the game, the pack, your current
title, and the version plus the commit it came from. `tools/stamp.js` writes the
commit into src/build-info.js before serving and before building, so a
distributed dist/theladder.html states exactly which commit produced it. An
uncommitted working tree gets a `+` suffix — "4cab9fc+" is honest in a way a
bare SHA would not be.

The whole game is now playable on Enter. Every screen marks one control and the
app focuses it after each render: the first *available* option while choosing,
the next-turn button once the result is in, "keep going" on the retrospective.
Tab and Shift+Tab reach the other options. Locked options needed no special
handling — the browser's tab order already skips disabled controls, so they stay
visible without being in the way.

"What is notable? under What changed?" — two fixes. `notable` is the
retrospective's own list of moments; a display rule can now say
`inLedger: false` and it is left out, because "Notable — changed" tells nobody
anything. Flags are the opposite and stay, but as sentences the pack supplies
rather than as booleans: "The envelope is in your locker." instead of "the
envelope: now true".

Exported logs live in logs/, gitignored, and `npm run analyze` reads the newest
one there when given no argument. A log records what a real person did turn by
turn, including whatever they typed into the feedback box — committing one once
would put it in the history for good.

Version set to 0.3.0 on the reasoning that the engine was 0.1 and the interface
0.2; say if you want a different scheme and I will renumber.

119 tests. Reasoning in docs/DECISIONS.md §25-28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VMSFHyVPitUoosW5wyEADj
2026-09-10 05:41:32 -04:00

156 lines
6.3 KiB
JavaScript

// Corporate Ladder — the default content pack for The Ladder.
//
// A modern office, the mailroom, and two people who have been there longer
// than you. Everything the engine needs to run this setting is in this file
// and the ones it imports; the engine itself knows nothing about mailrooms.
import { characters } from './characters.js';
import { events } from './events/index.js';
export const corporateLadder = {
id: 'corporateladder',
name: 'Corporate Ladder',
version: '0.1.0',
turnUnit: 'day',
startingStage: 'mailroom',
stages: {
mailroom: { label: 'Mailroom', title: 'Mail Associate I' },
dispatch: { label: 'Dispatch', title: 'Dispatch Coordinator' },
},
// Turn 1 is a Monday. Content can gate on the day — and, more importantly,
// money arrives and leaves on a rhythm the player can plan around instead of
// draining by an unexplained amount every day.
calendar: {
cycleLength: 7,
cycleName: 'Week',
unitNames: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
},
// Money is tight but not hopeless. The baseline bleeds about three dollars a
// day, so a careful player creeps forward and a careless one does not — the
// pressure comes from what choices cost, not from an unexplained drain.
state: {
'money': { initial: 520, min: -5000, max: 1000000 },
'debt': { initial: 0, min: 0, max: 1000000 },
'reputation': { initial: 50, min: 0, max: 100 },
'skills.negotiation': { initial: 5, min: 0, max: 100 },
'skills.logistics': { initial: 5, min: 0, max: 100 },
'flags.took_loan': { initial: false },
'flags.holds_the_envelope': { initial: false },
'flags.covered_for_trevor': { initial: false },
'flags.promoted': { initial: false },
'flags.recommended_trevor': { initial: false },
// Moments the retrospective reads back. Content decides what is memorable;
// the full turn-by-turn record lives in history regardless.
'notable': { initial: [] },
},
// Nobody fears a new hire. Trevor's defaults are lower still — see
// characters.js, where he starts two weeks ahead of you and knows it.
relationshipStats: {
likes_you: { label: 'Likes you', initial: 50, min: 0, max: 100 },
fears_you: { label: 'Wary of you', initial: 10, min: 0, max: 100 },
wants_to_help_you: { label: 'Will help you', initial: 40, min: 0, max: 100 },
},
// Money moves on a schedule the player can feel: paid on alternate Fridays,
// rent out every Monday, and only small change in between. Over a fortnight
// the baseline is about forty dollars down — a drift, not a bleed.
upkeep: [
{ label: 'Coffee, transit, lunch', effects: [{ path: 'money', op: 'add', value: -18 }] },
// Turn 1 is a Monday, so offset 4 is the first Friday and every 14 makes it
// alternate Fridays. Rent lands on Mondays, starting the second week.
// Wages depend on the job. Two entries rather than one, gated on stage:
// the engine has no idea what a promotion is, and does not need to.
{
label: 'Wages',
every: 14,
offset: 4,
requires: [{ path: 'stage', op: '==', value: 'mailroom' }],
effects: [{ path: 'money', op: 'add', value: 980 }],
},
{
label: 'Wages',
every: 14,
offset: 4,
// A real raise, but not a different life — the coordinator job is, as
// Marlene puts it, mostly the same work with a different phone. Net of
// rent it comes to a few dollars a day ahead instead of behind.
requires: [{ path: 'stage', op: '==', value: 'dispatch' }],
effects: [{ path: 'money', op: 'add', value: 1260 }],
},
{ label: 'Rent', every: 7, offset: 7, effects: [{ path: 'money', op: 'add', value: -455 }] },
// The service charge steps up with the size of the debt, so the spiral
// tightens instead of flattening out. A step function in data costs the
// engine nothing; a percentage would have needed a multiply operation.
{
label: 'Loan service charge',
every: 7,
offset: 7,
requires: [{ path: 'debt', op: '>', value: 0 }],
effects: [{ path: 'money', op: 'add', value: -70 }],
},
{
label: 'Loan service charge (arrears)',
every: 7,
offset: 7,
requires: [{ path: 'debt', op: '>=', value: 1000 }],
effects: [{ path: 'money', op: 'add', value: -40 }],
},
{
label: 'Loan service charge (collections)',
every: 7,
offset: 7,
requires: [{ path: 'debt', op: '>=', value: 2000 }],
effects: [{ path: 'money', op: 'add', value: -70 }],
},
],
// How the interface names and formats this pack's state. The UI reads only
// this map, so renaming a stat or reordering the header is content's job.
display: {
'money': { label: 'Money', format: 'money', order: 1 },
'debt': { label: 'Debt', format: 'money', order: 2, hideWhenZero: true },
'reputation': { label: 'Standing', format: 'meter', order: 3 },
'skills.negotiation': { label: 'Negotiation', format: 'meter', order: 4 },
'skills.logistics': { label: 'Logistics', format: 'meter', order: 5 },
// Kept out of the header, and out of the ledger too: `notable` is the
// retrospective's own list, and "Notable — changed" tells nobody anything.
'notable': { hidden: true, inLedger: false },
// Flags do belong in the ledger — a flag flipping is a fact about the
// world — but as a sentence rather than as "now true".
'flags.took_loan': {
label: 'The loan', hidden: true,
whenTrue: 'You are carrying a loan now.',
whenFalse: 'The loan is settled.',
},
'flags.holds_the_envelope': {
label: 'The envelope', hidden: true,
whenTrue: 'The envelope is in your locker.',
whenFalse: 'The envelope is back in the system.',
},
'flags.covered_for_trevor': {
label: "Trevor's Thursday", hidden: true,
whenTrue: 'Trevor owes you a Thursday.',
whenFalse: 'You and Trevor are square.',
},
'flags.promoted': {
label: 'Promotion', hidden: true,
whenTrue: 'You are out of the mailroom.',
whenFalse: 'Back in the mailroom.',
},
'flags.recommended_trevor': {
label: "Trevor's recommendation", hidden: true,
whenTrue: "Trevor's name is in for the mailroom.",
whenFalse: "Trevor's name is not in for the mailroom.",
},
},
characters,
events,
};
export default corporateLadder;