F12 pauses simulation, shows modal LineEdit prompt, saves three files
to user://bug-reports/gauntlet-t{tick}-{timestamp}/: snapshot.json
(full ObserverSnapshot), render.txt (simplified client-side text
render), description.txt (tester notes + tick/room/stance metadata).
Esc cancels without saving. Double-activation guard prevents stacking.
BUG_REPORT action added to InputMapper with wire guard in SimBridge
(client-only, never sent to server). Dialog on ModalLayer (CL 30).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.7 KiB
GDScript
108 lines
3.7 KiB
GDScript
extends Node
|
|
|
|
# Semantic actions — NO raw key codes cross the bridge
|
|
# Movement uses hold-to-move (polled each frame in _process).
|
|
# Discrete actions (interact, stance, etc.) use press events (_unhandled_input).
|
|
# Composite diagonals: holding W+D simultaneously → northeast.
|
|
#
|
|
# Movement throttle: client-side rate limit per stance (D-053).
|
|
# Sprint=5/s, Walk=2.5/s, Careful=1.7/s, Crouch=1.25/s.
|
|
# Server cooldown is authoritative, but the client throttle prevents
|
|
# flooding and gives correct movement feel in test mode.
|
|
enum Action {
|
|
MOVE_NORTH, MOVE_NORTHEAST, MOVE_EAST, MOVE_SOUTHEAST,
|
|
MOVE_SOUTH, MOVE_SOUTHWEST, MOVE_WEST, MOVE_NORTHWEST,
|
|
INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE, UNPAUSE,
|
|
TOGGLE_STANCE_UP, TOGGLE_STANCE_DOWN,
|
|
BUG_REPORT, # #495: F12 WRONG button — client-only, not sent to server
|
|
}
|
|
|
|
var input_queue: Array[Dictionary] = []
|
|
|
|
# Minimum milliseconds between movement commands, per stance.
|
|
# Tuned so Walk feels like walking, Sprint feels fast but readable.
|
|
const MOVE_INTERVAL_MS := {
|
|
"Sprint": 200, # 5/sec — fast but trackable
|
|
"Walk": 400, # 2.5/sec — comfortable walking pace
|
|
"Careful": 600, # ~1.7/sec — deliberate, scanning
|
|
"Crouch": 800, # 1.25/sec — creeping
|
|
}
|
|
var _last_move_msec: int = 0
|
|
|
|
|
|
# Hold-to-move: poll held direction keys each frame, throttled by stance.
|
|
# Server-side cooldown (D-053) is authoritative; this prevents client flooding.
|
|
# D-064: movement suppressed during dialogue (walk-away handled by dialogue_box).
|
|
func _process(_delta: float) -> void:
|
|
if GameState.dialogue_active:
|
|
return
|
|
var dir := Vector2i.ZERO
|
|
if Input.is_action_pressed("move_north"):
|
|
dir.y -= 1
|
|
if Input.is_action_pressed("move_south"):
|
|
dir.y += 1
|
|
if Input.is_action_pressed("move_east"):
|
|
dir.x += 1
|
|
if Input.is_action_pressed("move_west"):
|
|
dir.x -= 1
|
|
|
|
if dir != Vector2i.ZERO:
|
|
var now := Time.get_ticks_msec()
|
|
var interval: int = MOVE_INTERVAL_MS.get(GameState.player_stance, 200)
|
|
if now - _last_move_msec >= interval:
|
|
_last_move_msec = now
|
|
var action: Action = _dir_to_action(dir)
|
|
input_queue.append({
|
|
"action": action,
|
|
"timestamp_msec": now,
|
|
})
|
|
|
|
|
|
# Discrete actions: fire once on key press (not held).
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
var action: Action = -1
|
|
|
|
if event.is_action_pressed("interact"):
|
|
action = Action.INTERACT
|
|
elif event.is_action_pressed("perception_mode"):
|
|
action = Action.USE_PERCEPTION_MODE
|
|
elif event.is_action_pressed("open_menu"):
|
|
action = Action.OPEN_MENU
|
|
elif event.is_action_pressed("pause"):
|
|
var tick_rate = GameState.game_time.get("tick_rate", "Full")
|
|
action = Action.UNPAUSE if tick_rate == "Paused" else Action.PAUSE
|
|
elif event.is_action_pressed("stance_up"):
|
|
action = Action.TOGGLE_STANCE_UP
|
|
elif event.is_action_pressed("stance_down"):
|
|
action = Action.TOGGLE_STANCE_DOWN
|
|
elif event.is_action_pressed("bug_report"):
|
|
action = Action.BUG_REPORT
|
|
|
|
if action != -1:
|
|
input_queue.append({
|
|
"action": action,
|
|
"timestamp_msec": Time.get_ticks_msec(),
|
|
})
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
func flush_queue() -> Array[Dictionary]:
|
|
var queue = input_queue.duplicate()
|
|
input_queue.clear()
|
|
return queue
|
|
|
|
|
|
# Map a direction vector to the corresponding movement Action.
|
|
# Handles all 8 directions via composite W+D, W+A, etc.
|
|
static func _dir_to_action(dir: Vector2i) -> Action:
|
|
match dir:
|
|
Vector2i(0, -1): return Action.MOVE_NORTH
|
|
Vector2i(1, -1): return Action.MOVE_NORTHEAST
|
|
Vector2i(1, 0): return Action.MOVE_EAST
|
|
Vector2i(1, 1): return Action.MOVE_SOUTHEAST
|
|
Vector2i(0, 1): return Action.MOVE_SOUTH
|
|
Vector2i(-1, 1): return Action.MOVE_SOUTHWEST
|
|
Vector2i(-1, 0): return Action.MOVE_WEST
|
|
Vector2i(-1, -1): return Action.MOVE_NORTHWEST
|
|
_: return Action.MOVE_NORTH
|