@maka/maka-cli 5.173.0 → 5.175.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 +43 -16
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +16 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/repro-scene.js +15 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/room-factory.js +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +86 -5
- package/bundle/typescript/src/commands/game/sideQuest/factories/seed-migration.js +52 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +34 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +4 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +18 -53
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +5 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +17 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/jackpoint.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/repro-launch.js +27 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.175.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.",
|
|
@@ -490,14 +490,32 @@ export class HackCommand extends BypassCommand {
|
|
|
490
490
|
const found = hostsInReach(rooms, actor)
|
|
491
491
|
.map(r => ({ room: r, matches: matching(r) }))
|
|
492
492
|
.filter(x => x.matches.length === 1);
|
|
493
|
-
if (found.length
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
493
|
+
if (found.length === 1) {
|
|
494
|
+
host = found[0].room;
|
|
495
|
+
device = found[0].matches[0];
|
|
496
|
+
viaWan = true;
|
|
497
|
+
const onHost = host.hostMarksBy.get(actor.name) ?? 0;
|
|
498
|
+
if (onHost < 1) {
|
|
499
|
+
return `${device.name} is on the map, but it hangs off ${hostLabel(host)}'s WAN -- slaved, and a host's devices answer nobody who holds no mark on the host itself (p.233).${hint(` ("hack ${hostLabel(host)}" for a mark first; "enter" once you have one and the lock defends with its own DR ${device.rating} alone.)`)}`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
// A LOOSE DEVICE, NO HOST OVER IT (ZdhkP8mAP9QhpqFvA, the half
|
|
504
|
+
// the bench exposed 2026-09-12): the reporter's own fixture -- a
|
|
505
|
+
// wireless DR 2 maglock on a door in a room with NO host -- still
|
|
506
|
+
// fell through to "nothing on the grid answers", because this
|
|
507
|
+
// path only ever searched hosts' WANs. Every wireless device is
|
|
508
|
+
// its own icon on the grid (p.232), and an unslaved one defends
|
|
509
|
+
// with nothing but its own Device Rating (p.237-238: the rating
|
|
510
|
+
// stands in for the Mental attribute it lacks, and its Firewall
|
|
511
|
+
// is its rating) -- deviceDefensePool. It is found where the
|
|
512
|
+
// body stands: that is the room whose AR overlay drew the icon.
|
|
513
|
+
// A device with its wireless off has no icon and stays unfound.
|
|
514
|
+
const here = gridVicinity(actor);
|
|
515
|
+
const loose = hostOver(here) ? [] : matching(here).filter(d => d.wireless);
|
|
516
|
+
if (loose.length !== 1)
|
|
517
|
+
return null;
|
|
518
|
+
device = loose[0];
|
|
501
519
|
}
|
|
502
520
|
}
|
|
503
521
|
const held = device.marksBy.get(actor.name) ?? 0;
|
|
@@ -532,11 +550,17 @@ export class HackCommand extends BypassCommand {
|
|
|
532
550
|
if (mode === 'sleaze') {
|
|
533
551
|
// A blown Sleaze is NOTICED (p.236) -- and a slaved device that
|
|
534
552
|
// notices tells the host it hangs off.
|
|
535
|
-
host.hostAlert = true;
|
|
536
553
|
actor.sneaking = false;
|
|
537
|
-
|
|
554
|
+
if (host) {
|
|
555
|
+
host.hostAlert = true;
|
|
556
|
+
this.scene.addWorldEvent(`${device.name} flags a bad handshake -- ${hostLabel(host, { capital: true })} is awake.`);
|
|
557
|
+
this.scene.updateStatus();
|
|
558
|
+
return [`${device.name} refuses the key and SAYS SO -- the node it answers to knows something is in here.`, ...godLines].join('\n');
|
|
559
|
+
}
|
|
560
|
+
// A loose device has no host to wake; its owner still gets the
|
|
561
|
+
// mark on you a blown Sleaze hands over (p.240).
|
|
538
562
|
this.scene.updateStatus();
|
|
539
|
-
return [`${device.name} refuses the key and SAYS SO --
|
|
563
|
+
return [`${device.name} refuses the key and SAYS SO -- whoever owns it has your icon on file now.`, ...godLines].join('\n');
|
|
540
564
|
}
|
|
541
565
|
return [`${device.name} holds against the smash -- its firewall never even flexes. Nobody upstairs noticed.`, ...godLines].join('\n');
|
|
542
566
|
}
|
|
@@ -547,10 +571,12 @@ export class HackCommand extends BypassCommand {
|
|
|
547
571
|
// p.87-88 works it: "1 mark on the device and its master (the
|
|
548
572
|
// host)"). One, not `declared` -- the declaration bought marks on
|
|
549
573
|
// the lock; the host gets the one canon hands over with it.
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
574
|
+
if (host) {
|
|
575
|
+
const onHost = host.hostMarksBy.get(actor.name) ?? 0;
|
|
576
|
+
if (onHost < MAX_MARKS)
|
|
577
|
+
host.hostMarksBy.set(actor.name, onHost + 1);
|
|
578
|
+
}
|
|
579
|
+
this.logger.write(`Mark: ${actor.name} -> device ${device.name} (${now}/${MAX_MARKS}, ${mode}${host ? '' : ', loose'}).`);
|
|
554
580
|
const lines = [`${device.name} takes your key -- ${now} of ${MAX_MARKS} mark${now === 1 ? '' : 's'} on it now.`];
|
|
555
581
|
// BRUTE FORCE BURNS WHAT IT FORCES (p.238): 1 DV per two full net
|
|
556
582
|
// hits, resisted by Device Rating + Firewall. Unlike a host, a
|
|
@@ -566,7 +592,8 @@ export class HackCommand extends BypassCommand {
|
|
|
566
592
|
lines.push(` It dies open. Whatever it was holding shut is not shut any more.`);
|
|
567
593
|
}
|
|
568
594
|
}
|
|
569
|
-
host
|
|
595
|
+
if (host)
|
|
596
|
+
host.hostAlert = true;
|
|
570
597
|
actor.sneaking = false;
|
|
571
598
|
}
|
|
572
599
|
else {
|
|
@@ -415,5 +415,20 @@
|
|
|
415
415
|
// run's own transcript; an exit-serving spot that is furniture (a booth,
|
|
416
416
|
// a counter) sits beside its doorway instead of on it; a job's objective
|
|
417
417
|
// is clamped to a carriable weight at scene build.
|
|
418
|
-
|
|
418
|
+
// 1.49.0 (2026-09-12): A LOOSE LOCK IS HACKABLE. A wireless device in a
|
|
419
|
+
// room with no host over it is its own icon on the grid: "hack <it>"
|
|
420
|
+
// from the Matrix finds it where the body stands and it defends with its
|
|
421
|
+
// Device Rating alone (p.232, p.237-238) -- no more "nothing on the grid
|
|
422
|
+
// answers" for the reporter's DR 2 maglock. The bench fixture grows
|
|
423
|
+
// home / clinic / wounded / nuyen and npcs[].doc, so healing can be
|
|
424
|
+
// benched without a fight.
|
|
425
|
+
// 1.50.0 (2026-09-12): HOSTS HAVE A PURPOSE, AND THE HUB HAS NONE. A seed
|
|
426
|
+
// host carries hostPurpose (what the place runs on it) and hostSculpt
|
|
427
|
+
// (the inside as the owner built it); every generated host must declare
|
|
428
|
+
// the purpose, hold a matrix-plane file and station ice, and an empty
|
|
429
|
+
// one is dropped at normalize. The grid, the map and the interior print
|
|
430
|
+
// the purpose. A `hub: true` seed is stripped of hosts, ice and files on
|
|
431
|
+
// load -- the shipped hub (scene2) no longer stands one over the Bazaar
|
|
432
|
+
// or the Sim Den.
|
|
433
|
+
export const ENGINE_VERSION = '1.50.0';
|
|
419
434
|
//# sourceMappingURL=engine-version.js.map
|
|
@@ -295,7 +295,9 @@ export function buildReproScene(fixture) {
|
|
|
295
295
|
+ `${device.name} is fixed beside the doorway${barrier.open ? ', standing open' : ', holding it shut'}. `
|
|
296
296
|
+ `There is nothing else here, which is the point -- whatever is wrong is wrong at that door.`
|
|
297
297
|
: `A short concrete run with a door ${direction} of you and nothing else in it.`,
|
|
298
|
-
|
|
298
|
+
// fixture.home: the barrier room is the runner's own bed, so a rest
|
|
299
|
+
// here is the night rest.ts counts (vFMhSGwLhsfpiY7bs).
|
|
300
|
+
roomType: fixture.home ? 'Residence' : fixture.clinic ? 'Clinic' : 'Street',
|
|
299
301
|
size: level ? 'large' : 'medium',
|
|
300
302
|
spots,
|
|
301
303
|
levels: level ? [{ name: level.name, side: level.side ?? 'north', kind: level.kind ?? 'catwalk', climbOnly: level.climbOnly === true }] : undefined,
|
|
@@ -386,6 +388,18 @@ export function buildReproScene(fixture) {
|
|
|
386
388
|
},
|
|
387
389
|
};
|
|
388
390
|
});
|
|
391
|
+
// THE DOC'S HANDS (npcs[].doc): the medical skills treat.ts and
|
|
392
|
+
// street-doc.ts gate on, laid over the generic block above.
|
|
393
|
+
npcs.forEach((npc, i) => {
|
|
394
|
+
if (!npc.doc || npc.ice)
|
|
395
|
+
return;
|
|
396
|
+
const seed = npcSeeds[i];
|
|
397
|
+
seed.player.combat = {
|
|
398
|
+
...seed.player.combat,
|
|
399
|
+
logic: 6,
|
|
400
|
+
skills: { ...(seed.player.combat?.skills ?? {}), 'first-aid': 6, medicine: 6 },
|
|
401
|
+
};
|
|
402
|
+
});
|
|
389
403
|
const itemSeeds = items.map(it => ({
|
|
390
404
|
name: it.name,
|
|
391
405
|
description: it.description,
|
|
@@ -21,6 +21,9 @@ export class RoomFactory {
|
|
|
21
21
|
// below silently drops anything it does not list, which is how
|
|
22
22
|
// `owner` was lost once already.
|
|
23
23
|
hostRating: roomJson.hostRating,
|
|
24
|
+
hostGrid: roomJson.hostGrid,
|
|
25
|
+
hostPurpose: roomJson.hostPurpose,
|
|
26
|
+
hostSculpt: roomJson.hostSculpt,
|
|
24
27
|
warded: roomJson.warded,
|
|
25
28
|
watched: roomJson.watched,
|
|
26
29
|
// "At" spots ride through -- this whitelist SILENTLY DROPS any
|
|
@@ -351,10 +351,23 @@ ${JSON_ONLY}
|
|
|
351
351
|
// sprouting their own. Give every host a hostRating chosen for the
|
|
352
352
|
// place it actually is; a scene where every rating is 3 is as wrong
|
|
353
353
|
// as one where nothing is a host.
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
354
|
+
// EVERY HOST HAS A PURPOSE AND HOLDS SOMETHING (Data Trails
|
|
355
|
+
// p.167-168: a host is "designed to provide a select experience",
|
|
356
|
+
// and what it runs is what it keeps). A host with nothing in it
|
|
357
|
+
// is a host that should not be there: give every hasNode room a
|
|
358
|
+
// "hostPurpose", at least one "matrix"-plane item INSIDE it (the
|
|
359
|
+
// ledger, the client list, the camera archive, the door schedule
|
|
360
|
+
// -- whatever that business would protect, and a runner with the
|
|
361
|
+
// right skills could get), and an ice NPC stationed in it. Ice
|
|
362
|
+
// NPCs and matrix-plane items belong ONLY in hasNode rooms. A
|
|
363
|
+
// corner store whose host would hold nothing worth taking gets no
|
|
364
|
+
// host at all -- leave hasNode off and let the grid read thin there.
|
|
365
|
+
"hostPurpose"?: string, // REQUIRED with hasNode. One short phrase: what this place runs on
|
|
366
|
+
// its host and so what is worth protecting inside it -- "the
|
|
367
|
+
// clinic's patient records and drug ledger", "a bootleg sim parlor's
|
|
368
|
+
// library", "the gang's dead-drop board and door schedule", "a
|
|
369
|
+
// remote office for a shipping broker". The purpose dictates the
|
|
370
|
+
// host's sculpting (the rooms pass writes that from this).
|
|
358
371
|
"nodeAccess"?: "wireless", // Hosts are always reachable from the Matrix -- that is what
|
|
359
372
|
// makes them hosts. There is no air-gapped host: for a vault that
|
|
360
373
|
// must be entered in the flesh, put an OFFLINE SERVER device in the
|
|
@@ -587,11 +600,22 @@ a number in scenery is a number a player will carry to the nearest lock and type
|
|
|
587
600
|
someone stationed elsewhere. An NPC whose job is watching a barrier stands AT that
|
|
588
601
|
barrier's exit spot.
|
|
589
602
|
|
|
603
|
+
HOST SCULPTING -- for every room with "hasNode": true, ALSO write "hostSculpt": 1-2
|
|
604
|
+
sentences of what a persona sees INSIDE that host, built from the skeleton's
|
|
605
|
+
"hostPurpose". Form follows function: a host is a virtual world "designed to provide a
|
|
606
|
+
select experience to users" -- a shop's host sells, a clinic's files, a club's host is
|
|
607
|
+
the party, a broker's host is an office that never closes. The owner sets the physics:
|
|
608
|
+
"a maze, an open space, strange gravity or none at all, hot, cold, loud, quiet". Say what
|
|
609
|
+
the space is shaped like, how the icons and files in it appear (shelves, ledgers, pinned
|
|
610
|
+
notes, cold vaults, a bar with a back room), and what the theme, light, gravity or
|
|
611
|
+
temperature feel like. It is NOT the meat room: a persona never sees the furniture.
|
|
612
|
+
|
|
590
613
|
${JSON_ONLY}
|
|
591
614
|
[ { "name": string, "description": string, "roomType": string,
|
|
592
615
|
"size"?: string, "spots"?: [ { "name": string, "tags"?: string[], "place"?: string, "level"?: number } ],
|
|
593
616
|
"npcSpots"?: { }, "itemSpots"?: { }, "exitSpots"?: { },
|
|
594
|
-
"levels"?: [ { "name": string, "side": string, "kind": string } ]
|
|
617
|
+
"levels"?: [ { "name": string, "side": string, "kind": string } ],
|
|
618
|
+
"hostSculpt"?: string } ]
|
|
595
619
|
One entry per skeleton room, "name" copied exactly.${repairNote(previousError)}
|
|
596
620
|
`.trim();
|
|
597
621
|
}
|
|
@@ -1042,6 +1066,39 @@ export function validateSkeleton(skeleton, player, crew) {
|
|
|
1042
1066
|
fail(`the winCondition item "${winItem.name}" is matrix-plane paydata in hasNode room "${winItem.room}" with no ice guarding it -- station a "matrix"-plane NPC there. An undefended vault is a free win.`);
|
|
1043
1067
|
}
|
|
1044
1068
|
}
|
|
1069
|
+
// EVERY HOST HAS A PURPOSE AND HOLDS SOMETHING (N4erz3f63MiCZLkDE,
|
|
1070
|
+
// Deditri: "hosts should have purpose, if they don't have a purpose
|
|
1071
|
+
// then they shouldn't be there ... Whatever is in a host is worth
|
|
1072
|
+
// protecting, and that should be something the runner can get with
|
|
1073
|
+
// the right skills"). Canon agrees on the first half -- a host is "a
|
|
1074
|
+
// domain designed to provide a select experience" and its sculpting
|
|
1075
|
+
// follows its function (Data Trails p.167-168) -- and the engine's own
|
|
1076
|
+
// host model supplies the second: what a host holds is its files
|
|
1077
|
+
// (Host.files), sealed until it is cracked. So a hasNode room in a
|
|
1078
|
+
// generated run needs all three: a hostPurpose, a matrix-plane item
|
|
1079
|
+
// inside it, and ice stationed on it. The old prompt allowed a
|
|
1080
|
+
// "corner-store host with no paydata in it" that "costs the scene
|
|
1081
|
+
// nothing"; that is the empty architecture the report is about.
|
|
1082
|
+
//
|
|
1083
|
+
// At the SKELETON, because the skeleton owns hasNode, item planes and
|
|
1084
|
+
// NPC planes -- an items or npcs repair could never add the flag.
|
|
1085
|
+
// Worded to route to 'skeleton' through classifyRepair (no "items[",
|
|
1086
|
+
// no "npcs[", no "code"/"description"/"dialogue").
|
|
1087
|
+
for (const room of skeleton.rooms) {
|
|
1088
|
+
if (!room.hasNode)
|
|
1089
|
+
continue;
|
|
1090
|
+
if (typeof room.hostPurpose !== 'string' || room.hostPurpose.trim().length === 0) {
|
|
1091
|
+
fail(`room "${room.name}" has "hasNode": true but no "hostPurpose" -- every host is FOR something (what this place runs on it, and so what it protects). Give it a hostPurpose phrase, or drop hasNode if the place would run nothing worth taking.`);
|
|
1092
|
+
}
|
|
1093
|
+
const holds = skeleton.items.some(i => i.plane === 'matrix' && i.room === room.name);
|
|
1094
|
+
if (!holds) {
|
|
1095
|
+
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.`);
|
|
1096
|
+
}
|
|
1097
|
+
const guarded = skeleton.npcs.some(n => n.plane === 'matrix' && n.startLocation === room.name);
|
|
1098
|
+
if (!guarded) {
|
|
1099
|
+
fail(`room "${room.name}" is a host holding matrix-plane files with no ice on it -- station a "matrix"-plane NPC in that room. A host worth entering is a host that defends itself.`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1045
1102
|
// Lock-and-key reachability. CRITICAL: edges are BIDIRECTIONAL -- the
|
|
1046
1103
|
// engine mirrors every declared exit into a shared two-way Door
|
|
1047
1104
|
// (room.addExit writes both sides), so a one-sided skeleton exit still
|
|
@@ -1432,6 +1489,26 @@ export function normalizeSkeleton(skeleton) {
|
|
|
1432
1489
|
// Solvability first: a key sealed behind its own door is one moved
|
|
1433
1490
|
// object, not a reason to redraw the whole map.
|
|
1434
1491
|
notes.push(...freeStrandedRooms(skeleton));
|
|
1492
|
+
// AN EMPTY HOST IS NO HOST (N4erz3f63MiCZLkDE). The skeleton prompt
|
|
1493
|
+
// asks for a host only where it holds something; a model that flags
|
|
1494
|
+
// a hasNode room and puts nothing matrix-plane in it has written the
|
|
1495
|
+
// report's "empty architecture". Rather than burn a regeneration on
|
|
1496
|
+
// it, the flag comes off -- the grid reads thin there, which is the
|
|
1497
|
+
// honest rendering of a place that runs nothing worth taking. A host
|
|
1498
|
+
// that DOES hold something still has to declare its purpose and its
|
|
1499
|
+
// ice; validateSkeleton holds that line.
|
|
1500
|
+
for (const room of skeleton.rooms ?? []) {
|
|
1501
|
+
if (!room.hasNode)
|
|
1502
|
+
continue;
|
|
1503
|
+
const holds = (skeleton.items ?? []).some(i => i.plane === 'matrix' && i.room === room.name);
|
|
1504
|
+
if (holds)
|
|
1505
|
+
continue;
|
|
1506
|
+
delete room.hasNode;
|
|
1507
|
+
delete room.hostRating;
|
|
1508
|
+
delete room.hostPurpose;
|
|
1509
|
+
delete room.nodeAccess;
|
|
1510
|
+
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.`);
|
|
1511
|
+
}
|
|
1435
1512
|
// NOBODY MINDING THE STORE (playtest 2026-08-25: "the flooded
|
|
1436
1513
|
// undermarket could use some details -- it's just blank"). It was the
|
|
1437
1514
|
// district's only commerce room with neither a vendor nor stock: a
|
|
@@ -2004,6 +2081,10 @@ export function assembleScene(skeleton, details, playerName) {
|
|
|
2004
2081
|
hasNode: room.hasNode,
|
|
2005
2082
|
nodeAccess: room.nodeAccess,
|
|
2006
2083
|
hostRating: room.hostRating,
|
|
2084
|
+
// Purpose from the skeleton (structure), sculpt from the rooms
|
|
2085
|
+
// pass (prose) -- both only where there is a host to carry them.
|
|
2086
|
+
hostPurpose: room.hasNode && typeof room.hostPurpose === 'string' ? room.hostPurpose : undefined,
|
|
2087
|
+
hostSculpt: room.hasNode && typeof detail?.hostSculpt === 'string' ? detail.hostSculpt : undefined,
|
|
2007
2088
|
warded: room.warded,
|
|
2008
2089
|
watched: room.watched,
|
|
2009
2090
|
owner: room.owner,
|
|
@@ -39,8 +39,60 @@ function synthesiseCode(name) {
|
|
|
39
39
|
export function migrateLegacySeed(json) {
|
|
40
40
|
return migrateLegacySeedWithReport(json).seed;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* THE HUB CARRIES NO HOSTS (ruling 2026-09-12 on N4erz3f63MiCZLkDE).
|
|
44
|
+
*
|
|
45
|
+
* The hub is where a runner gets ready for a run -- shops, contacts,
|
|
46
|
+
* the bolt-hole -- and a host there had nothing to do: the shipped
|
|
47
|
+
* scene2 stood a Rating 3 host over Ratchet's counter and a Rating 5
|
|
48
|
+
* one over the Sim Den, with one file and one piece of ice between
|
|
49
|
+
* them, and the report ("hosts should have purpose, if they don't have
|
|
50
|
+
* a purpose then they shouldn't be there") was filed from exactly that
|
|
51
|
+
* room. Hosts with a purpose are a RUN's business (scene-chunks.ts
|
|
52
|
+
* validateSkeleton requires every generated host to declare one and
|
|
53
|
+
* hold something).
|
|
54
|
+
*
|
|
55
|
+
* Applied here, at the seed, rather than by editing scene2.json alone:
|
|
56
|
+
* every save EMBEDS its hub seed, so a returning runner's Purple Haze
|
|
57
|
+
* would otherwise keep the hosts the shipped file dropped. Same choke
|
|
58
|
+
* point, same contract as the puzzle migration -- pure, idempotent, and
|
|
59
|
+
* keyed on the seed's own `hub: true`, so a run seed (which may stand a
|
|
60
|
+
* host wherever it likes) is untouched.
|
|
61
|
+
*
|
|
62
|
+
* What goes: the host flags on every room, and with them the two
|
|
63
|
+
* things that only exist on a host -- matrix-plane NPCs (ice) and
|
|
64
|
+
* matrix-plane items (files). A file with no host to sit in is
|
|
65
|
+
* unreachable by design (utilities/planes.ts fileReach), and ice
|
|
66
|
+
* stationed in a room with no node has nothing to be inside; leaving
|
|
67
|
+
* either behind would be a quieter version of the same defect.
|
|
68
|
+
*/
|
|
69
|
+
export function stripHubHosts(json) {
|
|
70
|
+
if (json.hub !== true)
|
|
71
|
+
return json;
|
|
72
|
+
const hostFlags = (r) => r.hasNode !== undefined || r.hostRating !== undefined || r.nodeAccess !== undefined
|
|
73
|
+
|| r.hostGrid !== undefined || r.hostPurpose !== undefined || r.hostSculpt !== undefined;
|
|
74
|
+
const hasIce = (json.npcs ?? []).some(n => n.plane === 'matrix');
|
|
75
|
+
const hasFiles = (json.items ?? []).some(i => i.plane === 'matrix');
|
|
76
|
+
// IDEMPOTENCE: nothing to strip means the seed passes through as the
|
|
77
|
+
// same object (and migrate(migrate(x)) stays equal to migrate(x)).
|
|
78
|
+
if (!(json.rooms ?? []).some(hostFlags) && !hasIce && !hasFiles)
|
|
79
|
+
return json;
|
|
80
|
+
const rooms = (json.rooms ?? []).map(r => {
|
|
81
|
+
if (!hostFlags(r))
|
|
82
|
+
return r;
|
|
83
|
+
const { hasNode: _n, hostRating: _r, nodeAccess: _a, hostGrid: _g, hostPurpose: _p, hostSculpt: _s, ...rest } = r;
|
|
84
|
+
return rest;
|
|
85
|
+
});
|
|
86
|
+
const npcs = (json.npcs ?? []).filter(n => n.plane !== 'matrix');
|
|
87
|
+
const items = (json.items ?? []).filter(i => i.plane !== 'matrix');
|
|
88
|
+
return { ...json, rooms, npcs, items };
|
|
89
|
+
}
|
|
42
90
|
export function migrateLegacySeedWithReport(json) {
|
|
43
91
|
const fates = new Map();
|
|
92
|
+
// Hub hosts first, so the puzzle pass below sees the rooms as they
|
|
93
|
+
// will be played. Both passes are pure; order only matters for the
|
|
94
|
+
// spread that follows.
|
|
95
|
+
json = stripHubHosts(json);
|
|
44
96
|
const puzzles = json.puzzles ?? [];
|
|
45
97
|
// IDEMPOTENCE: a seed already migrated has no puzzles left to move,
|
|
46
98
|
// so the whole pass is a copy. Checked first so a re-migrated seed
|
|
@@ -52,11 +52,45 @@ export class Host {
|
|
|
52
52
|
patrolCountdown;
|
|
53
53
|
/** A haven (jackpoint.ts): never hunts, shows no rating, forgets cracks. */
|
|
54
54
|
sanctioned = false;
|
|
55
|
+
/**
|
|
56
|
+
* WHAT THE HOST IS FOR (N4erz3f63MiCZLkDE, Deditri: "hosts should have
|
|
57
|
+
* purpose; if they don't have a purpose then they shouldn't be there").
|
|
58
|
+
*
|
|
59
|
+
* Canon has form follow function: "a host is a domain -- a virtual
|
|
60
|
+
* world -- designed to provide a select experience to users", and "a
|
|
61
|
+
* shopping mall's host is going to be designed to enable sales and
|
|
62
|
+
* encourage return visits" (Data Trails p.167-168). A short phrase --
|
|
63
|
+
* "a bootleg sim parlor's ledger and library", "a clinic's patient
|
|
64
|
+
* records", "a go-gang's dead-drop board" -- that says what the place
|
|
65
|
+
* runs on this host, and so what is worth protecting inside it.
|
|
66
|
+
*
|
|
67
|
+
* `sculpt` is the inside as the owner built it: "the host can be a
|
|
68
|
+
* maze, an open space, have strange gravity or none at all, be hot,
|
|
69
|
+
* cold, loud, quiet" (SR5 p.219), "the owner of the host may adjust
|
|
70
|
+
* the laws of that reality to her whim" (DT p.167-168). Rendered as
|
|
71
|
+
* the interior's orientation line in place of the boilerplate.
|
|
72
|
+
*
|
|
73
|
+
* Both optional on the object -- a hand-authored seed may omit them
|
|
74
|
+
* -- but the generator requires both of every host it writes
|
|
75
|
+
* (scene-chunks.ts validateSkeleton), because a host with nothing
|
|
76
|
+
* to say for itself is the report.
|
|
77
|
+
*/
|
|
78
|
+
purpose;
|
|
79
|
+
sculpt;
|
|
55
80
|
constructor(over, opts = {}) {
|
|
56
81
|
this.over = over;
|
|
57
82
|
this.rating = Host.clampRating(opts.rating);
|
|
58
83
|
this.grid = opts.grid;
|
|
59
84
|
this.sanctioned = opts.sanctioned ?? false;
|
|
85
|
+
this.purpose = Host.phrase(opts.purpose);
|
|
86
|
+
this.sculpt = Host.phrase(opts.sculpt);
|
|
87
|
+
}
|
|
88
|
+
/** A seed string, trimmed; blank or non-string becomes undefined so a renderer can `??` it. */
|
|
89
|
+
static phrase(raw) {
|
|
90
|
+
if (typeof raw !== 'string')
|
|
91
|
+
return undefined;
|
|
92
|
+
const s = raw.trim();
|
|
93
|
+
return s.length > 0 ? s : undefined;
|
|
60
94
|
}
|
|
61
95
|
/** The host is named for the place it stands over -- every alias the game prints derives from this. */
|
|
62
96
|
get name() {
|
|
@@ -1619,8 +1619,11 @@ ${worldEventsSummary}
|
|
|
1619
1619
|
// doc about wounds gets the one line that actually helps, under the
|
|
1620
1620
|
// reply: the command and the price. Deterministic, so it does not
|
|
1621
1621
|
// depend on the AI reading its own brief (streetDocBrief below).
|
|
1622
|
+
// Not on the verb's own system note (treat.ts tells the doc what it
|
|
1623
|
+
// just did): the patient was just treated, and the hint under that
|
|
1624
|
+
// line is the one thing they no longer need.
|
|
1622
1625
|
if (isStreetDoc(this) && isClinicRoom(this.currentLocation) && WOUND_TALK.test(message)
|
|
1623
|
-
&& (actor.damageTaken > 0 || actor.stunTaken > 0)) {
|
|
1626
|
+
&& !message.includes('[System note') && (actor.damageTaken > 0 || actor.stunTaken > 0)) {
|
|
1624
1627
|
const h = hint(treatHint());
|
|
1625
1628
|
if (h)
|
|
1626
1629
|
this.logger.logWithColor(h, 'white', { actor: actor.name });
|
|
@@ -563,6 +563,11 @@ export class Room extends AbstractRoom {
|
|
|
563
563
|
this.hostRating = Room.clampHostRating(config.hostRating);
|
|
564
564
|
if (this.host && config.hostGrid)
|
|
565
565
|
this.host.gridSeed = config.hostGrid;
|
|
566
|
+
if (this.host) {
|
|
567
|
+
// Blank from a seed reads as "none declared", never as an empty line.
|
|
568
|
+
this.host.purpose = Host.phrase(config.hostPurpose);
|
|
569
|
+
this.host.sculpt = Host.phrase(config.hostSculpt);
|
|
570
|
+
}
|
|
566
571
|
this.warded = config.warded ?? false;
|
|
567
572
|
this.watched = config.watched ?? false;
|
|
568
573
|
this.exits = config.exits || new Map();
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
"rumors": [
|
|
20
20
|
"The Haze Dragons doubled their watch on the Smuggler's Tunnel -- something's moving under the district that even Brick won't name.",
|
|
21
21
|
"A corp scout in a too-clean coat has been photographing the Night Market stalls. Nobody sells to them, and nobody's seen them leave.",
|
|
22
|
-
"Mama Wu's cousin swears the Sim Den's
|
|
22
|
+
"Mama Wu's cousin swears the Sim Den's couches started whispering NAMES last week. Real ones.",
|
|
23
23
|
"Somebody's buying up fake SINs across three districts and paying triple. Whisper won't say who, which means Whisper knows.",
|
|
24
24
|
"The Drowned Choir under the chapel sang all night on Tuesday. Sister Verdigris burned sage until dawn and won't talk about it.",
|
|
25
25
|
"Doc Halloran turned away a man with corp-grade chrome and a bullet in him. The man walked north into the rain and nobody's seen him since.",
|
|
26
|
-
"Ratchet's
|
|
26
|
+
"Somebody from OUTSIDE the district has been sniffing Ratchet's shop link twice this month. He's rewiring the whole counter and swearing in three languages.",
|
|
27
27
|
"A docks crew found a shipping crate of medical-grade bioware, still cold. By morning the crate was gone -- and so was their foreman.",
|
|
28
28
|
"Mr. Krow's clients are getting jumpy -- two meets cancelled this month before the ink dried. Something upstream is scaring the money.",
|
|
29
29
|
"Old Cray says the water in the Flooded Undermarket dropped half a meter overnight. Nobody drains water down there on purpose."
|
|
@@ -295,11 +295,9 @@
|
|
|
295
295
|
},
|
|
296
296
|
{
|
|
297
297
|
"name": "Circuit Bazaar",
|
|
298
|
-
"description": "Ratchet's electronics den -- a counter by the door, workbenches of half-gutted decks and optical cable behind it, and a
|
|
298
|
+
"description": "Ratchet's electronics den -- a counter by the door, workbenches of half-gutted decks and optical cable behind it, and a wall of refurbished commlinks blinking for sale over the shopfront where locals trade parts and gossip.",
|
|
299
299
|
"roomType": "Shop",
|
|
300
300
|
"owner": "Ratchet",
|
|
301
|
-
"hasNode": true,
|
|
302
|
-
"hostRating": 3,
|
|
303
301
|
"size": "medium",
|
|
304
302
|
"spots": [
|
|
305
303
|
{
|
|
@@ -472,11 +470,8 @@
|
|
|
472
470
|
},
|
|
473
471
|
{
|
|
474
472
|
"name": "Sim Den",
|
|
475
|
-
"description": "Past the door, rows of scavenged sim-couches under a
|
|
473
|
+
"description": "Past the door, rows of scavenged sim-couches under a ceiling of dead fibre and hand-spliced cable, every rig running off its own box and nothing on the grid at all. The couch rows whisper with other people's dreams.",
|
|
476
474
|
"roomType": "Den",
|
|
477
|
-
"hasNode": true,
|
|
478
|
-
"hostRating": 5,
|
|
479
|
-
"nodeAccess": "wired",
|
|
480
475
|
"size": "small",
|
|
481
476
|
"spots": [
|
|
482
477
|
{
|
|
@@ -531,7 +526,9 @@
|
|
|
531
526
|
"intuition": 3,
|
|
532
527
|
"charisma": 2,
|
|
533
528
|
"skills": {
|
|
534
|
-
"automatics": 6,
|
|
529
|
+
"automatics": 6,
|
|
530
|
+
"pistols": 6,
|
|
531
|
+
"longarms": 6,
|
|
535
532
|
"unarmed": 4,
|
|
536
533
|
"perception": 4,
|
|
537
534
|
"intimidation": 4,
|
|
@@ -541,10 +538,10 @@
|
|
|
541
538
|
}
|
|
542
539
|
},
|
|
543
540
|
{
|
|
544
|
-
"description": "Ratchet is the Circuit Bazaar's owner: a wiry decker who sells, repairs, and quietly upgrades electronics. He knows every
|
|
541
|
+
"description": "Ratchet is the Circuit Bazaar's owner: a wiry decker who sells, repairs, and quietly upgrades electronics. He knows every deck in the district and who bought it, keeps the Sim Den's rigs running off the books, and gossips freely with fellow tech-heads.",
|
|
545
542
|
"dialogue": [
|
|
546
543
|
"Everything here boots. Mostly.",
|
|
547
|
-
"The Sim Den
|
|
544
|
+
"The Sim Den? Nothing in there touches the grid, chummer -- every couch runs off its own box, hard lines only. Somebody down there is paranoid for a REASON.",
|
|
548
545
|
"Bring me broken decks. I like the sad ones."
|
|
549
546
|
],
|
|
550
547
|
"player": {
|
|
@@ -681,7 +678,9 @@
|
|
|
681
678
|
"intuition": 4,
|
|
682
679
|
"charisma": 4,
|
|
683
680
|
"skills": {
|
|
684
|
-
"automatics": 5,
|
|
681
|
+
"automatics": 5,
|
|
682
|
+
"pistols": 5,
|
|
683
|
+
"longarms": 5,
|
|
685
684
|
"unarmed": 4,
|
|
686
685
|
"perception": 5,
|
|
687
686
|
"negotiation": 4,
|
|
@@ -746,7 +745,9 @@
|
|
|
746
745
|
"con": 4,
|
|
747
746
|
"perception": 4,
|
|
748
747
|
"negotiation": 3,
|
|
749
|
-
"gymnastics": 4,
|
|
748
|
+
"gymnastics": 4,
|
|
749
|
+
"running": 4,
|
|
750
|
+
"swimming": 4
|
|
750
751
|
}
|
|
751
752
|
}
|
|
752
753
|
}
|
|
@@ -775,7 +776,9 @@
|
|
|
775
776
|
"skills": {
|
|
776
777
|
"unarmed": 6,
|
|
777
778
|
"blades": 5,
|
|
778
|
-
"automatics": 4,
|
|
779
|
+
"automatics": 4,
|
|
780
|
+
"pistols": 4,
|
|
781
|
+
"longarms": 4,
|
|
779
782
|
"intimidation": 5,
|
|
780
783
|
"perception": 3
|
|
781
784
|
}
|
|
@@ -812,29 +815,6 @@
|
|
|
812
815
|
}
|
|
813
816
|
}
|
|
814
817
|
},
|
|
815
|
-
{
|
|
816
|
-
"plane": "matrix",
|
|
817
|
-
"description": "Static Ghost is the Sim Den host's resident ice: a scavenged military countermeasure someone wired into a bootleg sim parlor. It guards the den's files with disproportionate ferocity.",
|
|
818
|
-
"dialogue": [
|
|
819
|
-
"THIS HOST IS PROTECTED.",
|
|
820
|
-
"your dreams are not your own here. leave."
|
|
821
|
-
],
|
|
822
|
-
"player": {
|
|
823
|
-
"name": "Static Ghost",
|
|
824
|
-
"startLocation": "Sim Den",
|
|
825
|
-
"startSpot": "couch rows",
|
|
826
|
-
"lifestyleChoices": [],
|
|
827
|
-
"combat": {
|
|
828
|
-
"body": 3,
|
|
829
|
-
"agility": 3,
|
|
830
|
-
"reaction": 4,
|
|
831
|
-
"combatSkill": 4,
|
|
832
|
-
"willpower": 4,
|
|
833
|
-
"logic": 6,
|
|
834
|
-
"intuition": 4
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
},
|
|
838
818
|
{
|
|
839
819
|
"plane": "astral",
|
|
840
820
|
"description": "The Drowned Choir is a merged spirit of the chapel's lost congregation: many voices, one grief. It sings in the astral above the flooded pews, remembers every parishioner, and welcomes the Awakened who visit politely.",
|
|
@@ -1268,21 +1248,6 @@
|
|
|
1268
1248
|
"owner": "Brick",
|
|
1269
1249
|
"transferable": false
|
|
1270
1250
|
},
|
|
1271
|
-
{
|
|
1272
|
-
"name": "Ghost-Sim Bootlegs",
|
|
1273
|
-
"description": "A cache of bootleg sim-recordings on the den's host -- other people's stolen sensory memories, flickering behind the ice.",
|
|
1274
|
-
"details": "Someone's whole life is in here, felt first-hand. The den's regulars pay well for the vivid ones and better for the forbidden ones.",
|
|
1275
|
-
"size": "Tiny",
|
|
1276
|
-
"category": "Datachip",
|
|
1277
|
-
"shape": "Virtual Shelf",
|
|
1278
|
-
"color": "Static Gray",
|
|
1279
|
-
"texture": "Digital",
|
|
1280
|
-
"rating": "Rare",
|
|
1281
|
-
"weight": 0,
|
|
1282
|
-
"plane": "matrix",
|
|
1283
|
-
"room": "Sim Den",
|
|
1284
|
-
"atSpot": "couch rows"
|
|
1285
|
-
},
|
|
1286
1251
|
{
|
|
1287
1252
|
"name": "Torque's Commlink",
|
|
1288
1253
|
"description": "A ruggedized shop commlink, speed-dial worn down to the metal -- Torque answers for paying customers.",
|
|
@@ -93,7 +93,11 @@ export function hostLine(room, actor) {
|
|
|
93
93
|
// A haven shows no rating and no eyes (jackpoint.ts): it is not a
|
|
94
94
|
// thing you crack, so the numbers you would crack it with stay off.
|
|
95
95
|
const rating = room.hostSanctioned ? '' : ` [HR ${room.hostRating}]`;
|
|
96
|
-
|
|
96
|
+
// The host's business, where the seed declared one (Host.purpose): a
|
|
97
|
+
// grid map that tells a clinic's records from a club's door list is
|
|
98
|
+
// the map that lets a runner pick a target.
|
|
99
|
+
const purpose = room.host?.purpose ? ` (${room.host.purpose})` : '';
|
|
100
|
+
return `${inside ? '▣' : '●'} ${hostLabel(room, { capital: true })}${rating}${purpose}${onGrid} -- ${standing}${alert}${over}${inside ? ' -- INSIDE' : ''}`;
|
|
97
101
|
}
|
|
98
102
|
/** The Map dock's one-line host: icon, name, rating, standing -- cut to the dock. */
|
|
99
103
|
export function hostDockLine(room, actor, width) {
|
|
@@ -46,11 +46,15 @@ export function gridIcons(scene, actor, room) {
|
|
|
46
46
|
// books never gave it -- boilerplate describing the opposite of what
|
|
47
47
|
// the place is. A member is standing in a room they belong to.
|
|
48
48
|
const hr = ` {bold}[HR ${room.hostRating}]{/bold}`;
|
|
49
|
+
// WHAT IT IS FOR rides the outside icon too: a host's business is
|
|
50
|
+
// public knowledge -- "your local Stuffer Shack has a host icon"
|
|
51
|
+
// (p.219) and the icon looks like the shop. The inside stays sealed.
|
|
52
|
+
const purpose = room.host?.purpose ? ` (${room.host.purpose})` : '';
|
|
49
53
|
if (room.hostSanctioned) {
|
|
50
54
|
icons.push(`● ${hostLabel(room, { capital: true })} -- three spheres turning over rooms that were never sold to anyone. You're on the list; nothing here is hunting.`);
|
|
51
55
|
}
|
|
52
56
|
else if (room.hostCracked) {
|
|
53
|
-
icons.push(`● ${hostLabel(room, { capital: true })}${hr} -- cracked open, security in shreds. Whatever it guarded is unsealed, inside. ("enter" walks in)`);
|
|
57
|
+
icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- cracked open, security in shreds. Whatever it guarded is unsealed, inside. ("enter" walks in)`);
|
|
54
58
|
}
|
|
55
59
|
else {
|
|
56
60
|
const myMarks = room.hostMarksBy.get(actor.name) ?? 0;
|
|
@@ -59,7 +63,7 @@ export function gridIcons(scene, actor, room) {
|
|
|
59
63
|
// anyone inside it are on the far side of a wall (p.246). The
|
|
60
64
|
// marks line stays because a mark is the DOOR (p.239), so it is
|
|
61
65
|
// the one fact about the inside that belongs on the outside.
|
|
62
|
-
icons.push(`● ${hostLabel(room, { capital: true })}${hr} -- sculpted ice geometry, sealed. Whatever it holds is inside it.${room.hostAlert ? ' Walls UP -- it holds a mark on an intruder.' : ''}${myMarks > 0 ? ` [your marks: ${myMarks}/3]` : ''} (${myMarks > 0 ? '"enter" walks in on your mark; ' : ''}"hack" slips in on sleaze; "hack loud" smashes; "mark" softens it first)`);
|
|
66
|
+
icons.push(`● ${hostLabel(room, { capital: true })}${hr}${purpose} -- sculpted ice geometry, sealed. Whatever it holds is inside it.${room.hostAlert ? ' Walls UP -- it holds a mark on an intruder.' : ''}${myMarks > 0 ? ` [your marks: ${myMarks}/3]` : ''} (${myMarks > 0 ? '"enter" walks in on your mark; ' : ''}"hack" slips in on sleaze; "hack loud" smashes; "mark" softens it first)`);
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
// WHERE THE HOST'S OWN ICON ENDS AND THE STREET BEGINS.
|
|
@@ -402,9 +406,16 @@ export function gridSculpt(actor, room) {
|
|
|
402
406
|
// any more, and a header that kept saying so was half of why the
|
|
403
407
|
// boundary read as absent.
|
|
404
408
|
if (actor.insideHost === room) {
|
|
409
|
+
// THE INSIDE IS WHAT THE OWNER BUILT (N4erz3f63MiCZLkDE; SR5 p.219,
|
|
410
|
+
// Data Trails p.167-168). A host that declares its purpose and its
|
|
411
|
+
// sculpting is shown as that -- the purpose on the header, the
|
|
412
|
+
// sculpt where the boilerplate used to stand. The boilerplate stays
|
|
413
|
+
// for a hand-authored host that declared neither.
|
|
414
|
+
const purpose = room.host?.purpose;
|
|
415
|
+
const sculpt = room.host?.sculpt;
|
|
405
416
|
return [
|
|
406
|
-
`{cyan-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]{/cyan-fg}`,
|
|
407
|
-
`{cyan-fg}Architecture all the way around -- the outside grid is a wall away and might as well be weather.{/cyan-fg}`,
|
|
417
|
+
`{cyan-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]${purpose ? ` -- ${purpose}` : ''}{/cyan-fg}`,
|
|
418
|
+
`{cyan-fg}${sculpt ?? 'Architecture all the way around -- the outside grid is a wall away and might as well be weather.'}{/cyan-fg}`,
|
|
408
419
|
].join('\n');
|
|
409
420
|
}
|
|
410
421
|
// ON THE GRID (SR5 pp.217-218): no street under the persona -- a black
|
|
@@ -502,6 +513,8 @@ export function hostInteriorPanel(scene, actor, host, rows, cols) {
|
|
|
502
513
|
const cut = (raw) => `{cyan-fg}${[...raw].slice(0, Math.max(4, cols)).join('')}{/cyan-fg}`;
|
|
503
514
|
const lines = [];
|
|
504
515
|
lines.push(cut(`INSIDE ${host.name}`));
|
|
516
|
+
if (host.purpose)
|
|
517
|
+
lines.push(cut(host.purpose));
|
|
505
518
|
const marks = host.marksBy.get(actor.name) ?? 0;
|
|
506
519
|
lines.push(cut(host.sanctioned
|
|
507
520
|
? 'haven -- welcome here'
|
|
@@ -80,6 +80,14 @@ export function buildJackPoint() {
|
|
|
80
80
|
outdoors: false,
|
|
81
81
|
hasNode: true,
|
|
82
82
|
nodeAccess: 'wireless',
|
|
83
|
+
// The haven's purpose and sculpt (Host.purpose): it is the one host
|
|
84
|
+
// the engine builds itself, so it declares what every generated host
|
|
85
|
+
// must -- what it is for, and what the inside looks like.
|
|
86
|
+
hostPurpose: 'the shadowrunners\' data haven -- the cybercafe, the Museum, the Memorial',
|
|
87
|
+
hostSculpt: 'Independent rooms adrift in a vast grid, sculpted loosely and mostly abstract -- '
|
|
88
|
+
+ 'the cybercafe murmuring at one edge, the Museum of Shadowrunning further out, '
|
|
89
|
+
+ 'and the Memorial holding its silence. No gravity to speak of, no weather, '
|
|
90
|
+
+ 'and three spheres turning slowly overhead.',
|
|
83
91
|
});
|
|
84
92
|
room.hostSanctioned = true;
|
|
85
93
|
return room;
|
|
@@ -237,6 +237,33 @@ export async function dryRunRepro(repro) {
|
|
|
237
237
|
players: [saveForArchetype(REPRO_ACTOR, reproArchetypeName(fixture))],
|
|
238
238
|
events,
|
|
239
239
|
});
|
|
240
|
+
// START WOUNDED (fixture.wounded): the boxes go on before the first
|
|
241
|
+
// step, so the transcript opens on a runner who already needs the
|
|
242
|
+
// verb under test.
|
|
243
|
+
if (fixture.wounded) {
|
|
244
|
+
const runner = session.game.scene.getPlayerByName(REPRO_ACTOR);
|
|
245
|
+
if (runner) {
|
|
246
|
+
if (fixture.wounded.physical)
|
|
247
|
+
runner.takeDamage(fixture.wounded.physical);
|
|
248
|
+
if (fixture.wounded.stun)
|
|
249
|
+
runner.takeStun(fixture.wounded.stun);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// NUYEN (fixture.nuyen): a credstick in the pack, because money lives
|
|
253
|
+
// on a carrier item (Player.currency sums them) and the pregen has none.
|
|
254
|
+
if (fixture.nuyen && fixture.nuyen > 0) {
|
|
255
|
+
const runner = session.game.scene.getPlayerByName(REPRO_ACTOR);
|
|
256
|
+
if (runner) {
|
|
257
|
+
const { ItemFactory } = await import('../factories/item-factory.js');
|
|
258
|
+
const stick = await new ItemFactory().createItemFromJson({
|
|
259
|
+
name: 'Certified Credstick', description: 'Cash that does not ask questions.',
|
|
260
|
+
size: 'Tiny', category: 'Credstick', shape: 'Slab', color: 'Black', texture: 'Plastic',
|
|
261
|
+
rating: 'Common', weight: 0.1, transferable: true,
|
|
262
|
+
});
|
|
263
|
+
stick.addCurrency(fixture.nuyen);
|
|
264
|
+
runner.addInventory(stick);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
240
267
|
const lines = [];
|
|
241
268
|
// THE FIRST PAINT IS NOT PART OF STEP ONE. bootSession pushes every
|
|
242
269
|
// player's panels the moment the session comes up (B7NNNesy5gvRSbZpp),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.175.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.",
|