@maka/maka-cli 5.179.0 → 5.181.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/unlock.js +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +14 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +69 -23
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +6 -4
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +39 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/alarmed-staff.js +3 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.181.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,6 +1,7 @@
|
|
|
1
1
|
import { Door } from '../models/door.js';
|
|
2
2
|
import { Command } from './command.js';
|
|
3
3
|
import { ensureAtExit, lockedRouteHint } from '../utilities/spots.js';
|
|
4
|
+
import { applyDeviceOpened } from '../utilities/devices.js';
|
|
4
5
|
export class UnlockCommand extends Command {
|
|
5
6
|
static verb = 'unlock';
|
|
6
7
|
static description = 'Unlock a door.';
|
|
@@ -51,6 +52,34 @@ export class UnlockCommand extends Command {
|
|
|
51
52
|
if (reach.refusal)
|
|
52
53
|
return reach.refusal;
|
|
53
54
|
const crossNote = reach.line ? `${reach.line}\n` : '';
|
|
55
|
+
// A DOOR HELD BY A DEVICE IN THIS ROOM (8XDR2BCZoSmpqdkXG). The
|
|
56
|
+
// Door itself knows no key -- scene-factory seals it through the
|
|
57
|
+
// device (heldByDevice), and the KEY lives on the device as its
|
|
58
|
+
// keyItem. This verb only ever read the Door, so on a keyed maglock
|
|
59
|
+
// it answered "you don't have the key" to the player holding it --
|
|
60
|
+
// while the device's own wrong-verb line promised exactly this:
|
|
61
|
+
// "The right key opens it clean: 'unlock' with Corvid's Fob in
|
|
62
|
+
// hand." Same offer `use` makes, so the same code path and the same
|
|
63
|
+
// aftermath (applyDeviceOpened), never a second copy of "what opens
|
|
64
|
+
// this".
|
|
65
|
+
const holder = room.deviceOpening(direction);
|
|
66
|
+
if (holder) {
|
|
67
|
+
if (holder.keyItem && this.actor.inventory.hasItem(holder.keyItem.name)) {
|
|
68
|
+
const result = holder.offer('', this.actor.inventory);
|
|
69
|
+
this.logger.write(`UnlockCommand: ${this.actor.name} turned ${holder.keyItem.name} in ${holder.name}: ${result.success}`);
|
|
70
|
+
this.actor.performAction('unlocked', result.message);
|
|
71
|
+
if (!result.success)
|
|
72
|
+
return crossNote + result.message;
|
|
73
|
+
const lines = applyDeviceOpened(this.scene, this.actor, room, holder, 'unlock');
|
|
74
|
+
return crossNote + [result.message, ...lines].join('\n');
|
|
75
|
+
}
|
|
76
|
+
const ways = holder.verbs().map(v => `"${v} ${holder.name}"`).join(' or ');
|
|
77
|
+
const takes = holder.keyItem
|
|
78
|
+
? `It takes ${holder.keyItem.name}, which you don't have.`
|
|
79
|
+
: `No key you could carry fits it.`;
|
|
80
|
+
this.logger.write(`UnlockCommand: ${this.actor.name} has no key for ${holder.name} on "${target}".`);
|
|
81
|
+
return crossNote + `${holder.name} holds it. ${takes} Or force it: ${ways}.`;
|
|
82
|
+
}
|
|
54
83
|
const requiredKeyOrSolution = door.unlockItemOrSolution?.toLowerCase();
|
|
55
84
|
if (requiredKeyOrSolution && this.actor.inventory.hasItem(requiredKeyOrSolution)) {
|
|
56
85
|
const key = this.actor.inventory.getItem(requiredKeyOrSolution);
|
|
@@ -147,6 +147,20 @@ export class UseCommand extends Command {
|
|
|
147
147
|
return resolved.error ?? `You don't have the ${itemName} to use.`;
|
|
148
148
|
}
|
|
149
149
|
const itemToUse = resolved.item;
|
|
150
|
+
// A KEY, USED ON ITS OWN, GOES INTO THE LOCK IT OPENS
|
|
151
|
+
// (8XDR2BCZoSmpqdkXG). "use Corvid's Fob" is the most natural thing
|
|
152
|
+
// to type at a maglock that says it takes the fob, and it used to
|
|
153
|
+
// fall all the way down this ladder to "You can't use the fob in
|
|
154
|
+
// this way" -- the Key category has no use() of its own. When
|
|
155
|
+
// exactly one shut device in the room answers to this item, the
|
|
156
|
+
// item IS the offer, the same one "use <item> on <device>" makes.
|
|
157
|
+
const opensHere = devices.filter(d => d.keyItem?.name === itemToUse.name);
|
|
158
|
+
if (opensHere.length === 1) {
|
|
159
|
+
return this.useDevice(opensHere[0], itemToUse.name);
|
|
160
|
+
}
|
|
161
|
+
if (opensHere.length > 1) {
|
|
162
|
+
return `${itemToUse.name} opens more than one thing here -- say which: ${opensHere.map(d => d.name).join(', ')}.`;
|
|
163
|
+
}
|
|
150
164
|
if (itemToUse.category === Category.Medical) {
|
|
151
165
|
return this.applyFirstAid(itemToUse);
|
|
152
166
|
}
|
|
@@ -267,10 +267,13 @@ take ground. Build it accordingly:
|
|
|
267
267
|
Matrix presence, whose files open only to a runner whose BODY is in that room). Pick one
|
|
268
268
|
and commit to it -- the offline server is the harder, more interesting shape when the
|
|
269
269
|
fiction wants a vault you must walk into.
|
|
270
|
-
- THE OPPOSITION IS ICE, not muscle.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
- THE OPPOSITION IS THE HOST'S OWN ICE, not muscle. Give the vault host a "hostRating" of
|
|
271
|
+
5-8: the host launches its own intrusion countermeasures (Patrol, Probe, Killer...) at
|
|
272
|
+
that rating, so DO NOT write an NPC for the ice -- no "matrix"-plane npcs[] at all. The
|
|
273
|
+
NPCs are the client and the people who work the site. No guards patrolling meat
|
|
274
|
+
corridors, no gunfight in the lobby: a data run that ends in a firefight is a
|
|
275
|
+
smash-and-grab wearing the wrong name. A site can still be WATCHED (cameras) --
|
|
276
|
+
surveillance is pressure without being a gun.
|
|
274
277
|
- Keep it SHORT and tight. One site, one host, one file.
|
|
275
278
|
- It pays LESS than a full run of the same tier -- roughly half. The runner is trading pay
|
|
276
279
|
for a job that never puts a gun in their face, and the fixer prices it that way. Say so
|
|
@@ -407,8 +410,9 @@ ${JSON_ONLY}
|
|
|
407
410
|
// its description reads.
|
|
408
411
|
"npcs": [{
|
|
409
412
|
"name": string, "concept": string, "startLocation": string,
|
|
410
|
-
"plane"?: "meat"|"
|
|
411
|
-
|
|
413
|
+
"plane"?: "meat"|"astral", // "astral" = a spirit. Default meat. NEVER "matrix": a host's
|
|
414
|
+
// ice is not a person -- the host launches its own programs
|
|
415
|
+
// at its "hostRating", and an NPC written as ice is dropped.
|
|
412
416
|
"hostile"?: boolean // OPPOSITION: this one KNOWS the runner does not belong and will
|
|
413
417
|
// act on it -- a posted guard, a patrolling enforcer, a gang
|
|
414
418
|
// holding the room. Default false, and LEAVE IT FALSE unless the
|
|
@@ -418,9 +422,6 @@ ${JSON_ONLY}
|
|
|
418
422
|
// building, and marking them turns an ordinary scene into a
|
|
419
423
|
// brawl. If in doubt, leave it out -- a guard who stays calm is
|
|
420
424
|
// a missed beat, a hostile bartender is a broken scene.
|
|
421
|
-
// ICE NEEDS NO FLAG: a "matrix" NPC is intrusion countermeasures
|
|
422
|
-
// and is treated as opposition automatically. Write it only to
|
|
423
|
-
// say FALSE, for the rare tame construct.
|
|
424
425
|
}],
|
|
425
426
|
"devices": [{
|
|
426
427
|
"name": string, "concept": string, // what it physically IS (a maglock, a keypad, a
|
|
@@ -520,9 +521,10 @@ Hard structural rules (validated mechanically -- a violation is rejected):
|
|
|
520
521
|
skill can still take the shortcut -- pick the lock, hack the panel -- but the found way
|
|
521
522
|
must exist for a runner who has neither.
|
|
522
523
|
- The winCondition item must not be heldBy winCondition.toNpc. If a matrix-plane item is the
|
|
523
|
-
winCondition item, its room has hasNode true AND a "
|
|
524
|
-
|
|
525
|
-
|
|
524
|
+
winCondition item, its room has hasNode true AND a "hostRating" of at least 4 -- the host
|
|
525
|
+
defends its own files with its own ice; never write an NPC for that. If the runner can
|
|
526
|
+
jack in, a Datachip/Document winCondition item MUST be that matrix-plane item -- data for
|
|
527
|
+
a hacker lives on a host, never on a desk.
|
|
526
528
|
- ${isContinuation
|
|
527
529
|
? 'ZERO "starting"-role items -- the runner arrives with everything they own. 4-6'
|
|
528
530
|
: '3-5 "starting"-role items (basics the runner doesn\'t already own -- see the dossier), 4-6'}
|
|
@@ -652,8 +654,8 @@ For EVERY npc in the skeleton, expand its "concept":
|
|
|
652
654
|
"hacking"?, "cybercombat"?, "electronic-warfare"?, "hardware"?, "locksmith"?, "computer"?,
|
|
653
655
|
"negotiation"?, "con"?, "intimidation"?, "etiquette"?, "leadership"?, "performance"?, "disguise"?,
|
|
654
656
|
"medicine"?, "assensing"?, "counterspelling"?, "conjuring"?, "gunnery"? } }
|
|
655
|
-
"magic" 3-6 ONLY for a genuinely Awakened concept;
|
|
656
|
-
|
|
657
|
+
"magic" 3-6 ONLY for a genuinely Awakened concept; give combat blocks to anyone the story
|
|
658
|
+
expects in a firefight. "skills" is
|
|
657
659
|
optional flavor for defined concepts (a sharpshooter's "firearms": 6).
|
|
658
660
|
|
|
659
661
|
${JSON_ONLY}
|
|
@@ -886,6 +888,14 @@ const GENERIC_LOCK_WORDS = new Set([
|
|
|
886
888
|
'lock', 'maglock', 'door', 'gate', 'grate', 'hatch', 'safe', 'vault', 'cage', 'shutter', 'padlock', 'cabinet', 'locker',
|
|
887
889
|
'keycard', 'card', 'passkey', 'access', 'security', 'back', 'front', 'side', 'room', 'office', 'main', 'inner', 'outer',
|
|
888
890
|
]);
|
|
891
|
+
/**
|
|
892
|
+
* THE LEAST A VAULT HOST RATES. A host launches its own IC at its rating
|
|
893
|
+
* (p.247, one per Combat Turn up to the rating), so the rating is the
|
|
894
|
+
* whole defence of the paydata inside it; below this the run is a free
|
|
895
|
+
* walk. Four is the floor: a rating-3 default host fields three thin
|
|
896
|
+
* programs and no Killer.
|
|
897
|
+
*/
|
|
898
|
+
export const VAULT_HOST_RATING = 4;
|
|
889
899
|
export function validateSkeleton(skeleton, player, crew) {
|
|
890
900
|
const fail = (msg) => { throw new Error(`Skeleton invalid: ${msg}`); };
|
|
891
901
|
if (!Array.isArray(skeleton?.rooms) || skeleton.rooms.length === 0)
|
|
@@ -1107,11 +1117,18 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
1107
1117
|
// Worded to route to 'skeleton' (no "items[", no "Win condition item").
|
|
1108
1118
|
const winItem = skeleton.items.find(i => i.name === skeleton.winCondition.item);
|
|
1109
1119
|
if (winItem && isDataShaped(winItem.category) && winItem.plane !== 'matrix' && partyCanDeck(player)) {
|
|
1110
|
-
fail(`the winCondition item "${winItem.name}" is a ${winItem.category} on the meat plane, and this runner can jack in -- for a decker or technomancer, data is PAYDATA: give it "plane": "matrix", place it in a hasNode room (not heldBy anyone)
|
|
1120
|
+
fail(`the winCondition item "${winItem.name}" is a ${winItem.category} on the meat plane, and this runner can jack in -- for a decker or technomancer, data is PAYDATA: give it "plane": "matrix", place it in a hasNode room (not heldBy anyone) with a hostRating of ${VAULT_HOST_RATING} or more. A chip lying on a desk is no job for a hacker.`);
|
|
1111
1121
|
}
|
|
1112
1122
|
// The other half of the same rule, for ANY matrix-plane objective: the
|
|
1113
|
-
// schema says its room has hasNode and
|
|
1123
|
+
// schema says its room has hasNode and a rating that fields ice, and
|
|
1114
1124
|
// until now nothing at the skeleton checked it.
|
|
1125
|
+
//
|
|
1126
|
+
// THE HOST IS ITS OWN GUARD (player ruling 2026-09-13, "mute all
|
|
1127
|
+
// ice"): a vault used to demand a "matrix"-plane NPC -- a person
|
|
1128
|
+
// wearing an ice costume, with dialogue lines and a model behind it,
|
|
1129
|
+
// which is how four programs ended up bargaining over a chip in Vex's
|
|
1130
|
+
// log. Canon has none of that: the host launches its own IC at its
|
|
1131
|
+
// rating (p.247), so what a vault needs is a rating worth the name.
|
|
1115
1132
|
if (winItem?.plane === 'matrix') {
|
|
1116
1133
|
if (!winItem.room) {
|
|
1117
1134
|
fail(`the winCondition item "${winItem.name}" is matrix-plane paydata but has no "room" -- data lives on a host, never in a pocket. Place it in a hasNode room.`);
|
|
@@ -1120,9 +1137,17 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
1120
1137
|
if (!vault?.hasNode) {
|
|
1121
1138
|
fail(`the winCondition item "${winItem.name}" is matrix-plane paydata in "${winItem.room}", which has no "hasNode": true -- a matrix file can only exist on a host. Mark that room hasNode.`);
|
|
1122
1139
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1140
|
+
else if ((vault.hostRating ?? 0) < VAULT_HOST_RATING) {
|
|
1141
|
+
fail(`the winCondition item "${winItem.name}" is matrix-plane paydata in hasNode room "${winItem.room}" whose hostRating is ${vault.hostRating ?? 'unset'} -- a vault defends itself with its own ice, so give that room a hostRating of ${VAULT_HOST_RATING} or more. An undefended vault is a free win.`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
// NO NPC IS ICE. The same ruling from the other side: a model that
|
|
1145
|
+
// still writes a "matrix"-plane person is writing a voice for a
|
|
1146
|
+
// program. normalizeSkeleton drops them with a note; this holds the
|
|
1147
|
+
// line for anything that reaches the validator by another road.
|
|
1148
|
+
for (const npc of skeleton.npcs) {
|
|
1149
|
+
if (npc.plane === 'matrix') {
|
|
1150
|
+
fail(`npc "${npc.name}" is "matrix"-plane -- ice is not a person. Remove it; the host over its room fields its own countermeasures at its hostRating.`);
|
|
1126
1151
|
}
|
|
1127
1152
|
}
|
|
1128
1153
|
// EVERY HOST HAS A PURPOSE AND HOLDS SOMETHING (N4erz3f63MiCZLkDE,
|
|
@@ -1153,10 +1178,9 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
1153
1178
|
if (!holds) {
|
|
1154
1179
|
fail(`room "${room.name}" is a host (${room.hostPurpose}) with NOTHING inside it -- no "matrix"-plane item placed in that room. A host holds what its business protects: put at least one matrix-plane item there (the ledger, the client list, the camera archive), or drop hasNode.`);
|
|
1155
1180
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
}
|
|
1181
|
+
// "Ice stationed on it" used to be the third requirement. It is
|
|
1182
|
+
// now the host's own rating (see the vault rule above): a host
|
|
1183
|
+
// defends itself, and a person written as its ice is refused.
|
|
1160
1184
|
}
|
|
1161
1185
|
// Lock-and-key reachability. CRITICAL: edges are BIDIRECTIONAL -- the
|
|
1162
1186
|
// engine mirrors every declared exit into a shared two-way Door
|
|
@@ -1568,6 +1592,28 @@ export function normalizeSkeleton(skeleton) {
|
|
|
1568
1592
|
delete room.nodeAccess;
|
|
1569
1593
|
notes.push(`dropped the host over "${room.name}" -- it held no matrix-plane file, and a host with nothing in it should not be there.`);
|
|
1570
1594
|
}
|
|
1595
|
+
// ICE IS NOT A PERSON (player ruling 2026-09-13, "mute all ice"). The
|
|
1596
|
+
// prompt no longer asks for a "matrix"-plane NPC and validateSkeleton
|
|
1597
|
+
// refuses one; a model that writes one anyway is writing a voice for
|
|
1598
|
+
// a program, and the cheap repair is to drop it and let the host's
|
|
1599
|
+
// own rating do the guarding. The vault host gets that rating here
|
|
1600
|
+
// when the model forgot it -- clamp, don't burn a regeneration.
|
|
1601
|
+
const constructs = (skeleton.npcs ?? []).filter(n => n.plane === 'matrix');
|
|
1602
|
+
if (constructs.length > 0) {
|
|
1603
|
+
skeleton.npcs = skeleton.npcs.filter(n => n.plane !== 'matrix');
|
|
1604
|
+
for (const n of constructs) {
|
|
1605
|
+
notes.push(`normalized: dropped npc "${n.name}" -- it was "matrix"-plane ice, and a host fields its own countermeasures at its hostRating rather than a person in a costume.`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const winItem = (skeleton.items ?? []).find(i => i.name === skeleton.winCondition?.item);
|
|
1609
|
+
if (winItem?.plane === 'matrix' && winItem.room) {
|
|
1610
|
+
const vault = (skeleton.rooms ?? []).find(r => r.name === winItem.room);
|
|
1611
|
+
if (vault?.hasNode && (vault.hostRating ?? 0) < VAULT_HOST_RATING) {
|
|
1612
|
+
const was = vault.hostRating === undefined ? 'no hostRating' : `hostRating ${vault.hostRating}`;
|
|
1613
|
+
vault.hostRating = VAULT_HOST_RATING;
|
|
1614
|
+
notes.push(`normalized: the vault host over "${vault.name}" had ${was}; raised it to ${VAULT_HOST_RATING} so it fields ice worth the paydata.`);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1571
1617
|
// NOBODY MINDING THE STORE (playtest 2026-08-25: "the flooded
|
|
1572
1618
|
// undermarket could use some details -- it's just blank"). It was the
|
|
1573
1619
|
// district's only commerce room with neither a vendor nor stock: a
|
|
@@ -8,7 +8,7 @@ import { clampDeviceKind } from '../utilities/affordances.js';
|
|
|
8
8
|
import { GenerationCapture } from '../utilities/generation-capture.js';
|
|
9
9
|
import { fetchCanonContext } from '../utilities/canon-lore.js';
|
|
10
10
|
import { SceneSynthesizer } from './scene-factory.js';
|
|
11
|
-
import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, } from './scene-chunks.js';
|
|
11
|
+
import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, VAULT_HOST_RATING, } from './scene-chunks.js';
|
|
12
12
|
// The fixer name convention and the player dossier live with the prompt
|
|
13
13
|
// builders now (see scene-chunks.ts); re-exported so the existing
|
|
14
14
|
// import sites (call.ts, game.ts) keep working unchanged.
|
|
@@ -751,9 +751,11 @@ export class SceneSeedGenerator {
|
|
|
751
751
|
if (!room?.hasNode) {
|
|
752
752
|
throw new Error(`Matrix-plane win condition item "${wc.item}" is in room "${item.room}", which has no "hasNode": true -- matrix files can only exist on a host. Mark that room hasNode.`);
|
|
753
753
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
754
|
+
// THE HOST GUARDS ITSELF (2026-09-13): this used to demand an ice
|
|
755
|
+
// NPC; a host's countermeasures are its own programs at its rating
|
|
756
|
+
// (p.247), and normalizeSkeleton raises a vault to the floor.
|
|
757
|
+
if ((room.hostRating ?? 0) < VAULT_HOST_RATING) {
|
|
758
|
+
throw new Error(`Matrix-plane win condition item "${wc.item}" sits in hasNode room "${item.room}" whose hostRating is ${room.hostRating ?? 'unset'} -- a vault defends itself with its own ice; give that room a hostRating of ${VAULT_HOST_RATING} or more. An undefended data vault makes the run a free win.`);
|
|
757
759
|
}
|
|
758
760
|
}
|
|
759
761
|
/**
|
|
@@ -935,6 +935,29 @@ description text.`;
|
|
|
935
935
|
combatPhaseLocked() {
|
|
936
936
|
return !!this._scene?.encounterFor?.(this);
|
|
937
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* ICE HAS NO VOICE AND NO MIND OF ITS OWN (player ruling 2026-09-13,
|
|
940
|
+
* from Vex's Borrowed Time log: "Patrol IC: That chip belongs to
|
|
941
|
+
* Corvid. Put it back on the desk, nice and slow" -- four programs
|
|
942
|
+
* bargaining over a chip in four voices, and Killer IC "assembling
|
|
943
|
+
* in the back room" after the persona had already jacked out).
|
|
944
|
+
*
|
|
945
|
+
* SR5 p.247: IC is "mercilessly efficient, not very bright" -- it
|
|
946
|
+
* finds, disables, destroys and repels, and nothing in the books has
|
|
947
|
+
* it speak. A spawned program (icKind) and a seeded matrix-plane
|
|
948
|
+
* construct are the same thing to the engine: driven by dice
|
|
949
|
+
* (utilities/ic.ts), never by the model. No speech, no reflection,
|
|
950
|
+
* no exposition, and no listening either -- there is nothing for a
|
|
951
|
+
* stimulus to wake.
|
|
952
|
+
*/
|
|
953
|
+
isConstruct() {
|
|
954
|
+
return this.icKind !== undefined || this.plane === 'matrix';
|
|
955
|
+
}
|
|
956
|
+
/** Every AI entry point asks this: a construct never runs the model,
|
|
957
|
+
* and anyone in a Combat Turn yields to the turn loop. */
|
|
958
|
+
aiSuspended() {
|
|
959
|
+
return this.isConstruct() || this.combatPhaseLocked();
|
|
960
|
+
}
|
|
938
961
|
/**
|
|
939
962
|
* One verb, executed now, no reflection -- the brain's way of acting
|
|
940
963
|
* inside an Action Phase. Returns the command's own result so the
|
|
@@ -963,6 +986,10 @@ description text.`;
|
|
|
963
986
|
this.logger.write(`${this.name} is gone from the world -- dropping queued action "${commandString}".`);
|
|
964
987
|
return;
|
|
965
988
|
}
|
|
989
|
+
if (this.isConstruct()) {
|
|
990
|
+
this.logger.write(`${this.name} is ice -- the host's dice act for it; dropping "${commandString}".`);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
966
993
|
if (this.combatPhaseLocked()) {
|
|
967
994
|
this.logger.write(`${this.name} is in a Combat Turn -- the turn loop acts for them; dropping "${commandString}".`);
|
|
968
995
|
return;
|
|
@@ -995,6 +1022,10 @@ description text.`;
|
|
|
995
1022
|
// removed shell's AI.
|
|
996
1023
|
if (this.deathClaimed)
|
|
997
1024
|
return;
|
|
1025
|
+
// NOR DOES ICE (isConstruct): a program has no conversation to
|
|
1026
|
+
// hold and no history worth keeping.
|
|
1027
|
+
if (this.isConstruct())
|
|
1028
|
+
return;
|
|
998
1029
|
// NEITHER DO THE UNCONSCIOUS. A knocked-out actor now STAYS in the
|
|
999
1030
|
// scene so they can be brought round (player ruling 2026-08-25) --
|
|
1000
1031
|
// which means every AI entry point has to check, or the body on the
|
|
@@ -1113,6 +1144,10 @@ description text.`;
|
|
|
1113
1144
|
// Same cross-plane blindness as hear() above.
|
|
1114
1145
|
if (!this.canPerceive(actor))
|
|
1115
1146
|
return;
|
|
1147
|
+
// Ice watches with the host's dice, not with a model (see
|
|
1148
|
+
// isConstruct): what it notices, utilities/ic.ts rolls for.
|
|
1149
|
+
if (this.isConstruct())
|
|
1150
|
+
return;
|
|
1116
1151
|
// The unconscious see nothing (see hear()): a KO'd actor stays in
|
|
1117
1152
|
// the scene now, and must not react to what happens over it.
|
|
1118
1153
|
if (this.isIncapacitated())
|
|
@@ -1428,8 +1463,8 @@ description text.`;
|
|
|
1428
1463
|
this.logger.write(`${this.name} is gone from the world -- reflect chain ends.`);
|
|
1429
1464
|
return;
|
|
1430
1465
|
}
|
|
1431
|
-
if (this.
|
|
1432
|
-
this.logger.write(`${this.name} is in a Combat Turn -- reflect chain yields to the turn loop.`);
|
|
1466
|
+
if (this.aiSuspended()) {
|
|
1467
|
+
this.logger.write(`${this.name} ${this.isConstruct() ? 'is ice -- no reflect chain' : 'is in a Combat Turn -- reflect chain yields to the turn loop'}.`);
|
|
1433
1468
|
return;
|
|
1434
1469
|
}
|
|
1435
1470
|
// THE GRIND BREAKER. Record what just happened, then refuse to keep
|
|
@@ -1609,8 +1644,8 @@ ${worldEventsSummary}
|
|
|
1609
1644
|
: [];
|
|
1610
1645
|
}
|
|
1611
1646
|
async respondTo(actor, message) {
|
|
1612
|
-
if (this.
|
|
1613
|
-
this.logger.write(`${this.name} is in a Combat Turn -- no AI reaction to "${message.slice(0, 40)}".`);
|
|
1647
|
+
if (this.aiSuspended()) {
|
|
1648
|
+
this.logger.write(`${this.name} ${this.isConstruct() ? 'is ice' : 'is in a Combat Turn'} -- no AI reaction to "${message.slice(0, 40)}".`);
|
|
1614
1649
|
return;
|
|
1615
1650
|
}
|
|
1616
1651
|
this.logger.write('Attempting to respond');
|
|
@@ -59,6 +59,9 @@ export function alarmedStaff(scene, player) {
|
|
|
59
59
|
.filter((a) => a instanceof NPC)
|
|
60
60
|
.filter(npc => !isEphemeral(npc))
|
|
61
61
|
.filter(npc => npc.allyOf === undefined)
|
|
62
|
+
// Ice is not staff: a program answers to the host's dice
|
|
63
|
+
// (utilities/ic.ts), never to an alarm beat (NPC.isConstruct).
|
|
64
|
+
.filter(npc => !npc.isConstruct())
|
|
62
65
|
.filter(npc => !npc.isIncapacitated());
|
|
63
66
|
}
|
|
64
67
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.181.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.",
|