Skip to content

On-character weapon visuals — design

Date: 2026-07-06 Status: Approved (design), pending implementation plan Area: Unity port — on-stage entity presentation (unity/raiders/Assets/Scripts/World/EntityView.cs + controllers)

Summary

Show a weapon sprite on top of each combatant's character (player, rival raiders, enemy mobs). The weapon rotates to aim at the actor's current target (idle rest pose when there is no target) and plays a short procedural attack animation on each strike — a preset chosen per weapon (stab, swing, ranged recoil, …). The visual is data-driven: if an ItemDef/EnemyDef has an on-character weapon sprite it renders; if not, the actor shows no weapon. More weapon sprites/presets can be added later without code changes.

Goals

  • On-character weapon sprite, layered above the body billboard, for player / rival raider / enemy mob.
  • Aim: rotate the weapon toward the current target in screen space; ease to an idle rest pose (body-facing direction) when there is no target.
  • Attack animation per weapon via a preset (procedural, code-driven): stab, swing, ranged recoil. Triggered on the actor's existing attack beat.
  • Fully optional per actor: sprite present → render; absent → skip.
  • Extensible: adding a weapon = assigning a sprite + preset on a def; no new code.

Non-goals

  • No Unity Animator / sprite-sheet frame animation (the whole project animates procedurally — Lean, step-hop, fog are all code-driven; we stay consistent).
  • No new combat/targeting plumbing — we reuse the beats that already exist.
  • No inventory/loadout UI changes. This is on-stage presentation only.
  • Not re-skinning the body sprite ("hero sprite") — that already exists on EntityView.

Context / current state

  • EntityView is the camera-facing billboard character card. Hierarchy: View (billboards to camera) → Rig (flip root: localScale.x = ±1 to mirror left/right) → Body (the sprite, scaled to diameter). It already:
  • exposes RigRoot — documented as the attach point for "weapons/attachments" so they flip and their swing animations mirror with the body;
  • has SetLook(worldDirXZ) — a per-frame combat-facing override (the actor faces its target while kiting);
  • has Lean(dir, peak, dur) — a render-only body tip toward a melee target (peak > 0) or recoil kick back from a shot (peak < 0);
  • owns the mirror flip sign (_flipSign), the billboard, and the step-hop.
  • ItemDef already has weaponSprite ("On-character art — WEAPONS ONLY").
  • Attack beats already exist in all three controllers, at the SetLook/Lean call sites:
  • PlayerController: melee strike (_view.Lean(dir, +attackLean, …)), shot (_view.Lean(aim, -shootRecoil, …)); stance tracked by _meleeStance.
  • RaiderController: AttackTarget melee Lean, FireArc recoil Lean.
  • EnemyController: melee Lean (simple/lunge/sequence), FireRanged recoil Lean.
  • Art: Assets/Sprites/Entities/CharWeapons/ has char_fang_knife, char_moon_sickle (melee), char_ash_crossbow, char_hunters_bow, char_oak_bow (ranged). More to come.
  • Weapon archetypes: PlayerController.archetype mapping (Crossbow→sniper, Bow→assault, Staff→smg, Wand→shotgun) and RaiderController roll one of assault/smg/shotgun/sniper. Raiders always fire ranged.

Design

1. Data model

  • ItemDef — keep weaponSprite; add AttackPreset attackPreset (default Auto).
  • EnemyDef — add Sprite weaponSprite + AttackPreset attackPreset, both optional. Most mobs leave weaponSprite null (claws/innate) → no weapon shown.
  • AttackPreset enum: Auto, Stab, Swing, RangedRecoil.
  • Auto derives from the owning slot/kind: melee → Swing, ranged → RangedRecoil. So most items need no manual preset; authors override for special cases (e.g. a dagger → Stab).
  • Rival raiders have no ItemDef loadout. They resolve a CharWeapons sprite from their rolled archetype via the reverse of archetypeKeyOf (sniper→crossbow; assault/smg/shotgun→a bow/other ranged), looked up through ItemDatabase, always with the RangedRecoil preset (raiders only ever fire ranged). A small explicit archetype→ItemDef key map keeps this deterministic and tunable; a missing entry → no weapon (safe skip).

2. WeaponView component

A focused MonoBehaviour on a child GameObject under EntityView.RigRoot, owning a single SpriteRenderer. Pure presentation, no game logic.

Interface:

  • SetWeapon(Sprite sprite, AttackPreset preset) — assign the sprite + resolved preset; sprite == nullSpriteRenderer.enabled = false (skip). Idempotent; safe to call on stance change.
  • Aim(Vector3 worldDirXZ, bool hasTarget, float flipSign) — each frame:
  • hasTarget → rotate the sprite to point at the target in screen space, compensating for the parent rig mirror via flipSign (a negatively-scaled parent flips the sense of a local Z-rotation).
  • !hasTarget → ease the rotation back to an idle rest pose aligned with the body facing.
  • Play() — start the preset's procedural motion (see §4). Non-interrupting: a new Play during an in-flight animation restarts it.

The WeaponView is created/owned by EntityView (built in Build() alongside Rig/Body under RigRoot), so every actor with an EntityView can carry a weapon without prefab surgery. EntityView drives Aim each LateUpdate from its own look dir + flip sign.

3. EntityView integration

EntityView is the natural owner — it already has the billboard, RigRoot, the look dir (SetLook), the attack beats (Lean), and the flip sign.

  • Expose float FlipSign { get; }.
  • Build a WeaponView child under RigRoot in Build().
  • In LateUpdate, after computing this frame's facing/flip, call weapon.Aim(currentLookDir, hasLook, FlipSign). "Has target" tracks whether SetLook was called this frame (combat facing) vs. falling back to move direction.
  • Add pass-throughs so controllers never reach into the child:
  • SetWeapon(Sprite, AttackPreset)weapon.SetWeapon(...).
  • WeaponAttack()weapon.Play().

4. Attack presets (procedural)

Each preset is a short, time-based curve applied to the WeaponView's local transform, layered on top of the existing body Lean. Tunable by a few params (reach/angle/duration), authored as constants on WeaponView (or a small config block) so they stay easy to tune — consistent with how Lean/hop are tuned.

  • Stab — translate the weapon forward along the aim, then ease back to rest.
  • Swing — rotate the weapon through an arc around its grip pivot (e.g. −A° → +A° → rest).
  • RangedRecoil — kick the weapon back opposite the aim (+ a small twist), ease to rest. This is the weapon's own recoil, distinct from and layered on the body Lean recoil.

Presets are driven by a normalized timer in WeaponView.Update() (like _leanT in EntityView), so a hitstop freeze (Time.deltaTime == 0) holds the pose — matching the existing feel.

5. Wiring per actor (reuses existing beats — no new combat code)

  • Player (PlayerController):
  • On spawn and whenever _meleeStance flips, resolve the active weapon (_meleeStance ? RunLoadout.weaponMelee : RunLoadout.weaponRanged), look up its ItemDef via ItemDatabase (itemKey), and call _view.SetWeapon(def.weaponSprite, def.attackPreset).
  • Call _view.WeaponAttack() at the two spots that already call _view.Lean(...) — the melee strike and the shot.
  • Rival raider (RaiderController):
  • _view.SetWeapon(...) once on spawn from the archetype→sprite resolution (§1).
  • _view.WeaponAttack() at the AttackTarget melee Lean and the FireArc recoil Lean.
  • Enemy mob (EnemyController):
  • _view.SetWeapon(def.weaponSprite, def.attackPreset) on spawn (only if sprite non-null).
  • _view.WeaponAttack() at its melee Lean beats and the FireRanged recoil Lean.

6. Rendering / draw order

  • The weapon SpriteRenderer sorts just above the body sprite and below HUD / perception cues, using the existing draw-order ladder (see the project's RenderQueue+ZTest approach).
  • It billboards with the body (it's under RigRootView) and inherits the mirror flip.
  • Because the weapon has its own SpriteRenderer (not covered by the body's), EntityView extends its existing SetVisibility / SetConcealed handling to also drive the weapon renderer's enabled + colour in lock-step with the body — so a fogged/hidden/concealed actor's weapon never renders when the body doesn't, and dims to the same echo/alpha.

7. Skip-if-absent

SetWeapon(null, …) disables the renderer. Actors whose def has no weaponSprite (most mobs, armor/accessory-only situations) simply show no weapon. No per-call guards leak into combat code beyond the single SetWeapon at spawn/stance-change.

Isolation / boundaries

  • WeaponView — one job: show + aim + animate one weapon sprite. Depends only on a sprite, a preset, and a per-frame aim dir + flip sign. Testable in isolation.
  • EntityView — owns the weapon child and forwards aim/flip; adds two pass-through methods. No combat knowledge.
  • Controllers — decide which weapon and when it attacks, using data + beats they already have. No rendering knowledge.
  • Defs (ItemDef/EnemyDef) — data only: sprite + preset.

Testing

Edit-mode harness (mirrors the chest/inventory verification pattern):

  1. Instantiate an EntityView; SetWeapon(sprite, preset) → assert the weapon renderer is enabled and shows the sprite; SetWeapon(null, …) → assert disabled.
  2. Aim at several world directions including a mirrored (left-facing) case → assert the weapon's local rotation points at the target through the flip.
  3. Play() each preset → assert the animation runs (pose changes) and returns to rest.
  4. Manual/play-mode: player stance swap shows the correct melee vs ranged sprite; a raider shows its archetype weapon; a mob with a def sprite shows it, one without shows none.

Open items deferred to the plan

  • Exact preset tuning constants (reach/arc/durations) — tune in play mode.
  • The archetype→ItemDef key map for raiders (which bow for assault/smg/shotgun).
  • Assigning the CharWeapons sprites + presets onto the existing weapon ItemDef assets.
  • Precise sorting-order value for the weapon vs body vs cues.

Rollout (optional phasing)

  • Phase 1 (approach C subset): show the static sprite under RigRoot, flip + body Lean only — no aim rotation or weapon animation. Lands the pipeline end-to-end.
  • Phase 2: aim rotation (Aim) + the procedural presets (Play).
  • Phase 3: raider archetype resolution + enemy def sprites + tuning.