@maka/maka-cli 5.167.0 → 5.169.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/archetypes.js +238 -146
- package/bundle/typescript/src/commands/game/sideQuest/commands/attack.js +27 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/climb.js +21 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +54 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +381 -255
- package/bundle/typescript/src/commands/game/sideQuest/utilities/climb-state.js +51 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/matrix-intrusion.js +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +10 -6
- package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js +41 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +31 -0
- package/package.json +1 -1
|
@@ -6,7 +6,8 @@ import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
|
6
6
|
import { describeFledTarget } from '../utilities/pursuit.js';
|
|
7
7
|
import { shellsInRoom, resolveBodyAttack, crossPlaneRefusal } from '../utilities/planes.js';
|
|
8
8
|
import { heldBy, holding } from '../utilities/grapple.js';
|
|
9
|
-
import { spotOf, ensureAtSpot, ensureAtExit, canReach, spotDistanceMeters, OPEN_FLOOR } from '../utilities/spots.js';
|
|
9
|
+
import { spotOf, ensureAtSpot, ensureAtExit, canReach, spotDistanceMeters, OPEN_FLOOR, inMeleeReach, actorCell, standingRoomNear, spotForCell } from '../utilities/spots.js';
|
|
10
|
+
import { distanceMeters } from '../utilities/room-grid.js';
|
|
10
11
|
import { resolveBarrierStrike } from '../utilities/barrier-combat.js';
|
|
11
12
|
import { billAction, notYourPhase, phaseHint } from '../utilities/action-cost.js';
|
|
12
13
|
import { spendMovementMeters } from '../utilities/movement-cost.js';
|
|
@@ -400,6 +401,31 @@ export class AttackCommand extends Command {
|
|
|
400
401
|
if (reach.line)
|
|
401
402
|
report.push(reach.line);
|
|
402
403
|
}
|
|
404
|
+
// THE LAST FEW PACES (HBssKNQahWxGR95wB: "Ellis is able to attack
|
|
405
|
+
// me, however his position is several squares away from me, not
|
|
406
|
+
// in melee range"). Sharing a SPOT is not standing together: the
|
|
407
|
+
// open floor is one spot across most of a room, and bodies hold
|
|
408
|
+
// their own cells on it. The gate above closes spot to spot and
|
|
409
|
+
// stopped there, so a swing landed from wherever on the floor the
|
|
410
|
+
// attacker happened to be seated. Close cell to cell the way
|
|
411
|
+
// "move to <person>" does (move.ts crossToActor) -- billed as
|
|
412
|
+
// ground in a fight, walked for free outside one -- and if the
|
|
413
|
+
// ground can't be covered this phase, the blow does not happen.
|
|
414
|
+
if (!inMeleeReach(actor, target)) {
|
|
415
|
+
const theirs = actorCell(room, target);
|
|
416
|
+
const cell = theirs ? standingRoomNear(room, theirs, actor) : undefined;
|
|
417
|
+
if (cell) {
|
|
418
|
+
const from = actorCell(room, actor);
|
|
419
|
+
const paces = from ? distanceMeters(from, cell) : 0;
|
|
420
|
+
const spend = spendMovementMeters(this.scene, actor, paces, 'walk', this.logger);
|
|
421
|
+
if (spend)
|
|
422
|
+
return `${target.name} is ${Math.round(paces)} m away. ${spend}`;
|
|
423
|
+
actor.atCell = cell;
|
|
424
|
+
actor.atSpot = spotForCell(room, cell);
|
|
425
|
+
actor.performAction('steps up beside', target.name);
|
|
426
|
+
report.push(`${actor === this.scene.getPlayer() ? 'You close' : `${actor.name} closes`} the last few paces on ${target.name}.`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
403
429
|
}
|
|
404
430
|
// THE ACTION.
|
|
405
431
|
const label = isMelee ? (this.usesEquippedWeapon() ? 'Melee Attack' : 'Subdue') : 'Fire Weapon';
|
|
@@ -4,6 +4,7 @@ import { hint } from '../utilities/hints.js';
|
|
|
4
4
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
5
5
|
import { spotOf, spotsActive, sealedRouteTo } from '../utilities/spots.js';
|
|
6
6
|
import { distanceMeters } from '../utilities/room-grid.js';
|
|
7
|
+
import { onTheWall } from '../utilities/climb-state.js';
|
|
7
8
|
class VerticalCommand extends Command {
|
|
8
9
|
async travel(way, args) {
|
|
9
10
|
const actor = this.actor;
|
|
@@ -19,6 +20,26 @@ class VerticalCommand extends Command {
|
|
|
19
20
|
// rule move applies to every other crossing.
|
|
20
21
|
const from = mine ? grid.spotCells.get(mine) : undefined;
|
|
21
22
|
const myZ = from?.z ?? 0;
|
|
23
|
+
// ALREADY ON A WALL (6SmvvRRtn9eEMZSbF: "when I'm 1m up, and I use
|
|
24
|
+
// 'descend', it says I'm already on the floor").
|
|
25
|
+
//
|
|
26
|
+
// Destinations here are chosen by STOREY, and a runner a metre up
|
|
27
|
+
// a climb is still, by spot, on the storey they left. So "descend"
|
|
28
|
+
// looked for a level below the floor, found none, and reported the
|
|
29
|
+
// ground floor -- true, and useless to someone hanging off a wall.
|
|
30
|
+
// Part-way up, the two verbs mean the two directions along the
|
|
31
|
+
// wall you are on: the one the climb is going is more of it, the
|
|
32
|
+
// other is the way back. Naming a different place falls through to
|
|
33
|
+
// move, which refuses it (you are not on the floor to cross).
|
|
34
|
+
const wall = onTheWall(actor);
|
|
35
|
+
const query = (args ?? []).filter(w => !/^(to|the|a|an|up|down)$/i.test(w)).join(' ').trim();
|
|
36
|
+
if (wall && (query.length === 0 || fuzzyPickName(query, [wall.target, wall.from]) !== undefined)) {
|
|
37
|
+
const onward = way === 'up' ? wall.up : !wall.up;
|
|
38
|
+
const mover = new MoveCommand({
|
|
39
|
+
actor, scene: this.scene, rooms: this.rooms, screen: this.screen, game: this.game,
|
|
40
|
+
});
|
|
41
|
+
return onward ? await mover.execute([wall.target]) : await mover.retreatFromClimb();
|
|
42
|
+
}
|
|
22
43
|
// Every spot on a level in the direction asked for.
|
|
23
44
|
const candidates = [...grid.spotCells.entries()]
|
|
24
45
|
.filter(([, c]) => (way === 'up' ? c.z > myZ : c.z < myZ))
|
|
@@ -45,7 +66,6 @@ class VerticalCommand extends Command {
|
|
|
45
66
|
: `You're on the ground floor already.${hint(` ("${other}" goes up.)`)}`;
|
|
46
67
|
}
|
|
47
68
|
let target = candidates[0];
|
|
48
|
-
const query = (args ?? []).filter(w => !/^(to|the|a|an|up|down)$/i.test(w)).join(' ').trim();
|
|
49
69
|
if (query.length > 0) {
|
|
50
70
|
const picked = fuzzyPickName(query, candidates);
|
|
51
71
|
if (!picked) {
|
|
@@ -10,6 +10,7 @@ import { hint } from '../utilities/hints.js';
|
|
|
10
10
|
import { fuzzyPickName } from '../utilities/fuzzy-match.js';
|
|
11
11
|
import { gridIconBlock } from '../utilities/grid-view.js';
|
|
12
12
|
import { requirePhase } from '../utilities/action-cost.js';
|
|
13
|
+
import { midClimbRefusal } from '../utilities/climb-state.js';
|
|
13
14
|
import { camerasLive } from '../utilities/surveillance.js';
|
|
14
15
|
import { maybeSinSweep } from '../utilities/sin.js';
|
|
15
16
|
import { despawnEphemerals } from '../utilities/ephemeral.js';
|
|
@@ -167,6 +168,12 @@ export class GoCommand extends Command {
|
|
|
167
168
|
if (gripHeld) {
|
|
168
169
|
return `You're holding ${gripHeld.name} -- walk away and the hold is gone. "release" first if that's the plan.`;
|
|
169
170
|
}
|
|
171
|
+
// ON A WALL, NOT ON THE FLOOR (6SmvvRRtn9eEMZSbF): a runner
|
|
172
|
+
// part-way up a climb has no door within reach. Same gate as
|
|
173
|
+
// move.ts; the refusal names both ways off the wall.
|
|
174
|
+
const wall = midClimbRefusal(this.actor);
|
|
175
|
+
if (wall)
|
|
176
|
+
return wall;
|
|
170
177
|
// THE COMBAT TURN (utilities/combat-turn.ts): leaving the room is
|
|
171
178
|
// movement, and movement happens on your own Action Phase.
|
|
172
179
|
const phase = requirePhase(this.scene, this.actor);
|
|
@@ -12,7 +12,7 @@ import { matchesCameraName, hostLabel } from '../utilities/grid-names.js';
|
|
|
12
12
|
import { TapCommand } from './tap.js';
|
|
13
13
|
import { sameHostSide } from '../models/player.js';
|
|
14
14
|
import { MAX_MARKS } from '../utilities/marks.js';
|
|
15
|
-
import { declarationPenalty, parseMarkDeclaration, hostDefensePool, freeMatrixPerceptionHits, bruteForceMatrixDv, overwatchFromDefense, GO_BIG_QUALITY, deviceDefensePool } from '../utilities/matrix-intrusion.js';
|
|
15
|
+
import { declarationPenalty, parseMarkDeclaration, hostDefensePool, wanDefensePool, freeMatrixPerceptionHits, bruteForceMatrixDv, overwatchFromDefense, GO_BIG_QUALITY, deviceDefensePool } from '../utilities/matrix-intrusion.js';
|
|
16
16
|
/**
|
|
17
17
|
* MARKS A PAN INTRUSION MUST ALREADY HOLD before it can command the
|
|
18
18
|
* device rather than merely key it (Control Device, p.238: 1 mark for a
|
|
@@ -453,20 +453,53 @@ export class HackCommand extends BypassCommand {
|
|
|
453
453
|
*/
|
|
454
454
|
async tryDeviceHack(args, mode, declaredMarks) {
|
|
455
455
|
const actor = this.actor;
|
|
456
|
-
const host = actor.insideHost;
|
|
457
456
|
const named = args.join(' ').trim();
|
|
458
|
-
if (!
|
|
459
|
-
return null;
|
|
460
|
-
const devices = host.openableDevices().filter(d => d.rating > 0);
|
|
461
|
-
if (devices.length === 0)
|
|
457
|
+
if (!named)
|
|
462
458
|
return null;
|
|
463
459
|
const raw = named.toLowerCase();
|
|
464
|
-
const
|
|
460
|
+
const matching = (room) => room.openableDevices().filter(d => d.rating > 0).filter(d => d.name.toLowerCase().includes(raw)
|
|
465
461
|
|| (d.opensExit ?? '').toLowerCase() === raw
|
|
466
462
|
|| d.description.toLowerCase().includes(raw));
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
463
|
+
// INSIDE THE HOST you are directly connected to every device on
|
|
464
|
+
// its WAN (p.233; Data Trails p.87-88): the slave defends with its
|
|
465
|
+
// own rating and nothing more.
|
|
466
|
+
let host = actor.insideHost;
|
|
467
|
+
let viaWan = false;
|
|
468
|
+
let device;
|
|
469
|
+
if (host) {
|
|
470
|
+
const matches = matching(host);
|
|
471
|
+
if (matches.length !== 1)
|
|
472
|
+
return null;
|
|
473
|
+
device = matches[0];
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
// FROM THE GRID (ZdhkP8mAP9QhpqFvA: "shouldn't I be able to hack
|
|
477
|
+
// the Warehouse maglock, it has DR 2 and I can see the icon").
|
|
478
|
+
// The icon IS on the map, and this fell through to "nothing on
|
|
479
|
+
// the grid answers to 'maglock'" -- a lie about a thing the same
|
|
480
|
+
// screen had just drawn. Canon (p.233, p.355-356): a device on a
|
|
481
|
+
// host's WAN "cannot be accessed without first gaining access
|
|
482
|
+
// (via a mark) to the Host itself", and when it IS attacked
|
|
483
|
+
// through the network it defends with its own or its master's
|
|
484
|
+
// rating, whichever is higher, per rating. So: no mark on the
|
|
485
|
+
// host, the lock is named and refused with the rule; a mark
|
|
486
|
+
// held, the lock is hackable from out here against the host's
|
|
487
|
+
// numbers. Getting inside is still the way to face the lock's
|
|
488
|
+
// own DR alone.
|
|
489
|
+
const rooms = this.rooms ?? this.scene.getRooms();
|
|
490
|
+
const found = hostsInReach(rooms, actor)
|
|
491
|
+
.map(r => ({ room: r, matches: matching(r) }))
|
|
492
|
+
.filter(x => x.matches.length === 1);
|
|
493
|
+
if (found.length !== 1)
|
|
494
|
+
return null;
|
|
495
|
+
host = found[0].room;
|
|
496
|
+
device = found[0].matches[0];
|
|
497
|
+
viaWan = true;
|
|
498
|
+
const onHost = host.hostMarksBy.get(actor.name) ?? 0;
|
|
499
|
+
if (onHost < 1) {
|
|
500
|
+
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.)`)}`;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
470
503
|
const held = device.marksBy.get(actor.name) ?? 0;
|
|
471
504
|
if (held >= MAX_MARKS) {
|
|
472
505
|
return `You already hold ${MAX_MARKS} marks on ${device.name} -- it answers to you as readily as to the house.`;
|
|
@@ -484,7 +517,9 @@ export class HackCommand extends BypassCommand {
|
|
|
484
517
|
+ silence + actor.woundModifier - actor.sustainingPenalty + declarePenalty);
|
|
485
518
|
actor.performAction('works a slaved icon', device.name);
|
|
486
519
|
const roll = rollPool(pool, limit, { gremlins: actor.deckGremlins });
|
|
487
|
-
|
|
520
|
+
// From the grid the slave borrows its master's numbers where they are
|
|
521
|
+
// higher (p.233); inside, it stands alone (Data Trails p.87-88).
|
|
522
|
+
const defense = viaWan ? wanDefensePool(device, host.host) : deviceDefensePool(device);
|
|
488
523
|
const defenseRoll = rollPool(defense.pool);
|
|
489
524
|
const netHits = roll.hits - defenseRoll.hits;
|
|
490
525
|
if (this.scene.isHumanControlled(actor)) {
|
|
@@ -507,6 +542,14 @@ export class HackCommand extends BypassCommand {
|
|
|
507
542
|
}
|
|
508
543
|
const now = Math.min(MAX_MARKS, held + declared);
|
|
509
544
|
device.marksBy.set(actor.name, now);
|
|
545
|
+
// A MARK ON A SLAVE IS A MARK ON THE MASTER (p.233: "if you get a
|
|
546
|
+
// mark on a slave you also get a mark on the master"; Data Trails
|
|
547
|
+
// p.87-88 works it: "1 mark on the device and its master (the
|
|
548
|
+
// host)"). One, not `declared` -- the declaration bought marks on
|
|
549
|
+
// the lock; the host gets the one canon hands over with it.
|
|
550
|
+
const onHost = host.hostMarksBy.get(actor.name) ?? 0;
|
|
551
|
+
if (onHost < MAX_MARKS)
|
|
552
|
+
host.hostMarksBy.set(actor.name, onHost + 1);
|
|
510
553
|
this.logger.write(`Mark: ${actor.name} -> device ${device.name} (${now}/${MAX_MARKS}, ${mode}).`);
|
|
511
554
|
const lines = [`${device.name} takes your key -- ${now} of ${MAX_MARKS} mark${now === 1 ? '' : 's'} on it now.`];
|
|
512
555
|
// BRUTE FORCE BURNS WHAT IT FORCES (p.238): 1 DV per two full net
|