@maka/maka-cli 5.138.0 → 5.140.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 +9 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +7 -2
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +2 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +94 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/hostile-contact.js +6 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/room-view.js +38 -12
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.140.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,13 @@
|
|
|
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
|
+
// 1.44.0 (2026-09-07): WHO STARTED IT. The fight's first line names the
|
|
392
|
+
// aggressor and their target; the world event carries it; "initiative"
|
|
393
|
+
// marks them. Hostiles are red on the room map by stance or by live
|
|
394
|
+
// enmity, not by the retired exchange flag.
|
|
395
|
+
export const ENGINE_VERSION = '1.44.0';
|
|
388
396
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -35,7 +35,7 @@ import { hostileContactBeat } from './utilities/hostile-contact.js';
|
|
|
35
35
|
import { requestAlarmBeat, newAlarmBeatLatch, ALARM_STIMULUS } from './utilities/alarmed-staff.js';
|
|
36
36
|
import { renderMapViewport, mapCaption } from './utilities/map-view.js';
|
|
37
37
|
import { tallestRoomRows } from './utilities/room-grid.js';
|
|
38
|
-
import { renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
|
|
38
|
+
import { hostileByStance, enemyInFight, renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
|
|
39
39
|
import { getPref } from './utilities/prefs.js';
|
|
40
40
|
import { rollPool, formatRoll } from './utilities/dice.js';
|
|
41
41
|
import { Direction } from './types/shared/direction-enum.js';
|
|
@@ -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';
|
|
@@ -2553,6 +2554,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2553
2554
|
CommandFactory.registerCommand('aim', AimCommand);
|
|
2554
2555
|
CommandFactory.registerCommand('defend', DefendCommand);
|
|
2555
2556
|
CommandFactory.registerCommand('delay', DelayCommand);
|
|
2557
|
+
CommandFactory.registerCommand('nudge', NudgeCommand);
|
|
2556
2558
|
// 'train' used to alias advance; it belongs to crew instruction now
|
|
2557
2559
|
// (commands/crew.ts) -- self-training is "advance".
|
|
2558
2560
|
CommandFactory.registerCommand('advance', AdvanceCommand);
|
|
@@ -5621,6 +5623,9 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
5621
5623
|
isTableAlly: crewOfTable(this.scene.getPlayers()),
|
|
5622
5624
|
boxWidth,
|
|
5623
5625
|
isOtherPlayer: a => this.scene.isHumanControlled(a),
|
|
5626
|
+
// Red is the stance flag OR whoever the Combat Turn says is
|
|
5627
|
+
// fighting you (room-view.ts hostileByStance).
|
|
5628
|
+
isHostile: a => hostileByStance(a) || enemyInFight(this.scene, this.player, a),
|
|
5624
5629
|
});
|
|
5625
5630
|
if (roomLines.length > 0) {
|
|
5626
5631
|
roomLines.push(caption(roomCaption(here, this.player), boxWidth));
|
|
@@ -6040,7 +6045,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
6040
6045
|
static HELP_CATEGORIES = [
|
|
6041
6046
|
{ 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']] },
|
|
6042
6047
|
{ 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']] },
|
|
6043
|
-
{ 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']] },
|
|
6048
|
+
{ 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']] },
|
|
6044
6049
|
// The party layer: everyone who walks (or flies, or manifests) at
|
|
6045
6050
|
// your side answers to these.
|
|
6046
6051
|
// "players" folded back under crew after playtesting ("crew
|
|
@@ -17,7 +17,7 @@ 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
19
|
import { renderMapViewport, mapCaption } from './utilities/map-view.js';
|
|
20
|
-
import { renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
|
|
20
|
+
import { hostileByStance, enemyInFight, renderRoomLayoutCompact, crewOfTable, roomCaption } from './utilities/room-view.js';
|
|
21
21
|
import { serializeRoomGrid } from './utilities/room-grid.js';
|
|
22
22
|
import { entrySpotFor, spotOf } from './utilities/spots.js';
|
|
23
23
|
import { equipIntoEmptySlots } from './archetypes.js';
|
|
@@ -319,6 +319,7 @@ export async function createHeadlessSession(opts) {
|
|
|
319
319
|
try {
|
|
320
320
|
const roomLines = renderRoomLayoutCompact(here, p, {
|
|
321
321
|
isOtherPlayer: a => game.scene.isHumanControlled(a),
|
|
322
|
+
isHostile: a => hostileByStance(a) || enemyInFight(game.scene, p, a),
|
|
322
323
|
// The leader's crew are the TABLE's crew -- without this
|
|
323
324
|
// every non-leader saw them as neutral bystanders.
|
|
324
325
|
isTableAlly: crewOfTable(game.scene.getPlayers()),
|
|
@@ -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;
|
|
@@ -19,6 +84,10 @@ export class CombatEncounter {
|
|
|
19
84
|
/** Whose Action Phase is live, and their budget. */
|
|
20
85
|
phaseActor;
|
|
21
86
|
budget;
|
|
87
|
+
/** Who opened the fight, and on whom (playtest 2026-09-07: "nothing
|
|
88
|
+
* tells me who the aggressor is"). The tracker marks them. */
|
|
89
|
+
openedBy;
|
|
90
|
+
openedOn;
|
|
22
91
|
pairs = new Map();
|
|
23
92
|
surrenderSeenAt = new Map();
|
|
24
93
|
running = false;
|
|
@@ -41,13 +110,21 @@ export class CombatEncounter {
|
|
|
41
110
|
async start(members, opts) {
|
|
42
111
|
this.turn = 1;
|
|
43
112
|
this.pass = 1;
|
|
113
|
+
this.openedBy = opts.aggressor;
|
|
114
|
+
this.openedOn = opts.target;
|
|
44
115
|
for (const m of members)
|
|
45
116
|
this.addParticipant(m);
|
|
46
117
|
if (opts.target)
|
|
47
118
|
this.noteAttack(opts.aggressor, opts.target);
|
|
48
119
|
this.seedHostility();
|
|
49
120
|
const metaLines = ['Initiative (SR5 p.159):'];
|
|
50
|
-
|
|
121
|
+
// WHO STARTED IT is the first thing the room hears (playtest
|
|
122
|
+
// 2026-09-07): before the Surprise results and before the order.
|
|
123
|
+
const worldLines = [
|
|
124
|
+
opts.target
|
|
125
|
+
? `{red-fg}⚔ ${opts.aggressor.name} starts it -- opening on ${opts.target.name}.{/red-fg}`
|
|
126
|
+
: `{red-fg}⚔ ${opts.aggressor.name} starts it.{/red-fg}`,
|
|
127
|
+
];
|
|
51
128
|
// SURPRISE (p.192): the ambushed side rolls Reaction + Intuition (3).
|
|
52
129
|
// Failure: -10 Initiative and no defense against the surprisers
|
|
53
130
|
// until their next Action Phase. The aggressor never rolls -- an
|
|
@@ -76,7 +153,7 @@ export class CombatEncounter {
|
|
|
76
153
|
metaLines.push(` ${p.actor.name}: ${formatInitiative(p.roll)}${p.score !== p.roll.score ? ` -> ${p.score}` : ''}`);
|
|
77
154
|
}
|
|
78
155
|
worldLines.push(`Combat Turn ${this.turn} -- ${this.orderLine()}.`);
|
|
79
|
-
this.scene.addWorldEvent(`A fight broke out in ${this.room.name}: ${this.participants.map(p => p.actor.name).join(', ')}.`);
|
|
156
|
+
this.scene.addWorldEvent(`A fight broke out in ${this.room.name} -- ${opts.aggressor.name} opened${opts.target ? ` on ${opts.target.name}` : ''}. In it: ${this.participants.map(p => p.actor.name).join(', ')}.`);
|
|
80
157
|
await this.announce(worldLines, metaLines);
|
|
81
158
|
await this.run();
|
|
82
159
|
}
|
|
@@ -317,6 +394,7 @@ export class CombatEncounter {
|
|
|
317
394
|
this.budget = new ActionBudget();
|
|
318
395
|
p.surprised = false;
|
|
319
396
|
p.delayed = false;
|
|
397
|
+
p.nudges = 0;
|
|
320
398
|
p.actor.clearAim();
|
|
321
399
|
// Recoil resets the moment an Action Phase passes without firing --
|
|
322
400
|
// tracked by the phase, settled by attack.ts (p.175-176).
|
|
@@ -342,6 +420,19 @@ export class CombatEncounter {
|
|
|
342
420
|
const walkLeft = Math.max(0, actor.walkRateMeters - actor.movedMetersThisTurn);
|
|
343
421
|
return `${left} m of movement left this turn (${walkLeft} m at a walk)`;
|
|
344
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Another human nudged the phase holder (commands/nudge.ts): the
|
|
425
|
+
* running count for this phase, or undefined when `target` is not
|
|
426
|
+
* the one holding it. Bookkeeping only -- the verb owns the wording
|
|
427
|
+
* and calls endPhase() itself at NUDGES_TO_SKIP.
|
|
428
|
+
*/
|
|
429
|
+
noteNudge(target) {
|
|
430
|
+
const p = this.participantOf(target);
|
|
431
|
+
if (!p || this.ended || this.phaseActor !== target)
|
|
432
|
+
return undefined;
|
|
433
|
+
p.nudges = (p.nudges ?? 0) + 1;
|
|
434
|
+
return p.nudges;
|
|
435
|
+
}
|
|
345
436
|
/** The human typed "end turn" (or lost the phase -- fled, dropped). */
|
|
346
437
|
async endPhase(actor) {
|
|
347
438
|
const p = this.participantOf(actor);
|
|
@@ -545,6 +636,7 @@ export class CombatEncounter {
|
|
|
545
636
|
: p.actor.surrendered ? 'surrendered'
|
|
546
637
|
: p.acted ? 'acted' : p.delayed ? 'delaying' : p.score > 0 ? 'to act' : 'spent';
|
|
547
638
|
const marks = [
|
|
639
|
+
p.actor === this.openedBy ? 'started it' : '',
|
|
548
640
|
p.surprised ? 'surprised' : '',
|
|
549
641
|
p.fullDefenseTurn === this.turn ? 'full defense' : '',
|
|
550
642
|
p.actor.aimBonus > 0 ? `aiming +${p.actor.aimBonus}` : '',
|
|
@@ -98,7 +98,12 @@ async function open(scene, room, npc, player, why) {
|
|
|
98
98
|
: why === 'contact'
|
|
99
99
|
? `{yellow-fg}${npc.name} is done looking at you.{/yellow-fg}`
|
|
100
100
|
: `{yellow-fg}${npc.name} has waited long enough.{/yellow-fg}`;
|
|
101
|
+
// Logged to the room BEFORE the encounter opens, not returned after
|
|
102
|
+
// it: the fight announces itself ("⚔ X starts it -- opening on Y",
|
|
103
|
+
// Surprise, the order) while startEncounter runs, and a returned line
|
|
104
|
+
// would print under all of that, out of order.
|
|
105
|
+
Logger.getInstance().log(line, { room: room.name });
|
|
101
106
|
await scene.startEncounter(room, { aggressor: npc, target: player, ambush: false });
|
|
102
|
-
return [
|
|
107
|
+
return [];
|
|
103
108
|
}
|
|
104
109
|
//# sourceMappingURL=hostile-contact.js.map
|
|
@@ -276,7 +276,33 @@ export function crewOfTable(humans) {
|
|
|
276
276
|
return !!master && seated.has(master);
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
|
-
|
|
279
|
+
/**
|
|
280
|
+
* RED MEANS HOSTILE (playtest 2026-09-07: "the icons on the map should
|
|
281
|
+
* be red if they are hostile"). Until then the sword was keyed to
|
|
282
|
+
* `inExchange`, which only the old two-actor exchange ever set -- the
|
|
283
|
+
* Combat Turn (combat-turn.ts) never touches it, so a ganger standing
|
|
284
|
+
* over you drew as a white local. The stance flag (Player.hostile: set
|
|
285
|
+
* only by a seed or a scene that means it, cleared when talked round)
|
|
286
|
+
* is the structural answer that needs no Scene; the exchange case stays
|
|
287
|
+
* for the astral/Matrix fights still on that engine. A caller with a
|
|
288
|
+
* Scene adds live enmity through `isHostile` (game.ts, headless.ts).
|
|
289
|
+
*/
|
|
290
|
+
export function hostileByStance(a) {
|
|
291
|
+
if (a.surrendered)
|
|
292
|
+
return false;
|
|
293
|
+
const npc = a;
|
|
294
|
+
return (npc.hostile === true && !npc.allyOf) || a.inExchange;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Live enmity, for a caller that holds the Scene: `a` is in a running
|
|
298
|
+
* Combat Turn the viewer is also in, on the other side. Typed
|
|
299
|
+
* structurally so this file stays free of the Scene import.
|
|
300
|
+
*/
|
|
301
|
+
export function enemyInFight(scene, viewer, a) {
|
|
302
|
+
const enc = scene.encounterFor?.(a);
|
|
303
|
+
return !!enc && enc.has(viewer) && enc.enemiesOf(viewer).includes(a);
|
|
304
|
+
}
|
|
305
|
+
function actorGlyph(a, viewer, isOtherPlayer, isTableAlly, isHostile) {
|
|
280
306
|
// Still an actor, so still alive -- the dead are Room.bodies, drawn
|
|
281
307
|
// separately as skulls. This is someone face-down and revivable.
|
|
282
308
|
if (a.isIncapacitated())
|
|
@@ -313,7 +339,7 @@ function actorGlyph(a, viewer, isOtherPlayer, isTableAlly) {
|
|
|
313
339
|
if (isTableAlly?.(a) ?? a.allyOf === viewer.name) {
|
|
314
340
|
return { ch: CREW_GLYPH, color: 'green' };
|
|
315
341
|
}
|
|
316
|
-
if (a
|
|
342
|
+
if (isHostile?.(a) ?? hostileByStance(a))
|
|
317
343
|
return { ch: HOSTILE_GLYPH, color: 'red' };
|
|
318
344
|
return { ch: NEUTRAL_GLYPH, color: 'white' };
|
|
319
345
|
}
|
|
@@ -321,18 +347,18 @@ function actorGlyph(a, viewer, isOtherPlayer, isTableAlly) {
|
|
|
321
347
|
* The glyph for one cell, in priority order: people, then terrain, then
|
|
322
348
|
* vertical links, then exits, then the spot itself.
|
|
323
349
|
*/
|
|
324
|
-
function glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly) {
|
|
350
|
+
function glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly, isHostile) {
|
|
325
351
|
const k = key(c);
|
|
326
352
|
const occ = maps.occupants.get(k);
|
|
327
353
|
if (occ && occ.length > 0) {
|
|
328
354
|
if (occ.includes(viewer))
|
|
329
|
-
return actorGlyph(viewer, viewer, isOtherPlayer, isTableAlly);
|
|
355
|
+
return actorGlyph(viewer, viewer, isOtherPlayer, isTableAlly, isHostile);
|
|
330
356
|
if (occ.length === 1)
|
|
331
|
-
return actorGlyph(occ[0], viewer, isOtherPlayer, isTableAlly);
|
|
357
|
+
return actorGlyph(occ[0], viewer, isOtherPlayer, isTableAlly, isHostile);
|
|
332
358
|
// A crowded cell shows the most urgent icon standing in it: a fight
|
|
333
359
|
// outranks a runner outranks a bystander, and a body never hides
|
|
334
360
|
// someone still upright.
|
|
335
|
-
const glyphs = occ.map(a => actorGlyph(a, viewer, isOtherPlayer, isTableAlly));
|
|
361
|
+
const glyphs = occ.map(a => actorGlyph(a, viewer, isOtherPlayer, isTableAlly, isHostile));
|
|
336
362
|
return glyphs.find(g => g.ch === HOSTILE_GLYPH)
|
|
337
363
|
?? glyphs.find(g => g.ch === PLAYER_GLYPH)
|
|
338
364
|
// A fellow runner outranks a bystander for the same reason you
|
|
@@ -398,7 +424,7 @@ function levelPlansFor(room) {
|
|
|
398
424
|
* glyph used. Returns [] for a spotless room (no grid ever synthesizes
|
|
399
425
|
* -- see Room.ensureGrid).
|
|
400
426
|
*/
|
|
401
|
-
export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly) {
|
|
427
|
+
export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly, isHostile) {
|
|
402
428
|
const grid = room.ensureGrid();
|
|
403
429
|
if (!grid)
|
|
404
430
|
return [];
|
|
@@ -419,7 +445,7 @@ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly) {
|
|
|
419
445
|
let row = '';
|
|
420
446
|
for (let x = x0; x <= x1; x++) {
|
|
421
447
|
const c = { x, y, z: lvl.z };
|
|
422
|
-
row += exists.has(key(c)) ? cell(glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly)) : ' ';
|
|
448
|
+
row += exists.has(key(c)) ? cell(glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly, isHostile)) : ' ';
|
|
423
449
|
}
|
|
424
450
|
rows.push(row.trimEnd());
|
|
425
451
|
}
|
|
@@ -483,7 +509,7 @@ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly) {
|
|
|
483
509
|
}
|
|
484
510
|
// People, once, only for the icons actually standing in the room --
|
|
485
511
|
// a key to four kinds of person in an empty bar is just noise.
|
|
486
|
-
const present = new Set(visibleActorsIn(room, viewer).map(a => actorGlyph(a, viewer, isOtherPlayer, isTableAlly).ch));
|
|
512
|
+
const present = new Set(visibleActorsIn(room, viewer).map(a => actorGlyph(a, viewer, isOtherPlayer, isTableAlly, isHostile).ch));
|
|
487
513
|
const who = [
|
|
488
514
|
// "you" leads the key: on a party job it is the answer to the
|
|
489
515
|
// question that sent the player to the legend in the first place.
|
|
@@ -508,7 +534,7 @@ export function renderRoomLayout(room, viewer, isOtherPlayer, isTableAlly) {
|
|
|
508
534
|
present.has(OTHER_PLAYER_GLYPH) ? `{${OTHER_PLAYER_COLOR}-fg}${OTHER_PLAYER_GLYPH}{/${OTHER_PLAYER_COLOR}-fg} runner` : '',
|
|
509
535
|
present.has(CREW_GLYPH) ? `{green-fg}${CREW_GLYPH}{/green-fg} yours` : '',
|
|
510
536
|
present.has(NEUTRAL_GLYPH) ? `{white-fg}${NEUTRAL_GLYPH}{/white-fg} local` : '',
|
|
511
|
-
present.has(HOSTILE_GLYPH) ? `{red-fg}${HOSTILE_GLYPH}{/red-fg}
|
|
537
|
+
present.has(HOSTILE_GLYPH) ? `{red-fg}${HOSTILE_GLYPH}{/red-fg} hostile` : '',
|
|
512
538
|
// The knocked-out mark was missing from this key entirely -- the one
|
|
513
539
|
// glyph on the map a player is most likely to need explained.
|
|
514
540
|
present.has(DOWNED_GLYPH) ? `{yellow-fg}${DOWNED_GLYPH}{/yellow-fg} out cold` : '',
|
|
@@ -712,7 +738,7 @@ function renderShellCompact(room, viewer, availW, viewH, boxWidth) {
|
|
|
712
738
|
export function renderRoomLayoutCompact(room, viewer, opts = {}) {
|
|
713
739
|
// Default to the TALLEST room class, not a fixed 9 -- see
|
|
714
740
|
// room-grid.ts tallestRoomRows for why that number is derived.
|
|
715
|
-
const { viewH = tallestRoomRows(), boxWidth, isOtherPlayer, isTableAlly } = opts;
|
|
741
|
+
const { viewH = tallestRoomRows(), boxWidth, isOtherPlayer, isTableAlly, isHostile } = opts;
|
|
716
742
|
// Width left for the grid once the right-justified elevation bar and
|
|
717
743
|
// its gap are reserved.
|
|
718
744
|
const availW = Math.max(6, (boxWidth ?? 27) - ELEVATION_WIDTH - 1);
|
|
@@ -735,7 +761,7 @@ export function renderRoomLayoutCompact(room, viewer, opts = {}) {
|
|
|
735
761
|
const row = [];
|
|
736
762
|
for (let x = x0; x <= x1; x++) {
|
|
737
763
|
const c = { x, y, z };
|
|
738
|
-
row.push(exists.has(key(c)) ? glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly) : undefined);
|
|
764
|
+
row.push(exists.has(key(c)) ? glyphAt(c, maps, viewer, isOtherPlayer, isTableAlly, isHostile) : undefined);
|
|
739
765
|
}
|
|
740
766
|
cells.push(row);
|
|
741
767
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.140.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.",
|