@maka/maka-cli 5.84.0 → 5.85.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 (42) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/game/sideQuest/commands/call.d.ts +0 -28
  3. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +1 -41
  4. package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
  5. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +39 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js +16 -1
  8. package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js.map +1 -1
  9. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +13 -5
  10. package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
  11. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +9 -6
  12. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +14 -0
  14. package/bundle/typescript/src/commands/game/sideQuest/game.js +28 -4
  15. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +11 -0
  17. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +86 -16
  18. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/utilities/bystander.d.ts +17 -0
  20. package/bundle/typescript/src/commands/game/sideQuest/utilities/bystander.js +123 -0
  21. package/bundle/typescript/src/commands/game/sideQuest/utilities/bystander.js.map +1 -0
  22. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.d.ts +116 -0
  23. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +96 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js.map +1 -0
  25. package/bundle/typescript/src/commands/game/sideQuest/utilities/meet-location.d.ts +37 -0
  26. package/bundle/typescript/src/commands/game/sideQuest/utilities/meet-location.js +51 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/utilities/meet-location.js.map +1 -0
  28. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.d.ts +76 -0
  29. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +84 -0
  30. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js.map +1 -0
  31. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.d.ts +70 -0
  32. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +88 -10
  33. package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js.map +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.d.ts +10 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +55 -0
  36. package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js.map +1 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.d.ts +17 -0
  38. package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js +139 -0
  39. package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js.map +1 -0
  40. package/bundle/typescript/src/commands/testCmd/mocha.sub.cmd.js +46 -44
  41. package/bundle/typescript/src/commands/testCmd/mocha.sub.cmd.js.map +1 -1
  42. package/package.json +1 -1
@@ -0,0 +1,116 @@
1
+ import { NPC } from '../models/npc.js';
2
+ import { Room } from '../models/room.js';
3
+ import type { Scene } from '../models/scene.js';
4
+ /**
5
+ * THE EPHEMERAL NPC GOVERNOR (7ALf6u7PvTg6TZSm7, Maka: "an ephemeral NPC
6
+ * AI governer. This would handle non-scene generated NPCs, like
7
+ * authority checking SINs, hostile mobs, neutral npcs that don't impact
8
+ * the storyline/job").
9
+ *
10
+ * THE SPINE OF THE DESIGN: an ephemeral VOICES a result the engine has
11
+ * already computed. It never decides anything. The SIN sweep's officer
12
+ * is a mouth for mechanics that already work -- wandRead returns the
13
+ * band, addHeat bills it, the fake burns -- so the worst a bad brief can
14
+ * do is produce bad dialogue, never a wrong ruling. Every later kind
15
+ * (mobs, bystanders) rides these same rails, and the rails are the
16
+ * reason this is safe to grow.
17
+ *
18
+ * THE EPHEMERAL / CHARACTER BOUNDARY, stated as law:
19
+ * - an ephemeral is NOT IN THE PLOT -- no run, client, objective or
20
+ * payout reaches its brief;
21
+ * - an ephemeral CANNOT CREATE OBLIGATIONS -- no prices, no jobs, no
22
+ * items, no promises the world would have to honor later.
23
+ * A patrolman who starts discussing your Johnson has become a
24
+ * character, and characters have to persist. These two denials are what
25
+ * keep an ephemeral disposable; without them the first interesting
26
+ * conversation mints a debt the save file cannot pay.
27
+ *
28
+ * ONE GOVERNOR. Spawning, despawning and the brief trim all live here,
29
+ * and nothing else in the engine mints an ephemeral. A second spawn
30
+ * site is how a "temporary" NPC ends up outliving the scene.
31
+ *
32
+ * NO SAVE PERSISTENCE -- ephemeral means ephemeral (ruling). The
33
+ * mid-fight-save worry is already answered by the engine rather than by
34
+ * anything here: requestSave's guards only write at safe beats, and a
35
+ * mob fight is not one, so a load can neither resurrect a dead officer
36
+ * nor vanish one mid-swing. See despawn() below, which is where that
37
+ * reasoning has to be true.
38
+ */
39
+ /**
40
+ * How long this body is allowed to exist. ALWAYS EXPLICIT -- an implicit
41
+ * lifetime is how the "temporary" patrolman is still standing there
42
+ * three rooms later.
43
+ *
44
+ * 'exchange' -- for as long as the player is talking to them. A
45
+ * mid-exchange walkout ends the exchange; the officer
46
+ * does not follow through the save file.
47
+ * 'room' -- while the player is in this room.
48
+ * 'fight' -- while the fight lasts. Mob despawn is FIGHT-scoped, not
49
+ * room-scoped, on purpose: walking out of the room is a
50
+ * thing you do DURING a fight, and a mob that evaporated
51
+ * because you backed through a doorway would be a
52
+ * retreat that wins by itself.
53
+ */
54
+ export type EphemeralScope = 'exchange' | 'room' | 'fight';
55
+ export type EphemeralKind = 'authority' | 'mob' | 'bystander';
56
+ export interface IEphemeralMark {
57
+ kind: EphemeralKind;
58
+ until: EphemeralScope;
59
+ /** The room they were spawned into -- what 'room' scope is measured against. */
60
+ roomName: string;
61
+ }
62
+ /** Every ephemeral currently standing, by NPC name. */
63
+ export type EphemeralRegistry = Map<string, IEphemeralMark>;
64
+ /** What the governor needs of the Game, kept narrow so this is testable. */
65
+ export interface IEphemeralHost {
66
+ scene: Scene;
67
+ ephemerals: EphemeralRegistry;
68
+ }
69
+ /**
70
+ * Bring one into the world. Returns the NPC, or null when the name is
71
+ * already taken -- the same refusal spawnCompanionShell makes, and for
72
+ * the same reason: two actors answering to one name is a bug nobody can
73
+ * see until a command picks the wrong one.
74
+ */
75
+ export declare function spawnEphemeral(host: IEphemeralHost, opts: {
76
+ name: string;
77
+ description: string;
78
+ kind: EphemeralKind;
79
+ until: EphemeralScope;
80
+ room: Room;
81
+ combat: Partial<import('../models/player.js').ICombatAttributes>;
82
+ }): NPC | null;
83
+ /**
84
+ * Remove every ephemeral whose scope has ended.
85
+ *
86
+ * WHY NO SAVE HOOK IS NEEDED HERE, which is the ruling's reasoning and
87
+ * has to be true at exactly this line: ephemerals are never written to
88
+ * the save file, so there is nothing to reconcile on load. The obvious
89
+ * worry -- a save landing mid-fight, resurrecting a dead mob or erasing
90
+ * a live one -- is already answered upstream: requestSave's guards only
91
+ * write at safe beats and a fight is not one. So a load never sees a
92
+ * half-finished ephemeral, and this function is the only thing that
93
+ * ever ends one.
94
+ *
95
+ * Returns the names removed, so a caller can say so once.
96
+ */
97
+ export declare function despawnEphemerals(host: IEphemeralHost, opts: {
98
+ /** Where the player is NOW -- 'room'/'exchange' scopes end elsewhere. */
99
+ playerRoomName?: string;
100
+ /** True while a fight is running; 'fight'-scoped bodies stay up. */
101
+ fightActive?: boolean;
102
+ /** Ends everything regardless of scope (scene teardown). */
103
+ all?: boolean;
104
+ }): string[];
105
+ /**
106
+ * The two denials that make an ephemeral disposable, as prompt text.
107
+ *
108
+ * FULL LENGTH, DELIBERATELY. Everything else in an ephemeral's brief is
109
+ * trimmed hard -- no command help, no objective, no payout, no client,
110
+ * no wares, no history -- but the honesty pins are not where the budget
111
+ * gets saved. They are the majority of what remains precisely because
112
+ * the rest went, and trimming a denial is the exact economy that
113
+ * reintroduces the bug the denial exists to prevent. A shorter pin does
114
+ * not produce a shorter answer; it produces a confident wrong one.
115
+ */
116
+ export declare function ephemeralDenials(role: string): string;
@@ -0,0 +1,96 @@
1
+ import { NPC } from '../models/npc.js';
2
+ import { Logger } from './logger.js';
3
+ import { CommandRegistry } from '../commands/command-registry.js';
4
+ /**
5
+ * Bring one into the world. Returns the NPC, or null when the name is
6
+ * already taken -- the same refusal spawnCompanionShell makes, and for
7
+ * the same reason: two actors answering to one name is a bug nobody can
8
+ * see until a command picks the wrong one.
9
+ */
10
+ export function spawnEphemeral(host, opts) {
11
+ if (host.scene.getActor(opts.name)) {
12
+ Logger.getInstance().write(`spawnEphemeral: name "${opts.name}" already in scene -- refusing.`);
13
+ return null;
14
+ }
15
+ const npc = new NPC({
16
+ dialog: [],
17
+ description: opts.description,
18
+ playerConstructorConfig: { playerName: opts.name, startLocation: opts.room, combat: opts.combat },
19
+ }, new CommandRegistry(host.scene));
20
+ npc.plane = 'meat';
21
+ // NOT allyOf, and NOT companionKind: an ephemeral belongs to nobody
22
+ // and is on no roster. Those two fields are what make a companion
23
+ // persist, follow, and take orders.
24
+ npc.ephemeral = { kind: opts.kind, until: opts.until, roomName: opts.room.name };
25
+ host.scene.addActor(npc);
26
+ host.scene.setCharacterLocation(npc, opts.room.name);
27
+ npc.scene = host.scene;
28
+ host.ephemerals.set(opts.name, npc.ephemeral);
29
+ Logger.getInstance().write(`Ephemeral spawned: ${opts.name} (${opts.kind}, until ${opts.until}) in ${opts.room.name}.`);
30
+ return npc;
31
+ }
32
+ /**
33
+ * Remove every ephemeral whose scope has ended.
34
+ *
35
+ * WHY NO SAVE HOOK IS NEEDED HERE, which is the ruling's reasoning and
36
+ * has to be true at exactly this line: ephemerals are never written to
37
+ * the save file, so there is nothing to reconcile on load. The obvious
38
+ * worry -- a save landing mid-fight, resurrecting a dead mob or erasing
39
+ * a live one -- is already answered upstream: requestSave's guards only
40
+ * write at safe beats and a fight is not one. So a load never sees a
41
+ * half-finished ephemeral, and this function is the only thing that
42
+ * ever ends one.
43
+ *
44
+ * Returns the names removed, so a caller can say so once.
45
+ */
46
+ export function despawnEphemerals(host, opts) {
47
+ const removed = [];
48
+ for (const [name, mark] of [...host.ephemerals]) {
49
+ const ended = opts.all
50
+ ? true
51
+ : mark.until === 'fight'
52
+ ? opts.fightActive === false
53
+ // exchange- and room-scoped bodies both end when the player
54
+ // leaves: a walkout IS the end of the conversation.
55
+ : opts.playerRoomName !== undefined && opts.playerRoomName !== mark.roomName;
56
+ if (!ended)
57
+ continue;
58
+ if (host.scene.getActor(name) instanceof NPC)
59
+ host.scene.removeActor(name);
60
+ host.ephemerals.delete(name);
61
+ removed.push(name);
62
+ }
63
+ if (removed.length > 0) {
64
+ Logger.getInstance().write(`Ephemerals despawned: ${removed.join(', ')}.`);
65
+ }
66
+ return removed;
67
+ }
68
+ /**
69
+ * The two denials that make an ephemeral disposable, as prompt text.
70
+ *
71
+ * FULL LENGTH, DELIBERATELY. Everything else in an ephemeral's brief is
72
+ * trimmed hard -- no command help, no objective, no payout, no client,
73
+ * no wares, no history -- but the honesty pins are not where the budget
74
+ * gets saved. They are the majority of what remains precisely because
75
+ * the rest went, and trimming a denial is the exact economy that
76
+ * reintroduces the bug the denial exists to prevent. A shorter pin does
77
+ * not produce a shorter answer; it produces a confident wrong one.
78
+ */
79
+ export function ephemeralDenials(role) {
80
+ return `
81
+ YOU ARE A PASSING FACE, NOT A CHARACTER IN ANYONE'S STORY. You are ${role} -- here for
82
+ this moment and no longer. You know nothing about any job, run, client, employer,
83
+ objective, payout, or what these people are doing in this district beyond what is
84
+ happening in front of you right now. You have no opinion about their business because you
85
+ do not know they have any. If someone raises a job, a Johnson, a target, or a score, you
86
+ have no idea what they are talking about and no curiosity about it -- that is somebody
87
+ else's world and you are not in it.
88
+
89
+ YOU CANNOT PROMISE, SELL, HIRE, OR HAND OVER ANYTHING. You quote no prices, offer no work,
90
+ carry no goods to trade, and make no arrangements for later -- no favors owed, no "come
91
+ find me", no meeting tomorrow. You will not be here tomorrow. Anything you agreed to would
92
+ be an obligation nobody in the world can honor, and the person who trusted it would be
93
+ worse off for having met you. If pressed for any of it, say plainly that it is not yours
94
+ to give and point them at whoever actually handles such things.`;
95
+ }
96
+ //# sourceMappingURL=ephemeral.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ephemeral.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/ephemeral.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { ISceneSeedJson } from '../types/seed/scene-seed.js';
2
+ /**
3
+ * WHERE THE MEET ACTUALLY IS, read off the drafted seed.
4
+ *
5
+ * Item g5CgJgmw9ix6AcD78: "Krow said to meet at Mama Wu's, that
6
+ * shouldn't happen." There is no Mama Wu's. The fixer's pitch is
7
+ * AI-generated free text, and the brief told him the client "will hold
8
+ * the meet on-site" without telling him WHERE -- so a model writing a
9
+ * fixer's voice did the natural thing and invented a venue. The player
10
+ * then travelled to a scene that contains no such place.
11
+ *
12
+ * This is the codebase's signature defect: a NAME REFERRING TO NOTHING.
13
+ * The house cure is to resolve names against the real roster or room
14
+ * list and repair or reject, never to ship an unresolved one (see
15
+ * scene-chunks.ts resolveNamedRef). The same cure applies here, and it
16
+ * is better than a prohibition: the seed already KNOWS the answer, so
17
+ * the fixer can be told the truth rather than merely forbidden a lie.
18
+ * A brief that only says "don't invent one" leaves him with nothing to
19
+ * say and invites a vaguer dodge.
20
+ *
21
+ * THE MEET IS WHERE THE CLIENT STANDS, not the start room -- generation
22
+ * places the client "in a REACHABLE room that makes a characterful meet
23
+ * spot" (scene-chunks.ts), which is frequently not where the runner
24
+ * arrives. Falling back to the start room is still TRUE (it is where
25
+ * they land, and the client is always reachable from it), so the
26
+ * fallback degrades to vaguer rather than to wrong.
27
+ *
28
+ * IT LIVES HERE, not in call.ts, because ONE ANSWER IS THE WHOLE POINT.
29
+ * The reported bug is two sources disagreeing about where the meet is
30
+ * (the vetting on the item: "Krow directing the player to Mama Wu's,
31
+ * then a conflicting message from Mr. Vane at a different location"), so
32
+ * a resolver that only one of the speakers consults cannot fix it. Every
33
+ * surface that tells the player or an NPC where the meet is reads this
34
+ * function; if you are about to write the words "the meet" and a place
35
+ * name, call this instead.
36
+ */
37
+ export declare function meetLocationFor(seed: ISceneSeedJson | undefined): string | undefined;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WHERE THE MEET ACTUALLY IS, read off the drafted seed.
3
+ *
4
+ * Item g5CgJgmw9ix6AcD78: "Krow said to meet at Mama Wu's, that
5
+ * shouldn't happen." There is no Mama Wu's. The fixer's pitch is
6
+ * AI-generated free text, and the brief told him the client "will hold
7
+ * the meet on-site" without telling him WHERE -- so a model writing a
8
+ * fixer's voice did the natural thing and invented a venue. The player
9
+ * then travelled to a scene that contains no such place.
10
+ *
11
+ * This is the codebase's signature defect: a NAME REFERRING TO NOTHING.
12
+ * The house cure is to resolve names against the real roster or room
13
+ * list and repair or reject, never to ship an unresolved one (see
14
+ * scene-chunks.ts resolveNamedRef). The same cure applies here, and it
15
+ * is better than a prohibition: the seed already KNOWS the answer, so
16
+ * the fixer can be told the truth rather than merely forbidden a lie.
17
+ * A brief that only says "don't invent one" leaves him with nothing to
18
+ * say and invites a vaguer dodge.
19
+ *
20
+ * THE MEET IS WHERE THE CLIENT STANDS, not the start room -- generation
21
+ * places the client "in a REACHABLE room that makes a characterful meet
22
+ * spot" (scene-chunks.ts), which is frequently not where the runner
23
+ * arrives. Falling back to the start room is still TRUE (it is where
24
+ * they land, and the client is always reachable from it), so the
25
+ * fallback degrades to vaguer rather than to wrong.
26
+ *
27
+ * IT LIVES HERE, not in call.ts, because ONE ANSWER IS THE WHOLE POINT.
28
+ * The reported bug is two sources disagreeing about where the meet is
29
+ * (the vetting on the item: "Krow directing the player to Mama Wu's,
30
+ * then a conflicting message from Mr. Vane at a different location"), so
31
+ * a resolver that only one of the speakers consults cannot fix it. Every
32
+ * surface that tells the player or an NPC where the meet is reads this
33
+ * function; if you are about to write the words "the meet" and a place
34
+ * name, call this instead.
35
+ */
36
+ export function meetLocationFor(seed) {
37
+ if (!seed)
38
+ return undefined;
39
+ const client = seed.client
40
+ ? seed.npcs?.find(n => n.player?.name === seed.client)
41
+ : undefined;
42
+ const clientRoom = client?.player?.startLocation;
43
+ // Only trust a room the scene really declares -- a startLocation that
44
+ // names no room is the same unresolved-name bug one level down, and
45
+ // repeating it into the fixer's mouth would be worse than silence.
46
+ const rooms = seed.rooms ?? [];
47
+ if (clientRoom && rooms.some(r => r.name === clientRoom))
48
+ return clientRoom;
49
+ return rooms.find(r => r.isStartRoom)?.name;
50
+ }
51
+ //# sourceMappingURL=meet-location.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meet-location.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/meet-location.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,eAAe,CAAC,IAAgC;IAC9D,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM;QACxB,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC;QACtD,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,UAAU,GAAG,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC;IACjD,sEAAsE;IACtE,oEAAoE;IACpE,mEAAmE;IACnE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAC/B,IAAI,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IAC5E,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,76 @@
1
+ import type { Player } from '../models/player.js';
2
+ /**
3
+ * THE PERCEPTION TEST (SR5 core p.135-136), shared by every verb that
4
+ * makes one.
5
+ *
6
+ * Item FNgptvRhbA5yMunbB, the player writing the spec in two sentences:
7
+ * '"look" should reveal items that are under a perception threshold.
8
+ * "search" is an active perception check'.
9
+ *
10
+ * The SPLIT they asked for already shipped (52ba4dda): `look` lists
11
+ * plain-sight goods with no test at all, which is p.135's own
12
+ * instruction -- characters "should always be able to observe what is
13
+ * immediately obvious without spending an action or making a test" --
14
+ * while `search` rolls. What had NOT shipped is the mechanical
15
+ * difference between the two rolls, and canon is specific about what it
16
+ * is:
17
+ *
18
+ * "Perceiver is specifically looking/listening for it +3"
19
+ * (p.135-136, Perception Test Modifiers)
20
+ *
21
+ * That +3 IS the active/passive distinction. Without it, `search` was an
22
+ * active perception check in name while rolling the same pool as the
23
+ * passive glance -- the player's second sentence unimplemented, one
24
+ * layer below the one that was fixed.
25
+ *
26
+ * AND THE BRACKET IS A LIMIT. `Perception + Intuition [Mental]`: neither
27
+ * verb applied one, so a huge pool could count unlimited hits. Limits
28
+ * cap HITS, never dice (utilities/dice.ts, and the house rule at the top
29
+ * of CLAUDE.md).
30
+ *
31
+ * ONE FUNCTION, BOTH CALLERS, deliberately. look.ts and search.ts each
32
+ * built this pool inline and had already drifted apart; that is the same
33
+ * one-caller drift that let the meet address disagree with itself
34
+ * (g5CgJgmw9ix6AcD78) and the same duplicated-mechanic shape that let
35
+ * `use <medkit>` diverge from `heal`. If you are about to roll
36
+ * Perception, roll it here.
37
+ */
38
+ /** p.135-136: the modifier for deliberately looking, which is what makes
39
+ * an active search mechanically different from a passive glance. */
40
+ export declare const ACTIVELY_LOOKING = 3;
41
+ export interface IPerceptionTest {
42
+ pool: number;
43
+ /** The bracket. Undefined only when the actor has no limit to apply --
44
+ * see the Matrix note below; rollPool refuses a zero limit loudly. */
45
+ limit: number | undefined;
46
+ }
47
+ export interface IPerceptionOpts {
48
+ /** True for a deliberate hunt (`search`), false for a glance (`look`). */
49
+ active?: boolean;
50
+ /** Matrix Search only: a loaded Browse program is purpose-built for it. */
51
+ browse?: number;
52
+ }
53
+ /**
54
+ * ON THE GRID IT IS A DIFFERENT TEST WITH A DIFFERENT BRACKET. Matrix
55
+ * Search is Computer + Logic [Data Processing], so the Mental limit does
56
+ * NOT apply out there -- capping a decker's sweep by their Willpower
57
+ * would be a fresh deviation dressed as a fix. The engine takes the
58
+ * best of Perception and Computer so pre-Computer deckers lose nothing
59
+ * (a standing allowance, older than this function).
60
+ *
61
+ * Data Processing reads 0 for an actor with no deck at all; that is a
62
+ * caller's missing gate rather than a real limit, so it degrades to
63
+ * unlimited here instead of handing rollPool a zero it would reject.
64
+ */
65
+ export declare function perceptionTest(actor: Player, opts?: IPerceptionOpts): IPerceptionTest;
66
+ /**
67
+ * PERCEPTION THRESHOLDS (p.136). Named because the numbers appear in
68
+ * player-facing text and in cover reads, and a bare 2 or 4 in three
69
+ * files is how they drift.
70
+ */
71
+ export declare const PERCEPTION_THRESHOLD: {
72
+ /** Street sign, pedestrian, conversation, silenced gunfire. */
73
+ readonly normal: 2;
74
+ /** Secret door, needle in a haystack, subvocal speech. */
75
+ readonly hidden: 4;
76
+ };
@@ -0,0 +1,84 @@
1
+ import { mentalLimit } from './limits.js';
2
+ /**
3
+ * THE PERCEPTION TEST (SR5 core p.135-136), shared by every verb that
4
+ * makes one.
5
+ *
6
+ * Item FNgptvRhbA5yMunbB, the player writing the spec in two sentences:
7
+ * '"look" should reveal items that are under a perception threshold.
8
+ * "search" is an active perception check'.
9
+ *
10
+ * The SPLIT they asked for already shipped (52ba4dda): `look` lists
11
+ * plain-sight goods with no test at all, which is p.135's own
12
+ * instruction -- characters "should always be able to observe what is
13
+ * immediately obvious without spending an action or making a test" --
14
+ * while `search` rolls. What had NOT shipped is the mechanical
15
+ * difference between the two rolls, and canon is specific about what it
16
+ * is:
17
+ *
18
+ * "Perceiver is specifically looking/listening for it +3"
19
+ * (p.135-136, Perception Test Modifiers)
20
+ *
21
+ * That +3 IS the active/passive distinction. Without it, `search` was an
22
+ * active perception check in name while rolling the same pool as the
23
+ * passive glance -- the player's second sentence unimplemented, one
24
+ * layer below the one that was fixed.
25
+ *
26
+ * AND THE BRACKET IS A LIMIT. `Perception + Intuition [Mental]`: neither
27
+ * verb applied one, so a huge pool could count unlimited hits. Limits
28
+ * cap HITS, never dice (utilities/dice.ts, and the house rule at the top
29
+ * of CLAUDE.md).
30
+ *
31
+ * ONE FUNCTION, BOTH CALLERS, deliberately. look.ts and search.ts each
32
+ * built this pool inline and had already drifted apart; that is the same
33
+ * one-caller drift that let the meet address disagree with itself
34
+ * (g5CgJgmw9ix6AcD78) and the same duplicated-mechanic shape that let
35
+ * `use <medkit>` diverge from `heal`. If you are about to roll
36
+ * Perception, roll it here.
37
+ */
38
+ /** p.135-136: the modifier for deliberately looking, which is what makes
39
+ * an active search mechanically different from a passive glance. */
40
+ export const ACTIVELY_LOOKING = 3;
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).
48
+ *
49
+ * Data Processing reads 0 for an actor with no deck at all; that is a
50
+ * caller's missing gate rather than a real limit, so it degrades to
51
+ * unlimited here instead of handing rollPool a zero it would reject.
52
+ */
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
61
+ + (skill > 0 ? skill : -1)
62
+ + (opts.browse ?? 0)
63
+ + (opts.active ? ACTIVELY_LOOKING : 0)
64
+ + actor.augBonus('perception')
65
+ + actor.woundModifier
66
+ - 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 };
72
+ }
73
+ /**
74
+ * PERCEPTION THRESHOLDS (p.136). Named because the numbers appear in
75
+ * player-facing text and in cover reads, and a bare 2 or 4 in three
76
+ * files is how they drift.
77
+ */
78
+ export const PERCEPTION_THRESHOLD = {
79
+ /** Street sign, pedestrian, conversation, silenced gunfire. */
80
+ normal: 2,
81
+ /** Secret door, needle in a haystack, subvocal speech. */
82
+ hidden: 4,
83
+ };
84
+ //# sourceMappingURL=perception.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perception.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/perception.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH;qEACqE;AACrE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAgBlC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,OAAwB,EAAE;IACtE,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC;IACxC,MAAM,KAAK,GAAG,MAAM;QAClB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAC1E,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC;IAEzD,yEAAyE;IACzE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS;UAC9B,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;UACxB,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;UAClB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;UACpC,KAAK,CAAC,QAAQ,CAAC,YAAY,CAAC;UAC5B,KAAK,CAAC,aAAa;UACnB,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAE7B,MAAM,cAAc,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,KAAK,GAAG,MAAM;QAClB,CAAC,CAAC,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC;QACnD,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAEvB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,+DAA+D;IAC/D,MAAM,EAAE,CAAC;IACT,0DAA0D;IAC1D,MAAM,EAAE,CAAC;CACD,CAAC"}
@@ -231,6 +231,18 @@ export declare function tryCreateSharedSession(game: Game, preferredSessionID?:
231
231
  declined: string;
232
232
  code?: string;
233
233
  } | undefined>;
234
+ /** One event off the wire. Exported so tests can drive route() with a
235
+ * real frame shape instead of a hand-rolled duplicate that drifts. */
236
+ export interface ISessionEvent {
237
+ sessionID: string;
238
+ seq: number;
239
+ channel: string;
240
+ visibleTo: 'all' | string[];
241
+ text: string;
242
+ actor?: string;
243
+ kind?: string;
244
+ data?: Record<string, unknown>;
245
+ }
234
246
  /**
235
247
  * A live shared run, from this terminal's side: the server's event
236
248
  * stream renders into the same blessed panes solo play uses, and every
@@ -238,6 +250,36 @@ export declare function tryCreateSharedSession(game: Game, preferredSessionID?:
238
250
  * game.remoteSession -- Game.handleInput delegates here wholesale, so
239
251
  * ui.ts needs no new wiring.
240
252
  */
253
+ /**
254
+ * THE HUB'S MAPS ARE NOT THIS RUN'S MAPS.
255
+ *
256
+ * On a shared run the panels are painted only by the server's per-player
257
+ * kind:'map' and kind:'room' events, because Game.updateExits early-
258
+ * returns on `remoteSession` (the client holds no Room objects out
259
+ * here). Nothing ever cleared what the HUB had painted, so between
260
+ * entering the run and the server's first frame the player sat looking
261
+ * at a map of the bar they had just left -- captioned with its name,
262
+ * indistinguishable from a live one.
263
+ *
264
+ * That is why this arrived as "the maps are not refreshing"
265
+ * (KXgMCRQePfmsnXLRP, vYk4kcNYgR2CQY5P9) rather than "the run is dead".
266
+ * The failing session on 2026-08-30T22-12-28 received exactly two frames
267
+ * and then nothing at all -- no map, no room, nothing, for 59 seconds.
268
+ * The panels did not go blank and raise the alarm; they kept confidently
269
+ * displaying the hub, so a table that had received NOTHING looked like a
270
+ * table whose maps were merely stale.
271
+ *
272
+ * Blanking here turns a silent lie into a visible absence. An empty
273
+ * panel is honest -- it says the server has not painted yet, which is
274
+ * exactly true. Degrade to vaguer, never to wrong: the same rule
275
+ * meetLocationFor follows when it returns undefined rather than guess a
276
+ * venue (g5CgJgmw9ix6AcD78).
277
+ *
278
+ * BEFORE subscribe(), deliberately. A link that never opens is precisely
279
+ * when the stale hub map misleads most, so the blanking must not sit
280
+ * downstream of a socket that may never come up.
281
+ */
282
+ export declare function blankRunPanels(game: Game): void;
241
283
  export declare class SharedRunSession {
242
284
  private game;
243
285
  private sessionID;
@@ -256,6 +298,34 @@ export declare class SharedRunSession {
256
298
  * timestamps on different machines -- this is the only thing that
257
299
  * ties their reports to one run. */
258
300
  get id(): string;
301
+ /**
302
+ * THE HUB'S MAPS ARE NOT THIS RUN'S MAPS.
303
+ *
304
+ * On a shared run the panels are painted only by the server's per-player
305
+ * kind:'map' and kind:'room' events, because Game.updateExits early-
306
+ * returns on `remoteSession` (the client holds no Room objects out
307
+ * here). Nothing ever cleared what the HUB had painted, so between
308
+ * entering the run and the server's first frame the player sat looking
309
+ * at a map of the bar they had just left -- captioned with its name,
310
+ * pixel-identical to a live one.
311
+ *
312
+ * That is why this arrived as "the maps are not refreshing" rather than
313
+ * "the run is dead". Tonight's failing session (2026-08-30T22-12-28)
314
+ * received exactly two frames and then nothing at all: no map, no room,
315
+ * no anything for 59 seconds. The panels did not go blank and raise the
316
+ * alarm -- they kept confidently displaying the hub, so a table that had
317
+ * received NOTHING looked like a table whose maps were merely stale.
318
+ *
319
+ * Blanking here converts a silent lie into a visible absence. An empty
320
+ * panel is honest: it says the server has not painted yet, which is
321
+ * exactly true. Degrade to vaguer, never to wrong -- the same rule
322
+ * meetLocationFor follows when it returns undefined rather than guess a
323
+ * venue (g5CgJgmw9ix6AcD78).
324
+ *
325
+ * BEFORE subscribe(), deliberately. If the link never comes up at all,
326
+ * that is precisely when the stale hub map is most misleading, so the
327
+ * blanking must not be downstream of a socket that may never open.
328
+ */
259
329
  static startAsLeader(game: Game, sessionID: string): Promise<string>;
260
330
  static joinAsMember(game: Game, sessionID: string): Promise<string>;
261
331
  private static enter;