Fire modes, fitted sights and lamps, and a tab rail
Selectable fire, with the modifiers on the choice Every weapon has carried a fireMode since the packs were first built and nothing read it: a musket could rip a ten-round burst and "rapid" bought a weapon nothing. FIRE_MODE now gates the burst rules and carries its own attack modifier — slow +10% and then you work for the next one, fast a second shot at -20% to both, rapid bursts, beam +5% and it can be held on the target for damage instead of hits. The gate lives in burstAttack too, so no macro can route round it. `fast` was on five catalogue weapons and was not in the system's list of modes at all, so re-saving one of them silently blanked it; the list is derived from the rule now. burstAttack shipped complete and was reachable from nothing. It is the resolution path the selector chooses, and takes the dialog's whole stack rather than charging the player twice for the same burst. Sights and lamps that apply themselves A sight is graded by range band, so a telescopic sight is a -10% liability inside ten metres and worth +20% at extreme, and it only counts if it fits. The band is already known when the modifier is wanted, so nothing is selected and nothing is forgotten. The dark costs something now; a carried lamp buys it back to dim, never to daylight, and the card says it shows them exactly where you are. A migration fits gear that predates the fields, matched on the stamped key rather than by name. The damage card The last card still wearing Foundry's stock dice styling — it posted a bare Roll and let the core template dress it. It renders through the module's own card now, with the Roll still on the message so dice stay inspectable. A rail instead of a strip Eight tabs across an 820px sheet compressed to 11px type. They run down the side now, one line each, with counts as badges; the sheet is 152px wider to pay for it. Also fixed: any weapon with a Coherence cost could not be attacked with AT ALL — the handler re-read the weapon after an await, when currentTarget is already null, and the attack died on a TypeError with nothing shown. And the duplicate-rule guard failed an honest table because FIRE_MODE and speedBands share two key names; it wants a majority now, and prints partial overlaps rather than swallowing them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d4442a55f0
commit
2e10ecced9
@@ -564,6 +564,190 @@ export function burstRoundsSpent(size, loaded) {
|
||||
/** A fumbled burst is the interesting one: it is a jam, or it is worse. */
|
||||
export const BURST_FUMBLE = ["RINGBRP.Burst.Jam", "RINGBRP.Burst.Empty", "RINGBRP.Burst.Wild"];
|
||||
|
||||
/**
|
||||
* WHAT A WEAPON CAN DO WITH A ROUND.
|
||||
*
|
||||
* Every weapon in the catalogue has carried a `fireMode` since the packs were first
|
||||
* built, and until now precisely nothing read it. A musket could rip a ten-round
|
||||
* burst; a weapon marked "rapid" got no benefit from being rapid. The mode is now
|
||||
* the gate on the burst rules and it carries its own attack modifier, so choosing
|
||||
* a slow weapon is a decision rather than a drawback.
|
||||
*
|
||||
* The trade runs one way along the ladder. The faster a weapon puts rounds out, the
|
||||
* less each individual shot is worth; the slower it is, the more the one shot you
|
||||
* get is worth taking properly.
|
||||
*
|
||||
* slow one shot, then work — and it is fired by somebody who knows that
|
||||
* single the ordinary case, and the baseline every modifier is measured from
|
||||
* fast the action will give you a second shot if you will accept both being worse
|
||||
* rapid bursts, which is the automatic-fire rules above
|
||||
* beam no recoil, no lead, no drop — and it can be held on the target
|
||||
*
|
||||
* `fast` was in the catalogue on five weapons (longbow, sling, javelin, the Webley
|
||||
* and the Lee-Enfield) and was not in the system's list of modes at all, so the item
|
||||
* sheet offered no matching option and re-saving one of those weapons silently blanked
|
||||
* its mode. It is a real rate of fire and it is now a real mode.
|
||||
*/
|
||||
export const FIRE_MODE = {
|
||||
slow: { id: "slow", label: "RINGBRP.Fire.slow.Label", attack: 10, burst: [],
|
||||
reloads: true, second: false, sustain: false },
|
||||
single: { id: "single", label: "RINGBRP.Fire.single.Label", attack: 0, burst: [],
|
||||
reloads: false, second: false, sustain: false },
|
||||
fast: { id: "fast", label: "RINGBRP.Fire.fast.Label", attack: 0, burst: [],
|
||||
reloads: false, second: true, sustain: false },
|
||||
rapid: { id: "rapid", label: "RINGBRP.Fire.rapid.Label", attack: 0, burst: ["short", "long", "full"],
|
||||
reloads: false, second: false, sustain: false },
|
||||
beam: { id: "beam", label: "RINGBRP.Fire.beam.Label", attack: 5, burst: [],
|
||||
reloads: false, second: false, sustain: true }
|
||||
};
|
||||
|
||||
/** A blank mode is a melee weapon or an unclassified one: it behaves as `single`. */
|
||||
export function fireModeFor(id) {
|
||||
return FIRE_MODE[id] || FIRE_MODE.single;
|
||||
}
|
||||
|
||||
/** Which bursts this weapon can actually produce. Empty means it cannot. */
|
||||
export function burstSizesFor(id) {
|
||||
return fireModeFor(id).burst;
|
||||
}
|
||||
|
||||
/** The flat modifier for firing this weapon at all, before range and everything else. */
|
||||
export function fireModeAttackFor(id) {
|
||||
return fireModeFor(id).attack;
|
||||
}
|
||||
|
||||
/**
|
||||
* The second shot a fast action will give you. BOTH shots take it, not just the
|
||||
* second — the cost of hurrying is that you hurried the first one too, which is
|
||||
* what stops this from being a free extra attack for anyone holding a revolver.
|
||||
*/
|
||||
export const SECOND_SHOT = -20;
|
||||
|
||||
export function secondShotPenaltyFor(id) {
|
||||
return fireModeFor(id).second ? SECOND_SHOT : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* SUSTAINED BEAMS.
|
||||
*
|
||||
* A beam does not fire discrete rounds, so a burst is the wrong shape for it: there
|
||||
* is nothing to count hits of. What a beam can do instead is stay on the target,
|
||||
* and that buys DAMAGE rather than hits — the one place in this system where a firing
|
||||
* choice adds dice instead of adding rolls.
|
||||
*
|
||||
* It is paid for twice, in accuracy and in charge, because holding a line on a moving
|
||||
* person is hard and because the alternative is that everybody holds every shot.
|
||||
*/
|
||||
export const SUSTAIN = {
|
||||
tap: { id: "tap", label: "RINGBRP.Sustain.tap", mod: 0, bonus: "", charge: 1 },
|
||||
hold: { id: "hold", label: "RINGBRP.Sustain.hold", mod: -10, bonus: "1d6", charge: 3 },
|
||||
lock: { id: "lock", label: "RINGBRP.Sustain.lock", mod: -25, bonus: "2d6", charge: 6 }
|
||||
};
|
||||
|
||||
export function sustainFor(id) {
|
||||
return SUSTAIN[id] || SUSTAIN.tap;
|
||||
}
|
||||
|
||||
/** The extra dice a sustained beam adds to its damage formula, or "" for none. */
|
||||
export function sustainDamageFor(id) {
|
||||
return sustainFor(id).bonus;
|
||||
}
|
||||
|
||||
/**
|
||||
* What a sustained beam costs, counted in SHOTS — because `ammo.loaded` is spent one
|
||||
* per shot everywhere else in the system, so a cell holding 20 holds twenty shots.
|
||||
*
|
||||
* This was briefly multiplied by the weapon's `energyPerShot`, which made a locked
|
||||
* beam cost 60 from a 20-shot cell and therefore impossible to fire at all. That
|
||||
* field is written by the pack builder and read by nothing; the magazine is the
|
||||
* authority on what a weapon has left.
|
||||
*/
|
||||
export function sustainChargeFor(id) {
|
||||
return sustainFor(id).charge;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* What is bolted to the weapon */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* SIGHTS.
|
||||
*
|
||||
* A sight is not a flat bonus, and making it one is why most systems end up with
|
||||
* everybody carrying the same scope. A sight is good at the distance it was ground
|
||||
* for and a liability at the others, so the bonus is read off the RANGE BAND the
|
||||
* attack is already using — which means it applies itself, with nothing for the
|
||||
* player to remember and nothing for them to choose.
|
||||
*
|
||||
* A telescopic sight is a PENALTY inside ten metres. That is the whole point of it.
|
||||
*/
|
||||
export const SIGHT = {
|
||||
none: { id: "none", label: "RINGBRP.Sight.none",
|
||||
bands: { short: 0, medium: 0, long: 0, extreme: 0 }, dark: false },
|
||||
irons: { id: "irons", label: "RINGBRP.Sight.irons",
|
||||
bands: { short: 5, medium: 5, long: 0, extreme: 0 }, dark: false },
|
||||
reflex: { id: "reflex", label: "RINGBRP.Sight.reflex",
|
||||
bands: { short: 10, medium: 5, long: 0, extreme: -10 }, dark: false },
|
||||
telescopic: { id: "telescopic", label: "RINGBRP.Sight.telescopic",
|
||||
bands: { short: -10, medium: 5, long: 15, extreme: 20 }, dark: false },
|
||||
thermal: { id: "thermal", label: "RINGBRP.Sight.thermal",
|
||||
bands: { short: 5, medium: 5, long: 5, extreme: 0 }, dark: true }
|
||||
};
|
||||
|
||||
export function sightFor(id) {
|
||||
return SIGHT[id] || SIGHT.none;
|
||||
}
|
||||
|
||||
/** What this sight is worth at this band. Bands come from the range ladder. */
|
||||
export function sightBonusFor(id, band) {
|
||||
const b = sightFor(id).bands;
|
||||
return Number(b?.[band]) || 0;
|
||||
}
|
||||
|
||||
/** A thermal sight is the only one that removes the dark rather than surviving it. */
|
||||
export function sightSeesInDark(id) {
|
||||
return !!sightFor(id).dark;
|
||||
}
|
||||
|
||||
/**
|
||||
* LIGHT.
|
||||
*
|
||||
* The department issues lamps because most crossings do not come with the lights on,
|
||||
* and until now the dark cost an attack nothing at all. It costs something now, and
|
||||
* a carried lamp buys most of it back.
|
||||
*
|
||||
* What it does not buy back is the second half: a lamp is a position. The rule
|
||||
* returns `revealed` so the card can say so, and what the other side does about it
|
||||
* is the GM's, not the engine's.
|
||||
*/
|
||||
export const LIGHTING = {
|
||||
lit: { id: "lit", label: "RINGBRP.Lighting.lit", mod: 0 },
|
||||
dim: { id: "dim", label: "RINGBRP.Lighting.dim", mod: -10 },
|
||||
dark: { id: "dark", label: "RINGBRP.Lighting.dark", mod: -30 },
|
||||
pitch: { id: "pitch", label: "RINGBRP.Lighting.pitch", mod: -50 }
|
||||
};
|
||||
|
||||
/** A lamp never makes it daylight. This is the best it gets you. */
|
||||
export const LIGHT_FLOOR = "dim";
|
||||
|
||||
export function lightingFor(id) {
|
||||
return LIGHTING[id] || LIGHTING.lit;
|
||||
}
|
||||
|
||||
/**
|
||||
* The modifier the dark actually applies, after a lamp and after a thermal sight.
|
||||
* A sight that sees in the dark beats a lamp, and does not tell anybody where you are.
|
||||
*/
|
||||
export function lightingModFor(id, { lightSource = false, seesInDark = false } = {}) {
|
||||
const here = lightingFor(id);
|
||||
if (seesInDark) return { mod: 0, effective: "lit", revealed: false };
|
||||
if (!lightSource) return { mod: here.mod, effective: here.id, revealed: false };
|
||||
const floor = lightingFor(LIGHT_FLOOR);
|
||||
// A lamp cannot make anything worse, and cannot make it better than the floor.
|
||||
const eff = here.mod >= floor.mod ? here : floor;
|
||||
return { mod: eff.mod, effective: eff.id, revealed: eff.id !== here.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* TWO WEAPONS.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user