|
|
|
@@ -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
|