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 var dir_err := DirAccess.make_dir_recursive_absolute(base_path) if dir_err != OK: push_error("BugReport: failed to create directory %s (error %d)" % [base_path, dir_err]) return var files_saved := 0 # 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() files_saved += 1 else: push_error("BugReport: failed to write %s" % snapshot_path) # 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() files_saved += 1 else: push_error("BugReport: failed to write %s" % render_path) # 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() files_saved += 1 else: push_error("BugReport: failed to write %s" % desc_path) push_warning("BugReport: saved %d/3 files to %s" % [files_saved, 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