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()