Each pack declares a lifestyle tier, bought on the second rung and charged for every cycle after: Corporate Ladder's car and address, the Frontier's bay and the room over the Ellis place. Falling behind what the job expects costs standing, in steps that steepen as standing rises. A Dispatch coordinator keeping up fully nets -21 a week and earns the difference out of the days. No engine change; the only interface change is a tier display format. Sized by measurement, not feel (DECISIONS #36): a -2/week penalty is invisible against Dispatch's standing gains, and a player who chases standing every turn beats any flat penalty — 99 standing while buying nothing, and $2,245 richer than one who kept up. Hence the top step. Two defects fixed on the way. The conformance suite excluded debt from the economic baseline by matching /debt|owed|loan/i on path names, which would have counted every lifestyle tier into it; packs now declare tuning.baselineExcludes. And 'the promotion leaves a player better off' pooled end balances across archetypes that do not spend alike, so it measured the mix — it is now derived from the upkeep arithmetic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxdJFbLq1rBKsUeticGV1g
265 lines
11 KiB
JavaScript
265 lines
11 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 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: both are states a
|
|
// player opts into, and the baseline is the floor underneath everybody.
|
|
baselineExcludes: ['debt', 'lifestyle'],
|
|
},
|
|
|
|
// 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;
|