diablo2-web/PROJECT.md

10 KiB
Raw Blame History

Project: D2GS v1.13c Online Gameplay Loops, Server Authority & Spell Visual Effects (diablo2-web)

Architecture

The Diablo II v1.13c Web Client (diablo2-web) is structured into strictly isolated modules enforcing server-authoritative online gameplay over the D2GS v1.13c protocol:

diablo2-web/
├── src/
│   ├── common/
│   │   ├── items/             # 1.13c item tables (embedded-drop-tables.ts), stat formatter (item-bridge.ts), tooltip builder (item-tooltip.ts), ground-items.ts
│   │   ├── skills/            # 1.13c Skills.txt & Missiles.txt data (skills-data.ts, skills-meta.ts, missiles-data.ts)
│   │   └── units/             # All-Act Town & Quest NPC descriptors (npc-table.ts), 1.13c direction mapping (direction.ts: DIR64_TO_DCC)
│   ├── netproto/
│   │   ├── index.ts           # Sole public barrel export for netproto (strict boundary for src/client/**)
│   │   ├── domain/            # Typed ClientCommand (client-command.ts), ServerEvent (server-event.ts), ItemData & UnitSnapshot (ids.ts)
│   │   └── d2gs/
│   │       ├── tables/        # Authoritative 1.13c packet size tables (s2c-sizes.ts: C2S_PACKET_SIZES, S2C_PACKET_SIZES)
│   │       ├── items/         # 0x9C/0x9D item bitstream decoder (item-bitstream.ts)
│   │       ├── c2s/           # C2S binary packet encoders (items.ts, npc.ts, party.ts, movement.ts, interact.ts, ui.ts)
│   │       ├── s2c/           # S2C binary packet decoders (units.ts, npc.ts, merc.ts, party.ts, trade.ts, waypoint.ts, skills.ts, misc.ts)
│   │       └── registry.ts    # encodeClientCommand & decodeD2gsServerPacket dispatchers
│   └── client/
│       ├── world/             # Authoritative ClientWorld state reducer & 25Hz simulation (client-world.ts, client-unit.ts, inventory.ts, self.ts)
│       ├── view/              # Scene drawables & picking (unit-drawables.ts, scene-source.ts)
│       ├── render/            # Pre-baked 1.13c missile & overlay metadata (missiles-meta.ts, overlays-meta.ts)
│       ├── scene/             # WorldRenderer (world-renderer.ts, missile-overlay-renderer.ts)
│       ├── session/           # OnlineSession (online-session.ts)
│       ├── input/             # CommandMapper (command-mapper.ts)
│       ├── ui-model/          # HudModel (hud-model.ts): syncs ClientWorld -> HudManager/WorldPanelsHud & emits ClientCommand
│       └── ui/                # HudManager (hud-manager.ts), WorldPanelsHud (world-panels.ts), InventoryPanel (inventory.ts), Minimap (minimap.ts)
└── tests/
    ├── arch/                  # Architectural boundary enforcement (boundaries.test.ts)
    ├── client/                # Unit & integration tests (client-world, hud-session-play, missile-id-map, unit-facing-motion, hud-server-authority, vendor-stash-ui)
    └── e2e-d2gs-online/       # 4-tier opaque-box E2E test suite + Tier 5 adversarial coverage hardening

Architectural Invariants

  1. Diablo II v1.13c Ground Truth: Every C2S and S2C opcode, packet byte length (C2S_PACKET_SIZES and S2C_PACKET_SIZES in src/netproto/d2gs/tables/s2c-sizes.ts), item bitstream field (src/netproto/d2gs/items/item-bitstream.ts), and Excel/TBL data table (src/common/items/, src/common/skills/, src/common/units/npc-table.ts) must strictly conform to Diablo II v1.13c.
  2. Server Authority: When HudManager has a _commandSink attached (hasCommandSink === true), user actions MUST emit typed ClientCommand events (src/netproto/domain/client-command.ts) encoded via encodeClientCommand (src/netproto/d2gs/registry.ts) without mutating local container/stat/skill state ahead of the server. ServerEvent updates applied to ClientWorld (src/client/world/client-world.ts) and synced via HudModel.syncFromWorld() (src/client/ui-model/hud-model.ts) drive all UI state.
  3. Strict Import Boundary: Files in src/client/** MUST ONLY import protocol types/functions from src/netproto/index.ts (never deep-importing src/netproto/d2gs/** or src/server/**). Files in src/common/** MUST ONLY import from src/common/** and remain 100% deterministic.

Feature Inventory (Issue #550 — Spell Visual Effects)

Every feature from the Phase 0 Survey (derived from ORIGINAL_REQUEST.md 2026-10-02T07:31:06Z and Explorer Reports r1_1, r1_2, r1_3) is listed below with its assigned milestone. No feature is left unassigned.

# Feature Description Milestone Source
F17 Canonical 1.13c Cast Overlays & Caster State Lifecycle (R1) Display canonical 1.13c Skills.txt castoverlay (Overlay.txt, e.g. fire_cast_1, fire_cast_2, ice_cast_1, ice_cast_2, ice_cast_3, light_cast_1, light_cast_2, teleport, bonearmor_cast, bonecast, cursecast) on any casting unit (from server SkillCast 0x4C/0x4D/0x6C/0x99/0x9A or local useSkillAt/useSkillOn/attackUnit) as a one-shot non-looping animation with dynamic point lighting; deduplicate local player casts against server SkillCast echoes within 250ms; expire unit.castState back to neutral mode (1) when cast duration finishes. M5 R1, Survey r1_1–r1_3
F18 Client-Side Flying Missiles, 0x73 CreateMissile, & 1.13c Trajectory Simulation (R2) Preserve at: { x, y }, level, owner in decode0x73CreateMissile and spawn missile units (unitType = 3, kind = 'missile') on 0x73 and spell casts; step at 25Hz according to 1.13c Skills.txt & Missiles.txt (Vel, Range, LevRange) for single-bolt projectiles (Fire Bolt 36, Ice Bolt 39, Ice Blast 45, Fire Ball 47, Glacial Spike 55, Lightning 49, Chain Lightning 53, Bone Spear 84, Bone Spirit 93, Holy Bolt 101, bow/crossbow skills), multi-projectile fans (Charged Bolt 38 with orthogonal jitter, Teeth 67, Multiple Shot 12), radial rings (Frost Nova 44, Nova 48, Poison Nova 92), spiral trajectories (Blessed Hammer 112), sub-missile emitters (Frozen Orb 64 emitting radial icebolts + final nova), and ground/skyfall spells (Fire Wall 51, Blaze 46, Meteor 56, Blizzard 59); map velocity vectors to authentic 1.13c DCC directions (DIR64_TO_DCC / velocityToDccDirection). M5 R2, Survey r1_1–r1_3
F19 Collision Detection & Spell Hit / Impact Explosion Visuals (R3) Detect missile collisions against blocked map wall/obstacle sub-tiles and alive hostile units (respecting pierce); terminate non-piercing missiles on impact (or range expiry for alwaysExplode / ExplosionMissile missiles) and spawn canonical 1.13c ExplosionMissile / hit effect (fireexplode for Fire Bolt, explodingarrowexp for Fire Ball, iceexplode for Ice Bolt, freezeexplode for Ice Blast/Glacial Spike, frozenorbexplode for Frozen Orb, meteorexplode for Meteor, teethexplode for Teeth/Bone Spear, bonespiritexplode for Bone Spirit, lightninghit for Lightning/Chain Lightning/Static Field/Telekinesis) as a one-shot non-looping visual (loop: false, loopAnim: false) with dynamic point lighting and frame clamping (totalFrames - 1) that is removed upon completion. M5 R3, Survey r1_1–r1_3
F20 Scene Drawables, WorldRenderer One-Shot Clamping, Automated Tests & Gitea PR Merge (R4) Output SceneOverlayDrawable and SceneMissileDrawable with loop: false / loopAnim: false one-shot frame clamping and dynamic point lights in unit-drawables.ts and world-renderer.ts; verify with automated tests (npx vitest run) and production build (npm run build); commit, push fix/issue-550-spell-visual-effects, open Gitea PR (Closes #550), and merge into main. M5 R4, Survey r1_1–r1_3

Milestones

# Name Scope Dependencies Status
M1–M4, E2E Prior D2GS v1.13c Online Gameplay Milestones F1–F16 (Completed in prior cycle) none DONE
M5 Issue #550: Restore 1.13c Spell Visual Effects (Cast Overlays, Flying Missiles, Hit Explosions, Verification & PR Merge) F17, F18, F19, F20 across src/netproto/, src/common/skills/, src/client/world/, src/client/session/, src/client/view/, src/client/scene/, and tests/client/ M1–M4 IN_PROGRESS

Interface Contracts (Issue #550)

1. src/netproto/domain/server-event.ts & src/netproto/d2gs/s2c/misc.ts

  • SkillCast variant on ServerEvent includes optional at?: Pt | undefined and owner?: UnitRef | undefined.
  • decode0x73CreateMissile preserves at: { x, y }, level: level || 1, owner: { unitType: ownerType, id: ownerId }, and target: { x: targetX || x, y: targetY || y }.

2. ClientWorld (src/client/world/client-world.ts) & OnlineSession (src/client/session/online-session.ts)

  • OnlineSession exposes useSkillAt(x, y, skillId?, hand?, shift?), useSkillOn(unitId, unitType?, skillId?, hand?, shift?), and attackUnit(unitId, unitType?, shift?).
  • OnlineSession.tick(dtMs, nowMs) invokes this.world.tick(dtMs, ...) with map walkability/wall collision check when levelView is loaded (returning false / unblocked when levelView === null).
  • OnlineSession.buildRenderableUnits(nowMs) includes this.world.getUnitsByType(3) (missiles) and active caster overlays.
  • ClientWorld deduplicates local player skill casts against server SkillCast echoes within 250ms, expires unit.castState back to mode = 1, steps missiles at 25Hz, checks wall and hostile unit collisions, respects pierce, spawns one-shot ExplosionMissile units, and removes completed one-shot explosions/overlays.

3. unit-drawables.ts & world-renderer.ts

  • SceneOverlayDrawable and SceneMissileDrawable carry loop: boolean, loopAnim: boolean, totalFrames: number, and clamp frame to [0, totalFrames - 1] when !loop (loop === false / loopAnim === false).
  • MISSILE_ID_TO_KEY preserves exact Missiles.txt row names (tests/client/missile-id-map.test.ts); unit.token (missileKey) is passed to resolveMissileMetadata(unit.classId, unit.token) for client aliases such as freezeexplode.

Code Layout & Write Ownership

  • M5 Worker (teamwork_preview_worker_r1_1): Owns src/netproto/domain/server-event.ts, src/netproto/d2gs/s2c/misc.ts, src/common/skills/**, src/client/world/**, src/client/session/online-session.ts, src/client/view/unit-drawables.ts, src/client/scene/world-renderer.ts, src/client/scene/missile-overlay-renderer.ts, tests/client/client-world.test.ts, and tests/client/hud-session-play.test.ts.