Equipment layers + paperdoll UI (IEquippable, worn-gump layering) #106

Closed
opened 2026-07-20 18:24:01 +02:00 by marco · 2 comments
Owner

Full worn-gump layering paperdoll + a proper equipment model. Supersedes the UO-gump direction of #39/#19 (we go own extraction from UOML for the paperdoll wardrobe). Being built on feat/paperdoll-ui.

Goal (player POV)

Open the paperdoll (Ctrl+P): your character (M/F body) with worn equipment drawn on it — sword in hand, armor on the torso. Double-click a wearable (backpack OR ground <=1 tile) to equip; double-click a worn item to take it off. Equipping auto-replaces conflicting gear (old piece back to the backpack). All server-validated.

Equipment model (server, ModernUO-informed)

  • enum EquipLayer { OneHanded, TwoHanded, Torso } (extensible) — replaces EquipSlot { Weapon, Armor }.
  • interface IEquippable { EquipLayer Layer; void OnEquip(Mobile wearer); void OnUnequip(Mobile wearer); } on the item definition (Sword/Armor implement; Coin/Stone don't -> CannotEquip).
  • Conflicts as a table (EquipConflicts), replace-on-equip: equipping a layer clears itself + conflicting layers; freed items return to backpack. OneHanded<->TwoHanded mutually exclusive; Torso independent (2x2h / 2h+1h impossible by construction).
  • OnEquip/OnUnequip = effect hooks (per-type reaction; the system orchestrates conflicts, not the item). v1: weapon OnEquip triggers the existing ApplyEquippedWeapon. Future: "sword +20 STR" as an override. Effects are derived & re-applied at restore, never persisted baked; OnUnequip MUST run on every removal path (swap/unequip/restore) or stats leak. Future gate: CanEquip(Mobile) (str requirements, with #8).
  • Orchestration in an EquipmentSystem component; World stays a thin facade.

Equip flow + validation (server-authoritative)

  • One intent Equip(ItemId) (exists). Client never picks slot/source.
  • Server resolves source: owned+equippable -> equip; ground within InteractionRange (1 tile) + equippable -> atomic pickup+equip; ground too far -> OutOfRange; not equippable -> CannotEquip (DONE). Range checked server-side only.

Persistence + protocol

  • EquipSlot->EquipLayer: bump the item's serialization version + branch in its Deserialize. OwnedItem.Slot carries the layer name on the wire. ProtocolVersion bump (already 2 via CannotEquip).

Paperdoll rendering (client)

  • Background = extracted frame gump/paperdoll (0x07D0); its dragon region is the character window (like the empty area of a standard UO paperdoll) -> we draw the PG there.
  • Draw body (body_male 0x000C / body_female 0x000D), then worn overlays in layer order (all 260x237 same canvas -> align by stacking).
  • Worn art extracted: sword 0xC5C8 (F 0xECD8), torso armor 0xC721 (F 0xEE31). Female = male + 10000 per gump.def.
  • Client PaperdollArt catalog Kind -> (wornGump, layer) (gumps are presentation, no protocol). Double-click worn -> Unequip. Fallback plain panel if pack absent.

Interaction

  • Double-click = equip, uniform for backpack items AND ground sprites (within range). DoubleClickDetector (Client.Core, DONE) runs on the backpack panel + world ground sprites. No drag-to-equip in v1.

Art pipeline (config-driven)

  • Externalize the gump-id manifest out of Program.cs into a data file read by the extractor: adding a gump = a data row + just pack, zero recompile.
  • Committed ids: paperdoll 0x07D0, body_male 0x000C, body_female 0x000D, sword 0xC5C8(+F), armor 0xC721(+F).

Namespaces (folded-in refactor)

Namespaces as logical domains crossing the Core/Client assemblies (boundary enforced by project refs, not namespaces): IsoMmo.Client.UI (layouts + panels), IsoMmo.Client.Gumps (ItemArt/PaperdollArt), IsoMmo.Client.Iso, etc.

Already done (feat/paperdoll-ui)

  • Server CannotEquip + integration test (green). DoubleClickDetector + PaperdollLayout (Client.Core, tested). Extracted+committed pack: frame, M/F bodies, worn sword, worn armor.

Definition of Done

  • Ctrl+P shows the body with equipped sword (hand) + armor (torso) as UO worn gumps.
  • Double-click a backpack sword -> appears worn, leaves backpack; double-click it on the paperdoll -> back to backpack.
  • Double-click a wearable on the ground <=1 tile -> equipped (pickup+worn); >1 tile -> "out of range"; non-wearable -> "can't equip".
  • Equipping a 2nd weapon replaces the 1st (old one to backpack).
  • Female char shows female body + female worn art.
  • Adding a wearable's paperdoll art = a data row + just pack (no code change).
  • Whole solution builds 0 warnings; dotnet test green (new layout/equip tests); ProtocolVersion bumped.

Deferred

Multi-piece wardrobe beyond current items; stat-mod system via OnEquip; CanEquip requirements (with #8 M6); richer worn art from a stock UO gumpart.mul (this UOML pack's wardrobe is thin) or own-art.

Full worn-gump layering paperdoll + a proper equipment model. Supersedes the UO-gump direction of #39/#19 (we go **own extraction from UOML** for the paperdoll wardrobe). Being built on `feat/paperdoll-ui`. ## Goal (player POV) Open the paperdoll (**Ctrl+P**): your character (M/F body) with **worn equipment drawn on it** — sword in hand, armor on the torso. **Double-click** a wearable (backpack OR ground <=1 tile) to equip; double-click a worn item to take it off. Equipping **auto-replaces** conflicting gear (old piece back to the backpack). All server-validated. ## Equipment model (server, ModernUO-informed) - `enum EquipLayer { OneHanded, TwoHanded, Torso }` (extensible) — replaces `EquipSlot { Weapon, Armor }`. - `interface IEquippable { EquipLayer Layer; void OnEquip(Mobile wearer); void OnUnequip(Mobile wearer); }` on the item **definition** (`Sword`/`Armor` implement; `Coin`/`Stone` don't -> `CannotEquip`). - **Conflicts as a table** (`EquipConflicts`), replace-on-equip: equipping a layer clears itself + conflicting layers; freed items return to backpack. OneHanded<->TwoHanded mutually exclusive; Torso independent (2x2h / 2h+1h impossible by construction). - **`OnEquip`/`OnUnequip` = effect hooks** (per-type reaction; the **system** orchestrates conflicts, not the item). v1: weapon `OnEquip` triggers the existing `ApplyEquippedWeapon`. Future: "sword +20 STR" as an override. Effects are **derived & re-applied at restore, never persisted baked**; `OnUnequip` MUST run on every removal path (swap/unequip/restore) or stats leak. Future gate: `CanEquip(Mobile)` (str requirements, with #8). - Orchestration in an `EquipmentSystem` component; `World` stays a thin facade. ## Equip flow + validation (server-authoritative) - One intent `Equip(ItemId)` (exists). Client never picks slot/source. - Server resolves source: owned+equippable -> equip; **ground within `InteractionRange` (1 tile)** + equippable -> **atomic pickup+equip**; ground too far -> `OutOfRange`; not equippable -> `CannotEquip` (DONE). Range checked server-side only. ## Persistence + protocol - `EquipSlot`->`EquipLayer`: bump the item's serialization version + branch in its `Deserialize`. `OwnedItem.Slot` carries the layer name on the wire. **`ProtocolVersion` bump** (already 2 via `CannotEquip`). ## Paperdoll rendering (client) - Background = extracted frame `gump/paperdoll` (`0x07D0`); its dragon region is the **character window** (like the empty area of a standard UO paperdoll) -> we draw the PG there. - Draw **body** (`body_male 0x000C` / `body_female 0x000D`), then **worn overlays** in layer order (all 260x237 same canvas -> align by stacking). - Worn art extracted: **sword `0xC5C8`** (F `0xECD8`), **torso armor `0xC721`** (F `0xEE31`). Female = male + 10000 per `gump.def`. - Client `PaperdollArt` catalog `Kind -> (wornGump, layer)` (gumps are presentation, no protocol). Double-click worn -> `Unequip`. Fallback plain panel if pack absent. ## Interaction - **Double-click = equip**, uniform for backpack items AND ground sprites (within range). `DoubleClickDetector` (Client.Core, DONE) runs on the backpack panel + world ground sprites. No drag-to-equip in v1. ## Art pipeline (config-driven) - Externalize the gump-id manifest out of `Program.cs` into a **data file** read by the extractor: adding a gump = a data row + `just pack`, zero recompile. - Committed ids: `paperdoll 0x07D0`, `body_male 0x000C`, `body_female 0x000D`, `sword 0xC5C8`(+F), `armor 0xC721`(+F). ## Namespaces (folded-in refactor) Namespaces as **logical domains crossing the Core/Client assemblies** (boundary enforced by project refs, not namespaces): `IsoMmo.Client.UI` (layouts + panels), `IsoMmo.Client.Gumps` (ItemArt/PaperdollArt), `IsoMmo.Client.Iso`, etc. ## Already done (`feat/paperdoll-ui`) - Server `CannotEquip` + integration test (green). `DoubleClickDetector` + `PaperdollLayout` (Client.Core, tested). Extracted+committed pack: frame, M/F bodies, worn sword, worn armor. ## Definition of Done - Ctrl+P shows the body with equipped sword (hand) + armor (torso) as UO worn gumps. - Double-click a backpack sword -> appears worn, leaves backpack; double-click it on the paperdoll -> back to backpack. - Double-click a wearable on the ground <=1 tile -> equipped (pickup+worn); >1 tile -> "out of range"; non-wearable -> "can't equip". - Equipping a 2nd weapon replaces the 1st (old one to backpack). - Female char shows female body + female worn art. - Adding a wearable's paperdoll art = a data row + `just pack` (no code change). - Whole solution builds 0 warnings; `dotnet test` green (new layout/equip tests); `ProtocolVersion` bumped. ## Deferred Multi-piece wardrobe beyond current items; stat-mod system via `OnEquip`; `CanEquip` requirements (with #8 M6); richer worn art from a **stock UO gumpart.mul** (this UOML pack's wardrobe is thin) or own-art.
marco added this to the Alpha milestone 2026-07-20 18:37:56 +02:00
Author
Owner

Landed via #116: composable client gump framework + draggable paperdoll (body + worn gumps, double-click equip/unequip), the EquipLayer/IEquippable model with conflict resolution (2H displaces 1H) + OnEquip/OnUnequip hooks (weapon arms the wearer), Greatsword (2H), CannotEquip cue, config-driven gump manifest, item save v2. Green: 170 GameServer + 65 Client.Core tests, 0 warnings.

Remaining follow-ups (keeping this open):

  • Ground double-click equip (double-click a ground item <=1 tile -> pickup+equip) — wants a server EquipOutcome (Equipped/OutOfRange/CannotEquip) for the right cue.
  • Namespace reorg into logical domains (IsoMmo.Client.UI / .Gumps / …) crossing the Core/Client assemblies.
Landed via #116: composable client gump framework + draggable paperdoll (body + worn gumps, double-click equip/unequip), the EquipLayer/IEquippable model with conflict resolution (2H displaces 1H) + OnEquip/OnUnequip hooks (weapon arms the wearer), Greatsword (2H), CannotEquip cue, config-driven gump manifest, item save v2. Green: 170 GameServer + 65 Client.Core tests, 0 warnings. Remaining follow-ups (keeping this open): - Ground double-click equip (double-click a ground item <=1 tile -> pickup+equip) — wants a server EquipOutcome (Equipped/OutOfRange/CannotEquip) for the right cue. - Namespace reorg into logical domains (IsoMmo.Client.UI / .Gumps / …) crossing the Core/Client assemblies.
Author
Owner

Done — delivered by PR #116: server model (`IEquippable`/`EquipLayer`/`EquipConflicts`, `OnEquip/OnUnequip`) and client `PaperdollGump` (body + worn-gump layering by convention, double-click to unequip, draggable, right-click close). Showing worn gear on other players' in-world sprites is tracked separately by #18. Closing.

Done — delivered by PR #116: server model (\`IEquippable\`/\`EquipLayer\`/\`EquipConflicts\`, \`OnEquip/OnUnequip\`) and client \`PaperdollGump\` (body + worn-gump layering by convention, double-click to unequip, draggable, right-click close). Showing worn gear on *other players'* in-world sprites is tracked separately by #18. Closing.
marco closed this issue 2026-07-22 06:07:58 +02:00
Sign in to join this conversation.
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#106
No description provided.