@maka/maka-cli 5.196.0 → 5.198.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/advance.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/ar.js +6 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/buy.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +8 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/download.js +3 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/enter-host.js +5 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +62 -13
- 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/initiate.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/install.js +6 -1
- 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/quality-trade.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/register.js +6 -1
- 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/sell.js +6 -1
- 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/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 +45 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +13 -5
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +80 -57
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +24 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/action-cost.js +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ar.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/earshot.js +116 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +83 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +14 -14
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-style.js +1 -1
- 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/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { NPC } from '../models/npc.js';
|
|
3
|
+
import { hint } from '../utilities/hints.js';
|
|
4
|
+
import { SHOUT_METERS } from '../utilities/earshot.js';
|
|
5
|
+
import { speakOnTheGrid } from '../utilities/matrix-roster.js';
|
|
6
|
+
/**
|
|
7
|
+
* SHOUTING IS A VERB NOW (en3p6eCpiRuqsTSKm: "add 'shout' can be heard
|
|
8
|
+
* by anyone within a reasonable distance, 50m?").
|
|
9
|
+
*
|
|
10
|
+
* It used to be something that HAPPENED TO YOU. Room.say escalated to a
|
|
11
|
+
* room-wide shout whenever nobody shared your spot, so a runner standing
|
|
12
|
+
* on their own could not say anything quietly, and a runner at a crowded
|
|
13
|
+
* table could not deliberately call across the room. Both halves of the
|
|
14
|
+
* volume decision belonged to the furniture rather than to the player.
|
|
15
|
+
*
|
|
16
|
+
* Now `say` carries to whoever is close enough to hear it (earshot.ts)
|
|
17
|
+
* and this is how you reach everyone else. The range is this engine's
|
|
18
|
+
* number, not the book's -- see SHOUT_METERS -- and in practice it is
|
|
19
|
+
* room-wide, because the largest room the generator builds is tens of
|
|
20
|
+
* metres across.
|
|
21
|
+
*
|
|
22
|
+
* WHAT IT COSTS, and why that is not nothing: every hostile in the room
|
|
23
|
+
* hears you, and shouting is not sneaking. `sneaking` is dropped here
|
|
24
|
+
* the same way `say` drops it -- talking on comms is not a quiet
|
|
25
|
+
* activity, and bellowing across a warehouse is less so.
|
|
26
|
+
*/
|
|
27
|
+
export class ShoutCommand extends Command {
|
|
28
|
+
static verb = 'shout';
|
|
29
|
+
static description = `Raise your voice so the whole place hears it (~${SHOUT_METERS}m), instead of just whoever is standing close. "say" is ordinary speech and only carries as far as a listener's ears reach -- roughly 5m, further for someone with the chrome for it. Shouting reaches everyone in the room, hostiles included, and drops any sneaking.`;
|
|
30
|
+
context;
|
|
31
|
+
constructor(context) {
|
|
32
|
+
super(context);
|
|
33
|
+
this.context = context;
|
|
34
|
+
}
|
|
35
|
+
async execute(args) {
|
|
36
|
+
if (!args || args.length === 0) {
|
|
37
|
+
return `What do you want to shout?${hint(` ("shout <message>" -- "say" keeps it between you and whoever is close.)`)}`;
|
|
38
|
+
}
|
|
39
|
+
const message = args.join(' ');
|
|
40
|
+
const actor = this.actor;
|
|
41
|
+
// THE PLANES, worded as say.ts words them -- one mechanic, one set
|
|
42
|
+
// of sentences. An astral voice never reaches meat ears and a
|
|
43
|
+
// persona's never leaves the grid, and volume changes neither: a
|
|
44
|
+
// louder voice on the wrong plane is still no voice at all.
|
|
45
|
+
if (actor.plane === 'astral') {
|
|
46
|
+
return `You can shout all you like -- the meat world hears nothing. Spirits and projections do.`;
|
|
47
|
+
}
|
|
48
|
+
// Talking is not sneaking (sneak.ts); shouting least of all.
|
|
49
|
+
actor.sneaking = false;
|
|
50
|
+
if (actor.plane === 'matrix') {
|
|
51
|
+
// ON THE GRID THERE IS NO DISTANCE (grid-reach.ts): personas hear
|
|
52
|
+
// each other or they do not, and no amount of volume crosses a
|
|
53
|
+
// host wall. So this routes to the same fan-out `say` uses rather
|
|
54
|
+
// than inventing a louder one.
|
|
55
|
+
const heard = speakOnTheGrid(this.scene, actor, message).map(a => a.name);
|
|
56
|
+
return heard.length > 0
|
|
57
|
+
? `You push it out across the grid: "${message}"\n Heard by ${heard.join(', ')}.`
|
|
58
|
+
: `You push it out across the grid -- nothing out here is listening.`;
|
|
59
|
+
}
|
|
60
|
+
const heard = actor.currentLocation.shout(actor, message);
|
|
61
|
+
if (!(this.actor instanceof NPC)) {
|
|
62
|
+
this.actor.performAction('shouts', actor.currentLocation.name, { quiet: true });
|
|
63
|
+
}
|
|
64
|
+
return heard.length > 0
|
|
65
|
+
? `You raise your voice: "${message}"`
|
|
66
|
+
: `You raise your voice: "${message}"\n Nothing answers -- there's nobody here to hear it.`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=shout.js.map
|
|
@@ -49,7 +49,7 @@ export class SnoopCommand extends Command {
|
|
|
49
49
|
this.logger.write(`Snoop: ${actor.name} watching ${target.name} via camera feed.`);
|
|
50
50
|
const lines = [`CAMERA FEED: ${target.name.toUpperCase()}`];
|
|
51
51
|
if (!camerasLive(allRooms, target)) {
|
|
52
|
-
lines.push(`{
|
|
52
|
+
lines.push(`{light-blue-fg}(Security sees a loop of empty hallways -- these eyes answer only to you now.){/light-blue-fg}`);
|
|
53
53
|
}
|
|
54
54
|
const seen = this.scene.getActorsInRoom(target)
|
|
55
55
|
.filter(a => a !== actor && a.plane === 'meat' && !a.sneaking);
|
|
@@ -73,7 +73,7 @@ export class SpritesCommand extends Command {
|
|
|
73
73
|
const name = type?.name ?? set.spriteKey;
|
|
74
74
|
const isMine = mineType === set.spriteKey;
|
|
75
75
|
const powers = powersForSprite(set.spriteKey);
|
|
76
|
-
lines.push(`${name.toUpperCase()} SPRITE${isMine ? ' {
|
|
76
|
+
lines.push(`${name.toUpperCase()} SPRITE${isMine ? ' {light-blue-fg}(yours){/light-blue-fg}' : ''}${type ? ` -- ${type.blurb}` : ''}`);
|
|
77
77
|
lines.push(` Skills: ${skillsForSprite(set.spriteKey).join(', ')}${hint(` (${set.page})`)}`);
|
|
78
78
|
if (!detailed) {
|
|
79
79
|
// The names alone, which is the comparison view.
|
|
@@ -79,8 +79,8 @@ export function resolveSpokenTarget(parsedArgs, names) {
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
export class TalkCommand extends Command {
|
|
82
|
-
static verb = "
|
|
83
|
-
static description = "Speak privately to
|
|
82
|
+
static verb = "tell";
|
|
83
|
+
static description = "Speak privately to ONE person standing with you -- only they hear it, however loud the room is. \"tell <name> <message>\". (\"talk-to\" is the old spelling and still works.) Compare \"say\", which carries to whoever is close enough to hear, and \"shout\", which reaches the whole place.";
|
|
84
84
|
context;
|
|
85
85
|
constructor(context) {
|
|
86
86
|
super(context);
|
|
@@ -164,9 +164,9 @@ export class TapCommand extends Command {
|
|
|
164
164
|
}
|
|
165
165
|
this.logger.write(`${actor.name} now holds ${now} mark(s) on ${host.name} via the ${room.name} cameras.`);
|
|
166
166
|
const lines = [
|
|
167
|
-
`{
|
|
168
|
-
` {
|
|
169
|
-
` {
|
|
167
|
+
`{light-blue-fg}The filament finds the connector and the camera stops being a camera -- it's a device, and it's talking to you.{/light-blue-fg}`,
|
|
168
|
+
` {light-blue-fg}It never gets to hide behind ${hostLabel(host)}: a cable doesn't ask the network for permission, so the lens has to defend itself with what a lens has. It doesn't have much.{/light-blue-fg}`,
|
|
169
|
+
` {light-blue-fg}MARK PLACED -- and it lands on the host too. The camera answers to ${host.name}, and anything ${host.name} trusts to speak for it just handed you a key. [${now}/${MAX_MARKS}]{/light-blue-fg}`,
|
|
170
170
|
];
|
|
171
171
|
if (mode === 'attack') {
|
|
172
172
|
lines.push(` The housing is visibly forced -- the host FELT that one, even if nothing saw your face.`);
|
|
@@ -175,15 +175,15 @@ export class TapCommand extends Command {
|
|
|
175
175
|
lines.push(` Nothing logged, nothing flagged. You were never here.`);
|
|
176
176
|
}
|
|
177
177
|
if (host === room) {
|
|
178
|
-
lines.push(` {
|
|
178
|
+
lines.push(` {light-blue-fg}${now >= MAX_MARKS
|
|
179
179
|
? `The host thinks you belong now -- "hack" walks straight in.`
|
|
180
|
-
: `${hostLabel(host, { capital: true })} cracks easier with every key you hold${hint(` -- "jack in" and "hack", or come back for another lens`)}.`}{/
|
|
180
|
+
: `${hostLabel(host, { capital: true })} cracks easier with every key you hold${hint(` -- "jack in" and "hack", or come back for another lens`)}.`}{/light-blue-fg}`);
|
|
181
181
|
}
|
|
182
182
|
else {
|
|
183
|
-
lines.push(` {
|
|
183
|
+
lines.push(` {light-blue-fg}These lenses ride the site network, so the key fits ${hostLabel(host)}${hint(` -- "jack in", "go ${host.name}", "hack"`)}.{/light-blue-fg}`);
|
|
184
184
|
}
|
|
185
185
|
// A mark is all canSnoopFeeds ever wanted (surveillance.ts).
|
|
186
|
-
lines.push(` {
|
|
186
|
+
lines.push(` {light-blue-fg}And the eyes are half yours already${hint(` -- jacked in, "snoop <room>" rides any feed on this network`)}.{/light-blue-fg}`);
|
|
187
187
|
lines.push(...godLines);
|
|
188
188
|
this.scene.updateStatus();
|
|
189
189
|
return lines.join('\n');
|
|
@@ -67,7 +67,7 @@ export class ThreadCommand extends Command {
|
|
|
67
67
|
const lines = [`COMPLEX FORMS (thread <form> <level>)`];
|
|
68
68
|
for (const f of FORMS) {
|
|
69
69
|
const held = actor.sustainedForms.some(s => s.key === f.key);
|
|
70
|
-
lines.push(` ${f.name.padEnd(16)} ${f.kind === 'sustained' ? '[sustained]' : '[instant] '} ${f.blurb}${held ? ' {
|
|
70
|
+
lines.push(` ${f.name.padEnd(16)} ${f.kind === 'sustained' ? '[sustained]' : '[instant] '} ${f.blurb}${held ? ' {light-blue-fg}(HELD){/light-blue-fg}' : ''}`);
|
|
71
71
|
}
|
|
72
72
|
if (actor.sustainedForms.length > 0) {
|
|
73
73
|
lines.push('', ` Holding ${actor.sustainedForms.length} form${actor.sustainedForms.length === 1 ? '' : 's'} (${sustainTotal(actor.sustainedLedger().filter(w => w.kind === 'form'))}).${hint(` ("thread drop <form>" releases.)`)}`);
|
|
@@ -482,5 +482,49 @@
|
|
|
482
482
|
// hub, characterSheet.hydrate -- silently ATE the lifestyle point of a
|
|
483
483
|
// runner who left fed and slept. The tier is on the save; it is read
|
|
484
484
|
// there now (commerce.lifestyleTierFor, Squatter when unnamed).
|
|
485
|
-
|
|
485
|
+
// 1.56.0 (2026-09-15): TWO FLAGS THAT WERE STANDING IN FOR STATE THEY
|
|
486
|
+
// DO NOT HOLD.
|
|
487
|
+
// - A CRACKED HOST IS NOT A HOST YOU HOLD MARKS ON
|
|
488
|
+
// (spcebZTKN24GYRHXW). `hack`/`mark` refused outright on the
|
|
489
|
+
// room-global hostCracked, so a persona that rebooted mid-run -- which
|
|
490
|
+
// canon wipes marks on (p.236, p.242) -- could never re-place one,
|
|
491
|
+
// while every device on the node went on refusing them for want of
|
|
492
|
+
// marks. Command semantics in shared scenes: the refusal is gone, the
|
|
493
|
+
// standing guard is 3 marks held, a repeat break-in is worded as one,
|
|
494
|
+
// and the host line prints the site's state and the persona's marks
|
|
495
|
+
// apart. The crack's world consequences stay permanent.
|
|
496
|
+
// - NO CAB MID-FIGHT (hHvR2kxrqAKuuXwED). The downtime verbs (buy,
|
|
497
|
+
// sell, install, register, initiate, quality-trade, advance, call
|
|
498
|
+
// taxi) gated on Player.inExchange, a re-entrancy lock that is false
|
|
499
|
+
// for most of a Combat Turn, so each refused for the half-second an
|
|
500
|
+
// exchange resolved and permitted itself the rest of the fight. They
|
|
501
|
+
// ask inLiveCombat (action-cost.ts) now. NEW EMBEDDER SURFACE:
|
|
502
|
+
// Scene.anyLiveEncounter(), which the site reads off the live handle
|
|
503
|
+
// to refuse sideQuest.settle -- on a shared run the cab is intercepted
|
|
504
|
+
// before the engine ever sees it, so the server is the only place
|
|
505
|
+
// that path can be stopped.
|
|
506
|
+
// 1.57.0 (2026-09-15): A VOICE CARRIES AS FAR AS THE EAR, and two
|
|
507
|
+
// spellings change with it.
|
|
508
|
+
// - `say` was resolved by SPOT NAME -- whoever shared your spot, plus
|
|
509
|
+
// anyone CIRCULATING, because "no position" was read as "every
|
|
510
|
+
// position". A patrol officer walking through a back room answered a
|
|
511
|
+
// question put to a Johnson at a table. It also ESCALATED on its own:
|
|
512
|
+
// alone at a spot, a murmur became a room-wide shout nobody asked
|
|
513
|
+
// for. Now `say` reaches whoever is inside their OWN hearing range
|
|
514
|
+
// (utilities/earshot.ts canHearSpeech; range is 5 m plus the
|
|
515
|
+
// listener's perception dice) and never escalates. Canon gives no
|
|
516
|
+
// distance for speech -- it is dice and limits, p.445/p.453-454 --
|
|
517
|
+
// so the metres are this engine's and are labelled as such.
|
|
518
|
+
// - NEW VERB `shout` (~50 m, in practice room-wide) is the old
|
|
519
|
+
// escalation, now deliberate.
|
|
520
|
+
// - `talk-to` is spelled `tell`. Command semantics in shared scenes:
|
|
521
|
+
// the old spelling stays registered as an alias, so nothing that
|
|
522
|
+
// learned it breaks.
|
|
523
|
+
// - COLOUR TAGS: the run-together "lightblue" spelling never parsed
|
|
524
|
+
// in blessed (its tag parser wants "light blue fg", hyphens turned
|
|
525
|
+
// to spaces) and was printed to CLI players as literal braces for
|
|
526
|
+
// months, while the web client rendered it perfectly. 134 tags are
|
|
527
|
+
// now {light-blue-fg}. The site's render-tags.ts maps that back
|
|
528
|
+
// to a real CSS keyword, so the pin must carry both halves.
|
|
529
|
+
export const ENGINE_VERSION = '1.57.0';
|
|
486
530
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -73,6 +73,7 @@ import { LockCommand } from './commands/lock.js';
|
|
|
73
73
|
import { SearchCommand } from './commands/search.js';
|
|
74
74
|
import { SayCommand } from './commands/say.js';
|
|
75
75
|
import { TalkCommand } from './commands/talk.js';
|
|
76
|
+
import { ShoutCommand } from './commands/shout.js';
|
|
76
77
|
import { StoreCommand } from './commands/store.js';
|
|
77
78
|
import { OpenCommand, CloseCommand, PutCommand } from './commands/container-verbs.js';
|
|
78
79
|
import { GiveCommand } from './commands/give.js';
|
|
@@ -708,7 +709,7 @@ export default class Game {
|
|
|
708
709
|
Logger.getInstance().write(`Loose end "${c.id}" left: ${erased ? 'ERASED' : 'still open'}; hub is the scene again.`);
|
|
709
710
|
this.requestSave('cleanup');
|
|
710
711
|
return [erased
|
|
711
|
-
? `\n{
|
|
712
|
+
? `\n{light-blue-fg}The grid closes behind you. ${le?.site ?? 'The site'}'s feeds run clean -- whatever they had of you is gone.{/light-blue-fg}`
|
|
712
713
|
: `\n{yellow-fg}The grid closes behind you. The footage is still up there.{/yellow-fg}${hint(` (Jack in and "hop" again while the desk hasn't reviewed it.)`)}`];
|
|
713
714
|
}
|
|
714
715
|
/** The HUD's read on the district (bands, never the number). */
|
|
@@ -2692,7 +2693,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2692
2693
|
CommandFactory.registerCommand('open', OpenCommand);
|
|
2693
2694
|
CommandFactory.registerCommand('close', CloseCommand);
|
|
2694
2695
|
CommandFactory.registerCommand('put', PutCommand);
|
|
2696
|
+
// TELL IS THE VERB (en3p6eCpiRuqsTSKm: "let's adjust 'talk-to' to
|
|
2697
|
+
// 'tell' to be more concise"). `talk-to` stays registered as an
|
|
2698
|
+
// alias rather than being deleted: it is the spelling in every
|
|
2699
|
+
// transcript, every NPC brief and every backlog item written before
|
|
2700
|
+
// today, and a player who learned it should not be told it is gone.
|
|
2701
|
+
CommandFactory.registerCommand('tell', TalkCommand);
|
|
2695
2702
|
CommandFactory.registerCommand('talk-to', TalkCommand);
|
|
2703
|
+
CommandFactory.registerCommand('shout', ShoutCommand);
|
|
2696
2704
|
CommandFactory.registerCommand('give', GiveCommand);
|
|
2697
2705
|
CommandFactory.registerCommand('read', ReadCommand);
|
|
2698
2706
|
CommandFactory.registerCommand('kill', KillCommand);
|
|
@@ -5955,7 +5963,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5955
5963
|
// works the grid while you're back in the meat (the AR-drop exit).
|
|
5956
5964
|
const remoteAgent = this.companions.find(c => c.kind === 'agent');
|
|
5957
5965
|
if (remoteAgent && player.plane !== 'matrix') {
|
|
5958
|
-
lines.push(`{
|
|
5966
|
+
lines.push(`{light-blue-fg}▣ DECK IN AR -- ${remoteAgent.npc.name} on the grid${remoteAgent.npc.objective ? ', working its directive' : ''}${hint(' ("jack in" to rejoin it, "recall" to fold it home)')}{/light-blue-fg}`);
|
|
5959
5967
|
}
|
|
5960
5968
|
// Off-plane context: which reality you're in, your persona's Matrix
|
|
5961
5969
|
// track (cold-sim damage lands there), and where your empty body lies.
|
|
@@ -5974,7 +5982,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5974
5982
|
const track = jackedDeck
|
|
5975
5983
|
? ` Deck A${jackedDeck.attack} S${jackedDeck.sleaze} D${jackedDeck.dataProcessing} F${jackedDeck.firewall} ${jackedDeck.deckSummary()}${player.simMode === 'hot' ? ' +biofeedback' : ''}${pgms}`
|
|
5976
5984
|
: ` (living persona -- damage is REAL stun)`;
|
|
5977
|
-
lines.push(`{
|
|
5985
|
+
lines.push(`{light-blue-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/light-blue-fg}`);
|
|
5978
5986
|
}
|
|
5979
5987
|
else if (player.plane === 'astral') {
|
|
5980
5988
|
lines.push(`{magenta-fg}✧ ASTRAL -- ${player.astralTicks} move${player.astralTicks === 1 ? '' : 's'} out -- body at ${player.bodyRoom?.name ?? '???'}{/magenta-fg}`);
|
|
@@ -6066,7 +6074,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6066
6074
|
const dockW = mapWidth ?? 15;
|
|
6067
6075
|
const lines = hosts.length > 0
|
|
6068
6076
|
? hosts.map(h => hostDockLine(h, this.player, dockW))
|
|
6069
|
-
: ['{
|
|
6077
|
+
: ['{light-blue-fg}nothing overhead{/light-blue-fg}'];
|
|
6070
6078
|
lines.push(caption(this.player.insideHost ? `inside ${this.player.insideHost.name}` : `on the grid from ${gridVicinity(this.player).name}`, mapWidth));
|
|
6071
6079
|
this.setMapOverlay(lines);
|
|
6072
6080
|
if (onTheGrid) {
|
|
@@ -6575,7 +6583,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6575
6583
|
// technomancer-gated (a sprite task, SR5 p.256) and refuses anyone
|
|
6576
6584
|
// else, so the Emerged shelf is where its audience looks.
|
|
6577
6585
|
{ title: 'resonance', entries: [['compile'], ['decompile'], ['thread'], ['unravel'], ['register'], ['standby'], ['sustain'], ['sprites']] },
|
|
6578
|
-
{ title: 'social', entries: [['say'], ['emote', 'me'], ['talk-to'], ['call'], ['message'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
6586
|
+
{ title: 'social', entries: [['say'], ['shout'], ['emote', 'me'], ['tell', 'talk-to'], ['call'], ['message'], ['end-call', 'end', 'hangup'], ['accept'], ['decline', 'deny', 'refuse'], ['haggle', 'negotiate'], ['persuade', 'convince'], ['ask'], ['contacts']] },
|
|
6579
6587
|
{ title: 'street', entries: [['abandon'], ['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['garage'], ['pay']] },
|
|
6580
6588
|
{ title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
|
|
6581
6589
|
{ title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
|
|
@@ -9,7 +9,8 @@ import * as fuzzyMatch from '../utilities/fuzzy-match.js';
|
|
|
9
9
|
import { sameHostSide } from './player.js';
|
|
10
10
|
import { AI } from '../../../../tools/ai/ai.class.js';
|
|
11
11
|
import { AbstractRoom } from '../types/shared/abstracts.js';
|
|
12
|
-
import {
|
|
12
|
+
import { spotsActive, actorDistanceMeters, voiceBandFor, voiceTag, doorwayKindOf, exitDoorwayKind } from '../utilities/spots.js';
|
|
13
|
+
import { canHearSpeech } from '../utilities/earshot.js';
|
|
13
14
|
import { synthesizeGrid, } from '../utilities/room-grid.js';
|
|
14
15
|
export class Room extends AbstractRoom {
|
|
15
16
|
logger = Logger.getInstance();
|
|
@@ -724,16 +725,37 @@ Keep it concise and dramatic.`;
|
|
|
724
725
|
const others = mouths - 1;
|
|
725
726
|
return ` [System note: ${others} other ${others === 1 ? 'person' : 'people'} in earshot heard this too, and each is answering in the same moment as you -- none of you can see what the others are about to say. Say YOUR piece, in your own voice, but not the same piece: if the obvious reply is one anyone here would give, make yours specific to you instead. Two people reporting one event reads as a joke.]`;
|
|
726
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* ORDINARY SPEECH, which now carries as far as the LISTENER can hear
|
|
730
|
+
* and no further (en3p6eCpiRuqsTSKm: "'say' should only be perceived
|
|
731
|
+
* by anyone within 3-5m un-augmented. Really, it's the audible
|
|
732
|
+
* perception range of the listener").
|
|
733
|
+
*
|
|
734
|
+
* WHAT THIS REPLACES, and why the old shape produced the report.
|
|
735
|
+
* Speech used to be resolved by SPOT NAME: whoever shared your spot
|
|
736
|
+
* heard you, plus anyone CIRCULATING -- and if nobody shared your
|
|
737
|
+
* spot, the words escalated to a room-wide shout automatically. Two
|
|
738
|
+
* consequences, both reported in one session:
|
|
739
|
+
*
|
|
740
|
+
* - A circulating actor heard everything, because "no spot" was read
|
|
741
|
+
* as "everywhere". That is how Patrol Officer Kessler, `moving
|
|
742
|
+
* through` the back room, answered a question put to Ms. Quill at
|
|
743
|
+
* a table (CugfYsH8DWH8FcfgS).
|
|
744
|
+
* - Standing anywhere on your own turned every murmur into a shout
|
|
745
|
+
* without you asking for one, so there was no way to say something
|
|
746
|
+
* quietly and no verb that meant "I want the room to hear this".
|
|
747
|
+
*
|
|
748
|
+
* Now: earshot.ts owns the rule (canHearSpeech), distance is real
|
|
749
|
+
* metres between cells, and the range belongs to each listener's ears.
|
|
750
|
+
* The escalation is gone -- SHOUTING IS A VERB NOW (commands/shout.ts,
|
|
751
|
+
* Room.shout below). Saying something nobody is close enough to catch
|
|
752
|
+
* is a thing that can happen, and say.ts tells you so.
|
|
753
|
+
*
|
|
754
|
+
* Returns who actually heard it, so the caller can say "nobody did"
|
|
755
|
+
* without asking the same question a second way and risking a
|
|
756
|
+
* different answer.
|
|
757
|
+
*/
|
|
727
758
|
say(actor, message) {
|
|
728
|
-
// "At" spots: speech is LOCAL (player decision: speech local, sight
|
|
729
|
-
// room-wide -- walking into the bar had every NPC in the room
|
|
730
|
-
// engaging at once). Same-spot and circulating listeners hear
|
|
731
|
-
// normally. When NOBODY shares the speaker's spot but others are in
|
|
732
|
-
// the room, the words carry as a SHOUT -- delivered room-wide with
|
|
733
|
-
// a tag so an NPC AI reacts to being called across the room, not to
|
|
734
|
-
// bar-side murmur. When same-spot listeners exist, cross-spot
|
|
735
|
-
// actors hear nothing (v1: the murmur doesn't carry).
|
|
736
|
-
const speakerSpot = spotOf(actor);
|
|
737
759
|
// THE HOST WALL IS ALSO A WALL FOR VOICES (FhXadizkMCxAdMaJx).
|
|
738
760
|
//
|
|
739
761
|
// Every verb that PICKS a target honours sameHostSide -- that is what
|
|
@@ -748,66 +770,67 @@ Keep it concise and dramatic.`;
|
|
|
748
770
|
// have insideHost undefined, so they compare equal and nothing about
|
|
749
771
|
// ordinary speech changes.
|
|
750
772
|
const listeners = [...this.actors].filter(r => r !== actor && sameHostSide(actor, r));
|
|
773
|
+
const audience = listeners.filter(r => canHearSpeech(actor, r));
|
|
751
774
|
// broadcast: true -- room speech is overheard, not addressed. A
|
|
752
775
|
// companion shell at heel uses that distinction to stay quiet (see
|
|
753
|
-
// NPC.hear); targeted channels (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const local = listeners.filter(r => {
|
|
773
|
-
const rs = spotOf(r);
|
|
774
|
-
return rs === undefined || rs === speakerSpot;
|
|
775
|
-
});
|
|
776
|
-
if (local.length > 0) {
|
|
777
|
-
const crowd = this.crowdNote(local);
|
|
778
|
-
for (const recipient of local) {
|
|
779
|
-
void Promise.resolve(recipient.hear(actor, recipient instanceof NPC ? `${message}${crowd}` : message, { broadcast: true })).catch(err => {
|
|
780
|
-
this.logger.error(`${recipient.name} failed to hear ${actor.name}: ${err}`);
|
|
781
|
-
});
|
|
782
|
-
}
|
|
783
|
-
return;
|
|
776
|
+
// NPC.hear); targeted channels (tell/order/ask...) never set it.
|
|
777
|
+
const crowd = this.crowdNote(audience);
|
|
778
|
+
// CAUGHT, like every other hear() fan-out in the game (say.ts,
|
|
779
|
+
// planes.ts, game.ts heartbeat). These were the only bare ones
|
|
780
|
+
// left: a bare `void` on a rejecting promise is an
|
|
781
|
+
// unhandledRejection, and ui.ts fatal handler kills the session on
|
|
782
|
+
// one -- which is exactly how a 502 from the AI relay ended a live
|
|
783
|
+
// game twice on 2026-08-29. Belt AND braces: NPC.hear now swallows
|
|
784
|
+
// its own reaction failures too, so this is redundant for NPCs on
|
|
785
|
+
// purpose -- it still guards any other Player subclass that grows
|
|
786
|
+
// an async hear().
|
|
787
|
+
//
|
|
788
|
+
// NO VOICE TAG. Everyone here is inside their own hearing range by
|
|
789
|
+
// construction, which is what voiceBandFor calls 'conversation' --
|
|
790
|
+
// the band that has always rendered as no tag at all.
|
|
791
|
+
for (const recipient of audience) {
|
|
792
|
+
void Promise.resolve(recipient.hear(actor, recipient instanceof NPC ? `${message}${crowd}` : message, { broadcast: true })).catch(err => {
|
|
793
|
+
this.logger.error(`${recipient.name} failed to hear ${actor.name}: ${err}`);
|
|
794
|
+
});
|
|
784
795
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
796
|
+
return audience;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* SHOUTING, which used to happen to you and is now something you do
|
|
800
|
+
* (en3p6eCpiRuqsTSKm: "add 'shout' can be heard by anyone within a
|
|
801
|
+
* reasonable distance, 50m?").
|
|
802
|
+
*
|
|
803
|
+
* Reaches the whole room -- see earshot.ts SHOUT_METERS for why 50 m
|
|
804
|
+
* is in practice room-wide and why the number is this engine's rather
|
|
805
|
+
* than the book's. Each listener is told how far the words travelled
|
|
806
|
+
* (voiceTag), so an NPC's AI answers someone calling across a bar
|
|
807
|
+
* differently from someone at its elbow.
|
|
808
|
+
*
|
|
809
|
+
* THE NPC RESTRAINT IS KEPT from the old auto-escalation, because the
|
|
810
|
+
* reason for it has not changed: when every NPC's raised voice went
|
|
811
|
+
* room-wide, two barflies at opposite ends held a shouted conversation
|
|
812
|
+
* about the player's drink order through each other's AI (a real
|
|
813
|
+
* session -- Patch and Old Cray). An NPC shout reaches the PLAYER, the
|
|
814
|
+
* camera rather than the cast. A PLAYER's shout reaches everyone,
|
|
815
|
+
* which is the whole point of typing it.
|
|
816
|
+
*/
|
|
817
|
+
shout(actor, message) {
|
|
818
|
+
const listeners = [...this.actors].filter(r => r !== actor && sameHostSide(actor, r));
|
|
797
819
|
const player = [...this.actors].find(a => !(a instanceof NPC));
|
|
798
|
-
const
|
|
820
|
+
const targets = actor instanceof NPC
|
|
799
821
|
? listeners.filter(r => r === player)
|
|
800
822
|
: listeners;
|
|
801
|
-
const
|
|
802
|
-
for (const recipient of
|
|
823
|
+
const crowd = this.crowdNote(targets);
|
|
824
|
+
for (const recipient of targets) {
|
|
803
825
|
const band = voiceBandFor(actorDistanceMeters(this, actor, recipient));
|
|
804
826
|
// The crowd note goes LAST, after the voice tag: both are system
|
|
805
827
|
// dressing, and the tag describes the words while the note
|
|
806
828
|
// describes the room.
|
|
807
|
-
void Promise.resolve(recipient.hear(actor, `${message}${voiceTag(band)}${recipient instanceof NPC ?
|
|
829
|
+
void Promise.resolve(recipient.hear(actor, `${message}${voiceTag(band)}${recipient instanceof NPC ? crowd : ''}`, { broadcast: true })).catch(err => {
|
|
808
830
|
this.logger.error(`${recipient.name} failed to hear ${actor.name} shout: ${err}`);
|
|
809
831
|
});
|
|
810
832
|
}
|
|
833
|
+
return targets;
|
|
811
834
|
}
|
|
812
835
|
addExit(direction, targetRoom, keyItemOrSolution) {
|
|
813
836
|
let door = this.findExistingDoor(targetRoom);
|
|
@@ -185,6 +185,30 @@ export class Scene extends AbstractScene {
|
|
|
185
185
|
const enc = this.encounterAt(arenaOf(actor));
|
|
186
186
|
return enc?.has(actor) ? enc : undefined;
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* IS ANYBODY AT THIS TABLE IN A FIGHT, asked across every arena.
|
|
190
|
+
*
|
|
191
|
+
* For the EMBEDDER, not for a verb (hHvR2kxrqAKuuXwED: "I was able to
|
|
192
|
+
* call a taxi in the middle of a fight"). On a shared run the ride
|
|
193
|
+
* home is not an engine verb at all -- both the CLI
|
|
194
|
+
* (utilities/shared-run.ts) and the browser (the site's run-exit.ts)
|
|
195
|
+
* intercept "call taxi" before it reaches the table and call
|
|
196
|
+
* sideQuest.settle, which ends the run for EVERYONE. So the engine's
|
|
197
|
+
* own gate in commands/call.ts, correct as it is, never ran on the
|
|
198
|
+
* path the report came in on, and the site had no way to ask this
|
|
199
|
+
* question without reaching into `encounters`.
|
|
200
|
+
*
|
|
201
|
+
* Every arena, not just the leader's: settling the table yanks the
|
|
202
|
+
* whole party home, so a decker in a host trading with ice is reason
|
|
203
|
+
* enough to refuse even if the leader is standing in an empty street.
|
|
204
|
+
*/
|
|
205
|
+
anyLiveEncounter() {
|
|
206
|
+
for (const enc of this.encounters.values()) {
|
|
207
|
+
if (!enc.ended)
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
188
212
|
/**
|
|
189
213
|
* Who is in the fight when `aggressor` opens on `target`: the two of
|
|
190
214
|
* them, every human and bound shell in the room (the party fights as
|
|
@@ -3,6 +3,42 @@ import { hint } from './hints.js';
|
|
|
3
3
|
export function encounterOf(scene, actor) {
|
|
4
4
|
return scene.encounterFor?.(actor);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* IS THERE A FIGHT ON, for a verb that simply cannot happen during one.
|
|
8
|
+
*
|
|
9
|
+
* `Player.inExchange` is NOT this question, and a family of verbs was
|
|
10
|
+
* asking it as though it were (hHvR2kxrqAKuuXwED: "I was able to call a
|
|
11
|
+
* taxi in the middle of a fight"). That flag is a re-entrancy lock --
|
|
12
|
+
* true only while one exchange is mid-RESOLUTION, set by
|
|
13
|
+
* CombatExchange.beginExchange and cleared by finishExchange -- so it is
|
|
14
|
+
* false for almost all of a Combat Turn, including the whole of your own
|
|
15
|
+
* Action Phase. Every verb that gated on it alone was therefore refusing
|
|
16
|
+
* during the narration beats of a single trade of fire and permitting
|
|
17
|
+
* itself freely the rest of the fight, while its refusal line said
|
|
18
|
+
* "firefight".
|
|
19
|
+
*
|
|
20
|
+
* The Combat Turn (utilities/combat-turn.ts) is what actually knows a
|
|
21
|
+
* fight is on: an encounter exists for the actor's arena and has not
|
|
22
|
+
* ended. That is the primary test here. The two exchange checks stay
|
|
23
|
+
* underneath it because an exchange can resolve outside an encounter
|
|
24
|
+
* (an NPC AI loop, a scene with no Combat Turn running), and because
|
|
25
|
+
* NPCs have no encounter phase of their own to consult.
|
|
26
|
+
*
|
|
27
|
+
* This is the "no" answer, not the "not yet" answer. Use requirePhase
|
|
28
|
+
* for something that IS legal in a fight but only on your own phase
|
|
29
|
+
* (walking out of the room); use this for something that is not a combat
|
|
30
|
+
* action at all and has no phase that could pay for it -- hailing a cab,
|
|
31
|
+
* ringing up a sale, a training montage.
|
|
32
|
+
*/
|
|
33
|
+
export function inLiveCombat(scene, actor) {
|
|
34
|
+
const enc = encounterOf(scene, actor);
|
|
35
|
+
if (enc && !enc.ended)
|
|
36
|
+
return true;
|
|
37
|
+
if (actor.inExchange)
|
|
38
|
+
return true;
|
|
39
|
+
return scene.isHumanControlled?.(actor) === true
|
|
40
|
+
&& scene.isPlayerExchangeActive?.(actor.name) === true;
|
|
41
|
+
}
|
|
6
42
|
/**
|
|
7
43
|
* The refusal when it is not this actor's Action Phase -- the wording
|
|
8
44
|
* every verb shares, so a player learns one sentence.
|
|
@@ -256,7 +256,7 @@ export function aroBlock(scene, actor, room) {
|
|
|
256
256
|
const tags = aroTags(scene, actor, room);
|
|
257
257
|
if (tags.length === 0)
|
|
258
258
|
return '';
|
|
259
|
-
return `{
|
|
259
|
+
return `{light-blue-fg}AR OVERLAY:{/light-blue-fg}\n${tags.map(t => ` {light-blue-fg}${t}{/light-blue-fg}`).join('\n')}`;
|
|
260
260
|
}
|
|
261
261
|
/** Gear that talks to the grid on its own, lying LOOSE in the room.
|
|
262
262
|
* Weapons are absent here because a carried one is handled above, as
|