Files
security-game/src/game/ui/shift_review.gd
T
zwitschi c1cd4898e8
Full build / Import + headless tests (push) Successful in 36s
Full build / Full desktop build + optional release (push) Skipped
Close the 0.9.3 UI polish milestone
Add deterministic captures 14-21 for the remaining screen-design states, fix the panel/button layout issues they exposed, and record 21/21 compare and judge results. Bump the source to 0.9.3 and move the completed milestone to DONE.md.
2026-11-02 17:57:40 +01:00

179 lines
7.0 KiB
GDScript

class_name ShiftReview
extends CanvasLayer
## End-of-shift review screen (PLAN.md: Evidence and Body Camera, Shift Review).
const Strings := preload("res://src/core/strings.gd")
const UiArt := preload("res://src/game/ui/art.gd")
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
const UiFocus := preload("res://src/game/ui/ui_focus.gd")
signal restart_requested()
signal continue_requested()
signal timeline_requested(review: Dictionary)
## Kept so the timeline button can hand the full review back to the game manager.
var _review: Dictionary = {}
var report_label: Label
var _campaign_label: Label = null
var _continue_button: Button = null
var _timeline_button: Button = null
var _restart_button: Button = null
var _quit_button: Button = null
func _init() -> void:
name = "ShiftReview"
layer = 30
var root := Control.new()
root.name = "ReviewRoot"
root.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(root)
var art := UiArt.new()
if not art.add_screen_backdrop(root, "shift_review",
Rect2(0.172, 0.033, 0.59, 0.927)):
var dim := ColorRect.new()
dim.color = Color(0.04, 0.05, 0.08, 0.92)
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
root.add_child(dim)
var center_host := CenterContainer.new()
center_host.set_anchors_preset(Control.PRESET_FULL_RECT)
root.add_child(center_host)
var center := VBoxContainer.new()
center.custom_minimum_size = Vector2(760, 0)
center.alignment = BoxContainer.ALIGNMENT_CENTER
center.add_theme_constant_override("separation", 8)
center_host.add_child(center)
var title := Label.new()
title.text = Strings.lookup("review.title")
title.add_theme_font_size_override("font_size", 28)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
center.add_child(title)
report_label = Label.new()
report_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
report_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
report_label.custom_minimum_size = Vector2(720, 0)
center.add_child(report_label)
_campaign_label = Label.new()
_campaign_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_campaign_label.add_theme_color_override("font_color", Color(0.62, 0.8, 0.95))
center.add_child(_campaign_label)
_continue_button = Button.new()
_continue_button.name = "ContinueButton"
_continue_button.text = Strings.lookup("review.continue_campaign")
_continue_button.pressed.connect(func(): continue_requested.emit())
_continue_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(_continue_button)
_timeline_button = Button.new()
_timeline_button.name = "TimelineButton"
_timeline_button.text = Strings.lookup("review.timeline_button")
_timeline_button.pressed.connect(func(): timeline_requested.emit(_review))
_timeline_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(_timeline_button)
_restart_button = Button.new()
_restart_button.name = "ReturnMenuButton"
_restart_button.text = Strings.lookup("ui.review_return_menu")
_restart_button.pressed.connect(func(): restart_requested.emit())
_restart_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
center.add_child(_restart_button)
_quit_button = Button.new()
_quit_button.name = "QuitButton"
_quit_button.text = Strings.lookup("ui.quit_game")
_quit_button.pressed.connect(func(): get_tree().quit())
_quit_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
_style_review_button(_timeline_button)
_style_review_button(_continue_button)
_style_review_button(_restart_button)
_style_review_button(_quit_button)
center.add_child(_quit_button)
func _style_review_button(button: Button) -> void:
button.custom_minimum_size = Vector2(300, 46)
UiStyles.apply_button(button, 22)
func show_review(review: Dictionary, scenario: Resource, history: Array = [],
campaign_summary: Dictionary = {}) -> void:
_review = review
var reputation: Dictionary = review.get("reputation", {})
var incidents: Dictionary = review.get("incidents", {})
var crowd: Dictionary = review.get("crowd", {})
var radio: Dictionary = review.get("radio", {})
var outcomes: Array = review.get("responder_outcomes", [])
var investigations: Array = review.get("investigations", [])
var incident_list: Array = incidents.get("incidents", [])
var peaceful := 0
for inc in incident_list:
if inc.get("outcome", "") in ["peaceful", "left"]:
peaceful += 1
# Persisted-shift history (Save autoload): record count + best rating.
var history_line := ""
if history.size() > 0:
var best := -1.0
var past_grades: Array[String] = []
for entry in history:
var rep: Dictionary = entry.get("reputation", {})
var rating_v: float = float(rep.get("rating", 0.0))
best = maxf(best, rating_v)
var g: String = rep.get("grade", "?")
if not past_grades.has(g):
past_grades.append(g)
history_line = Strings.fmt("review.history", [
history.size(), best, ", ".join(past_grades)])
var lines: Array[String] = [
Strings.fmt("review.venue", [scenario.display_name if scenario else review.get("scenario_id", "")]),
Strings.fmt("review.policy", [scenario.policy_summary if scenario else "See venue policy."]),
Strings.fmt("review.grade", [reputation.get("grade", "?"), reputation.get("rating", 0.0)]),
history_line,
Strings.fmt("review.incidents", [incident_list.size(), peaceful]),
Strings.fmt("review.npcs", [review.get("spawned_count", 0)]),
Strings.fmt("review.radio", [radio.get("battery", 0.0), radio.get("emergency_cooldown_s", 0.0)]),
Strings.fmt("review.crowd", [crowd.get("mood", 0.0) * 100.0, str(crowd.get("panic", false))]),
Strings.fmt("review.responder", [", ".join(outcomes) if outcomes.size() > 0 else Strings.lookup("review.none")]),
Strings.fmt("review.investigations", [" | ".join(investigations) if investigations.size() > 0 else Strings.lookup("review.none")]),
"",
Strings.lookup("review.highlights"),
]
var mods: Array = reputation.get("modifiers", [])
var shown := 0
for mod in mods:
var delta: float = float(mod.get("delta", 0.0))
lines.append(Strings.fmt("review.modifier", [str(mod.get("category", "")), "%+.0f" % delta, str(mod.get("reason", ""))]))
shown += 1
if shown >= 12:
break
report_label.text = "\n".join(lines)
var next_venue := str(campaign_summary.get("next_venue", ""))
var campaign_complete := bool(campaign_summary.get("complete", false))
if _campaign_label:
if campaign_complete:
_campaign_label.text = Strings.lookup("review.campaign_complete")
elif next_venue != "":
_campaign_label.text = Strings.fmt("review.campaign_next", [
int(campaign_summary.get("shift_number", 1)) - 1,
next_venue.replace("_", " ").capitalize()])
else:
_campaign_label.text = ""
if _continue_button:
_continue_button.visible = not campaign_complete and next_venue != ""
visible = true
var buttons: Array = [_continue_button, _timeline_button, _restart_button, _quit_button]
UiFocus.link(buttons)
var first := UiFocus.first_enabled(buttons)
if first:
first.grab_focus()
func _unhandled_input(event: InputEvent) -> void:
if not visible:
return
if event.is_action_pressed("ui_cancel"):
restart_requested.emit()
get_viewport().set_input_as_handled()