Add chance and a cast that changes between runs, and a pack to try them
An option used to do exactly what it said. Now it may carry a check: one of
four attributes (Nerve, Wits, Charm, Resolve) against a set difficulty or
against a named person's own attribute, leaning on how they regard you, with a
success and a failure. The player sees only the odds in words ("a long shot",
"likely"), never a number. The roll is one draw from the run's generator, so a
run still replays exactly from its seed and choices.
A character may be a role with several authored candidates, one drawn per run,
or a group of two to four drawn into numbered slots. Everyone carries a name, a
description and the same four attributes as the player, so a replayed pack
meets a different office. Text names people as {partner} or {clerk}, and an
event about "a clerk" binds one present clerk for the turn. The draw and the
binding are saved with the run, and a save from before a pack drew a cast gets
the same one every time it is loaded.
All of it is generic and optional: Corporate Ladder and the Frontier use none
of it and play as before. The Counting House, a one-stage office about 1910
built from the story-to-pack catalogue, exercises it, and was tuned against the
conformance suite by measurement. DECISIONS #41. Version 0.4.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MhABSrqkwTdcybXmheDPr
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
112e558dbb
commit
3108319a2b
+18
-12
@@ -8,6 +8,7 @@
|
||||
|
||||
import { getPath } from '../engine/paths.js';
|
||||
import { check } from '../engine/conditions.js';
|
||||
import { personIn } from '../engine/cast.js';
|
||||
|
||||
const titleCase = (text) =>
|
||||
text.replace(/_/g, ' ').replace(/^\w/, (c) => c.toUpperCase());
|
||||
@@ -17,16 +18,21 @@ export function displayFor(pack, path) {
|
||||
return pack.display?.[path] ?? null;
|
||||
}
|
||||
|
||||
/** A human name for a path, derived from the path when the pack is silent. */
|
||||
export function labelFor(pack, path) {
|
||||
/**
|
||||
* A human name for a path, derived from the path when the pack is silent.
|
||||
* Given the run's state, a relationship path names whoever is in that slot this
|
||||
* run — a drawn boss, the second of three co-workers — not the role.
|
||||
*/
|
||||
export function labelFor(pack, path, state) {
|
||||
const declared = displayFor(pack, path)?.label;
|
||||
if (declared) return declared;
|
||||
|
||||
const segments = path.split('.');
|
||||
if (segments[0] === 'relationships' && segments.length === 3) {
|
||||
const character = pack.characters?.find((c) => c.id === segments[1]);
|
||||
const name = (state && personIn(pack, state, segments[1])?.name)
|
||||
?? pack.characters?.find((c) => c.id === segments[1])?.name;
|
||||
const stat = pack.relationshipStats?.[segments[2]]?.label ?? titleCase(segments[2]);
|
||||
return `${character?.name ?? titleCase(segments[1])} — ${stat.toLowerCase()}`;
|
||||
return `${name ?? titleCase(segments[1])} — ${stat.toLowerCase()}`;
|
||||
}
|
||||
return titleCase(segments.at(-1));
|
||||
}
|
||||
@@ -61,15 +67,15 @@ export function formatDelta(pack, path, from, to) {
|
||||
* One condition as a requirement the player can read: "Negotiation 15+",
|
||||
* "Trevor — likes you 48+", "not in debt".
|
||||
*/
|
||||
export function describeCondition(pack, condition) {
|
||||
export function describeCondition(pack, condition, state) {
|
||||
if (!condition || typeof condition !== 'object') return '';
|
||||
if (Array.isArray(condition)) return condition.map((c) => describeCondition(pack, c)).join(', ');
|
||||
if (condition.all) return condition.all.map((c) => describeCondition(pack, c)).join(', ');
|
||||
if (condition.any) return condition.any.map((c) => describeCondition(pack, c)).join(' or ');
|
||||
if (condition.not) return `not ${describeCondition(pack, condition.not)}`;
|
||||
if (Array.isArray(condition)) return condition.map((c) => describeCondition(pack, c, state)).join(', ');
|
||||
if (condition.all) return condition.all.map((c) => describeCondition(pack, c, state)).join(', ');
|
||||
if (condition.any) return condition.any.map((c) => describeCondition(pack, c, state)).join(' or ');
|
||||
if (condition.not) return `not ${describeCondition(pack, condition.not, state)}`;
|
||||
|
||||
const { path, op, value } = condition;
|
||||
const label = labelFor(pack, path);
|
||||
const label = labelFor(pack, path, state);
|
||||
|
||||
if (typeof value === 'boolean') return value ? label.toLowerCase() : `no ${label.toLowerCase()}`;
|
||||
|
||||
@@ -86,8 +92,8 @@ export function describeCondition(pack, condition) {
|
||||
}
|
||||
|
||||
/** What a locked option is waiting for. */
|
||||
export function requirementText(pack, failedConditions) {
|
||||
const parts = failedConditions.map((c) => describeCondition(pack, c)).filter(Boolean);
|
||||
export function requirementText(pack, failedConditions, state) {
|
||||
const parts = failedConditions.map((c) => describeCondition(pack, c, state)).filter(Boolean);
|
||||
return parts.length ? `Needs ${parts.join(', ')}` : 'Not available yet';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user