The pack
lightGun
function lightGun(): Pack<{ lightGun: typeof api }>
Install the light gun API.
Helpers
createGun
function createGun(def: GunDef): Gun
Build a gun.
emptyWorld
const emptyWorld: HitWorld
The empty world: every shot misses.
frontToBack
function frontToBack(targets: readonly ScreenTarget[]): ScreenTarget[]
Targets sorted front to back: higher z first; ties, later in the list first.
GUN_STEP
const GUN_STEP: number
The default fixed step: 1/60 s.
layeredWorld
function layeredWorld(...worlds: HitWorld[]): HitWorld
Combine worlds: candidates from earlier worlds come first (an overlay in front of a scene).
rayWorld
function rayWorld(def: RayWorldDef): HitWorld
Wrap a ray cast (3D picking) as a hit world; candidates are re-sorted by distance when given.
regionContains
function regionContains(t: ScreenTarget, r: ScreenRegion, x: number, y: number): boolean
Is a screen point inside a region of a target.
regionMeetsCircle
function regionMeetsCircle(t: ScreenTarget, r: ScreenRegion, x: number, y: number, radius: number): boolean
Does a region of a target meet a circle.
screenWorld
function screenWorld(targets: readonly ScreenTarget[] | (() => readonly ScreenTarget[])): HitWorld
A hit world over 2D screen targets. The list is read at query time, so it can be a live array.
validateGun
function validateGun(def: GunDef): void
Validate a gun definition; throws with a clear message.
Types
AimDef
interface AimDef
How the crosshair follows its input.
smoothing?: number— Time constant in seconds of an exponential follow toward the input (0, the default, is exact: a positional gun).stickSpeed?: number— Crosshair speed in pixels per second at full stick deflection (stick or keyboard aiming). Default 240.clamp?: boolean— Keep the crosshair inside the view (off-screen detection still uses the raw input). Default true.start?: { x: number; y: number }— Where the crosshair starts (default the view centre).
BoostState
interface BoostState
A temporary weapon boost (the Super Machine Gun).
weapon: string— Weapon id.rate: number— Fire-rate multiplier.free: boolean— Shots do not use ammunition.left: number— Seconds left.
ComboDef
interface ComboDef
Streak (combo) rules.
window?: number— Seconds between hostile hits that keep a streak going (default unlimited: only a miss ends it).
Gun
interface Gun
A running gun.
readonly def: GunDef— The definition.readonly state: GunState— Live state (plain data).tick(seconds: number, input: GunInput, world?: HitWorld): void— Advance bysecondsin fixed steps; trigger presses shorter than a step are latched, never lost.step(input: GunInput, world?: HitWorld): void— Advance one fixed step.drain(): GunEvent[]— Take the events since the last drain.weapon(id: string): Readonly<WeaponDef>— A weapon's definition.fireAt(x: number, y: number, weapon?: string, world?: HitWorld, opts?: { free?: boolean }): boolean— Fire a weapon at a point now (ignores the trigger and cooldown; uses ammunition unlessfree).reload(weapon?: string): boolean— Start a reload of the primary (or a named) weapon, as the reload button does.give(weapon: string, supply: Supply): void— Add ammunition.setAmmo(weapon: string, ammo: { rounds?: number; magazines?: number | null; stock?: number | null }): void— Set ammunition outright (a stage restock).boost(weapon: string, b: { rate?: number; free?: boolean; seconds: number }): void— Start or extend a boost: fire-rate multiplier and free rounds forseconds.select(weapon: string): void— Switch the primary weapon.setReady(ready: boolean): void— Allow or stop firing (cover, cut-scenes).cue(fx: "shake" | "flash", amount: number, seconds: number, color?: string): void— Queue a presenter cue (shake, flash) through the gun's event stream.peek(world: HitWorld, x?: number, y?: number): HitCandidate | null— The first candidate under the crosshair (or a point), skipping unhittable ones.inReloadBand(x?: number, y?: number, weapon?: string): boolean— Is the crosshair (or a point) in the weapon's off-screen reload band (reload.margin).canFire(weapon?: string): boolean— Can this weapon fire right now (ready, loaded or able to fire empty, not reloading).accuracy(): number— Accuracy so far: hits / shots (0 with no shots).shakeOffset(): { x: number; y: number }— Current shake offset for presenters (deterministic from the gun's time).snapshot(): GunState— Deep copy of the state.restore(s: GunState): void— Restore a snapshot from a gun built from the same definition.reset(): void— Back to the start.
GunDef
interface GunDef
A light gun.
view: ViewSize— The screen-space view the gun points at.aim?: AimDef— Crosshair behaviour.weapons: WeaponDef[]— Weapons; at least one.primary?: string— Weapon on the trigger (default the first).special?: string— Weapon on the special button (rockets); optional.parts?: Record<string, PartDef>— Body-part damage multipliers.protect?: ProtectDef— Protected-target rules.combo?: ComboDef— Streak rules.seed?: number— Seed for spread and recoil (default 1).step?: number— Fixed step in seconds (default 1/60).
GunEvent
type GunEvent = | { type: "shot"; shot: number; weapon: string; x: number; y: number; points: Array<{ x: number; y: number }>; free: boolean; onScreen: boolean } | { type: "launch"; shot: number; weapon: string; x: number; y: number; from: { x: number; y: number }; travel: number } | { type: "hit"; shot: number; weapon: string; target: string; region: string; part: string; damage: number; x: number; y: number; kind?: string; data?: Record<string, number | string | boolean>; streak: number; blast: boolean } | { type: "blocked"; shot: number; weapon: string; target: string; region: string; x: number; y: number; kind?: string } | { type: "protected-hit"; shot: number; weapon: string; target: string; region: string; protect: string; warning: boolean; penalty: Record<string, number> | null; x: number; y: number; kind?: string; data?: Record<string, number | string | boolean> } | { type: "miss"; shot: number; weapon: string; x: number; y: number } | { type: "blast"; shot: number; weapon: string; x: number; y: number; radius: number } | { type: "dry"; weapon: string } | { type: "reload"; weapon: string; time: number; via: ReloadMode } | { type: "reloaded"; weapon: string; rounds: number; magazines: number | null } | { type: "empty"; weapon: string } | { type: "boost"; weapon: string; seconds: number } | { type: "boost-end"; weapon: string } | { type: "streak-end"; streak: number } | { type: "fx"; fx: "muzzle" | "shake" | "flash"; x: number; y: number; amount: number; seconds: number; color?: string }
Events, drained with drain().
GunInput
interface GunInput
One step of gun input. Every field is optional.
x?: number— Pointer x in view pixels (absolute aim).y?: number— Pointer y in view pixels (absolute aim).onScreen?: boolean— Is the gun pointed at the screen (default: the raw point is inside the view).stickX?: number— Stick x in -1..1 (relative aim).stickY?: number— Stick y in -1..1, positive down (relative aim).snap?: boolean— Jump the crosshair to the input with no smoothing (a touch tap).trigger?: boolean— Trigger held.special?: boolean— Special button held (fires the special weapon).reload?: boolean— Reload button held.weapon?: string— Switch the primary weapon to this id.
GunState
interface GunState
The gun's state (plain data).
time: number— Seconds since creation.aim: { x: number; y: number }— The crosshair as drawn and fired: smoothed aim plus recoil, in view pixels.base: { x: number; y: number }— The smoothed aim without recoil.raw: { x: number; y: number }— The raw input point (may be off screen).recoil: { x: number; y: number }— Accumulated recoil offset.onScreen: boolean— Is the gun pointed at the screen.ready: boolean— Can the gun fire (false while holstered: cover, cut-scenes).primary: string— Weapon on the trigger.special: string | null— Weapon on the special button, or null.weapons: Record<string, WeaponState>— Weapons by id.boosts: BoostState[]— Active boosts.pending: PendingShot[]— Shots in flight.stats: GunStats— Statistics.warningsLeft: number— Protected-hit warnings left.flash: number— Seconds of muzzle flash left.shake: { amount: number; left: number; total: number }— Current screen shake: amplitude in pixels and seconds left of the longest shake.serial: number— Last shot serial number.rng: number[]— The seeded random stream's state: four words, from the kernel'srng.snapshot(). It advances in place and is saved with the state.specialHeld: boolean— Special button held last step.reloadHeld: boolean— Reload button held last step.
GunStats
interface GunStats
Accuracy and streak statistics.
shots: number— Trigger shots fired (a multi-pellet shot counts once).hits: number— Shots that hit at least one hostile target.misses: number— Shots that hit nothing hostile or protected.blocked: number— Shots stopped by a shield.protectedHits: number— Shots that hit a protected target.warnings: number— Warnings used.parts: Record<string, number>— Hits per body part.byWeapon: Record<string, { shots: number; hits: number }>— Shots and hits per weapon.streak: number— Current streak of consecutive hitting shots.bestStreak: number— Best streak.lastHit: number— Gun time of the last hostile hit (-1: none).
HitCandidate
interface HitCandidate
One thing a shot could hit, in the consumer's occlusion order.
target: string— Target id (the consumer's).region?: string— Region id within the target (default'body').part?: string— Body part for damage multipliers and statistics (default the region id).protected?: string | boolean— Protected:trueor a kind label ('hostage','friend'); hits are penalties, never damage.shield?: boolean— A shield: stops the shot without harm (a bullet-proof vest, a white eye).solid?: boolean— Stops the shot after a hit (default true).falselets it continue (thin cover that still counts a hit).damage?: number— Damage multiplier for this region (default 1).hittable?: boolean— Shots ignore it (default true).distance?: number— Distance along a ray (3D) or depth (2D), for reports.kind?: string— The consumer's kind id, copied into events.data?: Record<string, number | string | boolean>— Free data copied into events.
HitWorld
interface HitWorld
Answers "what is under this screen point" for the gun.
at(x: number, y: number, shot: ShotInfo): HitCandidate[]— Candidates under a screen point, front to back.within?(x: number, y: number, radius: number, shot: ShotInfo): HitCandidate[]— Candidates whose regions meet a circle (blasts); order does not matter. Default:atat the centre.
PartDef
interface PartDef
Damage multiplier for a named body part (head, body, limb ...).
damage?: number— Damage multiplier (default 1).
PendingShot
interface PendingShot
A shot travelling to its impact point.
shot: number— Shot serial number.weapon: string— Weapon id.x: number— Impact x.y: number— Impact y.from: { x: number; y: number }— Launch point.t: number— Seconds since launch.travel: number— Seconds to impact.
ProtectDef
interface ProtectDef
Protected targets: hostages, civilians, travellers.
warnings?: number— Free warnings per session before penalties apply (Crossbow: "DON'T SHOOT YOUR FRIENDS!"). Default 0.warnKinds?: string[]— Protected kinds the warnings cover (default every kind).penalties?: Record<string, Record<string, number>>— Penalty per protected kind ('*'for any): free-form numbers the consumer applies (score,damage,lives...).
RayWorldDef
interface RayWorldDef
A consumer's 3D picking, wrapped as a hit world.
cast(x: number, y: number, shot: ShotInfo): HitCandidate[]— Candidates along the ray through a screen point, nearest first (for rail-route: screenToAim, then pick).within?(x: number, y: number, radius: number, shot: ShotInfo): HitCandidate[]— Candidates inside a blast centred on a screen point (optional).
RecoilDef
interface RecoilDef
Felt recoil: the crosshair kicks per shot and recovers.
kick: number— Pixels the crosshair kicks up per shot.jitter?: number— Random sideways kick, up to this many pixels either way (default 0).recover?: number— Recovery time constant in seconds (default 0.12).max?: number— Largest accumulated kick in pixels (default 4 x kick).
ReloadDef
interface ReloadDef
Reload rules for a magazine weapon.
modes?: ReloadMode[]— Ways to reload (default['auto']):autowhen the magazine runs dry,buttonon the reload input orreload(),offscreenby pulling the trigger with the gun pointed off the screen.time?: number— Seconds a reload takes (default 0: instant). A reload fills the magazine and uses one spare; rounds left in the old one are dropped.margin?: number— Withoffscreen: a band around the edge of the view, as a fraction of its width and height, that counts as "off the screen" for reloading (default 0). A mouse or a finger cannot leave a browser game, so a trigger pulled with the crosshair in the band reloads a magazine that is not full; with a full magazine the shot fires as usual.
ReloadMode
type ReloadMode = "auto" | "button" | "offscreen"
How a magazine is refilled.
ScreenRegion
interface ScreenRegion
A hit region of a screen target, in target units relative to its centre (scaled by the target's scale).
id: string— Region id.part?: string— Body part for damage multipliers and statistics (default the id).shape?: "circle" | "rect"— Shape (default'rect').x?: number— Centre x offset (default 0).y?: number— Centre y offset, y down (default 0).r?: number— Circle radius.w?: number— Rectangle width.h?: number— Rectangle height.protected?: string | boolean— Protected region (overrides the target).shield?: boolean— Shield: stops shots without harm.solid?: boolean— Stops the shot after a hit (default true).damage?: number— Damage multiplier (default 1).hittable?: boolean— Shots ignore it (default true).
ScreenTarget
interface ScreenTarget
A target on the screen.
id: string— Target id.x: number— Centre x in view pixels.y: number— Centre y in view pixels.scale?: number— Scale applied to the regions (default 1).z?: number— Draw order: higher is in front and is tested first (default 0; ties: later in the list is in front).w?: number— Default rectangle width when there are no regions.h?: number— Default rectangle height when there are no regions.regions?: ScreenRegion[]— Regions, tested in order (default onewxhrectangle'body').protected?: string | boolean— The whole target is protected (trueor a kind label).shield?: boolean— Every region is a shield unless the region says otherwise.hittable?: boolean— Shots ignore the whole target.kind?: string— Kind id copied into candidates.data?: Record<string, number | string | boolean>— Free data copied into candidates.
ShotInfo
interface ShotInfo
What a resolver is told about the shot it is resolving.
shot: number— Shot serial number.weapon: string— Weapon id.radius: number— Blast radius for area queries (0 for point shots).
Supply
interface Supply
What give adds to a weapon.
rounds?: number— Rounds into the current magazine (clamped to the magazine size unlessoverfill).magazines?: number— Spare magazines.stock?: number— Stock.overfill?: boolean— Allow rounds beyond the magazine size (Operation Wolf's pickups add 30 rounds to the count).
TriggerMode
type TriggerMode = "single" | "auto" | "burst"
Trigger behaviour: one shot per press, repeat while held, or a burst per press.
ViewSize
interface ViewSize
A screen-space area in logical pixels (the gun's view).
w: number— Width.h: number— Height.
WeaponDef
interface WeaponDef
A weapon as data.
id: string— Unique id.trigger?: TriggerMode— Trigger mode (default'single').rate?: number— Shots per second forautoand within aburst(default 10).burst?: number— Shots per burst (default 3).cooldown?: number— Seconds after a single shot or a burst before the next can start (default 0).damage?: number— Damage per hit (default 1).spread?: number— Uniform spread radius in pixels around the crosshair (default 0).pellets?: number— Pellets per shot, each spread independently (default 1).pierce?: number— Extra targets a shot passes through after the first solid hit (default 0).pierceShields?: boolean— Shots go through shields (default false).radius?: number— Blast radius in pixels: every target in the circle is hit (rockets, grenades). Default none.blastShields?: boolean— Shields also stop blasts (default false).spareProtected?: boolean— Blasts do not hurt protected targets (default false).travel?: number— Seconds from the trigger to impact; resolved against the world when it lands (default 0: hitscan).launch?: { x: number; y: number }— Where a travelling shot starts on the screen, for presenters (default the bottom centre).magazine?: number— Rounds per magazine; omit for no magazine.magazines?: number— Spare magazines at the start; omit for unlimited spares.maxMagazines?: number— Most spare magazines carried (default unlimited).stock?: number— A simple stock of shots (rockets, grenades) when there is no magazine; omit both for unlimited.maxStock?: number— Most stock carried (default unlimited).reload?: ReloadDef— Reload rules (magazine weapons).whenEmpty?: { rate: number } | null— With no rounds and no spare magazines left, keep firing at this rate (shots per second) instead of clicking dry.recoil?: RecoilDef— Crosshair recoil.fx?: WeaponFx— Presenter cues.
WeaponFx
interface WeaponFx
Presenter cues a weapon emits.
flash?: number— Muzzle flash seconds per shot (default 0.05).shake?: number— Screen shake in pixels per shot (a recoil motor; default 0).shakeTime?: number— Seconds each per-shot shake lasts (default 0.08).blast?: number— Screen shake in pixels when a blast lands (default 0).blastTime?: number— Seconds a blast shake lasts (default 0.4).
WeaponState
interface WeaponState
Live state of one weapon.
id: string— Weapon id.rounds: number— Rounds in the magazine (magazine weapons).magazines: number | null— Spare magazines; null is unlimited.stock: number | null— Stock (stock weapons); null is unlimited or not a stock weapon.reloading: number— Seconds left of a reload in progress (0: none).cooldown: number— Seconds before the next shot may fire.burstLeft: number— Shots left in the current burst.held: boolean— The trigger driving this weapon was held last step.


