RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
results_screen_controller.gd
Go to the documentation of this file.
1extends RefCounted
2
3
6
7
8
9func create(summary:Dictionary, actions:Array = [], options:Dictionary = {}) -> Control:
10 var root = VBoxContainer.new()
11 root.name = str(options.get("name", "ResultsScreen"))
12 root.size_flags_horizontal = Control.SIZE_EXPAND_FILL
13 root.size_flags_vertical = Control.SIZE_EXPAND_FILL
14 root.custom_minimum_size = options.get("minimum_size", Vector2.ZERO)
15 root.add_theme_constant_override("separation", int(options.get("separation", 10)))
16 var title = Label.new()
17 title.name = "Title"
18 title.text = str(options.get("title", _title_for(summary)))
19 title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
20 title.add_theme_font_size_override("font_size", int(options.get("title_size", 22)))
21 root.add_child(title)
22 var message = Label.new()
23 message.name = "Message"
24 message.text = str(options.get("message", summary.get("result", {}).get("message", "")))
25 message.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
26 message.horizontal_alignment = int(options.get("message_alignment", HORIZONTAL_ALIGNMENT_LEFT))
27 message.visible = message.text != ""
28 var details_scroll = ScrollContainer.new()
29 details_scroll.name = "DetailsScroll"
30 details_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
31 details_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
32 details_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
33 root.add_child(details_scroll)
34 var details = VBoxContainer.new()
35 details.name = "Details"
36 details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
37 details.add_theme_constant_override("separation", int(options.get("detail_separation", 8)))
38 details_scroll.add_child(details)
39 details.add_child(message)
40 if bool(options.get("show_sections", true)):
41 details.add_child(_section("Counters", _format_counts(summary.get("counters", {}), "No counters recorded.")))
42 details.add_child(_section("Collected", _format_counts(summary.get("collected", {}), "Nothing collected.")))
43 details.add_child(_section("Rewards", _format_counts(summary.get("rewards", {}), "No rewards earned.")))
44 var objective_text = "%d / %d required objectives" % [
45 int(summary.get("completed_required_objectives", 0)),
46 int(summary.get("required_objectives", 0))
47 ]
48 details.add_child(_section("Objectives", objective_text))
49 var action_row = HBoxContainer.new()
50 action_row.name = "Actions"
51 action_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
52 action_row.add_theme_constant_override("separation", 8)
53 var free_on_action := bool(options.get("free_on_action", false))
54 for action in actions:
55 if not action is Dictionary:
56 continue
57 var button = Button.new()
58 button.name = str(action.get("name", _node_name(str(action.get("id", "Action")))))
59 button.text = str(action.get("label", action.get("id", "Action")))
60 button.disabled = bool(action.get("disabled", false))
61 button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
62 button.custom_minimum_size = Vector2(0, int(options.get("action_button_height", 44)))
63 var callback = action.get("callback", Callable())
64 button.pressed.connect(func():
65 _close_parent_popup(button, free_on_action)
66 if callback is Callable and callback.is_valid():
67 callback.call()
68 )
69 action_row.add_child(button)
70 root.add_child(action_row)
71 return root
72
73
74
75func create_popup(owner:Node, summary:Dictionary, actions:Array = [], options:Dictionary = {}) -> PopupPanel:
76 var popup = PopupPanel.new()
77 popup.name = str(options.get("popup_name", "ResultsPopup"))
78 popup.size = _coerce_size(options.get("size", Vector2i(360, 320)), Vector2i(360, 320))
79 var content_options = options.duplicate(true)
80 content_options["name"] = str(options.get("content_name", "Content"))
81 content_options["minimum_size"] = options.get("minimum_size", Vector2.ZERO)
82 var content = create(summary, actions, content_options)
83 content.set_anchors_preset(Control.PRESET_FULL_RECT)
84 content.offset_left = int(options.get("margin", 18))
85 content.offset_top = int(options.get("margin", 18))
86 content.offset_right = -int(options.get("margin", 18))
87 content.offset_bottom = -int(options.get("margin", 18))
88 content.custom_minimum_size = Vector2.ZERO
89 popup.add_child(content)
90 if owner != null:
91 owner.add_child(popup)
92 return popup
93
94
95func popup_centered(popup:PopupPanel, viewport_size:Vector2, options:Dictionary = {}) -> void:
96 if popup == null or not is_instance_valid(popup):
97 return
98 var popup_size = fit_popup_to_viewport(popup, viewport_size, options)
99 popup.size = popup_size
100 _fit_content(popup, popup_size, int(options.get("content_margin", 18)))
101 if popup.is_inside_tree():
102 popup.popup_centered(popup_size)
103
104
105
106func show_popup(owner:Node, summary:Dictionary, actions:Array = [], options:Dictionary = {}) -> PopupPanel:
107 var popup:PopupPanel = create_popup(owner, summary, actions, options)
108 var viewport_size:Vector2 = _coerce_vector2(options.get("viewport_size", Vector2.ZERO), Vector2.ZERO)
109 if viewport_size == Vector2.ZERO and owner != null and owner.is_inside_tree():
110 viewport_size = owner.get_viewport_rect().size
111 if viewport_size == Vector2.ZERO:
112 var fallback_size:Vector2i = _coerce_size(options.get("size", Vector2i(360, 320)), Vector2i(360, 320))
113 viewport_size = Vector2(float(fallback_size.x), float(fallback_size.y))
114 popup_centered(popup, viewport_size, options)
115 apply_theme_and_focus(
116 popup,
117 options.get("layout_manager", null),
118 options.get("settings_manager", null),
119 options.get("ui_flow_manager", null),
120 str(options.get("theme_id", "dark")),
121 options
122 )
123 return popup
124
125
126
127func fit_popup_to_viewport(popup:PopupPanel, viewport_size:Vector2, options:Dictionary = {}) -> Vector2i:
128 var requested = _coerce_size(options.get("size", popup.size), Vector2i(360, 320))
129 var margin := int(options.get("margin", 24))
130 var min_size = _coerce_size(options.get("minimum", Vector2i(260, 180)), Vector2i(260, 180))
131 var available = Vector2i(max(1, int(viewport_size.x) - margin), max(1, int(viewport_size.y) - margin))
132 var effective_min = Vector2i(min(min_size.x, available.x), min(min_size.y, available.y))
133 var popup_size = Vector2i(
134 clamp(requested.x, effective_min.x, available.x),
135 clamp(requested.y, effective_min.y, available.y)
136 )
137 popup.min_size = Vector2i(min(min_size.x, popup_size.x), min(min_size.y, popup_size.y))
138 return popup_size
139
140
141func _fit_content(popup:PopupPanel, popup_size:Vector2i, content_margin:int) -> void:
142 if popup == null or not is_instance_valid(popup):
143 return
144 var content = popup.get_node_or_null("Content")
145 if not content is Control:
146 return
147 content.set_anchors_preset(Control.PRESET_FULL_RECT)
148 content.offset_left = content_margin
149 content.offset_top = content_margin
150 content.offset_right = -content_margin
151 content.offset_bottom = -content_margin
152 content.custom_minimum_size = Vector2.ZERO
153 content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
154 content.size_flags_vertical = Control.SIZE_EXPAND_FILL
155 var details_scroll = content.get_node_or_null("DetailsScroll")
156 if details_scroll is Control:
157 details_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
158
159
160func apply_theme_and_focus(popup:PopupPanel, layout_manager, settings_manager = null, ui_flow_manager = null, theme_id:String = "dark", options:Dictionary = {}) -> void:
161 if popup == null or not is_instance_valid(popup):
162 return
163 if layout_manager is Object and layout_manager.has_method("apply_theme"):
164 layout_manager.apply_theme(popup, theme_id)
165 if settings_manager is Object and settings_manager.has_method("apply_text_size"):
166 settings_manager.apply_text_size(popup)
167 if not (ui_flow_manager is Object):
168 return
169 if options.has("focus_node") and ui_flow_manager.has_method("focus_named"):
170 ui_flow_manager.focus_named(popup, str(options.get("focus_node", "")))
171 elif options.has("focus_labels") and ui_flow_manager.has_method("focus_button_by_text"):
172 ui_flow_manager.focus_button_by_text(popup, options.get("focus_labels", []))
173 elif ui_flow_manager.has_method("focus_first"):
174 ui_flow_manager.focus_first(popup)
175
176
177func _title_for(summary:Dictionary) -> String:
178 var mode = str(summary.get("mode", "Run")).replace("_", " ").capitalize()
179 var status = str(summary.get("status", "complete")).replace("_", " ").capitalize()
180 return "%s %s" % [mode, status]
181
182
183func _section(title_text:String, body_text:String) -> Control:
184 var panel = VBoxContainer.new()
185 panel.name = _node_name(title_text)
186 panel.add_theme_constant_override("separation", 2)
187 var title = Label.new()
188 title.name = "Title"
189 title.text = title_text
190 title.add_theme_font_size_override("font_size", 16)
191 panel.add_child(title)
192 var body = Label.new()
193 body.name = "Body"
194 body.text = body_text
195 body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
196 panel.add_child(body)
197 return panel
198
199
200func _format_counts(counts, empty_label:String) -> String:
201 if not counts is Dictionary:
202 return empty_label
203 var parts := []
204 var keys = counts.keys()
205 keys.sort()
206 for key in keys:
207 var amount = int(counts[key])
208 if amount <= 0:
209 continue
210 parts.append("%dx %s" % [amount, str(key).replace("_", " ").capitalize()])
211 return empty_label if parts.is_empty() else ", ".join(parts)
212
213
214func _node_name(value:String) -> String:
215 var parts = value.replace("-", "_").replace(" ", "_").split("_")
216 var text := ""
217 for part in parts:
218 text += str(part).capitalize()
219 return text if text != "" else "Node"
220
221
222func _coerce_size(value, fallback:Vector2i) -> Vector2i:
223 if value is Vector2i:
224 return value
225 if value is Vector2:
226 return Vector2i(int(value.x), int(value.y))
227 if value is Dictionary:
228 return Vector2i(int(value.get("x", fallback.x)), int(value.get("y", fallback.y)))
229 return fallback
230
231
232func _coerce_vector2(value, fallback:Vector2) -> Vector2:
233 if value is Vector2:
234 return value
235 if value is Vector2i:
236 return Vector2(float(value.x), float(value.y))
237 if value is Dictionary:
238 return Vector2(float(value.get("x", fallback.x)), float(value.get("y", fallback.y)))
239 return fallback
240
241
242func _close_parent_popup(node:Node, should_free:bool = false) -> void:
243 if node == null or not is_instance_valid(node):
244 return
245 var current = node
246 while current != null:
247 if current is PopupPanel:
248 current.hide()
249 if should_free:
250 current.queue_free()
251 return
252 current = current.get_parent()