@maka/maka-cli 5.163.0 → 5.165.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/factories/scene-factory.js +33 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +138 -61
- package/bundle/typescript/src/commands/game/sideQuest/headless.js +29 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/condition-report.js +25 -4
- package/bundle/typescript/src/commands/game/sideQuest/utilities/npc-combat-brain.js +33 -2
- package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +9 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.165.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.",
|
|
@@ -408,6 +408,39 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
|
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
+
// Step 4.95: NO DOOR SHIPS LOCKED WITHOUT A WAY THROUGH
|
|
412
|
+
// (S8WdW7gm9tFGaxeCi, 2026-09-07: "I broke the gate, but the door is
|
|
413
|
+
// locked. It doesn't look like it has a way to open it"). A seed
|
|
414
|
+
// exit's `keyRequired` builds a locked Door with no Device: "pick",
|
|
415
|
+
// "breach" and "hack" all answer "nothing here", the refusal names
|
|
416
|
+
// no route, and the only verb that works ("unlock <dir>" while
|
|
417
|
+
// carrying the key) was never surfaced. The device path was
|
|
418
|
+
// hardened against exactly this (seed-migration synthesises a code
|
|
419
|
+
// rather than ship a routeless barrier); the keyed path never was.
|
|
420
|
+
//
|
|
421
|
+
// Same clamp-don't-throw rule as the keyItem repair above: a key
|
|
422
|
+
// that resolves (exactly or by the same fuzzy match) is written back
|
|
423
|
+
// under its real name so "unlock" finds it; a key that resolves to
|
|
424
|
+
// nothing means the door cannot be opened by anyone, ever, and it
|
|
425
|
+
// ships UNLOCKED with the error logged. Runs on every seed path --
|
|
426
|
+
// static, generated, repro, and the hub seed inside every save.
|
|
427
|
+
for (const roomJson of json.rooms) {
|
|
428
|
+
const room = scene.getRoom(roomJson.name);
|
|
429
|
+
if (!room)
|
|
430
|
+
continue;
|
|
431
|
+
for (const [direction, exit] of room.exits.entries()) {
|
|
432
|
+
if (!(exit instanceof Door) || !exit.checkIfLocked() || exit.heldBy)
|
|
433
|
+
continue;
|
|
434
|
+
const wanted = exit.unlockItemOrSolution;
|
|
435
|
+
const key = wanted ? findItem(wanted) : undefined;
|
|
436
|
+
if (key) {
|
|
437
|
+
exit.unlockItemOrSolution = key.name;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
exit.isLocked = false;
|
|
441
|
+
logger.error(`Scene "${json.name}": the door ${String(direction)} of ${room.name} was locked by "${wanted ?? '(nothing)'}", which no device holds and no item matches -- it ships unlocked rather than as a wall.`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
411
444
|
// Step 4.05: NPCs wear their gear. "heldBy" only puts items in an NPC's
|
|
412
445
|
// inventory, but combat pools (see Player.getWeaponProfile /
|
|
413
446
|
// getArmorValue) read from EQUIPPED slots -- without this, an enforcer
|
|
@@ -2380,6 +2380,8 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
2380
2380
|
* unchanged.
|
|
2381
2381
|
*/
|
|
2382
2382
|
_conditionSink;
|
|
2383
|
+
/** A hosted run's aftermath still owed to this hub (IPendingHomecoming). */
|
|
2384
|
+
_pendingHomecoming;
|
|
2383
2385
|
constructor(player, sceneSeed, conditionSink) {
|
|
2384
2386
|
this._conditionSink = conditionSink;
|
|
2385
2387
|
this.player = player;
|
|
@@ -3043,6 +3045,12 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3043
3045
|
// party-loadable within moments of launch now -- requestSave pushes
|
|
3044
3046
|
// the cloud mirror behind the local write.
|
|
3045
3047
|
if (this._hubScene && this.scene === this._hubScene) {
|
|
3048
|
+
// A HOSTED RUN'S AFTERMATH, SETTLED AT THE DOOR (8izFwokbehjHzkEAn,
|
|
3049
|
+
// 3gFcHfheGSMPtpCFh): the site wrote the runner back with a
|
|
3050
|
+
// homecoming owed; this is the hub entry that pays it.
|
|
3051
|
+
const owed = this.applyHostedHomecoming();
|
|
3052
|
+
if (owed)
|
|
3053
|
+
Logger.getInstance().logWithColor(owed, 'green');
|
|
3046
3054
|
this.requestSave('hub-arrival');
|
|
3047
3055
|
}
|
|
3048
3056
|
// A REGISTERED sprite from the save re-shells at the cot (the
|
|
@@ -3184,6 +3192,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3184
3192
|
}
|
|
3185
3193
|
applyHubOverlay(this.scene, save.hub, this.player, this._homeRoom);
|
|
3186
3194
|
this._tier = save.game.tier;
|
|
3195
|
+
this._pendingHomecoming = save.game.pendingHomecoming;
|
|
3187
3196
|
this.contacts = new Map(save.game.contacts);
|
|
3188
3197
|
// Roles are DERIVED, not earned -- re-derive them all on every
|
|
3189
3198
|
// resume so old saves self-heal (pre-role saves read all "Street
|
|
@@ -3469,6 +3478,7 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3469
3478
|
return all.length > 0 ? all : undefined;
|
|
3470
3479
|
})(),
|
|
3471
3480
|
crew: this.crew.map(m => ({ ...m, gear: [...m.gear] })),
|
|
3481
|
+
pendingHomecoming: this._pendingHomecoming,
|
|
3472
3482
|
commandRefusals: { ...this.commandRefusals },
|
|
3473
3483
|
fencedCategories: [...this.fencedCategories],
|
|
3474
3484
|
rumorIndex: this._rumorIndex,
|
|
@@ -3843,6 +3853,50 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3843
3853
|
contact.asked = false;
|
|
3844
3854
|
// Fresh material for the buskers too (commands/perform.ts).
|
|
3845
3855
|
this.performedThisCycle = false;
|
|
3856
|
+
// LIFESTYLE upkeep and the High-lifestyle shower: settleLifestyleUpkeep,
|
|
3857
|
+
// shared with the hosted run's owed homecoming (applyHostedHomecoming).
|
|
3858
|
+
const { rentLine, comfortLine } = this.settleLifestyleUpkeep();
|
|
3859
|
+
// Fresh streets, fresh gossip, restocked shelves, new work.
|
|
3860
|
+
this.advanceRumor();
|
|
3861
|
+
this.fenceSellsOn();
|
|
3862
|
+
const restocked = this.restockVendorShelves();
|
|
3863
|
+
const restockLine = restocked > 0 ? `\nThe district has restocked around you -- the wok steams, the bar's shelf is full, the market stalls are loud again.` : '';
|
|
3864
|
+
this.advanceGig();
|
|
3865
|
+
this.repaintForScene();
|
|
3866
|
+
this.refreshLogLabel();
|
|
3867
|
+
Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
|
|
3868
|
+
// Drones spin back up beside the cot.
|
|
3869
|
+
const companionArrivalLines = this.unfoldCompanionsOnArrival();
|
|
3870
|
+
const companionBlock = [...companionFoldLines, ...companionArrivalLines];
|
|
3871
|
+
// The commanded ally's tour ends with the run (RAW's Command lasts
|
|
3872
|
+
// turns; ours lasts the job): the crew disbands at the curb.
|
|
3873
|
+
// The crew stays hired between jobs (the roster persists); their
|
|
3874
|
+
// homecoming lines (obituaries, write-back) rode in above. Fresh
|
|
3875
|
+
// faces on the for-hire board each homecoming.
|
|
3876
|
+
this.rollHireCandidates();
|
|
3877
|
+
// Social settlement: the street hears you're home.
|
|
3878
|
+
this.beatPresence();
|
|
3879
|
+
const allyLine = crewLines.length > 0 ? `\n${crewLines.join('\n')}` : '';
|
|
3880
|
+
const docwagonLine = this.tickDocwagonHomecoming();
|
|
3881
|
+
const deliveryLine = this.deliverProcurements();
|
|
3882
|
+
// The night catches up on the ride (SINs & getting caught): a hot
|
|
3883
|
+
// run means the wire between there and home. Settled AFTER the
|
|
3884
|
+
// payout (the fee comes out of tonight's take) and BEFORE the save
|
|
3885
|
+
// below -- a burned fake or an issued SIN must survive a quit.
|
|
3886
|
+
const checkpointBlock = this.checkpointOnRideHome();
|
|
3887
|
+
// The homecoming save: everything above (payout, rent, restock,
|
|
3888
|
+
// contacts reset, deliveries, the checkpoint) settled first, so the
|
|
3889
|
+
// save reflects the hub as the player will find it.
|
|
3890
|
+
this.requestSave('homecoming');
|
|
3891
|
+
const fateLine = wrappedRun
|
|
3892
|
+
? `The run's wrapped and the street swallows you back up.`
|
|
3893
|
+
: `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
|
|
3894
|
+
return `${checkpointBlock ? `\n${checkpointBlock}\n` : ''}\n${this._hubName.toUpperCase()}\n${fateLine} You're home -- your hideout, one door off ${hub.determineStartRoom().name}.${hint(` Call ${FIXER_NAME} when you're hungry for the next job.`)}${clientLine}${awarenessLine}\n${rentLine}${comfortLine}${restockLine}${deliveryLine}${docwagonLine}${allyLine}${companionBlock.length > 0 ? `\n${companionBlock.join('\n')}` : ''}`;
|
|
3895
|
+
}
|
|
3896
|
+
/** The rent, the slide, the tab, and the High-lifestyle shower --
|
|
3897
|
+
* homecoming's money half, for the solo ride home and the hosted
|
|
3898
|
+
* run's owed homecoming alike. */
|
|
3899
|
+
settleLifestyleUpkeep() {
|
|
3846
3900
|
// LIFESTYLE upkeep comes due with the homecoming: this tier's share
|
|
3847
3901
|
// plus any standing debt, paid from carried cred as far as it goes.
|
|
3848
3902
|
// Coming up short SLIDES YOU DOWN a tier (the district notices) and
|
|
@@ -3887,75 +3941,98 @@ ${client.name} won't be remembering anything. Dead Johnsons pay nothing -- and t
|
|
|
3887
3941
|
const cleared = this.player.healStun(this.player.stunTaken);
|
|
3888
3942
|
comfortLine = `\nThe shower runs hot until the run is a rumor -- ${cleared} stun gone before your head hits the pillow.`;
|
|
3889
3943
|
}
|
|
3890
|
-
|
|
3944
|
+
return { rentLine, comfortLine };
|
|
3945
|
+
}
|
|
3946
|
+
/** DOCWAGON's year ticks in homecomings (12 = a street year, player
|
|
3947
|
+
* ruling): the contract lapses quietly unless renewed at Doc's. */
|
|
3948
|
+
tickDocwagonHomecoming() {
|
|
3949
|
+
if (!this.docwagon)
|
|
3950
|
+
return '';
|
|
3951
|
+
this.docwagon.homecomingsLeft -= 1;
|
|
3952
|
+
if (this.docwagon.homecomingsLeft <= 0) {
|
|
3953
|
+
const tier = this.docwagon.tier;
|
|
3954
|
+
this.docwagon = undefined;
|
|
3955
|
+
return `\nYour DocWagon ${tier} contract LAPSES -- the wristband goes dark.${hint(` ("docwagon <tier>" at Doc's renews it.)`)}`;
|
|
3956
|
+
}
|
|
3957
|
+
if (this.docwagon.homecomingsLeft <= 2) {
|
|
3958
|
+
return `\nDocWagon renewal coming due: ${this.docwagon.homecomingsLeft} homecoming${this.docwagon.homecomingsLeft === 1 ? '' : 's'} left on the ${this.docwagon.tier} contract.`;
|
|
3959
|
+
}
|
|
3960
|
+
return '';
|
|
3961
|
+
}
|
|
3962
|
+
/** SWAG deliveries (commands/source.ts): the contact's shopping run
|
|
3963
|
+
* lands with the homecoming -- straight into your gear, paid for at
|
|
3964
|
+
* order time. */
|
|
3965
|
+
deliverProcurements() {
|
|
3966
|
+
if (this.pendingProcurements.length === 0)
|
|
3967
|
+
return '';
|
|
3968
|
+
const delivered = [];
|
|
3969
|
+
for (const order of this.pendingProcurements.splice(0)) {
|
|
3970
|
+
try {
|
|
3971
|
+
const item = restoreItem(order.itemConfig);
|
|
3972
|
+
item.owner = this.player.name;
|
|
3973
|
+
this.player.inventory.addItem(item);
|
|
3974
|
+
delivered.push(`${item.name} (${order.contact} came through)`);
|
|
3975
|
+
}
|
|
3976
|
+
catch (err) {
|
|
3977
|
+
Logger.getInstance().error(`Procurement delivery failed: ${err}`);
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
if (delivered.length === 0)
|
|
3981
|
+
return '';
|
|
3982
|
+
this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
|
|
3983
|
+
return `\nWaiting with your gear: ${delivered.join(', ')}.`;
|
|
3984
|
+
}
|
|
3985
|
+
/**
|
|
3986
|
+
* A HOSTED RUN'S HOMECOMING (8izFwokbehjHzkEAn, 3gFcHfheGSMPtpCFh): the
|
|
3987
|
+
* aftermath returnToHub settles for a solo ride home, applied ONCE for
|
|
3988
|
+
* a run that ended on the site -- from the flag the server left on the
|
|
3989
|
+
* save (IPendingHomecoming), or handed in directly by a CLI that rode a
|
|
3990
|
+
* shared table. What it settles: the loud-run awareness point, the
|
|
3991
|
+
* Johnson's opinion, the crew's tally, contacts' legwork, rent and the
|
|
3992
|
+
* slide, the shower, restock/rumor/gig, the hire board, the DocWagon
|
|
3993
|
+
* year, deliveries. What it does not: the checkpoint on the ride home
|
|
3994
|
+
* (heat is the run's, and the run is gone) and crew gear write-back
|
|
3995
|
+
* (their shells never stood in this process). Clears the flag and
|
|
3996
|
+
* writes the homecoming save. Undefined when nothing is owed, or when
|
|
3997
|
+
* the player is not standing in the hub.
|
|
3998
|
+
*/
|
|
3999
|
+
applyHostedHomecoming(info) {
|
|
4000
|
+
const owed = info ?? this._pendingHomecoming;
|
|
4001
|
+
this._pendingHomecoming = undefined;
|
|
4002
|
+
if (!owed)
|
|
4003
|
+
return undefined;
|
|
4004
|
+
if (!this._hubScene || this.scene !== this._hubScene)
|
|
4005
|
+
return undefined;
|
|
4006
|
+
const awarenessLine = owed.alarmed && this.player.addPublicAwareness(`left "${owed.jobName ?? 'a job'}" screaming`)
|
|
4007
|
+
? `\nYou left the place howling behind you. Somebody filed a report, somebody kept a still -- your face is a little more public than it was this morning. (+1 Public Awareness, now ${this.player.publicAwareness})`
|
|
4008
|
+
: '';
|
|
4009
|
+
this.currentClient = owed.client ? { ...owed.client } : undefined;
|
|
4010
|
+
const clientLine = this.settleClientReputation(owed.wrapped);
|
|
4011
|
+
for (const member of this.crew) {
|
|
4012
|
+
if (owed.wrapped && !member.ghost?.liveOnly)
|
|
4013
|
+
member.jobsRun += 1;
|
|
4014
|
+
member.trainedThisCycle = false;
|
|
4015
|
+
}
|
|
4016
|
+
for (const contact of this.contacts.values())
|
|
4017
|
+
contact.asked = false;
|
|
4018
|
+
this.performedThisCycle = false;
|
|
4019
|
+
const { rentLine, comfortLine } = this.settleLifestyleUpkeep();
|
|
3891
4020
|
this.advanceRumor();
|
|
3892
4021
|
this.fenceSellsOn();
|
|
3893
4022
|
const restocked = this.restockVendorShelves();
|
|
3894
4023
|
const restockLine = restocked > 0 ? `\nThe district has restocked around you -- the wok steams, the bar's shelf is full, the market stalls are loud again.` : '';
|
|
3895
4024
|
this.advanceGig();
|
|
3896
|
-
this.repaintForScene();
|
|
3897
|
-
this.refreshLogLabel();
|
|
3898
|
-
Logger.getInstance().write(`Returned to hub "${this._hubName}". Lifestyle ${this.lifestyleTier}: ${paid} paid, ${this.rentDebt} owed. Restocked ${restocked}. Rumor: ${this.activeRumor ?? '(none)'}`);
|
|
3899
|
-
// Drones spin back up beside the cot.
|
|
3900
|
-
const companionArrivalLines = this.unfoldCompanionsOnArrival();
|
|
3901
|
-
const companionBlock = [...companionFoldLines, ...companionArrivalLines];
|
|
3902
|
-
// The commanded ally's tour ends with the run (RAW's Command lasts
|
|
3903
|
-
// turns; ours lasts the job): the crew disbands at the curb.
|
|
3904
|
-
// The crew stays hired between jobs (the roster persists); their
|
|
3905
|
-
// homecoming lines (obituaries, write-back) rode in above. Fresh
|
|
3906
|
-
// faces on the for-hire board each homecoming.
|
|
3907
4025
|
this.rollHireCandidates();
|
|
3908
|
-
|
|
3909
|
-
this.
|
|
3910
|
-
|
|
3911
|
-
// DOCWAGON's year ticks in homecomings (12 = a street year, player
|
|
3912
|
-
// ruling): the contract lapses quietly unless renewed at Doc's.
|
|
3913
|
-
let docwagonLine = '';
|
|
3914
|
-
if (this.docwagon) {
|
|
3915
|
-
this.docwagon.homecomingsLeft -= 1;
|
|
3916
|
-
if (this.docwagon.homecomingsLeft <= 0) {
|
|
3917
|
-
docwagonLine = `\nYour DocWagon ${this.docwagon.tier} contract LAPSES -- the wristband goes dark.${hint(` ("docwagon <tier>" at Doc's renews it.)`)}`;
|
|
3918
|
-
this.docwagon = undefined;
|
|
3919
|
-
}
|
|
3920
|
-
else if (this.docwagon.homecomingsLeft <= 2) {
|
|
3921
|
-
docwagonLine = `\nDocWagon renewal coming due: ${this.docwagon.homecomingsLeft} homecoming${this.docwagon.homecomingsLeft === 1 ? '' : 's'} left on the ${this.docwagon.tier} contract.`;
|
|
3922
|
-
}
|
|
3923
|
-
}
|
|
3924
|
-
// SWAG deliveries (commands/source.ts): the contact's shopping run
|
|
3925
|
-
// lands with the homecoming -- straight into your gear, paid for at
|
|
3926
|
-
// order time.
|
|
3927
|
-
let deliveryLine = '';
|
|
3928
|
-
if (this.pendingProcurements.length > 0) {
|
|
3929
|
-
const delivered = [];
|
|
3930
|
-
for (const order of this.pendingProcurements.splice(0)) {
|
|
3931
|
-
try {
|
|
3932
|
-
const item = restoreItem(order.itemConfig);
|
|
3933
|
-
item.owner = this.player.name;
|
|
3934
|
-
this.player.inventory.addItem(item);
|
|
3935
|
-
delivered.push(`${item.name} (${order.contact} came through)`);
|
|
3936
|
-
}
|
|
3937
|
-
catch (err) {
|
|
3938
|
-
Logger.getInstance().error(`Procurement delivery failed: ${err}`);
|
|
3939
|
-
}
|
|
3940
|
-
}
|
|
3941
|
-
if (delivered.length > 0) {
|
|
3942
|
-
this.updateInventory(this.player.inventory, capitalCase(this.scene.getCurrencyType()));
|
|
3943
|
-
deliveryLine = `\nWaiting with your gear: ${delivered.join(', ')}.`;
|
|
3944
|
-
}
|
|
3945
|
-
}
|
|
3946
|
-
// The night catches up on the ride (SINs & getting caught): a hot
|
|
3947
|
-
// run means the wire between there and home. Settled AFTER the
|
|
3948
|
-
// payout (the fee comes out of tonight's take) and BEFORE the save
|
|
3949
|
-
// below -- a burned fake or an issued SIN must survive a quit.
|
|
3950
|
-
const checkpointBlock = this.checkpointOnRideHome();
|
|
3951
|
-
// The homecoming save: everything above (payout, rent, restock,
|
|
3952
|
-
// contacts reset, deliveries, the checkpoint) settled first, so the
|
|
3953
|
-
// save reflects the hub as the player will find it.
|
|
4026
|
+
const docwagonLine = this.tickDocwagonHomecoming();
|
|
4027
|
+
const deliveryLine = this.deliverProcurements();
|
|
4028
|
+
Logger.getInstance().write(`Hosted homecoming applied (${owed.outcome}, ended ${owed.endedAt}): rent ${this.rentDebt} owed, lifestyle ${this.lifestyleTier}.`);
|
|
3954
4029
|
this.requestSave('homecoming');
|
|
3955
|
-
const fateLine =
|
|
4030
|
+
const fateLine = owed.outcome === 'wrapped'
|
|
3956
4031
|
? `The run's wrapped and the street swallows you back up.`
|
|
3957
|
-
:
|
|
3958
|
-
|
|
4032
|
+
: owed.outcome === 'interrupted'
|
|
4033
|
+
? `The run was lost to the static -- the street doesn't ask.`
|
|
4034
|
+
: `The run's behind you -- unfinished, unpaid, already someone else's problem. The street doesn't ask.`;
|
|
4035
|
+
return `${fateLine} The table's settled; home takes its cut.${clientLine}${awarenessLine}\n${rentLine}${comfortLine}${restockLine}${deliveryLine}${docwagonLine}`;
|
|
3959
4036
|
}
|
|
3960
4037
|
/**
|
|
3961
4038
|
* KNIGHT ERRANT CHECKPOINT (SINs & getting caught, ruling 2026-08-24;
|
|
@@ -261,6 +261,26 @@ export async function createHeadlessSession(opts) {
|
|
|
261
261
|
const clientSpot = clientActor ? spotOf(clientActor) : undefined;
|
|
262
262
|
logger.log(`📞 Your link chirps -- {bold}{underline}${clientName}{/underline}{/bold}: "${room ? `I'm at ${room}${clientSpot ? `, the ${clientSpot}` : ''}` : `I'm on-site`} -- waiting on you. Come sit down before somebody else does."`, 'all');
|
|
263
263
|
}
|
|
264
|
+
// THE CREW RIDES ALONG (aCjJiDDJM9jGZJeER, 2026-09-11: "I can't see
|
|
265
|
+
// Hammerhead Joe doing any combat"). Game.spawnCrew has exactly one
|
|
266
|
+
// caller -- the SOLO arrival (activateRunScene) -- and this boot
|
|
267
|
+
// loaded only the leader's DocWagon wristband out of save.game, so a
|
|
268
|
+
// hosted run had no crew at all: a hired face the player had paid
|
|
269
|
+
// for simply was not in the scene. Same filter as Game's own save
|
|
270
|
+
// load (a friend invite rides live or not at all; a dead ghost
|
|
271
|
+
// dissolves), same spawn, same arrival lines, for the primary only
|
|
272
|
+
// -- a late joiner's crew is a table question this does not answer.
|
|
273
|
+
try {
|
|
274
|
+
const primarySave = opts.players[0];
|
|
275
|
+
game.crew = (primarySave?.game?.crew ?? [])
|
|
276
|
+
.filter(m => !(m.ghost && !m.ghost.liveOnly))
|
|
277
|
+
.map(m => ({ ...m, gear: [...m.gear] }));
|
|
278
|
+
for (const line of game.spawnCrew())
|
|
279
|
+
logger.log(line, 'all');
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
logger.write(`spawnCrew (hosted boot) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
283
|
+
}
|
|
264
284
|
const spawnRoom = () => game.scene.determineStartRoom();
|
|
265
285
|
// PER-PLAYER MAP EVENTS (map-overlay wave 2026-08-24): each rider
|
|
266
286
|
// gets their own icon minimap -- the client holds no Room objects
|
|
@@ -368,6 +388,14 @@ export async function createHeadlessSession(opts) {
|
|
|
368
388
|
const res = await game.handleInputAs(actorName, input);
|
|
369
389
|
session.pushStatus();
|
|
370
390
|
pushMap();
|
|
391
|
+
// THE WEB MAP FOLLOWS THE ROOM, NOT JUST YOUR OWN BODY
|
|
392
|
+
// (oHhjxjqKuptDGMHLD: a looted body stayed on the map "until I
|
|
393
|
+
// move"; 9aEMA4APbB6rYtWnT: a killed NPC's dot never changed).
|
|
394
|
+
// The condition beat is what the browser draws actors from, and
|
|
395
|
+
// it fires only on the primary's OWN mutations -- a kill or a
|
|
396
|
+
// loot mutates the room, not the killer, so nothing fired until
|
|
397
|
+
// their next step. One nudge per command; the beat coalesces.
|
|
398
|
+
game.player.noteConditionChanged();
|
|
371
399
|
return res;
|
|
372
400
|
})),
|
|
373
401
|
addPlayer: (save) => runInSession(ctx, async () => {
|
|
@@ -489,6 +517,7 @@ export async function createHeadlessSession(opts) {
|
|
|
489
517
|
game.onSceneChanged = () => {
|
|
490
518
|
session.pushStatus();
|
|
491
519
|
pushMap();
|
|
520
|
+
game.player.noteConditionChanged();
|
|
492
521
|
};
|
|
493
522
|
return session;
|
|
494
523
|
});
|
|
@@ -236,23 +236,44 @@ export function buildConditionReport(player, deps) {
|
|
|
236
236
|
// guessed role): a construct on the matrix plane -- IC, sprite, agent --
|
|
237
237
|
// has no body in the room. Another player's body still shows even
|
|
238
238
|
// while their persona rides the grid.
|
|
239
|
+
// DOWNED, NOT DROPPED (9aEMA4APbB6rYtWnT, 2026-09-11: "when I kill
|
|
240
|
+
// an NPC, their icon on the map should change"): the room panel
|
|
241
|
+
// draws somebody face-down as ☓; this beat used to drop them, so
|
|
242
|
+
// the web dot simply vanished -- or, worse, stood there until the
|
|
243
|
+
// next beat. They ride with kind 'downed' now.
|
|
239
244
|
actors: (player.currentLocation ? visibleActorsIn(player.currentLocation, player) : [])
|
|
240
245
|
.filter(a => !(a.plane === 'matrix' && a !== player && (a.icHost !== undefined || a.companionKind === 'sprite' || a.companionKind === 'agent')))
|
|
241
|
-
.filter(a => a !== player
|
|
246
|
+
.filter(a => a !== player)
|
|
242
247
|
.slice(0, 24)
|
|
243
248
|
.map(a => ({
|
|
244
249
|
name: a.name,
|
|
245
250
|
// THE SAME RED THE ROOM PANEL USES (see IConditionReportDeps
|
|
246
251
|
// .isHostile): stance flag OR live enmity, and never a
|
|
247
252
|
// surrendered one -- hostileByStance owns that clause.
|
|
248
|
-
kind: a.
|
|
249
|
-
:
|
|
253
|
+
kind: a.isIncapacitated() ? 'downed'
|
|
254
|
+
: a.allyOf ? 'ally'
|
|
255
|
+
: (isHostile?.(a) ?? hostileByStance(a)) ? 'hostile' : 'neutral',
|
|
250
256
|
role: actorRole(a),
|
|
251
257
|
spot: a.atSpot,
|
|
252
258
|
cell: a.atCell ?? seats.get(a.name),
|
|
253
259
|
})),
|
|
260
|
+
// THE DEAD (same report): the room's bodies, where they fell -- the
|
|
261
|
+
// panel's 💀. Gone from the list the moment a body is picked clean
|
|
262
|
+
// (loot.ts removeBody), which with the per-command beat is at once.
|
|
263
|
+
bodies: roomBodies(player),
|
|
254
264
|
};
|
|
255
265
|
}
|
|
266
|
+
function roomBodies(player) {
|
|
267
|
+
const room = player.currentLocation;
|
|
268
|
+
if (!room || (player.plane !== 'meat' && player.plane !== 'drone'))
|
|
269
|
+
return [];
|
|
270
|
+
try {
|
|
271
|
+
return room.bodies.slice(0, 24).map(b => ({ name: b.name, cell: { x: b.cell.x, y: b.cell.y, z: b.cell.z } }));
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
256
277
|
let lastRoster = '';
|
|
257
278
|
/** One session-log line whenever the room or the roster changes -- the
|
|
258
279
|
* cheapest possible answer to "what did the engine send?" */
|
|
@@ -276,7 +297,7 @@ function noteRoster(report) {
|
|
|
276
297
|
const ADDITIVE_FIELDS = [
|
|
277
298
|
'ar', 'actors', 'deckLine', 'deckDamage', 'astralPerceiving', 'bodyRoom', 'sustainingPenalty',
|
|
278
299
|
'matrix', 'panMarks', 'hostMarksOnYou', 'marksPlaced', 'marksOnYou', 'deckStored', 'droneDamage', 'companions', 'pools', 'initiative',
|
|
279
|
-
'karma', 'nuyen', 'edge', 'grid',
|
|
300
|
+
'karma', 'nuyen', 'edge', 'grid', 'bodies',
|
|
280
301
|
];
|
|
281
302
|
/** Refusal reasons already written to the session log -- one line each. */
|
|
282
303
|
const announcedRefusals = new Map();
|
|
@@ -28,9 +28,25 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
28
28
|
const logger = Logger.getInstance();
|
|
29
29
|
if (npc.isIncapacitated() || npc.surrendered)
|
|
30
30
|
return;
|
|
31
|
+
// THE ROOM SEES THE PHASE (aCjJiDDJM9jGZJeER, web run 2026-09-11: "I
|
|
32
|
+
// can't see Hammerhead Joe doing any combat" -- a hostile NPC). The
|
|
33
|
+
// verbs below answer the ACTOR: their return text is second person,
|
|
34
|
+
// goes into the NPC's own history and to a shell's master, and to
|
|
35
|
+
// nobody else. Only an attack that connects narrates through the
|
|
36
|
+
// exchange. So a phase spent readying a gun, reloading, closing the
|
|
37
|
+
// distance or taking cover -- most of a melee NPC's phases -- read to
|
|
38
|
+
// the player as "Joe acts (Initiative 9)." followed by nothing, which
|
|
39
|
+
// is the report. One third-person line per action, room-scoped, the
|
|
40
|
+
// same scope the exchange itself announces on.
|
|
41
|
+
const room = npc.currentLocation;
|
|
42
|
+
const tell = (line) => {
|
|
43
|
+
if (room)
|
|
44
|
+
logger.log(line, { room: room.name });
|
|
45
|
+
};
|
|
31
46
|
const enemies = enc.enemiesOf(npc);
|
|
32
47
|
if (enemies.length === 0) {
|
|
33
48
|
logger.write(`Brain: ${npc.name} has nobody to fight -- phase forfeited.`);
|
|
49
|
+
tell(`${npc.name} looks for a target and finds none.`);
|
|
34
50
|
return;
|
|
35
51
|
}
|
|
36
52
|
const target = pickTarget(npc, enemies);
|
|
@@ -49,8 +65,11 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
49
65
|
if (weapon && !npc.weaponDrawn) {
|
|
50
66
|
const r = await npc.actInCombat('draw');
|
|
51
67
|
logger.write(`Brain: ${npc.name} draw -> ${r}`);
|
|
52
|
-
if (!npc.weaponDrawn)
|
|
68
|
+
if (!npc.weaponDrawn) {
|
|
69
|
+
tell(`${npc.name} fumbles for ${weapon.name} and comes up empty-handed.`);
|
|
53
70
|
return; // refused -- nothing else will go better
|
|
71
|
+
}
|
|
72
|
+
tell(`${npc.name} readies ${weapon.name}.`);
|
|
54
73
|
continue;
|
|
55
74
|
}
|
|
56
75
|
// 2. A dry gun: reload if the pack allows, else it is a club.
|
|
@@ -58,6 +77,7 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
58
77
|
if (dry && !weapon.jammed && hasAmmo(npc)) {
|
|
59
78
|
const r = await npc.actInCombat('reload');
|
|
60
79
|
logger.write(`Brain: ${npc.name} reload -> ${r}`);
|
|
80
|
+
tell(`${npc.name} reloads ${weapon.name}.`);
|
|
61
81
|
return; // a reload is the phase (p.163 Reloading Weapons)
|
|
62
82
|
}
|
|
63
83
|
const melee = !firearm || dry || weapon.jammed;
|
|
@@ -68,16 +88,22 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
68
88
|
logger.write(`Brain: ${npc.name} move to ${targetSpot} -> ${r}`);
|
|
69
89
|
if (!canReach(npc, targetSpot)) {
|
|
70
90
|
// Couldn't close this turn: get behind something and wait.
|
|
91
|
+
tell(`${npc.name} closes on ${target.name} but can't cover the ground this turn.`);
|
|
71
92
|
if (!isInCover(npc) && coverAvailableFor(npc) && (enc.budgetOf(npc)?.simple ?? 0) > 0) {
|
|
72
93
|
await npc.actInCombat('cover');
|
|
94
|
+
if (isInCover(npc))
|
|
95
|
+
tell(`${npc.name} ducks behind cover.`);
|
|
73
96
|
}
|
|
74
97
|
return;
|
|
75
98
|
}
|
|
99
|
+
tell(`${npc.name} closes on ${target.name}.`);
|
|
76
100
|
}
|
|
77
101
|
const b = enc.budgetOf(npc);
|
|
78
102
|
if (b?.complexAvailable) {
|
|
79
103
|
const r = await npc.actInCombat(`attack ${target.name}`);
|
|
80
104
|
logger.write(`Brain: ${npc.name} attack ${target.name} -> ${r.split('\n')[0]}`);
|
|
105
|
+
if (!enc.budgetOf(npc)?.attackTaken)
|
|
106
|
+
tell(`${npc.name} goes for ${target.name} and can't make it count.`);
|
|
81
107
|
}
|
|
82
108
|
return;
|
|
83
109
|
}
|
|
@@ -87,13 +113,18 @@ export async function runNpcActionPhase(enc, npc) {
|
|
|
87
113
|
logger.write(`Brain: ${npc.name} cover -> ${r}`);
|
|
88
114
|
if (!isInCover(npc))
|
|
89
115
|
return;
|
|
116
|
+
tell(`${npc.name} ducks behind cover.`);
|
|
90
117
|
continue;
|
|
91
118
|
}
|
|
92
119
|
if (!budget.attackTaken && budget.simple >= 1) {
|
|
93
120
|
const r = await npc.actInCombat(`attack ${target.name}`);
|
|
94
121
|
logger.write(`Brain: ${npc.name} attack ${target.name} -> ${r.split('\n')[0]}`);
|
|
95
|
-
if (!enc.budgetOf(npc)?.attackTaken)
|
|
122
|
+
if (!enc.budgetOf(npc)?.attackTaken) {
|
|
123
|
+
// The shot was refused (range, line, a jam): the room should
|
|
124
|
+
// see the attempt, not silence.
|
|
125
|
+
tell(`${npc.name} lines up on ${target.name} and the shot doesn't come.`);
|
|
96
126
|
return; // refused: don't loop on it
|
|
127
|
+
}
|
|
97
128
|
continue;
|
|
98
129
|
}
|
|
99
130
|
return;
|
|
@@ -1319,6 +1319,18 @@ export class SharedRunSession {
|
|
|
1319
1319
|
restorePlayer(g.player, cloud.player);
|
|
1320
1320
|
if (g.saveFilePath)
|
|
1321
1321
|
writeSaveAtomic(g.saveFilePath, cloud);
|
|
1322
|
+
// THE HOMECOMING OWED (8izFwokbehjHzkEAn, 3gFcHfheGSMPtpCFh): the
|
|
1323
|
+
// server settled the table and left the aftermath on the save;
|
|
1324
|
+
// this hub Game never went anywhere, so it pays it now -- rent,
|
|
1325
|
+
// the DocWagon year, the Johnson's opinion -- and the homecoming
|
|
1326
|
+
// save it writes clears the flag. After the disk write above, so
|
|
1327
|
+
// the settled state is what lands, not the cloud's pre-settled one.
|
|
1328
|
+
const owed = cloud.game?.pendingHomecoming;
|
|
1329
|
+
if (owed) {
|
|
1330
|
+
const lines = g.applyHostedHomecoming(owed);
|
|
1331
|
+
if (lines)
|
|
1332
|
+
Logger.getInstance().logWithColor(lines, 'green');
|
|
1333
|
+
}
|
|
1322
1334
|
}
|
|
1323
1335
|
}
|
|
1324
1336
|
catch (err) {
|
|
@@ -1241,7 +1241,15 @@ export function lockedRouteHint(room, direction, door) {
|
|
|
1241
1241
|
if (door.heldBy) {
|
|
1242
1242
|
return ` It's barred from ${door.heldBy.room} -- ${door.heldBy.device} is over there, not here.`;
|
|
1243
1243
|
}
|
|
1244
|
-
// A
|
|
1244
|
+
// A PLAIN KEYED DOOR (S8WdW7gm9tFGaxeCi): there IS a route, and the
|
|
1245
|
+
// Door knows it -- the key item it was locked with, and the one verb
|
|
1246
|
+
// that works it. The comment here used to say "no route to invent";
|
|
1247
|
+
// naming a route that exists is not inventing one, and the silence
|
|
1248
|
+
// read as a soft-lock in play. Scene-factory step 4.95 guarantees the
|
|
1249
|
+
// named key is a real item somewhere in the scene.
|
|
1250
|
+
if (door.unlockItemOrSolution) {
|
|
1251
|
+
return ` It takes ${door.unlockItemOrSolution} -- "unlock ${String(direction).toLowerCase()}" with it on you.`;
|
|
1252
|
+
}
|
|
1245
1253
|
return '';
|
|
1246
1254
|
}
|
|
1247
1255
|
export function doorwayKindOf(spotName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maka/maka-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.165.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.",
|