@maka/maka-cli 5.160.0 → 5.162.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/equip.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/equipment.js +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +12 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/read.js +12 -1
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +23 -3
- package/bundle/typescript/src/commands/game/sideQuest/game.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +16 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +36 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/clue-reading.js +84 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +60 -8
- package/bundle/typescript/src/commands/game/sideQuest/utilities/map-view.js +33 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +4 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +10 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.162.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.",
|
|
@@ -125,7 +125,7 @@ export class EquipCommand extends Command {
|
|
|
125
125
|
// hear which wetware that is (user, 2026-09-02).
|
|
126
126
|
const dni = actor.dniSource();
|
|
127
127
|
const link = dni?.kind === 'datajack' ? `It cables straight into your datajack -- VR ready.`
|
|
128
|
-
: dni?.kind === 'trodes' ? `It rides your ${dni.item?.name ?? 'trode net'}${dni.worn ? '' : ' (in the bag -- "equip" it to wear it)'} -- VR ready.`
|
|
128
|
+
: dni?.kind === 'trodes' ? `It rides your ${dni.item?.name ?? 'trode net'}${dni.host ? ` built into your ${dni.host.name}${dni.worn ? '' : ' (in the bag -- "equip" the helmet to wear it)'}` : dni.worn ? '' : ' (in the bag -- "equip" it to wear it)'} -- VR ready.`
|
|
129
129
|
: dni?.kind === 'technomancer' ? `Your living persona is the interface.`
|
|
130
130
|
: `No direct neural interface yet: AR only until you wear a trode net or install a datajack.`;
|
|
131
131
|
return `The ${name} doesn't wear -- it's a slab of hardware in your kit. ${link} "jack in" when you're ready to ride it${hint(`, "pan" to see your network`)}.`;
|
|
@@ -75,7 +75,7 @@ export class EquipmentCommand extends Command {
|
|
|
75
75
|
? `none -- AR only${hint(' (a trode net from any electronics shelf, or a datajack at the clinic)')}`
|
|
76
76
|
: dni.kind === 'technomancer' ? 'living persona -- VR ready'
|
|
77
77
|
: dni.kind === 'datajack' ? 'datajack (implanted) -- VR ready'
|
|
78
|
-
: `${dni.item?.name ?? 'trode net'} (${dni.worn ? 'worn' : 'in the bag'}) -- VR ready`;
|
|
78
|
+
: `${dni.item?.name ?? 'trode net'} (${dni.host ? `built into your ${dni.host.name}${dni.worn ? '' : ', in the bag'}` : dni.worn ? 'worn' : 'in the bag'}) -- VR ready`;
|
|
79
79
|
lines.push(` Interface: ${dniLine}`);
|
|
80
80
|
if (link) {
|
|
81
81
|
lines.push(` Commlink: ${link.name}${actor.runningSilent ? ' -- running silent' : ''}`);
|
|
@@ -5,6 +5,8 @@ import { matchesHostName, matchesCameraName, hostLabel } from '../utilities/grid
|
|
|
5
5
|
import { Door } from '../models/door.js';
|
|
6
6
|
import { Item } from '../models/item.js';
|
|
7
7
|
import { NPC } from '../models/npc.js';
|
|
8
|
+
import { Player } from '../models/player.js';
|
|
9
|
+
import { readDetails } from '../utilities/clue-reading.js';
|
|
8
10
|
import { Category } from '../types/shared/item-enum.js';
|
|
9
11
|
import { Command } from './command.js';
|
|
10
12
|
import { shellsInRoom, planeTintItemName, emphasizeFind, fileReach } from '../utilities/planes.js';
|
|
@@ -579,7 +581,16 @@ export class LookCommand extends Command {
|
|
|
579
581
|
if (!item) {
|
|
580
582
|
return `You don't see any ${itemName} here.`;
|
|
581
583
|
}
|
|
582
|
-
|
|
584
|
+
// A CODE ON THE PAGE IS A PERCEPTION TEST (Xxbwtgffkyq8uCHfR): a
|
|
585
|
+
// scrap of paper with a combination on it is read here, not by
|
|
586
|
+
// "read" (which is the Book verb), so it takes the same test -- one
|
|
587
|
+
// gate for one credential, whichever verb reaches it.
|
|
588
|
+
const details = readDetails(this.scene, item, {
|
|
589
|
+
reader: this.actor instanceof Player ? this.actor : undefined,
|
|
590
|
+
logger: this.logger,
|
|
591
|
+
isPlayer: this.scene.isHumanControlled(this.actor),
|
|
592
|
+
});
|
|
593
|
+
const prose = details || item.description || `It's just a regular ${itemName}.`;
|
|
583
594
|
if (item.category == Category.Book) {
|
|
584
595
|
// A book's payload IS its detail -- "read" is the verb that opens
|
|
585
596
|
// it, and saying so beats a stat block on a paperback.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from './command.js';
|
|
2
2
|
import { Category } from '../types/shared/item-enum.js';
|
|
3
|
+
import { Player } from '../models/player.js';
|
|
4
|
+
import { readDetails } from '../utilities/clue-reading.js';
|
|
3
5
|
export class ReadCommand extends Command {
|
|
4
6
|
static verb = 'read';
|
|
5
7
|
static description = 'Read a book from your surroundings.';
|
|
@@ -21,7 +23,16 @@ export class ReadCommand extends Command {
|
|
|
21
23
|
// "details" is where clue/note content is meant to live, but "description"
|
|
22
24
|
// is a reasonable fallback if a scene ever puts it there instead -- better
|
|
23
25
|
// to show something real than a dead "blank pages" non-answer.
|
|
24
|
-
|
|
26
|
+
//
|
|
27
|
+
// A CODE ON THE PAGE IS A PERCEPTION TEST (Xxbwtgffkyq8uCHfR):
|
|
28
|
+
// utilities/clue-reading.ts rolls it, and hands back the prose with
|
|
29
|
+
// the credential in or out of it.
|
|
30
|
+
const details = readDetails(this.scene, item, {
|
|
31
|
+
reader: this.actor instanceof Player ? this.actor : undefined,
|
|
32
|
+
logger: this.logger,
|
|
33
|
+
isPlayer: this.scene.isHumanControlled(this.actor),
|
|
34
|
+
});
|
|
35
|
+
return details || item.description || `You flip through "${item.name}" but it's mostly blank pages.`;
|
|
25
36
|
}
|
|
26
37
|
}
|
|
27
38
|
//# sourceMappingURL=read.js.map
|
|
@@ -392,5 +392,9 @@
|
|
|
392
392
|
// aggressor and their target; the world event carries it; "initiative"
|
|
393
393
|
// marks them. Hostiles are red on the room map by stance or by live
|
|
394
394
|
// enmity, not by the retired exchange flag.
|
|
395
|
-
|
|
395
|
+
// 1.45.0 (2026-09-11): THE MAPS AS DATA. The condition beat carries the
|
|
396
|
+
// room's live grid, every body's engine-resolved square, and the room
|
|
397
|
+
// panel's own hostile test; the map event carries the fog-applied
|
|
398
|
+
// district layout (data.district). Envelope and beat shape both grew.
|
|
399
|
+
export const ENGINE_VERSION = '1.45.0';
|
|
396
400
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -419,7 +419,9 @@ ${JSON_ONLY}
|
|
|
419
419
|
// exit's "guard"; people are got past socially, not picked.
|
|
420
420
|
"grantsItem"?: string // an items[].name with role "reward" -- what opening it hands
|
|
421
421
|
// over. Omit for a device that holds a door: the open way IS
|
|
422
|
-
// the reward.
|
|
422
|
+
// the reward. REQUIRED for a device on no exit (a safe, a
|
|
423
|
+
// lockbox, a cabinet): a device is a promise of something
|
|
424
|
+
// behind it, and a safe that opens onto nothing is refused.
|
|
423
425
|
}],
|
|
424
426
|
"items": [{
|
|
425
427
|
"name": string,
|
|
@@ -929,6 +931,20 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
929
931
|
// record -- and which looked like random flakiness precisely
|
|
930
932
|
// because it varied with the character.
|
|
931
933
|
const kind = device.kind ?? 'panel';
|
|
934
|
+
// A DEVICE IS A PROMISE OF SOMETHING BEHIND IT (bPiyyWu9TQ4pBfGbM,
|
|
935
|
+
// Jynx: "it looks like the safe is open, but it never dropped
|
|
936
|
+
// anything"). The Blood Ledger's skeleton declared a floor safe as
|
|
937
|
+
// a bare "lock" in the back office -- on no exit, granting no item
|
|
938
|
+
// -- while the prize sat in another room and the details pass,
|
|
939
|
+
// taking the safe at its word, wrote a combination clue for it.
|
|
940
|
+
// Every rule below checks that a device's way THROUGH is findable;
|
|
941
|
+
// none checked that there was anything on the other side. A safe
|
|
942
|
+
// that opens onto nothing is not a puzzle, it is a lie the room
|
|
943
|
+
// description tells, and the only honest fix is to refuse the
|
|
944
|
+
// skeleton before four detail passes build on it.
|
|
945
|
+
if (refs === 0 && !device.grantsItem) {
|
|
946
|
+
fail(`device "${device.name}" holds no door and grants nothing -- a device is a promise of something behind it. Declare an exit from "${device.room}" with "device": "${device.name}", or give it "grantsItem" naming a "reward"-role item.`);
|
|
947
|
+
}
|
|
932
948
|
if (kind === 'panel') {
|
|
933
949
|
const clue = skeleton.items.find(i => i.role === 'clue' && i.forDevice === device.name);
|
|
934
950
|
if (!clue) {
|
|
@@ -955,8 +971,12 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
955
971
|
const holdsDoor = refs > 0;
|
|
956
972
|
const keyed = skeleton.rooms.some(r => (r.exits ?? []).some(e => e.device === device.name && e.keyRequired));
|
|
957
973
|
const looseKey = skeleton.items.some(i => i.role === 'key');
|
|
958
|
-
|
|
959
|
-
|
|
974
|
+
// The same floor for a lock that holds a PRIZE rather than a
|
|
975
|
+
// door (a safe): its findable way in is a key, and "holdsDoor"
|
|
976
|
+
// used to exempt it -- so a keyless safe passed here and then
|
|
977
|
+
// stood in play with only breach options (bPiyyWu9TQ4pBfGbM).
|
|
978
|
+
if (!keyed && !looseKey) {
|
|
979
|
+
fail(`lock "${device.name}" ${holdsDoor ? 'holds a door with no "keyRequired"' : 'holds a prize'} and the scene has no "key"-role item -- a runner without locksmith has no way through it. Add a key.`);
|
|
960
980
|
}
|
|
961
981
|
}
|
|
962
982
|
// A WARD is the exception no clue can rescue: there is no findable
|
|
@@ -2401,7 +2401,11 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2401
2401
|
if (!this._conditionSink && process.env.MAKA_NO_DDP === '1')
|
|
2402
2402
|
return;
|
|
2403
2403
|
void import('./utilities/condition-report.js')
|
|
2404
|
-
.then(m => m.reportConditionSoon(this.player, this._conditionSink
|
|
2404
|
+
.then(m => m.reportConditionSoon(this.player, this._conditionSink, {
|
|
2405
|
+
// The room panel's own red (updateExits below): stance OR live
|
|
2406
|
+
// enmity in the viewer's encounter. Same predicate, same map.
|
|
2407
|
+
isHostile: a => hostileByStance(a) || enemyInFight(this.scene, this.player, a),
|
|
2408
|
+
}))
|
|
2405
2409
|
.catch(() => undefined);
|
|
2406
2410
|
};
|
|
2407
2411
|
// A ROOM CHANGE IS A SAVE BEAT (player request 2026-09-02): where you
|
|
@@ -16,7 +16,8 @@ import { Player, Room } from './models/_index.js';
|
|
|
16
16
|
import { Logger } from './utilities/logger.js';
|
|
17
17
|
import { currentSession, runInSession } from './utilities/session-context.js';
|
|
18
18
|
import { restorePlayer, serializePlayer } from './utilities/persistence.js';
|
|
19
|
-
import {
|
|
19
|
+
import { runWithAuthToken } from './utilities/cloud-saves.js';
|
|
20
|
+
import { renderMapViewport, mapCaption, serializeDistrictMap } from './utilities/map-view.js';
|
|
20
21
|
import { hostileByStance, enemyInFight, renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
|
|
21
22
|
import { serializeRoomGrid } from './utilities/room-grid.js';
|
|
22
23
|
import { entrySpotFor, spotOf } from './utilities/spots.js';
|
|
@@ -296,7 +297,18 @@ export async function createHeadlessSession(opts) {
|
|
|
296
297
|
const mapLines = renderMapViewport(rooms, here, sealing, eyes);
|
|
297
298
|
if (mapLines.length > 0)
|
|
298
299
|
mapLines.push(mapCaption(here.name));
|
|
299
|
-
|
|
300
|
+
// STRUCTURED DISTRICT MAP, ADDITIVE (2026-09-11): the same layout
|
|
301
|
+
// as data, on this same event, so the browser client draws it in
|
|
302
|
+
// its own style instead of reparsing the text (the `data.grid`
|
|
303
|
+
// pattern below). A decoration must never kill the push.
|
|
304
|
+
let district;
|
|
305
|
+
try {
|
|
306
|
+
district = serializeDistrictMap(rooms, here, sealing, eyes);
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
logger.write(`District map serialize failed for ${p.name} in ${here.name}: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
310
|
+
}
|
|
311
|
+
logger.system(mapLines.join('\n'), { actor: p.name }, 'map', district ? { district } : undefined);
|
|
300
312
|
// THE ROOM BOX'S TWIN (playtest 2026-08-27, reported twice:
|
|
301
313
|
// "the room minimap isn't updating in the shared run, I only
|
|
302
314
|
// see @open floor"). Game.updateExits early-returns on
|
|
@@ -352,12 +364,12 @@ export async function createHeadlessSession(opts) {
|
|
|
352
364
|
const session = {
|
|
353
365
|
game,
|
|
354
366
|
logger,
|
|
355
|
-
command: (actorName, input) => runInSession(ctx, async () => {
|
|
367
|
+
command: (actorName, input, opts) => runInSession(ctx, () => runWithAuthToken(opts?.authToken, async () => {
|
|
356
368
|
const res = await game.handleInputAs(actorName, input);
|
|
357
369
|
session.pushStatus();
|
|
358
370
|
pushMap();
|
|
359
371
|
return res;
|
|
360
|
-
}),
|
|
372
|
+
})),
|
|
361
373
|
addPlayer: (save) => runInSession(ctx, async () => {
|
|
362
374
|
// REJOIN IS NOT A JOIN (playtest 2026-08-27, Sherry: "got an
|
|
363
375
|
// internal server error"). A dropped DDP connection -- a
|
|
@@ -867,6 +867,9 @@ export class Item extends AbstractItem {
|
|
|
867
867
|
papers = [];
|
|
868
868
|
/** On a license: the name of the fake SIN it was bought for. */
|
|
869
869
|
linkedSin;
|
|
870
|
+
/** Who has already found the credential written in this item's
|
|
871
|
+
* details (utilities/clue-reading.ts): a found code stays found. */
|
|
872
|
+
revealedTo = [];
|
|
870
873
|
isPaper() {
|
|
871
874
|
return this.category === Category.FakeSIN || this.category === Category.License || this.category === Category.SIN;
|
|
872
875
|
}
|
|
@@ -703,9 +703,17 @@ export class Player extends AbstractPlayer {
|
|
|
703
703
|
return true;
|
|
704
704
|
return this.dniSource() !== null;
|
|
705
705
|
}
|
|
706
|
+
/** Failed reads of a clue, by item name, for p.49's Trying Again
|
|
707
|
+
* (-2 per retry). In memory only: a save and reload is the "break"
|
|
708
|
+
* the rule says clears it. See utilities/clue-reading.ts. */
|
|
709
|
+
readAttempts = new Map();
|
|
706
710
|
/** True for the one Electronics item that is a trode net (p.439-440). */
|
|
707
711
|
static isTrodeNet(item) {
|
|
708
|
-
|
|
712
|
+
// The item's OWN row, not Item.effects: that getter folds fitted
|
|
713
|
+
// mods in, so a helmet with a net built into it would read as a
|
|
714
|
+
// trode net itself and be named as the interface instead of the
|
|
715
|
+
// net inside it (dniSource reports the net, and the helmet as host).
|
|
716
|
+
if ((item.row?.effects ?? []).some(e => e.type === 'flag' && e.property === 'dni' && e.value > 0))
|
|
709
717
|
return true;
|
|
710
718
|
if (item.row)
|
|
711
719
|
return false;
|
|
@@ -726,8 +734,33 @@ export class Player extends AbstractPlayer {
|
|
|
726
734
|
const worn = this._equipment.head?.trodes;
|
|
727
735
|
if (worn)
|
|
728
736
|
return { kind: 'trodes', worn: true, item: worn };
|
|
729
|
-
|
|
730
|
-
|
|
737
|
+
// TRODES BUILT INTO HEADGEAR (koz9FKuH8Q685ogcy: "I see my helmet
|
|
738
|
+
// can hold trodes, this should be where my interface to the
|
|
739
|
+
// commlink is, right?"). Right: "Adding this to headgear takes two
|
|
740
|
+
// slots of Capacity" (p.439-440), and the helmet row has advertised
|
|
741
|
+
// a trodes slot all along -- so a net fitted into a WORN helmet is
|
|
742
|
+
// a worn net, and reads here as one. Any worn slot is searched, not
|
|
743
|
+
// just the skull: what the catalog lets hold trodes is the
|
|
744
|
+
// catalog's business.
|
|
745
|
+
for (const group of Object.values(this._equipment)) {
|
|
746
|
+
if (!group)
|
|
747
|
+
continue;
|
|
748
|
+
for (const host of Object.values(group)) {
|
|
749
|
+
if (!host)
|
|
750
|
+
continue;
|
|
751
|
+
const built = host.fitted.find(m => Player.isTrodeNet(m));
|
|
752
|
+
if (built)
|
|
753
|
+
return { kind: 'trodes', worn: true, item: built, host };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (const carried of this._inventory.getAllItems()) {
|
|
757
|
+
if (Player.isTrodeNet(carried))
|
|
758
|
+
return { kind: 'trodes', worn: false, item: carried };
|
|
759
|
+
const built = carried.fitted.find(m => Player.isTrodeNet(m));
|
|
760
|
+
if (built)
|
|
761
|
+
return { kind: 'trodes', worn: false, item: built, host: carried };
|
|
762
|
+
}
|
|
763
|
+
return null;
|
|
731
764
|
}
|
|
732
765
|
/**
|
|
733
766
|
* THE PRICE OF SILENCE (canon p.235-236 "Running Silent",
|
|
@@ -3,6 +3,7 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as http from 'http';
|
|
5
5
|
import * as https from 'https';
|
|
6
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
7
|
import { Logger } from './logger.js';
|
|
7
8
|
import { saveSlug, readSave, writeSaveAtomic, tombstonePathFor } from './persistence.js';
|
|
8
9
|
/**
|
|
@@ -90,7 +91,29 @@ export function siteRequest(method, apiPath, token, body) {
|
|
|
90
91
|
return request(method, apiPath, token, body);
|
|
91
92
|
}
|
|
92
93
|
const GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.meteor', '.maka', 'global-config.json');
|
|
94
|
+
/**
|
|
95
|
+
* A LOGIN SCOPED TO ONE ASYNC FLOW, for the site's in-process engine
|
|
96
|
+
* (user 2026-09-10: "I can't record to the backlog while in the web
|
|
97
|
+
* client -- I AM logged in", answered with "Your login's expired").
|
|
98
|
+
*
|
|
99
|
+
* authToken() reads the machine's `maka login` -- right on a terminal,
|
|
100
|
+
* wrong on www.maka-cli.com, where one Node process hosts every
|
|
101
|
+
* browser table and the box's own global config holds whichever stale
|
|
102
|
+
* token last ran `maka` there. The site knows who typed the command;
|
|
103
|
+
* it hands that user's token to HeadlessSession.command, which runs
|
|
104
|
+
* the verb inside this store, and every transport that asks
|
|
105
|
+
* authToken() -- backlog, social, telemetry -- answers as THEM for the
|
|
106
|
+
* length of that one command. No token, no store: a terminal's
|
|
107
|
+
* behavior is byte-for-byte what it was.
|
|
108
|
+
*/
|
|
109
|
+
const scopedToken = new AsyncLocalStorage();
|
|
110
|
+
export function runWithAuthToken(token, fn) {
|
|
111
|
+
return token ? scopedToken.run(token, fn) : fn();
|
|
112
|
+
}
|
|
93
113
|
export function authToken() {
|
|
114
|
+
const scoped = scopedToken.getStore();
|
|
115
|
+
if (scoped)
|
|
116
|
+
return scoped;
|
|
94
117
|
try {
|
|
95
118
|
const cfg = JSON.parse(fs.readFileSync(GLOBAL_CONFIG_PATH, 'utf8'));
|
|
96
119
|
const token = cfg?.token?.authToken;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { perceptionTest, PERCEPTION_THRESHOLD } from './perception.js';
|
|
2
|
+
import { rollPool, formatRoll } from './dice.js';
|
|
3
|
+
import { hint } from './hints.js';
|
|
4
|
+
/**
|
|
5
|
+
* READING A CLUE IS A PERCEPTION TEST (Xxbwtgffkyq8uCHfR, Maka:
|
|
6
|
+
* 'reading codes on "books" should use a perception check to get
|
|
7
|
+
* critical information').
|
|
8
|
+
*
|
|
9
|
+
* A clue's body is plain prose anyone can read; the CREDENTIAL in it
|
|
10
|
+
* -- a maglock code in a margin, a combination on a folded page -- is
|
|
11
|
+
* the detail that "isn't obvious", and p.135 says that is exactly what
|
|
12
|
+
* a Perception Test is for: Perception + Intuition [Mental], threshold
|
|
13
|
+
* 2 for something Normal ("street sign, conversation", p.136). Hits at
|
|
14
|
+
* the threshold and the page gives up the code; short of it the reader
|
|
15
|
+
* gets the prose and not the number.
|
|
16
|
+
*
|
|
17
|
+
* TRYING AGAIN (p.49): a re-read is allowed at a cumulative -2 per
|
|
18
|
+
* retry, tracked on the reader for the session. A found code stays
|
|
19
|
+
* found (Item.revealedTo, saved with the item) -- nobody re-rolls for a
|
|
20
|
+
* number they have already read out loud.
|
|
21
|
+
*
|
|
22
|
+
* WHAT COUNTS AS A CODE: the scene's own device codes, matched
|
|
23
|
+
* verbatim against the item's details -- the same fact ensureClueDetails
|
|
24
|
+
* guarantees is written there (scene-chunks.ts). No code on the page
|
|
25
|
+
* means no test: a diary is just a diary.
|
|
26
|
+
*/
|
|
27
|
+
/** The p.49 penalty per failed retry. */
|
|
28
|
+
export const TRYING_AGAIN_PENALTY = 2;
|
|
29
|
+
/** Every device code the scene knows that this item's details spell out. */
|
|
30
|
+
export function codesOnPage(scene, item) {
|
|
31
|
+
const text = (item.details ?? '').toLowerCase();
|
|
32
|
+
if (!text)
|
|
33
|
+
return [];
|
|
34
|
+
const codes = new Set();
|
|
35
|
+
for (const room of Object.values(scene.getRooms())) {
|
|
36
|
+
for (const device of room.devices) {
|
|
37
|
+
const code = (device.code ?? '').trim();
|
|
38
|
+
if (code && text.includes(code.toLowerCase()))
|
|
39
|
+
codes.add(code);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return [...codes];
|
|
43
|
+
}
|
|
44
|
+
/** The details with every sentence that spells a code taken out. */
|
|
45
|
+
export function redactCodes(details, codes) {
|
|
46
|
+
const lower = codes.map(c => c.toLowerCase());
|
|
47
|
+
const kept = details
|
|
48
|
+
.split(/(?<=[.!?]["'\u201d\u2019)]?)\s+/)
|
|
49
|
+
.filter(sentence => !lower.some(c => sentence.toLowerCase().includes(c)));
|
|
50
|
+
return kept.join(' ').trim();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The text a reader gets from an item's details: everything, when the
|
|
54
|
+
* page holds no code or the reader already found it; otherwise the
|
|
55
|
+
* Perception Test decides whether the credential is on the page.
|
|
56
|
+
*/
|
|
57
|
+
export function readDetails(scene, item, opts = {}) {
|
|
58
|
+
const details = item.details ?? '';
|
|
59
|
+
const reader = opts.reader;
|
|
60
|
+
if (!details || !reader)
|
|
61
|
+
return details;
|
|
62
|
+
const codes = codesOnPage(scene, item);
|
|
63
|
+
if (codes.length === 0)
|
|
64
|
+
return details;
|
|
65
|
+
if (item.revealedTo.includes(reader.name))
|
|
66
|
+
return details;
|
|
67
|
+
const attempts = reader.readAttempts.get(item.name) ?? 0;
|
|
68
|
+
const { pool, limit } = perceptionTest(reader);
|
|
69
|
+
const penalty = attempts * TRYING_AGAIN_PENALTY;
|
|
70
|
+
const roll = rollPool(Math.max(1, pool - penalty), limit);
|
|
71
|
+
const rollLine = `Perception: ${formatRoll(roll)} (threshold ${PERCEPTION_THRESHOLD.normal}${penalty ? `, trying again -${penalty}` : ''})`;
|
|
72
|
+
if (opts.isPlayer)
|
|
73
|
+
opts.logger?.meta(rollLine);
|
|
74
|
+
const prefix = opts.isPlayer ? '' : `${rollLine}\n`;
|
|
75
|
+
if (roll.hits >= PERCEPTION_THRESHOLD.normal) {
|
|
76
|
+
item.revealedTo.push(reader.name);
|
|
77
|
+
reader.readAttempts.delete(item.name);
|
|
78
|
+
return `${prefix}${details}`;
|
|
79
|
+
}
|
|
80
|
+
reader.readAttempts.set(item.name, attempts + 1);
|
|
81
|
+
const body = redactCodes(details, codes) || item.description || `You go through "${item.name}" page by page.`;
|
|
82
|
+
return `${prefix}${body}\nSomething in it is written to be missed -- you have the sense you skimmed past it.${hint(` (read it again: -${TRYING_AGAIN_PENALTY} dice per retry, p.49)`)}`;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=clue-reading.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Logger } from './logger.js';
|
|
2
|
-
import { visibleActorsIn } from './spots.js';
|
|
2
|
+
import { visibleActorsIn, seatingIn } from './spots.js';
|
|
3
3
|
import { hostLabel } from './grid-names.js';
|
|
4
|
+
import { serializeRoomGrid } from './room-grid.js';
|
|
5
|
+
import { hostileByStance } from './room-view.js';
|
|
4
6
|
/**
|
|
5
7
|
* THE LIVE CONDITION EMITTER -- the CLI half of the realtime sheet lane
|
|
6
8
|
* (contract agreed with maka-cli.com, 2026-09-01). On every condition
|
|
@@ -71,13 +73,56 @@ export function setMarksPlacedResolver(fn) {
|
|
|
71
73
|
export function setActorRoleResolver(fn) {
|
|
72
74
|
actorRole = fn;
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* THE ROOM'S GEOMETRY, ON THE BEAT (2026-09-11, 8bZXzNXLgKwE5wxgh: "no
|
|
78
|
+
* icons being drawn on the character sheet in the active run"). The web
|
|
79
|
+
* sheet rebuilt its room from the SAVE's hub seed, and a generated run
|
|
80
|
+
* room is not in it -- so in a run the sheet had no furniture, no doors,
|
|
81
|
+
* and no cell to seat a spot-sitting NPC on. The beat already fires on
|
|
82
|
+
* every position change and is the one channel both the sheet and the
|
|
83
|
+
* play page read, so the live grid rides it: the SAME wire shape the
|
|
84
|
+
* room event carries for the browser client (headless.ts pushMap ->
|
|
85
|
+
* serializeRoomGrid). Physical planes only -- jacked in, the meat map is
|
|
86
|
+
* a lie the sheet already refuses to draw. A decoration never kills the
|
|
87
|
+
* beat: any throw here is an absent field, not a lost report.
|
|
88
|
+
*/
|
|
89
|
+
function roomGridFor(player) {
|
|
90
|
+
const room = player.currentLocation;
|
|
91
|
+
if (!room || (player.plane !== 'meat' && player.plane !== 'drone'))
|
|
92
|
+
return undefined;
|
|
93
|
+
try {
|
|
94
|
+
const grid = room.ensureGrid();
|
|
95
|
+
return grid ? serializeRoomGrid(grid, room.name) : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
74
101
|
export function buildConditionReport(player, deps) {
|
|
75
|
-
const { saveSlug, arSightUp } = deps;
|
|
102
|
+
const { saveSlug, arSightUp, isHostile } = deps;
|
|
76
103
|
// BRICKED INCLUDED (web report 2026-09-02: "my deck got bricked, but its
|
|
77
104
|
// status still says 4/12"): a forced dump clears activeDeck and
|
|
78
105
|
// getCyberdeck() hides a bricked deck, so the beat read nothing and the
|
|
79
106
|
// sheet froze on the last number. Status reads getAnyCyberdeck.
|
|
80
107
|
const deck = player.activeDeck ?? player.getAnyCyberdeck();
|
|
108
|
+
// WHERE EVERYONE ACTUALLY STANDS (2026-09-11, user: "movement isn't
|
|
109
|
+
// snappy" on the web). The beat sent `atCell` alone, and `atCell` is
|
|
110
|
+
// the EXCEPTION -- set by a tactical step, cleared by "move to <spot>"
|
|
111
|
+
// and by most arrivals -- so after a plain move the beat carried no
|
|
112
|
+
// cell, the server kept the last one it had, and the dot on the web
|
|
113
|
+
// sat still at a square the runner had left. seatingIn is the engine's
|
|
114
|
+
// one answer to "which square" (the room panel, cover and ranges all
|
|
115
|
+
// read it), so the web now gets the same square the CLI draws. Read at
|
|
116
|
+
// fire time like everything else here; a throw means "no seat", never
|
|
117
|
+
// a lost beat.
|
|
118
|
+
const room = player.currentLocation;
|
|
119
|
+
let seats = new Map();
|
|
120
|
+
if (room && (player.plane === 'meat' || player.plane === 'drone')) {
|
|
121
|
+
try {
|
|
122
|
+
seats = seatingIn(room);
|
|
123
|
+
}
|
|
124
|
+
catch { /* keep the empty map */ }
|
|
125
|
+
}
|
|
81
126
|
return {
|
|
82
127
|
slug: saveSlug(player.name),
|
|
83
128
|
damageTaken: player.damageTaken,
|
|
@@ -85,7 +130,8 @@ export function buildConditionReport(player, deps) {
|
|
|
85
130
|
overflow: 0,
|
|
86
131
|
room: player.currentLocation?.name,
|
|
87
132
|
spot: player.atSpot,
|
|
88
|
-
cell: player.atCell,
|
|
133
|
+
cell: player.atCell ?? seats.get(player.name),
|
|
134
|
+
grid: roomGridFor(player),
|
|
89
135
|
plane: player.plane,
|
|
90
136
|
// Where the meat sits while the persona rides the grid (web accepts
|
|
91
137
|
// since 2026-09-02): "jacked in from Your Hideout". Absent in the meat.
|
|
@@ -196,11 +242,14 @@ export function buildConditionReport(player, deps) {
|
|
|
196
242
|
.slice(0, 24)
|
|
197
243
|
.map(a => ({
|
|
198
244
|
name: a.name,
|
|
245
|
+
// THE SAME RED THE ROOM PANEL USES (see IConditionReportDeps
|
|
246
|
+
// .isHostile): stance flag OR live enmity, and never a
|
|
247
|
+
// surrendered one -- hostileByStance owns that clause.
|
|
199
248
|
kind: a.allyOf ? 'ally'
|
|
200
|
-
: a
|
|
249
|
+
: (isHostile?.(a) ?? hostileByStance(a)) ? 'hostile' : 'neutral',
|
|
201
250
|
role: actorRole(a),
|
|
202
251
|
spot: a.atSpot,
|
|
203
|
-
cell: a.atCell,
|
|
252
|
+
cell: a.atCell ?? seats.get(a.name),
|
|
204
253
|
})),
|
|
205
254
|
};
|
|
206
255
|
}
|
|
@@ -227,7 +276,7 @@ function noteRoster(report) {
|
|
|
227
276
|
const ADDITIVE_FIELDS = [
|
|
228
277
|
'ar', 'actors', 'deckLine', 'deckDamage', 'astralPerceiving', 'bodyRoom', 'sustainingPenalty',
|
|
229
278
|
'matrix', 'panMarks', 'hostMarksOnYou', 'marksPlaced', 'marksOnYou', 'deckStored', 'droneDamage', 'companions', 'pools', 'initiative',
|
|
230
|
-
'karma', 'nuyen', 'edge',
|
|
279
|
+
'karma', 'nuyen', 'edge', 'grid',
|
|
231
280
|
];
|
|
232
281
|
/** Refusal reasons already written to the session log -- one line each. */
|
|
233
282
|
const announcedRefusals = new Map();
|
|
@@ -242,7 +291,10 @@ function noteRefusal(why) {
|
|
|
242
291
|
catch { /* no session */ }
|
|
243
292
|
}
|
|
244
293
|
}
|
|
245
|
-
export function reportConditionSoon(player, sink
|
|
294
|
+
export function reportConditionSoon(player, sink,
|
|
295
|
+
/** The Scene-side half of the beat (IConditionReportDeps.isHostile);
|
|
296
|
+
* the caller that owns a Scene supplies it, nobody else needs to. */
|
|
297
|
+
extra) {
|
|
246
298
|
// Before the TIMER, not just before the call: a scheduled beat firing
|
|
247
299
|
// after a Jest file finished lazy-imported into a torn-down
|
|
248
300
|
// environment and failed CI with every test green (2026-09-01). Under
|
|
@@ -269,7 +321,7 @@ export function reportConditionSoon(player, sink) {
|
|
|
269
321
|
// stance and brandish flow through their accessors' own reports;
|
|
270
322
|
// sustained and deck programs are composed here and ride whatever
|
|
271
323
|
// beat fires next. All read at FIRE time, like position.
|
|
272
|
-
const report = buildConditionReport(player, { saveSlug, arSightUp });
|
|
324
|
+
const report = buildConditionReport(player, { saveSlug, arSightUp, isHostile: extra?.isHostile });
|
|
273
325
|
noteRoster(report);
|
|
274
326
|
if (sink) {
|
|
275
327
|
// IN-PROCESS: no HubLink, no MAKA_NO_DDP gate, no version-skew
|
|
@@ -220,6 +220,39 @@ export function renderMapViewport(rooms, here, sealing, eyes, viewW = 21, viewH
|
|
|
220
220
|
out[halfH] = out[halfH].slice(0, halfW) + `{inverse}${out[halfH][halfW] === ' ' ? '▣' : out[halfH][halfW]}{/inverse}` + out[halfH].slice(halfW + 1);
|
|
221
221
|
return out;
|
|
222
222
|
}
|
|
223
|
+
export function serializeDistrictMap(rooms, here, sealing, eyes) {
|
|
224
|
+
const floors = layoutFloors(rooms, here, sealing);
|
|
225
|
+
if (floors.length === 0)
|
|
226
|
+
return undefined;
|
|
227
|
+
const eye = fogEye(rooms, eyes?.visited);
|
|
228
|
+
const ar = eyes?.arEyes !== false;
|
|
229
|
+
const out = floors.map(floor => {
|
|
230
|
+
const linkSeen = (k) => {
|
|
231
|
+
const a = floor.roomAt.get(`${k.ax},${k.ay}`);
|
|
232
|
+
const b = floor.roomAt.get(`${k.bx},${k.by}`);
|
|
233
|
+
return !!a && !!b && eye.sees(a) && eye.sees(b) && (eye.knows(a) || eye.knows(b));
|
|
234
|
+
};
|
|
235
|
+
const seen = [...floor.posByRoom.entries()].filter(([r]) => eye.sees(r));
|
|
236
|
+
return {
|
|
237
|
+
rooms: seen.map(([r, p]) => {
|
|
238
|
+
const known = eye.knows(r);
|
|
239
|
+
return {
|
|
240
|
+
x: p.x, y: p.y,
|
|
241
|
+
...(known ? { name: r.name } : {}),
|
|
242
|
+
known,
|
|
243
|
+
here: r === here,
|
|
244
|
+
node: known && ar && !!r.hasNode,
|
|
245
|
+
up: known && hasWay(r, 'up'),
|
|
246
|
+
down: known && hasWay(r, 'down'),
|
|
247
|
+
};
|
|
248
|
+
}),
|
|
249
|
+
links: floor.links.filter(linkSeen).map(k => ({ ...k })),
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
const current = Math.max(0, floors.findIndex(f => f.posByRoom.has(here)));
|
|
253
|
+
const notes = fogNotes(floors[current]?.notes ?? [], rooms, eye);
|
|
254
|
+
return { current, floors: out, notes };
|
|
255
|
+
}
|
|
223
256
|
/**
|
|
224
257
|
* The DETAILED view (the "map" text command): named cells, box-drawing
|
|
225
258
|
* connections, and annotations -- the renderer shipped in 5.48.0, now
|
|
@@ -178,6 +178,8 @@ export function serializeItem(item) {
|
|
|
178
178
|
out.linkedSin = item.linkedSin;
|
|
179
179
|
if (item.fitted.length > 0)
|
|
180
180
|
out.fitted = item.fitted.map(serializeItem);
|
|
181
|
+
if (item.revealedTo.length > 0)
|
|
182
|
+
out.revealedTo = [...item.revealedTo];
|
|
181
183
|
if (item.isCyberdeck() && item.deckOrder.join() !== '0,1,2,3')
|
|
182
184
|
out.deckOrder = item.deckOrder;
|
|
183
185
|
return out;
|
|
@@ -231,6 +233,8 @@ export function restoreItem(json) {
|
|
|
231
233
|
item.linkedSin = json.linkedSin;
|
|
232
234
|
if (json.fitted)
|
|
233
235
|
item.fitted = json.fitted.map(restoreItem);
|
|
236
|
+
if (json.revealedTo)
|
|
237
|
+
item.revealedTo = [...json.revealedTo];
|
|
234
238
|
// A running program is in storage by definition -- saves from before
|
|
235
239
|
// storage existed carried only the running set.
|
|
236
240
|
for (const k of item.loadedPrograms)
|
|
@@ -822,6 +822,14 @@ export function serializeRoomGrid(grid, roomName) {
|
|
|
822
822
|
const [x, y, z] = cellKey.split(',').map(Number);
|
|
823
823
|
return { kind: link.kind, x, y, z };
|
|
824
824
|
});
|
|
825
|
+
const terrain = [...grid.terrain.values()].map(r => ({
|
|
826
|
+
id: r.id, label: r.label, cover: r.cover, blocking: r.blocking, cleared: r.cleared,
|
|
827
|
+
cells: r.cells.map(c => ({ x: c.x, y: c.y, z: c.z })),
|
|
828
|
+
}));
|
|
829
|
+
const levels = [];
|
|
830
|
+
for (let z = 1; z < grid.dims.z; z++) {
|
|
831
|
+
levels.push({ z, cells: levelFootprint(grid, z).map(c => ({ x: c.x, y: c.y })) });
|
|
832
|
+
}
|
|
825
833
|
return {
|
|
826
834
|
name: roomName,
|
|
827
835
|
dims: { x: grid.dims.x, y: grid.dims.y, z: grid.dims.z },
|
|
@@ -829,6 +837,8 @@ export function serializeRoomGrid(grid, roomName) {
|
|
|
829
837
|
doors,
|
|
830
838
|
footprints,
|
|
831
839
|
vertical,
|
|
840
|
+
terrain,
|
|
841
|
+
levels,
|
|
832
842
|
};
|
|
833
843
|
}
|
|
834
844
|
function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.162.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.",
|