@adatechnology/game-stories 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1563 @@
1
+ // src/bench/module.ts
2
+ import { getProfession, getNamePools as getNamePools2, SHEET_RULESET as SHEET_RULESET3 } from "@adatechnology/game-characters-people";
3
+
4
+ // src/sheet/sheet.ts
5
+ import { SHEET_RULESET } from "@adatechnology/game-characters-people";
6
+
7
+ // src/data/sheet.json
8
+ var sheet_default = {
9
+ sources: {
10
+ page: 'site/pages/zumbis-personagens/personagens.html, "Mem\xF3ria e opini\xE3o" and "Modelo de dados"',
11
+ memories: '"Cada NPC guarda at\xE9 30 lembran\xE7as com peso e data"; memorias: [{ evento, alvoId, peso, dia }] // m\xE1x 30',
12
+ opinion: '"A opini\xE3o sobre cada jogador vai de \u2212100 a +100 e decai devagar para o neutro. Em +60 o NPC aceita entrar no grupo; abaixo de \u221260 pode roubar, delatar a base a outro grupo ou atacar \xE0 noite."',
13
+ decayPerDay: `not in the document ("decai devagar"): 1 point a day is the kit's reversible default, listed as a question`,
14
+ memoryDrop: "not in the document: past 30, the lightest memory goes (the oldest first on a tie), listed as a question"
15
+ },
16
+ memories: { max: 30 },
17
+ opinion: { min: -100, max: 100, joins: 60, hostile: -60, decayPerDay: 1 }
18
+ };
19
+
20
+ // src/sheet/sheet.ts
21
+ function createSheet(options = {}) {
22
+ const rules = options.ruleset ?? SHEET_RULESET;
23
+ const N = { ...sheet_default, ...options.numbers };
24
+ const professionId = options.profession ?? rules.professions[rules.professions.length - 1].id;
25
+ const profession = rules.professions.find((p) => p.id === professionId);
26
+ if (!profession) throw new Error(`game-stories: unknown profession "${professionId}"`);
27
+ const attributes = Object.fromEntries(rules.attributes.map((a) => [a.id, options.attributes?.[a.id] ?? rules.pointBuy.start]));
28
+ const traits = [...options.traits ?? []];
29
+ for (const id of traits) if (!rules.traits.some((t) => t.id === id)) throw new Error(`game-stories: unknown trait "${id}"`);
30
+ const skills = { ...profession.skills };
31
+ for (const [id, level] of Object.entries(options.skills ?? {})) skills[id] = (skills[id] ?? 0) + level;
32
+ const memories = [];
33
+ const opinions = {};
34
+ const history = [];
35
+ const has = (id) => traits.includes(id);
36
+ const traitPoints = () => traits.reduce((sum, id) => sum + rules.traits.find((t) => t.id === id).cost, 0);
37
+ const spent = () => Object.values(attributes).reduce((sum, v) => sum + (v - rules.pointBuy.start), 0);
38
+ const clampOpinion = (v) => Math.max(N.opinion.min, Math.min(N.opinion.max, v));
39
+ return {
40
+ profession: professionId,
41
+ attributes,
42
+ traits,
43
+ skills,
44
+ milestones: (skill) => rules.milestoneLevels.map((level, index) => (skills[skill] ?? 0) >= level ? index : -1).filter((index) => index >= 0),
45
+ pool: () => rules.pointBuy.pool + profession.points + traitPoints() - spent(),
46
+ valid() {
47
+ const inRange = Object.values(attributes).every((v) => v >= rules.pointBuy.min && v <= rules.pointBuy.max);
48
+ const excluded = traits.some((id) => rules.traits.find((t) => t.id === id).excludes.some(has));
49
+ return inRange && !excluded && this.pool() >= 0;
50
+ },
51
+ derived() {
52
+ const out = {};
53
+ for (const rule of rules.derived) {
54
+ let value = rule.base;
55
+ for (const [attribute, factor] of Object.entries(rule.per)) value += factor * attributes[attribute];
56
+ for (const t of rule.traits) if (has(t.trait) && t.add !== void 0) value += t.add;
57
+ for (const t of rule.traits) if (has(t.trait) && t.mul !== void 0) value *= t.mul;
58
+ out[rule.id] = rule.scale === void 0 ? value : value * rule.scale;
59
+ }
60
+ return out;
61
+ },
62
+ personality: options.personality ?? [50, 50, 50, 50, 50],
63
+ memories,
64
+ opinions,
65
+ history,
66
+ record(entry) {
67
+ history.push(entry);
68
+ if (entry.weight === void 0) return;
69
+ memories.push({ id: entry.id, weight: entry.weight, day: entry.day, ...entry.about ? { about: entry.about } : {} });
70
+ while (memories.length > N.memories.max) {
71
+ let drop = 0;
72
+ for (let i = 1; i < memories.length; i++) if (Math.abs(memories[i].weight) < Math.abs(memories[drop].weight)) drop = i;
73
+ memories.splice(drop, 1);
74
+ }
75
+ },
76
+ remembers: (id) => history.some((entry) => entry.id === id),
77
+ opine: (about, delta) => opinions[about] = clampOpinion((opinions[about] ?? 0) + delta),
78
+ decay(days) {
79
+ for (const [about, value] of Object.entries(opinions)) {
80
+ const step = N.opinion.decayPerDay * days;
81
+ opinions[about] = value > 0 ? Math.max(0, value - step) : Math.min(0, value + step);
82
+ }
83
+ },
84
+ stance(about) {
85
+ const value = opinions[about] ?? 0;
86
+ return value >= N.opinion.joins ? "joins" : value < N.opinion.hostile ? "hostile" : "neutral";
87
+ }
88
+ };
89
+ }
90
+ function drawSheet(random, input = {}) {
91
+ const rules = input.ruleset ?? SHEET_RULESET;
92
+ const profession = input.profession && rules.professions.some((p) => p.id === input.profession) ? input.profession : rules.professions[Math.floor(random() * rules.professions.length)].id;
93
+ const sheet = createSheet({ ...input, profession });
94
+ const attributes = sheet.attributes;
95
+ let left = sheet.pool();
96
+ while (left > 0) {
97
+ const open = rules.attributes.filter((a) => attributes[a.id] < rules.pointBuy.max);
98
+ if (open.length === 0) break;
99
+ attributes[open[Math.floor(random() * open.length)].id] += 1;
100
+ left -= 1;
101
+ }
102
+ return sheet;
103
+ }
104
+
105
+ // src/world.ts
106
+ import { createRng } from "@adatechnology/game-characters";
107
+ import { getNamePools, SHEET_RULESET as SHEET_RULESET2 } from "@adatechnology/game-characters-people";
108
+
109
+ // src/data/life.json
110
+ var life_default = {
111
+ sources: {
112
+ file: "specs/001-migracao-entregas-dos-times/sources/sobreviventes__os-vivos-do-vale.js (site/pages/sobreviventes/os-vivos-do-vale.html)",
113
+ states: "LIVE (line 368) for living; life() alive (line 628) for active",
114
+ affinity: "life() aff, line 634: fam 80, ali 60, odio -60, seg 10, medo -20; unknown pairs 10 + ((empathy a + empathy b)/2 - 50)/5",
115
+ clocks: "life() step 1, lines 643-656: heal in 3 + floor(R\xB78) days, a bite turns in 2, the missing come back in 2 + floor(R\xB75) days with chance 0.6; birth with a midwife 3% mother / 5% baby, without 15% / 20%",
116
+ risk: "life() step 2, lines 660-668: base risk by place (REG, default 0.006), exposure by person (EXPO, default 0.3; newcomers 0.05 under 12, else 0.35), \xD7(1 - defence), \xD71.5 under 2 days of food, \xD72 hurt; draw < 0.5 hurt, < 0.8 bitten, < 0.95 dead, else missing",
117
+ bonds: "life() step 3, lines 671-692: from 16 years; drift ((empathy a + empathy c)/2 - 50)/120 + (food < 5 ? -(greed a + greed c)/250 : 0.1) + (R - 0.5)\xB73; quiet 2 days after news; couple from 18, at most 15 years apart, affinity 65, chance 3%, then 90; pregnancy with 8 days of food, mother up to 42, 2%, due in 45 days, no story beat in 20 days; adoption 1%; rivalry at -50; fight at -70 with courage 60, 20%, affinity -40, < 0.6 hurt (heal 3 + floor(R\xB75)), < 0.7 dead, else the less loyal leaves",
118
+ betrayal: "life() lines 694-697: loyalty < 35, greed > 60, food < 5, 3%; takes 4 days of food; affinity -40 with everyone of the base",
119
+ food: "life() step 4, line 701: each day -0.11 (by option per base), between 0 and 60",
120
+ caravan: "lines 703-708: a base under 5 days of food, a host base over 12, someone there with affinity 55, 12%; 5 days of food, affinity +10; 8% the carrier is hurt (heal in 4)",
121
+ raid: "lines 709-718: 3% a day, not within 10 days of the last, targets with 3 days of food, the weakest defence first with \xB10.15 of noise; up to 5 days of food; a hero with courage 70 and affinity 60 arrives with chance 50%; else 25% dead; heal in 5; affinity +20",
122
+ arrivals: "step 5, lines 721-737: every 5 days, chance 0.5 (0.25 after day 60) of 1 + (30% 1 more); population cap 104 (CAP), 8 per base (ROOM); first names unique up to 30; age 18 + floor(R\xB742); personality 20 + R\xB770 (greed 10 + R\xB770); the bond from 16 years; affinity 75; 20% a partner (age max(18, age + floor(R\xB79) - 4)), 35% pregnant if up to 42, due in 10 + floor(R\xB730)",
123
+ fill: "simulate() O.fill, lines 746-749: up to 8 per base, age 18 + floor(R\xB740), personality 20 + R\xB770",
124
+ infection: "the bitten band and the turn are the apocalypse layer: off by default (normal world first), the bitten band then hurts"
125
+ },
126
+ unknownAge: 8,
127
+ livingStates: ["ok", "hurt", "party", "hostile", "moved"],
128
+ activeStates: ["ok", "hurt", "moved", "hostile"],
129
+ affinity: { family: 80, trust: 60, hatred: -60, secret: 10, fear: -20 },
130
+ rivalKind: "hatred",
131
+ unknownAffinity: { base: 10, divisor: 5 },
132
+ clocks: { healBase: 3, healSpread: 8, turnDays: 2, backBase: 2, backSpread: 5, backChance: 0.6 },
133
+ birth: { motherWithMidwife: 0.03, motherAlone: 0.15, babyWithMidwife: 0.05, babyAlone: 0.2, girl: 0.5 },
134
+ risk: {
135
+ defaultPlace: 6e-3,
136
+ defaultExposure: 0.3,
137
+ newcomerChild: 0.05,
138
+ newcomerAdult: 0.35,
139
+ childAge: 12,
140
+ hungryBelow: 2,
141
+ hungry: 1.5,
142
+ hurt: 2,
143
+ bands: { hurt: 0.5, bitten: 0.8, dead: 0.95 }
144
+ },
145
+ bonds: {
146
+ minAge: 16,
147
+ quietDays: 2,
148
+ driftEmpathy: 120,
149
+ lowFood: 5,
150
+ driftGreed: 250,
151
+ driftFed: 0.1,
152
+ driftNoise: 3,
153
+ coupleAge: 18,
154
+ coupleGap: 15,
155
+ coupleAffinity: 65,
156
+ coupleChance: 0.03,
157
+ coupledAffinity: 90,
158
+ pregnancyFood: 8,
159
+ pregnancyAge: 42,
160
+ pregnancyChance: 0.02,
161
+ pregnancyDays: 45,
162
+ pregnancyStoryWindow: 20,
163
+ adoptionChance: 0.01,
164
+ rivalryAt: -50,
165
+ fightAt: -70,
166
+ fightCourage: 60,
167
+ fightChance: 0.2,
168
+ afterFight: -40,
169
+ fightHurt: 0.6,
170
+ fightDead: 0.7,
171
+ fightHealSpread: 5
172
+ },
173
+ betrayal: { loyaltyBelow: 35, greedAbove: 60, foodBelow: 5, chance: 0.03, takes: 4, affinity: -40 },
174
+ food: { drift: -0.11, max: 60 },
175
+ caravan: { below: 5, hostAbove: 12, affinity: 55, chance: 0.12, carries: 5, bond: 10, hurtChance: 0.08, healDays: 4 },
176
+ raid: { chance: 0.03, cooldown: 10, targetFood: 3, noise: 0.3, loot: 5, heroCourage: 70, heroAffinity: 60, heroChance: 0.5, deadChance: 0.25, healDays: 5, heroBond: 20 },
177
+ arrivals: {
178
+ every: 5,
179
+ chance: 0.5,
180
+ lateChance: 0.25,
181
+ lateAfter: 60,
182
+ secondChance: 0.3,
183
+ cap: 104,
184
+ room: 8,
185
+ uniqueNames: 30,
186
+ ageBase: 18,
187
+ ageSpread: 42,
188
+ bondAge: 16,
189
+ bondAffinity: 75,
190
+ pairChance: 0.2,
191
+ pairAgeSpread: 9,
192
+ pairAgeShift: 4,
193
+ pregnantChance: 0.35,
194
+ pregnantDueBase: 10,
195
+ pregnantDueSpread: 30
196
+ },
197
+ fill: { perBase: 8, ageBase: 18, ageSpread: 40 },
198
+ personality: { base: 20, spread: 70, greedBase: 10 },
199
+ infection: false
200
+ };
201
+
202
+ // src/casting.ts
203
+ var fits = (person, role) => {
204
+ const needs = role.needs;
205
+ if (!needs) return true;
206
+ if (needs.sex && Boolean(person.female) !== (needs.sex === "f")) return false;
207
+ if (needs.age && (person.age === void 0 || person.age < needs.age[0] || person.age > needs.age[1])) return false;
208
+ if (needs.profession && (!person.profession || !needs.profession.includes(person.profession))) return false;
209
+ if (needs.base && person.base !== needs.base) return false;
210
+ if (needs.tags && !needs.tags.every((tag) => person.tags?.includes(tag))) return false;
211
+ return true;
212
+ };
213
+ function castStories(options) {
214
+ const R = options.random;
215
+ const pick = (list) => list[Math.floor(R() * list.length)];
216
+ const people = [...options.people ?? []];
217
+ const byId = new Map(people.map((person) => [person.id, person]));
218
+ const roles = {};
219
+ const made = /* @__PURE__ */ new Map();
220
+ const taken = /* @__PURE__ */ new Set();
221
+ const Q = life_default.personality;
222
+ const A = life_default.arrivals;
223
+ for (const story of options.stories) {
224
+ for (const role of story.roles) {
225
+ if (roles[role.id]) continue;
226
+ if (role.person) {
227
+ if (!byId.has(role.person)) throw new Error(`game-stories: role "${role.id}" of "${story.id}" names "${role.person}", who is not among the people`);
228
+ roles[role.id] = role.person;
229
+ taken.add(role.person);
230
+ continue;
231
+ }
232
+ const candidates = role.create ? [] : people.filter((person2) => !taken.has(person2.id) && !person2.extra && fits(person2, role));
233
+ if (candidates.length > 0) {
234
+ const chosen = pick(candidates);
235
+ roles[role.id] = chosen.id;
236
+ taken.add(chosen.id);
237
+ continue;
238
+ }
239
+ const bond = role.bond ? byId.get(roles[role.bond.role] ?? "") : void 0;
240
+ const female = role.needs?.sex ? role.needs.sex === "f" : R() < 0.5;
241
+ const first = pick(female ? options.names.female : options.names.male);
242
+ const surname = role.bond?.sameSurname && bond ? bond.name.split(" ").filter((x) => !/[“”]/.test(x)).slice(-1)[0] : pick(options.names.surnames);
243
+ const range = role.needs?.age;
244
+ const age = range ? range[0] + Math.floor(R() * (range[1] - range[0] + 1)) : A.ageBase + Math.floor(R() * A.ageSpread);
245
+ const base = role.needs?.base ?? (role.bond?.sameBase !== false && bond ? bond.base : options.defaultBase ?? people[0]?.base ?? "nomad");
246
+ const personality = [Q.base + R() * Q.spread, Q.base + R() * Q.spread, Q.base + R() * Q.spread, Q.greedBase + R() * Q.spread, Q.base + R() * Q.spread].map(Math.round);
247
+ const person = {
248
+ id: role.id,
249
+ name: `${first} ${surname}`,
250
+ age,
251
+ female,
252
+ base,
253
+ personality,
254
+ role: role.id,
255
+ generated: true,
256
+ ...role.needs?.profession?.length ? { profession: pick(role.needs.profession) } : {},
257
+ ...role.tags ? { tags: role.tags } : {},
258
+ ...bond ? { bond: bond.id } : {},
259
+ ...role.bond?.relation ? { relation: role.bond.relation } : {}
260
+ };
261
+ if (byId.has(person.id)) throw new Error(`game-stories: role "${role.id}" would make a person with the id of someone given`);
262
+ people.push(person);
263
+ byId.set(person.id, person);
264
+ roles[role.id] = person.id;
265
+ taken.add(person.id);
266
+ made.set(person.id, role.id);
267
+ }
268
+ }
269
+ const firstRole = /* @__PURE__ */ new Map();
270
+ for (const [role, id] of Object.entries(roles)) if (!firstRole.has(id)) firstRole.set(id, role);
271
+ return { roles, people, roleOf: (id) => made.get(id) ?? firstRole.get(id) };
272
+ }
273
+
274
+ // src/chronicle.ts
275
+ function createChronicle() {
276
+ const entries = [];
277
+ const listeners = /* @__PURE__ */ new Set();
278
+ return {
279
+ entries,
280
+ add(entry) {
281
+ entries.push(entry);
282
+ for (const listener of listeners) listener(entry);
283
+ },
284
+ onDay: (day) => entries.filter((entry) => entry.day === day),
285
+ about: (id) => entries.filter((entry) => entry.people.includes(id) || entry.changes.some((change) => change.id === id)),
286
+ until: (day) => entries.filter((entry) => entry.day <= day),
287
+ on(listener) {
288
+ listeners.add(listener);
289
+ return () => void listeners.delete(listener);
290
+ }
291
+ };
292
+ }
293
+
294
+ // src/data/life-text.json
295
+ var life_text_default = {
296
+ sources: {
297
+ file: "specs/001-migracao-entregas-dos-times/sources/sobreviventes__os-vivos-do-vale.js (site/pages/sobreviventes/os-vivos-do-vale.html), life(), lines 619-739",
298
+ "pt-BR": "the team's texts, verbatim; {name}, {a}, {b}... are the people's short names (NICK, short), {o} the gender ending (o(id))",
299
+ en: "translation of the pt-BR texts"
300
+ },
301
+ "pt-BR": {
302
+ titles: {
303
+ turn: "Reanima",
304
+ back: "Volta",
305
+ lost: "Sem volta",
306
+ birth: "Nascimento",
307
+ risk: "Risco",
308
+ bond: "La\xE7o",
309
+ pregnancy: "Gravidez",
310
+ adoption: "Ado\xE7\xE3o",
311
+ rivalry: "Rixa",
312
+ fight: "Briga",
313
+ betrayal: "Trai\xE7\xE3o",
314
+ caravan: "Entre bases",
315
+ raid: "Ataque",
316
+ arrival: "Chegada"
317
+ },
318
+ notes: {
319
+ healed: "curado",
320
+ undead_street: "zumbi nomeado na rua",
321
+ undead_near: "zumbi nomeado perto de {place}",
322
+ came_back: "voltou depois de sumir",
323
+ came_back_short: "voltou",
324
+ never_back: "nunca voltou",
325
+ newborn: "rec\xE9m-nascido",
326
+ childbirth: "no parto",
327
+ hurt_out: "ferido fora da base",
328
+ bitten_out: "mordido fora da base",
329
+ hides_bite: "esconde a mordida",
330
+ dead_out: "morto fora da base",
331
+ did_not_return: "n\xE3o voltou",
332
+ missing_out: "sumiu numa sa\xEDda",
333
+ missing_short: "sumiu",
334
+ fight_with: "briga com {other}",
335
+ fight: "briga",
336
+ dead_fight: "morto numa briga com {other}",
337
+ left_after_fight: "foi embora depois da briga",
338
+ betrayed: "roubou {from} e foi para {to}",
339
+ betrayed_short: "traiu a base",
340
+ hurt_caravan: "ferido na caravana",
341
+ caravan: "caravana",
342
+ hurt_raid: "ferido no ataque",
343
+ dead_raid: "morto no ataque de {from}",
344
+ raid: "ataque",
345
+ arrived: "chegou{origin}",
346
+ pair_of: "par de {first}"
347
+ },
348
+ lines: {
349
+ loses: " {partner} perde {name}.",
350
+ turn: "{name} reanima e passa a vagar pela rotina antiga.",
351
+ back: "{name} aparece no port\xE3o, magro e sem as botas, depois de dias sumido.",
352
+ lost: "Acham as coisas de {name} numa vala da estrada.{loss}",
353
+ birth: "{name} d\xE1 \xE0 luz em {place}{help}.",
354
+ birth_help: " com ajuda de quem sabe",
355
+ birth_alone: ", sem ningu\xE9m que saiba fazer parto",
356
+ baby_dies: " O beb\xEA n\xE3o respira. Enterram ao lado da base.",
357
+ baby_born: " Nasce {baby}.",
358
+ mother_dies: "{loss} {name} n\xE3o resiste ao parto.",
359
+ risk_hurt: "{name} volta ferid{o} de uma sa\xEDda perto de {place}.",
360
+ risk_bitten: "{name} volta com uma mordida no antebra\xE7o e n\xE3o conta a ningu\xE9m.",
361
+ risk_dead: "{name} n\xE3o volta de uma sa\xEDda perto de {place}.{loss}",
362
+ risk_missing: "{name} some numa sa\xEDda perto de {place}.",
363
+ couple: "{a} e {b} {line}.",
364
+ pregnancy: "{a} e {b} v\xE3o ter um beb\xEA. Nasce em uns {days} dias de jogo.",
365
+ adoption: "{a} e {b} adotam {child}.",
366
+ rivalry: "{a} e {b} {line}. Viraram rivais.",
367
+ fight_hurt: "A rixa explode: {winner} quebra o nariz de {loser} no p\xE1tio.",
368
+ fight_dead: "A rixa termina com faca: {loser} morre e {winner} carrega a morte para o resto da vida.{loss}",
369
+ fight_leaves: "{name} junta as coisas e vai para {to} depois da briga com {other}.",
370
+ betrayal: "{name} some de madrugada com a comida da despensa e o r\xE1dio de {from}. Dias depois \xE9 visto em {to}.",
371
+ caravan: "Caravana: {carrier} atravessa a cidade com comida de {from} para {to}, onde mora {host}.",
372
+ caravan_hurt: " Na volta, {carrier} \xE9 atacad{o} na avenida e chega ferid{o}.",
373
+ raid: "{who} atacam {target} de madrugada e levam {loot} dias de comida.",
374
+ raid_hero: " {hero}, de {heroBase}, ouve os tiros e chega a tempo: {victim} sai s\xF3 ferid{o}.",
375
+ raid_dead: "{loss} {victim} morre na porta.",
376
+ raid_grazed: " {victim} leva um tiro de rasp\xE3o.",
377
+ arrival: "{first}, {relation} de {bond}, chega{origin}{note}.",
378
+ arrival_gone: " Vinha procurando {bond} e descobre que {bond} {fate}. Fica em {base}.",
379
+ arrival_party: " Pergunta por {bond} e descobre que est\xE1 com o jogador. Fica em {base} esperando.",
380
+ arrival_found: " Encontra {bond} em {base} e fica.",
381
+ arrival_elsewhere: " Descobre que {bond} est\xE1 em {where} e prefere ficar em {base}.",
382
+ arrival_pair_pregnant: " Vem com {pair}, e {mother} est\xE1 gr\xE1vida.",
383
+ arrival_pair: " Vem com {pair}."
384
+ },
385
+ pools: {
386
+ couple: ["come\xE7am a dividir o turno de vigia, e depois o colch\xE3o", "se beijam no telhado durante a r\xE1dio das 20h", "assumem o namoro na frente da base inteira"],
387
+ rivalry: ["brigam pela \xFAltima lata de sardinha", "discutem aos gritos sobre quem dorme perto da porta", "se acusam de roubar ra\xE7\xE3o"]
388
+ },
389
+ fates: { dead: "morreu", undead: "virou um dos mortos", left: "saiu do vale no \xF4nibus", missing: "sumiu", bitten: "foi mordido", other: "n\xE3o est\xE1 mais aqui" },
390
+ places: { where_died: "onde morreu", road: "a estrada", open_field: "campo aberto", nowhere: "lugar nenhum" },
391
+ gender: { female: "a", male: "o" },
392
+ honorifics: ["Dona", "Seu", "Dr.", "Tenente", "Sargento", "Soldado", "Pastora", "Cabo", "Capit\xE3o", "Major"]
393
+ },
394
+ en: {
395
+ titles: {
396
+ turn: "Turns",
397
+ back: "Back",
398
+ lost: "Never back",
399
+ birth: "Birth",
400
+ risk: "Risk",
401
+ bond: "Bond",
402
+ pregnancy: "Pregnancy",
403
+ adoption: "Adoption",
404
+ rivalry: "Rivalry",
405
+ fight: "Fight",
406
+ betrayal: "Betrayal",
407
+ caravan: "Between bases",
408
+ raid: "Raid",
409
+ arrival: "Arrival"
410
+ },
411
+ notes: {
412
+ healed: "healed",
413
+ undead_street: "named undead in the street",
414
+ undead_near: "named undead near {place}",
415
+ came_back: "came back after going missing",
416
+ came_back_short: "came back",
417
+ never_back: "never came back",
418
+ newborn: "newborn",
419
+ childbirth: "in childbirth",
420
+ hurt_out: "hurt outside the base",
421
+ bitten_out: "bitten outside the base",
422
+ hides_bite: "hides the bite",
423
+ dead_out: "dead outside the base",
424
+ did_not_return: "did not return",
425
+ missing_out: "went missing on a run",
426
+ missing_short: "missing",
427
+ fight_with: "fight with {other}",
428
+ fight: "fight",
429
+ dead_fight: "killed in a fight with {other}",
430
+ left_after_fight: "left after the fight",
431
+ betrayed: "robbed {from} and went to {to}",
432
+ betrayed_short: "betrayed the base",
433
+ hurt_caravan: "hurt on the caravan",
434
+ caravan: "caravan",
435
+ hurt_raid: "hurt in the raid",
436
+ dead_raid: "killed in the raid of {from}",
437
+ raid: "raid",
438
+ arrived: "arrived{origin}",
439
+ pair_of: "partner of {first}"
440
+ },
441
+ lines: {
442
+ loses: " {partner} loses {name}.",
443
+ turn: "{name} comes back and wanders the old routine.",
444
+ back: "{name} shows up at the gate, thin and without boots, after days missing.",
445
+ lost: "They find {name}'s things in a ditch by the road.{loss}",
446
+ birth: "{name} gives birth at {place}{help}.",
447
+ birth_help: " with help from someone who knows how",
448
+ birth_alone: ", with no one who knows how to deliver a baby",
449
+ baby_dies: " The baby does not breathe. They bury it beside the base.",
450
+ baby_born: " {baby} is born.",
451
+ mother_dies: "{loss} {name} does not survive the birth.",
452
+ risk_hurt: "{name} comes back hurt from a run near {place}.",
453
+ risk_bitten: "{name} comes back with a bite on the forearm and tells no one.",
454
+ risk_dead: "{name} does not come back from a run near {place}.{loss}",
455
+ risk_missing: "{name} goes missing on a run near {place}.",
456
+ couple: "{a} and {b} {line}.",
457
+ pregnancy: "{a} and {b} are going to have a baby. Due in about {days} days.",
458
+ adoption: "{a} and {b} adopt {child}.",
459
+ rivalry: "{a} and {b} {line}. They are rivals now.",
460
+ fight_hurt: "The rivalry blows up: {winner} breaks {loser}'s nose in the yard.",
461
+ fight_dead: "The rivalry ends with a knife: {loser} dies and {winner} carries that death for the rest of their life.{loss}",
462
+ fight_leaves: "{name} packs up and goes to {to} after the fight with {other}.",
463
+ betrayal: "{name} vanishes before dawn with the pantry's food and the radio of {from}. Days later they are seen at {to}.",
464
+ caravan: "Caravan: {carrier} crosses town with food from {from} to {to}, where {host} lives.",
465
+ caravan_hurt: " On the way back, {carrier} is attacked on the avenue and arrives hurt.",
466
+ raid: "{who} raid {target} before dawn and take {loot} days of food.",
467
+ raid_hero: " {hero}, from {heroBase}, hears the shots and arrives in time: {victim} is only hurt.",
468
+ raid_dead: "{loss} {victim} dies at the door.",
469
+ raid_grazed: " {victim} is grazed by a bullet.",
470
+ arrival: "{first}, {relation} of {bond}, arrives{origin}{note}.",
471
+ arrival_gone: " They were looking for {bond} and learn that {bond} {fate}. They stay at {base}.",
472
+ arrival_party: " They ask for {bond} and learn that they are with the player. They stay at {base}, waiting.",
473
+ arrival_found: " They find {bond} at {base} and stay.",
474
+ arrival_elsewhere: " They learn that {bond} is at {where} and would rather stay at {base}.",
475
+ arrival_pair_pregnant: " They come with {pair}, and {mother} is pregnant.",
476
+ arrival_pair: " They come with {pair}."
477
+ },
478
+ pools: {
479
+ couple: ["start sharing the watch, and then the mattress", "kiss on the roof during the 8 pm radio", "go public in front of the whole base"],
480
+ rivalry: ["fight over the last tin of sardines", "shout about who sleeps by the door", "accuse each other of stealing rations"]
481
+ },
482
+ fates: { dead: "died", undead: "became one of the dead", left: "left on the bus", missing: "went missing", bitten: "was bitten", other: "is not here any more" },
483
+ places: { where_died: "where they died", road: "the road", open_field: "the open field", nowhere: "nowhere" },
484
+ gender: { female: "", male: "" },
485
+ honorifics: ["Mrs.", "Mr.", "Dr.", "Lieutenant", "Sergeant", "Private", "Pastor", "Corporal", "Captain", "Major"]
486
+ }
487
+ };
488
+
489
+ // src/conditions.ts
490
+ var stateOf = (context, role) => context.states[context.cast(role)]?.state;
491
+ function holds(condition, context) {
492
+ if ("all" in condition) return condition.all.every((c) => holds(c, context));
493
+ if ("any" in condition) return condition.any.some((c) => holds(c, context));
494
+ if ("not" in condition) return !holds(condition.not, context);
495
+ if ("action" in condition) return Boolean(context.actions[condition.action]);
496
+ if ("flag" in condition) return Boolean(context.flags[condition.flag]);
497
+ if ("state" in condition) {
498
+ const state = stateOf(context, condition.state);
499
+ return state !== void 0 && (typeof condition.is === "string" ? state === condition.is : condition.is.includes(state));
500
+ }
501
+ if ("living" in condition) {
502
+ const state = stateOf(context, condition.living);
503
+ return state !== void 0 && context.living.includes(state);
504
+ }
505
+ if ("base" in condition) return context.states[context.cast(condition.base)]?.base === condition.is;
506
+ if ("value" in condition) return Boolean(context.values?.[condition.value]);
507
+ if ("self" in condition) return context.self === context.cast(condition.self);
508
+ if ("changed" in condition) return (context.changes ?? []).some((change) => change.id === context.cast(condition.changed));
509
+ if ("remembers" in condition) return context.remembers?.(context.cast(condition.remembers), condition.entry) ?? false;
510
+ return false;
511
+ }
512
+
513
+ // src/story/apply.ts
514
+ var FINAL_STATES = ["dead", "left"];
515
+ function applyChanges(states, changes, playerBase) {
516
+ for (const { id, state, note, base } of changes ?? []) {
517
+ const current = states[id];
518
+ if (!current) continue;
519
+ if (FINAL_STATES.includes(current.state)) continue;
520
+ if (current.state === "undead" && state !== "dead") continue;
521
+ states[id] = { state, note, base: base || (state === "party" ? playerBase : current.base) };
522
+ }
523
+ }
524
+ function setState(states, id, state, note, base) {
525
+ const current = states[id];
526
+ if (FINAL_STATES.includes(current.state) || current.state === "undead" && state !== "dead") return;
527
+ states[id] = { state, note, base: base || current.base };
528
+ }
529
+
530
+ // src/text.ts
531
+ var fillText = (template, values) => template.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? ""));
532
+ var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
533
+ function shortName(name, honorifics) {
534
+ const quoted = name.match(/“(.*?)”/);
535
+ if (quoted) return quoted[1];
536
+ const prefix = honorifics.length > 0 ? new RegExp(`^(${honorifics.map(escape).join("|")}) `) : null;
537
+ return (prefix ? name.replace(prefix, "") : name).split(" ")[0];
538
+ }
539
+
540
+ // src/life/life.ts
541
+ var toChanges = (rows) => rows.map(([id, state, note, base]) => ({ id, state, note, ...base ? { base } : {} }));
542
+ function createLifeWorld(options, numbers) {
543
+ const W = {
544
+ raid: {},
545
+ food: {},
546
+ defence: {},
547
+ affinity: {},
548
+ partner: {},
549
+ rival: {},
550
+ adopted: {},
551
+ timers: {},
552
+ made: {},
553
+ count: 0,
554
+ lastNews: {},
555
+ stats: { births: 0, arrivals: 0, deaths: 0, couples: 0, fights: 0, betrayals: 0, caravans: 0, raids: 0, pregnancies: 0 },
556
+ usedNames: /* @__PURE__ */ new Set()
557
+ };
558
+ for (const [id, group] of Object.entries(options.groups)) {
559
+ W.food[id] = group.food;
560
+ W.defence[id] = group.defence;
561
+ }
562
+ W.defence[options.playerBase ?? "player"] = 1;
563
+ for (const [a, b, kind] of options.relations ?? []) if (kind === numbers.rivalKind) W.rival[a < b ? `${a}|${b}` : `${b}|${a}`] = 1;
564
+ for (const [a, b] of options.couples ?? []) {
565
+ W.partner[a] = b;
566
+ W.partner[b] = a;
567
+ }
568
+ return W;
569
+ }
570
+ function lifeDay(d, W, host) {
571
+ const { states: S, flags: F, random: R, numbers: N, text: T, options: O } = host;
572
+ const nomad = O.nomadBase ?? "nomad";
573
+ const playerBase = O.playerBase ?? "player";
574
+ const closed = /* @__PURE__ */ new Set([nomad, ...O.closedBases ?? []]);
575
+ const groups = O.groups;
576
+ const P = (id) => host.people[id] ?? W.made[id];
577
+ const age = (id) => P(id).age ?? N.unknownAge;
578
+ const fem = (id) => Boolean(P(id).female);
579
+ const nm = (id) => P(id).nick ?? shortName(P(id).name, T.honorifics);
580
+ const o = (id) => fem(id) ? T.gender.female : T.gender.male;
581
+ const pers = (id) => {
582
+ const p = P(id).personality;
583
+ return p && p.some((v) => v) ? p : [50, 50, 50, 50, 50];
584
+ };
585
+ const groupName = (b, fallback) => b !== void 0 && groups[b] ? groups[b].name : fallback;
586
+ const conditionContext = () => ({ states: S, flags: F, actions: host.actions, cast: (id) => id, living: N.livingStates });
587
+ const anchorDays = (id, from, to) => host.beatDays(id).some((day) => day > from && day <= to);
588
+ const anchor = (id) => anchorDays(id, d, d + 7) || Boolean(W.timers[id]?.due) || (O.anchors ?? []).some((a) => a.id === id && d < a.beforeDay) || Boolean(W.made[id]?.baby);
589
+ const alive = (id) => N.activeStates.includes(S[id].state);
590
+ const ids = Object.keys(S).filter((id) => alive(id) && age(id) > 0);
591
+ const set = (id, state, note, base) => setState(S, id, state, note, base);
592
+ const say = (rule, text, rows, base, people) => {
593
+ host.log({ day: d, kind: "life", id: rule, title: T.titles[rule], text, changes: toChanges(rows), people, ...base ? { base } : {} });
594
+ if (base) W.lastNews[base] = d;
595
+ };
596
+ const kill = (id, note) => {
597
+ set(id, "dead", note);
598
+ W.stats.deaths++;
599
+ const p = W.partner[id];
600
+ if (p && alive(p)) return fillText(T.lines.loses, { partner: nm(p), name: nm(id) });
601
+ return "";
602
+ };
603
+ const cnt = (b) => Object.keys(S).filter((id) => alive(id) && S[id].base === b).length;
604
+ const pop = () => Object.keys(S).filter((id) => alive(id) || S[id].state === "party").length;
605
+ const key = (a, b) => a < b ? `${a}|${b}` : `${b}|${a}`;
606
+ const aff = (a, b) => {
607
+ const k = key(a, b);
608
+ if (W.affinity[k] === void 0) {
609
+ const r = (O.relations ?? []).find((rel) => rel[0] === a && rel[1] === b || rel[0] === b && rel[1] === a);
610
+ W.affinity[k] = r ? N.affinity[r[2]] : N.unknownAffinity.base + ((pers(a)[1] + pers(b)[1]) / 2 - 50) / N.unknownAffinity.divisor;
611
+ }
612
+ return W.affinity[k];
613
+ };
614
+ const addAff = (a, b, v) => {
615
+ const k = key(a, b);
616
+ W.affinity[k] = Math.max(-100, Math.min(100, aff(a, b) + v));
617
+ };
618
+ const pick = (a) => a[Math.floor(R() * a.length)];
619
+ const hostileNow = () => {
620
+ const out = {};
621
+ for (const [b, rule] of Object.entries(O.hostileBases ?? {})) out[b] = rule === true ? true : holds(rule, conditionContext());
622
+ return out;
623
+ };
624
+ const bestBase = () => {
625
+ const H2 = hostileNow();
626
+ const c = Object.keys(groups).filter((b) => !closed.has(b) && !H2[b] && cnt(b) > 0 && cnt(b) < N.arrivals.room);
627
+ c.sort((a, b) => W.food[b] * (0.5 + W.defence[b]) - W.food[a] * (0.5 + W.defence[a]));
628
+ return c.length ? c[Math.floor(R() * Math.min(3, c.length))] : nomad;
629
+ };
630
+ const newPerson = (person) => {
631
+ const id = `g${++W.count}`;
632
+ W.made[id] = { id, generated: true, ...person };
633
+ S[id] = { state: "ok", note: "", base: person.base };
634
+ return id;
635
+ };
636
+ const heal = (spread) => d + N.clocks.healBase + Math.floor(R() * spread);
637
+ for (const { flag, a, b } of O.flagCouples ?? []) {
638
+ if (F[flag] && !W.partner[a] && !W.partner[b]) {
639
+ W.partner[a] = b;
640
+ W.partner[b] = a;
641
+ }
642
+ }
643
+ for (const [id, t] of Object.entries(W.timers)) {
644
+ if (t.heal === d && S[id].state === "hurt") S[id].note = T.notes.healed;
645
+ if (t.heal === d && S[id].state === "hurt") S[id].state = "ok";
646
+ if (t.turn === d && S[id].state === "bitten") {
647
+ const b = S[id].base;
648
+ set(id, "undead", b === nomad ? T.notes.undead_street : fillText(T.notes.undead_near, { place: groupName(b, T.places.where_died) }));
649
+ say("turn", fillText(T.lines.turn, { name: nm(id) }), [[id, "undead", S[id].note]], S[id].base, [id]);
650
+ }
651
+ if (t.back === d && S[id].state === "missing") {
652
+ if (R() < N.clocks.backChance) {
653
+ set(id, "ok", T.notes.came_back);
654
+ say("back", fillText(T.lines.back, { name: nm(id) }), [[id, "ok", T.notes.came_back_short]], null, [id]);
655
+ } else {
656
+ const x = kill(id, T.notes.never_back);
657
+ say("lost", fillText(T.lines.lost, { name: nm(id), loss: x }), [[id, "dead", T.notes.never_back]], null, [id]);
658
+ }
659
+ }
660
+ if (t.due === d) {
661
+ t.due = 0;
662
+ if (!alive(id)) continue;
663
+ const b = S[id].base;
664
+ const med = (O.midwives ?? []).some((m) => S[m] && alive(m) && S[m].base === b);
665
+ const pm = med ? N.birth.motherWithMidwife : N.birth.motherAlone;
666
+ const pb = med ? N.birth.babyWithMidwife : N.birth.babyAlone;
667
+ const dad = t.father;
668
+ const sx = R() < N.birth.girl ? "f" : "m";
669
+ const bn = sx === "f" ? pick(host.names.babyFemale) : pick(host.names.babyMale);
670
+ const momDies = R() < pm && !anchor(id);
671
+ const babyDies = R() < pb;
672
+ let txt = fillText(T.lines.birth, { name: nm(id), place: groupName(b, T.places.open_field), help: med ? T.lines.birth_help : T.lines.birth_alone });
673
+ const ch = [];
674
+ const people = [id];
675
+ if (babyDies) txt += T.lines.baby_dies;
676
+ else {
677
+ const surname = P(id).name.split(" ").filter((x) => !/[“”]/.test(x)).slice(-1)[0];
678
+ const bid = newPerson({ name: `${bn} ${surname}`, age: 0, base: b, female: sx === "f", baby: true, mother: id, ...dad ? { father: dad } : {} });
679
+ W.stats.births++;
680
+ txt += fillText(T.lines.baby_born, { baby: bn });
681
+ ch.push([bid, "ok", T.notes.newborn, b]);
682
+ people.push(bid);
683
+ }
684
+ if (momDies) {
685
+ txt += fillText(T.lines.mother_dies, { loss: kill(id, T.notes.childbirth), name: nm(id) });
686
+ ch.push([id, "dead", T.notes.childbirth]);
687
+ }
688
+ say("birth", txt, ch, b, people);
689
+ }
690
+ }
691
+ for (const id of ids) {
692
+ if (S[id].state === "hurt" && !(W.timers[id] && W.timers[id].heal > d)) {
693
+ W.timers[id] = W.timers[id] || {};
694
+ W.timers[id].heal = heal(N.clocks.healSpread);
695
+ }
696
+ }
697
+ for (const id of ids) {
698
+ if (!alive(id) || anchor(id) || W.made[id] && W.made[id].baby) continue;
699
+ const b = S[id].base;
700
+ if (b === playerBase) continue;
701
+ const ex = W.made[id] ? age(id) < N.risk.childAge ? N.risk.newcomerChild : N.risk.newcomerAdult : O.exposure?.[id] ?? N.risk.defaultExposure;
702
+ let r = (O.placeRisk?.[b] ?? N.risk.defaultPlace) * ex * (1 - (W.defence[b] ?? 0)) * (W.food[b] < N.risk.hungryBelow ? N.risk.hungry : 1) * (S[id].state === "hurt" ? N.risk.hurt : 1);
703
+ for (const [flag, factor] of Object.entries(O.riskFlags ?? {})) r *= F[flag] ? factor : 1;
704
+ if (R() >= r) continue;
705
+ const x = R();
706
+ const where = groupName(b, T.places.road);
707
+ const hurtBand = N.infection ? N.risk.bands.hurt : N.risk.bands.bitten;
708
+ if (x < hurtBand) {
709
+ set(id, "hurt", T.notes.hurt_out);
710
+ W.timers[id] = { ...W.timers[id], heal: heal(N.clocks.healSpread) };
711
+ say("risk", fillText(T.lines.risk_hurt, { name: nm(id), o: o(id), place: where }), [[id, "hurt", T.notes.hurt_out]], null, [id]);
712
+ } else if (x < N.risk.bands.bitten) {
713
+ set(id, "bitten", T.notes.bitten_out);
714
+ W.timers[id] = { ...W.timers[id], turn: d + N.clocks.turnDays };
715
+ say("risk", fillText(T.lines.risk_bitten, { name: nm(id) }), [[id, "bitten", T.notes.hides_bite]], b, [id]);
716
+ } else if (x < N.risk.bands.dead) {
717
+ const t = kill(id, T.notes.dead_out);
718
+ say("risk", fillText(T.lines.risk_dead, { name: nm(id), place: where, loss: t }), [[id, "dead", T.notes.did_not_return]], b, [id]);
719
+ } else {
720
+ set(id, "missing", T.notes.missing_out);
721
+ W.timers[id] = { ...W.timers[id], back: d + N.clocks.backBase + Math.floor(R() * N.clocks.backSpread) };
722
+ say("risk", fillText(T.lines.risk_missing, { name: nm(id), place: where }), [[id, "missing", T.notes.missing_short]], null, [id]);
723
+ }
724
+ }
725
+ const B = N.bonds;
726
+ const by = {};
727
+ for (const id of Object.keys(S)) {
728
+ if (!alive(id) || age(id) < B.minAge) continue;
729
+ const b = S[id].base;
730
+ if (!b || b === playerBase || b === nomad) continue;
731
+ (by[b] = by[b] || []).push(id);
732
+ }
733
+ for (const [b, m] of Object.entries(by)) {
734
+ const quiet = W.lastNews[b] >= d - B.quietDays;
735
+ for (let i = 0; i < m.length; i++) {
736
+ for (let j = i + 1; j < m.length; j++) {
737
+ const a = m[i];
738
+ const c = m[j];
739
+ const pa = pers(a);
740
+ const pc = pers(c);
741
+ addAff(a, c, ((pa[1] + pc[1]) / 2 - 50) / B.driftEmpathy + (W.food[b] < B.lowFood ? -(pa[3] + pc[3]) / B.driftGreed : B.driftFed) + (R() - 0.5) * B.driftNoise);
742
+ const v = aff(a, c);
743
+ const k = key(a, c);
744
+ if (quiet || W.lastNews[b] >= d) continue;
745
+ if (!W.partner[a] && !W.partner[c] && age(a) >= B.coupleAge && age(c) >= B.coupleAge && Math.abs(age(a) - age(c)) <= B.coupleGap && v >= B.coupleAffinity && R() < B.coupleChance) {
746
+ W.partner[a] = c;
747
+ W.partner[c] = a;
748
+ W.affinity[k] = B.coupledAffinity;
749
+ W.stats.couples++;
750
+ say("bond", fillText(T.lines.couple, { a: nm(a), b: nm(c), line: pick(T.pools.couple) }), [], b, [a, c]);
751
+ continue;
752
+ }
753
+ if (W.partner[a] === c && W.food[b] >= B.pregnancyFood) {
754
+ const f = fem(a) && !fem(c) ? a : fem(c) && !fem(a) ? c : null;
755
+ if (f && age(f) <= B.pregnancyAge && !W.timers[f]?.due && !(anchorDays(a, d, d + B.pregnancyStoryWindow) || anchorDays(c, d, d + B.pregnancyStoryWindow)) && R() < B.pregnancyChance) {
756
+ W.timers[f] = { ...W.timers[f], due: d + B.pregnancyDays, father: f === a ? c : a };
757
+ W.stats.pregnancies++;
758
+ say("pregnancy", fillText(T.lines.pregnancy, { a: nm(a), b: nm(c), days: B.pregnancyDays }), [], b, [a, c]);
759
+ continue;
760
+ }
761
+ if (!f && !W.adopted[k] && R() < B.adoptionChance) {
762
+ const orphan = Object.keys(S).find((x) => alive(x) && S[x].base === b && (O.orphans?.[x] !== void 0 || W.made[x]?.baby && !alive(W.made[x].mother)));
763
+ if (orphan) {
764
+ W.adopted[k] = 1;
765
+ say("adoption", fillText(T.lines.adoption, { a: nm(a), b: nm(c), child: O.orphans?.[orphan] ?? nm(orphan) }), [], b, [a, c, orphan]);
766
+ continue;
767
+ }
768
+ }
769
+ }
770
+ if (!W.rival[k] && v <= B.rivalryAt) {
771
+ W.rival[k] = 1;
772
+ say("rivalry", fillText(T.lines.rivalry, { a: nm(a), b: nm(c), line: pick(T.pools.rivalry) }), [], b, [a, c]);
773
+ continue;
774
+ }
775
+ if (W.rival[k] && v <= B.fightAt && Math.max(pa[0], pc[0]) >= B.fightCourage && R() < B.fightChance) {
776
+ W.stats.fights++;
777
+ W.affinity[k] = B.afterFight;
778
+ const x = R();
779
+ const lo = pa[0] < pc[0] ? a : c;
780
+ const hi = lo === a ? c : a;
781
+ if (x < B.fightHurt && !anchor(lo)) {
782
+ set(lo, "hurt", fillText(T.notes.fight_with, { other: nm(hi) }));
783
+ W.timers[lo] = { ...W.timers[lo], heal: heal(B.fightHealSpread) };
784
+ say("fight", fillText(T.lines.fight_hurt, { winner: nm(hi), loser: nm(lo) }), [[lo, "hurt", T.notes.fight]], b, [hi, lo]);
785
+ } else if (x < B.fightDead && !anchor(lo)) {
786
+ const t = kill(lo, fillText(T.notes.dead_fight, { other: nm(hi) }));
787
+ say("fight", fillText(T.lines.fight_dead, { loser: nm(lo), winner: nm(hi), loss: t }), [[lo, "dead", T.notes.fight]], b, [hi, lo]);
788
+ } else {
789
+ const out = pa[2] < pc[2] ? a : c;
790
+ const dest = bestBase();
791
+ if (dest !== b && !anchor(out)) {
792
+ set(out, "moved", T.notes.left_after_fight, dest);
793
+ say("fight", fillText(T.lines.fight_leaves, { name: nm(out), to: groupName(dest, dest), other: nm(out === a ? c : a) }), [[out, "moved", groupName(dest, dest), dest]], b, [out, out === a ? c : a]);
794
+ }
795
+ }
796
+ }
797
+ }
798
+ }
799
+ const V2 = N.betrayal;
800
+ for (const id of m) {
801
+ const p = pers(id);
802
+ if (quiet || W.lastNews[b] >= d || !alive(id) || anchor(id)) continue;
803
+ if (p[2] < V2.loyaltyBelow && p[3] > V2.greedAbove && W.food[b] < V2.foodBelow && R() < V2.chance) {
804
+ const dest = (O.traitorDestinations ?? []).find((t) => t.base !== b && (!t.when || holds(t.when, conditionContext())))?.base ?? nomad;
805
+ W.food[b] = Math.max(0, W.food[b] - V2.takes);
806
+ W.stats.betrayals++;
807
+ set(id, host.people[id] ? "hostile" : "moved", fillText(T.notes.betrayed, { from: groupName(b, b), to: groupName(dest, dest) }), dest);
808
+ for (const other of m) if (other !== id) addAff(id, other, V2.affinity);
809
+ say("betrayal", fillText(T.lines.betrayal, { name: nm(id), from: groupName(b, b), to: groupName(dest, dest) }), [[id, S[id].state, T.notes.betrayed_short, dest]], b, [id]);
810
+ }
811
+ }
812
+ }
813
+ for (const b of Object.keys(W.food)) {
814
+ const rule = O.foodDrift?.[b];
815
+ const drift = rule === void 0 ? N.food.drift : typeof rule === "number" ? rule : F[rule.flag] ? rule.value : N.food.drift;
816
+ W.food[b] = Math.max(0, Math.min(N.food.max, W.food[b] + drift));
817
+ }
818
+ const H = hostileNow();
819
+ const C = N.caravan;
820
+ for (const [b, m] of Object.entries(by)) {
821
+ if (W.food[b] >= C.below || W.lastNews[b] >= d - B.quietDays) continue;
822
+ caravan: for (const x of m) {
823
+ for (const [c, mc] of Object.entries(by)) {
824
+ if (c === b || H[c] || W.food[c] <= C.hostAbove) continue;
825
+ const y = mc.find((member) => aff(x, member) >= C.affinity);
826
+ if (!y || R() >= C.chance) continue;
827
+ W.food[b] += C.carries;
828
+ W.food[c] -= C.carries;
829
+ addAff(x, y, C.bond);
830
+ W.stats.caravans++;
831
+ let t = fillText(T.lines.caravan, { carrier: nm(y), from: groupName(c, c), to: groupName(b, b), host: nm(x) });
832
+ const ch = [];
833
+ if (R() < C.hurtChance && !anchor(y)) {
834
+ set(y, "hurt", T.notes.hurt_caravan);
835
+ W.timers[y] = { ...W.timers[y], heal: d + C.healDays };
836
+ t += fillText(T.lines.caravan_hurt, { carrier: nm(y), o: o(y) });
837
+ ch.push([y, "hurt", T.notes.caravan]);
838
+ }
839
+ say("caravan", t, ch, b, [y, x]);
840
+ break caravan;
841
+ }
842
+ }
843
+ }
844
+ const A = N.raid;
845
+ const raids = (O.raids ?? []).map((raid) => ({ ...raid, on: holds(raid.when, conditionContext()) }));
846
+ for (const { from: src, on, who } of raids) {
847
+ if (!on || R() >= A.chance || (W.raid[src] || 0) > d - A.cooldown) continue;
848
+ const alvo = Object.keys(by).filter((b) => b !== src && !H[b] && !(O.raidImmune ?? []).includes(b) && W.food[b] >= A.targetFood).sort((a, b) => W.defence[a] - W.defence[b] + (R() - 0.5) * A.noise)[0];
849
+ if (!alvo || W.lastNews[alvo] >= d - B.quietDays) continue;
850
+ W.raid[src] = d;
851
+ const v = pick(by[alvo]);
852
+ W.stats.raids++;
853
+ const loot = Math.min(A.loot, W.food[alvo]);
854
+ W.food[alvo] -= loot;
855
+ W.food[src] = (W.food[src] || 0) + loot;
856
+ const hero = Object.entries(by).filter(([b]) => b !== alvo && !H[b]).flatMap(([, mm]) => mm).find((h) => pers(h)[0] >= A.heroCourage && by[alvo].some((other) => aff(h, other) >= A.heroAffinity));
857
+ let t = fillText(T.lines.raid, { who, target: groupName(alvo, alvo), loot: Math.round(loot) });
858
+ const ch = [];
859
+ const people = [v];
860
+ if (hero && R() < A.heroChance) {
861
+ t += fillText(T.lines.raid_hero, { hero: nm(hero), heroBase: groupName(S[hero].base, S[hero].base), victim: nm(v), o: o(v) });
862
+ set(v, "hurt", T.notes.hurt_raid);
863
+ W.timers[v] = { ...W.timers[v], heal: d + A.healDays };
864
+ addAff(hero, v, A.heroBond);
865
+ ch.push([v, "hurt", T.notes.raid]);
866
+ people.push(hero);
867
+ } else if (R() < A.deadChance && !anchor(v)) {
868
+ t += fillText(T.lines.raid_dead, { loss: kill(v, fillText(T.notes.dead_raid, { from: groupName(src, src) })), victim: nm(v) });
869
+ ch.push([v, "dead", T.notes.raid]);
870
+ } else if (!anchor(v)) {
871
+ set(v, "hurt", T.notes.hurt_raid);
872
+ W.timers[v] = { ...W.timers[v], heal: d + A.healDays };
873
+ t += fillText(T.lines.raid_grazed, { victim: nm(v) });
874
+ ch.push([v, "hurt", T.notes.raid]);
875
+ }
876
+ say("raid", t, ch, alvo, people);
877
+ }
878
+ const V = N.arrivals;
879
+ const pool = Object.values(host.people).filter((x) => !x.extra && typeof x.age === "number" && x.age >= V.bondAge);
880
+ if (O.arrivals !== false && d % V.every === 0 && pool.length > 0) {
881
+ const origins = O.arrivals?.origins?.length ? O.arrivals.origins : [""];
882
+ let n = R() < (d > V.lateAfter ? V.lateChance : V.chance) ? 1 + (R() < V.secondChance ? 1 : 0) : 0;
883
+ while (n-- > 0 && pop() < V.cap) {
884
+ const f = R() < 0.5;
885
+ let nome;
886
+ do
887
+ nome = `${pick(f ? host.names.female : host.names.male)} ${pick(host.names.surnames)}`;
888
+ while (W.usedNames.has(nome.split(" ")[0]) && W.usedNames.size < V.uniqueNames);
889
+ const first = nome.split(" ")[0];
890
+ W.usedNames.add(first);
891
+ const origem = pick(origins);
892
+ const bond = pick(pool).id;
893
+ const rel = pick(host.names.kinship[f ? "female" : "male"]);
894
+ const st = S[bond].state;
895
+ const live = alive(bond) || st === "party";
896
+ const bondBase = S[bond].base;
897
+ const b = live && bondBase !== playerBase && bondBase !== nomad && !H[bondBase] && cnt(bondBase) < V.room ? bondBase : bestBase();
898
+ const Q = N.personality;
899
+ const id = newPerson({
900
+ name: nome,
901
+ age: V.ageBase + Math.floor(R() * V.ageSpread),
902
+ base: b,
903
+ female: f,
904
+ personality: [Q.base + R() * Q.spread, Q.base + R() * Q.spread, Q.base + R() * Q.spread, Q.greedBase + R() * Q.spread, Q.base + R() * Q.spread].map(Math.round),
905
+ bond,
906
+ relation: rel
907
+ });
908
+ W.stats.arrivals++;
909
+ const way = origem ? ` ${origem}` : "";
910
+ const note = (O.arrivals ? O.arrivals.notes ?? [] : []).filter((x) => x.origin === origem && holds(x.when, conditionContext())).map((x) => x.text).join("");
911
+ let t = fillText(T.lines.arrival, { first, relation: rel, bond: nm(bond), origin: way, note });
912
+ if (!live) {
913
+ const fate = T.fates[st] ?? T.fates.other;
914
+ t += fillText(T.lines.arrival_gone, { bond: nm(bond), fate, base: groupName(b, b) });
915
+ } else {
916
+ t += st === "party" ? fillText(T.lines.arrival_party, { bond: nm(bond), base: groupName(b, b) }) : b === S[bond].base ? fillText(T.lines.arrival_found, { bond: nm(bond), base: groupName(b, b) }) : fillText(T.lines.arrival_elsewhere, { bond: nm(bond), where: groupName(S[bond].base, T.places.nowhere), base: groupName(b, b) });
917
+ W.affinity[key(id, bond)] = V.bondAffinity;
918
+ }
919
+ for (const a of O.arrivals ? O.arrivals.announcements ?? [] : []) if (holds(a.when, conditionContext())) t += a.text;
920
+ const arrived = fillText(T.notes.arrived, { origin: way });
921
+ const ch = [[id, "ok", arrived, b]];
922
+ const people = [id, bond];
923
+ if (R() < V.pairChance && pop() < V.cap) {
924
+ const pf = !f;
925
+ let n2;
926
+ do
927
+ n2 = pick(pf ? host.names.female : host.names.male);
928
+ while (W.usedNames.has(n2) && W.usedNames.size < V.uniqueNames);
929
+ W.usedNames.add(n2);
930
+ const surname = nome.split(" ").slice(-1)[0];
931
+ const id2 = newPerson({
932
+ name: `${n2} ${surname}`,
933
+ age: Math.max(V.ageBase, W.made[id].age + Math.floor(R() * V.pairAgeSpread) - V.pairAgeShift),
934
+ base: b,
935
+ female: pf,
936
+ personality: [1, 2, 3, 4, 5].map(() => Math.round(Q.base + R() * Q.spread)),
937
+ bond,
938
+ relation: fillText(T.notes.pair_of, { first })
939
+ });
940
+ W.stats.arrivals++;
941
+ W.partner[id] = id2;
942
+ W.partner[id2] = id;
943
+ ch.push([id2, "ok", arrived, b]);
944
+ people.push(id2);
945
+ const mae = pf ? id2 : id;
946
+ if (R() < V.pregnantChance && W.made[mae].age <= B.pregnancyAge) {
947
+ W.timers[mae] = { due: d + V.pregnantDueBase + Math.floor(R() * V.pregnantDueSpread), father: mae === id ? id2 : id };
948
+ W.stats.pregnancies++;
949
+ t += fillText(T.lines.arrival_pair_pregnant, { pair: n2, mother: nm(mae) });
950
+ } else t += fillText(T.lines.arrival_pair, { pair: n2 });
951
+ }
952
+ say("arrival", t, ch, null, people);
953
+ }
954
+ }
955
+ }
956
+ function fillWorld(W, host, target) {
957
+ const { states: S, random: R, numbers: N, options: O } = host;
958
+ const closed = /* @__PURE__ */ new Set([O.nomadBase ?? "nomad", ...O.closedBases ?? []]);
959
+ const bases = Object.keys(O.groups).filter((b) => !closed.has(b));
960
+ const living = () => Object.keys(S).filter((id) => N.livingStates.includes(S[id].state));
961
+ let i = 0;
962
+ let idle = 0;
963
+ while (living().length < target && bases.length > 0) {
964
+ const b = bases[i++ % bases.length];
965
+ if (living().filter((id2) => S[id2].base === b).length >= N.fill.perBase) {
966
+ if (++idle >= bases.length) break;
967
+ continue;
968
+ }
969
+ idle = 0;
970
+ const id = `g${++W.count}`;
971
+ const f = R() < 0.5;
972
+ const first = f ? host.names.female : host.names.male;
973
+ W.made[id] = {
974
+ id,
975
+ generated: true,
976
+ name: `${first[W.count % first.length]} ${host.names.surnames[W.count % host.names.surnames.length]}`,
977
+ age: N.fill.ageBase + Math.floor(R() * N.fill.ageSpread),
978
+ base: b,
979
+ female: f,
980
+ personality: [1, 2, 3, 4, 5].map(() => Math.round(N.personality.base + R() * N.personality.spread))
981
+ };
982
+ S[id] = { state: "ok", note: "", base: b };
983
+ }
984
+ }
985
+
986
+ // src/story/beat.ts
987
+ var resolveChanges = (items, context, out) => {
988
+ for (const item of items) {
989
+ const here = { ...context, changes: out };
990
+ if ("when" in item) {
991
+ if (holds(item.when, here)) resolveChanges(item.changes, context, out);
992
+ continue;
993
+ }
994
+ if ("each" in item) {
995
+ const { roles, cast, where, except } = item.each;
996
+ const ids = roles ? roles.map((role) => context.cast(role)) : cast ? context.castList : [];
997
+ const skip = new Set((except ?? []).map((role) => context.cast(role)));
998
+ for (const id2 of ids) {
999
+ if (skip.has(id2) || !context.states[id2]) continue;
1000
+ if (where && !holds(where, { ...here, self: id2, cast: (role) => role === "self" ? id2 : context.cast(role) })) continue;
1001
+ out.push({ id: id2, state: item.change.state, note: item.change.note, ...item.change.base ? { base: item.change.base } : {} });
1002
+ }
1003
+ continue;
1004
+ }
1005
+ const id = context.cast(item.role);
1006
+ const base = item.base === void 0 ? void 0 : typeof item.base === "string" ? item.base : context.states[context.cast(item.base.baseOf)]?.base;
1007
+ out.push({ id, state: item.state, note: item.note, ...base ? { base } : {} });
1008
+ }
1009
+ };
1010
+ var renderText = (parts, context, changes, join) => {
1011
+ const piece = (part) => {
1012
+ if (typeof part === "string") return part;
1013
+ if ("when" in part) {
1014
+ const chosen = holds(part.when, { ...context, changes }) ? part.text : part.else;
1015
+ if (chosen === void 0) return "";
1016
+ return typeof chosen === "string" ? chosen : renderText(chosen, context, changes, void 0);
1017
+ }
1018
+ if ("value" in part) return context.values?.[part.value] ?? "";
1019
+ if ("count" in part) return String(changes.length);
1020
+ return context.nameOf(context.cast(part.name));
1021
+ };
1022
+ const pieces = parts.map(piece);
1023
+ return join === void 0 ? pieces.join("") : pieces.filter((text) => text !== "").join(join);
1024
+ };
1025
+ function runBeat(beat, context) {
1026
+ if (beat.run) {
1027
+ const code = context.codeBeats?.[beat.run];
1028
+ if (!code) throw new Error(`game-stories: code beat "${beat.run}" of "${beat.id}" is not registered`);
1029
+ return code({ states: context.states, actions: context.actions, flags: context.flags, cast: context.cast });
1030
+ }
1031
+ const values = {};
1032
+ for (const [name, options] of Object.entries(beat.values ?? {})) values[name] = options.find((option) => !option.when || holds(option.when, context))?.value ?? null;
1033
+ const here = { ...context, values };
1034
+ const variant = (beat.variants ?? []).find((v) => !v.when || holds(v.when, here));
1035
+ if (!variant || variant.none) return null;
1036
+ const changes = [];
1037
+ resolveChanges(variant.changes ?? [], here, changes);
1038
+ if (variant.needsChanges && changes.length === 0) return null;
1039
+ return {
1040
+ text: renderText(variant.text ?? [], here, changes, variant.join),
1041
+ ...variant.alt ? { alt: true } : {},
1042
+ ...variant.changes ? { changes } : {},
1043
+ ...variant.flags ? { flags: variant.flags } : {}
1044
+ };
1045
+ }
1046
+
1047
+ // src/world.ts
1048
+ var hashId = (text) => {
1049
+ let hash = 2166136261;
1050
+ for (let i = 0; i < text.length; i++) hash = Math.imul(hash ^ text.charCodeAt(i), 16777619);
1051
+ return hash >>> 0;
1052
+ };
1053
+ var lifeTextOf = (locale, patch) => {
1054
+ const table = life_text_default[locale] ?? life_text_default["pt-BR"];
1055
+ if (!patch) return table;
1056
+ const out = { ...table };
1057
+ for (const [key, value] of Object.entries(patch)) out[key] = Array.isArray(value) ? value : { ...table[key], ...value };
1058
+ return out;
1059
+ };
1060
+ var mergeNumbers = (base, patch) => {
1061
+ if (!patch) return base;
1062
+ const out = { ...base };
1063
+ for (const [key, value] of Object.entries(patch)) {
1064
+ out[key] = value !== null && typeof value === "object" && !Array.isArray(value) ? mergeNumbers(base[key], value) : value;
1065
+ }
1066
+ return out;
1067
+ };
1068
+ var lifeNumbersOf = (patch) => {
1069
+ const { sources: _sources, ...defaults } = life_default;
1070
+ void _sources;
1071
+ return mergeNumbers(defaults, patch);
1072
+ };
1073
+ function createStoryWorld(options = {}) {
1074
+ const seed = options.seed ?? 1;
1075
+ const locale = options.locale ?? "pt-BR";
1076
+ const names = options.names ?? getNamePools(locale);
1077
+ const ruleset = options.ruleset ?? SHEET_RULESET2;
1078
+ const stories = options.stories ?? [];
1079
+ const castRandom = createRng((seed ^ 2654435769) >>> 0);
1080
+ const casting = castStories({ stories, ...options.people ? { people: options.people } : {}, names, random: castRandom, ...options.life?.nomadBase ? { defaultBase: options.life.nomadBase } : {} });
1081
+ const given = Object.fromEntries(casting.people.map((person2) => [person2.id, person2]));
1082
+ const numbers = lifeNumbersOf(options.life?.numbers);
1083
+ const text = lifeTextOf(locale, options.life?.text);
1084
+ const states = {};
1085
+ for (const person2 of casting.people) states[person2.id] = { state: person2.start?.state ?? "ok", note: person2.start?.note ?? "", base: person2.base };
1086
+ const flags = {};
1087
+ const actions = { ...options.actions };
1088
+ const chronicle = createChronicle();
1089
+ const cast = (role) => casting.roles[role] ?? role;
1090
+ const beats = stories.flatMap((story) => story.beats).map((beat, order) => ({ beat, order })).sort((a, b) => a.beat.day - b.beat.day || a.order - b.order).map(({ beat }) => beat);
1091
+ const beatDays = /* @__PURE__ */ new Map();
1092
+ for (const beat of beats) for (const role of beat.roles) (beatDays.get(cast(role)) ?? beatDays.set(cast(role), []).get(cast(role))).push(beat.day);
1093
+ const random = options.random ?? createRng(seed);
1094
+ const life = options.life ? createLifeWorld(options.life, numbers) : null;
1095
+ const sheets = /* @__PURE__ */ new Map();
1096
+ const castList = casting.people.filter((person2) => !person2.extra).map((person2) => person2.id);
1097
+ const person = (id) => given[id] ?? life?.made[id];
1098
+ const nameOf = (id) => {
1099
+ const p = person(id);
1100
+ return p ? p.nick ?? shortName(p.name, text.honorifics) : id;
1101
+ };
1102
+ const sheet = (id) => {
1103
+ let made = sheets.get(id);
1104
+ if (!made) {
1105
+ const p = person(id);
1106
+ made = drawSheet(createRng((seed ^ hashId(id)) >>> 0), { ruleset, ...p?.profession ? { profession: p.profession } : {}, ...p?.personality ? { personality: p.personality } : {} });
1107
+ sheets.set(id, made);
1108
+ }
1109
+ return made;
1110
+ };
1111
+ const living = numbers.livingStates;
1112
+ const playerBase = options.playerBase ?? options.life?.playerBase ?? "player";
1113
+ let day = 0;
1114
+ let next = 0;
1115
+ if (life && options.fill) fillWorld(life, { states, random, names, numbers, options: options.life }, options.fill);
1116
+ const log = (entry) => {
1117
+ chronicle.add(entry);
1118
+ for (const id of entry.people) if (person(id)) sheet(id).record({ kind: entry.kind === "story" ? "story" : "event", id: entry.id, day: entry.day });
1119
+ };
1120
+ const world = {
1121
+ states,
1122
+ flags,
1123
+ actions,
1124
+ casting,
1125
+ chronicle,
1126
+ life,
1127
+ get day() {
1128
+ return day;
1129
+ },
1130
+ person,
1131
+ people: () => [...casting.people, ...Object.values(life?.made ?? {})],
1132
+ nameOf,
1133
+ cast,
1134
+ act(id) {
1135
+ actions[id] = 1;
1136
+ },
1137
+ advance() {
1138
+ day += 1;
1139
+ const d = day;
1140
+ while (next < beats.length && beats[next].day === d) {
1141
+ const beat = beats[next++];
1142
+ const outcome = runBeat(beat, { states, flags, actions, cast, living, castList, nameOf, remembers: (id, entry) => sheet(id).remembers(entry), ...options.codeBeats ? { codeBeats: options.codeBeats } : {} });
1143
+ if (!outcome) continue;
1144
+ applyChanges(states, outcome.changes, playerBase);
1145
+ Object.assign(flags, outcome.flags ?? {});
1146
+ log({ day: d, kind: "story", id: beat.id, title: beat.title, text: outcome.text, ...outcome.alt ? { alt: true } : {}, changes: outcome.changes ?? [], people: beat.roles.map(cast) });
1147
+ }
1148
+ while (next < beats.length && beats[next].day < d) next++;
1149
+ if (life) lifeDay(d, life, { states, flags, actions, people: given, beatDays: (id) => beatDays.get(id) ?? [], random, names, numbers, text, options: options.life, log });
1150
+ },
1151
+ runTo(target) {
1152
+ while (day < target) world.advance();
1153
+ },
1154
+ sheet
1155
+ };
1156
+ return world;
1157
+ }
1158
+ function simulateStories(days, options = {}) {
1159
+ const world = createStoryWorld(options);
1160
+ world.runTo(days);
1161
+ return world;
1162
+ }
1163
+
1164
+ // src/bench/example.ts
1165
+ var content = (t) => ({
1166
+ example: true,
1167
+ days: 30,
1168
+ actions: [
1169
+ { id: "welcome", name: t.welcome },
1170
+ { id: "fix_well", name: t.fixWell }
1171
+ ],
1172
+ stories: [
1173
+ {
1174
+ id: "stranger",
1175
+ example: true,
1176
+ roles: [
1177
+ { id: "host", needs: { age: [30, 70], base: "town" }, create: true },
1178
+ { id: "newcomer", create: true, bond: { role: "host", relation: t.cousin, sameSurname: false, sameBase: true } }
1179
+ ],
1180
+ beats: [
1181
+ { id: "stranger_arrives", day: 3, title: t.stranger, roles: ["newcomer", "host"], variants: [{ text: [{ name: "newcomer" }, t.arrives[0], { name: "host" }, t.arrives[1]] }] },
1182
+ {
1183
+ id: "stranger_stays",
1184
+ day: 7,
1185
+ title: t.stay,
1186
+ roles: ["newcomer", "host"],
1187
+ variants: [
1188
+ { when: { action: "welcome" }, alt: true, text: [{ name: "newcomer" }, t.staysText[0]], changes: [{ role: "newcomer", state: "ok", note: t.stays }] },
1189
+ { text: [{ name: "newcomer" }, t.goesText[0]], changes: [{ role: "newcomer", state: "moved", note: t.goes, base: "nomad" }] }
1190
+ ]
1191
+ }
1192
+ ]
1193
+ },
1194
+ {
1195
+ id: "well",
1196
+ example: true,
1197
+ crosses: ["stranger"],
1198
+ roles: [{ id: "keeper", needs: { base: "farm" }, create: true }],
1199
+ beats: [
1200
+ { id: "well_dry", day: 10, title: t.well, roles: ["keeper"], variants: [{ text: [{ name: "keeper" }, t.dryText[0]], flags: { well_dry: 1 } }] },
1201
+ {
1202
+ id: "well_after",
1203
+ day: 14,
1204
+ title: t.wellDry,
1205
+ roles: ["keeper", "newcomer"],
1206
+ variants: [
1207
+ { when: { all: [{ action: "fix_well" }, { living: "newcomer" }, { state: "newcomer", is: "ok" }] }, alt: true, text: [{ name: "newcomer" }, t.fixedText[0], { name: "keeper" }, t.fixedText[1]], changes: [] },
1208
+ { text: [{ name: "keeper" }, t.carriedText[0]], changes: [{ role: "keeper", state: "hurt", note: t.carried }] }
1209
+ ]
1210
+ }
1211
+ ]
1212
+ }
1213
+ ],
1214
+ life: {
1215
+ groups: { farm: { name: t.farm, defence: 0.5, food: 20 }, town: { name: t.town, defence: 0.4, food: 10 }, nomad: { name: t.nomads, defence: 0, food: 0 } },
1216
+ nomadBase: "nomad"
1217
+ },
1218
+ fill: 14
1219
+ });
1220
+ var STORIES_EXAMPLE = {
1221
+ "pt-BR": content({
1222
+ farm: "S\xEDtio (exemplo)",
1223
+ town: "Vila (exemplo)",
1224
+ nomads: "Estrada",
1225
+ stranger: "Um estranho no port\xE3o",
1226
+ stay: "Fica ou vai",
1227
+ stays: "ficou",
1228
+ goes: "seguiu pela estrada",
1229
+ well: "O po\xE7o secou",
1230
+ wellDry: "Depois do po\xE7o",
1231
+ fixed: "consertou o po\xE7o",
1232
+ carried: "carregou \xE1gua de longe",
1233
+ welcome: "Acolher o estranho",
1234
+ fixWell: "Consertar o po\xE7o",
1235
+ cousin: "primo",
1236
+ arrives: [" bate no port\xE3o de ", " pedindo abrigo por uma noite.", ""],
1237
+ staysText: [" fica e passa a ajudar na horta.", ""],
1238
+ goesText: [" junta as coisas e segue pela estrada.", ""],
1239
+ dryText: [" acorda e o balde volta vazio do po\xE7o.", ""],
1240
+ fixedText: [" desce no po\xE7o com uma corda e destrava a bomba. ", " agradece com um prato de comida.", ""],
1241
+ carriedText: [" passa a semana carregando \xE1gua do rio e machuca as costas.", ""]
1242
+ }),
1243
+ en: content({
1244
+ farm: "Farm (example)",
1245
+ town: "Town (example)",
1246
+ nomads: "Road",
1247
+ stranger: "A stranger at the gate",
1248
+ stay: "Stay or go",
1249
+ stays: "stayed",
1250
+ goes: "went down the road",
1251
+ well: "The well ran dry",
1252
+ wellDry: "After the well",
1253
+ fixed: "fixed the well",
1254
+ carried: "carried water from afar",
1255
+ welcome: "Welcome the stranger",
1256
+ fixWell: "Fix the well",
1257
+ cousin: "cousin",
1258
+ arrives: [" knocks at the gate of ", " asking for a night of shelter.", ""],
1259
+ staysText: [" stays and starts helping in the garden.", ""],
1260
+ goesText: [" packs up and goes down the road.", ""],
1261
+ dryText: [" wakes up and the bucket comes back empty from the well.", ""],
1262
+ fixedText: [" goes down the well on a rope and frees the pump. ", " says thanks with a plate of food.", ""],
1263
+ carriedText: [" spends the week carrying water from the river and hurts their back.", ""]
1264
+ })
1265
+ };
1266
+
1267
+ // src/bench/locale.ts
1268
+ var EN = {
1269
+ group: "Stories",
1270
+ name: "Stories area",
1271
+ controls: { seed: "Seed", day: "Day", life: "Life simulation", infection: "Bites and the turned", person: "Sheet of", next_day: "Next day", reset: "Start again" },
1272
+ hints: {
1273
+ seed: "The world drawn again: who plays each role, their names and every chance of the life simulation.",
1274
+ day: "The world runs to this day; the sheet and the chronicle show it.",
1275
+ life: "Off, only the stories happen; on, people also get hurt, fall in love, have children, fight, betray, trade and arrive.",
1276
+ infection: "The apocalypse layer of the life simulation: a run can end in a bite, and a bite turns in two days. Off by default: a normal world.",
1277
+ person: "Whose sheet the panel shows.",
1278
+ action: "A player action: the stories read it from its day on."
1279
+ },
1280
+ metrics: { day: "Day", people: "People", living: "Living", dead: "Dead", births: "Births", arrivals: "Arrivals", lines: "Chronicle lines", last: "Last line" },
1281
+ scenarios: {
1282
+ determinism: "The same seed, the same chronicle",
1283
+ casting: "Another seed casts other names",
1284
+ normal_world: "A normal world: nobody is bitten",
1285
+ sheet_keeps: "The sheet keeps the stories the person lived",
1286
+ point_buy: "Point buy: 14 points, the profession and traits",
1287
+ crossing: "An action in one story changes another"
1288
+ },
1289
+ sheet: { kicker: "Character sheet", state: "State", base: "Lives at", age: "Age", profession: "Profession", pool: "Points left", lived: "Lived", none: "Nothing yet", day: "Day" },
1290
+ states: { ok: "well", hurt: "hurt", bitten: "bitten", undead: "turned", dead: "dead", missing: "missing", party: "with the player", hostile: "hostile", moved: "moved", left: "left", unborn: "not born yet" },
1291
+ derived: { health: "Max health", stamina: "Stamina", carry: "Max load (kg)", run_speed: "Running speed (m/s)", detection: "Detection radius (m)", noise: "Footstep noise (%)", learning: "Learning speed (%)", composure: "Composure", melee: "Melee damage (%)" }
1292
+ };
1293
+ var PT_BR = {
1294
+ group: "Hist\xF3rias",
1295
+ name: "\xC1rea das hist\xF3rias",
1296
+ controls: { seed: "Semente", day: "Dia", life: "Vida sorteada", infection: "Mordidas e reanimados", person: "Ficha de", next_day: "Pr\xF3ximo dia", reset: "Recome\xE7ar" },
1297
+ hints: {
1298
+ seed: "O mundo sorteado de novo: quem faz cada papel, os nomes e cada chance da vida sorteada.",
1299
+ day: "O mundo corre at\xE9 esse dia; a ficha e a cr\xF4nica mostram.",
1300
+ life: "Desligada, s\xF3 as hist\xF3rias acontecem; ligada, as pessoas tamb\xE9m se ferem, se apaixonam, t\xEAm filhos, brigam, traem, trocam e chegam.",
1301
+ infection: "A camada do apocalipse na vida sorteada: uma sa\xEDda pode acabar em mordida, e a mordida reanima em dois dias. Desligada por padr\xE3o: mundo normal.",
1302
+ person: "De quem \xE9 a ficha no painel.",
1303
+ action: "Uma a\xE7\xE3o do jogador: as hist\xF3rias leem a partir do dia dela."
1304
+ },
1305
+ metrics: { day: "Dia", people: "Pessoas", living: "Vivos", dead: "Mortos", births: "Nascimentos", arrivals: "Chegadas", lines: "Linhas da cr\xF4nica", last: "\xDAltima linha" },
1306
+ scenarios: {
1307
+ determinism: "A mesma semente, a mesma cr\xF4nica",
1308
+ casting: "Outra semente escala outros nomes",
1309
+ normal_world: "Mundo normal: ningu\xE9m \xE9 mordido",
1310
+ sheet_keeps: "A ficha guarda as hist\xF3rias que a pessoa viveu",
1311
+ point_buy: "Compra de pontos: 14 pontos, a profiss\xE3o e os tra\xE7os",
1312
+ crossing: "Uma a\xE7\xE3o numa hist\xF3ria muda outra"
1313
+ },
1314
+ sheet: { kicker: "Ficha do personagem", state: "Estado", base: "Mora em", age: "Idade", profession: "Profiss\xE3o", pool: "Pontos livres", lived: "Viveu", none: "Nada ainda", day: "Dia" },
1315
+ states: { ok: "vivo", hurt: "ferido", bitten: "mordido", undead: "zumbi nomeado", dead: "morto", missing: "sumido", party: "com o jogador", hostile: "hostil", moved: "mudou de base", left: "saiu do vale", unborn: "ainda n\xE3o nasceu" },
1316
+ derived: { health: "Vida m\xE1xima", stamina: "F\xF4lego", carry: "Carga m\xE1xima (kg)", run_speed: "Velocidade de corrida (m/s)", detection: "Raio de detec\xE7\xE3o (m)", noise: "Ru\xEDdo dos passos (%)", learning: "Velocidade de aprendizado (%)", composure: "Compostura", melee: "Dano corpo a corpo (%)" }
1317
+ };
1318
+ var TEXT = { en: EN, "pt-BR": PT_BR };
1319
+ var storiesBenchTextOf = (locale) => TEXT[locale] ?? PT_BR;
1320
+
1321
+ // src/bench/module.ts
1322
+ var defaultProfessionName = (professionId, language) => getProfession(professionId)?.name[language === "en" ? "en" : "pt-BR"];
1323
+ var STORIES_AREA_ID = "stories_area";
1324
+ var MAX_DAYS = 365;
1325
+ var actionControl = (id) => `act_${id.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
1326
+ function createStoriesTestModule(options = {}) {
1327
+ const locale = options.locale ?? "pt-BR";
1328
+ const ruleset = options.ruleset ?? SHEET_RULESET3;
1329
+ const names = options.names ?? getNamePools2(locale);
1330
+ const text = storiesBenchTextOf(locale);
1331
+ let content2 = options.content ?? STORIES_EXAMPLE[locale === "en" ? "en" : "pt-BR"];
1332
+ const settings = { seed: options.seed ?? 1, day: Math.min(30, content2.days ?? 90), life: true, infection: false, person: "" };
1333
+ let actions = {};
1334
+ let host = null;
1335
+ const worldOptions = (seed, withLife, infection, acts) => {
1336
+ const life = withLife && content2.life ? { ...content2.life, numbers: { ...content2.life.numbers, infection } } : void 0;
1337
+ return {
1338
+ names,
1339
+ ruleset,
1340
+ stories: content2.stories,
1341
+ ...content2.people ? { people: content2.people } : {},
1342
+ seed,
1343
+ ...life ? { life } : {},
1344
+ ...life && content2.fill ? { fill: content2.fill } : {},
1345
+ ...content2.playerBase ? { playerBase: content2.playerBase } : {},
1346
+ actions: acts,
1347
+ locale
1348
+ };
1349
+ };
1350
+ const run = (seed, day, withLife = settings.life, infection = settings.infection, acts = actions) => {
1351
+ const made = createStoryWorld(worldOptions(seed, withLife, infection, acts));
1352
+ made.runTo(day);
1353
+ return made;
1354
+ };
1355
+ let world = run(settings.seed, settings.day);
1356
+ const personOptions = () => world.people().filter((p) => !p.extra || world.states[p.id]?.state !== "unborn").map((p) => ({ id: p.id, name: p.name }));
1357
+ const groupName = (base) => content2.life?.groups[base]?.name ?? base;
1358
+ const showSheet = () => {
1359
+ if (!host?.bench?.sheet) return;
1360
+ const id = settings.person && world.person(settings.person) ? settings.person : personOptions()[0]?.id ?? "";
1361
+ const person = world.person(id);
1362
+ if (!person) {
1363
+ host.bench.sheet(null);
1364
+ return;
1365
+ }
1366
+ const sheet = world.sheet(id);
1367
+ const derived = sheet.derived();
1368
+ const state = world.states[id];
1369
+ const lived = world.chronicle.about(id);
1370
+ const card = {
1371
+ kicker: `${text.sheet.kicker} \xB7 ${text.sheet.day} ${world.day}`,
1372
+ title: person.name,
1373
+ ...state?.note ? { body: state.note } : {},
1374
+ facts: [
1375
+ { label: text.sheet.state, value: state ? text.states[state.state] : "" },
1376
+ { label: text.sheet.base, value: state ? groupName(state.base) : "" },
1377
+ ...person.age !== void 0 ? [{ label: text.sheet.age, value: String(person.age) }] : [],
1378
+ { label: text.sheet.profession, value: (options.professionName ?? defaultProfessionName)(sheet.profession, locale === "en" ? "en" : "pt-BR") ?? sheet.profession },
1379
+ { label: text.sheet.pool, value: String(sheet.pool()) },
1380
+ ...Object.entries(derived).map(([key, value]) => ({ label: text.derived[key] ?? key, value: String(Math.round(value * 100) / 100) }))
1381
+ ],
1382
+ steps: lived.length > 0 ? lived.slice(-8).map((entry) => ({ at: `${text.sheet.day} ${entry.day}`, text: `${entry.title}: ${entry.text}` })) : [{ at: "", text: text.sheet.none }]
1383
+ };
1384
+ host.bench.sheet(card);
1385
+ };
1386
+ const rebuild = () => {
1387
+ world = run(settings.seed, settings.day);
1388
+ host?.bench?.patch("person", { options: personOptions(), ...settings.person ? { value: settings.person } : {} });
1389
+ showSheet();
1390
+ };
1391
+ const onChange = (change) => {
1392
+ if (change.kind === "action") {
1393
+ if (change.id === "next_day") {
1394
+ settings.day = Math.min(MAX_DAYS, settings.day + 1);
1395
+ host?.bench?.patch("day", { value: settings.day });
1396
+ } else if (change.id === "reset") actions = {};
1397
+ rebuild();
1398
+ return;
1399
+ }
1400
+ if (change.kind !== "control") return;
1401
+ const { id, value } = change;
1402
+ if (id === "seed" && typeof value === "number") settings.seed = value;
1403
+ else if (id === "day" && typeof value === "number") settings.day = value;
1404
+ else if (id === "life" && typeof value === "boolean") settings.life = value;
1405
+ else if (id === "infection" && typeof value === "boolean") settings.infection = value;
1406
+ else if (id === "person" && typeof value === "string") {
1407
+ settings.person = value;
1408
+ showSheet();
1409
+ return;
1410
+ } else {
1411
+ const act = (content2.actions ?? []).find((a) => actionControl(a.id) === id);
1412
+ if (!act || typeof value !== "boolean") return;
1413
+ if (value) actions = { ...actions, [act.id]: 1 };
1414
+ else {
1415
+ const { [act.id]: _off, ...rest } = actions;
1416
+ void _off;
1417
+ actions = rest;
1418
+ }
1419
+ }
1420
+ rebuild();
1421
+ };
1422
+ const controls = () => {
1423
+ const people = personOptions();
1424
+ return [
1425
+ { kind: "number", id: "seed", name: text.controls.seed, hint: text.hints.seed, min: 1, max: 9999, step: 1, default: settings.seed },
1426
+ { kind: "number", id: "day", name: text.controls.day, hint: text.hints.day, min: 0, max: MAX_DAYS, step: 1, default: settings.day },
1427
+ { kind: "boolean", id: "life", name: text.controls.life, hint: text.hints.life, default: settings.life },
1428
+ { kind: "boolean", id: "infection", name: text.controls.infection, hint: text.hints.infection, default: settings.infection },
1429
+ { kind: "choice", id: "person", name: text.controls.person, hint: text.hints.person, options: people.length > 0 ? people : [{ id: "", name: "-" }], default: people[0]?.id ?? "" },
1430
+ ...(content2.actions ?? []).map((a) => ({ kind: "boolean", id: actionControl(a.id), name: a.name, hint: text.hints.action, default: false })),
1431
+ { kind: "action", id: "next_day", name: text.controls.next_day },
1432
+ { kind: "action", id: "reset", name: text.controls.reset }
1433
+ ];
1434
+ };
1435
+ const count = (states) => Object.values(world.states).filter((s) => states.includes(s.state)).length;
1436
+ const metrics = () => [
1437
+ { id: "current_day", name: text.metrics.day, digits: 0, read: () => world.day },
1438
+ { id: "people", name: text.metrics.people, digits: 0, read: () => world.people().length },
1439
+ { id: "living", name: text.metrics.living, digits: 0, read: () => count(["ok", "hurt", "party", "hostile", "moved"]) },
1440
+ { id: "dead", name: text.metrics.dead, digits: 0, read: () => count(["dead"]) },
1441
+ { id: "births", name: text.metrics.births, digits: 0, read: () => world.life?.stats.births ?? 0 },
1442
+ { id: "arrivals", name: text.metrics.arrivals, digits: 0, read: () => world.life?.stats.arrivals ?? 0 },
1443
+ { id: "lines", name: text.metrics.lines, digits: 0, read: () => world.chronicle.entries.length },
1444
+ { id: "last", name: text.metrics.last, read: () => world.chronicle.entries.at(-1)?.title ?? "" }
1445
+ ];
1446
+ const days = () => content2.days ?? 90;
1447
+ const lines = (made) => made.chronicle.entries.map((e) => `${e.day}|${e.id}|${e.text}`).join("\n");
1448
+ const scenarios = () => [
1449
+ {
1450
+ id: "stories_determinism",
1451
+ name: text.scenarios.determinism,
1452
+ run: ({ expect }) => {
1453
+ const a = lines(run(7, days(), true, false, {}));
1454
+ expect(a === lines(run(7, days(), true, false, {})) && a.length > 0, "the same seed gives the same chronicle");
1455
+ }
1456
+ },
1457
+ {
1458
+ id: "stories_casting",
1459
+ name: text.scenarios.casting,
1460
+ run: ({ expect }) => {
1461
+ const names2 = (seed) => run(seed, 0, false, false, {}).people().map((p) => p.name).join(",");
1462
+ const drawn = new Set([1, 2, 3, 4, 5].map(names2));
1463
+ expect(drawn.size > 1, "five seeds do not all cast the same names", { casts: drawn.size });
1464
+ }
1465
+ },
1466
+ {
1467
+ id: "stories_normal_world",
1468
+ name: text.scenarios.normal_world,
1469
+ run: ({ expect }) => {
1470
+ let bites = 0;
1471
+ for (let seed = 1; seed <= 10; seed++) bites += run(seed, days(), true, false, {}).chronicle.entries.filter((e) => e.kind === "life" && e.changes.some((c) => c.state === "bitten" || c.state === "undead")).length;
1472
+ expect(bites === 0, "ten worlds without the infection layer: no bite, nobody turns", { bites });
1473
+ }
1474
+ },
1475
+ {
1476
+ id: "stories_sheet_keeps",
1477
+ name: text.scenarios.sheet_keeps,
1478
+ run: ({ expect }) => {
1479
+ const made = run(3, days(), false, false, {});
1480
+ const entry = made.chronicle.entries.find((e) => e.kind === "story");
1481
+ const id = entry?.people[0];
1482
+ expect(Boolean(entry && id && made.sheet(id).remembers(entry.id)), "whoever a beat is about keeps it in their sheet", { beat: entry?.id, person: id });
1483
+ }
1484
+ },
1485
+ {
1486
+ id: "stories_point_buy",
1487
+ name: text.scenarios.point_buy,
1488
+ run: ({ expect }) => {
1489
+ const nurse = createSheet({ ruleset, profession: "nurse" });
1490
+ expect(nurse.pool() === 12, "a nurse starts with 14 \u2212 2 = 12 points", { pool: nurse.pool() });
1491
+ const spent = createSheet({ ruleset, profession: "nurse", attributes: { agility: 8 }, traits: ["athlete", "smoker"] });
1492
+ expect(spent.pool() === 6 && spent.valid(), "agility 8 (\u22125), athlete (\u22124), smoker (+3): 6 left", { pool: spent.pool() });
1493
+ const clash = createSheet({ ruleset, traits: ["athlete", "asthmatic"] });
1494
+ expect(!clash.valid(), "athlete and asthmatic exclude each other");
1495
+ }
1496
+ },
1497
+ {
1498
+ id: "stories_crossing",
1499
+ name: text.scenarios.crossing,
1500
+ run: ({ expect }) => {
1501
+ const differ = (content2.actions ?? []).some((a) => lines(run(5, days(), false, false, {})) !== lines(run(5, days(), false, false, { [a.id]: 1 })));
1502
+ expect(differ, "some player action changes the chronicle");
1503
+ }
1504
+ }
1505
+ ];
1506
+ const module = {
1507
+ id: STORIES_AREA_ID,
1508
+ name: text.name,
1509
+ group: text.group,
1510
+ get world() {
1511
+ return world;
1512
+ },
1513
+ load(next) {
1514
+ content2 = next;
1515
+ actions = {};
1516
+ settings.person = "";
1517
+ rebuild();
1518
+ },
1519
+ items: () => [],
1520
+ controls: () => controls(),
1521
+ metrics: () => metrics(),
1522
+ scenarios: () => scenarios(),
1523
+ attach(lent) {
1524
+ host = lent;
1525
+ const off = lent.bench?.onChange(onChange);
1526
+ showSheet();
1527
+ return () => {
1528
+ off?.();
1529
+ host?.bench?.sheet?.(null);
1530
+ host = null;
1531
+ };
1532
+ }
1533
+ };
1534
+ return module;
1535
+ }
1536
+
1537
+ export {
1538
+ life_default,
1539
+ life_text_default,
1540
+ sheet_default,
1541
+ castStories,
1542
+ createChronicle,
1543
+ holds,
1544
+ FINAL_STATES,
1545
+ applyChanges,
1546
+ setState,
1547
+ fillText,
1548
+ shortName,
1549
+ createLifeWorld,
1550
+ lifeDay,
1551
+ fillWorld,
1552
+ runBeat,
1553
+ createSheet,
1554
+ drawSheet,
1555
+ lifeTextOf,
1556
+ lifeNumbersOf,
1557
+ createStoryWorld,
1558
+ simulateStories,
1559
+ STORIES_EXAMPLE,
1560
+ STORIES_AREA_ID,
1561
+ createStoriesTestModule
1562
+ };
1563
+ //# sourceMappingURL=chunk-27E2JRXQ.js.map