feat(client): add WRONG button F12 bug report capture (#495)
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>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
extends Control
|
||||
|
||||
## #495: WRONG button (F12) MVP — bug report capture dialog.
|
||||
## On F12: pause sim, show one-line prompt, save snapshot + render + description, unpause.
|
||||
## Output: user://bug-reports/gauntlet-t{tick}-{timestamp}/
|
||||
|
||||
signal capture_completed
|
||||
signal capture_cancelled
|
||||
|
||||
const BG_COLOR := Color(0.05, 0.05, 0.08, 0.85)
|
||||
const BORDER_COLOR := Color("#4a9ebb")
|
||||
const TEXT_COLOR := Color("#c8d0e0")
|
||||
const FONT_SIZE := 14
|
||||
const LABEL_FONT_SIZE := 13
|
||||
const BOX_WIDTH := 500
|
||||
const BOX_HEIGHT := 120
|
||||
const PADDING := 16
|
||||
|
||||
var _line_edit: LineEdit = null
|
||||
var _active: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
visible = false
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
|
||||
func start_capture() -> void:
|
||||
if _active:
|
||||
return
|
||||
_active = true
|
||||
visible = true
|
||||
|
||||
# Pause the simulation
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.PAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
|
||||
# Create the LineEdit dynamically
|
||||
_line_edit = LineEdit.new()
|
||||
_line_edit.placeholder_text = "Describe the issue..."
|
||||
_line_edit.size = Vector2(BOX_WIDTH - PADDING * 2, 30)
|
||||
_line_edit.position = Vector2(
|
||||
(get_viewport_rect().size.x - BOX_WIDTH) / 2.0 + PADDING,
|
||||
(get_viewport_rect().size.y - BOX_HEIGHT) / 2.0 + 50
|
||||
)
|
||||
_line_edit.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
_line_edit.text_submitted.connect(_on_text_submitted)
|
||||
add_child(_line_edit)
|
||||
_line_edit.grab_focus()
|
||||
|
||||
|
||||
func _on_text_submitted(text: String) -> void:
|
||||
_save_report(text)
|
||||
_close()
|
||||
capture_completed.emit()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not _active:
|
||||
return
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
_close()
|
||||
capture_cancelled.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _close() -> void:
|
||||
_active = false
|
||||
visible = false
|
||||
if _line_edit:
|
||||
_line_edit.queue_free()
|
||||
_line_edit = null
|
||||
|
||||
# Unpause the simulation
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.UNPAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
|
||||
|
||||
func _save_report(description: String) -> void:
|
||||
var tick := GameState.current_tick
|
||||
var timestamp := Time.get_datetime_string_from_system().replace(":", "-").replace("T", "_")
|
||||
var dir_name := "gauntlet-t%d-%s" % [tick, timestamp]
|
||||
var base_path := "user://bug-reports/" + dir_name
|
||||
|
||||
# Ensure directory exists
|
||||
DirAccess.make_dir_recursive_absolute(base_path)
|
||||
|
||||
# 1. snapshot.json — full current snapshot as JSON
|
||||
var snapshot_path := base_path + "/snapshot.json"
|
||||
var snapshot_file := FileAccess.open(snapshot_path, FileAccess.WRITE)
|
||||
if snapshot_file:
|
||||
snapshot_file.store_string(JSON.stringify(GameState.current_snapshot, "\t"))
|
||||
snapshot_file.close()
|
||||
|
||||
# 2. render.txt — simplified client-side text render of snapshot
|
||||
var render_path := base_path + "/render.txt"
|
||||
var render_file := FileAccess.open(render_path, FileAccess.WRITE)
|
||||
if render_file:
|
||||
render_file.store_string(_render_snapshot_text())
|
||||
render_file.close()
|
||||
|
||||
# 3. description.txt — tester description + metadata
|
||||
var desc_path := base_path + "/description.txt"
|
||||
var desc_file := FileAccess.open(desc_path, FileAccess.WRITE)
|
||||
if desc_file:
|
||||
desc_file.store_string("Description: %s\n" % description)
|
||||
desc_file.store_string("Tick: %d\n" % tick)
|
||||
desc_file.store_string("Room: %s\n" % str(GameState.room_id if GameState.room_id else "none"))
|
||||
desc_file.store_string("Stance: %s\n" % GameState.player_stance)
|
||||
desc_file.store_string("Facing: %s\n" % GameState.player_facing)
|
||||
desc_file.store_string("Position: %s\n" % str(GameState.player_position))
|
||||
desc_file.store_string("Timestamp: %s\n" % Time.get_datetime_string_from_system())
|
||||
desc_file.close()
|
||||
|
||||
print("BugReport: saved to %s" % base_path)
|
||||
|
||||
|
||||
## Simplified client-side text render of the current snapshot.
|
||||
## MVP version — full fidelity via server's format_snapshot_text() is a stretch goal.
|
||||
func _render_snapshot_text() -> String:
|
||||
var lines: PackedStringArray = []
|
||||
lines.append("=== Snapshot t%d ===" % GameState.current_tick)
|
||||
lines.append("Player: %s facing %s (%s)" % [
|
||||
str(GameState.player_position), GameState.player_facing, GameState.player_stance])
|
||||
|
||||
if GameState.game_time.size() > 0:
|
||||
lines.append("Time: day %s, %s, %s" % [
|
||||
str(GameState.game_time.get("day", "?")),
|
||||
str(GameState.game_time.get("day_phase", "?")),
|
||||
str(GameState.game_time.get("tick_rate", "?"))])
|
||||
|
||||
lines.append("")
|
||||
lines.append("Entities (%d):" % GameState.visible_entities.size())
|
||||
for entity in GameState.visible_entities:
|
||||
var kind_str: String = ""
|
||||
if entity.has("kind") and entity.kind is Dictionary:
|
||||
kind_str = entity.kind.get("variant", "?")
|
||||
elif entity.has("kind") and entity.kind is String:
|
||||
kind_str = entity.kind
|
||||
var vis: String = entity.get("visibility", "?")
|
||||
lines.append(" #%s %s at (%s, %s) [%s]" % [
|
||||
str(entity.get("entity_id", "?")),
|
||||
kind_str,
|
||||
str(entity.get("x", "?")),
|
||||
str(entity.get("y", "?")),
|
||||
vis])
|
||||
|
||||
lines.append("")
|
||||
lines.append("Visible tiles: %d" % GameState.visible_tiles.size())
|
||||
|
||||
if GameState.current_monologue != null:
|
||||
lines.append("Monologue: %s" % str(GameState.current_monologue.get("text", "")))
|
||||
if GameState.current_dialogue != null:
|
||||
lines.append("Dialogue: %s says '%s'" % [
|
||||
str(GameState.current_dialogue.get("npc_name", "?")),
|
||||
str(GameState.current_dialogue.get("speech", ""))])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if not _active:
|
||||
return
|
||||
var viewport_size := get_viewport_rect().size
|
||||
# Full-screen dim
|
||||
draw_rect(Rect2(Vector2.ZERO, viewport_size), BG_COLOR)
|
||||
|
||||
# Center box
|
||||
var box_pos := Vector2(
|
||||
(viewport_size.x - BOX_WIDTH) / 2.0,
|
||||
(viewport_size.y - BOX_HEIGHT) / 2.0
|
||||
)
|
||||
var box_rect := Rect2(box_pos, Vector2(BOX_WIDTH, BOX_HEIGHT))
|
||||
draw_rect(box_rect, Color(0.08, 0.08, 0.12, 0.95))
|
||||
draw_rect(box_rect, BORDER_COLOR, false, 1.0)
|
||||
|
||||
# Title
|
||||
var font := ThemeDB.fallback_font
|
||||
draw_string(font,
|
||||
box_pos + Vector2(PADDING, 24),
|
||||
"WRONG — Describe the issue (Enter to save, Esc to cancel):",
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, LABEL_FONT_SIZE, TEXT_COLOR)
|
||||
|
||||
|
||||
# -- Public API ---------------------------------------------------------------
|
||||
|
||||
func is_active() -> bool:
|
||||
return _active
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/bug_report_dialog.gd" id="1_bugreport"]
|
||||
|
||||
; #495: WRONG button (F12) — bug report capture dialog, ModalLayer
|
||||
[node name="BugReportDialog" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_bugreport")
|
||||
Reference in New Issue
Block a user