@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
|
@@ -313,8 +313,8 @@ export class SearchCommand extends Command {
|
|
|
313
313
|
// tells a player standing next to an offline server that it is
|
|
314
314
|
// there and what it wants from them.
|
|
315
315
|
description += room.offlineServer
|
|
316
|
-
? `\n{
|
|
317
|
-
: `\n{
|
|
316
|
+
? `\n{light-blue-fg}A ${room.offlineServer} sits here, dark and off the network -- no aerial, no marquee, nothing broadcasting. Whatever it holds comes out through a cable and no other way.${hint(` "jack in" RIGHT HERE to touch its files.`)}{/light-blue-fg}`
|
|
317
|
+
: `\n{light-blue-fg}The node here hums with locked files -- whatever's worth taking lives on the grid, inside the host.${hint(` "jack in", then "enter" the host.`)}{/light-blue-fg}`;
|
|
318
318
|
}
|
|
319
319
|
description += describeContainers(room, this.actor);
|
|
320
320
|
description += describeBarriers(room, this.actor);
|
|
@@ -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.
|
|
@@ -10,6 +10,9 @@ import { AttackCommand } from './attack.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export class SubdueCommand extends AttackCommand {
|
|
12
12
|
static verb = 'subdue';
|
|
13
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
14
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
15
|
+
static npcPolicy = 'authorized';
|
|
13
16
|
static description = 'Take someone down ALIVE -- stun damage with your bare hands. Slower than shooting, they fight back for real, and overdoing it can still kill them.';
|
|
14
17
|
async execute(args = []) {
|
|
15
18
|
// A chokehold needs hands and a neck: no grappling as a Matrix persona
|
|
@@ -8,6 +8,9 @@ import { Category } from '../types/shared/item-enum.js';
|
|
|
8
8
|
import { billAction } from '../utilities/action-cost.js';
|
|
9
9
|
export class TakeCommand extends Command {
|
|
10
10
|
static verb = 'take';
|
|
11
|
+
/** Touches another actor: authorized per-act for AI-driven actors
|
|
12
|
+
* (utilities/npc-authorization.ts, aCErAfvEW7G23FWDM). */
|
|
13
|
+
static npcPolicy = 'authorized';
|
|
11
14
|
static description = 'Pick up an item. "take all" sweeps everything here -- but only after you have searched the room.';
|
|
12
15
|
async execute(args) {
|
|
13
16
|
if (!args)
|
|
@@ -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.)`)}`);
|
|
@@ -503,5 +503,79 @@
|
|
|
503
503
|
// to refuse sideQuest.settle -- on a shared run the cab is intercepted
|
|
504
504
|
// before the engine ever sees it, so the server is the only place
|
|
505
505
|
// that path can be stopped.
|
|
506
|
-
|
|
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
|
+
// 1.58.0 (2026-09-15): THE AI PROPOSES, THE ENGINE DISPOSES
|
|
530
|
+
// (aCErAfvEW7G23FWDM, reporter: "the only way to solve these is with
|
|
531
|
+
// engine enforcement against AI allowed actions"). An NPC beat was
|
|
532
|
+
// free prose: any line of it beginning "[COMMAND] " went to the SAME
|
|
533
|
+
// registry the player types into with NO filter on the command text,
|
|
534
|
+
// and every other line was printed to the player as fact. So the
|
|
535
|
+
// engine could neither refuse an act nor even SEE one asserted.
|
|
536
|
+
// - WHAT AN NPC MAY DO. Command.npcPolicy, the same declarative shape
|
|
537
|
+
// as humanOnly, enforced once in CommandRegistry.parse and backed by
|
|
538
|
+
// utilities/npc-authorization.ts. A hands-on verb against a body
|
|
539
|
+
// whose owner is jacked in, projecting, or already down is refused
|
|
540
|
+
// -- SR5 resolves Subduing (p.195) as an OPPOSED melee attack, and
|
|
541
|
+
// there is nothing there to oppose. parse() grew a third outcome:
|
|
542
|
+
// null still means "no such verb", a REFUSAL carries a reason, and
|
|
543
|
+
// NPC.act feeds that reason back so the model hears the boundary.
|
|
544
|
+
// - WHAT A BEAT MAY CLAIM. utilities/narration-limits.ts: narration
|
|
545
|
+
// may do anything with the NPC's own body and nothing with anybody
|
|
546
|
+
// else's. A refused line is suppressed AND answered, so the next
|
|
547
|
+
// beat can commit to "[COMMAND] grapple X" and let the dice decide.
|
|
548
|
+
// - A GRAB IS A FIGHT. commands/grapple.ts never called startEncounter
|
|
549
|
+
// -- it billed through billAction, a no-op outside combat -- so the
|
|
550
|
+
// most hostile act in the engine was also its cheapest, and left the
|
|
551
|
+
// target held with no turn in which to struggle. Every outcome now
|
|
552
|
+
// opens a Combat Turn. `grapples` joins the PROVOCATION ladder, and
|
|
553
|
+
// turnHostile() is the runtime mirror standDown never had: `hostile`
|
|
554
|
+
// was written at BOOT and nowhere else, so an NPC who was not
|
|
555
|
+
// opposition when the scene was built had no path to becoming it.
|
|
556
|
+
// - WHAT AN NPC MAY KNOW. Seven blocks of the brief were scene-globals
|
|
557
|
+
// handed to everybody. World events now carry WHERE they happened
|
|
558
|
+
// and Matrix-plane events reach only the Matrix (a bartender could
|
|
559
|
+
// read "the host MADE the intruder -- ice hunting"); ephemerals get
|
|
560
|
+
// no grapevine at all; the run's payout goes to the parties to the
|
|
561
|
+
// deal. The raw, unparsed model reply is no longer written into the
|
|
562
|
+
// NPC's memory -- only what actually reached the world is.
|
|
563
|
+
// - NOBODY SAYS IT TWICE. utilities/beat-echo.ts, the engine's first
|
|
564
|
+
// dedupe of emitted text of any kind. Containment rather than
|
|
565
|
+
// Jaccard, with a lower bar when a speaker opens the same way twice;
|
|
566
|
+
// companion link traffic is never judged (two drones clearing two
|
|
567
|
+
// rooms say near-identical things on purpose).
|
|
568
|
+
// - FACTIONS, catalog-backed (site v20, kind 'faction'). factions.ts
|
|
569
|
+
// holds the rules; the rows say who exists. First rule reading them:
|
|
570
|
+
// a Johnson will not lay out terms in front of a badge she can SEE.
|
|
571
|
+
// An unknown faction is no opinion, so a catalog without the rows
|
|
572
|
+
// behaves exactly as this engine did before them.
|
|
573
|
+
// - NEW CATALOG LANE `hearing`: hearingRangeMeters read the flat
|
|
574
|
+
// `perception` pool, so CYBEREYES extended how far you could hear.
|
|
575
|
+
// Audio Enhancement I-III and Spatial Recognizer are rows at last
|
|
576
|
+
// (p.445, p.454; Chrome Flesh p.223).
|
|
577
|
+
// - BENCH: repro schema 2.6.0 adds npcs[].hostile. A fixture could not
|
|
578
|
+
// express a hostile PERSON -- only `ice` -- so no meat-side
|
|
579
|
+
// hostility report could be benched at all.
|
|
580
|
+
export const ENGINE_VERSION = '1.58.0';
|
|
507
581
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { factionFor } from './utilities/catalog.js';
|
|
2
|
+
/**
|
|
3
|
+
* THE SHADOW ECONOMY'S OWN SLUG. A Johnson, a fixer and a runner are
|
|
4
|
+
* not an organisation, but they are a SIDE -- the deniable one -- and
|
|
5
|
+
* the rules below need a name for it. Matches the catalog row.
|
|
6
|
+
*/
|
|
7
|
+
export const SHADOWS = 'shadows';
|
|
8
|
+
/**
|
|
9
|
+
* WHAT FACTION IS THIS ACTOR, if the engine can tell.
|
|
10
|
+
*
|
|
11
|
+
* An explicit `faction` on the NPC wins, always -- a seed or a
|
|
12
|
+
* generator that says so is the author speaking. Failing that, the one
|
|
13
|
+
* thing the engine already knew about strangers is read: the ephemeral
|
|
14
|
+
* KIND that spawned them (utilities/ephemeral.ts). A sweep officer is
|
|
15
|
+
* an 'authority' body and was already, in every way that mattered, the
|
|
16
|
+
* law; this just gives the engine the word for it.
|
|
17
|
+
*
|
|
18
|
+
* DERIVING IS DELIBERATELY SHALLOW. It maps the three spawn archetypes
|
|
19
|
+
* the engine already has and stops. Guessing a faction from a name or a
|
|
20
|
+
* description would be the kind of inference that looks clever in a
|
|
21
|
+
* demo and puts a gang tattoo on a bartender in play.
|
|
22
|
+
*/
|
|
23
|
+
export function factionOf(actor) {
|
|
24
|
+
const npc = actor;
|
|
25
|
+
if (npc.faction)
|
|
26
|
+
return npc.faction;
|
|
27
|
+
switch (npc.ephemeral?.kind) {
|
|
28
|
+
case 'authority': return 'knight-errant';
|
|
29
|
+
case 'mob': return 'ancients';
|
|
30
|
+
case 'bystander': return 'civilians';
|
|
31
|
+
default: return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* HOW `a` REGARDS `b`: -3 (blood) .. +3 (family), 0 for no opinion.
|
|
36
|
+
*
|
|
37
|
+
* ASYMMETRIC BY DESIGN. A gang can hate a corp that has never heard of
|
|
38
|
+
* it, so this reads the standing of the faction doing the JUDGING and
|
|
39
|
+
* never averages the two. Where a faction is silent about another, the
|
|
40
|
+
* SORTS answer instead -- the law and the shadows are structurally
|
|
41
|
+
* opposed whether or not anybody wrote a row about it, and two gangs
|
|
42
|
+
* are rivals until someone says otherwise.
|
|
43
|
+
*/
|
|
44
|
+
export function standingBetween(a, b) {
|
|
45
|
+
if (!a || !b)
|
|
46
|
+
return 0;
|
|
47
|
+
if (a === b)
|
|
48
|
+
return 3;
|
|
49
|
+
const rowA = factionFor(a);
|
|
50
|
+
const explicit = rowA?.standing?.[b];
|
|
51
|
+
if (typeof explicit === 'number')
|
|
52
|
+
return explicit;
|
|
53
|
+
// A DIVISION IS ITS PARENT'S. Knight Errant IS Ares Macrotechnology's
|
|
54
|
+
// security arm (p.38), so the two are one house unless a row says
|
|
55
|
+
// otherwise above -- and nobody writes "we like ourselves" into a
|
|
56
|
+
// standing table, which is exactly why this cannot be left to the
|
|
57
|
+
// declared numbers.
|
|
58
|
+
if (rowA?.parent === b)
|
|
59
|
+
return 3;
|
|
60
|
+
// And its opinion of a THIRD party is the house's, where it has not
|
|
61
|
+
// formed its own.
|
|
62
|
+
if (rowA?.parent) {
|
|
63
|
+
const inherited = factionFor(rowA.parent)?.standing?.[b];
|
|
64
|
+
if (typeof inherited === 'number')
|
|
65
|
+
return inherited;
|
|
66
|
+
}
|
|
67
|
+
const rowB = factionFor(b);
|
|
68
|
+
if (!rowA || !rowB)
|
|
69
|
+
return 0;
|
|
70
|
+
if (rowA.sort === 'law' && (rowB.sort === 'shadow' || rowB.sort === 'gang' || rowB.sort === 'syndicate'))
|
|
71
|
+
return -2;
|
|
72
|
+
if ((rowA.sort === 'shadow' || rowA.sort === 'gang' || rowA.sort === 'syndicate') && rowB.sort === 'law')
|
|
73
|
+
return -2;
|
|
74
|
+
if (rowA.sort === 'gang' && rowB.sort === 'gang')
|
|
75
|
+
return -1;
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* WOULD BUSINESS BE DISCUSSED IN FRONT OF THIS ACTOR?
|
|
80
|
+
*
|
|
81
|
+
* The Johnson's question, and the one the reported session turned on.
|
|
82
|
+
* Three ways to be safe to talk in front of, and a stranger with no
|
|
83
|
+
* faction at all is one of them -- an unknown face in a bar is the
|
|
84
|
+
* normal condition of a bar, and treating every unplaceable body as a
|
|
85
|
+
* threat would mean no deal was ever struck anywhere.
|
|
86
|
+
*/
|
|
87
|
+
export function wouldTalkBusinessNear(dealer, witness) {
|
|
88
|
+
const theirs = factionOf(witness);
|
|
89
|
+
if (!theirs)
|
|
90
|
+
return true;
|
|
91
|
+
const row = factionFor(theirs);
|
|
92
|
+
// A faction that keeps its mouth shut is not a problem to talk in
|
|
93
|
+
// front of, whatever it thinks of you: the syndicates and the gangs
|
|
94
|
+
// are IN the deniable economy, not auditing it.
|
|
95
|
+
if (row?.discreet)
|
|
96
|
+
return true;
|
|
97
|
+
return standingBetween(dealer ?? SHADOWS, theirs) > -2;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* THE FIRST BODY IN THIS ROOM THE DEAL CANNOT BE STRUCK IN FRONT OF,
|
|
101
|
+
* or undefined when the room will do.
|
|
102
|
+
*
|
|
103
|
+
* Lives here rather than in game.ts so it can be tested against a
|
|
104
|
+
* catalog the way every other faction rule is -- and because "who
|
|
105
|
+
* spoils a deal" is a faction question, not a Game one.
|
|
106
|
+
*
|
|
107
|
+
* SIGHT IS THE TEST, NOT EARSHOT, and getting that backwards was the
|
|
108
|
+
* first thing tried. Earshot is wrong twice over: a badge who cannot
|
|
109
|
+
* make out the WORDS can still make out the MEETING, and canHearSpeech
|
|
110
|
+
* deliberately refuses an unplaced listener -- which is precisely the
|
|
111
|
+
* patrol officer WORKING the room, the exact witness the report is
|
|
112
|
+
* about. The reporter's own words are about presence: "there wouldn't
|
|
113
|
+
* be a 'meet' if there was a police officer STANDING THERE."
|
|
114
|
+
*
|
|
115
|
+
* @param sees the engine's own "can this observer see that actor"
|
|
116
|
+
* (utilities/spots.ts wouldNotice), passed in so this
|
|
117
|
+
* module stays free of the spatial layer.
|
|
118
|
+
*/
|
|
119
|
+
export function businessWitness(client, others, sees) {
|
|
120
|
+
const dealer = factionOf(client) ?? SHADOWS;
|
|
121
|
+
for (const other of others) {
|
|
122
|
+
if (other === client)
|
|
123
|
+
continue;
|
|
124
|
+
if (other.isIncapacitated?.())
|
|
125
|
+
continue;
|
|
126
|
+
if (!client.canPerceive(other))
|
|
127
|
+
continue;
|
|
128
|
+
if (!sees(client, other))
|
|
129
|
+
continue;
|
|
130
|
+
if (!wouldTalkBusinessNear(dealer, other))
|
|
131
|
+
return other;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=factions.js.map
|
|
@@ -162,6 +162,23 @@ export function buildReproScene(fixture) {
|
|
|
162
162
|
throw new ReproFixtureError(`"${misplacedIce.name}" is ice, so it cannot also stand in a doorway, guard, circulate or carry a gun. `
|
|
163
163
|
+ `Ice lives in the host, not on the floor -- drop the meat-side flags, or drop "ice".`);
|
|
164
164
|
}
|
|
165
|
+
// HOSTILITY SAID TWICE IS DECORATION ONCE (aCErAfvEW7G23FWDM). Ice is
|
|
166
|
+
// opposition BY CONSTRUCTION -- scene-factory makes a matrix-plane
|
|
167
|
+
// actor hostile unless the seed says otherwise -- so a fixture
|
|
168
|
+
// declaring both is asserting the flag rather than the rule, which is
|
|
169
|
+
// the objection the `ice` comment in repro.ts raises and is right
|
|
170
|
+
// about. A street doc who opens fire on the patient is not a fixture
|
|
171
|
+
// either; it is two fixtures wearing one name.
|
|
172
|
+
const hostileIce = npcs.find(n => n.hostile && n.ice);
|
|
173
|
+
if (hostileIce) {
|
|
174
|
+
throw new ReproFixtureError(`"${hostileIce.name}" is asked to be both ice and hostile. Ice is opposition by construction -- `
|
|
175
|
+
+ `drop "hostile", or drop "ice" if you meant a hostile person.`);
|
|
176
|
+
}
|
|
177
|
+
const hostileDoc = npcs.find(n => n.hostile && n.doc);
|
|
178
|
+
if (hostileDoc) {
|
|
179
|
+
throw new ReproFixtureError(`"${hostileDoc.name}" is asked to be both a street doc and hostile. A doc who opens fire on the `
|
|
180
|
+
+ `patient is two fixtures -- split them, or pick one.`);
|
|
181
|
+
}
|
|
165
182
|
// A SEED HAS ONE PLANE. astral maps to 'astral' the same way ice maps
|
|
166
183
|
// to 'matrix' (npc-seed.ts) -- asking for both is asking the builder
|
|
167
184
|
// to pick a lie.
|
|
@@ -379,6 +396,13 @@ export function buildReproScene(fixture) {
|
|
|
379
396
|
// string, which would read as a spot whose name is nothing.
|
|
380
397
|
...(npc.circulating ? {} : { startSpot: inDoorway ? REPRO_DOORWAY_SPOT : REPRO_FLOOR_SPOT }),
|
|
381
398
|
guarding: npc.guarding === true,
|
|
399
|
+
// OPPOSITION, IN THE MEAT (aCErAfvEW7G23FWDM). Straight onto
|
|
400
|
+
// the seed's own `hostile`, which scene-factory reads and the
|
|
401
|
+
// hostile-contact ladder then drives by itself -- notice,
|
|
402
|
+
// contact, provocation, patience. Emitted only when asked, so
|
|
403
|
+
// a fixture that says nothing builds the same peaceable NPC it
|
|
404
|
+
// always did.
|
|
405
|
+
...(npc.hostile ? { hostile: true } : {}),
|
|
382
406
|
lifestyleChoices: [],
|
|
383
407
|
combat: {
|
|
384
408
|
body: 4, agility: 4, reaction: 4, combatSkill: 3, strength: 4,
|
|
@@ -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';
|
|
@@ -151,6 +152,8 @@ import { MoveCommand, RunCommand, SprintCommand } from './commands/move.js';
|
|
|
151
152
|
import { ClimbCommand, DescendCommand } from './commands/climb.js';
|
|
152
153
|
import { HintsCommand } from './commands/hints.js';
|
|
153
154
|
import { hint } from './utilities/hints.js';
|
|
155
|
+
import { businessWitness } from './factions.js';
|
|
156
|
+
import { wouldNotice } from './utilities/spots.js';
|
|
154
157
|
import { camerasLive, liveFootage } from './utilities/surveillance.js';
|
|
155
158
|
import { InstallCommand } from './commands/install.js';
|
|
156
159
|
import { BuyCommand } from './commands/buy.js';
|
|
@@ -708,7 +711,7 @@ export default class Game {
|
|
|
708
711
|
Logger.getInstance().write(`Loose end "${c.id}" left: ${erased ? 'ERASED' : 'still open'}; hub is the scene again.`);
|
|
709
712
|
this.requestSave('cleanup');
|
|
710
713
|
return [erased
|
|
711
|
-
? `\n{
|
|
714
|
+
? `\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
715
|
: `\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
716
|
}
|
|
714
717
|
/** The HUD's read on the district (bands, never the number). */
|
|
@@ -2399,6 +2402,38 @@ export default class Game {
|
|
|
2399
2402
|
* Completing the objective before ever meeting them still collapses
|
|
2400
2403
|
* cleanly -- the wrap-up is the only beat that moves money.
|
|
2401
2404
|
*/
|
|
2405
|
+
/**
|
|
2406
|
+
* ANYBODY IN THIS ROOM THE JOHNSON WOULD NOT TALK IN FRONT OF
|
|
2407
|
+
* (aCErAfvEW7G23FWDM). Returns the first, or undefined when the room
|
|
2408
|
+
* is safe to do business in.
|
|
2409
|
+
*
|
|
2410
|
+
* SIGHT IS THE TEST, NOT EARSHOT, and getting that backwards is worth
|
|
2411
|
+
* recording because it was the first thing tried. Earshot is the
|
|
2412
|
+
* wrong question twice over: a badge who cannot make out the words
|
|
2413
|
+
* can still make out the MEETING, and -- the part that actually broke
|
|
2414
|
+
* it -- canHearSpeech refuses an unplaced listener, so a patrol
|
|
2415
|
+
* officer WORKING THE ROOM is exactly the witness it excludes. The
|
|
2416
|
+
* reporter's own words are about presence: "there wouldn't be a
|
|
2417
|
+
* 'meet' if there was a police officer STANDING THERE."
|
|
2418
|
+
*
|
|
2419
|
+
* wouldNotice is the engine's existing "can this observer see that
|
|
2420
|
+
* actor" -- distance, cover and all -- so a cop at the far end of a
|
|
2421
|
+
* warehouse still counts and one behind a wall does not.
|
|
2422
|
+
*/
|
|
2423
|
+
witnessesUnsafeForBusiness(clientName) {
|
|
2424
|
+
const client = this.scene?.getActor?.(clientName);
|
|
2425
|
+
if (!client)
|
|
2426
|
+
return undefined;
|
|
2427
|
+
const room = client.currentLocation;
|
|
2428
|
+
if (!room)
|
|
2429
|
+
return undefined;
|
|
2430
|
+
// THE RUNNER AND THEIR OWN CREW ARE WHO THE DEAL IS WITH, so they
|
|
2431
|
+
// are filtered out here rather than inside the faction rule -- that
|
|
2432
|
+
// is a fact about this Game, not about factions.
|
|
2433
|
+
const others = (this.scene?.getActorsInRoom?.(room) ?? [])
|
|
2434
|
+
.filter(a => !this.scene?.isHumanControlled?.(a) && !a.allyOf);
|
|
2435
|
+
return businessWitness(client, others, wouldNotice);
|
|
2436
|
+
}
|
|
2402
2437
|
onClientInteraction(npcName) {
|
|
2403
2438
|
const client = this.currentClient;
|
|
2404
2439
|
if (!client || !this.scene || this.scene === this._hubScene)
|
|
@@ -2410,6 +2445,25 @@ export default class Game {
|
|
|
2410
2445
|
const payout = this.scene.getWinCondition()?.payout ?? 0;
|
|
2411
2446
|
const lines = [];
|
|
2412
2447
|
if (!client.met) {
|
|
2448
|
+
// A MEET WITH A BADGE IN IT IS NOT A MEET (aCErAfvEW7G23FWDM:
|
|
2449
|
+
// "there wouldn't be a 'meet' if there was a police officer
|
|
2450
|
+
// standing there").
|
|
2451
|
+
//
|
|
2452
|
+
// The reported session, 10:57. Ms. Quill laid out 8,500 nuyen
|
|
2453
|
+
// across a table with Patrol Officer Kessler working the same
|
|
2454
|
+
// room, and the WRITING knew -- she clocked him on arrival and
|
|
2455
|
+
// told the runner to keep his questions off the ledger. The
|
|
2456
|
+
// ENGINE had no idea, because the Meet is a name match on
|
|
2457
|
+
// currentClient and nothing else, and the scene generator demands
|
|
2458
|
+
// staff for any bar.
|
|
2459
|
+
//
|
|
2460
|
+
// Held, not cancelled: the job is still there and the Johnson is
|
|
2461
|
+
// still willing. Deal with the room, or take it somewhere else.
|
|
2462
|
+
const overheard = this.witnessesUnsafeForBusiness(client.name);
|
|
2463
|
+
if (overheard) {
|
|
2464
|
+
Logger.getInstance().write(`Client meet held off: ${overheard.name} is within earshot of ${client.name}.`);
|
|
2465
|
+
return `${client.name} sees ${overheard.name} in the room and lets the sentence die. Not here.${hint(` (Somewhere quieter, or without them.)`)}`;
|
|
2466
|
+
}
|
|
2413
2467
|
client.met = true;
|
|
2414
2468
|
lines.push(`THE MEET: ${client.name} lays out the terms -- ${payout}¥ certified, in full, on delivery.${hint(` (The rate is negotiable exactly once: "haggle" while you have them.)`)}`);
|
|
2415
2469
|
Logger.getInstance().write(`Client meet: ${this.player.name} met ${client.name}, terms ${payout} lump sum on delivery.`);
|
|
@@ -2692,7 +2746,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2692
2746
|
CommandFactory.registerCommand('open', OpenCommand);
|
|
2693
2747
|
CommandFactory.registerCommand('close', CloseCommand);
|
|
2694
2748
|
CommandFactory.registerCommand('put', PutCommand);
|
|
2749
|
+
// TELL IS THE VERB (en3p6eCpiRuqsTSKm: "let's adjust 'talk-to' to
|
|
2750
|
+
// 'tell' to be more concise"). `talk-to` stays registered as an
|
|
2751
|
+
// alias rather than being deleted: it is the spelling in every
|
|
2752
|
+
// transcript, every NPC brief and every backlog item written before
|
|
2753
|
+
// today, and a player who learned it should not be told it is gone.
|
|
2754
|
+
CommandFactory.registerCommand('tell', TalkCommand);
|
|
2695
2755
|
CommandFactory.registerCommand('talk-to', TalkCommand);
|
|
2756
|
+
CommandFactory.registerCommand('shout', ShoutCommand);
|
|
2696
2757
|
CommandFactory.registerCommand('give', GiveCommand);
|
|
2697
2758
|
CommandFactory.registerCommand('read', ReadCommand);
|
|
2698
2759
|
CommandFactory.registerCommand('kill', KillCommand);
|
|
@@ -5955,7 +6016,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5955
6016
|
// works the grid while you're back in the meat (the AR-drop exit).
|
|
5956
6017
|
const remoteAgent = this.companions.find(c => c.kind === 'agent');
|
|
5957
6018
|
if (remoteAgent && player.plane !== 'matrix') {
|
|
5958
|
-
lines.push(`{
|
|
6019
|
+
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
6020
|
}
|
|
5960
6021
|
// Off-plane context: which reality you're in, your persona's Matrix
|
|
5961
6022
|
// track (cold-sim damage lands there), and where your empty body lies.
|
|
@@ -5974,7 +6035,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5974
6035
|
const track = jackedDeck
|
|
5975
6036
|
? ` Deck A${jackedDeck.attack} S${jackedDeck.sleaze} D${jackedDeck.dataProcessing} F${jackedDeck.firewall} ${jackedDeck.deckSummary()}${player.simMode === 'hot' ? ' +biofeedback' : ''}${pgms}`
|
|
5976
6037
|
: ` (living persona -- damage is REAL stun)`;
|
|
5977
|
-
lines.push(`{
|
|
6038
|
+
lines.push(`{light-blue-fg}▣ MATRIX ${(player.simMode ?? 'cold').toUpperCase()}-SIM${track} -- body at ${player.bodyRoom?.name ?? '???'}{/light-blue-fg}`);
|
|
5978
6039
|
}
|
|
5979
6040
|
else if (player.plane === 'astral') {
|
|
5980
6041
|
lines.push(`{magenta-fg}✧ ASTRAL -- ${player.astralTicks} move${player.astralTicks === 1 ? '' : 's'} out -- body at ${player.bodyRoom?.name ?? '???'}{/magenta-fg}`);
|
|
@@ -6066,7 +6127,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6066
6127
|
const dockW = mapWidth ?? 15;
|
|
6067
6128
|
const lines = hosts.length > 0
|
|
6068
6129
|
? hosts.map(h => hostDockLine(h, this.player, dockW))
|
|
6069
|
-
: ['{
|
|
6130
|
+
: ['{light-blue-fg}nothing overhead{/light-blue-fg}'];
|
|
6070
6131
|
lines.push(caption(this.player.insideHost ? `inside ${this.player.insideHost.name}` : `on the grid from ${gridVicinity(this.player).name}`, mapWidth));
|
|
6071
6132
|
this.setMapOverlay(lines);
|
|
6072
6133
|
if (onTheGrid) {
|
|
@@ -6575,7 +6636,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6575
6636
|
// technomancer-gated (a sprite task, SR5 p.256) and refuses anyone
|
|
6576
6637
|
// else, so the Emerged shelf is where its audience looks.
|
|
6577
6638
|
{ 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']] },
|
|
6639
|
+
{ 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
6640
|
{ title: 'street', entries: [['abandon'], ['browse', 'buy'], ['sell'], ['catalog'], ['source', 'procure'], ['work', 'gig'], ['jobs'], ['journal'], ['perform'], ['palm'], ['docwagon'], ['treat'], ['eat'], ['drink'], ['lifestyle'], ['garage'], ['pay']] },
|
|
6580
6641
|
{ title: 'barriers', entries: [['pick'], ['breach'], ['dispel', 'dispell'], ['bluff'], ['unlock'], ['lock']] },
|
|
6581
6642
|
{ title: 'character', entries: [['sheet', 'stats'], ['advance'], ['qualities', 'quality'], ['sin', 'sins', 'papers'], ['initiate', 'submerge'], ['install', 'chrome']] },
|