feat: targeting system (cursor) — foundation for spells & GM commands (#32) #37

Merged
marco merged 1 commit from feat/targeting into main 2026-07-17 23:04:58 +02:00
Owner

Closes #32.

Summary

A general target-selection system (UO-style cursor) — the foundation for the spell system (#36) and a cleanup of GM commands.

  • An action returns NeedsTarget(TargetSpec); the server sends a TargetRequest; the client enters a crosshair cursor and replies with a TargetResponse (a picked mobile, a tile, or cancel).
  • The server validates the pick against the request's spec: kind, range (Chebyshev), and line of sight (TileMap.HasLineOfSight, Bresenham) — then re-dispatches the action with a typed, polymorphic ResolvedTarget (MobileTarget / TileTarget).
  • No universal "target" type: each request declares which kind it accepts + its constraints; command-specific validity (alive/dead/walkable) lives in the command, not the engine. Two use-cases both fit: a spell (Mobile, range 5, LoS) and a "create object" (Tile, range, no LoS).

GM commands retrofit

Commands now return CommandResult (Feedback | NeedsTarget):

  • spawn /spawn rat → target a tile (rejected if blocked).
  • kill /kill → target a creature (LoS + within view) — replaces the old client-side _attackTarget hack.
  • resurrect /resurrect → target a dead player (ghost).

Architecture / invariants

World stays a thin facade — targeting lives in the command pipeline + a pure TargetValidator; World only gained a thin TryGetMobilePosition read. The formula/spec is the client's intent; the server is authoritative. Off-tick.

How it was tested

TileMap.HasLineOfSight (clear / blocked / endpoints not tested); TargetValidator (kind / range / LoS / missing); command flow (kill/spawn/resurrect request a target, resume with a ResolvedTarget; blocked-tile rejected); protocol round-trips (TargetRequest/TargetResponse). 218 tests green, zero warnings, CSharpier clean. docs/gm-commands.md updated.

Needs your eyes (GUI)

/kill → crosshair → click a rat → it dies. /spawn rat → click a tile → rat appears (click a wall → "blocked"). Die, then /resurrect → click your ghost → back at spawn. The crosshair is a placeholder; the real UO cursor gump is #19.

Checklist

  • just lint clean · [x] just test green (218) · [x] whole solution builds, 0 warnings · [x] multi-platform preserved · [x] tests added
Closes #32. ## Summary A general **target-selection system** (UO-style cursor) — the foundation for the spell system (#36) and a cleanup of GM commands. - An action returns `NeedsTarget(TargetSpec)`; the server sends a `TargetRequest`; the client enters a **crosshair cursor** and replies with a `TargetResponse` (a picked mobile, a tile, or cancel). - The server **validates the pick against the request's spec**: kind, range (Chebyshev), and **line of sight** (`TileMap.HasLineOfSight`, Bresenham) — then re-dispatches the action with a typed, polymorphic **`ResolvedTarget`** (`MobileTarget` / `TileTarget`). - **No universal "target" type**: each request declares which kind it accepts + its constraints; command-specific validity (alive/dead/walkable) lives in the command, not the engine. Two use-cases both fit: a spell (`Mobile`, range 5, LoS) and a "create object" (`Tile`, range, no LoS). ## GM commands retrofit Commands now return `CommandResult` (`Feedback` | `NeedsTarget`): - **spawn** `/spawn rat` → target a **tile** (rejected if blocked). - **kill** `/kill` → target a **creature** (LoS + within view) — replaces the old client-side `_attackTarget` hack. - **resurrect** `/resurrect` → target a **dead player** (ghost). ## Architecture / invariants `World` stays a thin facade — targeting lives in the command pipeline + a pure `TargetValidator`; `World` only gained a thin `TryGetMobilePosition` read. The formula/spec is the client's intent; the server is authoritative. Off-tick. ## How it was tested `TileMap.HasLineOfSight` (clear / blocked / endpoints not tested); `TargetValidator` (kind / range / LoS / missing); command flow (`kill`/`spawn`/`resurrect` request a target, resume with a `ResolvedTarget`; blocked-tile rejected); protocol round-trips (`TargetRequest`/`TargetResponse`). **218 tests green**, zero warnings, CSharpier clean. `docs/gm-commands.md` updated. ## Needs your eyes (GUI) `/kill` → crosshair → click a rat → it dies. `/spawn rat` → click a tile → rat appears (click a wall → "blocked"). Die, then `/resurrect` → click your ghost → back at spawn. The crosshair is a **placeholder**; the real UO cursor gump is #19. ## Checklist - [x] `just lint` clean · [x] `just test` green (218) · [x] whole solution builds, 0 warnings · [x] multi-platform preserved · [x] tests added
feat: targeting system (cursor) — foundation for spells & GM commands (#32)
All checks were successful
ci / Lint & Test (pull_request) Successful in 2m3s
57d6bc288f
A general target-selection flow: an action returns NeedsTarget(TargetSpec) and
the server sends a TargetRequest; the client enters a crosshair cursor and replies
with a TargetResponse (a picked mobile, a tile, or cancel). The server validates
the pick against the request's spec — kind, range (Chebyshev), and line of sight
(TileMap.HasLineOfSight, Bresenham) — then re-dispatches the action with a typed,
polymorphic ResolvedTarget (MobileTarget / TileTarget). There is no universal
"target" type: each request declares which kind it accepts and its constraints,
and command-specific validity (alive/dead/walkable) lives in the command, not the
engine.

Commands return a CommandResult (Feedback | NeedsTarget). GM commands retrofit onto
it: spawn now targets a tile (rejected if blocked), kill targets a creature (LoS +
range), resurrect targets a dead player. World stays a thin facade (targeting logic
lives in the command pipeline + a pure TargetValidator; World only gained a thin
TryGetMobilePosition read). Client shows a placeholder crosshair; the real UO cursor
gump is a #19 follow-up.
marco merged commit 65f8e48a31 into main 2026-07-17 23:04:58 +02:00
marco deleted branch feat/targeting 2026-07-17 23:04:59 +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!37
No description provided.