@maka/maka-cli 5.207.0 → 5.208.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/complete.js +125 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +14 -14
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/force.js +40 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +61 -54
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/mark.js +49 -39
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +14 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/snoop.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/subdue.js +98 -17
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +16 -5
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +62 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +49 -35
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +3 -55
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +32 -30
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +20 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +2 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +50 -17
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +56 -9
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/nameables.js +107 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +3 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +1 -1
- package/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +0 -121
|
@@ -318,8 +318,13 @@ export class CombatExchange {
|
|
|
318
318
|
* involuntary strikes (retaliation) where refusing to swing at all would
|
|
319
319
|
* make no sense; a deliberately INITIATED attack with a dry gun is
|
|
320
320
|
* blocked with a reload prompt before ever reaching here (attack.ts).
|
|
321
|
+
*
|
|
322
|
+
* `opts.nonLethal` is the verb's INTENT to take the target alive -- set
|
|
323
|
+
* by `subdue` and by nothing else. Retaliation never passes it, which
|
|
324
|
+
* is the honest reading of p.236's cousin in the meat: someone you are
|
|
325
|
+
* choking out is not obliged to pull their own punch.
|
|
321
326
|
*/
|
|
322
|
-
weaponProfile(attacker, defender) {
|
|
327
|
+
weaponProfile(attacker, defender, opts = {}) {
|
|
323
328
|
const weapon = attacker.getWeaponProfile();
|
|
324
329
|
// The physical weapon only backs a MEAT-plane strike by someone who
|
|
325
330
|
// has it DRAWN (see Player.weaponDrawn) -- a holstered gun never
|
|
@@ -380,16 +385,24 @@ export class CombatExchange {
|
|
|
380
385
|
// - electricity is Stun: taser, stun baton, shock gloves (p.170-171, p.424);
|
|
381
386
|
// - stun ammunition: gel rounds Stun at AP +1, stick-n-shock Stun at
|
|
382
387
|
// DV -2 / AP -5 (p.434) -- whatever the magazine was loaded with;
|
|
383
|
-
// - NON-LETHAL BY
|
|
388
|
+
// - NON-LETHAL BY INTENT: a lethal melee weapon swung as a club deals
|
|
384
389
|
// Stun at Accuracy 3 and no Reach (p.186 "Changing Damage Types").
|
|
385
|
-
// A lethal gun with lethal rounds has no such door
|
|
386
|
-
//
|
|
390
|
+
// A lethal gun with lethal rounds has no such door.
|
|
391
|
+
//
|
|
392
|
+
// THE INTENT COMES FROM THE VERB NOW (f79LLqTerep4nKWcA). It used to
|
|
393
|
+
// come from `attacker.autoStance`, a standing posture set by Tab --
|
|
394
|
+
// so whether your blade cut or clubbed depended on a setting made
|
|
395
|
+
// some turns ago, and the only way to find out which was to read the
|
|
396
|
+
// label after the swing. `attack` is the lethal verb and `subdue` is
|
|
397
|
+
// the one that takes them alive; each says so when it builds the
|
|
398
|
+
// strike, and `lethalAnyway` no longer needs to exist, because
|
|
399
|
+
// subdue refuses a lethal gun outright rather than firing it and
|
|
400
|
+
// tagging the corpse (commands/subdue.ts).
|
|
387
401
|
const stunWeapon = !!weaponItem?.isStunWeapon();
|
|
388
402
|
const ammoMods = weaponItem?.isFirearm() ? weaponItem.loadedAmmoMods() : undefined;
|
|
389
403
|
const stunAmmo = weaponItem?.isFirearm() ? weaponItem.stunAmmo() : undefined;
|
|
390
|
-
const nonLethal =
|
|
404
|
+
const nonLethal = opts.nonLethal === true && attacker.plane === 'meat';
|
|
391
405
|
const flatOfTheBlade = nonLethal && !!weaponItem && weaponItem.category === Category.MeleeWeapon && !stunWeapon;
|
|
392
|
-
const lethalAnyway = nonLethal && !!weaponItem?.isFirearm() && !stunWeapon && !stunAmmo;
|
|
393
406
|
const stun = stunWeapon || !!stunAmmo || flatOfTheBlade || !!fistsAreStun;
|
|
394
407
|
const ap = (weapon.armorPen ?? 0) + (ammoMods?.apMod ?? 0);
|
|
395
408
|
const meatDefender = defender.plane === 'meat';
|
|
@@ -401,8 +414,7 @@ export class CombatExchange {
|
|
|
401
414
|
const roundTag = ammoMods && !stunAmmo && (ammoMods.dvMod !== 0 || ammoMods.apMod !== 0) ? `, ${ammoMods.kind}` : '';
|
|
402
415
|
const tag = flatOfTheBlade ? ' (flat of the blade, STUN)'
|
|
403
416
|
: stunAmmo ? ` (${stunAmmo.kind}, STUN)`
|
|
404
|
-
: stunWeapon ? ' (STUN)'
|
|
405
|
-
: lethalAnyway ? ' (LETHAL -- no stun rounds loaded)' : '';
|
|
417
|
+
: stunWeapon ? ' (STUN)' : '';
|
|
406
418
|
return {
|
|
407
419
|
label: `with ${weapon.name}${ap !== 0 ? ` (AP ${ap > 0 ? '+' : ''}${ap}${roundTag})` : roundTag ? ` (${roundTag.slice(2)})` : ''}${tag}`,
|
|
408
420
|
pool: attacker.getAttackPool() + edgeBoost,
|
|
@@ -143,7 +143,6 @@ export function buildConditionReport(player, deps) {
|
|
|
143
143
|
? { grid: player.currentGrid?.name, host: player.hostInside?.name }
|
|
144
144
|
: undefined,
|
|
145
145
|
simMode: player.simMode,
|
|
146
|
-
stance: player.autoStance,
|
|
147
146
|
drawn: player.weaponDrawn,
|
|
148
147
|
weapon: player.weaponDrawn ? player.getCarriedWeapon()?.name : undefined,
|
|
149
148
|
sustained: player.sustainedNames(),
|
|
@@ -348,8 +347,8 @@ extra) {
|
|
|
348
347
|
// sheet's "where" stays as fresh as its monitors). Read at FIRE
|
|
349
348
|
// time, not schedule time -- the coalescing beat means the player
|
|
350
349
|
// may have moved since the mutation that armed it.
|
|
351
|
-
// The full state installment (contract 2026-09-01): plane/sim
|
|
352
|
-
//
|
|
350
|
+
// The full state installment (contract 2026-09-01): plane/sim mode
|
|
351
|
+
// and brandish flow through their accessors' own reports;
|
|
353
352
|
// sustained and deck programs are composed here and ride whatever
|
|
354
353
|
// beat fires next. All read at FIRE time, like position.
|
|
355
354
|
const report = buildConditionReport(player, { saveSlug, arSightUp, isHostile: extra?.isHostile, isParty: extra?.isParty, phaseActor: extra?.phaseActor });
|
|
@@ -172,23 +172,50 @@ export function noiseNote(noise) {
|
|
|
172
172
|
export function hostLine(room, actor) {
|
|
173
173
|
const inside = actor.insideHost === room;
|
|
174
174
|
const marks = room.hostMarksBy.get(actor.name) ?? 0;
|
|
175
|
-
//
|
|
176
|
-
//
|
|
175
|
+
// SAY IT IN WORDS (f79LLqTerep4nKWcA, Maka: "'sealed, walls UP, over
|
|
176
|
+
// you' means nothing, it's just jargon. Remove it, when I have a mark
|
|
177
|
+
// on it, show my mark on it, and vice versa -- when it has a mark on
|
|
178
|
+
// me, show that.").
|
|
179
|
+
//
|
|
180
|
+
// This row was three tokens of shorthand welded together, and the
|
|
181
|
+
// first of them was not merely opaque, it was WRONG: `standing` read
|
|
182
|
+
// only the marks YOU hold, so a host holding three marks on your
|
|
183
|
+
// persona -- owning you outright -- printed "sealed", the word for a
|
|
184
|
+
// node nobody has touched. The two ledgers were always separate in
|
|
185
|
+
// the model (Host.marksBy / Host.marksOn); only the renderer collapsed
|
|
186
|
+
// them.
|
|
177
187
|
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
// Four facts, four plain clauses, and never more than the row has
|
|
189
|
+
// earned:
|
|
190
|
+
// - what YOU hold on it, counted;
|
|
191
|
+
// - what IT holds on you, counted -- the number that decides whether
|
|
192
|
+
// its ice can come find you;
|
|
193
|
+
// - whether it has been cracked, or is a haven you belong in;
|
|
194
|
+
// - whether it knows someone is in (Host.alert) or can see YOU
|
|
195
|
+
// specifically (Host.spotted) -- two different facts that
|
|
196
|
+
// "walls UP" flattened into one (p.236).
|
|
197
|
+
//
|
|
198
|
+
// STANDING IS TWO FACTS, NOT ONE (spcebZTKN24GYRHXW: "it's saying
|
|
199
|
+
// I've already cracked open (hold 3 marks) but I don't"). "Cracked
|
|
200
|
+
// open" is the SITE's state -- somebody took this node to three marks
|
|
201
|
+
// and its vault has hung open since. The marks count is YOUR state,
|
|
202
|
+
// and canon wipes it on every reboot (p.236). Print both whenever they
|
|
203
|
+
// disagree.
|
|
204
|
+
const onYou = room.hostMarksOn.get(actor.name) ?? 0;
|
|
205
|
+
const yours = marks > 0 ? `you hold ${marks} of ${MAX_MARKS} marks` : 'you hold no marks';
|
|
206
|
+
const theirs = onYou > 0
|
|
207
|
+
? `it holds ${onYou} on YOU`
|
|
208
|
+
: undefined;
|
|
209
|
+
const standing = room.hostSanctioned ? 'a haven -- you belong here'
|
|
210
|
+
: room.hostCracked ? (marks >= MAX_MARKS ? `cracked open, and ${yours}` : `cracked open, but ${yours}`)
|
|
211
|
+
: yours;
|
|
212
|
+
// Seen beats merely alerted: if it can pick your icon out it does not
|
|
213
|
+
// need to sweep for you.
|
|
214
|
+
const eyes = room.hostSanctioned ? ''
|
|
215
|
+
: room.host?.spotted.has(actor.name) ? ', and it can SEE you'
|
|
216
|
+
: room.hostAlert ? ', and it knows someone is on the grid' : '';
|
|
217
|
+
const alert = theirs ? `, ${theirs}${eyes}` : eyes;
|
|
218
|
+
const over = hostOver(gridVicinity(actor)) === room ? ', directly overhead' : '';
|
|
192
219
|
const onGrid = room.host?.grid ? ` on ${room.host.grid.name}` : '';
|
|
193
220
|
// A haven shows no rating and no eyes (jackpoint.ts): it is not a
|
|
194
221
|
// thing you crack, so the numbers you would crack it with stay off.
|
|
@@ -216,7 +243,13 @@ export function hostDockLine(room, actor, width) {
|
|
|
216
243
|
: room.hostCracked ? ` open ${marks}/${MAX_MARKS}`
|
|
217
244
|
: marks > 0 ? ` ${marks}/${MAX_MARKS}`
|
|
218
245
|
: '';
|
|
219
|
-
|
|
246
|
+
// AND WHAT IT HOLDS ON YOU (f79LLqTerep4nKWcA). The dock had room for
|
|
247
|
+
// one ledger and showed the flattering one; a persona with a host's
|
|
248
|
+
// key riding it could read every panel in the game and never meet the
|
|
249
|
+
// number. Two glyphs: yours forward, theirs back.
|
|
250
|
+
const onYou = room.hostMarksOn.get(actor.name) ?? 0;
|
|
251
|
+
const onYouTag = onYou > 0 ? ` <${onYou}` : '';
|
|
252
|
+
const raw = `${inside ? '▣' : '●'} ${room.name} HR${room.hostRating}${tag}${onYouTag}`;
|
|
220
253
|
const cut = [...raw].slice(0, Math.max(4, width)).join('');
|
|
221
254
|
return `{light-blue-fg}${cut}{/light-blue-fg}`;
|
|
222
255
|
}
|
|
@@ -16,6 +16,13 @@ import { isWatched, camerasLive, canSnoopFeeds } from './surveillance.js';
|
|
|
16
16
|
* ▣ PAN, ▲ dangerous device, ◇ data. (There is no air-gapped-host glyph
|
|
17
17
|
* any more -- see hhSWzLSXECFtfoAGa and canReachHost.)
|
|
18
18
|
*/
|
|
19
|
+
/** The heading gridIcons drops between a host's icon and the icons merely
|
|
20
|
+
* standing NEAR it. See the comment where it is spliced in. */
|
|
21
|
+
const NEAR_DIVIDER = '-- and NEAR it, out on the open grid (not inside the host): --';
|
|
22
|
+
/** A heading is not an icon: the distance bands must not name it. */
|
|
23
|
+
export function isNearDivider(line) {
|
|
24
|
+
return line === NEAR_DIVIDER;
|
|
25
|
+
}
|
|
19
26
|
/**
|
|
20
27
|
* A DEVICE ICON, AND WHAT WORKS IT (commands/disable.ts, 2026-09-13).
|
|
21
28
|
* Control Device (p.238) needs 2 marks on the device -- "disable" -- and
|
|
@@ -89,14 +96,30 @@ export function gridIcons(scene, actor, room) {
|
|
|
89
96
|
// is still unearned -- a host that has already told you its
|
|
90
97
|
// business has nothing left to sell. Built as a list so a dropped
|
|
91
98
|
// entry cannot leave a dangling separator.
|
|
99
|
+
// EVERY WAY NAMES THE ICON (f79LLqTerep4nKWcA: "How do I know which
|
|
100
|
+
// host/PAN/etc I'm hacking?"). These used to read `"hack" slips in
|
|
101
|
+
// on sleaze` / `"hack loud" smashes` / `"mark" softens it first` --
|
|
102
|
+
// a bare verb, a verb plus an adverb that looks like a target, and
|
|
103
|
+
// a third spelling of the first one. Now: one verb per book action,
|
|
104
|
+
// each shown with the name you would actually type.
|
|
105
|
+
const name = hostLabel(room);
|
|
92
106
|
const ways = [
|
|
93
|
-
...(myMarks > 0 ? [
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...(found ? [] : ['"search" reads what it runs (p.241)']),
|
|
107
|
+
...(myMarks > 0 ? [`"enter" walks in on your mark`] : []),
|
|
108
|
+
`"hack ${name}" slips a mark in quiet (Hack on the Fly, p.240)`,
|
|
109
|
+
`"force ${name}" smashes one in (Brute Force, p.238)`,
|
|
110
|
+
...(found ? [] : [`"search" reads what it runs (p.241)`]),
|
|
98
111
|
];
|
|
99
|
-
|
|
112
|
+
// BOTH LEDGERS, PLAINLY. "Walls UP -- it holds a mark on an
|
|
113
|
+
// intruder" named no number and no one: the reader could not tell
|
|
114
|
+
// whether the intruder it meant was them.
|
|
115
|
+
const onYou = room.hostMarksOn.get(actor.name) ?? 0;
|
|
116
|
+
const standing = [
|
|
117
|
+
myMarks > 0 ? `you hold ${myMarks} of 3 marks on it` : undefined,
|
|
118
|
+
onYou > 0 ? `it holds ${onYou} on YOU` : undefined,
|
|
119
|
+
room.host?.spotted.has(actor.name) ? 'and it can see you'
|
|
120
|
+
: room.hostAlert ? 'and it knows someone is on the grid' : undefined,
|
|
121
|
+
].filter(Boolean).join(', ');
|
|
122
|
+
icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- sculpted ice geometry, sealed. Whatever it holds is inside it.${standing.length > 0 ? ` ${standing.charAt(0).toUpperCase()}${standing.slice(1)}.` : ''} (${ways.join('; ')})`);
|
|
100
123
|
}
|
|
101
124
|
}
|
|
102
125
|
// WHERE THE HOST'S OWN ICON ENDS AND THE STREET BEGINS.
|
|
@@ -262,12 +285,19 @@ export function gridIcons(scene, actor, room) {
|
|
|
262
285
|
// is no wall for a reader to put an icon on the wrong side of, and a
|
|
263
286
|
// heading over an empty list is noise.
|
|
264
287
|
//
|
|
288
|
+
// IT IS A HEADING, NOT AN ICON, and gridIconBlock's distance bands have
|
|
289
|
+
// to know that: they take each entry, strip its glyph and cut at " -- "
|
|
290
|
+
// to get a name, which turned this sentence into an icon called "and
|
|
291
|
+
// NEAR it, out on the open grid (not inside the host):" listed beside
|
|
292
|
+
// the real ones. A constant so the one predicate that filters it out
|
|
293
|
+
// (isNearDivider, below) cannot drift from the string it matches.
|
|
294
|
+
//
|
|
265
295
|
// It says NEAR, not IN, and it says why they are visible -- proximity
|
|
266
296
|
// to your body, not a leak in the host (p.235). A reader who wondered
|
|
267
297
|
// whether the host was showing them its insides now has the answer in
|
|
268
298
|
// the list itself instead of having to file a report to get it.
|
|
269
299
|
if (room.hasNode && icons.length > afterHost) {
|
|
270
|
-
icons.splice(afterHost, 0,
|
|
300
|
+
icons.splice(afterHost, 0, NEAR_DIVIDER);
|
|
271
301
|
}
|
|
272
302
|
return icons;
|
|
273
303
|
}
|
|
@@ -542,7 +572,7 @@ export function gridIconBlock(scene, actor, room, rooms) {
|
|
|
542
572
|
for (const { room: r, noise } of roomsInReach(rooms, actor).slice(1)) {
|
|
543
573
|
let icons = [];
|
|
544
574
|
try {
|
|
545
|
-
icons = gridIcons(scene, actor, r).filter(i => !isHostLine(i));
|
|
575
|
+
icons = gridIcons(scene, actor, r).filter(i => !isHostLine(i) && !isNearDivider(i));
|
|
546
576
|
}
|
|
547
577
|
catch {
|
|
548
578
|
icons = [];
|
|
@@ -583,9 +613,26 @@ export function hostInteriorPanel(scene, actor, host, rows, cols) {
|
|
|
583
613
|
if (purpose)
|
|
584
614
|
lines.push(cut(purpose));
|
|
585
615
|
const marks = host.marksBy.get(actor.name) ?? 0;
|
|
616
|
+
// BOTH DIRECTIONS IN THE DOCK TOO (f79LLqTerep4nKWcA). "marks 2/3" was
|
|
617
|
+
// only ever the marks YOU hold, and "WALLS UP" was the host's whole
|
|
618
|
+
// side of the ledger compressed to two words that named no number.
|
|
619
|
+
// The dock is narrow, so this is the terse spelling of the same four
|
|
620
|
+
// facts hostLine spells out: yours forward, theirs back, and what it
|
|
621
|
+
// can see.
|
|
622
|
+
const onYou = host.marksOn.get(actor.name) ?? 0;
|
|
586
623
|
lines.push(cut(host.sanctioned
|
|
587
624
|
? 'haven -- welcome here'
|
|
588
|
-
: `HR ${host.rating} ·
|
|
625
|
+
: `HR ${host.rating} · you ${marks}/3 · on you ${onYou}${host.cracked ? ' · cracked' : ''}`));
|
|
626
|
+
// ITS EYES GET THEIR OWN ROW rather than a third token on the one
|
|
627
|
+
// above: the dock is ~30 columns and the ledger already fills it, so a
|
|
628
|
+
// cut line would eat exactly the word that matters. Seen beats merely
|
|
629
|
+
// alerted -- an icon it can pick out does not need to sweep for you.
|
|
630
|
+
if (!host.sanctioned) {
|
|
631
|
+
if (host.spotted.has(actor.name))
|
|
632
|
+
lines.push(cut('it can SEE you'));
|
|
633
|
+
else if (host.alert)
|
|
634
|
+
lines.push(cut('it knows someone is in'));
|
|
635
|
+
}
|
|
589
636
|
for (const kind of host.runningIC) {
|
|
590
637
|
const ice = host.iceActors.get(kind);
|
|
591
638
|
lines.push(cut(`▲ ${kind}${ice ? ` ${ice.damageTaken}/${ice.maxConditionBoxes}` : ''}`));
|
|
@@ -2,8 +2,9 @@ import { hostLabel } from './grid-names.js';
|
|
|
2
2
|
import { Logger } from './logger.js';
|
|
3
3
|
/**
|
|
4
4
|
* MARKS -- MAtrix Recognition Keys (canon p.235-236, player go
|
|
5
|
-
* 2026-08-24). Placing a mark is
|
|
6
|
-
*
|
|
5
|
+
* 2026-08-24). Placing a mark is "hack" (Hack on the Fly, p.240) or
|
|
6
|
+
* "force" (Brute Force, p.238) -- never "mark", which since
|
|
7
|
+
* f79LLqTerep4nKWcA is the LEDGER and nothing else; this module is the
|
|
7
8
|
* BOOKKEEPING: capped at 3 per icon, keyed by placer, and wiped by the
|
|
8
9
|
* canon events -- the PLACER's reboot/jack-out/convergence erases all
|
|
9
10
|
* marks they placed anywhere (a fresh persona has no keys), and a
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { hostLabel } from './grid-names.js';
|
|
2
|
+
import { gridVicinity, hostsInReach, roomsInReach } from './grid-reach.js';
|
|
3
|
+
import { cybercombatCandidates } from './matrix-roster.js';
|
|
4
|
+
import { visibleActorsIn } from './spots.js';
|
|
5
|
+
function push(out, seen, n) {
|
|
6
|
+
const key = n.name.toLowerCase();
|
|
7
|
+
if (n.name.trim().length === 0 || seen.has(key))
|
|
8
|
+
return;
|
|
9
|
+
seen.add(key);
|
|
10
|
+
out.push(n);
|
|
11
|
+
}
|
|
12
|
+
/** Every icon a persona could name from where it stands. */
|
|
13
|
+
function matrixNameables(scene, actor, rooms) {
|
|
14
|
+
const out = [];
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
// HOSTS have no distance on the grid (p.246) -- every one in reach is
|
|
17
|
+
// nameable from anywhere, which is precisely why naming one matters.
|
|
18
|
+
for (const room of hostsInReach(rooms, actor)) {
|
|
19
|
+
push(out, seen, { glyph: '●', name: hostLabel(room), what: `host, rating ${room.hostRating}` });
|
|
20
|
+
}
|
|
21
|
+
// PERSONAS AND CONSTRUCTS on your side of the host wall -- the same
|
|
22
|
+
// set cybercombat resolves against, so what Tab offers is what
|
|
23
|
+
// `attack` will find.
|
|
24
|
+
for (const a of cybercombatCandidates(scene, actor)) {
|
|
25
|
+
if (a === actor)
|
|
26
|
+
continue;
|
|
27
|
+
push(out, seen, { glyph: '◆', name: a.name, what: 'persona' });
|
|
28
|
+
}
|
|
29
|
+
// PANs AND DEVICES. A body in reach carries a PAN you can crack by its
|
|
30
|
+
// owner's name (hack.ts tryPanHack resolves it exactly that way), and
|
|
31
|
+
// every slaved or loose device answers to its own name.
|
|
32
|
+
for (const { room } of roomsInReach(rooms, actor)) {
|
|
33
|
+
for (const body of room.getActors()) {
|
|
34
|
+
if (body === actor)
|
|
35
|
+
continue;
|
|
36
|
+
push(out, seen, { glyph: '▣', name: body.name, what: 'a body in reach -- its PAN' });
|
|
37
|
+
}
|
|
38
|
+
for (const device of room.openableDevices()) {
|
|
39
|
+
push(out, seen, { glyph: '▤', name: device.name, what: `device, rating ${device.rating}` });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// THE ARCHIVE, once a Matrix Search has listed it (Host.searchedBy) or
|
|
43
|
+
// the node is cracked -- the same gate every other renderer asks.
|
|
44
|
+
const inside = actor.insideHost;
|
|
45
|
+
const host = inside?.host;
|
|
46
|
+
if (host && (host.cracked || host.searchedBy.has(actor.name))) {
|
|
47
|
+
for (const file of host.files()) {
|
|
48
|
+
push(out, seen, { glyph: '◇', name: file.name, what: 'file' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** Everything a body could name in the room it is standing in. */
|
|
54
|
+
function meatNameables(actor) {
|
|
55
|
+
const out = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const room = actor.currentLocation;
|
|
58
|
+
if (!room)
|
|
59
|
+
return out;
|
|
60
|
+
for (const a of visibleActorsIn(room, actor)) {
|
|
61
|
+
if (a === actor)
|
|
62
|
+
continue;
|
|
63
|
+
push(out, seen, { glyph: '◆', name: a.name, what: 'someone here' });
|
|
64
|
+
}
|
|
65
|
+
for (const device of room.openableDevices()) {
|
|
66
|
+
push(out, seen, { glyph: '▤', name: device.name, what: 'a device' });
|
|
67
|
+
}
|
|
68
|
+
for (const item of room.getItemsForActor(actor)) {
|
|
69
|
+
push(out, seen, { glyph: '◇', name: item.name, what: 'on the ground' });
|
|
70
|
+
}
|
|
71
|
+
// WHAT YOU ARE CARRYING is nameable too -- "equip", "drop", "reload
|
|
72
|
+
// <box>" and "use" all read the pack, and a long ammunition name is
|
|
73
|
+
// exactly the sort of thing nobody should have to spell.
|
|
74
|
+
for (const item of actor.inventory.getAllItems()) {
|
|
75
|
+
push(out, seen, { glyph: '▪', name: item.name, what: 'carried' });
|
|
76
|
+
}
|
|
77
|
+
// The ways out, by the name "go" takes.
|
|
78
|
+
for (const dir of Object.keys(room.exits ?? {})) {
|
|
79
|
+
push(out, seen, { glyph: '→', name: dir, what: 'a way out' });
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The nameable set for whichever plane this actor is standing on.
|
|
85
|
+
* Astral and rigged frames fall back to the meat room: an astral form
|
|
86
|
+
* still names the people and the ward in front of it.
|
|
87
|
+
*/
|
|
88
|
+
export function nameablesFor(scene, actor, rooms) {
|
|
89
|
+
if (actor.plane === 'matrix') {
|
|
90
|
+
return matrixNameables(scene, actor, rooms ?? scene.getRooms());
|
|
91
|
+
}
|
|
92
|
+
return meatNameables(actor);
|
|
93
|
+
}
|
|
94
|
+
/** The roster a refusal prints: just the names, ready to join. */
|
|
95
|
+
export function nameableNames(scene, actor, rooms) {
|
|
96
|
+
return nameablesFor(scene, actor, rooms).map(n => n.name);
|
|
97
|
+
}
|
|
98
|
+
/** Where the persona is, for a "nothing here" line that says where here is. */
|
|
99
|
+
export function whereAmI(actor) {
|
|
100
|
+
if (actor.plane !== 'matrix')
|
|
101
|
+
return actor.currentLocation?.name ?? 'nowhere';
|
|
102
|
+
const inside = actor.insideHost;
|
|
103
|
+
if (inside)
|
|
104
|
+
return hostLabel(inside);
|
|
105
|
+
return actor.currentGrid?.name ?? gridVicinity(actor).name;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=nameables.js.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
-
import { normalizeMatrixStance, normalizeStance } from '../models/player.js';
|
|
3
2
|
import { resolveSpot } from './spots.js';
|
|
4
3
|
import { isAspectKey } from '../aspects.js';
|
|
5
4
|
import * as path from 'path';
|
|
@@ -374,8 +373,6 @@ export function serializePlayer(p) {
|
|
|
374
373
|
hungerStreak: p.hungerStreak, sleepStreak: p.sleepStreak,
|
|
375
374
|
hungerFatigue: p.hungerFatigue, sleepFatigue: p.sleepFatigue,
|
|
376
375
|
},
|
|
377
|
-
autoStance: p.autoStance,
|
|
378
|
-
matrixStance: p.matrixStance,
|
|
379
376
|
inventory: p.inventory.getAllItems().map(serializeItem),
|
|
380
377
|
equipment: serializeEquipment(p.equipment),
|
|
381
378
|
};
|
|
@@ -542,10 +539,9 @@ export function restorePlayer(p, s) {
|
|
|
542
539
|
p.sleepStreak = s.needs.sleepStreak;
|
|
543
540
|
p.edgeRemaining = s.edgeRemaining;
|
|
544
541
|
p.haggledThisJob = s.haggledThisJob;
|
|
545
|
-
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
p.matrixStance = normalizeMatrixStance(s.matrixStance);
|
|
542
|
+
// (s.autoStance / s.matrixStance are ignored: the stance was retired in
|
|
543
|
+
// f79LLqTerep4nKWcA and the choice lives on the verbs. Older saves still
|
|
544
|
+
// carry both fields; reading them would restore a setting nothing obeys.)
|
|
549
545
|
if (s.archetypeName)
|
|
550
546
|
p.archetypeName = s.archetypeName;
|
|
551
547
|
if (s.gender)
|
|
@@ -123,7 +123,7 @@ export function enterMatrix(scene, actor, mode) {
|
|
|
123
123
|
: `Your ${actor.activeDeck?.name ?? 'deck'} spins up and the room dissolves into geometry -- you're in. Your body slumps where you stood in ${actor.bodyRoom.name}, empty.`;
|
|
124
124
|
return [
|
|
125
125
|
`${entry}${hotWarning}${deckNote} You come up on ${actor.currentGrid?.name ?? 'the grid'}.${actor.currentGrid?.userPenalty ? ' Public grid: -2 on everything you do out here.' : ''}`,
|
|
126
|
-
hint(`No streets out here: "map" lights the hosts overhead, "
|
|
126
|
+
hint(`No streets out here: "map" lights the hosts overhead and names them, "hack <host>" slips a mark in quiet (p.240) and "force <host>" smashes one in (p.238), one mark lets "enter <host>" walk you inside from anywhere, "mark" shows who holds what, "jack out" to return.`),
|
|
127
127
|
].filter(l => l.length > 0);
|
|
128
128
|
}
|
|
129
129
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.208.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { Command } from './command.js';
|
|
2
|
-
import { STANCE_CYCLE, STANCE_ALIASES, STANCE_DESCRIPTION, MATRIX_STANCE_CYCLE, MATRIX_STANCE_ALIASES, MATRIX_STANCE_ACTION } from '../models/player.js';
|
|
3
|
-
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
4
|
-
/**
|
|
5
|
-
* The typed twin of the Tab key: set (or inspect) the stance. On either
|
|
6
|
-
* plane the stance is what your next action DOES -- in the meat, the
|
|
7
|
-
* damage type your blow is meant to deal (lethal / non-lethal, see
|
|
8
|
-
* FightStance in models/player.ts); on the Matrix, which intrusion
|
|
9
|
-
* action a hack is (Brute Force / Hack on the Fly). It never decides
|
|
10
|
-
* whether you act: since the Combat Turn (2026-09-06) every phase is
|
|
11
|
-
* yours to play and "end turn" to commit, and the fight autopilot this
|
|
12
|
-
* verb used to set was retired with it (2026-09-07).
|
|
13
|
-
*/
|
|
14
|
-
export class StanceCommand extends Command {
|
|
15
|
-
static verb = 'stance';
|
|
16
|
-
static description = 'Set your stance -- what your next action does, never whether you take it. ON THE MATRIX it picks the intrusion action: attack (Brute Force, p.238) or sleaze (Hack on the Fly, p.240); "deck config" still sets the array. IN THE MEAT it picks the damage type you mean: lethal (the gear as sold) or non-lethal (Stun wherever the gear allows -- stun rounds when you reload, the flat of the blade at Accuracy 3 per p.186, bare hands; a lethal gun with lethal rounds warns you it stays lethal). Tab flips it.';
|
|
17
|
-
/**
|
|
18
|
-
* ON THE MATRIX, TAB FLIPS THE INTRUSION ACTION instead of the fight
|
|
19
|
-
* autopilot (mjmcee, uMQAhaAysgaKpWFkn). One verb and one key, two
|
|
20
|
-
* planes -- because the player's question is the same in both ("how
|
|
21
|
-
* hard am I about to go?") and they are never on both planes at once.
|
|
22
|
-
*/
|
|
23
|
-
matrixLines(actor) {
|
|
24
|
-
const cur = actor.matrixStance;
|
|
25
|
-
return [
|
|
26
|
-
`Matrix stance: ${MATRIX_STANCE_ACTION[cur].toUpperCase()} (${cur}) -- ${StanceCommand.describeMatrix(cur)}.`,
|
|
27
|
-
'',
|
|
28
|
-
...MATRIX_STANCE_CYCLE.map(m => ` ${MATRIX_STANCE_ACTION[m].padEnd(16)} -- ${StanceCommand.describeMatrix(m)}`),
|
|
29
|
-
'',
|
|
30
|
-
`Set with "stance attack" / "stance sleaze", or Tab to flip. "deck config" still sets the array itself.`,
|
|
31
|
-
].join('\n');
|
|
32
|
-
}
|
|
33
|
-
/** What each route costs and buys, in the book's terms (p.238/240). */
|
|
34
|
-
static describeMatrix(m) {
|
|
35
|
-
return m === 'attack'
|
|
36
|
-
? 'Cybercombat + Logic [Attack]; a miss goes unnoticed but the firewall bites, and net hits burn the target'
|
|
37
|
-
: 'Hacking + Logic [Sleaze]; a miss gets you MADE, and net hits read the target while you key it';
|
|
38
|
-
}
|
|
39
|
-
async execute(args = []) {
|
|
40
|
-
const actor = this.actor;
|
|
41
|
-
const onMatrix = actor.plane === 'matrix';
|
|
42
|
-
if (!args || args.length === 0) {
|
|
43
|
-
if (onMatrix)
|
|
44
|
-
return this.matrixLines(actor);
|
|
45
|
-
const lines = [
|
|
46
|
-
`Stance: ${actor.autoStance.toUpperCase()} -- ${STANCE_DESCRIPTION[actor.autoStance]}.`,
|
|
47
|
-
'',
|
|
48
|
-
...STANCE_CYCLE.map(s => ` ${s.padEnd(10)} -- ${STANCE_DESCRIPTION[s]}`),
|
|
49
|
-
'',
|
|
50
|
-
`Set with "stance <name>", or Tab to flip. The stance is what a blow does, not who throws it -- every action is yours to type, and "end turn" commits the phase.`,
|
|
51
|
-
];
|
|
52
|
-
return lines.join('\n');
|
|
53
|
-
}
|
|
54
|
-
// "stance next" -- THE TAB KEY'S WIRE FORM (playtest 2026-08-27:
|
|
55
|
-
// "the combat stance isn't updating in the menu bar"). Tab used to
|
|
56
|
-
// cycle by mutating game.player.autoStance directly, which in a
|
|
57
|
-
// SHARED RUN is the local hub player, not the runner on the table:
|
|
58
|
-
// the server's stance never moved, the autopilot never changed
|
|
59
|
-
// temperature, and the HUD -- painted from the server's status
|
|
60
|
-
// events -- never showed the switch. Tab now sends this instead, so
|
|
61
|
-
// the cycle advances from the ACTOR's real stance wherever the
|
|
62
|
-
// engine happens to be running. Checked before the fuzzy match: it
|
|
63
|
-
// is a control word, not a stance name, and fuzzyPickName would
|
|
64
|
-
// otherwise try to spell-correct it into one.
|
|
65
|
-
const raw = args.join(' ').trim().toLowerCase();
|
|
66
|
-
if (raw === 'next' || raw === 'cycle') {
|
|
67
|
-
if (onMatrix) {
|
|
68
|
-
const i = MATRIX_STANCE_CYCLE.indexOf(actor.matrixStance);
|
|
69
|
-
const next = MATRIX_STANCE_CYCLE[(i + 1) % MATRIX_STANCE_CYCLE.length];
|
|
70
|
-
actor.matrixStance = next;
|
|
71
|
-
this.scene.updateStatus();
|
|
72
|
-
this.logger.write(`${actor.name} flipped to ${MATRIX_STANCE_ACTION[next]}.`);
|
|
73
|
-
return `Matrix stance: ${MATRIX_STANCE_ACTION[next].toUpperCase()} -- ${StanceCommand.describeMatrix(next)}.`;
|
|
74
|
-
}
|
|
75
|
-
const cur = STANCE_CYCLE.indexOf(actor.autoStance);
|
|
76
|
-
const nextStance = STANCE_CYCLE[(cur + 1) % STANCE_CYCLE.length];
|
|
77
|
-
actor.autoStance = nextStance;
|
|
78
|
-
this.scene.updateStatus();
|
|
79
|
-
this.logger.write(`${actor.name} cycled stance to ${nextStance}.`);
|
|
80
|
-
return `Stance: ${nextStance.toUpperCase()} -- ${STANCE_DESCRIPTION[nextStance]}.`;
|
|
81
|
-
}
|
|
82
|
-
// A Matrix route by name, from either plane's vocabulary -- so
|
|
83
|
-
// "stance sleaze" works while jacked in and reads as a typo
|
|
84
|
-
// otherwise, rather than fuzzy-matching into a fight stance.
|
|
85
|
-
const matrixNamed = MATRIX_STANCE_ALIASES[raw] ?? (MATRIX_STANCE_CYCLE.includes(raw) ? raw : undefined);
|
|
86
|
-
if (onMatrix && matrixNamed) {
|
|
87
|
-
if (matrixNamed === actor.matrixStance)
|
|
88
|
-
return `Already ${MATRIX_STANCE_ACTION[matrixNamed].toUpperCase()} -- ${StanceCommand.describeMatrix(matrixNamed)}.`;
|
|
89
|
-
actor.matrixStance = matrixNamed;
|
|
90
|
-
this.scene.updateStatus();
|
|
91
|
-
this.logger.write(`${actor.name} set Matrix stance to ${MATRIX_STANCE_ACTION[matrixNamed]}.`);
|
|
92
|
-
return `Matrix stance: ${MATRIX_STANCE_ACTION[matrixNamed].toUpperCase()} -- ${StanceCommand.describeMatrix(matrixNamed)}.`;
|
|
93
|
-
}
|
|
94
|
-
if (onMatrix) {
|
|
95
|
-
return `That's not a Matrix route. Choose: ${MATRIX_STANCE_CYCLE.join(', ')} (${MATRIX_STANCE_CYCLE.map(m => MATRIX_STANCE_ACTION[m]).join(' / ')}).`;
|
|
96
|
-
}
|
|
97
|
-
// THE EXACT NAME ALWAYS WINS. "stance non-lethal" -- the very word the
|
|
98
|
-
// refusal below lists -- used to be refused: fuzzyPickName normalizes
|
|
99
|
-
// punctuation to whitespace, so "non-lethal" and the "non lethal"
|
|
100
|
-
// alias both became "non lethal", the exact tier saw two candidates,
|
|
101
|
-
// and an ambiguous guess deliberately misses. Caught by a bench that
|
|
102
|
-
// typed the name off the "Choose:" line (2jbprbSYqF7EPXrkR's dry run).
|
|
103
|
-
// The old words (and their typos) still land: fuzzy over the cycle AND the aliases, then through the alias map.
|
|
104
|
-
const pickedRaw = STANCE_CYCLE.includes(raw)
|
|
105
|
-
? raw
|
|
106
|
-
: STANCE_ALIASES[raw] ?? fuzzyPickName(args.join(' '), [...STANCE_CYCLE, ...Object.keys(STANCE_ALIASES)]);
|
|
107
|
-
const picked = pickedRaw ? (STANCE_ALIASES[pickedRaw] ?? pickedRaw) : undefined;
|
|
108
|
-
if (!picked) {
|
|
109
|
-
return `That's not a stance. Choose: ${STANCE_CYCLE.join(', ')}.`;
|
|
110
|
-
}
|
|
111
|
-
const stance = picked;
|
|
112
|
-
if (stance === actor.autoStance) {
|
|
113
|
-
return `Already ${stance.toUpperCase()} -- ${STANCE_DESCRIPTION[stance]}.`;
|
|
114
|
-
}
|
|
115
|
-
actor.autoStance = stance;
|
|
116
|
-
this.scene.updateStatus();
|
|
117
|
-
this.logger.write(`${actor.name} set stance to ${stance}.`);
|
|
118
|
-
return `Stance: ${stance.toUpperCase()} -- ${STANCE_DESCRIPTION[stance]}.`;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
//# sourceMappingURL=stance.js.map
|