feat(client): unify Player + creatures under one Mobile base (fixes PvP targeting) (#149) #151

Merged
marco merged 1 commit from feat/client-mobile-unify into main 2026-07-22 17:09:28 +02:00
Owner

Summary

Fixes the root cause behind "you can't click a friend to attack" (surfaced by the PvP-arena review, #109): the client kept players and creatures in two unrelated sealed types + two dicts (ClientWorld.Players / .Mobiles), and click-to-target only scanned the creature dict — so clicking a player did nothing. Every interaction had to remember both collections, a whole class of latent bugs.

This mirrors the server's model (MobilePlayerMobile + creatures) on the client:

  • New Mobile base (Entities/Mobile.cs): the shared server-driven state + behaviour — name, tile, glide-between-tiles, facing, tint, overhead/body points, Contains, Attack, PlayHit, Update. Abstract Draw/Contains/UpdateVisual.
  • Creature : Mobile (the former Mobile) — creature art + one-shot hit reaction.
  • Player : Mobile — appearance/equipment/speech/power-words/shield/swing; it now has a Contains hit-box (it never did — that's why players weren't clickable).
  • One ClientWorld.Mobiles (Dictionary<Guid, Mobile>); Self = Mobiles[SelfId] as Player; the five duplicated Players-then-Mobiles helpers collapse to one lookup each. Ids already share the Guid space, so no collision. Corpses/items stay separate (not mobiles).
  • Targeting (GameInputController): the pick iterates the one collection, excluding self → clicking a player or a creature engages it. That's the fix.
  • Renderer: the WorldKind.Player vs WorldKind.Mobile split collapses into one polymorphic Draw pass (extend-by-type, not switch).
  • Tint channels separated: a per-frame IsTarget (red highlight) is kept apart from the dispatcher-set status Tint (ghost), combined at draw (DrawTint), so unifying the update loop doesn't clobber a ghost's tint — and a live player-target now highlights red too.

No wire change — ingestion stays per-DTO (PlayerState → Player, MobileState → Creature) writing into the one dict; ProtocolVersion untouched.

Closes #149. Unblocks the PvP-arena work (#109) and is reused by spell-target / tile-highlight.

Screenshots / recording

No-regression check (fresh DB, debug harness): the self player renders correctly through the new unified Mobile path — sprite + name + tree occlusion — with biomes and ground items intact; the HUD/debug players N · mobiles M split (via the new PlayerCount) works.

world

Commands: login test; goto 24 26; screenshot.

Note: true friend-vs-friend PvP clicking needs two live clients, which the single-client debug harness can't drive; verified here that rendering/targeting is unbroken and the pick path now spans the unified collection (the same code that already engages creatures). Two-client PvP will be exercised when the arena (#109) is stood up.

How it was tested

  • just lint (CSharpier + analyzers, zero warnings) and just test (407 tests) green; whole solution builds incl. client + tools.
  • Smoke-ran the client from a fresh DB (screenshot above): world renders, no crash, player draws through Player : Mobile, debug status shows the new player/creature counts.

Checklist

  • just lint passes (CSharpier + analyzers, zero warnings)
  • just test is green
  • The whole solution builds (client and tools included)
  • Multi-platform preserved (client-only change; no OS-specific code)
  • Tests added/updated for this change — n/a (client MonoGame refactor; behaviour covered by existing suite + smoke run; no new pure logic to unit-test)
  • Linked the related issue (#149); PvP-targeting root cause resolved
## Summary Fixes the root cause behind "you can't click a friend to attack" (surfaced by the PvP-arena review, #109): the client kept **players and creatures in two unrelated `sealed` types + two dicts** (`ClientWorld.Players` / `.Mobiles`), and click-to-target only scanned the creature dict — so clicking a player did nothing. Every interaction had to remember both collections, a whole class of latent bugs. This mirrors the **server's model** (`Mobile` → `PlayerMobile` + creatures) on the client: - New **`Mobile` base** (`Entities/Mobile.cs`): the shared server-driven state + behaviour — name, tile, glide-between-tiles, facing, tint, overhead/body points, `Contains`, `Attack`, `PlayHit`, `Update`. Abstract `Draw`/`Contains`/`UpdateVisual`. - **`Creature : Mobile`** (the former `Mobile`) — creature art + one-shot hit reaction. - **`Player : Mobile`** — appearance/equipment/speech/power-words/shield/swing; it now has a `Contains` hit-box (it never did — that's why players weren't clickable). - **One `ClientWorld.Mobiles`** (`Dictionary<Guid, Mobile>`); `Self = Mobiles[SelfId] as Player`; the five duplicated `Players`-then-`Mobiles` helpers collapse to one lookup each. Ids already share the `Guid` space, so no collision. Corpses/items stay separate (not mobiles). - **Targeting** (`GameInputController`): the pick iterates the one collection, **excluding self** → clicking a **player or** a creature engages it. That's the fix. - **Renderer**: the `WorldKind.Player` vs `WorldKind.Mobile` split collapses into one polymorphic `Draw` pass (extend-by-type, not switch). - **Tint channels separated**: a per-frame `IsTarget` (red highlight) is kept apart from the dispatcher-set status `Tint` (ghost), combined at draw (`DrawTint`), so unifying the update loop doesn't clobber a ghost's tint — and a live player-target now highlights red too. **No wire change** — ingestion stays per-DTO (`PlayerState → Player`, `MobileState → Creature`) writing into the one dict; `ProtocolVersion` untouched. Closes #149. Unblocks the PvP-arena work (#109) and is reused by spell-target / tile-highlight. ## Screenshots / recording No-regression check (fresh DB, debug harness): the self player renders correctly through the new unified `Mobile` path — sprite + name + tree occlusion — with biomes and ground items intact; the HUD/debug `players N · mobiles M` split (via the new `PlayerCount`) works. ![world](https://git.homelab.devncode.it/attachments/dc5ddd2c-9f7c-4e52-8573-ef0be8b53d2d) Commands: `login test`; `goto 24 26`; `screenshot`. > Note: true friend-vs-friend PvP clicking needs two live clients, which the single-client debug harness can't drive; verified here that rendering/targeting is unbroken and the pick path now spans the unified collection (the same code that already engages creatures). Two-client PvP will be exercised when the arena (#109) is stood up. ## How it was tested - `just lint` (CSharpier + analyzers, **zero warnings**) and `just test` (**407 tests**) green; whole solution builds incl. client + tools. - Smoke-ran the client from a fresh DB (screenshot above): world renders, no crash, player draws through `Player : Mobile`, debug status shows the new player/creature counts. ## 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 (client-only change; no OS-specific code) - [x] Tests added/updated for this change — n/a (client MonoGame refactor; behaviour covered by existing suite + smoke run; no new pure logic to unit-test) - [x] Linked the related issue (#149); PvP-targeting root cause resolved
feat(client): unify Player + creatures under one Mobile base (fixes PvP targeting)
All checks were successful
ci / Lint & Test (pull_request) Successful in 2m37s
eeb9e6c3cc
marco merged commit 4b66a05b40 into main 2026-07-22 17:09:28 +02:00
marco deleted branch feat/client-mobile-unify 2026-07-22 17:09:29 +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!151
No description provided.