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