Fix aiming under a scrolling camera; add status and decisions docs
ci / verify (push) Successful in 46s

The aim bug was collateral from the camera work. "Mouse relative to the centre
of the screen" WAS the cursor's world position while the world was drawn fixed
at the origin, so subtracting the player position gave the right vector. Once
the camera scrolled, that expression became the aim vector itself, and
subtracting the player position again made the ship aim at a fixed world
location -- walking around swung the crosshair with the mouse held still.

Fixed by inverting the transform the view actually draws with (world = screen -
world_view.position, published by the game scene each frame) rather than
assuming the player is centred, so it still holds if the camera later clamps at
map edges or gets shake or look-ahead. tests/unit/test_aim.gd pins it, including
the regression directly: moving the player must not move the crosshair.

Documentation, for other sessions picking this up cold:
- docs/ROADMAP.md rewritten as the status map -- every feature in the brief
  against its state and the file implementing it, the known gaps called out
  (actor interest management is the notable one), and the ten design questions
  that are genuinely unspecified and should not be guessed at.
- docs/DECISIONS.md, new: settled decisions with their reasoning, so a session
  does not re-litigate or re-ask. Several are not the obvious default -- no
  i-frames, no contact damage, non-interruptible escape, and never sending the
  map seed.
- CLAUDE.md and README point at both.

137 tests; check.sh, test.sh and smoke.sh pass.
This commit is contained in:
2026-09-03 20:58:03 +02:00
parent 7af439341d
commit de48afcbd9
8 changed files with 342 additions and 54 deletions
+15 -2
View File
@@ -57,6 +57,19 @@ var cleared_countdown: int = Protocol.COUNTDOWN_NONE
## Where this world's dungeon portal is. Per-map now, so it has to be told.
var portal_pos := Vector2.ZERO
## Offset the view applies when drawing the world: screen = world + this.
## Published by the game scene every frame, rather than assumed, so aiming
## stays correct if the camera ever stops being exactly centred on the player
## (clamping at map edges, screen shake, a look-ahead offset).
var camera_offset := Vector2.ZERO
## Screen point to world point. The camera scrolls now, so this is no longer
## "relative to the middle of the screen" -- treating it as such made the ship
## aim at a fixed world location instead of at the cursor.
static func screen_to_world(screen: Vector2, offset: Vector2) -> Vector2:
return screen - offset
## Backstop for input-numbering drift: if the server stops acknowledging new
## inputs, our tick numbering has fallen outside its acceptance window and no
## amount of waiting fixes it. Counted in snapshots, not ticks.
@@ -133,8 +146,8 @@ func _sample_input() -> InputFrame:
var dead_buttons := InputFrame.BTN_INTERACT if request_respawn else 0
return InputFrame.make(input_tick, Vector2.ZERO, aim, dead_buttons)
var move := Input.get_vector("move_left", "move_right", "move_up", "move_down")
var mouse := get_viewport().get_mouse_position() - get_viewport().get_visible_rect().size * 0.5
var to_mouse := mouse - predicted_pos
var world_mouse := screen_to_world(get_viewport().get_mouse_position(), camera_offset)
var to_mouse := world_mouse - predicted_pos
if to_mouse.length_squared() > 1.0:
aim = to_mouse.angle()
var buttons := 0
+5
View File
@@ -32,6 +32,11 @@ func _follow_camera() -> void:
if _bound != null:
focus = _bound.predicted_pos
world_view.position = _screen_centre - focus
if _bound != null:
# The client converts cursor position to world space with this, so it
# has to come from the transform actually used to draw, not from a
# second assumption about where the camera is.
_bound.camera_offset = world_view.position
func _process(_delta: float) -> void: