@maka/maka-cli 5.181.0 → 5.183.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.
Files changed (28) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +20 -11
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +331 -0
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +14 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +17 -10
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +6 -0
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +6 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +13 -0
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +84 -30
  10. package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +10 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/game.js +51 -40
  12. package/bundle/typescript/src/commands/game/sideQuest/models/device.js +40 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/models/host.js +4 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/models/item.js +6 -0
  15. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +6 -0
  16. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +6 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +44 -17
  18. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +91 -23
  19. package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +30 -4
  20. package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +107 -0
  21. package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-brain.js +84 -0
  22. package/bundle/typescript/src/commands/game/sideQuest/utilities/ic.js +84 -51
  23. package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +5 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +50 -21
  25. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +6 -2
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +5 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/utilities/surveillance.js +4 -0
  28. package/package.json +1 -1
@@ -297,37 +297,37 @@ export function runHostTurn(room, actor, scene) {
297
297
  return ambientPatrol(room, actor);
298
298
  }
299
299
  /**
300
- * ONE COMBAT TURN of the host's ice.
300
+ * THE START OF A HOST'S COMBAT TURN (p.247): "a host can launch one IC
301
+ * program per Combat Turn, at the beginning of each Combat Turn", up to
302
+ * its rating running, never two of a kind. Crashed programs leave the
303
+ * list first and come back the very next turn (p.355-356, "delayed,
304
+ * never destroyed") -- the relaunch takes the turn's one launch.
301
305
  *
302
- * THE TURN MAPPING IS AN ENGINE MAPPING, NOT A RULE, and is labelled so
303
- * deliberately. Canon paces IC in Combat Turns -- one program launched
304
- * per turn, at the beginning of it. sideQuest has no Matrix initiative
305
- * structure to hang that on, so ONE PLAYER ACTION TAKEN INSIDE AN
306
- * ALERTED HOST counts as one Combat Turn. That is the closest structural
307
- * analogue available; it is not something the book says, and it is not
308
- * dressed up as a citation.
306
+ * Launches only; nothing acts here. In play the encounter's newTurn
307
+ * hook calls this (utilities/host-combat.ts) and each program then acts
308
+ * in its own Action Phase (utilities/ic-brain.ts). Returns the actor a
309
+ * launch produced, so the caller can seat it in the fight.
309
310
  */
310
- export function runICTurn(room, actor, scene) {
311
- if (!hostIsHunting(room, actor))
312
- return empty();
313
- const out = empty();
311
+ export function hostLaunchStep(room, scene, out) {
314
312
  const host = room.host;
315
- // CRASHED IC LEAVES, AND COMES BACK (p.355-356): a program whose monitor
316
- // filled since the last turn is off the list now, and the host
317
- // relaunches it the very next turn -- "delayed, never destroyed".
313
+ let launched;
318
314
  if (host) {
319
- // Last turn's crashes come back FIRST (p.355-356: "reactivated the very
320
- // next turn"); what crashes this turn waits for the next call.
315
+ // Crashes first, then the relaunch: a program whose monitor filled
316
+ // during the last turn is off the list at the start of this one and
317
+ // comes straight back -- "reactivated the very next turn" -- taking
318
+ // the turn's one launch.
319
+ for (const kind of crashedIC(host)) {
320
+ crashIC(scene, host, kind);
321
+ out.lines.push(` {red-fg}${kind} shears apart -- crashed. The host is already recompiling it.{/red-fg}`);
322
+ }
321
323
  if (host.relaunchQueue.length > 0 && room.runningIC.length < room.hostRating) {
322
324
  const kind = host.relaunchQueue.shift();
323
325
  room.runningIC.push(kind);
324
326
  if (scene)
325
- spawnIC(scene, host, kind);
327
+ launched = spawnIC(scene, host, kind);
326
328
  out.lines.push(` {red-fg}The host relaunches ${kind}.{/red-fg}`);
327
- }
328
- for (const kind of crashedIC(host)) {
329
- crashIC(scene, host, kind);
330
- out.lines.push(` {red-fg}${kind} shears apart -- crashed. The host is already recompiling it.{/red-fg}`);
329
+ // ONE launch per turn (p.247): the relaunch was it.
330
+ return launched;
331
331
  }
332
332
  }
333
333
  if (room.runningIC.length < room.hostRating) {
@@ -335,26 +335,49 @@ export function runICTurn(room, actor, scene) {
335
335
  if (next) {
336
336
  room.runningIC.push(next.name);
337
337
  if (host && scene)
338
- spawnIC(scene, host, next.name);
338
+ launched = spawnIC(scene, host, next.name) ?? launched;
339
339
  out.lines.push(next.kind === 'patrol'
340
340
  ? ` {red-fg}${next.name} is already walking the host -- it never had to be launched.{/red-fg}`
341
341
  : ` {red-fg}The host launches ${next.name}.{/red-fg}`);
342
342
  }
343
343
  }
344
- // Every running program acts. An IC attack is a Complex Action
345
- // (p.247), which is one program's whole turn.
344
+ return launched;
345
+ }
346
+ /**
347
+ * ONE PROGRAM'S ACTION -- an IC attack is a Complex Action (p.247), which
348
+ * is one program's whole Action Phase. `ice` is the program's own actor
349
+ * when the scene has one: it is what a failed attack damages.
350
+ */
351
+ export function icActs(room, actor, type, out, ice) {
352
+ if (type.kind === 'patrol')
353
+ patrolActs(room, actor, out);
354
+ else if (type.kind === 'probe')
355
+ probeActs(room, actor, type, out, ice);
356
+ else if (type.kind === 'damage')
357
+ damageActs(room, actor, type, out, ice);
358
+ else
359
+ reducerOrConditionalActs(room, actor, type, out, ice);
360
+ }
361
+ /**
362
+ * ONE WHOLE COMBAT TURN of the host's ice, as a single call: the launch
363
+ * step, then every running program acts once. This is the shape the
364
+ * tests drive and the shape the engine used to run after EVERY player
365
+ * command ("one action inside an alerted host = one Combat Turn" -- an
366
+ * engine mapping that was never a rule, and that had four programs
367
+ * hitting a persona for typing "look"). In play the Combat Turn owns the
368
+ * pacing now (utilities/host-combat.ts): the launch at newTurn, each
369
+ * program in its own phase.
370
+ */
371
+ export function runICTurn(room, actor, scene) {
372
+ if (!hostIsHunting(room, actor))
373
+ return empty();
374
+ const out = empty();
375
+ hostLaunchStep(room, scene, out);
346
376
  for (const name of room.runningIC) {
347
377
  const type = IC_TYPES.find(t => t.name === name);
348
378
  if (!type)
349
379
  continue;
350
- if (type.kind === 'patrol')
351
- patrolActs(room, actor, out);
352
- else if (type.kind === 'probe')
353
- probeActs(room, actor, out);
354
- else if (type.kind === 'damage')
355
- damageActs(room, actor, type, out);
356
- else
357
- reducerOrConditionalActs(room, actor, type, out);
380
+ icActs(room, actor, type, out, room.host?.iceActors.get(name));
358
381
  }
359
382
  return out;
360
383
  }
@@ -363,13 +386,29 @@ export function runICTurn(room, actor, scene) {
363
386
  * v. Intuition + Firewall (p.248). Returns the net hits; 0 or less is a
364
387
  * miss. Shared so the four reducers, Track and Scramble cannot drift
365
388
  * from Killer's version of the same roll.
389
+ *
390
+ * A FAILED ATTACK ACTION HURTS THE ATTACKER (p.247 "as with all Attack
391
+ * actions, a failed attack causes damage to the IC"; the amount is the
392
+ * general rule, Data Trails p.181: one box of Matrix damage per net hit
393
+ * the defender got, unresisted). Patrol makes no Attack action and is
394
+ * never here (hurtByFailure false). Applied to the program's own actor,
395
+ * which is its condition monitor (ic-actors.ts); a program crashed this
396
+ * way leaves at the next launch step like any other.
366
397
  */
367
- function icAttack(room, actor, type, out) {
398
+ function icAttack(room, actor, type, out, ice) {
368
399
  const { pool, limit } = icAttackPool(room);
369
400
  const attack = rollPool(pool, limit);
370
401
  const defence = rollPool(icDefencePool(actor));
371
402
  const net = attack.hits - defence.hits;
372
403
  out.meta.push(`${type.name} -- Host Rating x2 [Attack ${limit}]: ${formatRoll(attack)} v. Intuition + Firewall: ${formatRoll(defence)} (net ${net})`);
404
+ if (net < 0 && type.hurtByFailure && ice && !ice.isDown()) {
405
+ const rebound = -net;
406
+ ice.takeDamage(rebound);
407
+ out.meta.push(`${type.name} -- failed Attack action: ${rebound} box${rebound === 1 ? '' : 'es'} back on the program, unresisted (p.247)`);
408
+ out.lines.push(ice.isDown()
409
+ ? ` {red-fg}${ice.name}'s own attack code rejects and tears it apart -- ${rebound} box${rebound === 1 ? '' : 'es'}, and its monitor fills.{/red-fg}`
410
+ : ` ${ice.name} eats its own rejected code -- ${rebound} box${rebound === 1 ? '' : 'es'} (${ice.conditionSummary()}).`);
411
+ }
373
412
  return net;
374
413
  }
375
414
  /**
@@ -389,8 +428,8 @@ function icAttack(room, actor, type, out) {
389
428
  * against the next IC, and Jammer really does lower the ceiling on your
390
429
  * own Brute Force.
391
430
  */
392
- function reducerOrConditionalActs(room, actor, type, out) {
393
- const net = icAttack(room, actor, type, out);
431
+ function reducerOrConditionalActs(room, actor, type, out, ice) {
432
+ const net = icAttack(room, actor, type, out, ice);
394
433
  if (net <= 0) {
395
434
  out.lines.push(` ${type.name} probes at your icon and finds no purchase.`);
396
435
  return;
@@ -467,17 +506,13 @@ function applyMatrixDamage(actor, dealt, type, out) {
467
506
  * Probe IC spends its turns building. That coupling is canon's own, and
468
507
  * it is why Probe is worth a slot on a host that also fields a Killer.
469
508
  *
470
- * A FAILED IC ATTACK SHOULD DAMAGE THE IC (p.247) AND DOES NOT YET,
471
- * stated here rather than left for the next reader to discover. Modelling
472
- * it needs a condition monitor per program AND a way for the player to
473
- * attack ice, and neither exists: there is no cybercombat path against
474
- * IC in this engine at all. Half of it -- ice that hurts itself while
475
- * the player still cannot swing back -- would be worse than neither
476
- * half. It lands with the verb.
509
+ * A FAILED IC ATTACK DAMAGES THE IC (p.247) -- see icAttack, where it
510
+ * lands for every attacking program at once. The player's side of it,
511
+ * cybercombat against ice, is `attack <program>` inside the host.
477
512
  */
478
- function damageActs(room, actor, type, out) {
513
+ function damageActs(room, actor, type, out, ice) {
479
514
  const { limit } = icAttackPool(room);
480
- const net = icAttack(room, actor, type, out);
515
+ const net = icAttack(room, actor, type, out, ice);
481
516
  if (net <= 0) {
482
517
  out.lines.push(` ${type.name} lunges at your icon and closes on nothing.`);
483
518
  return;
@@ -608,12 +643,10 @@ function patrolActs(room, actor, out) {
608
643
  * read in Phase 4. The engine previously modelled the host's side as the
609
644
  * single boolean `hostAlert`, which cannot answer "how many".
610
645
  */
611
- function probeActs(room, actor, out) {
612
- const { pool, limit } = icAttackPool(room);
613
- const attack = rollPool(pool, limit);
614
- const defence = rollPool(icDefencePool(actor));
615
- const net = attack.hits - defence.hits;
616
- out.meta.push(`Probe IC -- Host Rating x2 [Attack ${limit}]: ${formatRoll(attack)} v. Intuition + Firewall: ${formatRoll(defence)} (net ${net})`);
646
+ function probeActs(room, actor, type, out, ice) {
647
+ // The same opposed roll as every attacking program (icAttack), so a
648
+ // failed probe rebounds on the program like any failed Attack action.
649
+ const net = icAttack(room, actor, type, out, ice);
617
650
  const held = room.hostMarksOn.get(actor.name) ?? 0;
618
651
  if (net <= 0) {
619
652
  out.lines.push(` Probe IC tests your edges and finds nothing to hold onto.`);
@@ -1,6 +1,7 @@
1
1
  import { Category } from '../types/shared/item-enum.js';
2
2
  import { inMeleeReach, isInCover, coverAvailableFor, actorDistanceMeters } from './spots.js';
3
3
  import { Logger } from './logger.js';
4
+ import { runIcActionPhase } from './ic-brain.js';
4
5
  /**
5
6
  * AN NPC'S ACTION PHASE, PLAYED BY RULE (SR5 p.163-167).
6
7
  *
@@ -25,6 +26,10 @@ import { Logger } from './logger.js';
25
26
  * Nothing here rolls a die of its own: the verbs do.
26
27
  */
27
28
  export async function runNpcActionPhase(enc, npc) {
29
+ // A HOST'S PROGRAM HAS ITS OWN BRAIN (utilities/ic-brain.ts): no
30
+ // weapon to draw, no ground to close -- one test, by type.
31
+ if (npc.icKind !== undefined)
32
+ return runIcActionPhase(enc, npc);
28
33
  const logger = Logger.getInstance();
29
34
  if (npc.isIncapacitated() || npc.surrendered)
30
35
  return;
@@ -39,36 +39,65 @@ import { mentalLimit } from './limits.js';
39
39
  * an active search mechanically different from a passive glance. */
40
40
  export const ACTIVELY_LOOKING = 3;
41
41
  /**
42
- * ON THE GRID IT IS A DIFFERENT TEST WITH A DIFFERENT BRACKET. Matrix
43
- * Search is Computer + Logic [Data Processing], so the Mental limit does
44
- * NOT apply out there -- capping a decker's sweep by their Willpower
45
- * would be a fresh deviation dressed as a fix. The engine takes the
46
- * best of Perception and Computer so pre-Computer deckers lose nothing
47
- * (a standing allowance, older than this function).
42
+ * THE MEAT PERCEPTION TEST. The Matrix used to ride through here as a
43
+ * "different test with a different bracket" -- Logic + the better of
44
+ * Perception and Computer [Data Processing] -- which was neither Matrix
45
+ * Perception nor Matrix Search. It is matrixSearchTest now, below; the
46
+ * grid never comes through this door.
47
+ */
48
+ export function perceptionTest(actor, opts = {}) {
49
+ const skill = actor.skillRating('perception');
50
+ // Unskilled defaults at -1 (p.130), exactly as both callers already did.
51
+ const pool = Math.max(1, actor.intuition
52
+ + (skill > 0 ? skill : -1)
53
+ + (opts.active ? ACTIVELY_LOOKING : 0)
54
+ + actor.bonus('perception')
55
+ + actor.woundModifier
56
+ - actor.sustainingPenalty);
57
+ return { pool, limit: mentalLimit(actor) };
58
+ }
59
+ /**
60
+ * MATRIX SEARCH (SR5 p.241, RAG-checked 2026-09-13): a Special action,
61
+ * "Computer + Intuition [Data Processing]" -- INTUITION, not Logic; the
62
+ * old grid branch above rolled Logic and called it canon. The threshold
63
+ * and the base time come from what is being looked for (the Matrix
64
+ * Search Table: public 1 / 1 minute, limited 3 / 30 minutes; inside a
65
+ * host the base time is one minute whatever the information), net hits
66
+ * over the threshold divide the time, and a failure still spends all
67
+ * of it. A Browse program "cuts the base time in half" (Splintered
68
+ * State p.54) -- time, not dice. Running silent costs its -2 like every
69
+ * Matrix action (p.235-236).
48
70
  *
49
71
  * Data Processing reads 0 for an actor with no deck at all; that is a
50
72
  * caller's missing gate rather than a real limit, so it degrades to
51
73
  * unlimited here instead of handing rollPool a zero it would reject.
52
74
  */
53
- export function perceptionTest(actor, opts = {}) {
54
- const onGrid = actor.plane === 'matrix';
55
- const skill = onGrid
56
- ? Math.max(actor.skillRating('perception'), actor.skillRating('computer'))
57
- : actor.skillRating('perception');
58
- const attribute = onGrid ? actor.logic : actor.intuition;
59
- // Unskilled defaults at -1 (p.130), exactly as both callers already did.
60
- const pool = Math.max(1, attribute
75
+ export function matrixSearchTest(actor) {
76
+ const skill = actor.skillRating('computer');
77
+ const pool = Math.max(1, actor.intuition
61
78
  + (skill > 0 ? skill : -1)
62
- + (opts.browse ?? 0)
63
- + (opts.active ? ACTIVELY_LOOKING : 0)
64
79
  + actor.bonus('perception')
80
+ + (typeof actor.matrixActionPenalty === 'number' ? actor.matrixActionPenalty : 0)
65
81
  + actor.woundModifier
66
82
  - actor.sustainingPenalty);
67
- const dataProcessing = onGrid ? actor.matrixAttribute('dataProcessing') : 0;
68
- const limit = onGrid
69
- ? (dataProcessing > 0 ? dataProcessing : undefined)
70
- : mentalLimit(actor);
71
- return { pool, limit };
83
+ const dataProcessing = actor.matrixAttribute('dataProcessing');
84
+ return { pool, limit: dataProcessing > 0 ? dataProcessing : undefined };
85
+ }
86
+ /** The Matrix Search Table (p.241-242): threshold and base time in
87
+ * seconds. Inside a host the base time is one minute regardless. */
88
+ export const MATRIX_SEARCH = {
89
+ public: { threshold: 1, seconds: 60 },
90
+ inHost: { threshold: 3, seconds: 60 },
91
+ };
92
+ /** How long a search takes once rolled: the base time divided by the
93
+ * net hits over the threshold, never under one Combat Turn (3s); a
94
+ * failure spends the whole base time. Browse halves the base. */
95
+ export function matrixSearchSeconds(baseSeconds, hits, threshold, browse) {
96
+ const base = browse ? Math.ceil(baseSeconds / 2) : baseSeconds;
97
+ if (hits < threshold)
98
+ return base;
99
+ const net = hits - threshold;
100
+ return net > 0 ? Math.max(3, Math.ceil(base / net)) : base;
72
101
  }
73
102
  /**
74
103
  * PERCEPTION THRESHOLDS (p.136). Named because the numbers appear in
@@ -164,6 +164,8 @@ export function serializeItem(item) {
164
164
  out.loadedAmmo = item.loadedAmmo;
165
165
  if (item.jammed)
166
166
  out.jammed = true;
167
+ if (item.bricked)
168
+ out.bricked = true;
167
169
  if (item.deckDamage > 0)
168
170
  out.deckDamage = item.deckDamage;
169
171
  if (item.droneDamage > 0)
@@ -219,6 +221,8 @@ export function restoreItem(json) {
219
221
  item.loadedAmmo = json.loadedAmmo;
220
222
  if (json.jammed)
221
223
  item.jammed = true;
224
+ if (json.bricked)
225
+ item.bricked = true;
222
226
  if (json.deckDamage)
223
227
  item.takeDeckDamage(json.deckDamage);
224
228
  if (json.droneDamage)
@@ -663,7 +667,7 @@ export function captureHubOverlay(scene, hubSeed, player, homeRoom) {
663
667
  .map(n => n.player.name)
664
668
  .filter(name => !scene.getActor(name));
665
669
  const devices = [...collectDevices(scene).entries()]
666
- .map(([name, d]) => ({ name, open: d.isOpen(), ...(d.barrierDamage > 0 ? { damage: d.barrierDamage } : {}) }));
670
+ .map(([name, d]) => ({ name, open: d.isOpen(), ...(d.barrierDamage > 0 ? { damage: d.barrierDamage } : {}), ...(d.isBricked ? { bricked: true } : {}) }));
667
671
  // THE TOMBSTONE (see ISavedHub.puzzles): always [], never read here.
668
672
  // Omitting it crashes v5.64.0's unguarded for-of on resume, and
669
673
  // saves move between builds via the cloud mirror.
@@ -824,7 +828,7 @@ export function applyHubOverlay(scene, saved, player, homeRoom) {
824
828
  for (const d of saved.devices ?? []) {
825
829
  const live = liveDevices.get(d.name);
826
830
  if (live)
827
- live.restoreState({ open: d.open, damage: d.damage });
831
+ live.restoreState({ open: d.open, damage: d.damage, bricked: d.bricked });
828
832
  }
829
833
  // A SAVE WRITTEN BEFORE DEVICES EXISTED has no `devices` key at all,
830
834
  // so every device would read shut -- while the door it holds stays
@@ -209,6 +209,11 @@ export const DUMPSHOCK_DV = 6;
209
209
  export function leaveMatrix(scene, actor, opts) {
210
210
  const lines = [];
211
211
  const body = actor.bodyRoom;
212
+ // OUT OF THE MATRIX IS OUT OF THE FIGHT (utilities/host-combat.ts): a
213
+ // persona leaving its host arena -- jack out, link-lock broken, deck
214
+ // bricked, Scramble, convergence -- is no longer a participant. Before
215
+ // endPersona clears the position the arena is read off.
216
+ scene.encounterFor?.(actor)?.leave(actor);
212
217
  if (opts?.forced) {
213
218
  lines.push(...applyDumpshock(actor, opts.reason ?? `The connection is severed from the other side.`));
214
219
  }
@@ -85,6 +85,10 @@ export function directConnectionBlocker(actor, room) {
85
85
  export function camerasLive(rooms, room) {
86
86
  if (!isWatched(room))
87
87
  return false;
88
+ // Looped by Control Device or bricked by a Data Spike (commands/
89
+ // disable.ts): the house sees nothing from this cluster either way.
90
+ if (room.camerasLooped || room.camerasBricked)
91
+ return false;
88
92
  if (room.hasNode)
89
93
  return !room.hostCracked;
90
94
  const hosts = Object.values(rooms).filter(r => r.hasNode);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.181.0",
3
+ "version": "5.183.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",