@maka/maka-cli 5.83.0 → 5.85.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/call.d.ts +0 -28
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js +1 -41
- package/bundle/typescript/src/commands/game/sideQuest/commands/call.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +7 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.d.ts +3 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +62 -4
- package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/heal.d.ts +53 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/heal.js +158 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/heal.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js +16 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/job-offer.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js +13 -5
- package/bundle/typescript/src/commands/game/sideQuest/commands/look.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +9 -6
- package/bundle/typescript/src/commands/game/sideQuest/commands/search.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/sustain.d.ts +12 -0
- package/bundle/typescript/src/commands/game/sideQuest/commands/sustain.js +52 -2
- package/bundle/typescript/src/commands/game/sideQuest/commands/sustain.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.d.ts +17 -11
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.js +45 -32
- package/bundle/typescript/src/commands/game/sideQuest/commands/use.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.d.ts +34 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +84 -6
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.d.ts +9 -2
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js +12 -5
- package/bundle/typescript/src/commands/game/sideQuest/factories/scene-seed-generator.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +22 -0
- package/bundle/typescript/src/commands/game/sideQuest/game.js +148 -7
- package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.d.ts +11 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +86 -16
- package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +16 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js +23 -0
- package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +8 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-vocab.js +6 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/backlog-vocab.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/bystander.d.ts +17 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/bystander.js +123 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/bystander.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.d.ts +116 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +96 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.d.ts +124 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js +209 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/meet-location.d.ts +37 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/meet-location.js +51 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/meet-location.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.d.ts +76 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +84 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +9 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.d.ts +57 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +121 -16
- package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/presence-gate.d.ts +21 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/presence-gate.js +28 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/presence-gate.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.d.ts +75 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js +121 -10
- package/bundle/typescript/src/commands/game/sideQuest/utilities/shared-run.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.d.ts +10 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js +55 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/sin.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.d.ts +17 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js +139 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/street-gang.js.map +1 -0
- package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +9 -1
- package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +3 -1
- package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
- package/bundle/typescript/src/commands/testCmd/mocha.sub.cmd.js +46 -44
- package/bundle/typescript/src/commands/testCmd/mocha.sub.cmd.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { NPC } from '../models/npc.js';
|
|
2
|
+
import { Logger } from './logger.js';
|
|
3
|
+
import { CommandRegistry } from '../commands/command-registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* Bring one into the world. Returns the NPC, or null when the name is
|
|
6
|
+
* already taken -- the same refusal spawnCompanionShell makes, and for
|
|
7
|
+
* the same reason: two actors answering to one name is a bug nobody can
|
|
8
|
+
* see until a command picks the wrong one.
|
|
9
|
+
*/
|
|
10
|
+
export function spawnEphemeral(host, opts) {
|
|
11
|
+
if (host.scene.getActor(opts.name)) {
|
|
12
|
+
Logger.getInstance().write(`spawnEphemeral: name "${opts.name}" already in scene -- refusing.`);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const npc = new NPC({
|
|
16
|
+
dialog: [],
|
|
17
|
+
description: opts.description,
|
|
18
|
+
playerConstructorConfig: { playerName: opts.name, startLocation: opts.room, combat: opts.combat },
|
|
19
|
+
}, new CommandRegistry(host.scene));
|
|
20
|
+
npc.plane = 'meat';
|
|
21
|
+
// NOT allyOf, and NOT companionKind: an ephemeral belongs to nobody
|
|
22
|
+
// and is on no roster. Those two fields are what make a companion
|
|
23
|
+
// persist, follow, and take orders.
|
|
24
|
+
npc.ephemeral = { kind: opts.kind, until: opts.until, roomName: opts.room.name };
|
|
25
|
+
host.scene.addActor(npc);
|
|
26
|
+
host.scene.setCharacterLocation(npc, opts.room.name);
|
|
27
|
+
npc.scene = host.scene;
|
|
28
|
+
host.ephemerals.set(opts.name, npc.ephemeral);
|
|
29
|
+
Logger.getInstance().write(`Ephemeral spawned: ${opts.name} (${opts.kind}, until ${opts.until}) in ${opts.room.name}.`);
|
|
30
|
+
return npc;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Remove every ephemeral whose scope has ended.
|
|
34
|
+
*
|
|
35
|
+
* WHY NO SAVE HOOK IS NEEDED HERE, which is the ruling's reasoning and
|
|
36
|
+
* has to be true at exactly this line: ephemerals are never written to
|
|
37
|
+
* the save file, so there is nothing to reconcile on load. The obvious
|
|
38
|
+
* worry -- a save landing mid-fight, resurrecting a dead mob or erasing
|
|
39
|
+
* a live one -- is already answered upstream: requestSave's guards only
|
|
40
|
+
* write at safe beats and a fight is not one. So a load never sees a
|
|
41
|
+
* half-finished ephemeral, and this function is the only thing that
|
|
42
|
+
* ever ends one.
|
|
43
|
+
*
|
|
44
|
+
* Returns the names removed, so a caller can say so once.
|
|
45
|
+
*/
|
|
46
|
+
export function despawnEphemerals(host, opts) {
|
|
47
|
+
const removed = [];
|
|
48
|
+
for (const [name, mark] of [...host.ephemerals]) {
|
|
49
|
+
const ended = opts.all
|
|
50
|
+
? true
|
|
51
|
+
: mark.until === 'fight'
|
|
52
|
+
? opts.fightActive === false
|
|
53
|
+
// exchange- and room-scoped bodies both end when the player
|
|
54
|
+
// leaves: a walkout IS the end of the conversation.
|
|
55
|
+
: opts.playerRoomName !== undefined && opts.playerRoomName !== mark.roomName;
|
|
56
|
+
if (!ended)
|
|
57
|
+
continue;
|
|
58
|
+
if (host.scene.getActor(name) instanceof NPC)
|
|
59
|
+
host.scene.removeActor(name);
|
|
60
|
+
host.ephemerals.delete(name);
|
|
61
|
+
removed.push(name);
|
|
62
|
+
}
|
|
63
|
+
if (removed.length > 0) {
|
|
64
|
+
Logger.getInstance().write(`Ephemerals despawned: ${removed.join(', ')}.`);
|
|
65
|
+
}
|
|
66
|
+
return removed;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The two denials that make an ephemeral disposable, as prompt text.
|
|
70
|
+
*
|
|
71
|
+
* FULL LENGTH, DELIBERATELY. Everything else in an ephemeral's brief is
|
|
72
|
+
* trimmed hard -- no command help, no objective, no payout, no client,
|
|
73
|
+
* no wares, no history -- but the honesty pins are not where the budget
|
|
74
|
+
* gets saved. They are the majority of what remains precisely because
|
|
75
|
+
* the rest went, and trimming a denial is the exact economy that
|
|
76
|
+
* reintroduces the bug the denial exists to prevent. A shorter pin does
|
|
77
|
+
* not produce a shorter answer; it produces a confident wrong one.
|
|
78
|
+
*/
|
|
79
|
+
export function ephemeralDenials(role) {
|
|
80
|
+
return `
|
|
81
|
+
YOU ARE A PASSING FACE, NOT A CHARACTER IN ANYONE'S STORY. You are ${role} -- here for
|
|
82
|
+
this moment and no longer. You know nothing about any job, run, client, employer,
|
|
83
|
+
objective, payout, or what these people are doing in this district beyond what is
|
|
84
|
+
happening in front of you right now. You have no opinion about their business because you
|
|
85
|
+
do not know they have any. If someone raises a job, a Johnson, a target, or a score, you
|
|
86
|
+
have no idea what they are talking about and no curiosity about it -- that is somebody
|
|
87
|
+
else's world and you are not in it.
|
|
88
|
+
|
|
89
|
+
YOU CANNOT PROMISE, SELL, HIRE, OR HAND OVER ANYTHING. You quote no prices, offer no work,
|
|
90
|
+
carry no goods to trade, and make no arrangements for later -- no favors owed, no "come
|
|
91
|
+
find me", no meeting tomorrow. You will not be here tomorrow. Anything you agreed to would
|
|
92
|
+
be an obligation nobody in the world can honor, and the person who trusted it would be
|
|
93
|
+
worse off for having met you. If pressed for any of it, say plainly that it is not yours
|
|
94
|
+
to give and point them at whoever actually handles such things.`;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=ephemeral.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ephemeral.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/ephemeral.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Player } from '../models/player.js';
|
|
2
|
+
import type { Item } from '../models/item.js';
|
|
3
|
+
import { IDiceRoll } from './dice.js';
|
|
4
|
+
/**
|
|
5
|
+
* FIRST AID (SR5 core p.205-206, with the gear rules at p.208/p.450).
|
|
6
|
+
*
|
|
7
|
+
* The engine already HAD a first-aid roll -- buried inside `use <medkit>`
|
|
8
|
+
* -- and it was wrong in a family rather than in one place, which is why
|
|
9
|
+
* this module exists at all. Every deviation below was measured against
|
|
10
|
+
* the book before it was touched:
|
|
11
|
+
*
|
|
12
|
+
* canon engine, before this module
|
|
13
|
+
* ----------------------------------- ---------------------------------
|
|
14
|
+
* threshold 2, NET hits heal a box hits + 1 + quality: an all-miss
|
|
15
|
+
* roll still closed a box
|
|
16
|
+
* max healed = First Aid RATING no cap at all -- kit quality was
|
|
17
|
+
* the only thing that scaled
|
|
18
|
+
* [Mental] limit (+ medkit rating) no limit applied to the roll
|
|
19
|
+
* once per set of wounds spammable
|
|
20
|
+
* treats Stun OR Physical physical only
|
|
21
|
+
* critical glitch: +1D3 boxes no glitch consequence
|
|
22
|
+
* not after magical healing no interaction with the spell
|
|
23
|
+
*
|
|
24
|
+
* The single most load-bearing consequence, and the reason this is worth
|
|
25
|
+
* doing: the RATING CAP inverts who is good at medicine. Before, an
|
|
26
|
+
* unskilled runner with an expensive kit out-healed a trained medic with
|
|
27
|
+
* a cheap one. Canon says the skill is the ceiling and the gear is the
|
|
28
|
+
* help -- medics matter.
|
|
29
|
+
*
|
|
30
|
+
* TWO ENGINE MAPPINGS, labelled as such (the ic.ts / astralMoveBudget
|
|
31
|
+
* house style: canon fixes the ratio, the unit is ours):
|
|
32
|
+
*
|
|
33
|
+
* 1. MEDKIT RATING. Canon rates medkits 1-6. This engine rates all gear
|
|
34
|
+
* on one Common..MilitaryGrade tier (Item.qualityBonus, 0-4), because
|
|
35
|
+
* gear tier is the whole progression system. So kit Rating is
|
|
36
|
+
* `qualityBonus + 1`: a common kit is Rating 1, a milspec one Rating
|
|
37
|
+
* 5. The +1 is not decoration -- a Rating 0 kit would heal nothing in
|
|
38
|
+
* an untrained hand, and canon has no Rating 0 medkit.
|
|
39
|
+
*
|
|
40
|
+
* 2. WIRELESS. p.208 gives a wireless kit a dice bonus; p.450 gives the
|
|
41
|
+
* rating to the limit. This engine has no per-item wireless toggle,
|
|
42
|
+
* and the Sixth World default is wireless-on, so kits are treated as
|
|
43
|
+
* wireless and BOTH apply -- which is exactly what p.208's own worked
|
|
44
|
+
* example does (Mental 6 + medkit 4 = limit 10).
|
|
45
|
+
*
|
|
46
|
+
* NOT MODELLED, deliberately, and each one is engine-lacks-the-noun
|
|
47
|
+
* rather than a rule we disagree with:
|
|
48
|
+
*
|
|
49
|
+
* - "within 1 hour of the damage" (p.205). sideQuest has no elapsed
|
|
50
|
+
* game clock -- see the same note in planes.ts -- so there is no
|
|
51
|
+
* "when" to compare against. The once-per-set-of-wounds lock below
|
|
52
|
+
* carries the anti-spam intent that the hour was there to serve.
|
|
53
|
+
* - "Complex Action, one per Combat Turn per box" (p.206). The engine
|
|
54
|
+
* has no action budget; combat is exchange-based and no verb spends
|
|
55
|
+
* an action. Importing canon's PERMISSION to heal mid-fight without
|
|
56
|
+
* its PRICE is a real gap, and it is called out in heal.ts.
|
|
57
|
+
*/
|
|
58
|
+
export declare const FIRST_AID_THRESHOLD = 2;
|
|
59
|
+
export type WoundTrack = 'physical' | 'stun';
|
|
60
|
+
export interface IFirstAidResult {
|
|
61
|
+
roll: IDiceRoll;
|
|
62
|
+
/** Boxes actually closed (0 on a failure or a critical glitch). */
|
|
63
|
+
healed: number;
|
|
64
|
+
/** Boxes ADDED by a critical glitch (p.206). */
|
|
65
|
+
worsened: number;
|
|
66
|
+
/** Hits over the threshold, before the cap and before armor. */
|
|
67
|
+
netHits: number;
|
|
68
|
+
/** The rating cap that applied -- skill, or kit rating if untrained. */
|
|
69
|
+
cap: number;
|
|
70
|
+
capBound: boolean;
|
|
71
|
+
halvedByArmor: boolean;
|
|
72
|
+
track: WoundTrack;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Canon rates medkits 1-6; this engine rates gear 0-4. See mapping 1 in
|
|
76
|
+
* the file header -- the +1 keeps a common kit useful in untrained hands.
|
|
77
|
+
*/
|
|
78
|
+
export declare function medkitRating(kit?: Item): number;
|
|
79
|
+
/**
|
|
80
|
+
* The ceiling on boxes (p.205: "the maximum damage healable with the
|
|
81
|
+
* First Aid skill is equal to the skill's rating").
|
|
82
|
+
*
|
|
83
|
+
* Defaulting would make that ceiling ZERO -- the skill rating is 0, so a
|
|
84
|
+
* bare-handed amateur heals nothing however well they roll. p.208 is what
|
|
85
|
+
* rescues the untrained case: with a kit, "she can still make an
|
|
86
|
+
* untrained First Aid test using her Logic attribute minus 1 die and the
|
|
87
|
+
* device's rating IN PLACE OF her First Aid skill". The kit stands in for
|
|
88
|
+
* the skill, so it stands in as the cap too.
|
|
89
|
+
*/
|
|
90
|
+
export declare function firstAidCap(medic: Player, kit?: Item): number;
|
|
91
|
+
/**
|
|
92
|
+
* First Aid + Logic (p.205), defaulting at Logic - 1 (p.130), plus the
|
|
93
|
+
* wireless kit's rating as dice (p.208) -- but only ONCE: when untrained
|
|
94
|
+
* the rating is already standing in for the skill, so adding the wireless
|
|
95
|
+
* bonus on top would pay the same kit twice.
|
|
96
|
+
*
|
|
97
|
+
* Quick Healer (p.77) and the medic's OWN wound modifiers (p.205 sends
|
|
98
|
+
* you to the Healing Modifiers table, and your own penalties travel with
|
|
99
|
+
* you) ride on top. Floored at 1: SR5 never rolls a zero pool.
|
|
100
|
+
*/
|
|
101
|
+
export declare function firstAidPool(medic: Player, kit?: Item): number;
|
|
102
|
+
/** [Mental], plus the kit's rating (p.208's worked example: 6 + 4 = 10). */
|
|
103
|
+
export declare function firstAidLimit(medic: Player, kit?: Item): number;
|
|
104
|
+
/**
|
|
105
|
+
* "divide the net effect in half (rounded up) if the victim being treated
|
|
106
|
+
* is wearing any kind of full-body armor" (p.205-206).
|
|
107
|
+
*
|
|
108
|
+
* Read off the equipped slots by name, the same duck-typing the rest of
|
|
109
|
+
* the gear code uses (Item.isArmor, isFence) so generated kit works
|
|
110
|
+
* without a schema change.
|
|
111
|
+
*/
|
|
112
|
+
export declare function inFullBodyArmor(patient: Player): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Why this treatment cannot happen, or undefined if it can. Ordered so
|
|
115
|
+
* the most informative refusal wins: a player who is out of options
|
|
116
|
+
* should be told the reason that is actually blocking them.
|
|
117
|
+
*/
|
|
118
|
+
export declare function firstAidRefusal(medic: Player, patient: Player, kit: Item | undefined, track: WoundTrack): string | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* Runs the test and applies the outcome. Callers MUST have cleared
|
|
121
|
+
* firstAidRefusal first -- this function assumes the treatment is legal
|
|
122
|
+
* and always consumes the once-per-wounds application.
|
|
123
|
+
*/
|
|
124
|
+
export declare function performFirstAid(medic: Player, patient: Player, kit: Item, track: WoundTrack): IFirstAidResult;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { rollPool } from './dice.js';
|
|
2
|
+
import { mentalLimit } from './limits.js';
|
|
3
|
+
/**
|
|
4
|
+
* FIRST AID (SR5 core p.205-206, with the gear rules at p.208/p.450).
|
|
5
|
+
*
|
|
6
|
+
* The engine already HAD a first-aid roll -- buried inside `use <medkit>`
|
|
7
|
+
* -- and it was wrong in a family rather than in one place, which is why
|
|
8
|
+
* this module exists at all. Every deviation below was measured against
|
|
9
|
+
* the book before it was touched:
|
|
10
|
+
*
|
|
11
|
+
* canon engine, before this module
|
|
12
|
+
* ----------------------------------- ---------------------------------
|
|
13
|
+
* threshold 2, NET hits heal a box hits + 1 + quality: an all-miss
|
|
14
|
+
* roll still closed a box
|
|
15
|
+
* max healed = First Aid RATING no cap at all -- kit quality was
|
|
16
|
+
* the only thing that scaled
|
|
17
|
+
* [Mental] limit (+ medkit rating) no limit applied to the roll
|
|
18
|
+
* once per set of wounds spammable
|
|
19
|
+
* treats Stun OR Physical physical only
|
|
20
|
+
* critical glitch: +1D3 boxes no glitch consequence
|
|
21
|
+
* not after magical healing no interaction with the spell
|
|
22
|
+
*
|
|
23
|
+
* The single most load-bearing consequence, and the reason this is worth
|
|
24
|
+
* doing: the RATING CAP inverts who is good at medicine. Before, an
|
|
25
|
+
* unskilled runner with an expensive kit out-healed a trained medic with
|
|
26
|
+
* a cheap one. Canon says the skill is the ceiling and the gear is the
|
|
27
|
+
* help -- medics matter.
|
|
28
|
+
*
|
|
29
|
+
* TWO ENGINE MAPPINGS, labelled as such (the ic.ts / astralMoveBudget
|
|
30
|
+
* house style: canon fixes the ratio, the unit is ours):
|
|
31
|
+
*
|
|
32
|
+
* 1. MEDKIT RATING. Canon rates medkits 1-6. This engine rates all gear
|
|
33
|
+
* on one Common..MilitaryGrade tier (Item.qualityBonus, 0-4), because
|
|
34
|
+
* gear tier is the whole progression system. So kit Rating is
|
|
35
|
+
* `qualityBonus + 1`: a common kit is Rating 1, a milspec one Rating
|
|
36
|
+
* 5. The +1 is not decoration -- a Rating 0 kit would heal nothing in
|
|
37
|
+
* an untrained hand, and canon has no Rating 0 medkit.
|
|
38
|
+
*
|
|
39
|
+
* 2. WIRELESS. p.208 gives a wireless kit a dice bonus; p.450 gives the
|
|
40
|
+
* rating to the limit. This engine has no per-item wireless toggle,
|
|
41
|
+
* and the Sixth World default is wireless-on, so kits are treated as
|
|
42
|
+
* wireless and BOTH apply -- which is exactly what p.208's own worked
|
|
43
|
+
* example does (Mental 6 + medkit 4 = limit 10).
|
|
44
|
+
*
|
|
45
|
+
* NOT MODELLED, deliberately, and each one is engine-lacks-the-noun
|
|
46
|
+
* rather than a rule we disagree with:
|
|
47
|
+
*
|
|
48
|
+
* - "within 1 hour of the damage" (p.205). sideQuest has no elapsed
|
|
49
|
+
* game clock -- see the same note in planes.ts -- so there is no
|
|
50
|
+
* "when" to compare against. The once-per-set-of-wounds lock below
|
|
51
|
+
* carries the anti-spam intent that the hour was there to serve.
|
|
52
|
+
* - "Complex Action, one per Combat Turn per box" (p.206). The engine
|
|
53
|
+
* has no action budget; combat is exchange-based and no verb spends
|
|
54
|
+
* an action. Importing canon's PERMISSION to heal mid-fight without
|
|
55
|
+
* its PRICE is a real gap, and it is called out in heal.ts.
|
|
56
|
+
*/
|
|
57
|
+
export const FIRST_AID_THRESHOLD = 2;
|
|
58
|
+
/**
|
|
59
|
+
* Canon rates medkits 1-6; this engine rates gear 0-4. See mapping 1 in
|
|
60
|
+
* the file header -- the +1 keeps a common kit useful in untrained hands.
|
|
61
|
+
*/
|
|
62
|
+
export function medkitRating(kit) {
|
|
63
|
+
return kit ? kit.qualityBonus + 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The ceiling on boxes (p.205: "the maximum damage healable with the
|
|
67
|
+
* First Aid skill is equal to the skill's rating").
|
|
68
|
+
*
|
|
69
|
+
* Defaulting would make that ceiling ZERO -- the skill rating is 0, so a
|
|
70
|
+
* bare-handed amateur heals nothing however well they roll. p.208 is what
|
|
71
|
+
* rescues the untrained case: with a kit, "she can still make an
|
|
72
|
+
* untrained First Aid test using her Logic attribute minus 1 die and the
|
|
73
|
+
* device's rating IN PLACE OF her First Aid skill". The kit stands in for
|
|
74
|
+
* the skill, so it stands in as the cap too.
|
|
75
|
+
*/
|
|
76
|
+
export function firstAidCap(medic, kit) {
|
|
77
|
+
const trained = medic.skillRating('first-aid');
|
|
78
|
+
return trained > 0 ? trained : medkitRating(kit);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* First Aid + Logic (p.205), defaulting at Logic - 1 (p.130), plus the
|
|
82
|
+
* wireless kit's rating as dice (p.208) -- but only ONCE: when untrained
|
|
83
|
+
* the rating is already standing in for the skill, so adding the wireless
|
|
84
|
+
* bonus on top would pay the same kit twice.
|
|
85
|
+
*
|
|
86
|
+
* Quick Healer (p.77) and the medic's OWN wound modifiers (p.205 sends
|
|
87
|
+
* you to the Healing Modifiers table, and your own penalties travel with
|
|
88
|
+
* you) ride on top. Floored at 1: SR5 never rolls a zero pool.
|
|
89
|
+
*/
|
|
90
|
+
export function firstAidPool(medic, kit) {
|
|
91
|
+
const trained = medic.skillRating('first-aid');
|
|
92
|
+
const rating = medkitRating(kit);
|
|
93
|
+
const base = trained > 0
|
|
94
|
+
? medic.logic + trained + rating
|
|
95
|
+
: medic.logic - 1 + rating;
|
|
96
|
+
return Math.max(1, base
|
|
97
|
+
+ medic.qualityBonus('healing')
|
|
98
|
+
+ medic.augBonus('healing')
|
|
99
|
+
+ medic.woundModifier);
|
|
100
|
+
}
|
|
101
|
+
/** [Mental], plus the kit's rating (p.208's worked example: 6 + 4 = 10). */
|
|
102
|
+
export function firstAidLimit(medic, kit) {
|
|
103
|
+
return mentalLimit(medic) + medkitRating(kit);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* "divide the net effect in half (rounded up) if the victim being treated
|
|
107
|
+
* is wearing any kind of full-body armor" (p.205-206).
|
|
108
|
+
*
|
|
109
|
+
* Read off the equipped slots by name, the same duck-typing the rest of
|
|
110
|
+
* the gear code uses (Item.isArmor, isFence) so generated kit works
|
|
111
|
+
* without a schema change.
|
|
112
|
+
*/
|
|
113
|
+
export function inFullBodyArmor(patient) {
|
|
114
|
+
const eq = patient.equipment;
|
|
115
|
+
for (const group of Object.values(eq)) {
|
|
116
|
+
if (!group)
|
|
117
|
+
continue;
|
|
118
|
+
for (const item of Object.values(group)) {
|
|
119
|
+
if (!item)
|
|
120
|
+
continue;
|
|
121
|
+
if (/full.?body|hardsuit|mil-?spec|full helmet/i.test(item.name))
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Why this treatment cannot happen, or undefined if it can. Ordered so
|
|
129
|
+
* the most informative refusal wins: a player who is out of options
|
|
130
|
+
* should be told the reason that is actually blocking them.
|
|
131
|
+
*/
|
|
132
|
+
export function firstAidRefusal(medic, patient, kit, track) {
|
|
133
|
+
const self = medic === patient;
|
|
134
|
+
const who = self ? 'You' : patient.name;
|
|
135
|
+
const is = self ? 'are' : 'is';
|
|
136
|
+
// p.205: "First Aid can only be used if you have a medkit (even if you
|
|
137
|
+
// do not currently have supplies for it)."
|
|
138
|
+
if (!kit) {
|
|
139
|
+
return `First aid takes a medkit -- bare hands and good intentions don't close wounds. Buy one, or find a clinic ("treat").`;
|
|
140
|
+
}
|
|
141
|
+
if (patient.isDown()) {
|
|
142
|
+
// Canon has an answer for this (Stabilization, p.209, threshold 3),
|
|
143
|
+
// and this engine does not implement it. Saying so beats pretending.
|
|
144
|
+
return `${who} ${is} past first aid -- that's a stabilization job, and this street kit isn't it.`;
|
|
145
|
+
}
|
|
146
|
+
const hurt = track === 'physical' ? patient.damageTaken : patient.stunTaken;
|
|
147
|
+
if (hurt === 0) {
|
|
148
|
+
return track === 'physical'
|
|
149
|
+
? `${who} ${is} not bleeding -- save the kit.`
|
|
150
|
+
: `${who} ${is} not winded -- save the kit.`;
|
|
151
|
+
}
|
|
152
|
+
// p.205: "First Aid may only be applied to a character once for that
|
|
153
|
+
// set of wounds." Note it is per CHARACTER, not per track -- so one
|
|
154
|
+
// application covers the patient, whichever track it treated. Canon is
|
|
155
|
+
// silent on split tracks, and this is the conservative reading.
|
|
156
|
+
if (patient.firstAidGiven) {
|
|
157
|
+
return `${who} ${is} already been worked on for these wounds -- there's nothing left first aid can do until ${self ? 'you take' : 'they take'} fresh damage. Rest, a clinic, or a heal spell from here.`;
|
|
158
|
+
}
|
|
159
|
+
// p.205: "First Aid may not be applied if the character has been
|
|
160
|
+
// magically healed."
|
|
161
|
+
if (patient.magicallyHealed) {
|
|
162
|
+
return `The mana already knit ${self ? 'your' : `${patient.name}'s`} wounds shut. First aid has nothing to add to a spell's work.`;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Runs the test and applies the outcome. Callers MUST have cleared
|
|
168
|
+
* firstAidRefusal first -- this function assumes the treatment is legal
|
|
169
|
+
* and always consumes the once-per-wounds application.
|
|
170
|
+
*/
|
|
171
|
+
export function performFirstAid(medic, patient, kit, track) {
|
|
172
|
+
const cap = firstAidCap(medic, kit);
|
|
173
|
+
const roll = rollPool(firstAidPool(medic, kit), firstAidLimit(medic, kit));
|
|
174
|
+
if (roll.criticalGlitch) {
|
|
175
|
+
// p.206: "A critical glitch on a First Aid Test increases the damage
|
|
176
|
+
// by 1D3 (1D6 / 2) boxes." 1D6/2 rounds up, giving 1-3.
|
|
177
|
+
const worsened = Math.ceil(rollPool(1).rolls[0] / 2);
|
|
178
|
+
if (track === 'physical')
|
|
179
|
+
patient.takeDamage(worsened);
|
|
180
|
+
else
|
|
181
|
+
patient.takeStun(worsened);
|
|
182
|
+
// AFTER the damage, never before: takeDamage clears firstAidGiven
|
|
183
|
+
// (fresh damage is a fresh set of wounds), and boxes opened by a
|
|
184
|
+
// botched treatment are part of the SAME set -- setting the flag
|
|
185
|
+
// first would let the fumbling medic immediately try again.
|
|
186
|
+
patient.firstAidGiven = true;
|
|
187
|
+
return {
|
|
188
|
+
roll, healed: 0, worsened, netHits: 0, cap,
|
|
189
|
+
capBound: false, halvedByArmor: false, track,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const netHits = Math.max(0, roll.hits - FIRST_AID_THRESHOLD);
|
|
193
|
+
let boxes = Math.min(netHits, cap);
|
|
194
|
+
const capBound = netHits > cap;
|
|
195
|
+
// Armor halves the EFFECT, applied after the cap so that treating
|
|
196
|
+
// through a hardsuit always bites. (The book says "net effect"; halving
|
|
197
|
+
// the net hits first would let a low cap absorb the penalty entirely,
|
|
198
|
+
// which reads against the rule's stated purpose -- "the difficulty of
|
|
199
|
+
// treating the patient through armor".)
|
|
200
|
+
const halvedByArmor = boxes > 0 && inFullBodyArmor(patient);
|
|
201
|
+
if (halvedByArmor)
|
|
202
|
+
boxes = Math.ceil(boxes / 2);
|
|
203
|
+
const healed = track === 'physical' ? patient.healDamage(boxes) : patient.healStun(boxes);
|
|
204
|
+
// The application is spent whether it went well or badly: canon says
|
|
205
|
+
// "may only be applied once", not "may only SUCCEED once".
|
|
206
|
+
patient.firstAidGiven = true;
|
|
207
|
+
return { roll, healed, worsened: 0, netHits, cap, capBound, halvedByArmor, track };
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=first-aid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-aid.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/first-aid.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAa,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAmBrC;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAU;IACrC,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,GAAU;IACnD,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IAC/C,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,GAAU;IACpD,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC;QACtB,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,OAAO,GAAG,MAAM;QAChC,CAAC,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC;IAC7B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI;UACnB,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC;UAC7B,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;UACzB,KAAK,CAAC,aAAa,CAAC,CAAC;AAC3B,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,GAAU;IACrD,OAAO,WAAW,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;AAChD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,EAAE,GAAG,OAAO,CAAC,SAAoF,CAAC;IACxG,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QACtC,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,IAAI,4CAA4C,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;QAChF,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa,EAAE,OAAe,EAAE,GAAqB,EAAE,KAAiB;IAExE,MAAM,IAAI,GAAG,KAAK,KAAK,OAAO,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IACxC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;IAE/B,uEAAuE;IACvE,2CAA2C;IAC3C,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,qHAAqH,CAAC;IAC/H,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;QACrB,oEAAoE;QACpE,qEAAqE;QACrE,OAAO,GAAG,GAAG,IAAI,EAAE,8EAA8E,CAAC;IACpG,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC;IAC5E,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;QACf,OAAO,KAAK,KAAK,UAAU;YACzB,CAAC,CAAC,GAAG,GAAG,IAAI,EAAE,gCAAgC;YAC9C,CAAC,CAAC,GAAG,GAAG,IAAI,EAAE,8BAA8B,CAAC;IACjD,CAAC;IACD,qEAAqE;IACrE,oEAAoE;IACpE,uEAAuE;IACvE,gEAAgE;IAChE,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;QAC1B,OAAO,GAAG,GAAG,IAAI,EAAE,2FAA2F,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,2DAA2D,CAAC;IAC3M,CAAC;IACD,iEAAiE;IACjE,qBAAqB;IACrB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;QAC5B,OAAO,yBAAyB,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,IAAI,+DAA+D,CAAC;IACrI,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,KAAa,EAAE,OAAe,EAAE,GAAS,EAAE,KAAiB;IAE5D,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,QAAQ,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;IAE3E,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,qEAAqE;QACrE,wDAAwD;QACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACrD,IAAI,KAAK,KAAK,UAAU;YAAE,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;;YAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChC,kEAAkE;QAClE,iEAAiE;QACjE,iEAAiE;QACjE,4DAA4D;QAC5D,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;QAC7B,OAAO;YACL,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG;YAC1C,QAAQ,EAAE,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK;SAC7C,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC,CAAC;IAC7D,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,OAAO,GAAG,GAAG,CAAC;IAE/B,kEAAkE;IAClE,wEAAwE;IACxE,sEAAsE;IACtE,sEAAsE;IACtE,wCAAwC;IACxC,MAAM,aAAa,GAAG,KAAK,GAAG,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC;IAC5D,IAAI,aAAa;QAAE,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAEhD,MAAM,MAAM,GAAG,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1F,qEAAqE;IACrE,2DAA2D;IAC3D,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC;AACrF,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ISceneSeedJson } from '../types/seed/scene-seed.js';
|
|
2
|
+
/**
|
|
3
|
+
* WHERE THE MEET ACTUALLY IS, read off the drafted seed.
|
|
4
|
+
*
|
|
5
|
+
* Item g5CgJgmw9ix6AcD78: "Krow said to meet at Mama Wu's, that
|
|
6
|
+
* shouldn't happen." There is no Mama Wu's. The fixer's pitch is
|
|
7
|
+
* AI-generated free text, and the brief told him the client "will hold
|
|
8
|
+
* the meet on-site" without telling him WHERE -- so a model writing a
|
|
9
|
+
* fixer's voice did the natural thing and invented a venue. The player
|
|
10
|
+
* then travelled to a scene that contains no such place.
|
|
11
|
+
*
|
|
12
|
+
* This is the codebase's signature defect: a NAME REFERRING TO NOTHING.
|
|
13
|
+
* The house cure is to resolve names against the real roster or room
|
|
14
|
+
* list and repair or reject, never to ship an unresolved one (see
|
|
15
|
+
* scene-chunks.ts resolveNamedRef). The same cure applies here, and it
|
|
16
|
+
* is better than a prohibition: the seed already KNOWS the answer, so
|
|
17
|
+
* the fixer can be told the truth rather than merely forbidden a lie.
|
|
18
|
+
* A brief that only says "don't invent one" leaves him with nothing to
|
|
19
|
+
* say and invites a vaguer dodge.
|
|
20
|
+
*
|
|
21
|
+
* THE MEET IS WHERE THE CLIENT STANDS, not the start room -- generation
|
|
22
|
+
* places the client "in a REACHABLE room that makes a characterful meet
|
|
23
|
+
* spot" (scene-chunks.ts), which is frequently not where the runner
|
|
24
|
+
* arrives. Falling back to the start room is still TRUE (it is where
|
|
25
|
+
* they land, and the client is always reachable from it), so the
|
|
26
|
+
* fallback degrades to vaguer rather than to wrong.
|
|
27
|
+
*
|
|
28
|
+
* IT LIVES HERE, not in call.ts, because ONE ANSWER IS THE WHOLE POINT.
|
|
29
|
+
* The reported bug is two sources disagreeing about where the meet is
|
|
30
|
+
* (the vetting on the item: "Krow directing the player to Mama Wu's,
|
|
31
|
+
* then a conflicting message from Mr. Vane at a different location"), so
|
|
32
|
+
* a resolver that only one of the speakers consults cannot fix it. Every
|
|
33
|
+
* surface that tells the player or an NPC where the meet is reads this
|
|
34
|
+
* function; if you are about to write the words "the meet" and a place
|
|
35
|
+
* name, call this instead.
|
|
36
|
+
*/
|
|
37
|
+
export declare function meetLocationFor(seed: ISceneSeedJson | undefined): string | undefined;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE THE MEET ACTUALLY IS, read off the drafted seed.
|
|
3
|
+
*
|
|
4
|
+
* Item g5CgJgmw9ix6AcD78: "Krow said to meet at Mama Wu's, that
|
|
5
|
+
* shouldn't happen." There is no Mama Wu's. The fixer's pitch is
|
|
6
|
+
* AI-generated free text, and the brief told him the client "will hold
|
|
7
|
+
* the meet on-site" without telling him WHERE -- so a model writing a
|
|
8
|
+
* fixer's voice did the natural thing and invented a venue. The player
|
|
9
|
+
* then travelled to a scene that contains no such place.
|
|
10
|
+
*
|
|
11
|
+
* This is the codebase's signature defect: a NAME REFERRING TO NOTHING.
|
|
12
|
+
* The house cure is to resolve names against the real roster or room
|
|
13
|
+
* list and repair or reject, never to ship an unresolved one (see
|
|
14
|
+
* scene-chunks.ts resolveNamedRef). The same cure applies here, and it
|
|
15
|
+
* is better than a prohibition: the seed already KNOWS the answer, so
|
|
16
|
+
* the fixer can be told the truth rather than merely forbidden a lie.
|
|
17
|
+
* A brief that only says "don't invent one" leaves him with nothing to
|
|
18
|
+
* say and invites a vaguer dodge.
|
|
19
|
+
*
|
|
20
|
+
* THE MEET IS WHERE THE CLIENT STANDS, not the start room -- generation
|
|
21
|
+
* places the client "in a REACHABLE room that makes a characterful meet
|
|
22
|
+
* spot" (scene-chunks.ts), which is frequently not where the runner
|
|
23
|
+
* arrives. Falling back to the start room is still TRUE (it is where
|
|
24
|
+
* they land, and the client is always reachable from it), so the
|
|
25
|
+
* fallback degrades to vaguer rather than to wrong.
|
|
26
|
+
*
|
|
27
|
+
* IT LIVES HERE, not in call.ts, because ONE ANSWER IS THE WHOLE POINT.
|
|
28
|
+
* The reported bug is two sources disagreeing about where the meet is
|
|
29
|
+
* (the vetting on the item: "Krow directing the player to Mama Wu's,
|
|
30
|
+
* then a conflicting message from Mr. Vane at a different location"), so
|
|
31
|
+
* a resolver that only one of the speakers consults cannot fix it. Every
|
|
32
|
+
* surface that tells the player or an NPC where the meet is reads this
|
|
33
|
+
* function; if you are about to write the words "the meet" and a place
|
|
34
|
+
* name, call this instead.
|
|
35
|
+
*/
|
|
36
|
+
export function meetLocationFor(seed) {
|
|
37
|
+
if (!seed)
|
|
38
|
+
return undefined;
|
|
39
|
+
const client = seed.client
|
|
40
|
+
? seed.npcs?.find(n => n.player?.name === seed.client)
|
|
41
|
+
: undefined;
|
|
42
|
+
const clientRoom = client?.player?.startLocation;
|
|
43
|
+
// Only trust a room the scene really declares -- a startLocation that
|
|
44
|
+
// names no room is the same unresolved-name bug one level down, and
|
|
45
|
+
// repeating it into the fixer's mouth would be worse than silence.
|
|
46
|
+
const rooms = seed.rooms ?? [];
|
|
47
|
+
if (clientRoom && rooms.some(r => r.name === clientRoom))
|
|
48
|
+
return clientRoom;
|
|
49
|
+
return rooms.find(r => r.isStartRoom)?.name;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=meet-location.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meet-location.js","sourceRoot":"","sources":["../../../../../../../src/commands/game/sideQuest/utilities/meet-location.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,UAAU,eAAe,CAAC,IAAgC;IAC9D,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM;QACxB,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC;QACtD,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,UAAU,GAAG,MAAM,EAAE,MAAM,EAAE,aAAa,CAAC;IACjD,sEAAsE;IACtE,oEAAoE;IACpE,mEAAmE;IACnE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAC/B,IAAI,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC;QAAE,OAAO,UAAU,CAAC;IAC5E,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Player } from '../models/player.js';
|
|
2
|
+
/**
|
|
3
|
+
* THE PERCEPTION TEST (SR5 core p.135-136), shared by every verb that
|
|
4
|
+
* makes one.
|
|
5
|
+
*
|
|
6
|
+
* Item FNgptvRhbA5yMunbB, the player writing the spec in two sentences:
|
|
7
|
+
* '"look" should reveal items that are under a perception threshold.
|
|
8
|
+
* "search" is an active perception check'.
|
|
9
|
+
*
|
|
10
|
+
* The SPLIT they asked for already shipped (52ba4dda): `look` lists
|
|
11
|
+
* plain-sight goods with no test at all, which is p.135's own
|
|
12
|
+
* instruction -- characters "should always be able to observe what is
|
|
13
|
+
* immediately obvious without spending an action or making a test" --
|
|
14
|
+
* while `search` rolls. What had NOT shipped is the mechanical
|
|
15
|
+
* difference between the two rolls, and canon is specific about what it
|
|
16
|
+
* is:
|
|
17
|
+
*
|
|
18
|
+
* "Perceiver is specifically looking/listening for it +3"
|
|
19
|
+
* (p.135-136, Perception Test Modifiers)
|
|
20
|
+
*
|
|
21
|
+
* That +3 IS the active/passive distinction. Without it, `search` was an
|
|
22
|
+
* active perception check in name while rolling the same pool as the
|
|
23
|
+
* passive glance -- the player's second sentence unimplemented, one
|
|
24
|
+
* layer below the one that was fixed.
|
|
25
|
+
*
|
|
26
|
+
* AND THE BRACKET IS A LIMIT. `Perception + Intuition [Mental]`: neither
|
|
27
|
+
* verb applied one, so a huge pool could count unlimited hits. Limits
|
|
28
|
+
* cap HITS, never dice (utilities/dice.ts, and the house rule at the top
|
|
29
|
+
* of CLAUDE.md).
|
|
30
|
+
*
|
|
31
|
+
* ONE FUNCTION, BOTH CALLERS, deliberately. look.ts and search.ts each
|
|
32
|
+
* built this pool inline and had already drifted apart; that is the same
|
|
33
|
+
* one-caller drift that let the meet address disagree with itself
|
|
34
|
+
* (g5CgJgmw9ix6AcD78) and the same duplicated-mechanic shape that let
|
|
35
|
+
* `use <medkit>` diverge from `heal`. If you are about to roll
|
|
36
|
+
* Perception, roll it here.
|
|
37
|
+
*/
|
|
38
|
+
/** p.135-136: the modifier for deliberately looking, which is what makes
|
|
39
|
+
* an active search mechanically different from a passive glance. */
|
|
40
|
+
export declare const ACTIVELY_LOOKING = 3;
|
|
41
|
+
export interface IPerceptionTest {
|
|
42
|
+
pool: number;
|
|
43
|
+
/** The bracket. Undefined only when the actor has no limit to apply --
|
|
44
|
+
* see the Matrix note below; rollPool refuses a zero limit loudly. */
|
|
45
|
+
limit: number | undefined;
|
|
46
|
+
}
|
|
47
|
+
export interface IPerceptionOpts {
|
|
48
|
+
/** True for a deliberate hunt (`search`), false for a glance (`look`). */
|
|
49
|
+
active?: boolean;
|
|
50
|
+
/** Matrix Search only: a loaded Browse program is purpose-built for it. */
|
|
51
|
+
browse?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* ON THE GRID IT IS A DIFFERENT TEST WITH A DIFFERENT BRACKET. Matrix
|
|
55
|
+
* Search is Computer + Logic [Data Processing], so the Mental limit does
|
|
56
|
+
* NOT apply out there -- capping a decker's sweep by their Willpower
|
|
57
|
+
* would be a fresh deviation dressed as a fix. The engine takes the
|
|
58
|
+
* best of Perception and Computer so pre-Computer deckers lose nothing
|
|
59
|
+
* (a standing allowance, older than this function).
|
|
60
|
+
*
|
|
61
|
+
* Data Processing reads 0 for an actor with no deck at all; that is a
|
|
62
|
+
* caller's missing gate rather than a real limit, so it degrades to
|
|
63
|
+
* unlimited here instead of handing rollPool a zero it would reject.
|
|
64
|
+
*/
|
|
65
|
+
export declare function perceptionTest(actor: Player, opts?: IPerceptionOpts): IPerceptionTest;
|
|
66
|
+
/**
|
|
67
|
+
* PERCEPTION THRESHOLDS (p.136). Named because the numbers appear in
|
|
68
|
+
* player-facing text and in cover reads, and a bare 2 or 4 in three
|
|
69
|
+
* files is how they drift.
|
|
70
|
+
*/
|
|
71
|
+
export declare const PERCEPTION_THRESHOLD: {
|
|
72
|
+
/** Street sign, pedestrian, conversation, silenced gunfire. */
|
|
73
|
+
readonly normal: 2;
|
|
74
|
+
/** Secret door, needle in a haystack, subvocal speech. */
|
|
75
|
+
readonly hidden: 4;
|
|
76
|
+
};
|