@maka/maka-cli 5.137.0 → 5.139.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/nudge.js +88 -0
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +18 -5
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +17 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +79 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +40 -14
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +31 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.139.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.",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Command } from './command.js';
|
|
2
|
+
import { hint } from '../utilities/hints.js';
|
|
3
|
+
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
4
|
+
import { NUDGES_TO_SKIP } from '../utilities/combat-turn.js';
|
|
5
|
+
/**
|
|
6
|
+
* NUDGE -- the table's answer to a runner who is holding the Action
|
|
7
|
+
* Phase and has gone quiet (project owner, 2026-09-07: "three nudges
|
|
8
|
+
* skips the player being nudged").
|
|
9
|
+
*
|
|
10
|
+
* Since the Combat Turn (utilities/combat-turn.ts) a human's phase
|
|
11
|
+
* waits for "end turn", and since the autopilot went (v5.136.0)
|
|
12
|
+
* nothing plays a phase for anyone. On a shared run that leaves one
|
|
13
|
+
* hole: a human who walks away mid-phase stalls the whole table. There
|
|
14
|
+
* is deliberately no clock -- a timer skips the player who was reading
|
|
15
|
+
* the rules -- so the escape is the table's: every nudge tells the
|
|
16
|
+
* nudged runner, in their own log, that people are waiting; the third
|
|
17
|
+
* nudge on one phase ends it for them, whatever they had left forfeit,
|
|
18
|
+
* exactly as "end turn" with nothing spent (SR5 p.158-159; the book
|
|
19
|
+
* gives no refund). The count is the phase's (CombatEncounter.noteNudge)
|
|
20
|
+
* and starts over the next time they act.
|
|
21
|
+
*
|
|
22
|
+
* Off the phase, or out of a fight, a nudge is a tap on the shoulder
|
|
23
|
+
* only the nudged runner sees. Human-only: an NPC acts by rule on its
|
|
24
|
+
* own phase and must never be able to skip a human's.
|
|
25
|
+
*/
|
|
26
|
+
export class NudgeCommand extends Command {
|
|
27
|
+
static verb = 'nudge';
|
|
28
|
+
static humanOnly = true;
|
|
29
|
+
static description = 'Prod another runner on a shared run: "nudge <name>". If they are holding the Action Phase, that tells them the table is waiting -- the third nudge on one phase ends it for them, whatever is unspent forfeit, the same as "end turn" with nothing spent. Off the phase, or out of a fight, it is a tap on the shoulder only they see. "initiative" names who is holding the phase.';
|
|
30
|
+
/** The scene's other humans -- the only legal targets. */
|
|
31
|
+
others() {
|
|
32
|
+
return this.scene.getPlayers().filter(p => p !== this.actor);
|
|
33
|
+
}
|
|
34
|
+
async execute(args = []) {
|
|
35
|
+
if (!this.scene.isHumanControlled(this.actor))
|
|
36
|
+
return '';
|
|
37
|
+
const me = this.actor;
|
|
38
|
+
const query = (args ?? []).join(' ').trim();
|
|
39
|
+
const others = this.others();
|
|
40
|
+
if (others.length === 0) {
|
|
41
|
+
return `You're running this one alone -- there's nobody to nudge.${hint(` (Seat a table with "crew hire"; "nudge" is for a shared run.)`)}`;
|
|
42
|
+
}
|
|
43
|
+
if (!query) {
|
|
44
|
+
return `Nudge who? ${others.map(p => p.name).join(', ')}. Usage: "nudge <runner>".`;
|
|
45
|
+
}
|
|
46
|
+
if (query.toLowerCase() === me.name.toLowerCase()) {
|
|
47
|
+
return `Nudging yourself moves nothing. If it's your phase, act -- or "end turn".`;
|
|
48
|
+
}
|
|
49
|
+
const picked = fuzzyPickName(query, others.map(p => p.name));
|
|
50
|
+
const target = picked ? others.find(p => p.name === picked) : undefined;
|
|
51
|
+
if (!target) {
|
|
52
|
+
return `No runner on this job answers to "${query}". Out here with you: ${others.map(p => p.name).join(', ')}. (An NPC acts by rule on its own phase -- nothing to nudge there.)`;
|
|
53
|
+
}
|
|
54
|
+
if (this.scene.getKnockout?.(target.name)) {
|
|
55
|
+
return `${target.name} is out cold -- no nudge reaches them.`;
|
|
56
|
+
}
|
|
57
|
+
const enc = this.scene.encounterFor?.(target);
|
|
58
|
+
const count = enc?.noteNudge(target);
|
|
59
|
+
if (count === undefined) {
|
|
60
|
+
// A TAP ON THE SHOULDER, seen by them alone -- the same per-human
|
|
61
|
+
// scope beginPhase uses for the cyan "your Action Phase" line.
|
|
62
|
+
this.logger.log(`{yellow-fg}${me.name} nudges you.{/yellow-fg}`, { actor: target.name });
|
|
63
|
+
this.logger.write(`${me.name} nudged ${target.name} (no phase to count against).`);
|
|
64
|
+
const holder = enc?.phaseActor;
|
|
65
|
+
const aside = enc && holder && holder !== target
|
|
66
|
+
? ` It isn't ${target.name}'s Action Phase -- ${holder === me ? `it's yours` : `${holder.name} is holding it`}.`
|
|
67
|
+
: '';
|
|
68
|
+
return `You nudge ${target.name}.${aside}`;
|
|
69
|
+
}
|
|
70
|
+
const room = target.currentLocation;
|
|
71
|
+
if (count >= NUDGES_TO_SKIP) {
|
|
72
|
+
// THE SKIP. Logged, not returned: the phases that follow announce
|
|
73
|
+
// themselves while this command is still running (end-turn.ts has
|
|
74
|
+
// the same note), and a return value would land under them.
|
|
75
|
+
this.logger.log(`{yellow-fg}${target.name}'s Action Phase is skipped -- nudged ${NUDGES_TO_SKIP} times; whatever they had left is forfeit.{/yellow-fg}`, { room: room.name });
|
|
76
|
+
this.logger.write(`${me.name}'s nudge #${count} skips ${target.name}'s Action Phase.`);
|
|
77
|
+
await enc.endPhase(target);
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
this.logger.log(`{yellow-fg}${me.name} nudges you -- the table is waiting on your Action Phase (${count}/${NUDGES_TO_SKIP}; ${NUDGES_TO_SKIP} and it is skipped). "end turn" when you're done.{/yellow-fg}`, { actor: target.name });
|
|
81
|
+
this.logger.log(`${me.name} nudges ${target.name} (${count}/${NUDGES_TO_SKIP}).`, { room: room.name });
|
|
82
|
+
this.logger.write(`${me.name} nudged ${target.name} (${count}/${NUDGES_TO_SKIP}).`);
|
|
83
|
+
// The room line above already reached the nudger when they share the
|
|
84
|
+
// room; from elsewhere they get the summary instead.
|
|
85
|
+
return me.currentLocation === room ? '' : `You nudge ${target.name} (${count}/${NUDGES_TO_SKIP}).`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=nudge.js.map
|
|
@@ -384,5 +384,9 @@
|
|
|
384
384
|
// arrival. During a live encounter the ephemeral governor, open-floor
|
|
385
385
|
// re-seating and doorway yielding are frozen: nobody moves off the
|
|
386
386
|
// player's keystrokes. bluff/persuade wins clear the hostile flag.
|
|
387
|
-
|
|
387
|
+
// 1.43.0 (2026-09-07): NUDGE. New verb in shared scenes: "nudge <runner>"
|
|
388
|
+
// tells a human holding the Action Phase that the table is waiting; the
|
|
389
|
+
// third nudge on one phase ends it for them (forfeit, as "end turn" with
|
|
390
|
+
// nothing spent). Off the phase it is a private tap on the shoulder.
|
|
391
|
+
export const ENGINE_VERSION = '1.43.0';
|
|
388
392
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -80,6 +80,7 @@ import { InitiativeCommand } from './commands/initiative.js';
|
|
|
80
80
|
import { AimCommand } from './commands/aim.js';
|
|
81
81
|
import { DefendCommand } from './commands/defend.js';
|
|
82
82
|
import { DelayCommand } from './commands/delay.js';
|
|
83
|
+
import { NudgeCommand } from './commands/nudge.js';
|
|
83
84
|
import { SpellsCommand } from './commands/spells.js';
|
|
84
85
|
import { InventoryCommand } from './commands/inv.js';
|
|
85
86
|
import { EquipmentCommand } from './commands/equipment.js';
|
|
@@ -2341,7 +2342,17 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2341
2342
|
static HOME_DIRECTION_PRIORITY = [
|
|
2342
2343
|
Direction.DOWN, Direction.UP, Direction.SOUTH, Direction.NORTH, Direction.EAST, Direction.WEST,
|
|
2343
2344
|
];
|
|
2344
|
-
|
|
2345
|
+
/**
|
|
2346
|
+
* IN-PROCESS CONDITION REPORTING (2026-09-07): a site-hosted headless
|
|
2347
|
+
* session hands this in so `onConditionChanged` below can skip HubLink
|
|
2348
|
+
* entirely -- see condition-report.ts's `ConditionSink` doc comment for
|
|
2349
|
+
* why this lives on the instance rather than as module state. Absent
|
|
2350
|
+
* for the interactive CLI, which keeps today's HubLink/DDP behavior
|
|
2351
|
+
* unchanged.
|
|
2352
|
+
*/
|
|
2353
|
+
_conditionSink;
|
|
2354
|
+
constructor(player, sceneSeed, conditionSink) {
|
|
2355
|
+
this._conditionSink = conditionSink;
|
|
2345
2356
|
this.player = player;
|
|
2346
2357
|
this.sceneSeed = sceneSeed;
|
|
2347
2358
|
this.initialized = false;
|
|
@@ -2356,11 +2367,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2356
2367
|
return;
|
|
2357
2368
|
// Checked HERE too, before the import -- a dynamic import after a
|
|
2358
2369
|
// Jest file's teardown is itself the failure, so the gate cannot
|
|
2359
|
-
// live only on the far side of one.
|
|
2360
|
-
|
|
2370
|
+
// live only on the far side of one. Irrelevant when a conditionSink
|
|
2371
|
+
// is wired (no DDP hop for that path to silence).
|
|
2372
|
+
if (!this._conditionSink && process.env.MAKA_NO_DDP === '1')
|
|
2361
2373
|
return;
|
|
2362
2374
|
void import('./utilities/condition-report.js')
|
|
2363
|
-
.then(m => m.reportConditionSoon(this.player))
|
|
2375
|
+
.then(m => m.reportConditionSoon(this.player, this._conditionSink))
|
|
2364
2376
|
.catch(() => undefined);
|
|
2365
2377
|
};
|
|
2366
2378
|
// A ROOM CHANGE IS A SAVE BEAT (player request 2026-09-02): where you
|
|
@@ -2542,6 +2554,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2542
2554
|
CommandFactory.registerCommand('aim', AimCommand);
|
|
2543
2555
|
CommandFactory.registerCommand('defend', DefendCommand);
|
|
2544
2556
|
CommandFactory.registerCommand('delay', DelayCommand);
|
|
2557
|
+
CommandFactory.registerCommand('nudge', NudgeCommand);
|
|
2545
2558
|
// 'train' used to alias advance; it belongs to crew instruction now
|
|
2546
2559
|
// (commands/crew.ts) -- self-training is "advance".
|
|
2547
2560
|
CommandFactory.registerCommand('advance', AdvanceCommand);
|
|
@@ -6029,7 +6042,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6029
6042
|
static HELP_CATEGORIES = [
|
|
6030
6043
|
{ title: 'moving', entries: [['look'], ['go'], ['move', 'walk', 'approach'], ['run'], ['sprint'], ['climb', 'mantle', 'scale', 'clamber'], ['descend'], ['sit', 'kneel'], ['lie', 'prone'], ['stand'], ['follow'], ['unfollow'], ['map', 'exits'], ['search'], ['sneak']] },
|
|
6031
6044
|
{ title: 'gear', entries: [['inv', 'inventory'], ['equipment', 'eq'], ['take'], ['loot'], ['drop'], ['give'], ['store'], ['open'], ['close'], ['put'], ['equip'], ['unequip'], ['fit'], ['unfit'], ['brandish', 'draw'], ['holster'], ['reload'], ['use'], ['read']] },
|
|
6032
|
-
{ title: 'combat', entries: [['attack'], ['kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['stance'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
|
|
6045
|
+
{ title: 'combat', entries: [['attack'], ['kill'], ['subdue'], ['grapple', 'restrain', 'clinch'], ['struggle'], ['release'], ['cover'], ['aim'], ['defend'], ['delay'], ['end-turn', 'endturn', 'done', 'pass'], ['initiative', 'tracker', 'turn'], ['nudge'], ['stance'], ['surrender'], ['edge'], ['rest'], ['heal', 'firstaid', 'bandage', 'patch']] },
|
|
6033
6046
|
// The party layer: everyone who walks (or flies, or manifests) at
|
|
6034
6047
|
// your side answers to these.
|
|
6035
6048
|
// "players" folded back under crew after playtesting ("crew
|
|
@@ -18,6 +18,7 @@ import { currentSession, runInSession } from './utilities/session-context.js';
|
|
|
18
18
|
import { restorePlayer, serializePlayer } from './utilities/persistence.js';
|
|
19
19
|
import { renderMapViewport, mapCaption } from './utilities/map-view.js';
|
|
20
20
|
import { renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
|
|
21
|
+
import { serializeRoomGrid } from './utilities/room-grid.js';
|
|
21
22
|
import { entrySpotFor, spotOf } from './utilities/spots.js';
|
|
22
23
|
import { equipIntoEmptySlots } from './archetypes.js';
|
|
23
24
|
/**
|
|
@@ -191,7 +192,7 @@ export async function createHeadlessSession(opts) {
|
|
|
191
192
|
const ctx = { sessionId: opts.sessionId, logger };
|
|
192
193
|
return runInSession(ctx, async () => {
|
|
193
194
|
const primary = buildPlayerFromSave(opts.players[0]);
|
|
194
|
-
const game = new Game(primary, opts.seed);
|
|
195
|
+
const game = new Game(primary, opts.seed, opts.conditionSink);
|
|
195
196
|
// The run's tier, stated rather than defaulted -- see
|
|
196
197
|
// HeadlessOptions.tier and Game.tier.
|
|
197
198
|
if (typeof opts.tier === 'number')
|
|
@@ -325,7 +326,21 @@ export async function createHeadlessSession(opts) {
|
|
|
325
326
|
if (roomLines.length > 0) {
|
|
326
327
|
roomLines.push(mapCaption(roomCaption(here, p)));
|
|
327
328
|
}
|
|
328
|
-
|
|
329
|
+
// STRUCTURED ROOM GEOMETRY, ADDITIVE (2026-09-07): the ASCII
|
|
330
|
+
// above stays the log-adjacent text every consumer already
|
|
331
|
+
// reads; a browser client wanting the site's real SVG DotMap
|
|
332
|
+
// instead reads `data.grid` off this SAME event -- no new
|
|
333
|
+
// event kind, no publication change (GameEvent.data already
|
|
334
|
+
// round-trips end to end). ensureGrid() always synthesizes now
|
|
335
|
+
// ("EVERY ROOM IS MATRIXED", room.ts:357-370, the 2026-08-25
|
|
336
|
+
// ruling that retired the spotless-room compat gap) -- the
|
|
337
|
+
// `undefined` in its return type is a vestige, kept guarded
|
|
338
|
+
// here anyway rather than asserted, since a decoration must
|
|
339
|
+
// never take down the room push (same reasoning as the
|
|
340
|
+
// try/catch this sits inside).
|
|
341
|
+
const grid = here.ensureGrid();
|
|
342
|
+
const data = grid ? { grid: serializeRoomGrid(grid, here.name) } : undefined;
|
|
343
|
+
logger.system(roomLines.join('\n'), { actor: p.name }, 'room', data);
|
|
329
344
|
}
|
|
330
345
|
catch (err) {
|
|
331
346
|
logger.write(`Room overlay render failed for ${p.name} in ${here.name}: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
|
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
import { rollInitiativeScore, formatInitiative, INITIATIVE_PASS_DROP, rollPool, formatRoll } from './dice.js';
|
|
2
2
|
import { ActionBudget } from './action-budget.js';
|
|
3
3
|
import { runNpcActionPhase } from './npc-combat-brain.js';
|
|
4
|
+
/**
|
|
5
|
+
* THE COMBAT TURN (SR5 p.158-161, RAG-checked 2026-09-06).
|
|
6
|
+
*
|
|
7
|
+
* This engine used to resolve a fight as EXCHANGES: one typed "attack"
|
|
8
|
+
* rolled a two-actor initiative, both sides struck, and a >10 gap bought
|
|
9
|
+
* one extra strike. That was the closest thing it had to the book's
|
|
10
|
+
* turn, and combat-exchange.ts said so. What it could never express was
|
|
11
|
+
* the thing the book is built on -- a sequence of Action Phases in
|
|
12
|
+
* which every participant spends a budget of actions, in an order the
|
|
13
|
+
* dice decided, until nobody has Initiative Score left.
|
|
14
|
+
*
|
|
15
|
+
* The sequence, as the book gives it:
|
|
16
|
+
* 1. ROLL INITIATIVE: Initiative attribute + Initiative Dice, summed
|
|
17
|
+
* (p.159). Highest acts first; ties break Edge, Reaction,
|
|
18
|
+
* Intuition, coin (p.159 "ERIC").
|
|
19
|
+
* 2-4. INITIATIVE PASS: each participant, highest score first, takes
|
|
20
|
+
* an ACTION PHASE -- two Simple Actions or one Complex, plus one
|
|
21
|
+
* Free (p.158). When everyone has acted, EVERY score drops by 10
|
|
22
|
+
* (p.159), and anyone still above zero goes again.
|
|
23
|
+
* 5. When nobody is above zero the Combat Turn ends and a new one
|
|
24
|
+
* begins at step 1 (p.159).
|
|
25
|
+
*
|
|
26
|
+
* Also carried here because the score lives here:
|
|
27
|
+
* - INTERRUPT ACTIONS (p.167-168) spend Initiative Score: Full
|
|
28
|
+
* Defense -10 for the Combat Turn, Block/Dodge/Parry -5 each for
|
|
29
|
+
* one test. Only affordable "if he has enough Initiative Score
|
|
30
|
+
* left" -- a score already at or below zero buys nothing (the p.191
|
|
31
|
+
* worked example).
|
|
32
|
+
* - WOUND MODIFIERS CHANGE THE SCORE AS THEY LAND (p.160 Changing
|
|
33
|
+
* Initiative): the difference is applied immediately, and it can
|
|
34
|
+
* reorder the pass. It never grants an extra action.
|
|
35
|
+
* - ENTERING LATE (p.160): roll as normal, then subtract 10 for every
|
|
36
|
+
* pass already gone.
|
|
37
|
+
* - DELAYING (p.161): an actor may hold their Action Phase and take
|
|
38
|
+
* it later in the same pass. Modelled as "act after everyone else
|
|
39
|
+
* this pass, keeping your score"; the pass-end -10 still applies.
|
|
40
|
+
* - SURPRISE (p.192): a fight opened on the unaware makes every other
|
|
41
|
+
* participant roll Reaction + Intuition (3); failure is -10 and no
|
|
42
|
+
* defense against, nor action against, the surprising side until
|
|
43
|
+
* their next Action Phase. +3 for anyone already alert.
|
|
44
|
+
* - MOVEMENT is a WHOLE-TURN total (p.161-162, Player.movedMetersThisTurn)
|
|
45
|
+
* and comes back at step 5, not per pass.
|
|
46
|
+
*
|
|
47
|
+
* SCOPE: one encounter per ROOM, on the physical planes (meat and a
|
|
48
|
+
* jumped-in drone). The Matrix keeps its own initiative structure
|
|
49
|
+
* (utilities/ic.ts maps IC to Combat Turns per player action) and
|
|
50
|
+
* astral combat still rides the exchange engine -- both are noted as
|
|
51
|
+
* gaps rather than folded in half-right.
|
|
52
|
+
*
|
|
53
|
+
* WHO DRIVES IT: a human's Action Phase waits for them to type actions
|
|
54
|
+
* and then "end turn" (commands/end-turn.ts). An NPC's phase is played
|
|
55
|
+
* by a deterministic brain (utilities/npc-combat-brain.ts) -- the LLM
|
|
56
|
+
* reflect chain is suspended for participants while the encounter runs,
|
|
57
|
+
* because a model answering "what next?" in a second or two cannot be
|
|
58
|
+
* sequenced against a dice-ordered pass, and was the reason NPCs used
|
|
59
|
+
* to shoot from between the beats of the player's own exchange.
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* Nudges on one Action Phase before the table ends it for the holder
|
|
63
|
+
* (commands/nudge.ts, owner ruling 2026-09-07). A ruling, not canon --
|
|
64
|
+
* the book has no idle player -- kept as a count rather than a clock so
|
|
65
|
+
* that a runner reading the rules is never skipped by a timer, only by
|
|
66
|
+
* three people saying so.
|
|
67
|
+
*/
|
|
68
|
+
export const NUDGES_TO_SKIP = 3;
|
|
4
69
|
export class CombatEncounter {
|
|
5
70
|
scene;
|
|
6
71
|
logger;
|
|
@@ -317,6 +382,7 @@ export class CombatEncounter {
|
|
|
317
382
|
this.budget = new ActionBudget();
|
|
318
383
|
p.surprised = false;
|
|
319
384
|
p.delayed = false;
|
|
385
|
+
p.nudges = 0;
|
|
320
386
|
p.actor.clearAim();
|
|
321
387
|
// Recoil resets the moment an Action Phase passes without firing --
|
|
322
388
|
// tracked by the phase, settled by attack.ts (p.175-176).
|
|
@@ -342,6 +408,19 @@ export class CombatEncounter {
|
|
|
342
408
|
const walkLeft = Math.max(0, actor.walkRateMeters - actor.movedMetersThisTurn);
|
|
343
409
|
return `${left} m of movement left this turn (${walkLeft} m at a walk)`;
|
|
344
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Another human nudged the phase holder (commands/nudge.ts): the
|
|
413
|
+
* running count for this phase, or undefined when `target` is not
|
|
414
|
+
* the one holding it. Bookkeeping only -- the verb owns the wording
|
|
415
|
+
* and calls endPhase() itself at NUDGES_TO_SKIP.
|
|
416
|
+
*/
|
|
417
|
+
noteNudge(target) {
|
|
418
|
+
const p = this.participantOf(target);
|
|
419
|
+
if (!p || this.ended || this.phaseActor !== target)
|
|
420
|
+
return undefined;
|
|
421
|
+
p.nudges = (p.nudges ?? 0) + 1;
|
|
422
|
+
return p.nudges;
|
|
423
|
+
}
|
|
345
424
|
/** The human typed "end turn" (or lost the phase -- fled, dropped). */
|
|
346
425
|
async endPhase(actor) {
|
|
347
426
|
const p = this.participantOf(actor);
|
|
@@ -18,7 +18,24 @@ import { hostLabel } from './grid-names.js';
|
|
|
18
18
|
* storm into a single report of where the monitors LANDED.
|
|
19
19
|
*/
|
|
20
20
|
const COALESCE_MS = 250;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* KEYED PER PLAYER, NOT A SINGLE SLOT (fixed 2026-09-07 alongside the
|
|
23
|
+
* ConditionSink work above). A lone real terminal process only ever
|
|
24
|
+
* calls this for its own primary, so a single `pending` variable never
|
|
25
|
+
* showed a problem: every arm captured the SAME player object, and
|
|
26
|
+
* reading its state at fire time was always correct regardless of which
|
|
27
|
+
* call armed the timer. That stops being true the moment more than one
|
|
28
|
+
* `Game` can be reporting condition in the same process -- exactly what
|
|
29
|
+
* ConditionSink exists for. With one shared slot, a second session's
|
|
30
|
+
* mutation arriving while the first session's beat is still gathering
|
|
31
|
+
* hit `if (pending) return` and vanished silently: not delayed, not
|
|
32
|
+
* coalesced into the wrong report -- simply never sent, for that
|
|
33
|
+
* session, for that beat. Keying the map on the Player instance gives
|
|
34
|
+
* each session (each primary Player is a distinct object) its own
|
|
35
|
+
* independent 250ms coalescing window, with no change to the
|
|
36
|
+
* single-session behavior this always had.
|
|
37
|
+
*/
|
|
38
|
+
const pendingByPlayer = new WeakMap();
|
|
22
39
|
/** The method name is the server's -- co-owned; do not rename alone. */
|
|
23
40
|
export const REPORT_METHOD = 'characterSheet.reportCondition';
|
|
24
41
|
/**
|
|
@@ -215,21 +232,21 @@ function noteRefusal(why) {
|
|
|
215
232
|
catch { /* no session */ }
|
|
216
233
|
}
|
|
217
234
|
}
|
|
218
|
-
export function reportConditionSoon(player) {
|
|
235
|
+
export function reportConditionSoon(player, sink) {
|
|
219
236
|
// Before the TIMER, not just before the call: a scheduled beat firing
|
|
220
237
|
// after a Jest file finished lazy-imported into a torn-down
|
|
221
238
|
// environment and failed CI with every test green (2026-09-01). Under
|
|
222
239
|
// MAKA_NO_DDP nothing is armed at all -- the same switch the whole
|
|
223
|
-
// live layer honors.
|
|
224
|
-
|
|
240
|
+
// live layer honors. Irrelevant to a caller-supplied sink: there is no
|
|
241
|
+
// DDP hop to silence when reporting stays in-process.
|
|
242
|
+
if (!sink && process.env.MAKA_NO_DDP === '1')
|
|
225
243
|
return;
|
|
226
|
-
if (
|
|
227
|
-
return; // a beat is already gathering
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
if (pendingByPlayer.has(player))
|
|
245
|
+
return; // a beat is already gathering for THIS player
|
|
246
|
+
const timer = setTimeout(() => {
|
|
247
|
+
pendingByPlayer.delete(player);
|
|
230
248
|
void (async () => {
|
|
231
|
-
const [{
|
|
232
|
-
import('./shared-run.js'),
|
|
249
|
+
const [{ saveSlug }, { arSightUp }] = await Promise.all([
|
|
233
250
|
import('./persistence.js'),
|
|
234
251
|
import('./ar.js'),
|
|
235
252
|
]);
|
|
@@ -244,6 +261,14 @@ export function reportConditionSoon(player) {
|
|
|
244
261
|
// beat fires next. All read at FIRE time, like position.
|
|
245
262
|
const report = buildConditionReport(player, { saveSlug, arSightUp });
|
|
246
263
|
noteRoster(report);
|
|
264
|
+
if (sink) {
|
|
265
|
+
// IN-PROCESS: no HubLink, no MAKA_NO_DDP gate, no version-skew
|
|
266
|
+
// retry -- the engine module and its caller are always the same
|
|
267
|
+
// deployed version here (see ConditionSink's doc comment).
|
|
268
|
+
await sink(report);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const { HubLink } = await import('./shared-run.js');
|
|
247
272
|
try {
|
|
248
273
|
await HubLink.call(REPORT_METHOD, report);
|
|
249
274
|
}
|
|
@@ -270,10 +295,11 @@ export function reportConditionSoon(player) {
|
|
|
270
295
|
noteRefusal(err instanceof Error ? err.message : String(err));
|
|
271
296
|
});
|
|
272
297
|
}, COALESCE_MS);
|
|
273
|
-
|
|
298
|
+
pendingByPlayer.set(player, timer);
|
|
299
|
+
timer.unref?.();
|
|
274
300
|
}
|
|
275
|
-
/** Test seam: is a beat currently gathering? */
|
|
276
|
-
export function conditionReportPending() {
|
|
277
|
-
return
|
|
301
|
+
/** Test seam: is a beat currently gathering for this player? */
|
|
302
|
+
export function conditionReportPending(player) {
|
|
303
|
+
return pendingByPlayer.has(player);
|
|
278
304
|
}
|
|
279
305
|
//# sourceMappingURL=condition-report.js.map
|
|
@@ -800,6 +800,37 @@ export function synthesizeGrid(input) {
|
|
|
800
800
|
// the module doc); Room.ensureGrid queries this cell by that same
|
|
801
801
|
// string. A rename on either side must update both.
|
|
802
802
|
export const OPEN_FLOOR_NAME = 'open floor';
|
|
803
|
+
/** Flattens a live IRoomGrid's Maps into ISerializedRoomGrid. Spots ship
|
|
804
|
+
* UNFILTERED (the caller resolves actor positions against the full
|
|
805
|
+
* list, open floor included -- see room-view.ts's seatingIn); only the
|
|
806
|
+
* DRAWN footprints skip the open-floor anchor, matching the site's own
|
|
807
|
+
* filter. */
|
|
808
|
+
export function serializeRoomGrid(grid, roomName) {
|
|
809
|
+
const spots = [...grid.spotCells.entries()].map(([name, c]) => ({
|
|
810
|
+
name, x: c.x, y: c.y, z: c.z,
|
|
811
|
+
}));
|
|
812
|
+
const doors = [...grid.exitCells.entries()].map(([dir, c]) => ({
|
|
813
|
+
dir: String(dir), x: c.x, y: c.y, z: c.z,
|
|
814
|
+
}));
|
|
815
|
+
const footprints = [...grid.spotFootprints.entries()]
|
|
816
|
+
.filter(([name]) => name !== OPEN_FLOOR_NAME)
|
|
817
|
+
.map(([name, cells]) => ({
|
|
818
|
+
name,
|
|
819
|
+
cells: cells.map(c => ({ x: c.x, y: c.y, z: c.z })),
|
|
820
|
+
}));
|
|
821
|
+
const vertical = [...grid.vertical.entries()].map(([cellKey, link]) => {
|
|
822
|
+
const [x, y, z] = cellKey.split(',').map(Number);
|
|
823
|
+
return { kind: link.kind, x, y, z };
|
|
824
|
+
});
|
|
825
|
+
return {
|
|
826
|
+
name: roomName,
|
|
827
|
+
dims: { x: grid.dims.x, y: grid.dims.y, z: grid.dims.z },
|
|
828
|
+
spots,
|
|
829
|
+
doors,
|
|
830
|
+
footprints,
|
|
831
|
+
vertical,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
803
834
|
function clamp(n, lo, hi) { return Math.max(lo, Math.min(hi, n)); }
|
|
804
835
|
function jitter(rng) { return Math.floor(rng() * 3) - 1; }
|
|
805
836
|
function chebyshev(a, b) { return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y)); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.139.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.",
|