@maka/maka-cli 5.206.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/factories/repro-scene.js +34 -4
- 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/types/repro.js +1 -1
- 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
|
@@ -7,6 +7,7 @@ import { Logger } from './utilities/logger.js';
|
|
|
7
7
|
import { getArchetype, outfitArchetype } from './archetypes.js';
|
|
8
8
|
import { attachLineEditor, attachArrowMovement } from './utilities/line-editor.js';
|
|
9
9
|
import { attachInputHistory } from './utilities/input-history.js';
|
|
10
|
+
import { COMPLETE_MARKER } from './commands/complete.js';
|
|
10
11
|
import { TERMINAL_RESET_SEQUENCE } from './utilities/terminal-reset.js';
|
|
11
12
|
import { readSave } from './utilities/persistence.js';
|
|
12
13
|
import { requireCatalog, CatalogUnavailableError } from './utilities/catalog.js';
|
|
@@ -322,10 +323,11 @@ export async function main(playerName, sceneSeed, options) {
|
|
|
322
323
|
game.updateInventory(game.player.inventory, capitalCase(game.scene.getCurrencyType()));
|
|
323
324
|
game.updateExits(game.player.currentLocation);
|
|
324
325
|
game.updateStatus();
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// focus lives
|
|
326
|
+
// TAB COMPLETES THE NAME YOU ARE TYPING (f79LLqTerep4nKWcA). It used
|
|
327
|
+
// to flip the stance; the stance is gone, and what the same report
|
|
328
|
+
// asked for in its place is a way to know which icon you are about
|
|
329
|
+
// to hit. Bound on the input box because that's where focus lives
|
|
330
|
+
// during play.
|
|
329
331
|
game.inputBox.key(['tab'], () => {
|
|
330
332
|
// Blessed's textarea reader appends the literal '\t' to the box
|
|
331
333
|
// value on this same keypress (listener order isn't guaranteed
|
|
@@ -339,36 +341,36 @@ export async function main(playerName, sceneSeed, options) {
|
|
|
339
341
|
};
|
|
340
342
|
stripTab();
|
|
341
343
|
setImmediate(() => { stripTab(); screen.render(); });
|
|
342
|
-
// ONE ROUTE, BOTH PLANES, BOTH RUN SHAPES
|
|
343
|
-
//
|
|
344
|
-
// mutated game.player.autoStance here by hand. That duplicate is
|
|
345
|
-
// what made Tab plane-blind -- the Matrix stance
|
|
346
|
-
// (uMQAhaAysgaKpWFkn, 2026-09-03) would have had to be added
|
|
347
|
-
// twice, and the second copy is the one that gets forgotten.
|
|
344
|
+
// ONE ROUTE, BOTH PLANES, BOTH RUN SHAPES -- and the reason the
|
|
345
|
+
// completion is answered by the ENGINE rather than built here.
|
|
348
346
|
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
347
|
+
// Locally this process holds the whole scene and could list the
|
|
348
|
+
// names itself. In a SHARED RUN it holds nothing: game.player is
|
|
349
|
+
// the LOCAL HUB player, the runner on the table is a different
|
|
350
|
+
// object in a different process, and the room arrives as rendered
|
|
351
|
+
// text with the structured payload dropped (utilities/shared-run.ts).
|
|
352
|
+
// A completer built on what is in memory here would work solo and
|
|
353
|
+
// silently do nothing on the grid with three other people -- which
|
|
354
|
+
// is the same duplicate-path trap the old stance binding fell into.
|
|
355
|
+
// So: send the box, let the engine answer, on both run shapes.
|
|
358
356
|
void (async () => {
|
|
359
|
-
const
|
|
357
|
+
const box = game.inputBox.getValue().replace(/\t/g, '');
|
|
358
|
+
const res = await game.handleInput(`complete ${box}`);
|
|
360
359
|
if (!res)
|
|
361
360
|
return;
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
361
|
+
const lines = res.split('\n');
|
|
362
|
+
// THE MARKER LINE refills the box (see commands/complete.ts).
|
|
363
|
+
// Everything else is the candidate list, and goes to the ticker
|
|
364
|
+
// rather than the transcript: Tab is not a turn.
|
|
365
|
+
if (lines[0]?.startsWith(COMPLETE_MARKER)) {
|
|
366
|
+
const filled = lines[0].slice(COMPLETE_MARKER.length);
|
|
367
|
+
game.inputBox.setValue(filled);
|
|
368
|
+
lines.shift();
|
|
369
|
+
}
|
|
370
|
+
for (const l of lines) {
|
|
371
|
+
if (l.trim().length > 0)
|
|
372
|
+
gameLog.meta(l, 'white');
|
|
373
|
+
}
|
|
372
374
|
screen.render();
|
|
373
375
|
})();
|
|
374
376
|
});
|
|
@@ -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.",
|