Room reset (#502): amber reset_plate tile type in TileRenderer, 0.15s screen flash on room_reset monologue, 'Reset Room' verb via existing nearby_interactions. Insert pause (#518, D-058): explicit PauseSimulation on insert open, ResumeSimulation on close. Replaces toggle-style pause with idempotent pair per D-058. Auto-checklist (#503): ChecklistEvaluator parses room YAML, evaluates 7 condition types against GameState with latching. ChecklistOverlay renders progress in gauntlet mode only. 48 tests covering parser, evaluation, latching, visibility, and integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
extends Control
|
||||
|
||||
## #503: Auto-checklist HUD overlay — shows condition progress in Gauntlet mode.
|
||||
## Renders below the GauntletHUD timer. Each condition shows a check/dash + description.
|
||||
## Only visible in gauntlet_mode. Latched conditions stay checked.
|
||||
##
|
||||
## Spec ref: D-030 (testability), #503, Sprint 10 Completion Proof.
|
||||
|
||||
const _ChecklistEvaluator = preload("res://scripts/checklist/checklist_evaluator.gd")
|
||||
|
||||
const BG_COLOR := Color(0.05, 0.05, 0.08, 0.45)
|
||||
const MET_COLOR := Color("#6bc9a6") # Friendly green — condition met
|
||||
const UNMET_COLOR := Color("#8890a0") # Dim grey — condition pending
|
||||
const HEADER_COLOR := Color("#c8d0e0") # Insert text color — header/summary
|
||||
const COMPLETE_COLOR := Color("#e8c547") # Amber — all conditions met
|
||||
const FONT_SIZE := 11
|
||||
const LINE_HEIGHT := 16
|
||||
const PADDING := Vector2(8, 6)
|
||||
const MAX_DESC_CHARS := 52 # Truncate long descriptions
|
||||
|
||||
var _evaluator = null # ChecklistEvaluator instance
|
||||
var _last_room_id: Variant = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
visible = false
|
||||
_evaluator = _ChecklistEvaluator.new()
|
||||
|
||||
|
||||
func update_from_state() -> void:
|
||||
if not GameState.gauntlet_mode:
|
||||
if visible:
|
||||
visible = false
|
||||
return
|
||||
|
||||
if not visible:
|
||||
visible = true
|
||||
|
||||
var room_id: Variant = GameState.room_id
|
||||
if room_id == null:
|
||||
if _evaluator.is_loaded():
|
||||
_evaluator.reset()
|
||||
_last_room_id = null
|
||||
queue_redraw()
|
||||
return
|
||||
|
||||
# Load checklist on room change
|
||||
if room_id != _last_room_id:
|
||||
_evaluator.load_room(str(room_id))
|
||||
_last_room_id = room_id
|
||||
|
||||
# Evaluate conditions against current snapshot
|
||||
_evaluator.evaluate()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if _evaluator == null or not _evaluator.is_loaded():
|
||||
return
|
||||
|
||||
var font := get_theme_default_font()
|
||||
var results: Array = _evaluator.get_results()
|
||||
if results.is_empty():
|
||||
return
|
||||
|
||||
var met_count: int = _evaluator.get_met_count()
|
||||
var total_count: int = _evaluator.get_total_count()
|
||||
var all_complete: bool = _evaluator.is_complete()
|
||||
|
||||
# Header line: "CHECK: 5/8"
|
||||
var header_text := "CHECK: %d/%d" % [met_count, total_count]
|
||||
var header_color: Color = COMPLETE_COLOR if all_complete else HEADER_COLOR
|
||||
|
||||
# Calculate box height: header + one line per condition + padding
|
||||
var line_count: int = 1 + results.size()
|
||||
var box_height: float = PADDING.y * 2 + line_count * LINE_HEIGHT
|
||||
|
||||
# Calculate box width from longest line
|
||||
var max_width: float = font.get_string_size(header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
for r in results:
|
||||
var desc: String = r.get("description", r.get("id", ""))
|
||||
if desc.length() > MAX_DESC_CHARS:
|
||||
desc = desc.substr(0, MAX_DESC_CHARS - 1) + "..."
|
||||
var prefix: String = "[x] " if r.get("met", false) else "[ ] "
|
||||
var line_width: float = font.get_string_size(prefix + desc, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE).x
|
||||
if line_width > max_width:
|
||||
max_width = line_width
|
||||
|
||||
var box_width: float = max_width + PADDING.x * 2
|
||||
|
||||
# Background
|
||||
draw_rect(Rect2(Vector2.ZERO, Vector2(box_width, box_height)), BG_COLOR)
|
||||
|
||||
# Header
|
||||
var y: float = PADDING.y + FONT_SIZE
|
||||
draw_string(font, Vector2(PADDING.x, y), header_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, header_color)
|
||||
|
||||
# Condition lines
|
||||
for r in results:
|
||||
y += LINE_HEIGHT
|
||||
var is_met: bool = r.get("met", false)
|
||||
var prefix: String = "[x] " if is_met else "[ ] "
|
||||
var desc: String = r.get("description", r.get("id", ""))
|
||||
if desc.length() > MAX_DESC_CHARS:
|
||||
desc = desc.substr(0, MAX_DESC_CHARS - 1) + "..."
|
||||
var color: Color = MET_COLOR if is_met else UNMET_COLOR
|
||||
draw_string(font, Vector2(PADDING.x, y), prefix + desc, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE, color)
|
||||
|
||||
|
||||
# -- Public API ---------------------------------------------------------------
|
||||
|
||||
func get_evaluator():
|
||||
return _evaluator
|
||||
|
||||
|
||||
func get_met_count() -> int:
|
||||
if _evaluator == null:
|
||||
return 0
|
||||
return _evaluator.get_met_count()
|
||||
|
||||
|
||||
func get_total_count() -> int:
|
||||
if _evaluator == null:
|
||||
return 0
|
||||
return _evaluator.get_total_count()
|
||||
|
||||
|
||||
func is_complete() -> bool:
|
||||
if _evaluator == null:
|
||||
return false
|
||||
return _evaluator.is_complete()
|
||||
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/checklist_overlay.gd" id="1_checklist"]
|
||||
|
||||
; #503: Auto-checklist overlay — below GauntletHUD timer, right-aligned
|
||||
[node name="ChecklistOverlay" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -420.0
|
||||
offset_top = 78.0
|
||||
offset_right = -16.0
|
||||
offset_bottom = 400.0
|
||||
grow_horizontal = 0
|
||||
mouse_filter = 2
|
||||
script = ExtResource("1_checklist")
|
||||
+12
-12
@@ -120,24 +120,24 @@ func _confirm_selection() -> void:
|
||||
|
||||
|
||||
func _activate_insert() -> void:
|
||||
# TODO(v7): replace PAUSE toggle with dedicated ToggleInsert action in protocol
|
||||
# #518/D-058: Send PauseSimulation when insert opens. Idempotent —
|
||||
# if already paused (e.g. Gauntlet interlude), server ignores duplicate.
|
||||
if not _insert_active:
|
||||
_insert_active = true
|
||||
_send_pause()
|
||||
|
||||
|
||||
func _send_pause() -> void:
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.PAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.PAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
|
||||
|
||||
func deactivate_insert() -> void:
|
||||
# Called when closing insert view — send Pause again (toggle)
|
||||
# #518/D-058: Send ResumeSimulation when insert closes.
|
||||
if _insert_active:
|
||||
_insert_active = false
|
||||
_send_pause()
|
||||
SimBridge.send_input({
|
||||
"action": InputMapper.Action.UNPAUSE,
|
||||
"timestamp_msec": Time.get_ticks_msec(),
|
||||
})
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
@@ -174,7 +174,7 @@ func _draw() -> void:
|
||||
|
||||
# Label
|
||||
var label: String = SPOKE_NAMES.get(spoke, "")
|
||||
var font := ThemeDB.fallback_font
|
||||
var font := get_theme_default_font()
|
||||
var text_size := font.get_string_size(label, HORIZONTAL_ALIGNMENT_CENTER, -1, 11)
|
||||
var label_pos := icon_center + Vector2(-text_size.x / 2.0, ICON_SIZE + 14.0)
|
||||
draw_string(font, label_pos, label, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, color)
|
||||
|
||||
Reference in New Issue
Block a user