feat: ranged combat (archery) — bow, line-of-sight, generic projectile #175

Merged
marco merged 4 commits from feat/ranged-archery into main 2026-07-23 23:09:15 +02:00
Owner

Closes #111.

Summary

Adds ranged combat (archery) as a general ranged path in CombatSystem, not a bow special-case. Weapon reach is now data on BaseWeapon (melee = 1, bow = 8); combat validates Chebyshev range and line of sight at both swing start and resolution, so a target that steps out of range or ducks behind a wall dodges the shot — the same server-paced honesty as the spell bolt. LOS is checked unconditionally: it's a no-op for adjacent melee (no cell between the tiles) and the real gate for a ranged shot.

Adds the Archery skill and a Bow (auto-discovered like the other weapons; /give bow works with no command change). Following the issue's steer to "reuse the spell projectile broadcast", SpellBolt is generalised to a single Projectile message (SourceId/TargetId/ProjectileKind/Damage/Hit) shared by spell bolts and bow shots — so ProtocolVersion is bumped to 11. Nothing shows an arrow that was dodged: the projectile is emitted at resolution, AoI-scoped, carrying only damage (never HP).

Also the archery presentation: the character plays a bow-shoot pose (UO people action 18) instead of the melee swing when a bow is wielded, the bow is drawn worn on the body in-world and on the paperdoll, and the arrow launches ahead of the shooter rotated to its flight path.

No persistence version bump: the bow persists as an ordinary item and Archery rides the existing skill list; both re-derive on restore.

Screenshots / recording

Captured by piloting the client via the debug harness (fresh DB → just dev), commands per state below.

Bow given (item in backpack)

Bow given (item in backpack)

Bow worn on the paperdoll

Bow worn on the paperdoll

Arrow in flight at 6-tile range, rotated to its path, -12 hit

Arrow in flight at 6-tile range, rotated to its path, -12 hit

Harness commands (abridged): login test → chat /give bow → double-click the bow in the backpack (equip) → open doll (paperdoll) → chat /spawn dummy + click a tile 6 away → click the dummy (engage) → burst screenshot to catch the arrow at resolution.

How it was tested

  • Unit (tests/.../Gameplay/RangedCombatTests.cs, against World/CombatSystem): bow hits a target 6 tiles away as an Arrow projectile; a melee weapon can't reach that far; a target that leaves range or ducks behind a static before resolution whiffs; a shot with no LOS at start is refused; adjacent melee still lands (LOS no-op); an equipped bow + Archery round-trips through persistence and re-arms the archer on restore.
  • Protocol round-trip for the new Projectile message (ProtocolJsonTests); HumanArtLayoutTests updated for the new shoot key.
  • just test green (461 tests), just lint zero warnings, whole solution builds.
  • Manual: drove the flow above end-to-end — arrow flies at 6-tile range, deals 12 damage, trains Archery; bow shows in backpack + paperdoll; behind-cover/out-of-range shots whiff.

Checklist

  • just lint passes (CSharpier + analyzers, zero warnings)
  • just test is green
  • The whole solution builds (client and tools included)
  • Multi-platform preserved (server on Win/macOS/Linux, client on Win/macOS)
  • Tests added/updated for this change
  • Linked the related issue (#111) and its Definition of Done is met
Closes #111. ## Summary Adds **ranged combat (archery)** as a general ranged path in `CombatSystem`, not a bow special-case. Weapon reach is now data on `BaseWeapon` (melee = 1, bow = 8); combat validates Chebyshev range **and** line of sight at both swing start and resolution, so a target that steps out of range or ducks behind a wall dodges the shot — the same server-paced honesty as the spell bolt. LOS is checked unconditionally: it's a no-op for adjacent melee (no cell between the tiles) and the real gate for a ranged shot. Adds the `Archery` skill and a `Bow` (auto-discovered like the other weapons; `/give bow` works with no command change). Following the issue's steer to "reuse the spell projectile broadcast", `SpellBolt` is **generalised to a single `Projectile` message** (`SourceId`/`TargetId`/`ProjectileKind`/`Damage`/`Hit`) shared by spell bolts and bow shots — so `ProtocolVersion` is bumped to 11. Nothing shows an arrow that was dodged: the projectile is emitted at resolution, AoI-scoped, carrying only damage (never HP). Also the archery **presentation**: the character plays a bow-shoot pose (UO people action 18) instead of the melee swing when a bow is wielded, the bow is drawn worn on the body in-world and on the paperdoll, and the arrow launches ahead of the shooter rotated to its flight path. No persistence version bump: the bow persists as an ordinary item and `Archery` rides the existing skill list; both re-derive on restore. ## Screenshots / recording Captured by piloting the client via the debug harness (fresh DB → `just dev`), commands per state below. **Bow given (item in backpack)** ![Bow given (item in backpack)](https://git.homelab.devncode.it/attachments/4e7948d4-53e6-42fd-9ca4-d886aeda6aba) **Bow worn on the paperdoll** ![Bow worn on the paperdoll](https://git.homelab.devncode.it/attachments/d7f26ebf-b115-4648-9877-dda42a565cf2) **Arrow in flight at 6-tile range, rotated to its path, -12 hit** ![Arrow in flight at 6-tile range, rotated to its path, -12 hit](https://git.homelab.devncode.it/attachments/e8e117c3-a965-4dff-95a4-d61db52aab10) Harness commands (abridged): `login test` → chat `/give bow` → double-click the bow in the backpack (equip) → `open doll` (paperdoll) → chat `/spawn dummy` + click a tile 6 away → click the dummy (engage) → burst `screenshot` to catch the arrow at resolution. ## How it was tested - **Unit** (`tests/.../Gameplay/RangedCombatTests.cs`, against `World`/`CombatSystem`): bow hits a target 6 tiles away as an `Arrow` projectile; a melee weapon can't reach that far; a target that leaves range **or** ducks behind a static before resolution whiffs; a shot with no LOS at start is refused; adjacent melee still lands (LOS no-op); an equipped bow + `Archery` round-trips through persistence and re-arms the archer on restore. - **Protocol** round-trip for the new `Projectile` message (`ProtocolJsonTests`); `HumanArtLayoutTests` updated for the new `shoot` key. - `just test` green (461 tests), `just lint` zero warnings, whole solution builds. - **Manual**: drove the flow above end-to-end — arrow flies at 6-tile range, deals 12 damage, trains Archery; bow shows in backpack + paperdoll; behind-cover/out-of-range shots whiff. ## Checklist - [x] `just lint` passes (CSharpier + analyzers, zero warnings) - [x] `just test` is green - [x] The whole solution builds (client and tools included) - [x] Multi-platform preserved (server on Win/macOS/Linux, client on Win/macOS) - [x] Tests added/updated for this change - [x] Linked the related issue (#111) and its Definition of Done is met
Weapon reach is now data on BaseWeapon (melee = 1, bow = 8); CombatSystem
validates range + line of sight (a no-op for adjacent melee) at both swing
start and resolution, so a target that leaves range or ducks behind cover
dodges the shot. Adds the Archery skill and a Bow. SpellBolt is generalised
to Projectile (source/target/kind/damage/hit) so a bow's arrow and a spell
bolt share one AoI-scoped broadcast; ProtocolVersion bumped to 11.
item_bow (UO 0x13B2) so the bow draws as an item, and the arrow_shot effect
(0x0F42, a single arrow tile flown source→target) so a ranged shot is visible.
Rebuilt assets.isoa from the classic .mul client.
The bow now reads as archery, not a punch: a shoot body action (UO people
action 18) plays instead of the melee swing when a bow is wielded, the worn
bow overlays the body in-world (equip anim 0x0289) and shows on the paperdoll
(gump 0xC5D9), and the arrow launches ahead of the shooter rotated to its
flight path instead of spawning on top of them flying sideways.
feat(assets): pack the human shoot pose and worn/paperdoll bow art
All checks were successful
ci / Lint & Test (pull_request) Successful in 2m8s
c0fbbdf254
Rebuilt assets.isoa with human/shoot_*, equip/bow/* and gump/worn_bow_male.
marco merged commit 6b0b7dca4a into main 2026-07-23 23:09:15 +02:00
marco deleted branch feat/ranged-archery 2026-07-23 23:09:16 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo!175
No description provided.