@maka/maka-cli 5.186.0 → 5.188.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/hack.js +27 -2
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +116 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +31 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +26 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ic-actors.js +48 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.188.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.",
|
|
@@ -556,6 +556,31 @@ export class HackCommand extends BypassCommand {
|
|
|
556
556
|
this.logger.meta(`${defense.label} (DR ${device.rating}): ${formatRoll(defenseRoll)} -- net ${netHits >= 0 ? '+' : ''}${netHits}`);
|
|
557
557
|
}
|
|
558
558
|
const godLines = this.godBill(overwatchFromDefense(defenseRoll.hits), `device intrusion (${mode})`);
|
|
559
|
+
// WHY THE KEYS YOU ALREADY HOLD DID NOT OPEN THIS (u8u2koi3THQM9dzSz:
|
|
560
|
+
// "I _OWN_ the host with 3 marks, I should be able to hack the
|
|
561
|
+
// maglock, or simply disable it").
|
|
562
|
+
//
|
|
563
|
+
// Checked against the books rather than the report's own reading:
|
|
564
|
+
// marks share ONE WAY. "If you get a mark on a slave you also get a
|
|
565
|
+
// mark on the master ... This doesn't work both ways" (p.233). Three
|
|
566
|
+
// marks on the host are three marks on the HOST; the lock hanging off
|
|
567
|
+
// it is still its own icon with its own monitor, and Control Device
|
|
568
|
+
// (p.238) counts marks on the thing being controlled. So the engine
|
|
569
|
+
// was right to make them roll -- and said nothing at all about why,
|
|
570
|
+
// which is the part that earned the report. A rule the player cannot
|
|
571
|
+
// see is indistinguishable from a bug.
|
|
572
|
+
//
|
|
573
|
+
// The line also names the move that DOES cash those marks in: inside
|
|
574
|
+
// the host you are directly connected to everything on its WAN, and
|
|
575
|
+
// a directly-connected slave "cannot use its master's ratings to
|
|
576
|
+
// defend itself" (p.233) -- which is exactly the pool that just beat
|
|
577
|
+
// them. Only said when they actually hold host marks and are still
|
|
578
|
+
// outside; from in here it would be telling them to walk through the
|
|
579
|
+
// door they are standing in.
|
|
580
|
+
const hostMarksHeld = viaWan && host ? host.hostMarksBy.get(actor.name) ?? 0 : 0;
|
|
581
|
+
const wanNote = hostMarksHeld > 0
|
|
582
|
+
? hint(` (Your ${hostMarksHeld} mark${hostMarksHeld === 1 ? '' : 's'} on ${hostLabel(host)} buy nothing against the lock itself -- marks run slave-to-master, never master-to-slave (p.233). "enter ${hostLabel(host)}": from inside its WAN you are directly connected, and ${device.name} has to defend on DR ${device.rating} alone.)`)
|
|
583
|
+
: '';
|
|
559
584
|
if (netHits <= 0) {
|
|
560
585
|
if (mode === 'sleaze') {
|
|
561
586
|
// A blown Sleaze is NOTICED (p.236) -- and a slaved device that
|
|
@@ -565,14 +590,14 @@ export class HackCommand extends BypassCommand {
|
|
|
565
590
|
host.hostAlert = true;
|
|
566
591
|
this.scene.addWorldEvent(`${device.name} flags a bad handshake -- ${hostLabel(host, { capital: true })} is awake.`);
|
|
567
592
|
this.scene.updateStatus();
|
|
568
|
-
return [`${device.name} refuses the key and SAYS SO -- the node it answers to knows something is in here
|
|
593
|
+
return [`${device.name} refuses the key and SAYS SO -- the node it answers to knows something is in here.${wanNote}`, ...godLines].join('\n');
|
|
569
594
|
}
|
|
570
595
|
// A loose device has no host to wake; its owner still gets the
|
|
571
596
|
// mark on you a blown Sleaze hands over (p.240).
|
|
572
597
|
this.scene.updateStatus();
|
|
573
598
|
return [`${device.name} refuses the key and SAYS SO -- whoever owns it has your icon on file now.`, ...godLines].join('\n');
|
|
574
599
|
}
|
|
575
|
-
return [`${device.name} holds against the smash -- its firewall never even flexes. Nobody upstairs noticed
|
|
600
|
+
return [`${device.name} holds against the smash -- its firewall never even flexes. Nobody upstairs noticed.${wanNote}`, ...godLines].join('\n');
|
|
576
601
|
}
|
|
577
602
|
const now = Math.min(MAX_MARKS, held + declared);
|
|
578
603
|
device.marksBy.set(actor.name, now);
|
|
@@ -733,6 +733,15 @@ For EVERY item in the skeleton, expand it into a full item ("name" and "category
|
|
|
733
733
|
Clue requirements (mechanically validated -- the exact phrase must appear in "details"):
|
|
734
734
|
${clueSpecs || '- (no clue items in this scene)'}
|
|
735
735
|
|
|
736
|
+
THE ONLY PRIZE IN THIS WORLD is "${skeleton.winCondition?.item ?? ''}" -- the item
|
|
737
|
+
"winCondition" is for. No other item may present itself as the thing the client wants: not
|
|
738
|
+
"it's what ${skeleton.winCondition?.toNpc || skeleton.client || 'the Johnson'} is looking
|
|
739
|
+
for", not "this is what the job is about", not a second set of goods that reads like the
|
|
740
|
+
objective wearing a different name. The engine stars exactly one item and pays out on
|
|
741
|
+
exactly one; a second item that CLAIMS the role sends the runner home with the wrong thing
|
|
742
|
+
in their pack, believing they blew the job they just finished. Loot is loot -- say what it
|
|
743
|
+
IS, never what it is worth to the client.
|
|
744
|
+
|
|
736
745
|
THE ONLY CODES IN THIS WORLD are the clue phrases listed just above. No other item may
|
|
737
746
|
carry a code-shaped string -- no serial numbers, no model numbers, no unit or locker or
|
|
738
747
|
apartment numbers, no "access code" on a scrap of paper that isn't a listed clue. A player
|
|
@@ -2046,6 +2055,112 @@ export function ensureCredentialKindAgrees(details) {
|
|
|
2046
2055
|
}
|
|
2047
2056
|
return notes;
|
|
2048
2057
|
}
|
|
2058
|
+
/**
|
|
2059
|
+
* ONE PRIZE, AND THE PROSE HAS TO AGREE (aSpARWJXXieMAL4eM, 2026-09-14:
|
|
2060
|
+
* "the item marked with the star as the mission objective was 'The
|
|
2061
|
+
* Scout's Contact Sheet'. But the mission was clearly about camera
|
|
2062
|
+
* data ... When I returned to the Hub, I had 'Developed Negatives',
|
|
2063
|
+
* which state it's what Mr. Leger is looking for. It's like the two
|
|
2064
|
+
* items got mis labeld as to what was important.").
|
|
2065
|
+
*
|
|
2066
|
+
* Nothing was mislabelled in the STATE. winCondition.item was the
|
|
2067
|
+
* contact sheet, the star followed it (planes.ts objectiveMark), the
|
|
2068
|
+
* completion check accepted it, and the payout landed. What went wrong
|
|
2069
|
+
* is that the items pass wrote a SECOND item's flavour as though that
|
|
2070
|
+
* one were the job -- so the run had one prize by the engine's reckoning
|
|
2071
|
+
* and a different one by its own prose, and the player believed the
|
|
2072
|
+
* prose. That is worse than a wrong star: they finished the job
|
|
2073
|
+
* convinced they had failed it.
|
|
2074
|
+
*
|
|
2075
|
+
* Same family as payoutSummary and heatSummary in npc.ts -- the model
|
|
2076
|
+
* inventing a fact the engine already owns -- and the same answer:
|
|
2077
|
+
* state the fact in the prompt (buildItemsPrompt, "THE ONLY PRIZE IN
|
|
2078
|
+
* THIS WORLD"), and enforce it here so a draft that ignores the rule
|
|
2079
|
+
* costs nothing.
|
|
2080
|
+
*
|
|
2081
|
+
* SURGERY, narrowly, and this file's own preference is against it:
|
|
2082
|
+
* ensureCredentialKindAgrees corrects "by ADDING a plain sentence rather
|
|
2083
|
+
* than rewriting generated prose". That works when the generated line is
|
|
2084
|
+
* merely incomplete. It cannot work here, because the line is FALSE --
|
|
2085
|
+
* appending "but the client doesn't want this" under "this is what the
|
|
2086
|
+
* client wants" leaves prose that argues with itself, which is exactly
|
|
2087
|
+
* the confusion the report describes. So the false sentence comes out,
|
|
2088
|
+
* whole, and everything around it is left byte-identical.
|
|
2089
|
+
*
|
|
2090
|
+
* THE BAR IS THREE THINGS IN ONE SENTENCE, because two is not enough to
|
|
2091
|
+
* tell a claim from colour: someone the job is ABOUT (the client by
|
|
2092
|
+
* name, or a generic for them), a verb of WANTING, and a reference back
|
|
2093
|
+
* to THIS item. "Ledger drinks alone and wants nothing from anybody"
|
|
2094
|
+
* keeps all three words apart and survives; "it's what Mr. Ledger is
|
|
2095
|
+
* looking for" does not.
|
|
2096
|
+
*/
|
|
2097
|
+
const PRIZE_WANTING = /\b(look(?:s|ing|ed)?\s+for|after\s+(?:this|it|these|them)|want(?:s|ed|ing)?|need(?:s|ed|ing)?|ask(?:s|ed|ing)?\s+for|pay(?:s|ing)?\s+for|hir(?:ed|ing)\s+\w+\s+for|sent\s+\w+\s+for|came\s+for|is\s+the\s+(?:job|prize|objective|target))\b/i;
|
|
2098
|
+
const PRIZE_SELF = /\b(this|these|those|it|its|it's|the\s+one)\b/i;
|
|
2099
|
+
const PRIZE_GENERIC_CLIENT = /\b(the\s+client|the\s+johnson|mr\.?\s+johnson|your\s+employer|the\s+employer|the\s+buyer|the\s+job|the\s+run|the\s+contract)\b/i;
|
|
2100
|
+
/**
|
|
2101
|
+
* Sentence-ish split that keeps the terminator with its sentence.
|
|
2102
|
+
*
|
|
2103
|
+
* IT DOES HAVE TO HANDLE "Mr." -- this comment first said it didn't,
|
|
2104
|
+
* and the first test written against it failed. Every client in this
|
|
2105
|
+
* game is "a Mr. Johnson" (canon p.27-28), so the one sentence this
|
|
2106
|
+
* function exists to read -- "It's what Mr. Ledger is looking for." --
|
|
2107
|
+
* is precisely the one a naive split cuts in half, into two fragments
|
|
2108
|
+
* neither of which claims anything. The titles below are the ones a
|
|
2109
|
+
* Shadowrun scene actually writes; a single capital initial ("J.
|
|
2110
|
+
* Ledger") goes with them.
|
|
2111
|
+
*/
|
|
2112
|
+
const SENTENCE_BREAK = /(?<!\b(?:Mr|Mrs|Ms|Dr|Sr|Jr|St|Prof|Rev|Inc|Ltd|Co|vs|No|[A-Z])\.)(?<=[.!?])\s+/;
|
|
2113
|
+
function prizeSentences(text) {
|
|
2114
|
+
return text.split(SENTENCE_BREAK).filter(s => s.length > 0);
|
|
2115
|
+
}
|
|
2116
|
+
function claimsThePrize(sentence, clientNames) {
|
|
2117
|
+
if (!PRIZE_WANTING.test(sentence) || !PRIZE_SELF.test(sentence))
|
|
2118
|
+
return false;
|
|
2119
|
+
const named = clientNames.some(n => n.length > 0 && sentence.toLowerCase().includes(n.toLowerCase()));
|
|
2120
|
+
return named || PRIZE_GENERIC_CLIENT.test(sentence);
|
|
2121
|
+
}
|
|
2122
|
+
export function ensureOnePrize(skeleton, details) {
|
|
2123
|
+
const notes = [];
|
|
2124
|
+
const prize = (skeleton.winCondition?.item ?? '').trim().toLowerCase();
|
|
2125
|
+
if (prize.length === 0)
|
|
2126
|
+
return notes;
|
|
2127
|
+
// The people a claim can be made on behalf of: the client who holds
|
|
2128
|
+
// the meet, and whoever the goods are owed to (usually the same
|
|
2129
|
+
// person, sometimes not -- a courier job delivers to a third party).
|
|
2130
|
+
// Last names alone are how these lines actually read ("Ledger takes
|
|
2131
|
+
// the sleeve"), so each name contributes its words too.
|
|
2132
|
+
const clientNames = [skeleton.client ?? '', skeleton.winCondition?.toNpc ?? '']
|
|
2133
|
+
.flatMap(n => [n, ...n.split(/\s+/)])
|
|
2134
|
+
.map(n => n.replace(/[^\w\s'-]/g, '').trim())
|
|
2135
|
+
.filter(n => n.length >= 4);
|
|
2136
|
+
for (const item of details.items ?? []) {
|
|
2137
|
+
if ((item.name ?? '').trim().toLowerCase() === prize)
|
|
2138
|
+
continue;
|
|
2139
|
+
for (const field of ['description', 'details']) {
|
|
2140
|
+
const text = (item[field] ?? '').trim();
|
|
2141
|
+
if (text.length === 0)
|
|
2142
|
+
continue;
|
|
2143
|
+
const kept = prizeSentences(text).filter(s => !claimsThePrize(s, clientNames));
|
|
2144
|
+
if (kept.length === prizeSentences(text).length)
|
|
2145
|
+
continue;
|
|
2146
|
+
const rebuilt = kept.join(' ').trim();
|
|
2147
|
+
// A description is always-visible flavour and cannot be blank --
|
|
2148
|
+
// an item with no line at all reads as a generator failure. A
|
|
2149
|
+
// "details" that empties out simply goes away, which is what an
|
|
2150
|
+
// item with nothing more to say should do.
|
|
2151
|
+
if (field === 'description') {
|
|
2152
|
+
item.description = rebuilt.length > 0
|
|
2153
|
+
? rebuilt
|
|
2154
|
+
: `Someone's property once, and worth carrying if nobody's looking.`;
|
|
2155
|
+
}
|
|
2156
|
+
else {
|
|
2157
|
+
item.details = rebuilt.length > 0 ? rebuilt : undefined;
|
|
2158
|
+
}
|
|
2159
|
+
notes.push(`normalized: item "${item.name}" claimed in its ${field} to be what the job is for; "${skeleton.winCondition.item}" is, so the claim was cut.`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return notes;
|
|
2163
|
+
}
|
|
2049
2164
|
/**
|
|
2050
2165
|
* RESOLVE A NAME ONE LIST WROTE AGAINST THE LIST THAT OWNS IT.
|
|
2051
2166
|
*
|
|
@@ -2110,6 +2225,7 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
2110
2225
|
// The answer has to be legible before anything is assembled from it.
|
|
2111
2226
|
ensureClueDetails(skeleton, details);
|
|
2112
2227
|
ensureCredentialKindAgrees(details);
|
|
2228
|
+
ensureOnePrize(skeleton, details);
|
|
2113
2229
|
const roomDetail = new Map(details.rooms.map(r => [r.name, r]));
|
|
2114
2230
|
const npcDetail = new Map(details.npcs.map(n => [n.name, n]));
|
|
2115
2231
|
const deviceDetail = new Map(details.devices.map(d => [d.name, d]));
|
|
@@ -13,6 +13,7 @@ import { Logger } from '../utilities/logger.js';
|
|
|
13
13
|
import { migrateLegacySeed } from './seed-migration.js';
|
|
14
14
|
import { Device } from '../models/device.js';
|
|
15
15
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
16
|
+
import { adoptSeededIce } from '../utilities/ic-actors.js';
|
|
16
17
|
/**
|
|
17
18
|
* How many AI enrichment calls may be in flight at once.
|
|
18
19
|
*
|
|
@@ -252,6 +253,17 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
252
253
|
// architecture nobody wrote.
|
|
253
254
|
if (isIce && npc.currentLocation?.hasNode) {
|
|
254
255
|
npc.insideHost = npc.currentLocation;
|
|
256
|
+
// ...AND THE HOST HAS TO KNOW IT IS RUNNING IT (mPCh2FJykPsyJKmcc's
|
|
257
|
+
// bench, 2026-09-14). Standing inside the node was only half the
|
|
258
|
+
// adoption: nothing registered the construct with the Host, so it
|
|
259
|
+
// was ice by every reading except the one the host itself uses.
|
|
260
|
+
// The host then launched its own twin on the first Combat Turn
|
|
261
|
+
// (p.247 forbids two of a type), the construct swung with an
|
|
262
|
+
// Attack of 0 because "IC uses the Matrix attributes of its host"
|
|
263
|
+
// is read off icHost, and the grid view listed it twice. See
|
|
264
|
+
// adoptSeededIce for the three-for-one.
|
|
265
|
+
if (npc.currentLocation.host)
|
|
266
|
+
adoptSeededIce(npc, npc.currentLocation.host);
|
|
255
267
|
}
|
|
256
268
|
npc.scene = scene;
|
|
257
269
|
builtNpcs.push(npc);
|
|
@@ -458,6 +458,28 @@ export function splitSpokenLines(lines) {
|
|
|
458
458
|
rebuilt.push(`[COMMAND] say ${bracketed[1]}`);
|
|
459
459
|
continue;
|
|
460
460
|
}
|
|
461
|
+
// ...AND SO IS ONE FILED UNDER A TAG THE MODEL INVENTED
|
|
462
|
+
// (zQTWGJFWDXoLtiJX6, 2026-09-13: Whisper's whole sales line
|
|
463
|
+
// arrived as `[TALK] say Depends what you're buying or
|
|
464
|
+
// confessing. The credstick's hot and priced like it...` and
|
|
465
|
+
// printed into the world log verbatim, marker and all -- the
|
|
466
|
+
// reporter first read it as his OWN typing echoed back).
|
|
467
|
+
//
|
|
468
|
+
// Third sighting of one root: the model reaches for the bracket
|
|
469
|
+
// convention and gets the WORD inside it wrong. [say] was the
|
|
470
|
+
// second; this is the general case, and generalising beats
|
|
471
|
+
// adding [TALK] to a list that would need [DIALOGUE], [ACTION]
|
|
472
|
+
// and whatever the next model invents.
|
|
473
|
+
//
|
|
474
|
+
// The bound that holds every repair in this function holds here
|
|
475
|
+
// too: the VERB after the bracket must be `say`. `[go] north`
|
|
476
|
+
// stays exactly what it was -- only `say` carries free prose, so
|
|
477
|
+
// only `say` can be promoted without risking a silent act.
|
|
478
|
+
const tagged = t.match(/^\[\s*[A-Za-z][A-Za-z ._-]*\s*\]\s*say\s+(\S.*)$/i);
|
|
479
|
+
if (tagged) {
|
|
480
|
+
rebuilt.push(`[COMMAND] say ${tagged[1]}`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
461
483
|
// ...AND A LINE THAT IS NOTHING BUT SPEECH MARKS IS SPEECH
|
|
462
484
|
// (playtest 2026-08-27, reported twice in one session: 'the
|
|
463
485
|
// "Take a seat..." comment isn't showing as he said it. It's in
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hostConcealedFrom } from './jackpoint.js';
|
|
2
2
|
import { matchesHostName, hostLabel } from './grid-names.js';
|
|
3
|
+
import { fuzzyPickName } from './fuzzy-match.js';
|
|
3
4
|
/**
|
|
4
5
|
* THE GRID HAS NO STREETS (user canon check 2026-09-02; SR5 pp.217-218,
|
|
5
6
|
* p.246). Jacked in and outside a host, a persona is on its grid: "a
|
|
@@ -44,6 +45,28 @@ export function hostOver(room) {
|
|
|
44
45
|
* anywhere on the grid. Bare "host" (or nothing) means the one over the
|
|
45
46
|
* persona's vicinity, else nothing -- with several overhead, a bare word
|
|
46
47
|
* cannot pick.
|
|
48
|
+
*
|
|
49
|
+
* THE ALIAS LIST IS EXACT, AND EXACT IS NOT ENOUGH (qHwKnqEGrJFn3tmF7).
|
|
50
|
+
* grid-names.ts made the printer and the resolver agree, which fixed
|
|
51
|
+
* the lie -- but it left the player typing the label back BYTE FOR
|
|
52
|
+
* BYTE. A run whose room was called "The Grinning Idol -- Back Room"
|
|
53
|
+
* refused `mark Back Room host` and accepted only
|
|
54
|
+
* `mark The Grinning Idol -- Back Room host`; the reporter's word for
|
|
55
|
+
* it was "ridiculous", and they are right. A name you can read off the
|
|
56
|
+
* screen but not shorten is a name you have to transcribe.
|
|
57
|
+
*
|
|
58
|
+
* So: aliases first, unchanged -- an exact hit still resolves exactly,
|
|
59
|
+
* and the vicinity still wins ties. Only when nothing answers does the
|
|
60
|
+
* shared fuzzy matcher get a turn, over the names the map actually
|
|
61
|
+
* printed. It is the same resolver items and NPCs go through, with the
|
|
62
|
+
* same discipline: an AMBIGUOUS guess misses rather than picking a host
|
|
63
|
+
* at random, which matters more here than anywhere -- cracking the
|
|
64
|
+
* wrong node spends the action, wakes the wrong ice and cannot be
|
|
65
|
+
* taken back.
|
|
66
|
+
*
|
|
67
|
+
* The trailing "host"/"node" comes off first: it is part of the LABEL
|
|
68
|
+
* ("the Back Room host"), never part of the room's name, and leaving it
|
|
69
|
+
* on drags every score down by a word.
|
|
47
70
|
*/
|
|
48
71
|
export function resolveHostInReach(rooms, actor, query) {
|
|
49
72
|
const q = query.trim().toLowerCase();
|
|
@@ -56,7 +79,14 @@ export function resolveHostInReach(rooms, actor, query) {
|
|
|
56
79
|
const near = hostOver(gridVicinity(actor));
|
|
57
80
|
if (near && matchesHostName(q, near))
|
|
58
81
|
return near;
|
|
59
|
-
|
|
82
|
+
const exact = hosts.find(r => matchesHostName(q, r));
|
|
83
|
+
if (exact)
|
|
84
|
+
return exact;
|
|
85
|
+
const bare = q.replace(/\s*\b(hosts?|nodes?)\s*$/, '').trim();
|
|
86
|
+
if (bare.length === 0)
|
|
87
|
+
return undefined;
|
|
88
|
+
const picked = fuzzyPickName(bare, hosts.map(r => r.name));
|
|
89
|
+
return picked ? hosts.find(r => r.name === picked) : undefined;
|
|
60
90
|
}
|
|
61
91
|
/** Noise between the persona and a room's icons: 0 for the vicinity (and inside a host), -DISTRICT_NOISE dice elsewhere. */
|
|
62
92
|
export function noiseFor(actor, room) {
|
|
@@ -299,8 +299,23 @@ function hostInteriorIcons(scene, actor, room) {
|
|
|
299
299
|
// and that would put the wrong noun on a person.
|
|
300
300
|
// Inside a host, the roster is who is INSIDE it (matrix-roster.ts) -- a
|
|
301
301
|
// persona's body is never in this room.
|
|
302
|
+
// THE HOST'S OWN PROGRAMS ARE LISTED ONCE, BELOW, NOT TWICE, HERE AND
|
|
303
|
+
// BELOW (mPCh2FJykPsyJKmcc's bench, 2026-09-14). A running IC is an
|
|
304
|
+
// actor inside the host AND a name in room.runningIC, and both loops
|
|
305
|
+
// printed it -- in different words, with only the second offering the
|
|
306
|
+
// verbs, so one program read as two and the reader had to guess which
|
|
307
|
+
// line was real:
|
|
308
|
+
//
|
|
309
|
+
// ▲ Patrol IC #2 -- intrusion countermeasures, and you are inside its host.
|
|
310
|
+
// ▲ Patrol IC #2 -- hunting, and it has your icon. (0/10) ("attack ..." crash it)
|
|
311
|
+
//
|
|
312
|
+
// hostInteriorPanel already resolved this the right way round -- it
|
|
313
|
+
// skips `a.icHost === host` in its persona list -- so this matches it
|
|
314
|
+
// rather than inventing a third rule. Everything else inside the host
|
|
315
|
+
// still comes through here: a rival decker's persona, a companion, a
|
|
316
|
+
// construct nothing registered as the host's.
|
|
302
317
|
for (const a of (room.host ? personasInside(scene, room.host) : [])) {
|
|
303
|
-
if (a === actor)
|
|
318
|
+
if (a === actor || a.icHost === room.host)
|
|
304
319
|
continue;
|
|
305
320
|
if (a.plane === 'matrix' && sameHostSide(actor, a) && actor.canPerceive(a)) {
|
|
306
321
|
const companion = scene.ownerGame?.companions.find(c => c.npc === a);
|
|
@@ -350,11 +365,16 @@ function hostInteriorIcons(scene, actor, room) {
|
|
|
350
365
|
// game will not name, and a room that contradicts the damage log reads
|
|
351
366
|
// as broken rules rather than thin prose.
|
|
352
367
|
//
|
|
353
|
-
//
|
|
354
|
-
// in room.runningIC as names
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
368
|
+
// A VERB IS OFFERED NOW, where there is an actor to point it at. When
|
|
369
|
+
// this loop was written IC lived in room.runningIC as names only and
|
|
370
|
+
// nothing could target them, so it deliberately advertised nothing;
|
|
371
|
+
// ic-actors.ts made each running program an actor, and the line grew
|
|
372
|
+
// the monitor and the verbs to match. A kind with no actor behind it
|
|
373
|
+
// (a scene that cannot host actors at all) still gets named and still
|
|
374
|
+
// offers nothing, for the original reason.
|
|
375
|
+
//
|
|
376
|
+
// THIS IS THE ONE PLACE THE HOST'S OWN PROGRAMS ARE LISTED -- see the
|
|
377
|
+
// skip in the persona loop above.
|
|
358
378
|
for (const name of room.runningIC) {
|
|
359
379
|
const ice = room.host?.iceActors.get(name);
|
|
360
380
|
icons.push(`▲ ${ice?.name ?? name} -- hunting, and it has your icon.${ice ? ` (${ice.conditionSummary()}) ("thread resonance-spike ${ice.name}" / "attack ${ice.name}" crash it)` : ''}`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NPC } from '../models/npc.js';
|
|
2
2
|
import { CommandRegistry } from '../commands/command-registry.js';
|
|
3
3
|
import { Logger } from './logger.js';
|
|
4
|
+
import { IC_TYPES } from './ic.js';
|
|
4
5
|
/**
|
|
5
6
|
* IC AS ICONS (increment 5 of the Matrix position model, 2026-09-02).
|
|
6
7
|
* SR5 p.247: "Each IC program has a persona with its own Condition
|
|
@@ -95,4 +96,51 @@ export function crashedIC(host) {
|
|
|
95
96
|
}
|
|
96
97
|
return out;
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* ADOPT A SEEDED CONSTRUCT AS ONE OF ITS HOST'S PROGRAMS
|
|
101
|
+
* (mPCh2FJykPsyJKmcc's bench, 2026-09-14).
|
|
102
|
+
*
|
|
103
|
+
* scene-factory already settles what a matrix-plane NPC IS -- "a
|
|
104
|
+
* matrix-plane NPC in this engine IS intrusion countermeasures" -- and
|
|
105
|
+
* already stands it inside the node it guards. What it never did was
|
|
106
|
+
* tell the HOST. So a seeded construct was ice by every reading except
|
|
107
|
+
* the one the host itself uses, and three things went wrong at once,
|
|
108
|
+
* all of them visible in one bench transcript:
|
|
109
|
+
*
|
|
110
|
+
* 1. THE HOST LAUNCHED ITS TWIN. hostLaunchStep picks the first
|
|
111
|
+
* IC_TYPE not in `runningIC`, and a seeded "Patrol IC" was not in
|
|
112
|
+
* it -- so the host launched a second Patrol on the first Combat
|
|
113
|
+
* Turn and the player fought "Patrol IC" and "Patrol IC #2" at
|
|
114
|
+
* once. Straight against p.247: a host "can't launch more than one
|
|
115
|
+
* of each type of IC program at once."
|
|
116
|
+
* 2. IT ATTACKED WITH NO LIMIT. "IC uses the Matrix attributes of its
|
|
117
|
+
* host" (p.247) is read off `icHost` (Player.baseMatrixAttribute).
|
|
118
|
+
* Unset, a construct's Attack was 0, so every swing it took
|
|
119
|
+
* printed dice.ts's caller-bug diagnostic to the player:
|
|
120
|
+
* `[LIMIT IGNORED -- a caller computed one that is not a limit]`.
|
|
121
|
+
* 3. IT WAS LISTED TWICE, once as an icon inside the host and again
|
|
122
|
+
* off `runningIC` (grid-view.ts), with different words each time.
|
|
123
|
+
*
|
|
124
|
+
* One registration, three symptoms -- which is the argument for fixing
|
|
125
|
+
* it here at the seam rather than three times downstream.
|
|
126
|
+
*
|
|
127
|
+
* THE KIND IS THE CANON NAME WHEN THE SEED USED ONE. A construct called
|
|
128
|
+
* "Patrol IC" IS the host's Patrol, and registering it under its own
|
|
129
|
+
* name would leave the launch step free to add the real one beside it --
|
|
130
|
+
* the bug. A construct with a name of its own ("Sentinel", "the
|
|
131
|
+
* Gatekeeper") registers under that name: it counts against the host's
|
|
132
|
+
* Rating cap because it is a program the host is running, and it blocks
|
|
133
|
+
* no canon type, because it is not one.
|
|
134
|
+
*/
|
|
135
|
+
export function adoptSeededIce(npc, host) {
|
|
136
|
+
const canon = IC_TYPES.find(t => t.name.toLowerCase() === npc.name.trim().toLowerCase());
|
|
137
|
+
const kind = canon?.name ?? npc.name;
|
|
138
|
+
npc.icHost = host;
|
|
139
|
+
npc.icKind = kind;
|
|
140
|
+
npc.matrixPosition = { kind: 'host', host };
|
|
141
|
+
if (!host.iceActors.has(kind))
|
|
142
|
+
host.iceActors.set(kind, npc);
|
|
143
|
+
if (!host.runningIC.includes(kind))
|
|
144
|
+
host.runningIC.push(kind);
|
|
145
|
+
}
|
|
98
146
|
//# sourceMappingURL=ic-actors.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.188.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.",
|