Flash a pawn red when its move command fails #100

Closed
opened 2026-09-15 09:09:34 +01:00 by liamjd · 0 comments
Owner

Part of milestone #4. Split out of #83, which implemented the re-path itself but left the "flash it red" half of its design undone.

#83's design says: "If the re-path is empty (the building sealed the only route), stop the pawn and flash it red, the same feedback as an unreachable command." As merged (30b67c6), Pawns.repath_pawns() calls stop() then move_to(), and when move_to() returns false the pawn just stands there with no feedback — indistinguishable from a pawn that arrived.

What to build

A short, self-reverting tint on the pawn's view, triggered wherever a move command fails:

  • PawnView.flash(colour, duration) (or a narrower flash_rejected()) that tints the capsule and restores the original look when it ends. One Tween on the view, cancelled and restarted if a second flash lands while one is running, so repeated failed commands don't leave the pawn stuck red.
  • Pawns.repath_pawns() flashes the pawn when move_to() returns false after the re-path.
  • The same call on a directly issued move command that returns false — the "unreachable command" case #83 refers to. Which caller owns that depends on how #78's picker/move command lands; if it isn't in place yet, wire only the re-path case and note the other in a comment.

Implementation notes

  • Tint via MeshInstance3D.material_override, not by mutating the mesh's material. pawn.tscn's CapsuleMesh and its StandardMaterial3D are sub-resources shared by every instance of the scene, so writing albedo_color through mesh.material would flash all pawns red. (pawn_view.gd:24 already mutates the shared mesh's height for the same reason — worth a separate look, out of scope here.) Setting and then clearing material_override keeps it per-instance.
  • Duration wants to be wall-clock, not game-clock: feedback about a rejected command shouldn't stretch or freeze with GameClock. Use the raw delta/Tween, not GameClock.delta_for().
  • Keep it in the view. Pawn is a record; nothing about a transient tint belongs on it.
  • Godot's headless mode under gl_compatibility renders nothing, so unit tests can only assert the state, not the pixels.

AC

  • Unit test: a pawn mid-path, a building placed that seals the only route — the pawn stops and the view is left in its flashing state; after the duration elapses the override is cleared.
  • Unit test: two failed commands in quick succession leave the pawn back at its normal colour, not permanently tinted.
  • Headed check: seal a walking pawn's route with a building and see the flash. Screenshot mid-flash.
  • No orphans in the GUT summary (the Tween is owned by the view, not leaked).

Not in scope

Toast/UI messaging for the same failure — that's #57.

Part of milestone #4. Split out of #83, which implemented the re-path itself but left the "flash it red" half of its design undone. #83's design says: *"If the re-path is empty (the building sealed the only route), stop the pawn and flash it red, the same feedback as an unreachable command."* As merged (30b67c6), `Pawns.repath_pawns()` calls `stop()` then `move_to()`, and when `move_to()` returns `false` the pawn just stands there with no feedback — indistinguishable from a pawn that arrived. ## What to build A short, self-reverting tint on the pawn's view, triggered wherever a move command fails: - `PawnView.flash(colour, duration)` (or a narrower `flash_rejected()`) that tints the capsule and restores the original look when it ends. One `Tween` on the view, cancelled and restarted if a second flash lands while one is running, so repeated failed commands don't leave the pawn stuck red. - `Pawns.repath_pawns()` flashes the pawn when `move_to()` returns `false` after the re-path. - The same call on a directly issued move command that returns `false` — the "unreachable command" case #83 refers to. Which caller owns that depends on how #78's picker/move command lands; if it isn't in place yet, wire only the re-path case and note the other in a comment. ## Implementation notes - **Tint via `MeshInstance3D.material_override`, not by mutating the mesh's material.** `pawn.tscn`'s `CapsuleMesh` and its `StandardMaterial3D` are sub-resources shared by every instance of the scene, so writing `albedo_color` through `mesh.material` would flash *all* pawns red. (`pawn_view.gd:24` already mutates the shared mesh's `height` for the same reason — worth a separate look, out of scope here.) Setting and then clearing `material_override` keeps it per-instance. - Duration wants to be wall-clock, not game-clock: feedback about a rejected command shouldn't stretch or freeze with `GameClock`. Use the raw `delta`/`Tween`, not `GameClock.delta_for()`. - Keep it in the view. `Pawn` is a record; nothing about a transient tint belongs on it. - Godot's headless mode under `gl_compatibility` renders nothing, so unit tests can only assert the state, not the pixels. ## AC - Unit test: a pawn mid-path, a building placed that seals the only route — the pawn stops *and* the view is left in its flashing state; after the duration elapses the override is cleared. - Unit test: two failed commands in quick succession leave the pawn back at its normal colour, not permanently tinted. - Headed check: seal a walking pawn's route with a building and see the flash. Screenshot mid-flash. - No orphans in the GUT summary (the `Tween` is owned by the view, not leaked). ## Not in scope Toast/UI messaging for the same failure — that's #57.
Sign in to join this conversation.
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
liamjd/UntitledColonyBuilder#100
No description provided.