@maka/maka-cli 5.174.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.174.0",
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.",
@@ -422,5 +422,13 @@
422
422
  // answers" for the reporter's DR 2 maglock. The bench fixture grows
423
423
  // home / clinic / wounded / nuyen and npcs[].doc, so healing can be
424
424
  // benched without a fight.
425
- export const ENGINE_VERSION = '1.49.0';
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';
426
434
  //# sourceMappingURL=engine-version.js.map
@@ -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
- // Ice NPCs and matrix-plane items belong ONLY in hasNode rooms, and
355
- // any hasNode room holding matrix items MUST have an ice NPC
356
- // stationed in it -- so a corner-store host with no paydata in it
357
- // carries no ice, and costs the scene nothing.
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 ghost started whispering NAMES last week. Real ones.",
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 public host got probed twice this month from OUTSIDE the district. He's rebuilding his ice and swearing in three languages.",
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 public host humming over the shopfront where locals trade files and gossip.",
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 host that hums on the grid like any other. The paranoia is one rack further in: a matte-black server with no antenna on it, holding what matters on hard lines only. The couch rows whisper with other people's dreams.",
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, "pistols": 6, "longarms": 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 host in the district, including the Sim Den's and the box it keeps off the grid, and gossips freely with fellow tech-heads.",
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 host? Right there on the grid like anyone's. It's the SERVER behind it that never sees a signal -- hard lines only, chummer. Somebody down there is paranoid for a REASON.",
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, "pistols": 5, "longarms": 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, "running": 4, "swimming": 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, "pistols": 4, "longarms": 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
- return `${inside ? '▣' : '●'} ${hostLabel(room, { capital: true })}${rating}${onGrid} -- ${standing}${alert}${over}${inside ? ' -- INSIDE' : ''}`;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.174.0",
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.",