feat(client): hub teleport UX — Home key, fade transition (#501)

Home key sends TeleportToHub in Gauntlet mode. Camera snaps to hub
spawn with 0.3s fade-from-black. Clears dialogue/monologue buffers
on teleport. Teleport detection uses distance threshold (>5 tiles)
so future teleport types get the transition for free.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 08:46:02 +01:00
co-authored by Claude Opus 4.6
parent fd2101368e
commit 7274151671
5 changed files with 314 additions and 0 deletions
+4
View File
@@ -20,6 +20,7 @@ enum Action {
TOGGLE_STANCE_UP, TOGGLE_STANCE_DOWN,
BUG_REPORT, # #495: F12 WRONG button — client-only, not sent to server
SET_FACING, # D-054: facing octant update (no movement)
TELEPORT_HUB, # #501: Home key — teleport to hub (Gauntlet-only)
}
var input_queue: Array[Dictionary] = []
@@ -105,6 +106,9 @@ func _unhandled_input(event: InputEvent) -> void:
action = Action.TOGGLE_STANCE_DOWN
elif event.is_action_pressed("bug_report"):
action = Action.BUG_REPORT
elif event.is_action_pressed("teleport_hub"):
if GameState.gauntlet_mode:
action = Action.TELEPORT_HUB
if action != -1:
input_queue.append({
+7
View File
@@ -269,6 +269,8 @@ static func _action_enum_to_wire(action: int) -> String:
return "" # Client-only action (#495), not part of wire protocol
InputMapper.Action.SET_FACING:
return "SetFacing" # D-054: facing octant update (no movement)
InputMapper.Action.TELEPORT_HUB:
return "TeleportToHub" # #501: hub teleport (Gauntlet-only)
_:
push_warning("SimBridge: unknown action enum %s" % action)
return ""
@@ -284,6 +286,11 @@ func _test_snapshot() -> Dictionary:
# Process queued inputs
for action_name in _test_input_queue:
if action_name == "TeleportToHub":
# #501: Reset to hub spawn position, clear dialogue
_test_player_pos = Vector2i(10, 10)
_test_in_dialogue = false
continue
if action_name == "Interact":
# Mock dialogue trigger (#434): if near NPC, start dialogue
var npc_pos := Vector2i(12, 9)
+38
View File
@@ -62,8 +62,13 @@ func _process(_delta: float) -> void:
# Main game loop: poll snapshot, apply state, flush input
var snapshot: Variant = SimBridge.poll_snapshot()
if snapshot != null:
var old_pos := GameState.player_position
GameState.apply_snapshot(snapshot)
# #501: Detect teleport (large position jump > 5 tiles) and trigger fade
if _camera_anchored and _detect_teleport(old_pos, GameState.player_position):
_teleport_transition()
# Late anchor: live mode — first snapshot arrives during _process.
# Smoothing is already OFF (disabled in _ready), so setting
# global_position takes effect immediately with no lerp.
@@ -230,6 +235,39 @@ func _on_connection_state_changed(old_state: SimBridge.ConnectionState, new_stat
gauntlet_hud.finalize()
# #501: Detect large position jump indicating a teleport (not normal movement).
func _detect_teleport(old_pos: Vector2, new_pos: Vector2) -> bool:
return old_pos.distance_to(new_pos) > 5.0
# #501: Hub teleport transition — snap camera + 0.3s fade-from-black.
# Clears dialogue/monologue/interaction state (server clears its side too).
func _teleport_transition() -> void:
# Snap camera: disable smoothing, force re-anchor
camera.position_smoothing_enabled = false
camera.global_position = GameState.player_position * Constants.TILE_SIZE
_camera_anchored = true
# Clear client-side buffers
GameState.current_monologue = null
GameState.current_dialogue = null
GameState.dialogue_active = false
if dialogue_box and dialogue_box.is_dialogue_active():
dialogue_box.hide_dialogue()
# Fade from black: instant black overlay, fades to transparent over 0.3s
if _flash_rect and is_instance_valid(_flash_rect):
_flash_rect.queue_free()
_flash_rect = ColorRect.new()
_flash_rect.color = Color(0, 0, 0, 1.0)
_flash_rect.anchors_preset = Control.PRESET_FULL_RECT
_flash_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE
$UILayer.add_child(_flash_rect)
var tween := create_tween()
tween.tween_property(_flash_rect, "color:a", 0.0, 0.3)
tween.tween_callback(_flash_rect.queue_free)
# #502: Full-screen color flash — fades from color to transparent over duration.
# Used for room reset amber flash. Creates ephemeral ColorRect on UILayer.
func _screen_flash(color: Color, duration: float) -> void: