ZOA Armory
Configure firing behavior and track each weapon’s operational state.
Armory runs a weapon’s firing and reload cycle. WeaponOperationProfileDefinition references fire modes, delivery, ballistics, recoil, and reload profiles, and adds heat and jam settings. Reuse those assets across weapons that share behavior.
Open Weapons & Armory → Armory → Create Weapon for the guided wizard, or Weapons to edit an existing definition. Operation Profile Composer controls shared firing and reload settings. During play, ArmoryWeaponRuntime tracks the ammunition and operational state of each equipped weapon.
Tooling
Editor tools
Create Weapon
Workbench → Weapons & Armory → Armory → Create Weapon
Create a weapon through eight guided steps, from its definition and model through mount points, attachments, firing settings, and presentation. Check the preview and summary before creating the assets.
Weapons definition browser
Workbench → Weapons & Armory → Armory → Weapons
Browse existing weapon definitions, edit their properties, and validate the result. You can also duplicate an asset to start a variant.
Operation Profile Composer
ZOA Workbench > Armory > Operation Profile Composer
Assign fire modes, delivery, ballistics, recoil, and reload profiles to an operation profile, then configure its heat and jam settings.
Weapon Model Bundles
ZOA Workbench > Armory > Weapon Model Bundles
Browse the operational profile bundles available as package samples. Inspect and import a bundle to use its fire modes, ballistics, recoil, and reload definitions as a starting point.
Auto-Import Bundled Weapons
Tools > ZOA > Advanced > Generate > Weapons > Content Packs > Auto-Import Bundled Weapons
Runs the Weapon Modding importer over bundled FBX models to create weapon definitions and rigged prefabs. Inspect the generated assets and their references before using them in a scene.
Armory scene installer
Adds ArmoryRuntime to the active scene. The installer checks for an existing component before creating one.
From the development project
Screenshots


Declared ZOA dependencies (10)
Referenced by ZOA manifests (5)
How it works
Concepts
Configure firing cadence and shot delivery
FireModeDefinition controls the firing pattern: Single, Burst, Auto, Continuous, Charge, or Scatter. Its settings include burst count, projectiles per shot, minimum shot interval, spread, and charge time. FireDeliveryDefinition selects Hitscan, Projectile, or Beam and configures how the shot travels and which layers it can hit.
A weapon can support several fire modes and a default selection. The operation profile has one delivery reference shared by those modes.
For a pellet-based weapon, inspect both the fire mode’s projectiles-per-shot setting and the operation profile’s PelletPatternDefinition. The pattern asset provides cone and per-pellet falloff settings.
The runtime is a state machine you tick
ArmoryWeaponRuntime holds an ArmoryWeaponState struct: magazine rounds, chambered flag, jammed, reloading, unjamming, heat, selected fire mode index, charging, charge percent, and overheated. Everything the runtime does is a transition on that struct plus an event.
TryConsumeShot first checks whether the weapon can fire, including its jam, reload, heat, chamber, and delivery state. A refusal returns a reason suitable for the interface. After that check, the runtime can still roll a jam: the attempt changes the weapon’s state and returns false without consuming a successful shot.
TickCooling dissipates heat and clears overheat lockout, TickReload advances the reload timer, and TickCharge accumulates charge. The owning controller must call the relevant tick methods to advance those states.
The runtime takes an optional seed, and with one supplied the jam rolls are reproducible. It also carries a restoration constructor that adopts a previously captured state snapshot, and ApplyStateSnapshot for save-load and network reconciliation, where ammo, heat, jam, reload, charge, and fire mode have to be restored as one coherent value rather than replayed as partial events.
Heat degrades the weapon before it locks it
Heat settings control the delay before cooling, the threshold for clearing lockout, and changes to firing rate, spread, recoil, and jam chance as heat rises.
Cooling does not start the instant you stop shooting: CoolingDelayAfterShot holds it off briefly so tapping the trigger does not reset the bar. Lockout does not clear at zero either; CooldownThresholdPercent is where the weapon becomes usable again, which by default is halfway down. Between those, HeatFireRatePenaltyStartPercent marks where thermal slowdown begins, ramping toward OverheatedFireRateMultiplier at maximum heat, so a hot weapon fires visibly slower before it stops entirely.
Three more multipliers blend in with heat rather than switching at a threshold: spread widens, recoil grows, and jam chance rises, each toward its authored value at maximum. The runtime exposes them as GetHeatSpreadMultiplier, GetHeatRecoilMultiplier, and GetHeatAdjustedJamChance so the firing path can fold them into the shot it is about to take.
Reload is a guarded state machine with an ammo economy
StartReload refuses to restart an in-progress reload. Without that guard a held or mashed reload key zeroes the elapsed counter every frame, so TickReload never reaches the duration and the weapon is locked in a perpetual reload while re-firing the magazine-eject sound each frame.
It also honours the tactical-reload flag. When the profile forbids tactical reloads, as a bolt-action or shell-fed weapon would, topping off a partial magazine is refused outright and the reload only starts once the magazine is empty. Whether the reload also has to chamber a round is captured at start rather than recomputed, so the duration stays stable for the whole cycle.
FinishReload is reserve-aware but only when you opt in. With no IAmmoReserveSource attached, a reload fills the magazine for free, which is the historical behaviour and keeps existing content working. Attach a source and the runtime computes how many rounds the refill actually needed, draws that many from reserve, and trims the magazine, then the chamber, by whatever the reserve could not fund.
Recoil follows a seeded pattern
RecoilHandler accumulates vertical and horizontal offsets per shot and recovers them over time. Consecutive shots increase the kick within a firing string. A short memory window determines when that string resets.
The lateral component blends a deterministic pattern against per-shot jitter. PatternPeriodShots sets the length of one pattern cycle and PatternRandomness blends between following it exactly and rolling freely, with HorizontalBias pulling the whole thing one way for an asymmetric weapon. The jitter runs on a private seeded System.Random rather than the global Unity RNG, because the global stream is shared with every other system and basing recoil on it made replays and networked clients diverge. SeedPattern re-seeds it for per-weapon or per-client synchronisation.
Recovery is two-stage. RecoveryDelay holds the peak briefly so the kick is felt before it starts unwinding, then linear recovery and an exponential settle term run together, with maximum offsets clamping how far the climb can accumulate before recovery wins.
Ballistics model velocity, energy, and falloff
A BallisticProfileDefinition can be as simple as muzzle velocity, effective range, and impact damage. Past that it models the parts of exterior ballistics that change how a weapon reads: a ballistic coefficient and drag coefficient for velocity retention, projectile mass in grams for real kinetic energy, a damage falloff exponent and a minimum damage fraction so a round has a floor rather than decaying to nothing, a transonic instability velocity with a spread penalty for rounds that go unstable as they slow, and a wind drift scale.
BallisticsCalculator implements the maths as static functions, and its overloads are layered so the simple call still works. The three-argument damage-at-range call preserves the original inverse-distance curve; the five-argument one adds the floor and exponent; the profile-taking one uses everything authored. Alongside those sit muzzle and at-range energy, velocity retention, drag acceleration, wind drift, penetration energy cost, trajectory prediction, flight time, and drop.
Penetration composes with the Combat package rather than duplicating it. The profile's armour penetration is the power a round starts with, PenetrationProperties on each target declares what passing through it costs, and the delivery layer runs one deduct-and-continue loop over the two.
Pellets are distributed, not rolled
PelletPatternDefinition attaches to an operation profile and turns one trigger pull into a set of rays. The cone half-angle at impact is a base spread plus a growth per metre, clamped at a maximum, which is what separates a tight choke that stays lethal at medium range from a scattergun that is a close-quarters weapon by construction.
Damage falls off per pellet on its own curve: full damage inside one distance, a linear ramp to a minimum fraction at another, and a floor beyond that. For hitscan this is pure attenuation; for real projectiles the same curve doubles as the velocity-decay surface.
PelletConeSampler decides where each pellet actually goes, using a sunflower spiral, a golden-angle spiral mapped onto the disk by square-root radius, plus a small per-shot jitter. Independent random rolls leave visible empty arcs in roughly one shot in three at typical pellet counts, and a stratified grid reads as machine-perfect; the spiral covers the cone evenly without looking regular.
Attachment stats reach the runtime through the stats kernel
ArmoryWeaponRuntime implements IStatModifierSource. It builds a WeaponStatCalculator over the weapon's installed attachments, converts what comes back into StatModifier values, and evaluates them against a StatBlock seeded from the operation profile's authored numbers.
That evaluation produces a set of Effective properties and GetEffective methods that the firing path reads instead of the raw profile: EffectiveMaxHeat, EffectiveHeatPerShot, EffectiveCoolingPerSecond, EffectiveJamChancePerShot, and GetEffectiveMuzzleVelocity, GetEffectiveRange, GetEffectiveImpactDamage, GetEffectiveSpreadAngle, GetEffectiveReloadDuration, GetEffectiveMagazineCapacity, and so on.
Nothing in Armory knows what an attachment is beyond that. Weapon Modding decides that a compensator emits a recoil multiplier of 0.85, the stats kernel decides how modifiers fold, and Armory only asks what the number is now. An attachment authored for a rifle therefore needs no Armory-side registration.
Services register at scene boot, not by reference
ArmoryRuntime is a single scene component that registers five services into the Foundry service registry during Awake: the equip service, the pack registry, the attachment swap service, the loadout service, and the presentation event stream. It runs at execution order -9500 so consumers resolving in Start find them ready.
Registration is idempotent and reversible. Each Ensure step reuses a pre-registered instance rather than stomping it, which keeps an additive scene load or a third-party override intact, and OnDestroy unregisters only the instances this host actually created.
That indirection exists for a dependency rule: Nucleon's player controller must be able to drive weapons, and Armory must not reference Nucleon. Resolving IWeaponEquipService from the registry satisfies both. The same pattern lets an attachment pickup in another package swap a scope without importing Armory internals.
Firing publishes on two channels
A successful shot raises the local OnFired event and calls every registered IArmoryEventListener, which is how the HUD attached to this weapon updates. It then resolves IFoundryEventBus and publishes a WeaponFiredEvent carrying the weapon's instance and definition ids, which is how audio, analytics, AI, and networking react without importing an Armory type.
The bus resolution is resolve-and-forget: Armory must not hard-require Messaging host setup in order to fire, so a missing bus is silently fine. WeaponFiredEvent carries no origin or direction, because the runtime holds no world position for the shot. Delivery handlers own that and publish ProjectileHitEvent with the resolved spatial context.
A third stream sits alongside for presentation. IWeaponPresentationEventStream is the production source for muzzle VFX, shot audio, and camera impulse; the delivery handlers publish to it and handlers in other packages subscribe. WeaponShotEventBus and DamageFlowBus are diagnostics only, feeding the debug HUDs, and a bridge mirrors a subset across so the debug surfaces keep working.
Setup
Workflow
- 01
Install the runtime into the scene
Add an ArmoryRuntime component to a scene root, or run the Armory scene installer, which is idempotent and finds an existing one before creating another. Awake registers the equip service, pack registry, attachment swap service, loadout service, and presentation stream. Nothing else in the package resolves until that has happened.
- 02
Author the sub-profiles, then the aggregate
Create the fire modes a weapon supports, one delivery, one ballistic profile, one recoil profile, and one reload profile, then a WeaponOperationProfileDefinition that references them and fills in the jam and heat model. The Operation Profile Composer in the Workbench does this composition step in a single panel.
- 03
Bind the profile to a weapon
Create a WeaponArmoryBindingDefinition pointing at the weapon and the profile, or at a platform when a whole family should share it. ArmoryProfileResolver matches the exact weapon first and falls back to the platform, so a family-level binding plus one weapon-level override is a supported shape.
- 04
Equip through the service
Build a WeaponEquipRequest with the definition, the profile, and a host that exposes a weapon mount and an aim camera, then call TryEquip. The service spawns the model, scans it for mount sockets, applies the preset's attachments, constructs the runtime, and hands back a handle. Supply a random seed when determinism matters.
- 05
Drive the runtime from your controller
Tick cooling, reload, and charge every frame, call TryConsumeShot on trigger input, and route a success into the delivery handler that matches the profile's delivery kind. Read the effective stats rather than the raw profile numbers so attachments are accounted for. Subscribe to the runtime's events for HUD, audio, and animation.
- 06
Add stowage when the player carries more than one
Register the host with the loadout service and an IWeaponStowage backend, then use TryPickup, TrySwitchActive, and TryStowActive instead of calling the equip service directly. Each transition snapshots the outgoing weapon onto its item payload and restores the incoming one, so magazine, heat, and attachments round-trip exactly.
- 07
Ship content as packs
Group weapons into a WeaponPackDefinition with a dotted slug id and register it, either from a catalog pass or by serialising it onto the ArmoryRuntime component so a scene ships with its pack pre-registered. The package's own sample bundles, thirty-odd weapon packs from ancient arms to space opera, install through the Package Manager samples list.
Reference
Key types
ArmoryWeaponRuntime
class
One per equipped weapon. The state machine for fire mode selection, ammunition, heat, jams, charge, and reload, deterministic when constructed with a seed and restorable from a state snapshot.
- ArmoryWeaponRuntime(WeaponInstance weaponInstance, WeaponOperationProfileDefinition operationProfile, int? seed = null, GameDatabase database = null)
- bool CanFire(); bool TryConsumeShot(out string reason)
- void TickCooling(float dt); void TickReload(float dt); void TickCharge(float dt)
- void StartReload(); void FinishReload(); bool CancelReload()
- void StartUnjam(); void FinishUnjam(); void ClearJam()
- void StartCharge(); float ReleaseCharge(); void CancelCharge()
- bool TryCycleFireMode(int direction); bool TrySetFireMode(int index); FireModeDefinition SelectedFireMode
- float HeatPercent, EffectiveMaxHeat, EffectiveHeatPerShot, EffectiveJamChancePerShot
- float GetHeatFireRateMultiplier(), GetHeatSpreadMultiplier(), GetHeatRecoilMultiplier()
- float GetEffectiveImpactDamage(float), GetEffectiveSpreadAngle(float), GetEffectiveReloadDuration(float)
- void SetReserveSource(IAmmoReserveSource source)
- void ApplyStateSnapshot(in ArmoryWeaponState snapshot, bool publishEvents = true)
- void AddListener(IArmoryEventListener listener); void RemoveListener(...)
- events OnFired, OnJammed, OnAmmoChanged, OnReloadStart, OnReloadComplete, OnOverheat, OnCooledDown, OnHeatChanged, OnChargeStart/Complete/Fire/Cancel, OnFireModeSwitched
ArmoryWeaponState
struct
The serializable snapshot the runtime mutates: rounds, chamber, jam, reload, heat, fire mode, charge, overheat, unjam. Save and network layers move this struct whole.
- int magazineRounds; bool chamberLoaded, jammed, reloading, unjamming, overheated
- float heat; int selectedFireModeIndex; bool charging; float chargePercent
IArmoryEventListener
interface
Listener form of the runtime's events, with default empty implementations on every method so an implementer overrides only what it cares about. Registered on the runtime alongside, not instead of, the C# events.
- void OnFired(FiredEventData data); void OnJammed(); void OnHit(HitEventData data)
- void OnAmmoChanged(...); void OnReloadStart(...); void OnReloadComplete(...); void OnChamberLoaded()
- void OnOverheat(...); void OnCooledDown(...); void OnHeatChanged(...)
WeaponFiredEvent
struct
The cross-package fire event on the Foundry bus. Unlike the local FiredEventData it carries the weapon's instance and definition ids, so a subscriber in another package can correlate without holding the runtime.
- InstanceId WeaponInstanceId; DefinitionId WeaponDefinitionId
- int FireModeIndex, MagazineRoundsRemaining; bool ChamberLoaded; float HeatAfterShot
ProjectileHitEvent
struct
Published by the delivery handlers when a shot connects. Positions are float triples rather than Vector3 so the struct lives in the engine-free Core assembly.
- InstanceId WeaponInstanceId; DefinitionId WeaponDefinitionId
- HitPointX/Y/Z, HitNormalX/Y/Z; float Distance, Damage, Force; bool Pierced
IWeaponEquipService
service
The single call site that turns a definition, a profile, and a host into a live weapon: spawns the model under the host's mount, scans it for sockets, builds the weapon instance, applies the preset, constructs the runtime, and returns a handle.
- bool TryEquip(WeaponEquipRequest request, out EquippedWeaponHandle handle, out string reason)
- bool Unequip(EquippedWeaponHandle handle)
- event Action<EquippedWeaponHandle> WeaponEquipped, WeaponUnequipped
WeaponEquipRequest
class
Immutable equip request. Model prefab and existing model are mutually exclusive; supplying an existing ItemInstance makes a draw restore that weapon's exact state instead of minting a fresh one.
- WeaponDefinition Definition; WeaponOperationProfileDefinition OperationProfile; IWeaponEquipHost Host
- GameObject ModelPrefab | GameObject ExistingModel
- WeaponPresetDefinition PresetOverride; WeaponPresentationDefinition PresentationOverride
- int? RandomSeed; ItemInstance ExistingItemInstance
- WeaponPresetDefinition EffectivePreset; WeaponPresentationDefinition EffectivePresentation
EquippedWeaponHandle
class
A plain DTO bundling everything needed to drive the equipped weapon: the runtime, the weapon instance and its attachment graph, the spawned model, the cached socket scan, and the effective presentation. Nothing ticks from the handle itself.
- IWeaponEquipHost Host; WeaponDefinition Definition; WeaponOperationProfileDefinition OperationProfile
- WeaponInstance Instance; ArmoryWeaponRuntime Runtime
- GameObject Model; MountPointScanner.ScanResult Sockets
- WeaponPresentationDefinition Presentation; bool IsUnequipped
IWeaponEquipHost
interface
A minimal host contract, declared here instead of imported from Nucleon so the dependency arrow never reverses. Two members: where to parent the model, and what defines the aim line.
- Transform WeaponMount
- Camera AimCamera
IWeaponLoadoutService
service
Owns what a host carries. Sits above the equip service and a stowage backend, answers what the host owns and which weapon is live, and provides the three mutating verbs. Every verb raises LoadoutChanged, including on refusal, so UI can simply re-read.
- void RegisterHost(IWeaponEquipHost host, IWeaponStowage stowage); void UnregisterHost(...)
- EquippedWeaponHandle GetActive(IWeaponEquipHost host)
- IReadOnlyList<OwnedWeaponView> GetOwnedWeapons(IWeaponEquipHost host)
- bool TryPickup(IWeaponEquipHost host, WeaponEquipRequest request, out EquippedWeaponHandle handle, out string reason)
- bool TrySwitchActive(IWeaponEquipHost host, InstanceId targetItemId, out string reason)
- bool TryStowActive(IWeaponEquipHost host, out string reason)
- event Action<WeaponLoadoutChangedArgs> LoadoutChanged
IWeaponStowage
interface
Where stowed weapons live. The production adapter persists them in the player's inventory; tests and headless AI supply an in-memory one. Implementations must preserve the state payload reference and must refuse rather than clobber when capacity is exhausted.
- bool TryStow(ItemInstance weaponItem, out string reason)
- bool TryWithdraw(InstanceId weaponInstanceId, out ItemInstance weaponItem, out string reason)
- bool TryGetItem(InstanceId weaponInstanceId, out ItemInstance weaponItem)
- IEnumerable<ItemInstance> EnumerateWeapons(); event Action Changed
WeaponInstanceState
class
The item state payload a stowed weapon carries so it remembers its magazine, heat, jam and reload flags, fire mode, pinned operation profile, and installed attachments across a stow and draw cycle.
- const string TypeId = "zoa.armory.weapon.v1"; int PayloadVersion
- ArmoryWeaponState State; DefinitionId OperationProfileId
- List<InstalledAttachmentRecord> InstalledAttachments
IAttachmentSwapService
service
Installs, swaps, and removes attachments on a live weapon: resolves the socket from the handle's cached scan, evicts whatever occupies it, instantiates the visual under the socket, and updates the attachment graph so stat modifiers follow.
- bool TrySwapAttachment(AttachmentSwapRequest request, out AttachmentSwapResult result, out string reason)
- bool TryRemoveAttachment(EquippedWeaponHandle weapon, string socketName, out AttachmentRemovalResult result, out string reason)
- event Action<AttachmentSwapResult> AttachmentInstalled; event Action<AttachmentRemovalResult> AttachmentRemoved
IWeaponPackRegistry
service
Runtime lookup for weapon packs by their dotted slug id, so a package can ship a pack and self-register while consumers resolve by id rather than by asset reference or Resources.Load. Registration is idempotent by id.
- bool Register(WeaponPackDefinition pack); bool Unregister(string packId)
- event WeaponPackRegisteredEventHandler PackRegistered; event WeaponPackUnregisteredEventHandler PackUnregistered
ArmoryRuntime
component
The scene-root bootstrap. Registers the five services at execution order -9500, reuses anything already registered, unregisters only what it created, and can pre-register weapon packs serialised onto the component.
- void EnsureRegistered(); void HandleDestroy()
- IWeaponEquipService EquipService; IWeaponPackRegistry PackRegistry
- IAttachmentSwapService SwapService; IWeaponLoadoutService LoadoutService
- IWeaponPresentationEventStream PresentationStream
ArmoryProfileResolver
class
Finds the operation profile for a weapon by scanning binding assets in the game database, matching the weapon definition exactly first and falling back to its platform. Bindings are cached per database instance.
- static WeaponOperationProfileDefinition Resolve(GameDatabase database, WeaponDefinition weaponDefinition)
BallisticsCalculator
class
Static exterior-ballistics maths. Overloads layer so the simple three-argument call keeps the original curve while the profile-taking form uses every authored knob.
- static float CalculateDamageAtRange(float baseDamage, float distance, BallisticProfileDefinition profile, float fallbackEffectiveRange = 0f)
- static float ApplyArmorPenetration(float baseDamage, float armorRating, float armorPenetration)
- static float CalculateMuzzleEnergy(...); static float CalculateEnergyAtRange(...)
- static float CalculateVelocityAtRange(...); static float CalculateDragAcceleration(...)
- static Vector3 CalculateWindDrift(...); static float CalculatePenetrationEnergyCost(...)
- static Vector3 PredictPosition(...); static float EstimateFlightTime(...); static float CalculateDrop(...)
HitscanDeliveryHandler
class
Instant ray delivery with optional piercing. The options overload adds a pre-fire line-of-sight gate, so an AI shooting at a target behind partial cover produces zero hits instead of accidentally shooting the wall, plus near-miss publishing for reaction systems.
- static void Fire(Vector3 origin, Vector3 direction, FireDeliveryDefinition delivery, BallisticProfileDefinition ballistics, List<HitEventData> hits, InstanceId weaponInstanceId = default, DefinitionId weaponDefinitionId = default, GameObject owner = null)
- static void Fire(..., HitscanFireOptions options, ...)
- HitscanFireOptions — IntendedTarget, RequireLosToTarget, LosBlockingMask, NearMissRadius, NearMissLayerMask, ImpactClassId
ProjectileDeliveryHandler
class
Spawns physical projectiles at a muzzle with velocity and gravity, distributing them through a spread cone for scatter fire and stamping weapon identity and impact class onto each so hits correlate downstream.
- static List<GameObject> Fire(Vector3 origin, Vector3 direction, FireDeliveryDefinition delivery, BallisticProfileDefinition ballistics, int projectilesPerShot = 1, float spreadAngle = 0f, Action<HitEventData> onHit = null, InstanceId weaponInstanceId = default, DefinitionId weaponDefinitionId = default, string impactClassId = null, GameObject owner = null)
BeamDeliveryHandler
class
Sustained beam delivery by continuous raycast over a duration, dealing damage per second and driving an optional LineRenderer. Tracks the last collider touched so a beam held on one wall spawns a single decal rather than one per tick.
- void Start(FireDeliveryDefinition delivery, BallisticProfileDefinition ballistics); void Stop()
- HitEventData? Tick(Vector3 origin, Vector3 direction, float deltaTime, LineRenderer lineRenderer = null)
- bool IsActive; float Elapsed, Remaining
- Action<HitEventData> OnHit; string ImpactClassId; GameObject Owner
RecoilHandler
class
One per weapon. Accumulates kick, grows it across a firing string, blends a deterministic lateral pattern against seeded jitter, and recovers through a delay plus linear and exponential terms.
- void Configure(RecoilProfileDefinition profile, float recoilReduction = 0f)
- void ApplyRecoilKick(float impulseMultiplier = 1f); void Tick(float deltaTime)
- Vector2 GetCurrentOffset(); Vector3 GetCurrentImpulseOffset(); void ApplyToTransform(Transform target)
- void SeedPattern(int seed); void Reset()
- float VerticalOffset, HorizontalOffset, CameraImpulse, PushbackOffset
PelletConeSampler
class
Places each pellet inside a scatter cone using a golden-angle sunflower spiral plus per-shot jitter, which covers the cone evenly without the empty arcs of independent random rolls or the regularity of a grid.
- static Vector3 Sample(Vector3 centerDirection, int pelletIndex, int pelletCount, float coneHalfAngleDegrees, bool addJitter = true)
MaterialPenetrationHandler
class
Evaluates whether a round punches through a surface or glances off it, using material density against projectile energy for penetration and impact angle against surface hardness for ricochet.
- static PenetrationResult Evaluate(...)
- PenetrationResult — penetrated, ricocheted, exitPoint, exitDirection, remainingEnergyFactor, hitData
IAmmoReserveSource
interface
Opt-in reserve pool for reloads. With no source attached the runtime reloads for free; with one, FinishReload draws only what reserve can fund and trims the magazine accordingly.
- int Available
- int TryTake(int requested)
IWeaponPresentationEventStream
service
The production fan-out for presentation. Armory publishes a typed event the moment a fire, reload, jam, or impact decision is made; VFX, audio, camera, and decal handlers in other packages subscribe. Publishing never throws into the firing path.
- void Publish(WeaponPresentationEvent evt)
- event Action<WeaponPresentationEvent> Published
WeaponShotEventBus
class
Static diagnostics hub with a rolling 32-shot history, read by the debug HUDs and scenario tests. Distinct from the presentation stream: this one exists to be looked at, not to drive gameplay feedback.
- const int HistoryCapacity = 32
- static event Action<WeaponShotDiagnostic> ShotPublished
- static WeaponShotDiagnostic[] RecentShots
Reference
Authoring assets
WeaponOperationProfileDefinition
asset
The aggregate that makes a weapon operate. Holds references to the fire modes, delivery, ballistic, recoil, and reload sub-profiles plus an optional pellet pattern, and owns the jam and heat model directly. Create via ZOA > Armory > Weapon Operation Profile.
- IReadOnlyList<FireModeDefinition> SupportedFireModes; int DefaultFireModeIndex; FireModeDefinition GetFireModeAt(int index)
- FireDeliveryDefinition FireDelivery; BallisticProfileDefinition BallisticProfile
- RecoilProfileDefinition RecoilProfile; ReloadProfileDefinition ReloadProfile
- PelletPatternDefinition PelletPattern; string ImpactClassId
- float JamChancePerShot, HeatPerShot, MaxHeat, CoolingPerSecond, CoolingDelayAfterShot
- float CooldownThresholdPercent, HeatFireRatePenaltyStartPercent, OverheatedFireRateMultiplier
- float HeatSpreadMultiplierAtMax, HeatRecoilMultiplierAtMax, HeatJamChanceBonusAtMax
FireModeDefinition
asset
Cadence. One asset per mode a weapon supports, so a rifle references a Single and an Auto asset and the runtime cycles between them.
- FireModeKind ModeKind — Single, Burst, Auto, Continuous, Charge, Scatter
- int BurstCount, ProjectilesPerShot; float TimeBetweenShots, SpreadAngle, ChargeTime
- bool RequiresTriggerReleaseBetweenBursts
FireDeliveryDefinition
asset
Geometry. How the damage travels from muzzle to target, and which layers are eligible to receive it.
- DeliveryKind DeliveryKindValue — Hitscan, Projectile, Beam
- GameObject ProjectilePrefab; LayerMask DamageLayers
- float ProjectileSpeed, MaxDistance, BeamDuration, ImpactForce; bool PiercesTargets
BallisticProfileDefinition
asset
What happens to a round in flight. Start with muzzle velocity, effective range, and impact damage; reach for the rest when a weapon needs real velocity retention, a damage floor, or transonic behaviour.
- float MuzzleVelocity, EffectiveRange, GravityScale, ArmorPenetration, ImpactDamage
- float ProjectileMass (authored in grams); bool HasAuthoredProjectileMass
- float BallisticCoefficient, DragCoefficient
- float MinimumDamageFraction, DamageFalloffExponent
- float TransonicInstabilityVelocity, TransonicSpreadPenalty, WindDriftScale
RecoilProfileDefinition
asset
Kick and its recovery. The first four fields cover a basic weapon; the rest shape a firing string into a pattern the player can learn.
- float VerticalKick, HorizontalKick, RecoverySpeed, CameraImpulse
- float MaxVerticalOffset, MaxHorizontalOffset, RecoveryDelay
- float VerticalKickGrowthPerShot, HorizontalKickGrowthPerShot, HorizontalBias
- float PatternRandomness; int PatternPeriodShots
- float SustainedFireShotMemorySeconds, SettleSharpness
ReloadProfileDefinition
asset
Magazine size and the timing around refilling it, including whether the weapon tracks a chambered round separately and whether topping off a partial magazine is allowed at all.
- int MagazineCapacity; bool UsesChamberedRound, TacticalReloadSupported
- float ReloadDuration, ChamberDuration, UnjamDuration
PelletPatternDefinition
asset
Multi-pellet scatter. Attach one to an operation profile and the delivery fires that many rays per trigger pull inside a distance-growing cone, each attenuated by its own falloff curve.
- int PelletCount; float BaseSpreadDegrees, SpreadGrowthPerMeter, MaxSpreadDegrees
- float FullDamageDistance, MinDamageDistance, MinDamageFraction
- float ResolveSpreadAtDistance(float distance); float ResolveDamageMultiplier(float distance)
WeaponArmoryBindingDefinition
asset
Attaches an operation profile to a weapon definition, a whole platform, or both. This is the indirection that lets Weapon Modding stay ignorant of Armory: the weapon asset never references a profile.
- WeaponDefinition WeaponDefinition; WeaponPlatformDefinition WeaponPlatform
- WeaponOperationProfileDefinition OperationProfile
WeaponPackDefinition
asset
The unit of weapon distribution: a dotted slug id and an ordered list of weapons with optional pack-scoped preset and presentation overrides. List order is display order, and consumers resolve a pack by id through the registry.
- string PackId, Description; Sprite Icon; int Version; string CacheKey
- IReadOnlyList<WeaponEntry> Weapons; int WeaponCount; WeaponEntry GetWeaponAt(int index)
- WeaponEntry — definition, preset, presentation, EffectivePreset, EffectivePresentation
- bool TryFindEntryByWeaponId(string weaponDefinitionIdString, out WeaponEntry entry)
Usage
Examples
using UnityEngine;
using ZOA.Armory.Unity.Definitions;
using ZOA.Armory.Unity.Services;
using ZOA.Foundation.Unity.Database;
using ZOA.Messaging;
// The host only has to expose a mount transform and an aim camera.
public sealed class SimpleWeaponHost : MonoBehaviour, IWeaponEquipHost
{
[SerializeField] private Transform weaponMount;
[SerializeField] private Camera aimCamera;
public Transform WeaponMount => weaponMount;
public Camera AimCamera => aimCamera;
private EquippedWeaponHandle _handle;
public void Equip(WeaponDefinition definition, GameObject modelPrefab)
{
var equip = FoundryServiceRegistry.Get<IWeaponEquipService>();
var profile = ArmoryProfileResolver.Resolve(GameDatabase.Instance, definition);
var request = new WeaponEquipRequest(
definition: definition,
operationProfile: profile,
host: this,
modelPrefab: modelPrefab,
randomSeed: 1337); // deterministic jam rolls
if (!equip.TryEquip(request, out _handle, out var reason))
Debug.LogWarning(reason);
}
}using UnityEngine;
using ZOA.Armory.Unity.Definitions;
private void Start()
{
var runtime = _handle.Runtime;
runtime.OnFired += data => _hud.SetAmmo(data.magazineRoundsRemaining, data.chamberLoaded);
runtime.OnOverheat += data => _hud.FlashOverheat(data.currentHeat, data.maxHeat);
runtime.OnReloadStart += data => _animator.SetTrigger("Reload");
}
private void Update()
{
var runtime = _handle.Runtime;
float dt = Time.deltaTime;
// The runtime owns no clock of its own.
runtime.TickCooling(dt);
runtime.TickReload(dt);
var mode = runtime.SelectedFireMode;
if (mode != null && mode.ModeKind == FireModeDefinition.FireModeKind.Charge)
runtime.TickCharge(dt);
if (_reloadPressed)
runtime.StartReload(); // no-op while already reloading
// Heat stretches the interval between shots before it locks the weapon out.
float interval = runtime.GetEffectiveTimeBetweenShots(mode.TimeBetweenShots);
if (_triggerHeld && Time.time - _lastShot >= interval)
{
if (runtime.TryConsumeShot(out var reason))
{
_lastShot = Time.time;
DeliverShot();
}
else
{
_hud.ShowRefusal(reason); // "Weapon is jammed." and friends
}
}
}using System.Collections.Generic;
using UnityEngine;
using ZOA.Armory.Core.Events;
using ZOA.Armory.Unity.Handlers;
private readonly List<HitEventData> _hits = new();
private void DeliverShot()
{
var profile = _handle.OperationProfile;
var runtime = _handle.Runtime;
// Read effective values so installed attachments are accounted for,
// and fold in the heat penalty the runtime is currently reporting.
float spread = runtime.GetEffectiveSpreadAngle(runtime.SelectedFireMode.SpreadAngle)
* (1f + runtime.GetHeatSpreadMultiplier());
Vector3 direction = ApplyCone(_aimCamera.transform.forward, spread);
HitscanDeliveryHandler.Fire(
origin: _muzzle.position,
direction: direction,
delivery: profile.FireDelivery,
ballistics: profile.BallisticProfile,
hits: _hits,
weaponInstanceId: _handle.Instance.InstanceId,
weaponDefinitionId: _handle.Instance.DefinitionId,
owner: gameObject);
_recoil.ApplyRecoilKick(1f + runtime.GetHeatRecoilMultiplier());
}using UnityEngine;
using ZOA.Armory.Unity.Ballistics;
private void DeliverPellets(float estimatedRange)
{
var pattern = _handle.OperationProfile.PelletPattern;
if (pattern == null) { DeliverShot(); return; }
// The cone widens with range, then clamps.
float coneHalfAngle = pattern.ResolveSpreadAtDistance(estimatedRange);
for (int i = 0; i < pattern.PelletCount; i++)
{
Vector3 direction = PelletConeSampler.Sample(
centerDirection: _aimCamera.transform.forward,
pelletIndex: i,
pelletCount: pattern.PelletCount,
coneHalfAngleDegrees: coneHalfAngle,
addJitter: true);
if (!Physics.Raycast(_muzzle.position, direction, out var hit,
_handle.OperationProfile.FireDelivery.MaxDistance))
continue;
// Each pellet attenuates on its own travelled distance.
float damage = _handle.OperationProfile.BallisticProfile.ImpactDamage
* pattern.ResolveDamageMultiplier(hit.distance);
ApplyDamage(hit, direction, damage, pelletIndex: i);
}
}using ZOA.Armory.Core.Runtime;
public sealed class MagazinePouch : IAmmoReserveSource
{
private int _rounds;
public MagazinePouch(int rounds) => _rounds = rounds;
public int Available => _rounds;
public int TryTake(int requested)
{
int taken = requested < _rounds ? requested : _rounds;
_rounds -= taken;
return taken;
}
}
// Opt in per weapon. With no source attached the runtime keeps its
// historical free full reload, so existing content is unaffected.
_handle.Runtime.SetReserveSource(new MagazinePouch(120));using ZOA.Armory.Unity.Services;
using ZOA.Messaging;
var loadout = FoundryServiceRegistry.Get<IWeaponLoadoutService>();
loadout.RegisterHost(this, new InventoryStowageAdapter(playerInventory));
// LoadoutChanged fires on refusals too, so the UI just re-reads.
loadout.LoadoutChanged += args =>
{
if (args.Transition == WeaponLoadoutChangedArgs.Kind.Refused)
_hud.ShowRefusal(args.Reason);
_wheel.Rebuild(loadout.GetOwnedWeapons(this));
};
// Each verb snapshots the outgoing weapon onto its item payload and
// restores the incoming one, so the magazine and attachments survive.
if (!loadout.TrySwitchActive(this, sidearmInstanceId, out var reason))
_hud.ShowRefusal(reason);Integration
Behavior to account for
See also