@maka/maka-cli 5.136.0 → 5.138.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/bluff.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +9 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +40 -5
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +17 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +29 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +40 -14
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +20 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hostile-contact.js +104 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-grid.js +31 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js +16 -7
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.138.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
|
|
6
6
|
"description": "A command line tool for scaffolding Meteor 3.x applications using React.",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { standDown } from '../utilities/hostile-contact.js';
|
|
1
2
|
import { Command } from './command.js';
|
|
2
3
|
import { hint } from '../utilities/hints.js';
|
|
3
4
|
import { sameSpot, spotOf, sealedRouteTo } from '../utilities/spots.js';
|
|
@@ -152,7 +153,11 @@ export class BluffCommand extends Command {
|
|
|
152
153
|
return `${who.name} lets you finish, then doesn't move. "Nice try."`;
|
|
153
154
|
}
|
|
154
155
|
// They stand down -- for everyone, deliberately (see Player.guarding).
|
|
156
|
+
// Opposition too: "cleared the same way guarding is -- talked round"
|
|
157
|
+
// (Player.hostile), which nothing did until 2026-09-07.
|
|
155
158
|
who.guarding = false;
|
|
159
|
+
if (who.hostile)
|
|
160
|
+
standDown(who);
|
|
156
161
|
void Promise.resolve(who.hear(this.actor, `[System note: ${this.actor.name} just talked you round and you're STEPPING ASIDE -- you bought the story. One short line as you move, in your own voice. Don't re-block the way.]`)).catch(() => undefined);
|
|
157
162
|
return [
|
|
158
163
|
`${who.name} weighs it, decides you're somebody's problem but not theirs, and steps out of the way.`,
|
|
@@ -629,7 +629,10 @@ export class MoveCommand extends Command {
|
|
|
629
629
|
// "Away" is wherever the room's empty ground currently IS -- re-seat
|
|
630
630
|
// it on the loneliest reachable cell before measuring the crossing
|
|
631
631
|
// or committing the move (see spots.ts reseatOpenFloor).
|
|
632
|
-
|
|
632
|
+
// ...except mid-fight: the anchor re-derivation moves every spotless
|
|
633
|
+
// body in the room, and during a Combat Turn nothing but the phase
|
|
634
|
+
// actor moves (utilities/ephemeral.ts has the ruling).
|
|
635
|
+
if (spot.name === OPEN_FLOOR && !this.scene.encounterIn?.(room))
|
|
633
636
|
reseatOpenFloor(room, actor);
|
|
634
637
|
// The route a body actually walks -- around the furniture and
|
|
635
638
|
// around other people (spots.ts routeForMove). A crowded floor
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { standDown } from '../utilities/hostile-contact.js';
|
|
1
2
|
import { Command } from './command.js';
|
|
2
3
|
import { NPC } from '../models/npc.js';
|
|
3
4
|
import { rollPool, formatRoll } from '../utilities/dice.js';
|
|
@@ -93,6 +94,10 @@ export class PersuadeCommand extends Command {
|
|
|
93
94
|
// win). A strong win IS that pressure, by name.
|
|
94
95
|
note = `[System note: ${actor.name}'s pitch genuinely LANDED -- you now actively want to help them this job: share what you know freely, drop any demands to a token gesture, and treat them as a friend. If you guard a secret, a pass, or a way through, you have just been MASTERFULLY PLAYED -- divulging it now is exactly in character.]`;
|
|
95
96
|
verdict = `Something shifts -- ${target.name} is with you now.`;
|
|
97
|
+
// A strong win talks opposition round (Player.hostile: "cleared the
|
|
98
|
+
// same way guarding is"): the ladder in hostile-contact.ts stops.
|
|
99
|
+
if (target.hostile)
|
|
100
|
+
standDown(target);
|
|
96
101
|
}
|
|
97
102
|
else if (net >= 1) {
|
|
98
103
|
note = `[System note: ${actor.name} is persuasive -- you're inclined to cooperate: be more forthcoming and reasonable with them than you'd otherwise be. If you guard a secret or a way through, you're wavering -- a little more pressure or goodwill would tip you.]`;
|
|
@@ -376,5 +376,13 @@
|
|
|
376
376
|
// the damage type your next blow means (p.186 flat of the blade, stun
|
|
377
377
|
// rounds on reload). SAVE SHAPE: autoStance is lethal | non-lethal;
|
|
378
378
|
// "manual" loads as lethal.
|
|
379
|
-
|
|
379
|
+
// 1.42.0 (2026-09-07): HOSTILES OPEN COMBAT, AND NOTHING BUT THE PHASE ACTOR
|
|
380
|
+
// MOVES. Shared-scene semantics: a hostile NPC (gang mob or seeded) opens
|
|
381
|
+
// the Combat Turn itself -- on contact, on a witnessed draw/cast/summon,
|
|
382
|
+
// or when its patience runs out (utilities/hostile-contact.ts; SR5
|
|
383
|
+
// p.34-35, p.49, p.158) -- and a seeded hostile is no longer muted on
|
|
384
|
+
// arrival. During a live encounter the ephemeral governor, open-floor
|
|
385
|
+
// re-seating and doorway yielding are frozen: nobody moves off the
|
|
386
|
+
// player's keystrokes. bluff/persuade wins clear the hostile flag.
|
|
387
|
+
export const ENGINE_VERSION = '1.42.0';
|
|
380
388
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -31,6 +31,7 @@ import { attachBrandMark, attachBrandMarkBottomRight } from './utilities/brand-m
|
|
|
31
31
|
import { logoPanelLines } from './utilities/logo-mark.js';
|
|
32
32
|
import { bestFakeSin, wandRead, processArrest } from './utilities/sin.js';
|
|
33
33
|
import { tickEphemerals } from './utilities/ephemeral.js';
|
|
34
|
+
import { hostileContactBeat } from './utilities/hostile-contact.js';
|
|
34
35
|
import { requestAlarmBeat, newAlarmBeatLatch, ALARM_STIMULUS } from './utilities/alarmed-staff.js';
|
|
35
36
|
import { renderMapViewport, mapCaption } from './utilities/map-view.js';
|
|
36
37
|
import { tallestRoomRows } from './utilities/room-grid.js';
|
|
@@ -2340,7 +2341,17 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2340
2341
|
static HOME_DIRECTION_PRIORITY = [
|
|
2341
2342
|
Direction.DOWN, Direction.UP, Direction.SOUTH, Direction.NORTH, Direction.EAST, Direction.WEST,
|
|
2342
2343
|
];
|
|
2343
|
-
|
|
2344
|
+
/**
|
|
2345
|
+
* IN-PROCESS CONDITION REPORTING (2026-09-07): a site-hosted headless
|
|
2346
|
+
* session hands this in so `onConditionChanged` below can skip HubLink
|
|
2347
|
+
* entirely -- see condition-report.ts's `ConditionSink` doc comment for
|
|
2348
|
+
* why this lives on the instance rather than as module state. Absent
|
|
2349
|
+
* for the interactive CLI, which keeps today's HubLink/DDP behavior
|
|
2350
|
+
* unchanged.
|
|
2351
|
+
*/
|
|
2352
|
+
_conditionSink;
|
|
2353
|
+
constructor(player, sceneSeed, conditionSink) {
|
|
2354
|
+
this._conditionSink = conditionSink;
|
|
2344
2355
|
this.player = player;
|
|
2345
2356
|
this.sceneSeed = sceneSeed;
|
|
2346
2357
|
this.initialized = false;
|
|
@@ -2355,11 +2366,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2355
2366
|
return;
|
|
2356
2367
|
// Checked HERE too, before the import -- a dynamic import after a
|
|
2357
2368
|
// Jest file's teardown is itself the failure, so the gate cannot
|
|
2358
|
-
// live only on the far side of one.
|
|
2359
|
-
|
|
2369
|
+
// live only on the far side of one. Irrelevant when a conditionSink
|
|
2370
|
+
// is wired (no DDP hop for that path to silence).
|
|
2371
|
+
if (!this._conditionSink && process.env.MAKA_NO_DDP === '1')
|
|
2360
2372
|
return;
|
|
2361
2373
|
void import('./utilities/condition-report.js')
|
|
2362
|
-
.then(m => m.reportConditionSoon(this.player))
|
|
2374
|
+
.then(m => m.reportConditionSoon(this.player, this._conditionSink))
|
|
2363
2375
|
.catch(() => undefined);
|
|
2364
2376
|
};
|
|
2365
2377
|
// A ROOM CHANGE IS A SAVE BEAT (player request 2026-09-02): where you
|
|
@@ -5805,8 +5817,14 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5805
5817
|
};
|
|
5806
5818
|
const command = CommandFactory.createCommand(verb, context);
|
|
5807
5819
|
if (command) {
|
|
5820
|
+
// THE BEAT CLOCK (Scene.beat) ticks BEFORE the command, so a body
|
|
5821
|
+
// that spawns or a guard that registers you during it carries this
|
|
5822
|
+
// beat's number, and the contact check after it can grant them the
|
|
5823
|
+
// one-beat grace the ephemeral governor already gives a mob.
|
|
5824
|
+
if (this.scene.isHumanControlled(actor))
|
|
5825
|
+
this.scene.beat += 1;
|
|
5808
5826
|
const result = await command.execute(args);
|
|
5809
|
-
const wrapped = this.withEphemeralTurn(actor, this.withAlarmTurn(actor, this.withICTurn(actor, result)));
|
|
5827
|
+
const wrapped = await this.withHostileContact(actor, this.withEphemeralTurn(actor, this.withAlarmTurn(actor, this.withICTurn(actor, result))));
|
|
5810
5828
|
// The IC turn above may have filled a track -- settle it now, not
|
|
5811
5829
|
// on the next keystroke.
|
|
5812
5830
|
const settled = await settleUnresolvedHarm(this.scene, actor);
|
|
@@ -5923,6 +5941,23 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5923
5941
|
const lines = tickEphemerals(this, actor);
|
|
5924
5942
|
return lines.length === 0 ? result : `${result}\n\n${lines.join('\n')}`;
|
|
5925
5943
|
}
|
|
5944
|
+
/**
|
|
5945
|
+
* HOSTILES OPEN COMBAT (utilities/hostile-contact.ts): after every
|
|
5946
|
+
* human command, every hostile in that human's room gets its beat --
|
|
5947
|
+
* contact, a provocation they witnessed, or patience run out opens
|
|
5948
|
+
* the Combat Turn with THEM as aggressor. Runs AFTER the ephemeral
|
|
5949
|
+
* tick so the step a mob just took toward you counts this beat. Any
|
|
5950
|
+
* human, not only the primary: a second runner on the table can be
|
|
5951
|
+
* the one a ganger is standing over.
|
|
5952
|
+
*/
|
|
5953
|
+
async withHostileContact(actor, result) {
|
|
5954
|
+
if (result === null)
|
|
5955
|
+
return null;
|
|
5956
|
+
if (!this.scene.isHumanControlled(actor))
|
|
5957
|
+
return result;
|
|
5958
|
+
const lines = await hostileContactBeat(this.scene, actor, this.scene.beat);
|
|
5959
|
+
return lines.length === 0 ? result : `${result}\n\n${lines.join('\n')}`;
|
|
5960
|
+
}
|
|
5926
5961
|
withICTurn(actor, result) {
|
|
5927
5962
|
if (result === null)
|
|
5928
5963
|
return null;
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { registerNotice, registerProvocation, PROVOCATION } from '../utilities/hostile-contact.js';
|
|
1
2
|
import { capitalCase } from 'change-case';
|
|
2
3
|
import { tableCrewNote, tableDeliveryNote } from '../utilities/table-notes.js';
|
|
3
4
|
import { Player } from './player.js';
|
|
@@ -673,6 +674,15 @@ export class NPC extends Player {
|
|
|
673
674
|
* none of the three.
|
|
674
675
|
*/
|
|
675
676
|
ephemeral;
|
|
677
|
+
/**
|
|
678
|
+
* WHERE THIS HOSTILE IS ON THE LADDER that opens a fight
|
|
679
|
+
* (utilities/hostile-contact.ts, 2026-09-07): the beat they registered
|
|
680
|
+
* the player, the beat they watched a provocation, and why the fight
|
|
681
|
+
* opened once it has. Transient and never saved -- a hostile you left
|
|
682
|
+
* behind starts over when you come back. Written only by that module
|
|
683
|
+
* and by see() below (the witness); read only by that module.
|
|
684
|
+
*/
|
|
685
|
+
hostileContact;
|
|
676
686
|
/**
|
|
677
687
|
* Every human seated at this scene's table. utilities/table-notes.ts
|
|
678
688
|
* turns it into what a companion is told; in solo play it is just
|
|
@@ -1174,6 +1184,18 @@ description text.`;
|
|
|
1174
1184
|
this.logger.logWithColor(`${this.name} clocks you slipping in -- so much for quiet.`, 'yellow');
|
|
1175
1185
|
}
|
|
1176
1186
|
}
|
|
1187
|
+
// HOSTILE CONTACT (utilities/hostile-contact.ts): a hostile who
|
|
1188
|
+
// just registered a human -- or watched them draw, cast, or call
|
|
1189
|
+
// a spirit -- is on the ladder that opens the fight. Marks only;
|
|
1190
|
+
// the beat after the command decides. Past the sneak contest on
|
|
1191
|
+
// purpose: a sneaking player who won it never reaches this line.
|
|
1192
|
+
if (this.hostile && !(actor instanceof NPC)) {
|
|
1193
|
+
const beat = this._scene?.beat ?? 0;
|
|
1194
|
+
if (PROVOCATION.test(verb))
|
|
1195
|
+
registerProvocation(this, beat);
|
|
1196
|
+
else
|
|
1197
|
+
registerNotice(this, beat);
|
|
1198
|
+
}
|
|
1177
1199
|
// A jumped-in arrival is a MACHINE arriving -- the history should
|
|
1178
1200
|
// say so, or the AI greets a rotor drone like the person's face.
|
|
1179
1201
|
const form = actor.plane === 'drone'
|
|
@@ -1280,7 +1302,13 @@ description text.`;
|
|
|
1280
1302
|
blendFailed = true;
|
|
1281
1303
|
}
|
|
1282
1304
|
}
|
|
1283
|
-
|
|
1305
|
+
// A HOSTILE IS NEVER "NOT WARM ENOUGH TO HAIL" (2026-09-07): the
|
|
1306
|
+
// warmth gate is about greetings, and opposition has something
|
|
1307
|
+
// to say to a stranger on its ground -- the challenge that
|
|
1308
|
+
// canon puts before the fight (p.34-35). Muting it here was why
|
|
1309
|
+
// a seeded guard in a spotted room watched you walk in and said
|
|
1310
|
+
// nothing until you drew.
|
|
1311
|
+
if (spotsActive(this.currentLocation) && !this.hostile
|
|
1284
1312
|
&& !(rel && rel.loyalty >= 3) && !knownByRep && !arrivedAtMySpot) {
|
|
1285
1313
|
this.logger.write(`[DEBUG] ${this.name} quietly notes the arrival -- not warm enough to hail (loyalty ${rel?.loyalty ?? 'none'}).`);
|
|
1286
1314
|
return;
|
|
@@ -144,6 +144,15 @@ export class Scene extends AbstractScene {
|
|
|
144
144
|
const enc = this.encounters.get(room);
|
|
145
145
|
return enc && !enc.ended ? enc : undefined;
|
|
146
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* THE BEAT CLOCK: which human command the world is on. Game bumps it
|
|
149
|
+
* before each human command runs, so anything that happens DURING a
|
|
150
|
+
* command (a gang spawning, a guard registering you) carries that
|
|
151
|
+
* command's number, and the hostile-contact beat that runs after it
|
|
152
|
+
* can tell "noticed you this action" from "has been watching you for
|
|
153
|
+
* three". Transient; never saved.
|
|
154
|
+
*/
|
|
155
|
+
beat = 0;
|
|
147
156
|
/** The fight this actor is a participant in, if any. */
|
|
148
157
|
encounterFor(actor) {
|
|
149
158
|
const enc = this.encounterIn(actor.currentLocation);
|
|
@@ -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
|
|
@@ -173,11 +173,19 @@ function walkCells(npc) {
|
|
|
173
173
|
* room, and each kind spends it differently.
|
|
174
174
|
*
|
|
175
175
|
* WHY THIS HANGS OFF THE COMMAND DISPATCH, which is the same choice
|
|
176
|
-
* withICTurn made and for the same reason:
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
176
|
+
* withICTurn made and for the same reason: OUTSIDE A FIGHT there is no
|
|
177
|
+
* turn loop to live in, and hooking one verb would give a mob
|
|
178
|
+
* permission to close only while you happened to be walking. It closes
|
|
179
|
+
* while you look, while you talk, while you check your gear. That is
|
|
180
|
+
* what makes it opposition rather than furniture.
|
|
181
|
+
*
|
|
182
|
+
* INSIDE A FIGHT THE GOVERNOR IS SILENT (playtest 2026-09-07: "if I
|
|
183
|
+
* move, they move -- not respecting the combat turn, which is mine").
|
|
184
|
+
* Since the Combat Turn (combat-turn.ts, engine 1.40.0) a room with a
|
|
185
|
+
* live encounter IS a turn loop, every hostile in it is a participant,
|
|
186
|
+
* and a participant moves on its own Action Phase through the verbs
|
|
187
|
+
* (npc-combat-brain.ts: "move to <spot>", then the swing) and never on
|
|
188
|
+
* the player's keystrokes. Nothing but the phase actor moves.
|
|
181
189
|
*
|
|
182
190
|
* WHAT THIS IS NOT: it is NOT what made the NPC seem to follow the
|
|
183
191
|
* player. Nothing here existed when that was reported -- an ephemeral
|
|
@@ -198,10 +206,17 @@ export function tickEphemerals(host, player) {
|
|
|
198
206
|
const room = player.currentLocation;
|
|
199
207
|
if (!room)
|
|
200
208
|
return lines;
|
|
209
|
+
// A Combat Turn is running here: initiative owns every body in the
|
|
210
|
+
// room, and the beat clock (mark.ticks) stops with it -- a mob's
|
|
211
|
+
// patience does not run out while the dice are rolling.
|
|
212
|
+
if (host.scene.encounterIn?.(room))
|
|
213
|
+
return lines;
|
|
201
214
|
for (const [name, mark] of host.ephemerals) {
|
|
202
215
|
const npc = host.scene.getActor(name);
|
|
203
216
|
if (!(npc instanceof NPC) || npc.isIncapacitated())
|
|
204
217
|
continue;
|
|
218
|
+
if (host.scene.encounterFor?.(npc))
|
|
219
|
+
continue;
|
|
205
220
|
// A BODY IN ANOTHER ROOM IS NOT YOUR PROBLEM AND YOU ARE NOT ITS
|
|
206
221
|
// BUSINESS. This is the line the report was really about: a mob you
|
|
207
222
|
// left behind holds the block it was holding, and nothing here can
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { actorDistanceMeters, ADJACENT_METERS, wouldNotice } from './spots.js';
|
|
2
|
+
import { Logger } from './logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Beats after notice before a hostile stops waiting for you to leave.
|
|
5
|
+
* Three player actions is the length of a challenge and an answer;
|
|
6
|
+
* canon gives no number (the GM decides when "fighting breaks out"), so
|
|
7
|
+
* this is a documented ruling, kept short enough that "they were hostile
|
|
8
|
+
* and nothing happened" cannot recur.
|
|
9
|
+
*/
|
|
10
|
+
export const PATIENCE_BEATS = 3;
|
|
11
|
+
/** What a hostile watching you do it will not let pass (performAction verbs). */
|
|
12
|
+
export const PROVOCATION = /^(draws\b|casts\b|chants a summoning)/;
|
|
13
|
+
/** The hostiles in `room` who could open on `player` -- structural, no LLM. */
|
|
14
|
+
export function hostilesIn(scene, room, player) {
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const a of room.getActors()) {
|
|
17
|
+
if (a === player || scene.isHumanControlled(a))
|
|
18
|
+
continue;
|
|
19
|
+
const npc = a;
|
|
20
|
+
if (npc.hostile !== true || npc.allyOf || npc.surrendered)
|
|
21
|
+
continue;
|
|
22
|
+
if (npc.isIncapacitated() || !npc.sharesCombatPlane(player))
|
|
23
|
+
continue;
|
|
24
|
+
out.push(npc);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
/** A hostile has registered the player (idempotent; the first beat wins). */
|
|
29
|
+
export function registerNotice(npc, beat) {
|
|
30
|
+
if (npc.hostile !== true)
|
|
31
|
+
return;
|
|
32
|
+
npc.hostileContact ??= { noticedBeat: beat };
|
|
33
|
+
}
|
|
34
|
+
/** A hostile watched the player do something it cannot ignore. */
|
|
35
|
+
export function registerProvocation(npc, beat) {
|
|
36
|
+
if (npc.hostile !== true)
|
|
37
|
+
return;
|
|
38
|
+
registerNotice(npc, beat);
|
|
39
|
+
npc.hostileContact.provokedBeat ??= beat;
|
|
40
|
+
}
|
|
41
|
+
/** Talked round, or beaten: opposition stands down. */
|
|
42
|
+
export function standDown(npc) {
|
|
43
|
+
npc.hostile = false;
|
|
44
|
+
npc.hostileContact = undefined;
|
|
45
|
+
}
|
|
46
|
+
function onPhysicalPlane(player) {
|
|
47
|
+
return player.plane === 'meat' || player.plane === 'drone';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One beat of every hostile in the player's room. Runs after every
|
|
51
|
+
* human command (Game.withHostileContact) and once on arrival after the
|
|
52
|
+
* ephemerals have spawned (go.ts describeArrival) -- the reported case
|
|
53
|
+
* is a gang that did not exist when the arrival stimulus fanned out.
|
|
54
|
+
* Returns narration; the encounter announces its own initiative.
|
|
55
|
+
*/
|
|
56
|
+
export async function hostileContactBeat(scene, player, beat) {
|
|
57
|
+
const room = player.currentLocation;
|
|
58
|
+
if (!room || !onPhysicalPlane(player) || player.isIncapacitated())
|
|
59
|
+
return [];
|
|
60
|
+
if (scene.encounterIn?.(room))
|
|
61
|
+
return [];
|
|
62
|
+
for (const npc of hostilesIn(scene, room, player)) {
|
|
63
|
+
const c = npc.hostileContact;
|
|
64
|
+
if (!c) {
|
|
65
|
+
// A sneaking player is the arrival contest's call, not ours.
|
|
66
|
+
if (!player.sneaking && wouldNotice(npc, player))
|
|
67
|
+
registerNotice(npc, beat);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// NOBODY OPENS ON THE BEAT THEY NOTICED YOU -- the same grace the
|
|
71
|
+
// ephemeral governor gives a mob its first beat: a gang that squares
|
|
72
|
+
// up and swings in one breath is the ambush street-gang.ts says it
|
|
73
|
+
// is not. A provocation is the exception: you drew on them.
|
|
74
|
+
const provoked = c.provokedBeat !== undefined;
|
|
75
|
+
if (!provoked && beat <= c.noticedBeat)
|
|
76
|
+
continue;
|
|
77
|
+
const inReach = actorDistanceMeters(room, npc, player) <= ADJACENT_METERS;
|
|
78
|
+
const patienceOut = beat - c.noticedBeat >= PATIENCE_BEATS;
|
|
79
|
+
const why = provoked ? 'provocation' : inReach ? 'contact' : patienceOut ? 'patience' : undefined;
|
|
80
|
+
if (!why)
|
|
81
|
+
continue;
|
|
82
|
+
return open(scene, room, npc, player, why);
|
|
83
|
+
}
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
/** The commit line, then the Combat Turn, with the hostile as aggressor. */
|
|
87
|
+
async function open(scene, room, npc, player, why) {
|
|
88
|
+
npc.hostileContact.openedBy = why;
|
|
89
|
+
// The predicate go.ts's despawn and resolvePartingShot read for "a
|
|
90
|
+
// fight is on": a mob that opened on you stays with it through the
|
|
91
|
+
// doorway; one you walked away from before this beat let you go.
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
npc.lastExchangeAt = now;
|
|
94
|
+
player.lastExchangeAt = now;
|
|
95
|
+
Logger.getInstance().write(`Hostile contact: ${npc.name} opens on ${player.name} in ${room.name} (${why}).`);
|
|
96
|
+
const line = why === 'provocation'
|
|
97
|
+
? `{yellow-fg}${npc.name} saw that -- and that settles it.{/yellow-fg}`
|
|
98
|
+
: why === 'contact'
|
|
99
|
+
? `{yellow-fg}${npc.name} is done looking at you.{/yellow-fg}`
|
|
100
|
+
: `{yellow-fg}${npc.name} has waited long enough.{/yellow-fg}`;
|
|
101
|
+
await scene.startEncounter(room, { aggressor: npc, target: player, ambush: false });
|
|
102
|
+
return [line];
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=hostile-contact.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)); }
|
|
@@ -367,7 +367,12 @@ function holdsGround(other, mover) {
|
|
|
367
367
|
if (other.allyOf === mover.name)
|
|
368
368
|
return false;
|
|
369
369
|
const guarding = other.guarding === true;
|
|
370
|
-
|
|
370
|
+
// Two fight engines, one rule: the old exchange flag OR a place in a
|
|
371
|
+
// running Combat Turn (combat-turn.ts). A ganger in the doorway you
|
|
372
|
+
// back through holds it while the fight is on.
|
|
373
|
+
const fighting = other.inExchange
|
|
374
|
+
|| other.combatPhaseLocked?.() === true;
|
|
375
|
+
return (fighting || guarding) && !other.surrendered;
|
|
371
376
|
}
|
|
372
377
|
/**
|
|
373
378
|
* Move a non-hostile body off an exit so the way opens, returning their
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerNotice } from './hostile-contact.js';
|
|
1
2
|
import { Logger } from './logger.js';
|
|
2
3
|
import { isOutdoorsPlace } from './outdoors.js';
|
|
3
4
|
import { spawnEphemeral, claimEphemeralName } from './ephemeral.js';
|
|
@@ -25,11 +26,16 @@ import { AI } from '../../../../tools/ai/ai.class.js';
|
|
|
25
26
|
* Splintered State p.11 "law enforcement will not show up at any point
|
|
26
27
|
* -- this is the Barrens"; Run Faster p.220 on Zone 0).
|
|
27
28
|
*
|
|
28
|
-
* THEY DO NOT ATTACK ON SIGHT. They are spawned `hostile`,
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* THEY DO NOT ATTACK ON SIGHT. They are spawned `hostile`, and the
|
|
30
|
+
* spawn line is the challenge canon puts before the fight (p.34-35).
|
|
31
|
+
* WHERE THE CHALLENGE GOES (2026-09-07, playtest "the combat turn never
|
|
32
|
+
* started until I attacked"): utilities/hostile-contact.ts. A ganger
|
|
33
|
+
* closes on the beat clock (ephemeral.ts closeOn); when it reaches you,
|
|
34
|
+
* when it watches you draw, or when its patience runs out, the Combat
|
|
35
|
+
* Turn opens with the ganger as aggressor. Walk away first and it lets
|
|
36
|
+
* you go; talk it round ("bluff", "persuade") and it stands down. The
|
|
37
|
+
* governor's founding property holds: the engine computes the ladder,
|
|
38
|
+
* the ephemeral never decides anything.
|
|
33
39
|
*/
|
|
34
40
|
/**
|
|
35
41
|
* PROFESSIONAL RATING 1 GANGER, canon stats (SR5 p.382, "Gangers &
|
|
@@ -114,9 +120,12 @@ export function maybeStreetGang(host, actor, room) {
|
|
|
114
120
|
});
|
|
115
121
|
if (!npc)
|
|
116
122
|
continue;
|
|
117
|
-
// OPPOSITION, not an ambush (Player.hostile)
|
|
118
|
-
//
|
|
123
|
+
// OPPOSITION, not an ambush (Player.hostile). A body that peeled
|
|
124
|
+
// off the wall watching you has, by definition, noticed you: it is
|
|
125
|
+
// on the contact ladder from this beat (hostile-contact.ts), with
|
|
126
|
+
// the one-beat grace every mob gets before it moves.
|
|
119
127
|
npc.hostile = true;
|
|
128
|
+
registerNotice(npc, host.scene.beat ?? 0);
|
|
120
129
|
spawned.push(npc.name);
|
|
121
130
|
}
|
|
122
131
|
if (spawned.length === 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.138.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.",
|