@maka/maka-cli 5.159.0 → 5.161.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/factories/scene-chunks.js +23 -3
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +63 -5
- 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 +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +4 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.161.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
|
|
@@ -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
|
|
@@ -16,6 +16,7 @@ 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 { runWithAuthToken } from './utilities/cloud-saves.js';
|
|
19
20
|
import { renderMapViewport, mapCaption } 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';
|
|
@@ -352,12 +353,12 @@ export async function createHeadlessSession(opts) {
|
|
|
352
353
|
const session = {
|
|
353
354
|
game,
|
|
354
355
|
logger,
|
|
355
|
-
command: (actorName, input) => runInSession(ctx, async () => {
|
|
356
|
+
command: (actorName, input, opts) => runInSession(ctx, () => runWithAuthToken(opts?.authToken, async () => {
|
|
356
357
|
const res = await game.handleInputAs(actorName, input);
|
|
357
358
|
session.pushStatus();
|
|
358
359
|
pushMap();
|
|
359
360
|
return res;
|
|
360
|
-
}),
|
|
361
|
+
})),
|
|
361
362
|
addPlayer: (save) => runInSession(ctx, async () => {
|
|
362
363
|
// REJOIN IS NOT A JOIN (playtest 2026-08-27, Sherry: "got an
|
|
363
364
|
// 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",
|
|
@@ -1237,7 +1270,18 @@ export class Player extends AbstractPlayer {
|
|
|
1237
1270
|
// Edge is a per-job pool: spend a point (see commands/edge.ts) to arm a
|
|
1238
1271
|
// boost that supercharges your next strike. Refreshed to `edge` on every
|
|
1239
1272
|
// new job (see Game.continueToNextScene).
|
|
1240
|
-
|
|
1273
|
+
// AN ACCESSOR, like weaponDrawn (ZpgTTPgX4Hn7qL5M7, 2026-09-10): the
|
|
1274
|
+
// play page's HUD header shows edge remaining, and a browser run has
|
|
1275
|
+
// no mid-run save to carry a spend -- only the beat can. Source-
|
|
1276
|
+
// compatible: every `edgeRemaining =` still works; it now reports.
|
|
1277
|
+
_edgeRemaining = 0;
|
|
1278
|
+
get edgeRemaining() { return this._edgeRemaining; }
|
|
1279
|
+
set edgeRemaining(v) {
|
|
1280
|
+
if (this._edgeRemaining === v)
|
|
1281
|
+
return;
|
|
1282
|
+
this._edgeRemaining = v;
|
|
1283
|
+
this.noteConditionChanged();
|
|
1284
|
+
}
|
|
1241
1285
|
edgeBoostArmed = false;
|
|
1242
1286
|
// Physical condition monitor, straight from SR5: 8 + (Body / 2, rounded
|
|
1243
1287
|
// up) boxes. Damage fills boxes; a full track is death.
|
|
@@ -2075,7 +2119,17 @@ export class Player extends AbstractPlayer {
|
|
|
2075
2119
|
// Karma: the advancement currency, earned per completed job (see
|
|
2076
2120
|
// Scene.checkWinCondition) and spent in the hideout via "advance" --
|
|
2077
2121
|
// gear stops being the only progression axis.
|
|
2078
|
-
|
|
2122
|
+
// AN ACCESSOR (2026-09-10, same reason as edgeRemaining): the web's
|
|
2123
|
+
// HUD header follows a karma award through the beat, since a browser
|
|
2124
|
+
// run's save summary never moves mid-job. `karma +=` still works.
|
|
2125
|
+
_karma = 0;
|
|
2126
|
+
get karma() { return this._karma; }
|
|
2127
|
+
set karma(v) {
|
|
2128
|
+
if (this._karma === v)
|
|
2129
|
+
return;
|
|
2130
|
+
this._karma = v;
|
|
2131
|
+
this.noteConditionChanged();
|
|
2132
|
+
}
|
|
2079
2133
|
/**
|
|
2080
2134
|
* The effective rating for a skill: explicit rating if set; otherwise
|
|
2081
2135
|
* combat skills fall back to the legacy `combatSkill` attribute (which
|
|
@@ -4189,6 +4243,10 @@ export class Player extends AbstractPlayer {
|
|
|
4189
4243
|
break;
|
|
4190
4244
|
}
|
|
4191
4245
|
this._currency += remaining;
|
|
4246
|
+
// The wallet moved: the web's HUD header reads nuyen off the beat
|
|
4247
|
+
// (2026-09-10) -- a payout, a fare, a purchase all land here.
|
|
4248
|
+
if (delta !== 0)
|
|
4249
|
+
this.noteConditionChanged();
|
|
4192
4250
|
}
|
|
4193
4251
|
}
|
|
4194
4252
|
//# sourceMappingURL=player.js.map
|
|
@@ -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
|
|
@@ -166,6 +166,15 @@ export function buildConditionReport(player, deps) {
|
|
|
166
166
|
// The ONE honest AR answer (2026-09-01): muted, cut, and no-rig
|
|
167
167
|
// all collapse into arSightUp, computed at fire time.
|
|
168
168
|
ar: arSightUp(player),
|
|
169
|
+
// THE HEADER'S NUMBERS (contract 2026-09-10, web accepted first;
|
|
170
|
+
// ZpgTTPgX4Hn7qL5M7: the play page's HUD shows the sheet header). A
|
|
171
|
+
// browser run writes no mid-run save and its roster summary is
|
|
172
|
+
// frozen for the whole job, so an edge spend, a payout, or a karma
|
|
173
|
+
// award reaches the web only through the beat. The engine's own
|
|
174
|
+
// fields, verbatim; edge is BOTH halves or neither on the web side.
|
|
175
|
+
karma: player.karma,
|
|
176
|
+
nuyen: player.currency,
|
|
177
|
+
edge: { remaining: player.edgeRemaining, max: player.edge },
|
|
169
178
|
// THE ROOM'S OTHER DOTS (user: the map becomes a tactical
|
|
170
179
|
// readout). Current room only, read at fire time; spot-seated
|
|
171
180
|
// actors carry `spot` and the web places them on the grid it
|
|
@@ -218,6 +227,7 @@ function noteRoster(report) {
|
|
|
218
227
|
const ADDITIVE_FIELDS = [
|
|
219
228
|
'ar', 'actors', 'deckLine', 'deckDamage', 'astralPerceiving', 'bodyRoom', 'sustainingPenalty',
|
|
220
229
|
'matrix', 'panMarks', 'hostMarksOnYou', 'marksPlaced', 'marksOnYou', 'deckStored', 'droneDamage', 'companions', 'pools', 'initiative',
|
|
230
|
+
'karma', 'nuyen', 'edge',
|
|
221
231
|
];
|
|
222
232
|
/** Refusal reasons already written to the session log -- one line each. */
|
|
223
233
|
const announcedRefusals = new Map();
|
|
@@ -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)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.161.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.",
|