@maka/maka-cli 5.167.0 → 5.170.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.
@@ -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 (!host || !named)
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 matches = devices.filter(d => d.name.toLowerCase().includes(raw)
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
- if (matches.length !== 1)
468
- return null;
469
- const device = matches[0];
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
- const defense = deviceDefensePool(device);
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