@adatechnology/game-weapons
v0.6.0
Published
Engine-agnostic weapons and ammunition: real calibres as data, magazines, fire modes, spread, recoil, pellets, melee, flame, heat, throwables and tools. Produces shots (energy, calibre, direction) any damage model can consume. Knows nothing about any game
Maintainers
Readme
@adatechnology/game-weapons
Engine-agnostic weapons and ammunition. 28 real calibres as data (muzzle energy from 0.5 × m × v²), 79 real models with their brands and measures
(Glock 17, AK-47, Remington 870, Browning M2...), 17 explosives, 42 generic archetypes, and a deterministic runtime: magazines, fire modes, reload by kind
with its steps, draw, manual cycle, jams and clearing, safety, barrel heat, spread and bloom, shotgun pellets, recoil, noise, plus melee, flame, heat,
throwables, launchers, tools and a fire extinguisher. A trigger pull returns plain-data Shots and Uses, so any damage model can consume them. It knows nothing about any game.
Units: metres, seconds, joules, mm (calibre diameter), grams (bullet mass), m/s, degrees, °C. Every random choice comes from a seeded generator, so the same seed gives the same shots on every machine.
Contracts live in @adatechnology/game-contracts; the types are re-exported here, so existing imports keep working.
Install
pnpm add @adatechnology/game-weaponsA plain <script src=".../dist/index.global.js"> exposes the GameWeapons global. No runtime dependencies.
Usage
import { createArsenal, DEFAULT_LOADOUT } from '@adatechnology/game-weapons'
const arsenal = createArsenal({ seed: 42, locale: 'pt-BR' })
const gun = arsenal.create('shotgun_pump_12', { reserve: 24 })
arsenal.name('shotgun_pump_12') // "espingarda de repetição calibre 12"
// every frame
gun.update(dt)
if (mouseDown) {
const r = gun.trigger(cameraPos, cameraDir, dt, mouseHeldSinceLastFrame)
for (const shot of r.shots) {
// feed the shots to @adatechnology/game-destruction
world.shoot(shot.origin, shot.direction, shot.energy, shot.caliber)
}
for (const use of r.uses) handleUse(use) // melee swing, flame tick, throw, tool...
camera.kick(...r.recoil)
horde.hear(cameraPos, r.noise)
} else gun.release()
if (reloadKey) gun.reload()Semi / single / pump / bolt weapons fire once per pull: call trigger(..., held = true) while the button stays down and they fire only the first
time (status cooldown afterwards) until release(), or pass held = false for every new click. auto fires at the cyclic rpm while held (several
shots per dt if needed). burst emits its rounds together in one call. Reloading is never automatic: the host calls reload(); a tube-fed weapon
loads one round per reload seconds and the trigger interrupts it. trigger(dt) and update(dt) can both be called in the same frame without
counting dt twice.
Non-firearms: flame/heat drain fuelPerSecond × dt of fuel (no_fuel when dry), tool ticks with duration: dt, extinguisher spends its
charge in seconds, throwable throws (velocity = direction × throwSpeed plus a small upward arc, three by default; a timer one is cooked, see below), melee swings at
its rpm, hand produces one hand use per pull.
Handling: events, faults and heat
const rifle = arsenal.create('ak_47', { condition: 'used', ammoQuality: 'reloaded' })
rifle.draw() // 0.35 s: the trigger answers `drawing`
rifle.reload('auto') // 'full' loads a revolver all at once, 'one_by_one' forces round by round
const result = rifle.trigger(origin, direction, dt, held)
result.rounds // cartridges spent: a shell is one, a burst is several
result.roundTimes // seconds after the start of the call at which each round left the barrel
if (result.fault) rifle.clearFault() // 1.2 s; `rifle.fault` stays set until it ends
rifle.setSafety(true) // the trigger answers `safe`
rifle.heatLevel // 0..1, from the barrel heat (`rifle.heat`, up to 140)
for (const event of rifle.drain()) animate(event) // draw, shot, misfire, cycle, reload (+ stages, feed), empty, mode, fault, clear, safetyEach cartridge rolls its own fault from a second generator (seed ^ 0x6a09e667), so the spread of a seed does not change when faults are on. The chance is
condition × faultFactor × (1 + max(0, heat − 60) / 40) (new 0.3%, used 1.5%, worn 5%, rusty 12%), and reloaded ammunition adds a misfire chance of 8%.
A revolver or break-action only misfires (the round is spent, no jam); a pump, bolt or lever fails to extract; the rest stovepipe or double feed.
A bow or crossbow has no cartridge: it adds no heat and never jams. Everything is a number you can change with rules (faults: false turns jams off,
faultChance takes a function), and the order of the reload steps is in RELOAD_STAGES.
Real models point to a generic archetype for the handling numbers (spread, bloom, range, recoil); patch, remove and include change the shipped
entries by id, and checkArsenal reports every inconsistency at once.
Data
CALIBERS (28, generated by tools/weapons-catalog): from 22lr and 9x19 to 50bmg, with the new 9x18, 5_7x28, 44mag, 50ae, 5_45x39, 30_30,
7_62x54r, 30_06, 300wm and 408cheytac. Muzzle energy is per projectile (per pellet for shotguns), e.g. 9×19 ~500 J, 5.56 ~1750 J, .308 ~3500 J,
.50 BMG ~18 kJ, 00 buck ~300 J per pellet. Each calibre also carries the audible distance of its shot (noise: 150 / 300 / 420 / 600 m), the case type
that sets the heat per round, and the cartridge measures.
WEAPONS is the union of four files: firearms.json (73 real models, from the Galeria and the Arsenal), launchers.json (4 rocket launchers and 2
flamethrowers), explosive-weapons.json (flashbang, dynamite, C4 and the Claymore, kept by hand: they point to frag_grenade for the throw) and archetypes.json (the 42 generic weapons below). Every real model has its maker, dimensions (length, barrel, empty mass), family
(how it reloads), timings (draw, reload, empty reload, per round, cycle, clear), muzzleSpeed, faultFactor and an archetype. EXPLOSIVES has 17
entries with lethal and injury radius, energy, noise and fuse.
The generic archetypes:
| Category | Ids |
|---|---|
| Firearms | pistol_9mm, pistol_45, pistol_22, revolver_357, revolver_38, smg_9mm, carbine_22, rifle_5_56, rifle_7_62x39, rifle_bolt_308, sniper_338, anti_materiel_50, shotgun_pump_12, shotgun_double_12, shotgun_semi_12, bow_compound, crossbow |
| Melee | hammer, sledgehammer, baseball_bat, crowbar, machete, axe, knife, fists |
| Fire and heat | lighter, flamethrower, cutting_torch, blowtorch, molotov |
| Explosives | frag_grenade, pipe_bomb |
| Tools | screwdriver, wrench, pry_bar, hacksaw, utility_knife, claw_hammer, extinguisher, hand |
DEFAULT_LOADOUT lists a starter kit of 19 ids. Pass calibers / weapons / explosives to createArsenal to add or override entries by id.
Ammo table: construction and penetration
AMMO (src/data/ammo.json, 50 loads) says, per calibre and kind of round (fmj, hp, ap, buck, slug, birdshot, rimfire, arrow, bolt,
incendiary, tracer; id <caliber>:<kind>), what the projectile is made of (core, jacket, tracer, incendiary), its mass and speed when they differ from
the calibre's (an armour-piercing core is lighter and faster) and how many millimetres of steel (RHA-equivalent), concrete, brick and
wood (dry hardwood) it goes through at close range and normal incidence. Every calibre ships with the load of its own kind. The numbers are
documented in the _notes of the file: one anchor is a manufacturer figure (7.62x51 M993 AP: 18 mm RHA at 100 m, Nammo) and the rest are estimates
from published orders of magnitude, muzzle energy and construction, each marked basis: 'estimated'; refine any entry with ArsenalOptions.ammo / patch.ammo.
arsenal.loadOf(caliber, kind?)returns{ def, kind, grams, speed, energy, penetration };weapon.setAmmoKind('ap')(orcreate(id, { ammoKind })) loads another kind, the nextShotcarriesammo,mass,energy,penetration,incendiaryandtracerof that load, andsnapshot()keeps it.- The shot's
penetrationis what a damage model reads:shotToHit(shot, point)/shotExtras(shot, distance)of@adatechnology/game-contractsscale it by the energy left at the distance and put it inHit.penetration, whichgame-destructioncompares with the thickness of the part (it reads no calibre names). - Add, change or remove loads without touching the code:
createArsenal({ ammo: [...], patch: { ammo: { '7_62x51:fmj': { penetration: { steel: 7 } } } }, remove: { ammo: [...] } }).checkArsenalreports a load of an unknown calibre, a missing penetration, or a calibre whose own kind has no entry.
| Load | kind | g | m/s | J | core / jacket | steel mm | concrete mm | brick mm | wood mm | basis | |---|---|---|---|---|---|---|---|---|---|---| | .22 LR lead round nose | rimfire | 2.6 | 350 | 159 | lead / none | 1.4 | 12 | 25 | 60 | estimated | | .380 ACP FMJ | fmj | 6.2 | 295 | 270 | lead / gilding_metal | 2.2 | 20 | 40 | 80 | estimated | | 9x18 Makarov FMJ | fmj | 6.1 | 315 | 303 | lead / steel | 2.6 | 22 | 45 | 85 | estimated | | 9x19 FMJ 124 gr | fmj | 7.5 | 365 | 500 | lead / gilding_metal | 3 | 25 | 50 | 80 | estimated | | 9x19 JHP | hp | 7.5 | 365 | 500 | lead / gilding_metal | 2 | 15 | 35 | 55 | estimated | | .40 S&W FMJ | fmj | 11.7 | 310 | 562 | lead / gilding_metal | 3 | 28 | 55 | 85 | estimated | | .45 ACP FMJ 230 gr | fmj | 14.9 | 254 | 481 | lead / gilding_metal | 2.8 | 28 | 55 | 85 | estimated | | .45 ACP JHP | hp | 14.9 | 254 | 481 | lead / gilding_metal | 1.8 | 18 | 40 | 60 | estimated | | 5.7x28 SS190 | fmj | 2 | 715 | 511 | lead_steel / gilding_metal | 4 | 25 | 50 | 100 | estimated | | .38 Special lead round nose | fmj | 10.2 | 247 | 311 | lead / none | 2 | 20 | 40 | 70 | estimated | | .357 Magnum JHP | hp | 10.2 | 396 | 800 | lead / gilding_metal | 3 | 25 | 45 | 90 | estimated | | .44 Magnum FMJ | fmj | 15.6 | 440 | 1510 | lead / gilding_metal | 4 | 40 | 70 | 150 | estimated | | .50 AE FMJ | fmj | 19.4 | 470 | 2143 | lead / gilding_metal | 4 | 38 | 65 | 130 | estimated | | 12 ga 00 buck (one pellet) | buck | 3.5 | 414 | 300 | lead / none | 0.8 | 10 | 25 | 60 | estimated | | 12 ga rifled slug | slug | 28 | 463 | 3001 | lead / none | 4 | 60 | 100 | 250 | estimated | | 12 ga birdshot (one pellet) | birdshot | 0.2 | 400 | 16 | lead / none | 0.2 | 3 | 8 | 20 | estimated | | 20 ga buck (one pellet) | buck | 1.3 | 400 | 104 | lead / none | 0.6 | 8 | 20 | 50 | estimated | | 5.56x45 M855 (steel tip, lead core) | fmj | 4 | 935 | 1748 | lead_steel / gilding_metal | 4.5 | 90 | 180 | 280 | estimated | | 5.45x39 7N6 (steel core) | fmj | 3.4 | 900 | 1377 | steel / steel | 5.5 | 90 | 180 | 280 | estimated | | 7.62x39 M43 (steel core) | fmj | 8 | 725 | 2102 | steel / steel | 6.5 | 110 | 220 | 300 | estimated | | .30-30 Winchester flat nose | fmj | 9.7 | 670 | 2177 | lead / gilding_metal | 3 | 80 | 160 | 250 | estimated | | 7.62x51 M80 ball | fmj | 9.7 | 850 | 3504 | lead / gilding_metal | 5 | 120 | 250 | 330 | estimated | | 7.62x54R LPS (steel core) | fmj | 9.6 | 865 | 3591 | steel / steel | 6.5 | 130 | 260 | 330 | estimated | | .30-06 M1 ball | fmj | 9.7 | 880 | 3756 | lead / gilding_metal | 5.5 | 130 | 260 | 340 | estimated | | .300 Win Mag FMJ | fmj | 11.7 | 900 | 4738 | lead / gilding_metal | 6.5 | 150 | 290 | 360 | estimated | | .338 Lapua Magnum FMJ | fmj | 16.2 | 903 | 6605 | lead / gilding_metal | 8 | 190 | 360 | 450 | estimated | | .408 CheyTac solid | fmj | 27.2 | 900 | 11016 | copper / none | 12 | 250 | 450 | 550 | estimated | | Arrow (steel broadhead) | arrow | 25 | 90 | 101 | steel / none | 0.5 | 5 | 15 | 100 | estimated | | Crossbow bolt | bolt | 20 | 122 | 149 | steel / none | 0.8 | 8 | 20 | 120 | estimated | | 4.6x30 DM11 | fmj | 2 | 735 | 540 | steel / gilding_metal | 3.5 | 25 | 50 | 100 | estimated | | .32 ACP FMJ | fmj | 4.6 | 276 | 175 | lead / gilding_metal | 1.8 | 15 | 30 | 60 | estimated | | 9x19 tracer | tracer | 7.5 | 365 | 500 | lead / gilding_metal | 2.8 | 22 | 45 | 75 | estimated | | 5.56x45 M995 AP (tungsten carbide) | ap | 3.6 | 1013 | 1847 | tungsten_carbide / gilding_metal | 10 | 110 | 210 | 300 | estimated | | 5.56x45 M856 tracer | tracer | 3.95 | 914 | 1650 | lead / gilding_metal | 3.5 | 80 | 160 | 260 | estimated | | 5.45x39 7T3M tracer | tracer | 3.4 | 900 | 1377 | lead / steel | 4 | 80 | 160 | 260 | estimated | | 7.62x39 BZ API | incendiary | 7.7 | 720 | 1996 | hardened_steel / steel | 8 | 110 | 220 | 300 | estimated | | 7.62x39 T-45 tracer | tracer | 8 | 725 | 2102 | lead / steel | 4.5 | 100 | 200 | 280 | estimated | | 7.62x51 M993 AP (tungsten carbide) | ap | 8.2 | 930 | 3546 | tungsten_carbide / gilding_metal | 18 | 160 | 300 | 360 | published | | 7.62x51 M62 tracer | tracer | 9.7 | 850 | 3504 | lead / gilding_metal | 4 | 110 | 230 | 320 | estimated | | 7.62x54R 7N37 AP | ap | 9.6 | 865 | 3591 | hardened_steel / steel | 11 | 140 | 270 | 340 | estimated | | 7.62x54R B-32 API | incendiary | 10 | 830 | 3445 | hardened_steel / steel | 10 | 130 | 260 | 330 | estimated | | 7.62x54R T-46 tracer | tracer | 9.6 | 865 | 3591 | lead / steel | 4.5 | 110 | 220 | 300 | estimated | | .30-06 M2 AP (hardened steel) | ap | 10.8 | 860 | 3994 | hardened_steel / gilding_metal | 10 | 140 | 270 | 340 | estimated | | .30-06 M25 tracer | tracer | 9.7 | 880 | 3756 | lead / gilding_metal | 4 | 110 | 230 | 320 | estimated | | .338 Lapua AP (tungsten) | ap | 16.2 | 903 | 6605 | tungsten_carbide / gilding_metal | 16 | 200 | 380 | 470 | estimated | | .50 BMG M33 ball | fmj | 42 | 925 | 17968 | lead_steel / gilding_metal | 14 | 330 | 560 | 600 | estimated | | .50 BMG M2 AP (hardened steel) | ap | 42 | 925 | 17968 | hardened_steel / gilding_metal | 22 | 350 | 580 | 600 | estimated | | .50 BMG M8 API | incendiary | 42 | 925 | 17968 | hardened_steel / gilding_metal | 18 | 340 | 570 | 600 | estimated | | .50 BMG M17 tracer | tracer | 42 | 925 | 17968 | lead / gilding_metal | 9 | 300 | 520 | 580 | estimated | | 12 ga incendiary shell (one pellet, magnesium) | incendiary | 3.5 | 414 | 300 | none / none | 0.1 | 3 | 8 | 15 | estimated |
Ignition: what a round does to a fuel tank
A normal bullet that goes through a fuel tank, a jerrycan or a gas bottle makes a leak and does not set it alight by itself. rules.ignition gives each round the
chances that it supplies a flame, built from its construction in the ammo table, and they travel in Shot.ignition and Hit.ignition ({ spark, flame }, plus the
flags incendiary and tracer); the damage model rolls them against the container (see "Ignition of a leak" in game-destruction).
- Spark (only against steel): the chance of the
coreand the chance of thejacketare independent,spark = 1 - (1 - core) x (1 - jacket). Defaults,rules.ignition.core: lead 0.005, lead_steel (steel tip or insert) 0.04, steel 0.12, hardened_steel 0.25, tungsten_carbide 0.25, copper 0.01, none 0;rules.ignition.jacket: gilding_metal 0.005, copper 0.005, brass 0.01, steel 0.08, none 0. So a lead round in a copper jacket (7.62x51 M80) gives about 1 percent, a steel core in a steel jacket (7.62x39 M43, 5.45x39 7N6) about 19 percent and an armour-piercing core (M993, M2 AP) about 25 percent: all of that before the fill of the tank weighs on it (a full tank keeps 12 percent of it). - Flame (any wall):
rules.ignition.flame.incendiary0.7 and.tracer0.2. An incendiary composition or a burning tracer base lights the vapour without a spark. ammoIgnition(ammoDef, rules.ignition)computes the pair for any load;createArsenal({ rules: { ignition: { core: { steel: 0.3 }, flame: { tracer: 0 } } } })changes any number.- No public per-cartridge table of the ignition of a fuel tank exists: the numbers are the kit's model (owner decision, 2026-10-24), with the order of magnitude of the fuel-tank vulnerability literature (ball ammunition rarely lights a tank, armour-piercing incendiary does), meant to be tuned per game.
Explosives, launchers and flamethrowers
A trigger pull on a launcher returns a throw use (the rocket: explosive, trigger: 'impact', armDistance, speed in velocity) and, when the launcher has one, a
backblast use (5 m, cone of 19°, pointing away from the target). A throwable returns a throw use (fuse from the explosive: M67 4 s, pipe bomb 3 s; fuse 0 means it
goes off on the first hit, like the molotov, which leaves fire of 4 m for 9 s) or, for the Claymore and C4, a place use that waits for wire or remote.
arsenal.explosiveBody(use) makes the flying (or waiting) body: step(dt, probe) applies gravity and bounces (rules.explosives), counts the fuse, arms by distance and
returns armed, impact, dud and detonate events; trigger('wire' | 'remote' | ...) sets off what waits. blastEffect(detonation, point) says if a point is in
the lethal or the injury zone (with the cone of a directional charge and the stun of a flashbang) and backblastEffect(use, point) the danger behind a tube.
The world explosives (propane_p13, fuel_barrel, car_fuel_tank, fuel_pump, fuel_station_tank) carry chain, the delays of the chain reaction game-destruction uses.
A world explosive that is a pressure vessel carries two more optional blocks, as data a host plays (the P13 cylinder has both, from the Laboratório de Explosões SC.p13):
leak: { fluid, liters, pressurized, igniteDelay, hissSeconds? } is what it holds (24 l of gas = 13 kg of liquid propane at 0.54 kg/l) and the seconds from the first leak
(a bullet through the shell, or the valve opened) to the jet catching fire as a torch (igniteDelay 0.8 s; the blast still comes from chain: shot 1.4 s). shell: { launchSpeed, trailSeconds }
is what is left of it after the blast: the torn shell flies up at 13 m/s trailing fire for 1.8 s. Change them like any other value: patch.explosives.propane_p13.leak.igniteDelay.
Cooking a grenade, safety fuses, the flashbang and trip wires
A throwable with a timer fuse is cooked (owner decision, 2026-10-24): the pull takes the pin out and the fuse starts to count (cook event, start); keep calling
trigger(..., held) each frame and, on the frame the trigger goes up, call release(origin, direction): it returns the throw with the fuse that is left
(use.fuse, and use.cooked for the seconds held; cook event throw). Holding past the fuse sets it off in the hand: the trigger (or the release) returns a throw use with
inHand: true, fuse: 0 and no velocity (cook event burst). Impact throwables (molotov, launchers) and the ones set down (C4, Claymore, a long-fuse dynamite) act on the pull, as before.
ExplosiveDef has four ways to say how long the fuse is: fuse (fixed seconds), fuseRange: [min, max] (sampled per throw; the flashbang is the real M84 delay of 1.0 to 2.3 s from the lever),
and fuseLength (metres) with burnRate (s/m) and burnTolerance (a fraction): the dynamite is 0.03 m of safety fuse at 131 s/m ±10%, about 4 s. The sampling uses a stream of its own
(seed ^ 0xbb67ae85), so the spread of the shots does not move. weapon.setFuseLength(metres) (the test area's fuse_length control) or
patch.explosives.dynamite.fuseLength makes a longer fuse; when the fuse reaches rules.explosives.placeFuseFrom (10 s) the explosive is set down lit (place use with the timer trigger)
instead of thrown, which builds a delayed trap. nominalFuse(explosive) and sampleFuse(explosive, random) are exported.
An explosive with the wire trigger (the Claymore) carries tripwire: { length, angleDeg?, height? } (default rules.explosives.tripwire: 10 m ahead, 0.15 m above the charge, derived: no source
gives the length of the M18A1 kit). The place use has it, and tripwireSegment(use) gives the segment { from, to } the host stretches and tests bodies against; when a body crosses it the host calls body.trigger('wire').
Accessories, stocks and aiming
arsenal.attachments is the catalogue of accessories (data/attachments.json, generated from the Arsenal §11 lines and decisions.json: suppressors, brakes, optics, grips, bipod, light, laser,
magazines, barrels, sling, ammunition backpack, tripod, grenade launchers). A weapon lists its places (mounts: muzzle, rails, underbarrel, magazine, barrel, sling, feed, each with the
interfaces it takes: picatinny, ak_side, thread_14x1lh...), what it ships with (fitted, already in the numbers of action; integral: true cannot come off) and its stock (stock:
fixed, fold, retract, fold_retract, none, and the foldAxis).
weapon.canFit('suppressor') // { ok: true, mount: 'muzzle' } | { ok: false, reason: 'no_mount' | 'interface' | 'caliber' | 'category' | 'occupied' | 'blocked' | 'rejected' }
weapon.fit('suppressor', { install: 'tool' }) // phase `fit` (rules.attachments.fitSeconds: field 2 s, tool 15 s, bench 30 s); counts when the phase ends; `trigger` answers `busy` meanwhile
weapon.remove('vertical_grip') // a factory part that is `integral` stays; what no longer fits in a smaller magazine goes back to the reserve
weapon.toggle('weapon_light') // lights and lasers count only while on (event `toggle`)
weapon.setStock('collapsed') // phase `stock` (rules.stock.seconds); then rules.stock.collapsed applies (draw ×0.8, aim ×0.85, vertical recoil ×1.3, spread ×1.15)
weapon.setStance({ aiming: true, supported: false })
weapon.aimSeconds // rules.aim.base + rules.aim.perKg × mass, with the effects (red dot ×0.8...)
const snapshot = weapon.snapshot(); arsenal.restore(snapshot, { seed })Effects are StatModifiers (stat, then set, add, mul, with a condition: always, aiming, hip, supported, subsonic, collapsed): every set first, then the sum of the adds,
then the product of the muls. A factory part is already in the numbers, so taking it off applies its inverse and putting it back returns the numbers of the def exactly. A bullet slower than
rules.attachments.speedOfSound (343 m/s) is subsonic, which a suppressor mutes more (×0.25 in all). A suppressed pull sets TriggerResult.suppressed (and on the shot event): pass it to the shot sound.
A home-made suppressor breaks at a shot drawn in durability.shots from a stream of its own (seed ^ 0x3c6ef372), so spreads and faults do not move (event attachment, action: 'break').
ArsenalOptions gains attachments, patch.attachments, remove.attachments and isCompatible(def, attachment, fitted) (a game vetoes an accessory); canFit(def, attachment, fitted, context, mount?) is exported.
A launcher under the barrel. m203 (M16, M4), gp_25 (AK-47, AKM, AK-74 and AK-12 only: the SKS has a bayonet lug but not the GP-25 mount) and ag36 (HK416) are accessories whose launcher names a weapon of the
catalogue (category: 'launcher', tags: ['underbarrel'], family underbarrel: breech open, grenade in, breech close). Fitting one gives the weapon a second state, weapon.underbarrel, with its own
ammunition (the M79 launcher rule: one grenade, launcherUses, explosive m79_40mm, or vog_25 for the GP-25, which has the same radii). useUnderbarrel(true) takes rules.attachments.switchSeconds (0.8 s,
phase switch, event underbarrel); while it is on, trigger and reload go to the launcher and drain() carries its events too. The launcher covers the lower rail (blocks: ['rail_bottom']).
snapshot() carries the launcher's snapshot. A shipped launcher accessory leaves the arsenal when its launcher weapon is removed or filtered out.
Weapons beyond the team pages. 21 more firearms come from decisions.creations of the catalogue tool, each number with its source and reason (a reference always with a URL): hk_mp5sd3, fn_parafal,
taurus_t4_civil, taurus_th380, mossberg_500_cruiser, hk_mp5k, hk_ump9, mini_uzi, hk_mp7a1, hk_g36 (k, c), fn_m249, fn_minimi_para, fn_mag, rpk, sig_mpx, mg3, pp19_bizon, vz61
and m134, and the calibres 4_6x30 and 32acp. The MP5SD3 ships with an integral suppressor (its noise and speed are the suppressed ones; the subsonic factor is already in its base numbers) and the G36 and
G36K with an integral 3x optic; neither comes off. The M134 is a rotary gun with three modes: hand-held with a 200 round belt box, with the ammunition backpack (500, see above) or on the tripod mount
(supported). action.rpmRange ([2000, 6000]) bounds the rate a game may set with patch.weapons.m134.action.rpm (3000 by default) and action.spin ({ upSeconds: 0.5, holdSeconds: 1 }, derived) delays the
first round of a pull until the barrels are at speed and keeps them spinning for a second after the last round (hold the trigger with held).
The hip: rules.aim.hipSpread times the spread when not aiming. The SDK default is 1 (neutral); the Arsenal says ×6 (§08) and ZombiAdA turns it on with rules: { aim: { hipSpread: 6 } }. A laser takes
half of the excess (hipSpreadExcess ×0.5). The ammunition backpack (feed mount, belt) sets the capacity to 500 rounds by default (patch.attachments.ammo_backpack); a tripod counts as support.
In the weapons test area (@adatechnology/game-weapons-three): L, K, V and U work the light, the laser, the stock and the launcher; the right mouse button aims with a firearm in hand (the camera zooms by the
optic that is fitted over aimSeconds), and only when the click lands on the room's canvas or the pointer is locked: a click on a panel, an input or an overlay is never taken, and a tool that does not fire (the hand,
a throwable) leaves the button to the room, which grabs with it. The bench's Supported switch (supported_stance) puts the weapon in the supported stance (deployed or prone), so the bipod effect can be exercised;
it goes back off when another tool is taken. The laser measures its distance at most 30 times a second and ignores hidden nodes (a hidden ancestor counts) and the weapon in the hands.
API
createArsenal(options?)returns anArsenal:calibers,weapons,explosives(maps),rules,create(id, { seed?, reserve?, condition?, ammoQuality? })(unknown id throws),attachments,canFit(weapon, attachment, options?),restore(snapshot, options?),ammo(map),loadOf(caliberId, kind?),muzzleEnergy(caliberId, barrel?),energy(weaponId),name(id)(weapon, explosive, fire mode, category, fault, condition or calibre, translated),list(category?). Options:calibers,weapons,explosives,patch,remove,include,rules,strings,seed,locale.WeaponState:def,caliber,ammo,capacity,reserve,mode,reloading,locked,phase,fault,clearing,safety,heat,heatLevel,condition,ammoQuality,spread,trigger(origin, direction, dt, held?, source?),release(origin?, direction?),draw(),reload(kind?),cycleMode(),setSafety(on),clearFault(),setCondition(id),setAmmoQuality(id),setAmmoKind(kind),ammoKind,setFuseLength(metres),cooking,attachments,mass,aimSeconds,suppressed,stock,canFit,fit,remove,toggle,setStock,setStance,snapshot,update(dt),refill(),drain().release(origin?, direction?)returns the throw of a cooked grenade (null for anything else).Shot:origin,direction,energy(J),caliber(mm),caliberId,speed,mass,energyAt(distance)(energy × speed factor², drag per 100 m),pellet,weapon,source,ammo,penetration,incendiary,tracer.Use:melee,flame,heat,throw,tool,extinguish,hand.TriggerResult:shots,uses,status(ok,empty,reloading,cooldown,no_fuel,safe,drawing,fault,misfire,clearing,busy),recoil,noise,rounds,roundTimes,fault?,suppressed?.muzzleEnergy(caliber, barrel?),AMMO,ammoId,resolveLoad,CALIBERS,WEAPONS,EXPLOSIVES,ARCHETYPE_IDS,DEFAULT_LOADOUT,DEFAULT_RULES,RELOAD_STAGES,checkArsenal,locales,resolveStrings.
Locales: en and pt-BR (locale option; unknown falls back to en). Calibre labels are technical names and are not translated.
Português
Armas e munição independentes de engine: 28 calibres reais como dados (energia de boca = 0,5 × m × v²), 79 armas reais com marca e medidas, 17 explosivos,
42 arquétipos genéricos e um runtime determinístico (carregador, modos de tiro, saque, ciclo manual, recarga por tipo com suas etapas, panes e desobstruir,
trava, calor do cano, dispersão, chumbos de espingarda, recuo, ruído), além de corpo a corpo, chama, calor, arremessáveis, lançadores, ferramentas e extintor.
weapon.drain() entrega os eventos (saque, tiro, pane, recarga...) para animação e som; rules muda qualquer número (faults: false desliga as panes). Cada disparo vira um Shot (energia em joules, calibre em mm, direção) que se passa direto para
world.shoot(origem, direção, shot.energy, shot.caliber) do @adatechnology/game-destruction. Use createArsenal({ locale: 'pt-BR' }) para os nomes
em português (arsenal.name('rifle_5_56') devolve "fuzil 5,56"). A recarga nunca é automática; o jogo chama reload().
