@maka/maka-cli 5.134.0 → 5.136.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/aim.js +60 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +132 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/brandish.js +12 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +102 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/cover.js +24 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/defend.js +74 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/delay.js +34 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/dispel.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/drop.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/end-call.js +11 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/end-turn.js +58 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/equip.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +15 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/heal.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/initiative.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +42 -59
- package/bundle/typescript/src/commands/game/sideQuest/commands/posture.js +23 -7
- package/bundle/typescript/src/commands/game/sideQuest/commands/reload.js +31 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/stance.js +16 -15
- package/bundle/typescript/src/commands/game/sideQuest/commands/summon.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/surrender.js +10 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/take.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/unequip.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +18 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +35 -18
- package/bundle/typescript/src/commands/game/sideQuest/headless-harness.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +21 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +50 -13
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +102 -5
- package/bundle/typescript/src/commands/game/sideQuest/ui.js +12 -12
- package/bundle/typescript/src/commands/game/sideQuest/utilities/action-budget.js +87 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/action-cost.js +66 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +101 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +571 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/dice.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/movement-cost.js +80 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +122 -0
- package/package.json +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/auto-fight.js +0 -182
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Category } from '../types/shared/item-enum.js';
|
|
2
|
+
import { spotOf, canReach, isInCover, coverAvailableFor, actorDistanceMeters } from './spots.js';
|
|
3
|
+
import { Logger } from './logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* AN NPC'S ACTION PHASE, PLAYED BY RULE (SR5 p.163-167).
|
|
6
|
+
*
|
|
7
|
+
* The LLM reflect chain (models/npc.ts) is how NPCs live between fights
|
|
8
|
+
* -- talk, wander, react. It is the wrong instrument for a Combat Turn:
|
|
9
|
+
* it answers on its own clock, cannot be told "you have two Simple
|
|
10
|
+
* Actions and it is your phase now", and its grind-breaker stops after
|
|
11
|
+
* two refusals. So while an encounter runs, every participant NPC's
|
|
12
|
+
* chain is suspended (NPC.combatPhaseLocked) and this plays their
|
|
13
|
+
* phases instead, through the same verbs a player types, so every
|
|
14
|
+
* action pays the same cost through the same gate (action-cost.ts).
|
|
15
|
+
*
|
|
16
|
+
* The doctrine is a soldier's, not a genius's, and deliberately short:
|
|
17
|
+
* 1. A holstered weapon is readied first (Ready Weapon, Simple, p.165).
|
|
18
|
+
* 2. A dry gun is reloaded when there is ammunition to do it with
|
|
19
|
+
* (the whole phase, p.163/167); otherwise the fists come up.
|
|
20
|
+
* 3. Melee closes the distance (movement, p.161) and swings (Complex,
|
|
21
|
+
* p.167) -- or takes cover if it cannot reach this turn.
|
|
22
|
+
* 4. A shooter with a free Simple takes cover if there is any to take
|
|
23
|
+
* (Simple, p.166), then fires (Simple, p.165). One attack per
|
|
24
|
+
* phase (p.164), so the second Simple is never a second shot.
|
|
25
|
+
* Nothing here rolls a die of its own: the verbs do.
|
|
26
|
+
*/
|
|
27
|
+
export async function runNpcActionPhase(enc, npc) {
|
|
28
|
+
const logger = Logger.getInstance();
|
|
29
|
+
if (npc.isIncapacitated() || npc.surrendered)
|
|
30
|
+
return;
|
|
31
|
+
const enemies = enc.enemiesOf(npc);
|
|
32
|
+
if (enemies.length === 0) {
|
|
33
|
+
logger.write(`Brain: ${npc.name} has nobody to fight -- phase forfeited.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const target = pickTarget(npc, enemies);
|
|
37
|
+
npc.combatOpponent = target;
|
|
38
|
+
npc.lastExchangeAt = Date.now();
|
|
39
|
+
let guard = 0;
|
|
40
|
+
while (guard++ < 6) {
|
|
41
|
+
const budget = enc.budgetOf(npc);
|
|
42
|
+
if (!budget || budget.actionsExhausted)
|
|
43
|
+
return;
|
|
44
|
+
if (target.isIncapacitated() || target.currentLocation !== npc.currentLocation)
|
|
45
|
+
return;
|
|
46
|
+
const weapon = npc.getCarriedWeapon();
|
|
47
|
+
const firearm = !!weapon?.isFirearm();
|
|
48
|
+
// 1. Ready Weapon.
|
|
49
|
+
if (weapon && !npc.weaponDrawn) {
|
|
50
|
+
const r = await npc.actInCombat('draw');
|
|
51
|
+
logger.write(`Brain: ${npc.name} draw -> ${r}`);
|
|
52
|
+
if (!npc.weaponDrawn)
|
|
53
|
+
return; // refused -- nothing else will go better
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// 2. A dry gun: reload if the pack allows, else it is a club.
|
|
57
|
+
const dry = firearm && (weapon.ammo <= 0);
|
|
58
|
+
if (dry && !weapon.jammed && hasAmmo(npc)) {
|
|
59
|
+
const r = await npc.actInCombat('reload');
|
|
60
|
+
logger.write(`Brain: ${npc.name} reload -> ${r}`);
|
|
61
|
+
return; // a reload is the phase (p.163 Reloading Weapons)
|
|
62
|
+
}
|
|
63
|
+
const melee = !firearm || dry || weapon.jammed;
|
|
64
|
+
if (melee) {
|
|
65
|
+
const targetSpot = spotOf(target);
|
|
66
|
+
if (!canReach(npc, targetSpot)) {
|
|
67
|
+
const r = await npc.actInCombat(`move to ${targetSpot}`);
|
|
68
|
+
logger.write(`Brain: ${npc.name} move to ${targetSpot} -> ${r}`);
|
|
69
|
+
if (!canReach(npc, targetSpot)) {
|
|
70
|
+
// Couldn't close this turn: get behind something and wait.
|
|
71
|
+
if (!isInCover(npc) && coverAvailableFor(npc) && (enc.budgetOf(npc)?.simple ?? 0) > 0) {
|
|
72
|
+
await npc.actInCombat('cover');
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const b = enc.budgetOf(npc);
|
|
78
|
+
if (b?.complexAvailable) {
|
|
79
|
+
const r = await npc.actInCombat(`attack ${target.name}`);
|
|
80
|
+
logger.write(`Brain: ${npc.name} attack ${target.name} -> ${r.split('\n')[0]}`);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Ranged: cover first when both Simples are still on the table.
|
|
85
|
+
if (!isInCover(npc) && coverAvailableFor(npc) && budget.simple >= 2) {
|
|
86
|
+
const r = await npc.actInCombat('cover');
|
|
87
|
+
logger.write(`Brain: ${npc.name} cover -> ${r}`);
|
|
88
|
+
if (!isInCover(npc))
|
|
89
|
+
return;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!budget.attackTaken && budget.simple >= 1) {
|
|
93
|
+
const r = await npc.actInCombat(`attack ${target.name}`);
|
|
94
|
+
logger.write(`Brain: ${npc.name} attack ${target.name} -> ${r.split('\n')[0]}`);
|
|
95
|
+
if (!enc.budgetOf(npc)?.attackTaken)
|
|
96
|
+
return; // refused: don't loop on it
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** The opponent already engaged, else the nearest of the rest. */
|
|
103
|
+
function pickTarget(npc, enemies) {
|
|
104
|
+
const engaged = npc.combatOpponent;
|
|
105
|
+
if (engaged && enemies.includes(engaged))
|
|
106
|
+
return engaged;
|
|
107
|
+
const room = npc.currentLocation;
|
|
108
|
+
let best = enemies[0];
|
|
109
|
+
let bestD = Number.POSITIVE_INFINITY;
|
|
110
|
+
for (const e of enemies) {
|
|
111
|
+
const d = actorDistanceMeters(room, npc, e);
|
|
112
|
+
if (d < bestD) {
|
|
113
|
+
best = e;
|
|
114
|
+
bestD = d;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return best;
|
|
118
|
+
}
|
|
119
|
+
function hasAmmo(npc) {
|
|
120
|
+
return npc.inventory.getAllItems().some(i => i.category === Category.Ammunition);
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=npc-combat-brain.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.136.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,182 +0,0 @@
|
|
|
1
|
-
import { Logger } from './logger.js';
|
|
2
|
-
/**
|
|
3
|
-
* Fight-or-flight autopilot -- the FIGHT half. The player sets a
|
|
4
|
-
* temperature (Tab in the UI, or the "stance" command) and the game keeps
|
|
5
|
-
* swinging for them; flight stays manual, always, because stopping YOUR
|
|
6
|
-
* swings never stops THEIRS -- the only real defense is the door, and
|
|
7
|
-
* that's the player's call.
|
|
8
|
-
*
|
|
9
|
-
* manual -- counters only. Struck, you strike back (the exchange
|
|
10
|
-
* engine answers every swing in EVERY stance -- see
|
|
11
|
-
* CombatExchange.resolveStrike's two-way resolution);
|
|
12
|
-
* but pressing the fight, chasing, or finishing is yours
|
|
13
|
-
* to type. (A real session read "manual" as fully
|
|
14
|
-
* passive, saw the built-in counters, and concluded the
|
|
15
|
-
* stances were redundant -- they never were; the old
|
|
16
|
-
* descriptions were wrong.)
|
|
17
|
-
* guarded -- keeps swinging FOR you between exchanges, with a
|
|
18
|
-
* conscience: switches to subdue when the target is
|
|
19
|
-
* nearly down, and stands down the moment they surrender
|
|
20
|
-
* aggressive -- keeps swinging until they're dead; surrender means
|
|
21
|
-
* nothing
|
|
22
|
-
*
|
|
23
|
-
* Wiring: CombatExchange.finishExchange() -> Scene.notifyExchangeSettled()
|
|
24
|
-
* -> onExchangeSettled() here. If the player was a combatant and the
|
|
25
|
-
* stance says fight, the next action is scheduled a short, interruptible
|
|
26
|
-
* beat later (REENGAGE_DELAY_MS). Submitting ANY command in the UI cancels
|
|
27
|
-
* the pending action and takes over (see ui.ts) -- the beat exists exactly
|
|
28
|
-
* so fleeing and healing stay possible mid-autofight. Everything is
|
|
29
|
-
* revalidated at fire time: the fight may have ended, moved, or been
|
|
30
|
-
* surrendered out of during the delay.
|
|
31
|
-
*
|
|
32
|
-
* Auto actions lead with whatever hits hardest: a caster whose manabolt
|
|
33
|
-
* out-damages their equipped weapon throws the bolt (and eats the drain);
|
|
34
|
-
* everyone else uses the gun/blade/fists they're holding. A dry gun's
|
|
35
|
-
* refusal (reload prompt) is surfaced to the player and auto simply waits
|
|
36
|
-
* for them to deal with it -- reloading is a decision, not a reflex.
|
|
37
|
-
*/
|
|
38
|
-
export class AutoFight {
|
|
39
|
-
game;
|
|
40
|
-
static REENGAGE_DELAY_MS = 2000;
|
|
41
|
-
// One pending timer per HUMAN player (symmetric MP: each human's
|
|
42
|
-
// autopilot runs independently).
|
|
43
|
-
pending = new Map();
|
|
44
|
-
constructor(game) {
|
|
45
|
-
this.game = game;
|
|
46
|
-
}
|
|
47
|
-
/** Pending auto actions, dropped. No name = all of them (the local
|
|
48
|
-
* player typed, or the stance changed -- ui.ts's call). */
|
|
49
|
-
cancelPending(playerName) {
|
|
50
|
-
if (playerName === undefined) {
|
|
51
|
-
for (const t of this.pending.values())
|
|
52
|
-
clearTimeout(t);
|
|
53
|
-
this.pending.clear();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const t = this.pending.get(playerName);
|
|
57
|
-
if (t) {
|
|
58
|
-
clearTimeout(t);
|
|
59
|
-
this.pending.delete(playerName);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Called after every combat exchange resolves (player-involved or not).
|
|
64
|
-
* Schedules the next action for every HUMAN combatant whose stance
|
|
65
|
-
* says fight.
|
|
66
|
-
*/
|
|
67
|
-
onExchangeSettled(combatants) {
|
|
68
|
-
for (const player of this.game.scene.getPlayers()) {
|
|
69
|
-
if (!combatants.includes(player))
|
|
70
|
-
continue;
|
|
71
|
-
if (player.autoStance === 'manual')
|
|
72
|
-
continue;
|
|
73
|
-
const target = combatants.find(c => c !== player);
|
|
74
|
-
if (!target)
|
|
75
|
-
continue;
|
|
76
|
-
this.cancelPending(player.name);
|
|
77
|
-
const t = setTimeout(() => {
|
|
78
|
-
this.pending.delete(player.name);
|
|
79
|
-
void this.fire(player, target);
|
|
80
|
-
}, AutoFight.REENGAGE_DELAY_MS);
|
|
81
|
-
this.pending.set(player.name, t);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* The beat elapsed uninterrupted -- act, if the fight still stands.
|
|
86
|
-
* Every guard here re-checks live state: the target may have died, fled,
|
|
87
|
-
* surrendered, or started another exchange during the delay.
|
|
88
|
-
*/
|
|
89
|
-
async fire(player, target) {
|
|
90
|
-
const logger = Logger.getInstance();
|
|
91
|
-
if (player.autoStance === 'manual')
|
|
92
|
-
return;
|
|
93
|
-
if (player.isIncapacitated() || target.isIncapacitated())
|
|
94
|
-
return;
|
|
95
|
-
if (player.currentLocation !== target.currentLocation)
|
|
96
|
-
return;
|
|
97
|
-
// Cross-plane fights don't exist: a dump or a return mid-beat (see
|
|
98
|
-
// utilities/planes.ts) ends the engagement, autopilot included.
|
|
99
|
-
if (player.plane !== target.plane)
|
|
100
|
-
return;
|
|
101
|
-
// A fresh exchange is already in flight -- when IT settles,
|
|
102
|
-
// onExchangeSettled reschedules us. No waiting here.
|
|
103
|
-
if (player.inExchange || target.inExchange)
|
|
104
|
-
return;
|
|
105
|
-
// Guarded honors a surrender -- but doesn't trust it: the final
|
|
106
|
-
// action is a RESTRAIN (player request; SR5e Subduing p.195, the
|
|
107
|
-
// surrendered branch is an automatic no-roll hold). Hands-up
|
|
108
|
-
// prisoners get zip-tied, not taken at their word. One-shot: an
|
|
109
|
-
// already-held target (by anyone) means truly stand down, and off
|
|
110
|
-
// the meat plane there are no hands to hold with.
|
|
111
|
-
if (player.autoStance === 'non-lethal' && target.surrendered) {
|
|
112
|
-
if (player.plane === 'meat' && target.plane === 'meat'
|
|
113
|
-
&& player.grappling !== target.name && !target.grappledBy) {
|
|
114
|
-
const cmd = `restrain ${target.name}`;
|
|
115
|
-
logger.meta(`⚔ [non-lethal] ${cmd}`, 'cyan');
|
|
116
|
-
const result = (await this.game.handleInputAs(player.name, cmd)).text;
|
|
117
|
-
if (result) {
|
|
118
|
-
logger.log(result);
|
|
119
|
-
}
|
|
120
|
-
this.game.updateStatus();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
logger.logWithColor(`⚔ ${target.name} has surrendered -- you hold fire. (non-lethal)`, 'yellow');
|
|
124
|
-
this.game.updateStatus();
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const cmd = this.chooseAction(player, target);
|
|
128
|
-
// Cyan, not gray: the autopilot announcing it acted must be legible on
|
|
129
|
-
// stock dark palettes (gray = ANSI bright-black, near-invisible there).
|
|
130
|
-
// Mechanics ticker, not the narrative -- the strike it triggers tells
|
|
131
|
-
// the story in the world log (readability pass).
|
|
132
|
-
logger.meta(`⚔ [${player.autoStance}] ${cmd}`, 'cyan');
|
|
133
|
-
const result = (await this.game.handleInputAs(player.name, cmd)).text;
|
|
134
|
-
// Player-initiated exchanges self-announce ('' result); anything else
|
|
135
|
-
// (a dry-gun reload prompt, a refusal) is information the player needs.
|
|
136
|
-
if (result) {
|
|
137
|
-
logger.log(result);
|
|
138
|
-
}
|
|
139
|
-
this.game.updateStatus();
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Lead with what hits hardest -- except Guarded near the end, which
|
|
143
|
-
* finishes non-lethally: when the target is within a few boxes of death,
|
|
144
|
-
* the next auto action is a subdue (stun damage, bare hands) instead of
|
|
145
|
-
* another killing blow.
|
|
146
|
-
*/
|
|
147
|
-
chooseAction(player, target) {
|
|
148
|
-
// Subduing takes real hands (meat plane only); a guarded decker
|
|
149
|
-
// "killing" ice is just crashing a program, and astral entities go
|
|
150
|
-
// down by force -- no non-lethal switch off the meat plane.
|
|
151
|
-
if (player.autoStance === 'non-lethal' && player.plane === 'meat') {
|
|
152
|
-
if (target.maxConditionBoxes - target.damageTaken <= 3)
|
|
153
|
-
return `subdue ${target.name}`;
|
|
154
|
-
// A gun that can only kill is not what this stance reaches for:
|
|
155
|
-
// no stun rounds loaded means hands, not lead (p.434).
|
|
156
|
-
const gun = player.weaponDrawn ? player.getCarriedWeapon() : null;
|
|
157
|
-
if (gun?.isFirearm() && !gun.isStunWeapon() && !gun.stunAmmo())
|
|
158
|
-
return `subdue ${target.name}`;
|
|
159
|
-
}
|
|
160
|
-
// Magic doesn't compile in the Matrix (see cast.ts); everywhere else,
|
|
161
|
-
// lead with the harder hit. The autopilot casts at the mage's FULL
|
|
162
|
-
// safe Force (bolt damage = F - 2, drain billed, never overcast) --
|
|
163
|
-
// a bare "cast manabolt" would use the drain-neutral default (F3,
|
|
164
|
-
// damage 1: the Force pass silently gutted the old comparison).
|
|
165
|
-
// Same safe ceiling as cast.ts: a book-taught mundane works at 4.
|
|
166
|
-
const boltForce = player.magic > 0 ? player.magic : 4;
|
|
167
|
-
const boltDamage = Math.max(1, boltForce - 2);
|
|
168
|
-
if (player.plane !== 'matrix' && player.isCaster() && boltDamage > player.getWeaponProfile().damage) {
|
|
169
|
-
return `cast manabolt ${boltForce} at ${target.name}`;
|
|
170
|
-
}
|
|
171
|
-
return `attack ${target.name}`;
|
|
172
|
-
}
|
|
173
|
-
/** One-line HUD/announce description for a stance. */
|
|
174
|
-
static describe(stance) {
|
|
175
|
-
switch (stance) {
|
|
176
|
-
case 'manual': return 'counters when struck (every stance does) -- but pressing the fight is yours to type';
|
|
177
|
-
case 'non-lethal': return 'presses the fight for you with Stun wherever the gear allows (stun rounds, the flat of the blade, bare hands), subdues when they are nearly down, and RESTRAINS them when they surrender';
|
|
178
|
-
case 'lethal': return 'presses the fight to the death; surrender means nothing';
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
//# sourceMappingURL=auto-fight.js.map
|