Fix bullet/ship desync; rework death, escape, arrival and hub awareness
(1) Bullets appeared to trail the ship. Two independent causes, measured with
the new tools/diag_prediction.gd rather than guessed at:
- ServerRuntime ticked before ClientRuntime, so input sampled on frame N was
not consumed until frame N+1, leaving the drawn ship a constant one tick
(4.00px at 240 u/s) ahead of the authoritative one that bullets spawn from.
ClientRuntime now sets process_physics_priority = -10. Gap on a listen
server: 4.00px -> 0.10px mean, 0.30px worst.
- PLAYER_MUZZLE_OFFSET was PLAYER_RADIUS + 6 = 12px against a 13px drawn
ship, so bullets were born inside the sprite. Regression from the previous
commit's hitbox shrink; it now derives from PLAYER_VISUAL_RADIUS.
(2) No more timed respawn. A downed player stays down until they ask for the
hub (E), which is an ordinary input -- the server has no "revive me" message.
(3) Escape channel 3s -> 1s, and damage no longer cancels it. An interruptible
channel makes killing the process strictly better than using the button, so a
dropped connection now runs the same channel: the player stays in the world as
linkdead, still killable, and is only released once it completes. Instances
refuse to close while a linkdead body is resolving, or a solo drop would delete
it on the next tick and hand the exploit straight back.
(4) Escape opens an in-game menu: return to hub (routed through the same held-
escape channel, not a new message), disconnect, quit.
(5) Server pushes a roster so the hub shows who is online and which dungeon
they are in. Entering a dungeon grants 2s arrival protection -- invulnerable
AND weapons-cold, since invulnerability alone would make the spawn a free
firing position -- flagged in the snapshot and drawn on every protected ship.
(6) Cleared dungeons hold the party 30s (was 5s) with a visible countdown.
(7) The hub's grey circle was a 100k-HP target dummy that read as scenery. Now
drawn as a bullseye so its purpose is legible.
Protocol version 1 -> 2. 91 tests (was 78); smoke.sh gains a bot that is
SIGKILLed mid-dungeon to prove the disconnect path end to end. check.sh,
test.sh and smoke.sh all pass.
This commit is contained in:
@@ -5,6 +5,7 @@ extends Node2D
|
||||
|
||||
@onready var world_view: Node2D = $WorldView
|
||||
@onready var hud: CanvasLayer = $HUD
|
||||
@onready var menu: CanvasLayer = $GameMenu
|
||||
|
||||
var _bound: ClientRuntime = null
|
||||
|
||||
@@ -12,6 +13,8 @@ var _bound: ClientRuntime = null
|
||||
func _ready() -> void:
|
||||
world_view.position = get_viewport_rect().size * 0.5
|
||||
get_viewport().size_changed.connect(_recentre)
|
||||
menu.return_to_hub_requested.connect(_on_return_to_hub)
|
||||
menu.disconnect_requested.connect(_on_disconnect)
|
||||
|
||||
|
||||
func _recentre() -> void:
|
||||
@@ -23,6 +26,20 @@ func _process(_delta: float) -> void:
|
||||
_bound = Net.client
|
||||
if _bound != null and not _bound.local_hit.is_connected(_on_local_hit):
|
||||
_bound.local_hit.connect(_on_local_hit)
|
||||
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()
|
||||
|
||||
|
||||
func _on_local_hit(_damage: int) -> void:
|
||||
|
||||
+32
-2
@@ -11,6 +11,8 @@ const COL_LOCAL := Color(0.5, 1.0, 0.8)
|
||||
const COL_REMOTE := Color(0.55, 0.75, 1.0)
|
||||
const COL_DEAD := Color(0.4, 0.4, 0.45, 0.5)
|
||||
const COL_PORTAL := Color(0.5, 0.9, 1.0)
|
||||
## EnemyDef.visual index for the hub's practice target.
|
||||
const VISUAL_DUMMY := 3
|
||||
const ENEMY_COLORS := [
|
||||
Color(0.95, 0.55, 0.55), # drifter
|
||||
Color(0.85, 0.7, 0.35), # turret
|
||||
@@ -71,10 +73,25 @@ func _draw_portal() -> void:
|
||||
func _draw_enemy(e: Dictionary) -> void:
|
||||
var col: Color = ENEMY_COLORS[clampi(int(e["visual"]), 0, ENEMY_COLORS.size() - 1)]
|
||||
var r: float = e["radius"]
|
||||
if int(e["visual"]) == VISUAL_DUMMY:
|
||||
_draw_target_dummy(e["pos"], r, col)
|
||||
return
|
||||
draw_circle(e["pos"], r, Color(col, 0.35))
|
||||
draw_arc(e["pos"], r, 0.0, TAU, 24, col, 2.0)
|
||||
|
||||
|
||||
## The hub dummy is a shooting-range target, not an enemy. It used to be drawn
|
||||
## as a plain grey disc with 100k hit points, which read as scenery -- nothing
|
||||
## about it said "shoot me" and nothing visibly happened when you did. Concentric
|
||||
## rings make the intent obvious at a glance.
|
||||
func _draw_target_dummy(pos: Vector2, r: float, col: Color) -> void:
|
||||
draw_circle(pos, r, Color(col, 0.18))
|
||||
for i in 3:
|
||||
var ring := r * (1.0 - 0.3 * float(i))
|
||||
draw_arc(pos, ring, 0.0, TAU, 24, Color(col, 0.5 + 0.15 * float(i)), 1.5)
|
||||
draw_circle(pos, r * 0.12, Color(1.0, 0.5, 0.4, 0.9))
|
||||
|
||||
|
||||
func _draw_boss() -> void:
|
||||
var b := client.boss_state()
|
||||
if b.is_empty():
|
||||
@@ -87,10 +104,13 @@ func _draw_boss() -> void:
|
||||
|
||||
|
||||
func _draw_remote_player(p: Dictionary) -> void:
|
||||
var alive := (int(p["flags"]) & Protocol.F_ALIVE) != 0
|
||||
var flags := int(p["flags"])
|
||||
var alive := (flags & Protocol.F_ALIVE) != 0
|
||||
var col := COL_REMOTE if alive else COL_DEAD
|
||||
_draw_ship(p["pos"], p["aim"], col, alive)
|
||||
if (int(p["flags"]) & Protocol.F_ESCAPING) != 0:
|
||||
if (flags & Protocol.F_SPAWN_GRACE) != 0:
|
||||
_draw_grace_ring(p["pos"])
|
||||
if (flags & Protocol.F_ESCAPING) != 0:
|
||||
_draw_escape_ring(p["pos"], float(p["escape"]), col)
|
||||
|
||||
|
||||
@@ -99,10 +119,20 @@ func _draw_local_player() -> void:
|
||||
_draw_ship(client.predicted_pos, client.aim, COL_DEAD, false)
|
||||
return
|
||||
_draw_ship(client.predicted_pos, client.aim, COL_LOCAL, true)
|
||||
if client.my_spawn_grace:
|
||||
_draw_grace_ring(client.predicted_pos)
|
||||
if client.my_escaping:
|
||||
_draw_escape_ring(client.predicted_pos, client.my_escape, COL_LOCAL)
|
||||
|
||||
|
||||
## Arrival protection. Drawn on every protected ship, not just your own, so it
|
||||
## is clear who can currently be shot and who cannot.
|
||||
func _draw_grace_ring(pos: Vector2) -> void:
|
||||
var pulse := 0.5 + 0.5 * sin(float(Time.get_ticks_msec()) * 0.012)
|
||||
draw_arc(pos, SimConfig.PLAYER_VISUAL_RADIUS + 5.0, 0.0, TAU, 28,
|
||||
Color(0.55, 0.85, 1.0, 0.35 + 0.45 * pulse), 2.0)
|
||||
|
||||
|
||||
## Drawn at PLAYER_VISUAL_RADIUS, larger than the PLAYER_RADIUS hitbox actually
|
||||
## used for hits -- see the comment on those constants in sim_config.gd. The
|
||||
## mismatch is deliberate, not a placeholder: a bullet can visibly clip the
|
||||
|
||||
Reference in New Issue
Block a user