de48afcbd9
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.
72 lines
2.2 KiB
GDScript
72 lines
2.2 KiB
GDScript
extends Node2D
|
|
## Root of the playable client scene. Wires the view and HUD to whatever client
|
|
## runtime [Net] currently has, and survives that runtime being null (during
|
|
## connection) or replaced (on reconnect).
|
|
|
|
@onready var world_view: Node2D = $WorldView
|
|
@onready var hud: CanvasLayer = $HUD
|
|
@onready var menu: CanvasLayer = $GameMenu
|
|
|
|
var _bound: ClientRuntime = null
|
|
|
|
|
|
func _ready() -> void:
|
|
_recentre()
|
|
get_viewport().size_changed.connect(_recentre)
|
|
menu.return_to_hub_requested.connect(_on_return_to_hub)
|
|
menu.disconnect_requested.connect(_on_disconnect)
|
|
hud.respawn_pressed.connect(_on_respawn_pressed)
|
|
|
|
|
|
var _screen_centre := Vector2.ZERO
|
|
|
|
|
|
func _recentre() -> void:
|
|
_screen_centre = get_viewport_rect().size * 0.5
|
|
|
|
|
|
## Scroll the world so the local player stays centred. Dungeons are larger than
|
|
## the viewport now, so a fixed camera would simply lose the player off-screen.
|
|
func _follow_camera() -> void:
|
|
var focus := Vector2.ZERO
|
|
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:
|
|
if Net.client != _bound:
|
|
_bound = Net.client
|
|
if _bound != null and not _bound.local_hit.is_connected(_on_local_hit):
|
|
_bound.local_hit.connect(_on_local_hit)
|
|
_follow_camera()
|
|
menu.set_in_dungeon(_bound != null
|
|
and _bound.instance_kind == Protocol.InstanceKind.DUNGEON)
|
|
|
|
|
|
## Routed through the same held-escape channel the F key uses, rather than a
|
|
## direct "teleport me" message -- the server has no such message, and adding
|
|
## one would hand clients an instant, uninterruptible exit.
|
|
func _on_return_to_hub() -> void:
|
|
if _bound != null:
|
|
_bound.request_escape = true
|
|
|
|
|
|
func _on_disconnect() -> void:
|
|
Net.shutdown()
|
|
|
|
|
|
## Sets the intent bit; the server decides whether the lockout has expired.
|
|
func _on_respawn_pressed() -> void:
|
|
if _bound != null:
|
|
_bound.request_respawn = true
|
|
|
|
|
|
func _on_local_hit(_damage: int) -> void:
|
|
hud.flash_hit()
|