@maka/maka-cli 5.200.0 → 5.202.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/ask.js +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/compile.js +2 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/crew.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/disable.js +7 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +10 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/edit-file.js +20 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/erase-mark.js +237 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +20 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/hide.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/jack.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/order.js +15 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/overwatch.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +36 -10
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprite-power.js +85 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sprites.js +12 -3
- package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +3 -2
- package/bundle/typescript/src/commands/game/sideQuest/engine-version.js +165 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/npc-factory.js +37 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +109 -11
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +41 -9
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +10 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.js +95 -3
- package/bundle/typescript/src/commands/game/sideQuest/models/host.js +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/item.js +38 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +20 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +44 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/room.js +11 -2
- package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +23 -3
- package/bundle/typescript/src/commands/game/sideQuest/npc-kits.js +295 -0
- package/bundle/typescript/src/commands/game/sideQuest/sprite-powers.js +48 -7
- package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +34 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-errand.js +67 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/cleanup-hire.js +113 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-turn.js +87 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/commerce.js +5 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/drone-prose.js +11 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +17 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-names.js +42 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-reach.js +10 -5
- package/bundle/typescript/src/commands/game/sideQuest/utilities/grid-view.js +37 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/host-combat.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/marks.js +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/mechanics-audience.js +71 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +36 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/programs.js +7 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +13 -3
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sprite-power-actions.js +270 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js +25 -13
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { rollInitiativeScore, formatInitiative, INITIATIVE_PASS_DROP, rollPool, formatRoll } from './dice.js';
|
|
2
2
|
import { ActionBudget } from './action-budget.js';
|
|
3
3
|
import { runNpcActionPhase } from './npc-combat-brain.js';
|
|
4
|
+
import { groupEdgeFor } from '../npc-kits.js';
|
|
4
5
|
export function arenaKey(a) {
|
|
5
6
|
return a.kind === 'room' ? `room:${a.room.name}` : a.kind === 'host' ? `host:${a.host.name}` : 'grid:open';
|
|
6
7
|
}
|
|
@@ -114,6 +115,20 @@ export class CombatEncounter {
|
|
|
114
115
|
pairs = new Map();
|
|
115
116
|
surrenderSeenAt = new Map();
|
|
116
117
|
running = false;
|
|
118
|
+
/**
|
|
119
|
+
* GRUNT TEAMS BY KIT (p.379-380, npc-kits.ts). Every kitted NPC who
|
|
120
|
+
* enters the fight is rostered under its kit slug, and the roster is
|
|
121
|
+
* the denominator morale reads -- a team that started four and lost
|
|
122
|
+
* two has lost half, whether or not the dead are still actors. Group
|
|
123
|
+
* Edge is a pool per kit, seeded to the PR on first sight (+1 with a
|
|
124
|
+
* lieutenant, p.380-381) and spent by the brain one point per grunt
|
|
125
|
+
* per fight. Keyed by KIT rather than by side because one scene can
|
|
126
|
+
* field a PR 1 gang and a PR 3 patrol, and the book prices them
|
|
127
|
+
* separately.
|
|
128
|
+
*/
|
|
129
|
+
kitRoster = new Map();
|
|
130
|
+
groupEdge = new Map();
|
|
131
|
+
edgeSpentBy = new Set();
|
|
117
132
|
/** Every human phase that began -- a seam for tests and for any future
|
|
118
133
|
* phase clock; nothing plays the phase for a human (the fight
|
|
119
134
|
* autopilot was retired 2026-09-07: a phase you commit with "end
|
|
@@ -213,8 +228,80 @@ export class CombatEncounter {
|
|
|
213
228
|
acted: false, delayed: false, surprised: false,
|
|
214
229
|
};
|
|
215
230
|
this.participants.push(p);
|
|
231
|
+
this.rosterGrunt(actor);
|
|
216
232
|
return p;
|
|
217
233
|
}
|
|
234
|
+
/** Put a kitted NPC on its team's roster; seed the team's Group Edge on first sight. */
|
|
235
|
+
rosterGrunt(actor) {
|
|
236
|
+
const npc = actor;
|
|
237
|
+
if (!this.isNpc(actor) || npc.kit === undefined || npc.professionalRating === undefined)
|
|
238
|
+
return;
|
|
239
|
+
let roster = this.kitRoster.get(npc.kit);
|
|
240
|
+
if (!roster) {
|
|
241
|
+
roster = new Set();
|
|
242
|
+
this.kitRoster.set(npc.kit, roster);
|
|
243
|
+
}
|
|
244
|
+
roster.add(npc.name);
|
|
245
|
+
// p.380: "a team of grunts has a Group Edge equal to its Professional
|
|
246
|
+
// Rating". p.380-381: a lieutenant "adds 1 to their Edge". Seeded once;
|
|
247
|
+
// a lieutenant who joins late tops it up by their one point.
|
|
248
|
+
if (!this.groupEdge.has(npc.kit)) {
|
|
249
|
+
this.groupEdge.set(npc.kit, groupEdgeFor(npc.professionalRating));
|
|
250
|
+
}
|
|
251
|
+
if (npc.lieutenant)
|
|
252
|
+
this.groupEdge.set(npc.kit, (this.groupEdge.get(npc.kit) ?? 0) + 1);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* THE STATE OF A GRUNT'S TEAM, for the brain's morale check. Casualties
|
|
256
|
+
* are rostered names that are dead (gone from the scene), incapacitated
|
|
257
|
+
* or death-claimed; a grunt who surrendered still stands and is not
|
|
258
|
+
* one. The lieutenant's +1 to PR lasts only while the lieutenant does.
|
|
259
|
+
*/
|
|
260
|
+
teamOf(npc) {
|
|
261
|
+
if (npc.kit === undefined || npc.professionalRating === undefined)
|
|
262
|
+
return undefined;
|
|
263
|
+
const roster = this.kitRoster.get(npc.kit);
|
|
264
|
+
if (!roster)
|
|
265
|
+
return undefined;
|
|
266
|
+
let casualties = 0;
|
|
267
|
+
let lieutenantStands = false;
|
|
268
|
+
for (const name of roster) {
|
|
269
|
+
const member = this.scene.getActor(name);
|
|
270
|
+
const down = !member || member.isIncapacitated() || member.deathClaimed;
|
|
271
|
+
if (down)
|
|
272
|
+
casualties++;
|
|
273
|
+
else if (member.lieutenant)
|
|
274
|
+
lieutenantStands = true;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
kit: npc.kit,
|
|
278
|
+
total: roster.size,
|
|
279
|
+
casualties,
|
|
280
|
+
effectivePr: npc.professionalRating + (lieutenantStands ? 1 : 0),
|
|
281
|
+
edge: this.groupEdge.get(npc.kit) ?? 0,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* SPEND A POINT OF GROUP EDGE ON THIS GRUNT (p.380: "the gamemaster
|
|
286
|
+
* can spend a point of Group Edge on any grunt on the team"). The
|
|
287
|
+
* engine is the gamemaster, and its standing policy is the plain one:
|
|
288
|
+
* one point per grunt per fight, on that grunt's first attack, while
|
|
289
|
+
* the pool lasts. Arms the same boost a runner's "edge" verb arms
|
|
290
|
+
* (Player.consumeEdgeBoost adds `edge` dice to the next strike), so
|
|
291
|
+
* the spend goes through the one lane every edge boost already uses.
|
|
292
|
+
*/
|
|
293
|
+
spendGroupEdge(npc) {
|
|
294
|
+
if (npc.kit === undefined || npc.edgeBoostArmed || this.edgeSpentBy.has(npc.name))
|
|
295
|
+
return false;
|
|
296
|
+
const pool = this.groupEdge.get(npc.kit) ?? 0;
|
|
297
|
+
if (pool <= 0)
|
|
298
|
+
return false;
|
|
299
|
+
this.groupEdge.set(npc.kit, pool - 1);
|
|
300
|
+
this.edgeSpentBy.add(npc.name);
|
|
301
|
+
npc.edgeBoostArmed = true;
|
|
302
|
+
this.logger.write(`Encounter: Group Edge spent on ${npc.name} (${npc.kit}: ${pool - 1} left).`);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
218
305
|
/**
|
|
219
306
|
* Someone walks into a running fight (p.160 Changing Initiative):
|
|
220
307
|
* roll as normal, minus 10 per pass already gone this turn.
|
|
@@ -72,6 +72,11 @@ const roundTo5 = (n) => Math.max(5, Math.round(n / 5) * 5);
|
|
|
72
72
|
* 120-nuyen credstick selling for its 25-nuyen shell prints money. */
|
|
73
73
|
export function priceOf(item) {
|
|
74
74
|
const cash = item.isCurrencyCarrier() ? item.currencyAmount : 0;
|
|
75
|
+
// Armor that stopped the rounds that killed its wearer is not a
|
|
76
|
+
// trade good (Room.layOutBody): a fence pays for the plates, and the
|
|
77
|
+
// plates are what is gone.
|
|
78
|
+
if (item.ruined)
|
|
79
|
+
return cash;
|
|
75
80
|
if (typeof item.price === 'number' && item.price > 0)
|
|
76
81
|
return Math.round(item.price) + cash;
|
|
77
82
|
const base = CATEGORY_BASE[item.category] ?? 50;
|
|
@@ -246,7 +246,15 @@ export function droneReconNowhereLine(item) {
|
|
|
246
246
|
missile: `The ${item.name} climbs, circles once -- nowhere to go from here.`,
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
/**
|
|
250
|
+
* THE PROSE NO LONGER CARRIES THE DICE (wjFBY3zgyWzJP9adw sweep). This
|
|
251
|
+
* baked `Piloting: 8d6 -> 2 hits` into the world log while every other
|
|
252
|
+
* player test in the engine sends its anatomy to the Mechanics panel --
|
|
253
|
+
* the recon sweep was the one roll a rigger had to read out of a
|
|
254
|
+
* sentence. `rollText` is gone; commands/drone.ts emits the roll through
|
|
255
|
+
* logger.meta, and this line goes back to being prose.
|
|
256
|
+
*/
|
|
257
|
+
export function droneReconLaunchLine(item, ok) {
|
|
250
258
|
const off = pick(item, {
|
|
251
259
|
'flier-palm': `The ${item.name} whirs off your palm`,
|
|
252
260
|
flier: `The ${item.name} lifts off your hand`,
|
|
@@ -256,7 +264,7 @@ export function droneReconLaunchLine(item, rollText, ok) {
|
|
|
256
264
|
missile: `The ${item.name} kicks off its rail`,
|
|
257
265
|
});
|
|
258
266
|
if (!ok)
|
|
259
|
-
return
|
|
267
|
+
return off;
|
|
260
268
|
const threads = pick(item, {
|
|
261
269
|
'flier-palm': 'threads the gaps',
|
|
262
270
|
flier: 'threads the gaps',
|
|
@@ -265,7 +273,7 @@ export function droneReconLaunchLine(item, rollText, ok) {
|
|
|
265
273
|
anthro: 'goes to look',
|
|
266
274
|
missile: 'makes a fast pass',
|
|
267
275
|
});
|
|
268
|
-
return `${off} and ${threads}
|
|
276
|
+
return `${off} and ${threads}. Feed incoming:`;
|
|
269
277
|
}
|
|
270
278
|
export function droneReconReturnLine(item) {
|
|
271
279
|
return pick(item, {
|
|
@@ -3,6 +3,7 @@ import { Logger } from './logger.js';
|
|
|
3
3
|
import { CommandRegistry } from '../commands/command-registry.js';
|
|
4
4
|
import { actorCell, actorDistanceMeters, standingRoomNear, ADJACENT_METERS } from './spots.js';
|
|
5
5
|
import { METERS_PER_CELL } from './room-grid.js';
|
|
6
|
+
import { outfitNpcBySlug, equipKitIntoHands } from '../npc-kits.js';
|
|
6
7
|
/**
|
|
7
8
|
* Bring one into the world. Returns the NPC, or null when the name is
|
|
8
9
|
* already taken -- the same refusal spawnCompanionShell makes, and for
|
|
@@ -17,9 +18,11 @@ export function spawnEphemeral(host, opts) {
|
|
|
17
18
|
const npc = new NPC({
|
|
18
19
|
dialog: [],
|
|
19
20
|
description: opts.description,
|
|
20
|
-
playerConstructorConfig: { playerName: opts.name, startLocation: opts.room, combat: opts.combat },
|
|
21
|
+
playerConstructorConfig: { playerName: opts.name, startLocation: opts.room, combat: opts.combat ?? {} },
|
|
21
22
|
}, new CommandRegistry(host.scene));
|
|
22
23
|
npc.plane = 'meat';
|
|
24
|
+
if (outfitNpcBySlug(npc, opts.kit))
|
|
25
|
+
equipKitIntoHands(npc);
|
|
23
26
|
// NOT allyOf, and NOT companionKind: an ephemeral belongs to nobody
|
|
24
27
|
// and is on no roster. Those two fields are what make a companion
|
|
25
28
|
// persist, follow, and take orders.
|
|
@@ -62,8 +65,19 @@ export function despawnEphemerals(host, opts) {
|
|
|
62
65
|
: opts.playerRoomName !== undefined && opts.playerRoomName !== mark.roomName;
|
|
63
66
|
if (!ended)
|
|
64
67
|
continue;
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const body = host.scene.getActor(name);
|
|
69
|
+
// A DEPARTURE IS NOT A DEATH (2026-09-15, the day kits gave gangers
|
|
70
|
+
// a Browning and an armor vest). removeActor spills what an actor
|
|
71
|
+
// carried onto the floor, which is right for a corpse and wrong for
|
|
72
|
+
// a ganger who watched you walk off their corner: the street was
|
|
73
|
+
// raining pistols, per ganger, per block. A body that is DOWN when
|
|
74
|
+
// its scope ends spills, because it is a body; one that simply
|
|
75
|
+
// stops existing takes its gear with it. That is also the honest
|
|
76
|
+
// reading of this governor's founding denial -- an ephemeral
|
|
77
|
+
// cannot create obligations, and a gun the world must keep track
|
|
78
|
+
// of after its owner is gone is one.
|
|
79
|
+
if (body instanceof NPC)
|
|
80
|
+
host.scene.removeActor(name, { spill: body.isIncapacitated() });
|
|
67
81
|
host.ephemerals.delete(name);
|
|
68
82
|
removed.push(name);
|
|
69
83
|
}
|
|
@@ -77,6 +77,48 @@ export function hostLabel(room, opts = {}) {
|
|
|
77
77
|
const bare = room.name.replace(/^the\s+/i, '');
|
|
78
78
|
return `${opts.capital ? 'The' : 'the'} ${bare} host`;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* WHAT THE HOST RUNS ON, ONCE YOU HAVE PAID FOR IT (np6YBSPsdfAY7NcuZ,
|
|
82
|
+
* Maka: "data like this ... shouldn't be visible without a matrix search
|
|
83
|
+
* on the host").
|
|
84
|
+
*
|
|
85
|
+
* `Host.purpose` is seed content and the generator is told to fill it
|
|
86
|
+
* with "what this place runs on its host and so what is worth protecting
|
|
87
|
+
* inside it" (factories/scene-chunks.ts) -- so in practice it enumerates
|
|
88
|
+
* the archive: "patient intake logs, med supply orders, and the doc's
|
|
89
|
+
* private black book of who paid what for what". Four renderers printed
|
|
90
|
+
* that on the host's OUTSIDE icon with no roll, which handed a passer-by
|
|
91
|
+
* the inventory of a sealed host for free. The file NAMES were already
|
|
92
|
+
* gated (grid-view.ts, Host.searchedBy); only this phrase leaked.
|
|
93
|
+
*
|
|
94
|
+
* TWO TIERS, and both are rolls the engine already makes
|
|
95
|
+
* (commands/search.ts matrixSearch):
|
|
96
|
+
*
|
|
97
|
+
* tier 1 Room.searchedBy -- the threshold-1 public row out on the
|
|
98
|
+
* grid (MATRIX_SEARCH.public). What the place runs on its host
|
|
99
|
+
* is the kind of thing a public search turns up: canon has the
|
|
100
|
+
* icon look like the shop (p.219). This set is also flipped by
|
|
101
|
+
* a MEAT search of the room, and that is intended -- casing the
|
|
102
|
+
* clinic in the flesh is a fair way to learn it keeps a ledger.
|
|
103
|
+
* tier 2 Host.searchedBy -- the threshold-3 archive search from
|
|
104
|
+
* inside, which additionally NAMES the files (grid-view.ts).
|
|
105
|
+
*
|
|
106
|
+
* A cracked host spills it free, exactly as its files already do: the
|
|
107
|
+
* seal is broken, there is nothing left to hide behind.
|
|
108
|
+
*
|
|
109
|
+
* Returns undefined when the seed declared no purpose OR the persona has
|
|
110
|
+
* not earned it, so every caller can `??` it away.
|
|
111
|
+
*/
|
|
112
|
+
export function hostPurposeFor(room, actor) {
|
|
113
|
+
const host = room.host;
|
|
114
|
+
if (!host?.purpose)
|
|
115
|
+
return undefined;
|
|
116
|
+
if (host.cracked)
|
|
117
|
+
return host.purpose;
|
|
118
|
+
return host.searchedBy.has(actor.name) || room.searchedBy.has(actor.name)
|
|
119
|
+
? host.purpose
|
|
120
|
+
: undefined;
|
|
121
|
+
}
|
|
80
122
|
export function hostNameHint(room) {
|
|
81
123
|
return room.hasNode ? `the host here answers to "host" or "${room.name}"` : '';
|
|
82
124
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hostConcealedFrom } from './jackpoint.js';
|
|
2
|
-
import { matchesHostName, hostLabel, hostAliases } from './grid-names.js';
|
|
2
|
+
import { matchesHostName, hostLabel, hostAliases, hostPurposeFor } from './grid-names.js';
|
|
3
3
|
import { fuzzyPickName, normalizeLoose } from './fuzzy-match.js';
|
|
4
4
|
import { MAX_MARKS } from './marks.js';
|
|
5
5
|
/**
|
|
@@ -193,10 +193,15 @@ export function hostLine(room, actor) {
|
|
|
193
193
|
// A haven shows no rating and no eyes (jackpoint.ts): it is not a
|
|
194
194
|
// thing you crack, so the numbers you would crack it with stay off.
|
|
195
195
|
const rating = room.hostSanctioned ? '' : ` [HR ${room.hostRating}]`;
|
|
196
|
-
// The host's business, where the seed declared one
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
196
|
+
// The host's business, where the seed declared one AND this persona has
|
|
197
|
+
// searched for it (hostPurposeFor, np6YBSPsdfAY7NcuZ). A grid map that
|
|
198
|
+
// tells a clinic's records from a club's door list is the map that lets
|
|
199
|
+
// a runner pick a target -- but it is a Matrix Search that draws it,
|
|
200
|
+
// not a glance. Map rows stay terse when it is unearned: the verb is
|
|
201
|
+
// taught on the host's own icon (grid-view.ts gridIcons), not once per
|
|
202
|
+
// row of a map that may list a dozen.
|
|
203
|
+
const found = hostPurposeFor(room, actor);
|
|
204
|
+
const purpose = found ? ` (${found})` : '';
|
|
200
205
|
return `${inside ? '▣' : '●'} ${hostLabel(room, { capital: true })}${rating}${purpose}${onGrid} -- ${standing}${alert}${over}${inside ? ' -- INSIDE' : ''}`;
|
|
201
206
|
}
|
|
202
207
|
/** The Map dock's one-line host: icon, name, rating, standing -- cut to the dock. */
|
|
@@ -2,7 +2,7 @@ import { sameHostSide } from '../models/player.js';
|
|
|
2
2
|
import { personasInside } from './matrix-roster.js';
|
|
3
3
|
import { hostsInReach, hostLine, roomsInReach, gridVicinity } from './grid-reach.js';
|
|
4
4
|
import { NPC } from '../models/npc.js';
|
|
5
|
-
import { hostLabel } from './grid-names.js';
|
|
5
|
+
import { hostLabel, hostPurposeFor } from './grid-names.js';
|
|
6
6
|
import { isLinked } from './comm-style.js';
|
|
7
7
|
import { isWatched, camerasLive, canSnoopFeeds } from './surveillance.js';
|
|
8
8
|
/**
|
|
@@ -65,10 +65,13 @@ export function gridIcons(scene, actor, room) {
|
|
|
65
65
|
// books never gave it -- boilerplate describing the opposite of what
|
|
66
66
|
// the place is. A member is standing in a room they belong to.
|
|
67
67
|
const hr = ` {bold}[HR ${room.hostRating}]{/bold}`;
|
|
68
|
-
// WHAT IT IS FOR rides the outside icon
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
68
|
+
// WHAT IT IS FOR rides the outside icon -- but only once a Matrix
|
|
69
|
+
// Search has turned it up (hostPurposeFor, np6YBSPsdfAY7NcuZ). The
|
|
70
|
+
// icon looking like the shop (p.219) is the NAME, which this line
|
|
71
|
+
// always prints; the seed's purpose phrase is an inventory of the
|
|
72
|
+
// archive, and handing that over for free is the report.
|
|
73
|
+
const found = hostPurposeFor(room, actor);
|
|
74
|
+
const purpose = found ? ` (${found})` : '';
|
|
72
75
|
if (room.hostSanctioned) {
|
|
73
76
|
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.`);
|
|
74
77
|
}
|
|
@@ -82,7 +85,18 @@ export function gridIcons(scene, actor, room) {
|
|
|
82
85
|
// anyone inside it are on the far side of a wall (p.246). The
|
|
83
86
|
// marks line stays because a mark is the DOOR (p.239), so it is
|
|
84
87
|
// the one fact about the inside that belongs on the outside.
|
|
85
|
-
|
|
88
|
+
// The verb that EARNS the purpose phrase is offered only while it
|
|
89
|
+
// is still unearned -- a host that has already told you its
|
|
90
|
+
// business has nothing left to sell. Built as a list so a dropped
|
|
91
|
+
// entry cannot leave a dangling separator.
|
|
92
|
+
const ways = [
|
|
93
|
+
...(myMarks > 0 ? ['"enter" walks in on your mark'] : []),
|
|
94
|
+
'"hack" slips in on sleaze',
|
|
95
|
+
'"hack loud" smashes',
|
|
96
|
+
'"mark" softens it first',
|
|
97
|
+
...(found ? [] : ['"search" reads what it runs (p.241)']),
|
|
98
|
+
];
|
|
99
|
+
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]` : ''} (${ways.join('; ')})`);
|
|
86
100
|
}
|
|
87
101
|
}
|
|
88
102
|
// WHERE THE HOST'S OWN ICON ENDS AND THE STREET BEGINS.
|
|
@@ -457,7 +471,11 @@ export function gridSculpt(actor, room) {
|
|
|
457
471
|
// sculpting is shown as that -- the purpose on the header, the
|
|
458
472
|
// sculpt where the boilerplate used to stand. The boilerplate stays
|
|
459
473
|
// for a hand-authored host that declared neither.
|
|
460
|
-
|
|
474
|
+
// The purpose is still the searched-for fact in here (hostPurposeFor):
|
|
475
|
+
// standing inside a host is not the same as having read its index.
|
|
476
|
+
// The SCULPT is not gated -- it is the architecture around you, and
|
|
477
|
+
// you are looking at it.
|
|
478
|
+
const purpose = hostPurposeFor(room, actor);
|
|
461
479
|
const sculpt = room.host?.sculpt;
|
|
462
480
|
return [
|
|
463
481
|
`{light-blue-fg}INSIDE :: ${hostLabel(room, { capital: true }).toUpperCase()} [HR ${room.hostRating}]${purpose ? ` -- ${purpose}` : ''}{/light-blue-fg}`,
|
|
@@ -559,8 +577,11 @@ export function hostInteriorPanel(scene, actor, host, rows, cols) {
|
|
|
559
577
|
const cut = (raw) => `{light-blue-fg}${[...raw].slice(0, Math.max(4, cols)).join('')}{/light-blue-fg}`;
|
|
560
578
|
const lines = [];
|
|
561
579
|
lines.push(cut(`INSIDE ${host.name}`));
|
|
562
|
-
|
|
563
|
-
|
|
580
|
+
// Same gate as every other renderer (hostPurposeFor): the dock is a
|
|
581
|
+
// view, not a back door.
|
|
582
|
+
const purpose = hostPurposeFor(host.over, actor);
|
|
583
|
+
if (purpose)
|
|
584
|
+
lines.push(cut(purpose));
|
|
564
585
|
const marks = host.marksBy.get(actor.name) ?? 0;
|
|
565
586
|
lines.push(cut(host.sanctioned
|
|
566
587
|
? 'haven -- welcome here'
|
|
@@ -574,7 +595,13 @@ export function hostInteriorPanel(scene, actor, host, rows, cols) {
|
|
|
574
595
|
continue;
|
|
575
596
|
lines.push(cut(`◆ ${a.name}`));
|
|
576
597
|
}
|
|
577
|
-
|
|
598
|
+
// THE COUNT IS PART OF THE ARCHIVE (np6YBSPsdfAY7NcuZ). The icon list
|
|
599
|
+
// above gates the file NAMES on a Matrix Search; this line quietly
|
|
600
|
+
// published how many there were regardless, which is most of what the
|
|
601
|
+
// roll was protecting -- "three files sealed" tells a runner whether
|
|
602
|
+
// the trip is worth it. Same gate, same set.
|
|
603
|
+
const indexed = host.cracked || host.searchedBy.has(actor.name);
|
|
604
|
+
const files = indexed ? host.files().length : 0;
|
|
578
605
|
if (files > 0)
|
|
579
606
|
lines.push(cut(`◇ ${files} file${files === 1 ? '' : 's'} ${host.cracked ? 'open' : 'sealed'}`));
|
|
580
607
|
return lines.slice(0, Math.max(1, rows));
|
|
@@ -47,6 +47,18 @@ async function tell(enc, scene, turn) {
|
|
|
47
47
|
*/
|
|
48
48
|
export async function hostTurnStart(scene, host, enc) {
|
|
49
49
|
const out = empty();
|
|
50
|
+
// SUPPRESSION (p.257, sprite-power-actions.ts): a sprite arguing with
|
|
51
|
+
// the host's launch paths delays what it brings up by (Level / 2)
|
|
52
|
+
// Combat Turns, and "delayed IC can't act or be targeted" -- which is
|
|
53
|
+
// exactly "the launch step does not happen this turn". Counted down
|
|
54
|
+
// here so the delay is spent whether or not the host wanted to launch.
|
|
55
|
+
if (host.icLaunchDelay > 0) {
|
|
56
|
+
host.icLaunchDelay -= 1;
|
|
57
|
+
out.lines.push(`{light-blue-fg}Something in ${host.name}'s architecture is arguing with itself -- whatever it meant to bring up does not arrive.{/light-blue-fg}`);
|
|
58
|
+
seatRunningIce(enc, host);
|
|
59
|
+
await tell(enc, scene, out);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
50
62
|
hostLaunchStep(host.over, scene, out);
|
|
51
63
|
seatRunningIce(enc, host);
|
|
52
64
|
await tell(enc, scene, out);
|
|
@@ -18,6 +18,16 @@ export function clearMarksPlacedBy(scene, placerName) {
|
|
|
18
18
|
for (const room of Object.values(scene.getRooms())) {
|
|
19
19
|
if (room.hostMarksBy.delete(placerName))
|
|
20
20
|
cleared++;
|
|
21
|
+
// A DEVICE'S MARKS ARE MARKS (f79LLqTerep4nKWcA sweep). Device.marksBy
|
|
22
|
+
// is the third ledger -- hack.ts writes it for every slaved and loose
|
|
23
|
+
// icon -- and no wipe had ever touched it, so the maglock you keyed
|
|
24
|
+
// last run still answered to a persona canon says was deleted on
|
|
25
|
+
// reboot (p.236). It also meant `disable` could drive a device on
|
|
26
|
+
// marks the sheet no longer showed you holding.
|
|
27
|
+
for (const device of room.allDevices()) {
|
|
28
|
+
if (device.marksBy.delete(placerName))
|
|
29
|
+
cleared++;
|
|
30
|
+
}
|
|
21
31
|
}
|
|
22
32
|
for (const a of scene.allActors) {
|
|
23
33
|
if (a.panMarksBy.delete(placerName))
|
|
@@ -71,6 +81,13 @@ export function describeMarks(scene, placerName) {
|
|
|
71
81
|
const n = room.hostMarksBy.get(placerName) ?? 0;
|
|
72
82
|
if (n > 0)
|
|
73
83
|
lines.push(`● ${hostLabel(room, { capital: true })} -- ${n} mark${n === 1 ? '' : 's'}${n >= MAX_MARKS ? ' (it thinks you belong)' : ''}`);
|
|
84
|
+
// Devices too -- the ledger `disable` reads to drive a maglock was
|
|
85
|
+
// invisible on the listing that is supposed to show what you hold.
|
|
86
|
+
for (const device of room.allDevices()) {
|
|
87
|
+
const d = device.marksBy.get(placerName) ?? 0;
|
|
88
|
+
if (d > 0)
|
|
89
|
+
lines.push(`▲ ${device.name} -- ${d} mark${d === 1 ? '' : 's'}`);
|
|
90
|
+
}
|
|
74
91
|
}
|
|
75
92
|
for (const a of scene.allActors) {
|
|
76
93
|
const n = a.panMarksBy.get(placerName) ?? 0;
|
|
@@ -92,6 +109,11 @@ export function marksPlaced(scene, placerName) {
|
|
|
92
109
|
const n = room.hostMarksBy.get(placerName) ?? 0;
|
|
93
110
|
if (n > 0)
|
|
94
111
|
out.push({ target: hostLabel(room, { capital: true }), count: n });
|
|
112
|
+
for (const device of room.allDevices()) {
|
|
113
|
+
const d = device.marksBy.get(placerName) ?? 0;
|
|
114
|
+
if (d > 0)
|
|
115
|
+
out.push({ target: device.name, count: d });
|
|
116
|
+
}
|
|
95
117
|
}
|
|
96
118
|
for (const a of scene.allActors) {
|
|
97
119
|
const n = a.panMarksBy.get(placerName) ?? 0;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHO IS WATCHING THE DICE (wjFBY3zgyWzJP9adw, Deditri: "the sprite put a
|
|
3
|
+
* mark on the target but there was no output about the rolls in the
|
|
4
|
+
* logs").
|
|
5
|
+
*
|
|
6
|
+
* The sprite's dice WERE rolled. `order <sprite> mark <lock>` runs the
|
|
7
|
+
* real verb with the sprite as actor -- commands/order.ts hands the
|
|
8
|
+
* string to NPC.tryRunCommand, commands/mark.ts re-dispatches into
|
|
9
|
+
* HackCommand, and hack.ts rolls Hack on the Fly against the device
|
|
10
|
+
* exactly as it would for a decker. The roll was then thrown away at the
|
|
11
|
+
* REPORTING layer, by a guard that has been copied into ~30 commands:
|
|
12
|
+
*
|
|
13
|
+
* if (this.scene.isHumanControlled(actor)) this.logger.meta(...)
|
|
14
|
+
*
|
|
15
|
+
* That asks "is the ROLLER a human". The question the Mechanics panel
|
|
16
|
+
* actually wants is "is a HUMAN WATCHING" -- and the codebase already
|
|
17
|
+
* answers it correctly two files over, in CombatExchange.playerWitnesses
|
|
18
|
+
* (utilities/combat-exchange.ts), which is precisely why a sprite's
|
|
19
|
+
* COMBAT rolls have always shown up while the same sprite's ordered
|
|
20
|
+
* mark went silent. One question, asked two ways, in one engine.
|
|
21
|
+
*
|
|
22
|
+
* WHY OWNERSHIP AND NOT PROXIMITY. playerWitnesses is a bystander test:
|
|
23
|
+
* same place, perceivable plane. That is right for a fight, which is an
|
|
24
|
+
* event in a room. It is wrong for an ORDER, which is a thing the player
|
|
25
|
+
* deliberately did: a rigger whose drone is sweeping the floor above,
|
|
26
|
+
* or a technomancer whose sprite is working a host their body is nowhere
|
|
27
|
+
* near, typed that command and is owed its dice. So a companion's rolls
|
|
28
|
+
* are shown to its master wherever it is standing. Anything that is
|
|
29
|
+
* NOT a companion -- IC, a guard, a passer-by -- keeps the old silence
|
|
30
|
+
* and reports through the channels it already had (utilities/ic.ts
|
|
31
|
+
* accumulates into IICTurn.meta; a fight goes through announce).
|
|
32
|
+
*
|
|
33
|
+
* THE FORK MUST STAY EXCLUSIVE. Several commands route the roll to
|
|
34
|
+
* EITHER the Mechanics panel (player) OR their own return string, which
|
|
35
|
+
* for an NPC is its AI history:
|
|
36
|
+
*
|
|
37
|
+
* const isPlayer = scene.isHumanControlled(actor);
|
|
38
|
+
* if (isPlayer) this.logger.meta(`Perception: ${formatRoll(roll)}`);
|
|
39
|
+
* const rollPrefix = isPlayer ? '' : `Perception: ${formatRoll(roll)}\n`;
|
|
40
|
+
*
|
|
41
|
+
* Both halves must read the SAME predicate or an ordered companion
|
|
42
|
+
* prints its dice twice. Callers swapping to showsMechanics must swap
|
|
43
|
+
* both.
|
|
44
|
+
*/
|
|
45
|
+
/** The companion ledger entry for this actor, if it is somebody's. */
|
|
46
|
+
function companionEntryFor(scene, actor) {
|
|
47
|
+
return scene.ownerGame?.companions?.find(c => c.npc === actor);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Should this actor's dice anatomy go to the Mechanics panel?
|
|
51
|
+
*
|
|
52
|
+
* True for a human-controlled actor (the old rule, unchanged), and for a
|
|
53
|
+
* companion a human is running -- a sprite, spirit, agent or drone whose
|
|
54
|
+
* master typed the order that got here.
|
|
55
|
+
*/
|
|
56
|
+
export function showsMechanics(scene, actor) {
|
|
57
|
+
if (scene.isHumanControlled?.(actor) === true)
|
|
58
|
+
return true;
|
|
59
|
+
return companionEntryFor(scene, actor) !== undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The name to hang on a Mechanics line when the roller is not the player
|
|
63
|
+
* -- "Crack Sprite -- Hack on the Fly (sleaze) on Hatch Maglock: ...".
|
|
64
|
+
* Empty for the player's own rolls, which stay exactly as they read
|
|
65
|
+
* today: a panel that started prefixing the player's own name on every
|
|
66
|
+
* line would be noisier for the common case to serve the rare one.
|
|
67
|
+
*/
|
|
68
|
+
export function mechanicsActorPrefix(scene, actor) {
|
|
69
|
+
return scene.isHumanControlled?.(actor) === true ? '' : `${actor.name} -- `;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=mechanics-audience.js.map
|
|
@@ -2,6 +2,7 @@ import { Category } from '../types/shared/item-enum.js';
|
|
|
2
2
|
import { inMeleeReach, isInCover, coverAvailableFor, actorDistanceMeters } from './spots.js';
|
|
3
3
|
import { Logger } from './logger.js';
|
|
4
4
|
import { runIcActionPhase } from './ic-brain.js';
|
|
5
|
+
import { moraleBandFor, moraleBreakLine, moraleBreaks } from '../npc-kits.js';
|
|
5
6
|
/**
|
|
6
7
|
* AN NPC'S ACTION PHASE, PLAYED BY RULE (SR5 p.163-167).
|
|
7
8
|
*
|
|
@@ -54,6 +55,27 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
54
55
|
tell(`${npc.name} looks for a target and finds none.`);
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
58
|
+
// MORALE (SR5 p.379-380, npc-kits.ts moraleBreaks): a team of grunts
|
|
59
|
+
// breaks at the book's casualty thresholds -- untrained when one
|
|
60
|
+
// drops, semi-trained past a quarter, trained past half, elite never.
|
|
61
|
+
// THE VERB IS SURRENDER where the book says flee / retreat / withdraw:
|
|
62
|
+
// this engine's one primitive for "stops fighting without dying" is
|
|
63
|
+
// `surrender`, and it carries the consequence the book intends -- the
|
|
64
|
+
// fight ends cleanly, no parting shot, and the pair drops out of
|
|
65
|
+
// stillHostile(). A grunt sent through `go` instead would stand in the
|
|
66
|
+
// next room still hostile and still on the contact ladder, which is
|
|
67
|
+
// the opposite of having broken. The THRESHOLD is canon; only the word
|
|
68
|
+
// is the engine's. Without this, a kitted ganger who fights to the
|
|
69
|
+
// death is a harder, longer, less canon fight than a naked one.
|
|
70
|
+
const team = enc.teamOf(npc);
|
|
71
|
+
if (team && moraleBreaks(team.effectivePr, team.casualties, team.total)) {
|
|
72
|
+
const r = await npc.actInCombat('surrender');
|
|
73
|
+
logger.write(`Brain: ${npc.name} morale breaks (${team.kit}: ${team.casualties}/${team.total} down, PR ${team.effectivePr}) -> ${r}`);
|
|
74
|
+
if (npc.surrendered) {
|
|
75
|
+
tell(moraleBreakLine(npc.name, moraleBandFor(team.effectivePr)));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
57
79
|
const target = pickTarget(npc, enemies);
|
|
58
80
|
npc.combatOpponent = target;
|
|
59
81
|
npc.lastExchangeAt = Date.now();
|
|
@@ -109,6 +131,7 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
109
131
|
}
|
|
110
132
|
const b = enc.budgetOf(npc);
|
|
111
133
|
if (b?.complexAvailable) {
|
|
134
|
+
spendGroupEdge(enc, npc, tell);
|
|
112
135
|
const r = await npc.actInCombat(`attack ${target.name}`);
|
|
113
136
|
logger.write(`Brain: ${npc.name} attack ${target.name} -> ${r.split('\n')[0]}`);
|
|
114
137
|
if (!enc.budgetOf(npc)?.attackTaken)
|
|
@@ -126,6 +149,7 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
126
149
|
continue;
|
|
127
150
|
}
|
|
128
151
|
if (!budget.attackTaken && budget.simple >= 1) {
|
|
152
|
+
spendGroupEdge(enc, npc, tell);
|
|
129
153
|
const r = await npc.actInCombat(`attack ${target.name}`);
|
|
130
154
|
logger.write(`Brain: ${npc.name} attack ${target.name} -> ${r.split('\n')[0]}`);
|
|
131
155
|
if (!enc.budgetOf(npc)?.attackTaken) {
|
|
@@ -139,6 +163,18 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
139
163
|
return;
|
|
140
164
|
}
|
|
141
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* GROUP EDGE (SR5 p.380): the gamemaster spends the team's pool on a
|
|
168
|
+
* grunt about to strike. The engine is the gamemaster; the policy is
|
|
169
|
+
* CombatEncounter.spendGroupEdge's. One line to the room when it lands,
|
|
170
|
+
* so the runner knows why that shot rolled hot.
|
|
171
|
+
*/
|
|
172
|
+
function spendGroupEdge(enc, npc, tell) {
|
|
173
|
+
if (npc.kit === undefined)
|
|
174
|
+
return;
|
|
175
|
+
if (enc.spendGroupEdge(npc))
|
|
176
|
+
tell(`${npc.name} digs deep.`);
|
|
177
|
+
}
|
|
142
178
|
/** The opponent already engaged, else the nearest of the rest. */
|
|
143
179
|
function pickTarget(npc, enemies) {
|
|
144
180
|
const engaged = npc.combatOpponent;
|
|
@@ -184,6 +184,8 @@ export function serializeItem(item) {
|
|
|
184
184
|
out.revealedTo = [...item.revealedTo];
|
|
185
185
|
if (item.isCyberdeck() && item.deckOrder.join() !== '0,1,2,3')
|
|
186
186
|
out.deckOrder = item.deckOrder;
|
|
187
|
+
if (item.ruined)
|
|
188
|
+
out.ruined = true;
|
|
187
189
|
return out;
|
|
188
190
|
}
|
|
189
191
|
/** Mirrors ItemFactory.createItemFromJson (synchronously), then lays
|
|
@@ -223,6 +225,8 @@ export function restoreItem(json) {
|
|
|
223
225
|
item.jammed = true;
|
|
224
226
|
if (json.bricked)
|
|
225
227
|
item.bricked = true;
|
|
228
|
+
if (json.ruined)
|
|
229
|
+
item.ruined = true;
|
|
226
230
|
if (json.deckDamage)
|
|
227
231
|
item.takeDeckDamage(json.deckDamage);
|
|
228
232
|
if (json.droneDamage)
|
|
@@ -601,6 +605,9 @@ function serializeRoomOverlay(room, player) {
|
|
|
601
605
|
inventory: b.inventory.getAllItems().map(serializeItem),
|
|
602
606
|
})),
|
|
603
607
|
searchedBy: [...room.searchedBy],
|
|
608
|
+
// A SANCTIONED HOST KEEPS NO INDEX EITHER, for the same reason it
|
|
609
|
+
// forgets a crack: a haven is not a target you cased.
|
|
610
|
+
hostSearchedBy: room.hostSanctioned ? [] : [...(room.host?.searchedBy ?? [])],
|
|
604
611
|
visitedBy: [...room.visitedBy],
|
|
605
612
|
// A SANCTIONED HOST NEVER REMEMBERS A CRACK (Room.hostSanctioned).
|
|
606
613
|
// The overlay rule is right for run content -- you broke that
|
|
@@ -771,6 +778,10 @@ export function applyHubOverlay(scene, saved, player, homeRoom) {
|
|
|
771
778
|
return { name: b.name, cell: { ...b.cell }, inventory: held };
|
|
772
779
|
});
|
|
773
780
|
room.searchedBy = new Set(overlay.searchedBy);
|
|
781
|
+
// Only onto a room that HAS a host -- a seed that dropped its node
|
|
782
|
+
// between saves must not grow one back through the overlay.
|
|
783
|
+
if (room.host)
|
|
784
|
+
room.host.searchedBy = new Set(overlay.hostSearchedBy ?? []);
|
|
774
785
|
room.visitedBy = new Set(overlay.visitedBy);
|
|
775
786
|
// Both ends, so an OLD save written before hostSanctioned existed
|
|
776
787
|
// cannot restore a cracked haven either.
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export const PROGRAMS = [
|
|
16
16
|
// ---- Common programs (p.245; pricing canon-approximate) ----
|
|
17
|
-
|
|
17
|
+
// BROWSE BUYS TIME, NOT DICE (Splintered State p.54: it "cuts the base
|
|
18
|
+
// time in half"). The catalogue advertised "+2 dice on Matrix Search"
|
|
19
|
+
// and no code ever added them -- utilities/perception.ts has always
|
|
20
|
+
// halved the clock, which is the half that is right. A program whose
|
|
21
|
+
// shop blurb promises a bonus it does not grant is a lie the player
|
|
22
|
+
// pays 80 nuyen for.
|
|
23
|
+
{ key: 'browse', name: 'Browse', kind: 'common', cost: 80, availability: 2, effect: 'Halves the base time of a Matrix Search (p.241).' },
|
|
18
24
|
{ key: 'toolbox', name: 'Toolbox', kind: 'common', cost: 80, availability: 2, effect: '+1 Data Processing.' },
|
|
19
25
|
{ key: 'encryption', name: 'Encryption', kind: 'common', cost: 80, availability: 2, effect: '+1 Firewall.' },
|
|
20
26
|
{ key: 'signal-scrub', name: 'Signal Scrub', kind: 'common', cost: 80, availability: 2, effect: 'Cleans your noise floor: +1 die defending your PAN.' },
|