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
314 lines
13 KiB
JavaScript
314 lines
13 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',
|
|
// One sentence for the setting picker. The interface reads it and knows
|
|
// nothing else about what this pack is.
|
|
blurb:
|
|
'A modern office and a mailroom, two people who have been here longer ' +
|
|
'than you, and a laminated org chart with your name not on it.',
|
|
version: '0.1.0',
|
|
turnUnit: 'day',
|
|
startingStage: 'mailroom',
|
|
// What the interface calls the panel of people. The UI used to hardcode
|
|
// "The mailroom" here, which was the one place it named a setting.
|
|
peopleHeading: 'The 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 },
|
|
// What standing costs to keep. Nothing forces a player to climb this, but
|
|
// Dispatch charges standing for staying at the bottom of it — see the
|
|
// "Looking the part" entries in upkeep, and DECISIONS #36.
|
|
'lifestyle': { initial: 0, min: 0, max: 2 },
|
|
'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.
|
|
//
|
|
// Keep this arithmetic honest when tuning: wages, minus two rents, minus
|
|
// fourteen days of small change. Per week that is 560 - 455 - 126 = -21, so
|
|
// about forty dollars down over a fortnight — a drift, not a bleed. Rent was
|
|
// once raised without re-deriving it, which quietly made the baseline four
|
|
// times steeper and trapped a real playtester below zero for 30% of a
|
|
// 182-turn run. The range is asserted from `tuning` below.
|
|
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: 1120 }],
|
|
},
|
|
{
|
|
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 }] },
|
|
// What the job expects of you, and what meeting that expectation costs.
|
|
// Dispatch nets +49 a week; a coordinator with the car is +19, and one
|
|
// with the car and the address is -21 and has to earn the difference out
|
|
// of the days themselves. That is the answer to "money stops mattering
|
|
// late": it stops mattering only if standing is free to keep.
|
|
{
|
|
label: 'Car payment and parking',
|
|
every: 7,
|
|
offset: 7,
|
|
requires: [{ path: 'lifestyle', op: '>=', value: 1 }],
|
|
effects: [{ path: 'money', op: 'add', value: -30 }],
|
|
},
|
|
{
|
|
label: 'The better address',
|
|
every: 7,
|
|
offset: 7,
|
|
requires: [{ path: 'lifestyle', op: '>=', value: 2 }],
|
|
effects: [{ path: 'money', op: 'add', value: -40 }],
|
|
},
|
|
// The pressure that makes the spending a choice rather than a vanity. Both
|
|
// are gated on Dispatch: the mailroom pays too little for this to be
|
|
// anything but a trap, and "later in a career" is the point.
|
|
{
|
|
label: 'Looking the part',
|
|
every: 7,
|
|
offset: 7,
|
|
requires: [
|
|
{ path: 'stage', op: '==', value: 'dispatch' },
|
|
{ path: 'lifestyle', op: '<', value: 1 },
|
|
],
|
|
effects: [{ path: 'reputation', op: 'add', value: -4 }],
|
|
},
|
|
{
|
|
// Standing above sixty-five is a thing you are seen to be, and being seen
|
|
// to be it runs on a better address than a coordinator's first one.
|
|
//
|
|
// Sized by measurement, not by feel. At -2 a week this was invisible:
|
|
// Dispatch hands out standing several points at a time, so a coordinator
|
|
// who bought nothing still finished pinned at 100 and the charge may as
|
|
// well not have existed. At -6, with the entry above, a non-buyer settles
|
|
// in the low seventies while somebody keeping up finishes at 100 — the
|
|
// ceiling is real and money is what lifts it.
|
|
label: 'Looking the part',
|
|
every: 7,
|
|
offset: 7,
|
|
requires: [
|
|
{ path: 'stage', op: '==', value: 'dispatch' },
|
|
{ path: 'reputation', op: '>=', value: 65 },
|
|
{ path: 'lifestyle', op: '<', value: 2 },
|
|
],
|
|
effects: [{ path: 'reputation', op: 'add', value: -6 }],
|
|
},
|
|
{
|
|
// The top step, and the one that makes the whole thing a choice rather
|
|
// than a tax on carelessness. Without it, a player who spends every turn
|
|
// chasing standing simply out-earns the charge: measured, they held 99
|
|
// while never buying anything, and finished $2,245 richer than a player
|
|
// who kept up — so the lifestyle was a pure loss to anybody paying
|
|
// attention. With it they plateau in the low eighties. What money buys
|
|
// up here is the last fifteen points, which cannot be worked for.
|
|
label: 'Looking the part',
|
|
every: 7,
|
|
offset: 7,
|
|
requires: [
|
|
{ path: 'stage', op: '==', value: 'dispatch' },
|
|
{ path: 'reputation', op: '>=', value: 80 },
|
|
{ path: 'lifestyle', op: '<', value: 2 },
|
|
],
|
|
effects: [{ path: 'reputation', op: 'add', value: -14 }],
|
|
},
|
|
// The charge that never stops, and the only one with no purchase that ends
|
|
// it. Everything above is a ladder with a top: buy both tiers, reach 100
|
|
// standing, and every charge switches off. A real 96-turn session arrived
|
|
// there at turn 84 with nothing left to spend on and a balance that only
|
|
// climbed.
|
|
//
|
|
// It steps on money rather than being flat, and that is the whole design.
|
|
// A flat charge cannot flatten a slope: late income comes from choices and
|
|
// scales with how well the player is playing, so a charge big enough to
|
|
// stop an optimiser accumulating (-360 a week, measured) put ordinary
|
|
// archetypes below zero for 44% of a run and ended them $1,385 in debt.
|
|
// Stepping on money lands it on whoever has it. Measured: the balance
|
|
// settles around $4,250 and stops climbing (-3 a week), while ordinary play
|
|
// gets *better* than before it existed — 13% of a run below zero against
|
|
// 17%, and a tier-2 run ending +$175 rather than -$25.
|
|
{
|
|
label: 'What the standing costs',
|
|
every: 7,
|
|
offset: 7,
|
|
requires: [
|
|
{ path: 'stage', op: '==', value: 'dispatch' },
|
|
{ path: 'reputation', op: '>=', value: 85 },
|
|
{ path: 'money', op: '>=', value: 1200 },
|
|
],
|
|
effects: [{ path: 'money', op: 'add', value: -60 }],
|
|
},
|
|
{
|
|
label: 'What the standing costs',
|
|
every: 7,
|
|
offset: 7,
|
|
requires: [
|
|
{ path: 'stage', op: '==', value: 'dispatch' },
|
|
{ path: 'reputation', op: '>=', value: 85 },
|
|
{ path: 'money', op: '>=', value: 2400 },
|
|
],
|
|
effects: [{ path: 'money', op: 'add', value: -120 }],
|
|
},
|
|
{
|
|
label: 'What the standing costs',
|
|
every: 7,
|
|
offset: 7,
|
|
requires: [
|
|
{ path: 'stage', op: '==', value: 'dispatch' },
|
|
{ path: 'reputation', op: '>=', value: 85 },
|
|
{ path: 'money', op: '>=', value: 3600 },
|
|
],
|
|
effects: [{ path: 'money', op: 'add', value: -240 }],
|
|
},
|
|
// 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 }],
|
|
},
|
|
],
|
|
|
|
// What the conformance suite holds this pack to. The numbers a setting must
|
|
// hit are the setting's business, so they are declared here rather than
|
|
// written into a test that names this pack. See DECISIONS.md #31.
|
|
tuning: {
|
|
// Net movement of `money` from upkeep alone, per calendar cycle — here a
|
|
// week — at each stage, with no debt outstanding.
|
|
baselinePerCycle: {
|
|
mailroom: [-45, 5],
|
|
dispatch: [5, 125],
|
|
},
|
|
// Upkeep gated on these is excluded from that baseline: a debt taken on, a
|
|
// lifestyle bought, a standing earned. The baseline is the floor underneath
|
|
// everybody — what a player faces having none of the three.
|
|
baselineExcludes: ['debt', 'lifestyle', 'reputation'],
|
|
},
|
|
|
|
// 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 },
|
|
// Hidden until it is bought: a mailroom associate has no lifestyle to
|
|
// speak of, and a row reading "The bus" from day one is noise.
|
|
'lifestyle': {
|
|
label: 'Lifestyle', format: 'tier', order: 6, hideWhenZero: true,
|
|
labels: ['The bus', 'A car', 'The address'],
|
|
},
|
|
// 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;
|