@maka/maka-cli 5.197.0 → 5.199.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.
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +6 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/command-registry.js +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/command.js +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/decompile.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/give.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +49 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +8 -8
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/kill.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/map.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/palm.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/say.js +19 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/shout.js +69 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/talk.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +7 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/thread.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +75 -1
- package/bundle/typescript/src/commands/game/sideQuest/factions.js +135 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +24 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +66 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +165 -14
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +90 -57
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +84 -8
- package/bundle/typescript/src/commands/game/sideQuest/types/repro.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/beat-echo.js +160 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +25 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +153 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +14 -14
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hostile-contact.js +48 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/narration-limits.js +235 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-authorization.js +122 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/overwatch.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE SAME BEAT TWICE (aCErAfvEW7G23FWDM, reporter: "The NPC speaking
|
|
3
|
+
* the same thing, this is likely something more pervasive, and not
|
|
4
|
+
* really logically an issue of the scene").
|
|
5
|
+
*
|
|
6
|
+
* They were right that it is pervasive: THERE IS NO DEDUPE OF EMITTED
|
|
7
|
+
* TEXT ANYWHERE IN THIS ENGINE. Not a hash, not a similarity check, not
|
|
8
|
+
* a recently-said buffer. What exists is four things that all stop
|
|
9
|
+
* short of it:
|
|
10
|
+
*
|
|
11
|
+
* - the prompt asks nicely ("Do NOT repeat, rephrase, or re-ask
|
|
12
|
+
* anything you have already said"), and the session that filed this
|
|
13
|
+
* item has an NPC delivering one hook twice in two phrasings;
|
|
14
|
+
* - _spokeThisStimulus allows one speech per stimulus, which does
|
|
15
|
+
* nothing about the NEXT stimulus a second later;
|
|
16
|
+
* - _triedThisStimulus dedupes normalized COMMAND STRINGS, not prose;
|
|
17
|
+
* - Room.crowdNote tells each NPC that others are answering too --
|
|
18
|
+
* and its own test says the behavioural claim is "UNVERIFIABLE BY
|
|
19
|
+
* CONSTRUCTION", because the fan-out is parallel and un-awaited.
|
|
20
|
+
*
|
|
21
|
+
* That last one is the reason this has to be a gate at EMISSION rather
|
|
22
|
+
* than another sentence in a prompt. Two NPCs answering the same
|
|
23
|
+
* stimulus are already in flight when either one's prompt is built;
|
|
24
|
+
* neither can be told what the other is about to say. Only the thing
|
|
25
|
+
* that prints them both can know.
|
|
26
|
+
*
|
|
27
|
+
* THE TWO REPORTED PAIRS, both from 2026-09-15T14-45-01-617Z:
|
|
28
|
+
*
|
|
29
|
+
* 11:05:10 "Tallow keeps wiping the glass, watching Ted's body stir
|
|
30
|
+
* behind the beads, then glances at Kessler."
|
|
31
|
+
* "Tallow keeps polishing the same glass, eyes flicking
|
|
32
|
+
* toward the back room as the beads settle."
|
|
33
|
+
*
|
|
34
|
+
* 10:57:18 "Chapel on Tallow Street. The Drowned Choir -- sang for
|
|
35
|
+
* the dead, then went quiet. I want the quiet explained."
|
|
36
|
+
* 10:57:19 "Chapel on Tallow Street. The Drowned Choir. Answer me
|
|
37
|
+
* one question -- why did the singing stop?"
|
|
38
|
+
*
|
|
39
|
+
* Note what they are NOT: identical. A hash would catch neither. They
|
|
40
|
+
* are the same BEAT rewritten, which is what a model does when it is
|
|
41
|
+
* asked twice in two seconds with the same context, and it is why the
|
|
42
|
+
* test below is written against these exact lines.
|
|
43
|
+
*/
|
|
44
|
+
/** How long a line stays echo-able. Beyond this an NPC returning to
|
|
45
|
+
* their glass is a character trait, not a stutter. */
|
|
46
|
+
export const ECHO_WINDOW_MS = 45_000;
|
|
47
|
+
/** How many lines per room to remember. Small deliberately: this is a
|
|
48
|
+
* stutter detector, not a transcript. */
|
|
49
|
+
const ECHO_DEPTH = 12;
|
|
50
|
+
/** Words that carry no identity, so overlap on them means nothing.
|
|
51
|
+
* Deliberately broader than utilities/fuzzy-match.ts's STOPWORDS,
|
|
52
|
+
* which is tuned for matching NAMES rather than prose. */
|
|
53
|
+
const NOISE = new Set([
|
|
54
|
+
'a', 'an', 'the', 'and', 'or', 'but', 'of', 'on', 'in', 'at', 'to', 'for',
|
|
55
|
+
'with', 'from', 'by', 'as', 'is', 'are', 'was', 'were', 'be', 'been',
|
|
56
|
+
'it', 'its', 'he', 'she', 'they', 'them', 'his', 'her', 'their', 'him',
|
|
57
|
+
'that', 'this', 'these', 'those', 'then', 'than', 'there', 'here',
|
|
58
|
+
'you', 'your', 'i', 'me', 'my', 'we', 'us', 'not', 'no', 'so', 'up',
|
|
59
|
+
'out', 'off', 'over', 'into', 'through', 'about', 'like', 'just',
|
|
60
|
+
]);
|
|
61
|
+
/**
|
|
62
|
+
* Keyed on the ROOM OBJECT, weakly. A room that leaves play takes its
|
|
63
|
+
* echoes with it, and nothing here keeps a scene alive.
|
|
64
|
+
*/
|
|
65
|
+
const heard = new WeakMap();
|
|
66
|
+
function contentWords(line) {
|
|
67
|
+
return line
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.replace(/[^\w\s']/g, ' ')
|
|
70
|
+
.split(/\s+/)
|
|
71
|
+
.filter(w => w.length > 1 && !NOISE.has(w));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* CONTAINMENT, not Jaccard, and the difference decides whether this
|
|
75
|
+
* works. Jaccard divides by the UNION, so a long line and a short one
|
|
76
|
+
* saying the same thing score low purely because one has more words --
|
|
77
|
+
* and "Tallow keeps polishing the same glass" scores 0.2 against its
|
|
78
|
+
* own twin. Containment asks the question actually being asked: how
|
|
79
|
+
* much of the SHORTER line is already in the longer one.
|
|
80
|
+
*/
|
|
81
|
+
function containment(a, b) {
|
|
82
|
+
if (a.length === 0 || b.length === 0)
|
|
83
|
+
return 0;
|
|
84
|
+
const set = new Set(b);
|
|
85
|
+
let shared = 0;
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
for (const w of a) {
|
|
88
|
+
if (seen.has(w))
|
|
89
|
+
continue;
|
|
90
|
+
seen.add(w);
|
|
91
|
+
if (set.has(w))
|
|
92
|
+
shared += 1;
|
|
93
|
+
}
|
|
94
|
+
return shared / Math.min(new Set(a).size, new Set(b).size);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* SAME SPEAKER, SAME OPENING. "Tallow keeps wiping" and "Tallow keeps
|
|
98
|
+
* polishing" share their first two content words, and that is a
|
|
99
|
+
* stronger signal of a stutter than any amount of shared vocabulary
|
|
100
|
+
* later in the sentence -- it is the shape of a model restarting the
|
|
101
|
+
* same thought. Paired with a low overlap bar it catches the Tallow
|
|
102
|
+
* pair, which pure overlap (0.36) would otherwise let through.
|
|
103
|
+
*/
|
|
104
|
+
function sameOpening(a, b) {
|
|
105
|
+
if (a.length < 2 || b.length < 2)
|
|
106
|
+
return false;
|
|
107
|
+
return a[0] === b[0] && a[1] === b[1];
|
|
108
|
+
}
|
|
109
|
+
/** Overlap at which two lines by one speaker are the same beat. */
|
|
110
|
+
const SAME_SPEAKER = 0.5;
|
|
111
|
+
/** ...and the lower bar once they also START the same way. */
|
|
112
|
+
const SAME_SPEAKER_OPENING = 0.3;
|
|
113
|
+
/**
|
|
114
|
+
* Two DIFFERENT NPCs delivering one hook. A higher bar on purpose:
|
|
115
|
+
* people in a room legitimately talk about the same thing, and two
|
|
116
|
+
* characters agreeing is a scene rather than a bug. Only near-identical
|
|
117
|
+
* content is a second delivery of the same line.
|
|
118
|
+
*/
|
|
119
|
+
const OTHER_SPEAKER = 0.65;
|
|
120
|
+
/**
|
|
121
|
+
* HAS THIS ROOM JUST HEARD THIS?
|
|
122
|
+
*
|
|
123
|
+
* Records the line when it is new, so callers need exactly one call.
|
|
124
|
+
* A line judged an echo is NOT recorded -- it never reached anybody, so
|
|
125
|
+
* it cannot be the thing a third line echoes.
|
|
126
|
+
*
|
|
127
|
+
* @param room the room object the line prints in
|
|
128
|
+
* @param speaker who is saying it
|
|
129
|
+
* @param line the emitted prose, as the player would read it
|
|
130
|
+
* @param now injectable clock; the window is the only time rule
|
|
131
|
+
*/
|
|
132
|
+
export function judgeEcho(room, speaker, line, now = Date.now()) {
|
|
133
|
+
if (!room)
|
|
134
|
+
return { echo: false };
|
|
135
|
+
const words = contentWords(line);
|
|
136
|
+
// Too short to judge. "Drek." twice in a row is a person, not a
|
|
137
|
+
// stutter, and there is not enough here to tell them apart.
|
|
138
|
+
if (words.length < 4)
|
|
139
|
+
return { echo: false };
|
|
140
|
+
const log = (heard.get(room) ?? []).filter(e => now - e.at < ECHO_WINDOW_MS);
|
|
141
|
+
for (const prior of log) {
|
|
142
|
+
const overlap = containment(words, prior.words);
|
|
143
|
+
const mine = prior.speaker === speaker;
|
|
144
|
+
const bar = mine
|
|
145
|
+
? (sameOpening(words, prior.lead) ? SAME_SPEAKER_OPENING : SAME_SPEAKER)
|
|
146
|
+
: OTHER_SPEAKER;
|
|
147
|
+
if (overlap >= bar) {
|
|
148
|
+
heard.set(room, log);
|
|
149
|
+
return { echo: true, of: prior.speaker };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
log.push({ speaker, words, lead: words, at: now });
|
|
153
|
+
heard.set(room, log.slice(-ECHO_DEPTH));
|
|
154
|
+
return { echo: false };
|
|
155
|
+
}
|
|
156
|
+
/** Test seam: forget what a room has heard. */
|
|
157
|
+
export function forgetEchoes(room) {
|
|
158
|
+
heard.delete(room);
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=beat-echo.js.map
|
|
@@ -927,6 +927,31 @@ export function metamagicFor(key) {
|
|
|
927
927
|
...baseFields(row),
|
|
928
928
|
})).get(key);
|
|
929
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* A FACTION ROW, rebuilt as the engine reads it (aCErAfvEW7G23FWDM).
|
|
932
|
+
*
|
|
933
|
+
* Same shape as every other kind: the rows live on maka-cli.com and the
|
|
934
|
+
* engine ships none. An unknown slug answers undefined and every rule
|
|
935
|
+
* in factions.ts treats that as "no opinion" -- a cached catalog from
|
|
936
|
+
* before v20 simply behaves the way the engine did before factions.
|
|
937
|
+
*/
|
|
938
|
+
export function factionFor(key) {
|
|
939
|
+
return kindIndex('faction', row => ({
|
|
940
|
+
key: row.slug,
|
|
941
|
+
name: row.name,
|
|
942
|
+
description: row.description,
|
|
943
|
+
sort: row.faction.sort,
|
|
944
|
+
...(row.faction.parent !== undefined ? { parent: row.faction.parent } : {}),
|
|
945
|
+
...(row.faction.standing !== undefined ? { standing: row.faction.standing } : {}),
|
|
946
|
+
...(row.faction.territorial !== undefined ? { territorial: row.faction.territorial } : {}),
|
|
947
|
+
...(row.faction.discreet !== undefined ? { discreet: row.faction.discreet } : {}),
|
|
948
|
+
page: row.page ?? '',
|
|
949
|
+
})).get(key);
|
|
950
|
+
}
|
|
951
|
+
/** Every faction the resolved catalog knows. */
|
|
952
|
+
export function catalogFactions() {
|
|
953
|
+
return listOfKind('faction', factionFor);
|
|
954
|
+
}
|
|
930
955
|
/** Every metamagic and echo the resolved catalog knows. */
|
|
931
956
|
export function catalogMetamagics() {
|
|
932
957
|
return listOfKind('metamagic', metamagicFor);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sameSpot, spotOf, spotsActive, OPEN_FLOOR } from './spots.js';
|
|
1
|
+
import { sameSpot, spotOf, spotsActive, OPEN_FLOOR, IMMEDIATE_VICINITY_M, actorDistanceMeters } from './spots.js';
|
|
2
2
|
/**
|
|
3
3
|
* WHO CAN OVERHEAR A MURMUR (playtest, Deditri: "a private place for
|
|
4
4
|
* calls"). A call is audible to the room, and that is correct and
|
|
@@ -17,17 +17,25 @@ import { sameSpot, spotOf, spotsActive, OPEN_FLOOR } from './spots.js';
|
|
|
17
17
|
* Market's "shadowed edge". Nothing new is invented here; the call path
|
|
18
18
|
* just respects the position everyone else already respects.
|
|
19
19
|
*
|
|
20
|
-
* THE RULE, and why it is not Room.say's rule
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* escalates
|
|
24
|
-
* murmur reaches your spot and stops there.
|
|
20
|
+
* THE RULE, and why it is STILL not Room.say's rule. A murmur into a
|
|
21
|
+
* commlink reaches your spot and stops there, full stop -- talking
|
|
22
|
+
* quietly into a phone is the whole point of a phone, and it never
|
|
23
|
+
* escalates however alone you are standing.
|
|
25
24
|
*
|
|
26
|
-
*
|
|
25
|
+
* Room speech used to share that shape and no longer does. `say` is now
|
|
26
|
+
* bounded by each LISTENER's hearing range in real metres
|
|
27
|
+
* (canHearSpeech below, en3p6eCpiRuqsTSKm) and never escalates on its
|
|
28
|
+
* own; shouting is a verb you type (commands/shout.ts). The two rules
|
|
29
|
+
* live in one file so they can be read against each other, which is the
|
|
30
|
+
* lesson of the drift that produced this module in the first place.
|
|
31
|
+
*
|
|
32
|
+
* WHAT DOES NOT BUY PRIVACY ON A CALL, deliberately:
|
|
27
33
|
* - CIRCULATING (no atSpot) is the compat state and means "anywhere in
|
|
28
|
-
* the room" --
|
|
29
|
-
*
|
|
30
|
-
*
|
|
34
|
+
* the room" -- your half of a CALL still reaches and is reached by
|
|
35
|
+
* everything, because there is no spot for it to stop at. Note this
|
|
36
|
+
* is the opposite of what circulating now means for ordinary speech,
|
|
37
|
+
* and the asymmetry is argued at canHearSpeech: a murmur has a place
|
|
38
|
+
* it comes from and a phone call does not.
|
|
31
39
|
* - THE OPEN FLOOR is a real position but it is bare ground in the
|
|
32
40
|
* middle of the room, no cover, always visible (see spots.ts). Being
|
|
33
41
|
* apart from people is not the same as being out of earshot, and a
|
|
@@ -40,6 +48,141 @@ import { sameSpot, spotOf, spotsActive, OPEN_FLOOR } from './spots.js';
|
|
|
40
48
|
* Nothing here caches, stores, or derives a second notion of where
|
|
41
49
|
* anybody is.
|
|
42
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* HOW FAR ORDINARY SPEECH CARRIES, and how far one listener can hear it
|
|
53
|
+
* (en3p6eCpiRuqsTSKm: "'say' should only be perceived by anyone within
|
|
54
|
+
* 3-5m un-augmented. Really, it's the audible perception range of the
|
|
55
|
+
* listener").
|
|
56
|
+
*
|
|
57
|
+
* CANON IS SILENT ON THE DISTANCE, checked rather than assumed. SR5
|
|
58
|
+
* handles hearing as dice and limits, never as a range table: an audio
|
|
59
|
+
* enhancement "adds its Rating to your limit in audio Perception Tests"
|
|
60
|
+
* (p.445), cyberears give "hearing with normal ranges" (p.453-454), and
|
|
61
|
+
* the only metre figures anywhere near it belong to the directional and
|
|
62
|
+
* laser microphones (p.445, 100 m). There is no rule for how far a
|
|
63
|
+
* sentence travels, so THESE NUMBERS ARE THIS ENGINE'S, said out loud
|
|
64
|
+
* rather than dressed as the book's.
|
|
65
|
+
*
|
|
66
|
+
* What is defensible about them:
|
|
67
|
+
* - 5 m is already this engine's word for conversational range
|
|
68
|
+
* (spots.ts IMMEDIATE_VICINITY_M, and voiceBandFor calls anything
|
|
69
|
+
* inside it 'conversation'). Speech reaching exactly as far as the
|
|
70
|
+
* engine already says a conversation reaches is one number, not two.
|
|
71
|
+
* - The listener's perception dice extend it. Canon gives audio gear a
|
|
72
|
+
* DICE/LIMIT bonus and this engine models speech by DISTANCE, so the
|
|
73
|
+
* conversion is ours; a metre per die is the smallest unit that can
|
|
74
|
+
* express "you hear better than they do" in a model made of 2 m
|
|
75
|
+
* cells. Cyberears (+1) hear a metre further. That is an
|
|
76
|
+
* extrapolation of a canon bonus, not a canon rule.
|
|
77
|
+
*/
|
|
78
|
+
export const SPEECH_METERS = IMMEDIATE_VICINITY_M;
|
|
79
|
+
/**
|
|
80
|
+
* A SHOUT (commands/shout.ts) -- "anyone within a reasonable distance,
|
|
81
|
+
* 50m?" Also this engine's number and not the book's. In practice it is
|
|
82
|
+
* room-wide: the largest room this generator builds is tens of metres
|
|
83
|
+
* across, so the figure is a ceiling that says "the whole place hears
|
|
84
|
+
* you" rather than a boundary anyone will stand outside.
|
|
85
|
+
*/
|
|
86
|
+
export const SHOUT_METERS = 50;
|
|
87
|
+
/**
|
|
88
|
+
* How far THIS listener can hear, in metres. The report's own framing:
|
|
89
|
+
* range is a property of the ear, not of the mouth.
|
|
90
|
+
*
|
|
91
|
+
* Reads the same `perception` pool a Perception test reads, so a piece
|
|
92
|
+
* of chrome and a quality that both say "+1 perception" extend hearing
|
|
93
|
+
* identically and neither needs a line here. Never below the base -- a
|
|
94
|
+
* perception PENALTY should make you miss things you roll for, not
|
|
95
|
+
* shrink the room to nothing.
|
|
96
|
+
*/
|
|
97
|
+
export function hearingRangeMeters(listener) {
|
|
98
|
+
// AN EAR IS NOT AN EYE (aCErAfvEW7G23FWDM: "a human officer standing
|
|
99
|
+
// 20 meters away... shouldn't overhear w/o augmentation").
|
|
100
|
+
//
|
|
101
|
+
// This used to read augBonus('perception'), which is ONE FLAT LANE
|
|
102
|
+
// OVER EVERY SENSE -- so cybereyes (+2) and vision-enhancement-3 (+3)
|
|
103
|
+
// extended how far their owner could HEAR. Nobody noticed because a
|
|
104
|
+
// perception POOL genuinely does not care which sense noticed; it
|
|
105
|
+
// only became wrong when this function made that pool a distance.
|
|
106
|
+
// Audio chrome now declares `hearing` (augmentations.ts AugBonusKind,
|
|
107
|
+
// and the same lane on the site's catalog rows).
|
|
108
|
+
//
|
|
109
|
+
// QUALITIES STAY ON `perception` and that is not an oversight: the
|
|
110
|
+
// quality this fans in is Perceptive, which is a whole-sense trait
|
|
111
|
+
// ("you notice things"), not a piece of ear. A quality that meant
|
|
112
|
+
// hearing specifically would declare `hearing` like any row.
|
|
113
|
+
const bonus = listener.augBonus('hearing') + listener.qualityBonus('perception');
|
|
114
|
+
return Math.max(SPEECH_METERS, SPEECH_METERS + bonus);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* CAN THIS LISTENER HEAR ORDINARY SPEECH FROM THERE -- the whole of the
|
|
118
|
+
* `say` rule, in one place so the verb, the NPC fan-out and any future
|
|
119
|
+
* reader cannot drift apart the way Room.say and murmurAudience once did.
|
|
120
|
+
*
|
|
121
|
+
* THE CIRCULATING ASYMMETRY, which is the actual defect this item was
|
|
122
|
+
* filed about. "Circulating" (no atSpot) is the compat state meaning
|
|
123
|
+
* "somewhere in this room, unfixed", and the old rule read it as "at
|
|
124
|
+
* every spot at once" -- so a circulating listener heard everything.
|
|
125
|
+
* That is how a patrol officer walking through the back room answered a
|
|
126
|
+
* question put to a Johnson at a table (CugfYsH8DWH8FcfgS, same
|
|
127
|
+
* session): Kessler was `moving through`, so he was nowhere, so he was
|
|
128
|
+
* everywhere.
|
|
129
|
+
*
|
|
130
|
+
* ASK spotOf, NEVER actorCell. A circulating body IS given a cell by
|
|
131
|
+
* seatingIn -- its "drift seat" -- and that cell is deliberately not a
|
|
132
|
+
* position: spots.ts says so in as many words ("a place to DRAW
|
|
133
|
+
* someone, never a position they hold"), and sameSpot and canReach both
|
|
134
|
+
* short-circuit on atSpot before any of it is consulted. A distance
|
|
135
|
+
* check written against the drift seat would have put Kessler a
|
|
136
|
+
* plausible two metres from the table and let him hear after all --
|
|
137
|
+
* correct-looking arithmetic over a number that means nothing.
|
|
138
|
+
*
|
|
139
|
+
* A LISTENER with no position is therefore NOT in earshot of a murmur:
|
|
140
|
+
* someone crossing the room is not standing at your table. A SPEAKER
|
|
141
|
+
* with no position still reaches the room, and that is deliberate rather
|
|
142
|
+
* than an oversight -- they are moving among everybody, there is no
|
|
143
|
+
* "here" for the words to fall short of, and refusing to let a
|
|
144
|
+
* circulating player be heard at all would make the game's main verb
|
|
145
|
+
* fail for the commonest position in it.
|
|
146
|
+
*
|
|
147
|
+
* A room that declares NO spots keeps the old room-wide behaviour
|
|
148
|
+
* outright. There is no geometry in such a room to be near or far in,
|
|
149
|
+
* and inventing one would refuse speech in the small abstract rooms
|
|
150
|
+
* where `say` is all there is.
|
|
151
|
+
*/
|
|
152
|
+
/**
|
|
153
|
+
* CAN THIS LISTENER HEAR A SHOUT FROM THERE (aCErAfvEW7G23FWDM).
|
|
154
|
+
*
|
|
155
|
+
* SHOUT_METERS was declared, documented, and never read: Room.shout
|
|
156
|
+
* reached the whole room and measured the distance only to DESCRIBE the
|
|
157
|
+
* shout (voiceTag), never to bound it. Written here beside canHearSpeech
|
|
158
|
+
* for the reason this module exists at all -- two rules answering "who
|
|
159
|
+
* hears this" in two files is how Room.say and murmurAudience drifted
|
|
160
|
+
* apart in the first place.
|
|
161
|
+
*
|
|
162
|
+
* The listener's own range extends a shout exactly as it extends a
|
|
163
|
+
* murmur: the ear is the rule, the mouth only sets the baseline.
|
|
164
|
+
*/
|
|
165
|
+
export function canHearShout(speaker, listener) {
|
|
166
|
+
const room = speaker.currentLocation;
|
|
167
|
+
if (!spotsActive(room))
|
|
168
|
+
return true;
|
|
169
|
+
if (spotOf(speaker) === undefined || spotOf(listener) === undefined)
|
|
170
|
+
return true;
|
|
171
|
+
const reach = SHOUT_METERS + (hearingRangeMeters(listener) - SPEECH_METERS);
|
|
172
|
+
return actorDistanceMeters(room, speaker, listener) <= reach;
|
|
173
|
+
}
|
|
174
|
+
export function canHearSpeech(speaker, listener) {
|
|
175
|
+
const room = speaker.currentLocation;
|
|
176
|
+
if (!spotsActive(room))
|
|
177
|
+
return true;
|
|
178
|
+
// The speaker is unplaced: they are moving among everyone.
|
|
179
|
+
if (spotOf(speaker) === undefined)
|
|
180
|
+
return true;
|
|
181
|
+
// The listener is unplaced: they are passing through, not standing here.
|
|
182
|
+
if (spotOf(listener) === undefined)
|
|
183
|
+
return false;
|
|
184
|
+
return actorDistanceMeters(room, speaker, listener) <= hearingRangeMeters(listener);
|
|
185
|
+
}
|
|
43
186
|
/**
|
|
44
187
|
* Everyone in the speaker's room close enough to catch a murmur.
|
|
45
188
|
* From a named spot: whoever shares it, plus anyone circulating.
|
|
@@ -213,6 +213,6 @@ export function hostDockLine(room, actor, width) {
|
|
|
213
213
|
: '';
|
|
214
214
|
const raw = `${inside ? '▣' : '●'} ${room.name} HR${room.hostRating}${tag}`;
|
|
215
215
|
const cut = [...raw].slice(0, Math.max(4, width)).join('');
|
|
216
|
-
return `{
|
|
216
|
+
return `{light-blue-fg}${cut}{/light-blue-fg}`;
|
|
217
217
|
}
|
|
218
218
|
//# sourceMappingURL=grid-reach.js.map
|
|
@@ -460,20 +460,20 @@ export function gridSculpt(actor, room) {
|
|
|
460
460
|
const purpose = room.host?.purpose;
|
|
461
461
|
const sculpt = room.host?.sculpt;
|
|
462
462
|
return [
|
|
463
|
-
`{
|
|
464
|
-
`{
|
|
463
|
+
`{light-blue-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]${purpose ? ` -- ${purpose}` : ''}{/light-blue-fg}`,
|
|
464
|
+
`{light-blue-fg}${sculpt ?? 'Architecture all the way around -- the outside grid is a wall away and might as well be weather.'}{/light-blue-fg}`,
|
|
465
465
|
].join('\n');
|
|
466
466
|
}
|
|
467
467
|
// ON THE GRID (SR5 pp.217-218): no street under the persona -- a black
|
|
468
468
|
// flatland, the body's room named only because that is where the
|
|
469
469
|
// signal comes from. A host directly overhead gets a line of its own.
|
|
470
470
|
const body = actor.bodyRoom ?? room;
|
|
471
|
-
const header = `{
|
|
471
|
+
const header = `{light-blue-fg}THE GRID :: jacked in from ${body.name.toUpperCase()}{/light-blue-fg}`;
|
|
472
472
|
const grid = actor.currentGrid;
|
|
473
|
-
const flat = `{
|
|
473
|
+
const flat = `{light-blue-fg}Black flatland under a black sky -- the icons are the only things out here.${grid ? ` Riding ${grid.name}${grid.userPenalty ? ' (-2 on everything you do, p.233)' : ''}.` : ''}{/light-blue-fg}`;
|
|
474
474
|
if (!room.hasNode)
|
|
475
475
|
return `${header}\n${flat}`;
|
|
476
|
-
return `${header}\n${flat}\n{
|
|
476
|
+
return `${header}\n${flat}\n{light-blue-fg}${hostLabel(room, { capital: true })} hangs directly overhead, sealed. What it holds is inside it.{/light-blue-fg}`;
|
|
477
477
|
}
|
|
478
478
|
/**
|
|
479
479
|
* The icons rendered as the standard cyan block, or the thin-grid line.
|
|
@@ -502,19 +502,19 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
502
502
|
if (actor.insideHost || !rooms) {
|
|
503
503
|
const icons = gridIcons(scene, actor, room);
|
|
504
504
|
if (icons.length === 0) {
|
|
505
|
-
return `{
|
|
505
|
+
return `{light-blue-fg}Thin grid out here -- background noise, nothing worth cracking.{/light-blue-fg}`;
|
|
506
506
|
}
|
|
507
|
-
return `{
|
|
507
|
+
return `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${icons.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`;
|
|
508
508
|
}
|
|
509
509
|
const out = [];
|
|
510
510
|
const hosts = hostsInReach(rooms, actor);
|
|
511
511
|
out.push(hosts.length > 0
|
|
512
|
-
? `{
|
|
513
|
-
: `{
|
|
512
|
+
? `{light-blue-fg}HOSTS OVERHEAD:{/light-blue-fg}\n${hosts.map(h => ` {light-blue-fg}${hostLine(h, actor)}{/light-blue-fg}`).join('\n')}`
|
|
513
|
+
: `{light-blue-fg}Nothing overhead -- no host stands over this district.{/light-blue-fg}`);
|
|
514
514
|
const near = gridIcons(scene, actor, gridVicinity(actor)).filter(i => !isHostLine(i));
|
|
515
515
|
out.push(near.length > 0
|
|
516
|
-
? `{
|
|
517
|
-
: `{
|
|
516
|
+
? `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${near.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`
|
|
517
|
+
: `{light-blue-fg}Nothing near -- background noise around your signal.{/light-blue-fg}`);
|
|
518
518
|
// FARTHER OUT, WITHOUT AN ADDRESS (SR5 p.235, p.243): Matrix Perception
|
|
519
519
|
// never tells you where a device physically sits -- that is Trace Icon,
|
|
520
520
|
// two marks and an opposed test. So far icons are listed by the noise
|
|
@@ -539,9 +539,9 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
539
539
|
byBand.set(noise, list);
|
|
540
540
|
}
|
|
541
541
|
const far = [...byBand.entries()].sort((a, b) => a[0] - b[0])
|
|
542
|
-
.map(([noise, names]) => ` {
|
|
542
|
+
.map(([noise, names]) => ` {light-blue-fg}· dim, noise -${noise}: ${names.join(', ')}{/light-blue-fg}`);
|
|
543
543
|
if (far.length > 0)
|
|
544
|
-
out.push(`{
|
|
544
|
+
out.push(`{light-blue-fg}FARTHER OUT -- dimmer with distance:{/light-blue-fg}\n${far.join('\n')}`);
|
|
545
545
|
// A blank line between the distance bands (user, 2026-09-02): three
|
|
546
546
|
// sections read as three when they do not touch.
|
|
547
547
|
return out.join('\n\n');
|
|
@@ -556,7 +556,7 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
556
556
|
* the other personas, and the files. Cut to the dock's rows and columns.
|
|
557
557
|
*/
|
|
558
558
|
export function hostInteriorPanel(scene, actor, host, rows, cols) {
|
|
559
|
-
const cut = (raw) => `{
|
|
559
|
+
const cut = (raw) => `{light-blue-fg}${[...raw].slice(0, Math.max(4, cols)).join('')}{/light-blue-fg}`;
|
|
560
560
|
const lines = [];
|
|
561
561
|
lines.push(cut(`INSIDE ${host.name}`));
|
|
562
562
|
if (host.purpose)
|
|
@@ -8,8 +8,16 @@ import { Logger } from './logger.js';
|
|
|
8
8
|
* and nothing happened" cannot recur.
|
|
9
9
|
*/
|
|
10
10
|
export const PATIENCE_BEATS = 3;
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* What a hostile watching you do it will not let pass (performAction verbs).
|
|
13
|
+
*
|
|
14
|
+
* `grapples` joined on 2026-09-15 (aCErAfvEW7G23FWDM). Seizing someone
|
|
15
|
+
* is a melee attack in canon (p.195) and reads as one to anybody in the
|
|
16
|
+
* room, but the ladder did not list it -- so a hostile could stand and
|
|
17
|
+
* watch you lay hands on their colleague and count it as patience. The
|
|
18
|
+
* verb is the one grapple.ts passes to performAction.
|
|
19
|
+
*/
|
|
20
|
+
export const PROVOCATION = /^(draws\b|casts\b|chants a summoning|grapples\b)/;
|
|
13
21
|
/** The hostiles in `room` who could open on `player` -- structural, no LLM. */
|
|
14
22
|
export function hostilesIn(scene, room, player) {
|
|
15
23
|
const out = [];
|
|
@@ -43,6 +51,44 @@ export function standDown(npc) {
|
|
|
43
51
|
npc.hostile = false;
|
|
44
52
|
npc.hostileContact = undefined;
|
|
45
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* THE MIRROR OF standDown, and until 2026-09-15 it did not exist
|
|
56
|
+
* (aCErAfvEW7G23FWDM).
|
|
57
|
+
*
|
|
58
|
+
* `hostile` was written at BOOT and nowhere else -- scene-factory from
|
|
59
|
+
* the seed, street-gang at ganger spawn, ic-actors for a program. The
|
|
60
|
+
* only runtime writer in the whole engine was standDown, which can
|
|
61
|
+
* only ever turn hostility OFF. So an NPC who was not hostile when the
|
|
62
|
+
* scene was built had no path to becoming hostile, whatever you did to
|
|
63
|
+
* them: the bartender you grabbed by the collar could be provoked in
|
|
64
|
+
* prose and never in fact.
|
|
65
|
+
*
|
|
66
|
+
* ONE WRITER, here, for the same reason hostile-contact.ts has always
|
|
67
|
+
* insisted on one opener (see the header): hostility that can be set
|
|
68
|
+
* from anywhere is hostility nobody can explain afterwards. Callers
|
|
69
|
+
* pass WHY, and it is logged.
|
|
70
|
+
*
|
|
71
|
+
* This does NOT open a fight by itself. It puts the NPC on the ladder
|
|
72
|
+
* -- noticed, and provoked as of this beat -- and the ordinary
|
|
73
|
+
* hostileContactBeat decides when they act on it. An act that should
|
|
74
|
+
* start a fight NOW opens the encounter itself (grapple.ts, attack.ts).
|
|
75
|
+
*/
|
|
76
|
+
export function turnHostile(npc, beat, why) {
|
|
77
|
+
if (npc.hostile === true) {
|
|
78
|
+
// Already opposition: the act still counts as provocation, which is
|
|
79
|
+
// what moves a patient hostile off the fence.
|
|
80
|
+
registerProvocation(npc, beat);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// A COMPANION IS NEVER TURNED. A bound spirit or a hired drone
|
|
84
|
+
// answers to its master (models/npc.ts allyOf); flipping one hostile
|
|
85
|
+
// from a passing act would hand the player's own crew to the scene.
|
|
86
|
+
if (npc.allyOf)
|
|
87
|
+
return;
|
|
88
|
+
npc.hostile = true;
|
|
89
|
+
npc.hostileContact = { noticedBeat: beat, provokedBeat: beat };
|
|
90
|
+
Logger.getInstance().write(`${npc.name} turns hostile: ${why}.`);
|
|
91
|
+
}
|
|
46
92
|
function onPhysicalPlane(player) {
|
|
47
93
|
return player.plane === 'meat' || player.plane === 'drone';
|
|
48
94
|
}
|