Cold Fork is a freight yard: 29 events across two stages, a four-day cycle, and its own relationship and skill vocabulary. src/engine/ was not touched. Adding it needed three changes outside the engine: one save slot per pack, app.js holding the current pack, and turn.js no longer naming the mailroom (DECISIONS #34). build.js now handles imports wrapped across lines. A conformance suite runs over every registered pack. DECISIONS #35 records three calls on open questions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RxdJFbLq1rBKsUeticGV1g
193 lines
7.7 KiB
JavaScript
193 lines
7.7 KiB
JavaScript
// The Frontier — the second content pack for The Ladder.
|
||
//
|
||
// Cold Fork, a freight town at the end of a stage line. You are a hired hand
|
||
// in Vane & Co.'s yard; the town has a marshal who is tired and a road that is
|
||
// not safe. Two rungs, the same as Corporate Ladder, and not one line of
|
||
// engine code between them.
|
||
//
|
||
// This pack exists to prove the split holds. Everything that makes it a
|
||
// different game from Corporate Ladder is in this directory: a four-day cycle
|
||
// instead of a week, money that arrives from work rather than from a wage,
|
||
// relationship stats named for a place where being owed a favour matters more
|
||
// than being liked, and a skill pair of nerve and trade.
|
||
|
||
import { characters } from './characters.js';
|
||
import { events } from './events/index.js';
|
||
|
||
export const frontier = {
|
||
id: 'frontier',
|
||
name: 'The Frontier',
|
||
blurb:
|
||
'Cold Fork, at the end of a stage line: a freight yard, a road that is ' +
|
||
'not safe, and a marshal who has been six years retiring.',
|
||
version: '0.1.0',
|
||
turnUnit: 'day',
|
||
startingStage: 'hand',
|
||
peopleHeading: 'Cold Fork',
|
||
|
||
stages: {
|
||
hand: { label: 'The Yard', title: 'Hired Hand' },
|
||
deputy: { label: 'The Office', title: 'Deputy Marshal' },
|
||
},
|
||
|
||
// Not a week. The stage comes every fourth day, and the whole town runs on
|
||
// that: the outfit settles up when the stage comes in, the boarding house
|
||
// collects on the rest day before it. A pack that wants a seven-day week
|
||
// says seven here; the engine has no opinion either way, and Corporate
|
||
// Ladder's calendar is the same field with different numbers in it.
|
||
calendar: {
|
||
cycleLength: 4,
|
||
cycleName: 'Run',
|
||
unitNames: ['Stage day', 'Haul', 'Yard', 'Rest'],
|
||
},
|
||
|
||
// Money is smaller here than in Corporate Ladder by a factor of about
|
||
// fifteen, and it behaves differently: the settlement covers board and room
|
||
// and very little else, so the difference between a good run and a bad one
|
||
// is what you took from the work. See `tuning` below for the arithmetic that
|
||
// is actually asserted.
|
||
state: {
|
||
'money': { initial: 40, min: -400, max: 100000 },
|
||
// The store ledger. Named for what it is rather than for a loan: nobody in
|
||
// Cold Fork lends money, they let you carry a balance.
|
||
'owed': { initial: 0, min: 0, max: 100000 },
|
||
'standing': { initial: 50, min: 0, max: 100 },
|
||
'skills.nerve': { initial: 5, min: 0, max: 100 },
|
||
'skills.trade': { initial: 5, min: 0, max: 100 },
|
||
'flags.carries_iron': { initial: false },
|
||
'flags.owes_the_store': { initial: false },
|
||
'flags.covered_for_dutch': { initial: false },
|
||
'flags.sworn_in': { initial: false },
|
||
'flags.spoke_for_dutch': { initial: false },
|
||
'notable': { initial: [] },
|
||
},
|
||
|
||
// A different vocabulary from Corporate Ladder's likes/fears/helps, and the
|
||
// reason the vocabulary is content's to choose. Out here the useful question
|
||
// about a person is not whether they like you: it is whether they will take
|
||
// your word, and whether they are carrying a debt to you they mean to pay.
|
||
relationshipStats: {
|
||
trusts_you: { label: 'Takes your word', initial: 45, min: 0, max: 100 },
|
||
owes_you: { label: 'Owes you', initial: 10, min: 0, max: 100 },
|
||
wary_of_you: { label: 'Wary of you', initial: 15, min: 0, max: 100 },
|
||
},
|
||
|
||
// The economic shape, and the thing that makes this pack a real test of the
|
||
// engine rather than Corporate Ladder in a hat: there is no wage worth
|
||
// living on. Vane settles up when the stage comes in and it covers the room
|
||
// and the feed and four dollars besides. Everything else you earn, you earn
|
||
// on a turn, by choosing to — and what the choices cost is where the
|
||
// pressure comes from, not an unexplained drain.
|
||
//
|
||
// Keep this arithmetic honest when tuning. Per four-day run:
|
||
// hand: -12 board -8 room +24 settlement = +4
|
||
// deputy: -12 board -8 room +38 pay = +18
|
||
// Both are asserted directly by the conformance suite against `tuning`
|
||
// below, so a number changed here without re-deriving it fails the build
|
||
// rather than trapping a player forty turns in.
|
||
upkeep: [
|
||
{ label: 'Board and feed', effects: [{ path: 'money', op: 'add', value: -3 }] },
|
||
// Turn 1 is a Stage day, so offset 3 is the first Rest day and offset 4
|
||
// the next Stage day — the room is collected the night before the outfit
|
||
// settles up, which is the boarding house's whole business model.
|
||
{
|
||
label: 'Room at the Cordwood',
|
||
every: 4,
|
||
offset: 3,
|
||
effects: [{ path: 'money', op: 'add', value: -8 }],
|
||
},
|
||
{
|
||
label: 'Settlement',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'stage', op: '==', value: 'hand' }],
|
||
effects: [{ path: 'money', op: 'add', value: 24 }],
|
||
},
|
||
{
|
||
label: 'Deputy pay',
|
||
every: 4,
|
||
offset: 4,
|
||
// The town pays a real wage, and it is still not much of a living. What
|
||
// changes with the star is not the money; it is what the days contain.
|
||
requires: [{ path: 'stage', op: '==', value: 'deputy' }],
|
||
effects: [{ path: 'money', op: 'add', value: 38 }],
|
||
},
|
||
// Store credit costs more the deeper in you are, the same step function
|
||
// Corporate Ladder uses for its loan — a shape, not a setting.
|
||
{
|
||
label: 'Store credit',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'owed', op: '>', value: 0 }],
|
||
effects: [{ path: 'money', op: 'add', value: -3 }],
|
||
},
|
||
{
|
||
label: 'Store credit (carried)',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'owed', op: '>=', value: 60 }],
|
||
effects: [{ path: 'money', op: 'add', value: -3 }],
|
||
},
|
||
{
|
||
label: 'Store credit (posted)',
|
||
every: 4,
|
||
offset: 4,
|
||
requires: [{ path: 'owed', op: '>=', value: 120 }],
|
||
effects: [{ path: 'money', op: 'add', value: -4 }],
|
||
},
|
||
],
|
||
|
||
// What the conformance suite holds this pack to. Declaring it here rather
|
||
// than writing it into a test is the same principle as the rest of the pack:
|
||
// the numbers a setting must hit are the setting's business. See
|
||
// DECISIONS.md #31.
|
||
tuning: {
|
||
// Net movement of `money` from upkeep alone, per calendar cycle, at each
|
||
// stage. Upkeep is the floor a player plays against: barely above water in
|
||
// the yard, and a real if unremarkable living with the star.
|
||
baselinePerCycle: {
|
||
hand: [0, 12],
|
||
deputy: [10, 30],
|
||
},
|
||
},
|
||
|
||
display: {
|
||
'money': { label: 'Money', format: 'money', order: 1 },
|
||
'owed': { label: 'Owed at the store', format: 'money', order: 2, hideWhenZero: true },
|
||
'standing': { label: 'Standing', format: 'meter', order: 3 },
|
||
'skills.nerve': { label: 'Nerve', format: 'meter', order: 4 },
|
||
'skills.trade': { label: 'Trade', format: 'meter', order: 5 },
|
||
'notable': { hidden: true, inLedger: false },
|
||
'flags.carries_iron': {
|
||
label: 'The scattergun', hidden: true,
|
||
whenTrue: 'You are carrying Vane’s scattergun on the box.',
|
||
whenFalse: 'The scattergun is back under the office counter.',
|
||
},
|
||
'flags.owes_the_store': {
|
||
label: 'The store ledger', hidden: true,
|
||
whenTrue: 'Your name is in the book at the store.',
|
||
whenFalse: 'You are square with the store.',
|
||
},
|
||
'flags.covered_for_dutch': {
|
||
label: 'Dutch’s run', hidden: true,
|
||
whenTrue: 'Dutch owes you a run.',
|
||
whenFalse: 'You and Dutch are square.',
|
||
},
|
||
'flags.sworn_in': {
|
||
label: 'The star', hidden: true,
|
||
whenTrue: 'You are sworn in.',
|
||
whenFalse: 'You are back in the yard.',
|
||
},
|
||
'flags.spoke_for_dutch': {
|
||
label: 'Dutch’s name', hidden: true,
|
||
whenTrue: 'Dutch’s name is in for the good runs.',
|
||
whenFalse: 'Dutch’s name is not in for the good runs.',
|
||
},
|
||
},
|
||
|
||
characters,
|
||
events,
|
||
};
|
||
|
||
export default frontier;
|