RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
status_feed_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
11
12var messages := []
13var last_message := ""
14var node_prefix := "StatusFeed"
15
16
17func push(message:String, now_ms:int, options:Dictionary = {}) -> void:
18 message = _normalize(message)
19 if message == "":
20 return
21 if bool(options.get("dedupe_last", true)) and message == last_message:
22 return
23 last_message = message
24 var until_ms = now_ms + int(options.get("duration_ms", 3600))
25 for entry in messages:
26 if entry is Dictionary and str(entry.get("text", "")) == message:
27 entry["until"] = until_ms
28 return
29 messages.push_front({
30 "text": message,
31 "until": until_ms
32 })
33 var limit = int(options.get("history_limit", 4))
34 while messages.size() > limit:
35 messages.pop_back()
36
37
38func render(parent:Control, now_ms:int, rect:Rect2, options:Dictionary = {}) -> void:
39 if parent == null:
40 return
41 node_prefix = str(options.get("node_prefix", node_prefix))
42 expire(now_ms)
43 if messages.is_empty():
44 clear_nodes(parent)
45 return
46 var max_entries = int(options.get("max_entries", 4))
47 var gap = float(options.get("gap", 6.0))
48 var padding = float(options.get("padding", 8.0))
49 var line_height = float(options.get("line_height", 18.0))
50 var max_lines = int(options.get("max_lines", 4))
51 var font_size = int(options.get("font_size", 13))
52 var y = rect.position.y
53 var rendered_count = min(max_entries, messages.size())
54 for index in range(rendered_count):
55 var entry = messages[index]
56 if not entry is Dictionary:
57 continue
58 var wrapped = wrap_text(str(entry.get("text", "")), rect.size.x - padding * 2.0, options)
59 var height = padding * 2.0 + float(wrapped.get("line_count", 1)) * line_height
60 var alpha = max(float(options.get("min_alpha", 0.42)), float(options.get("alpha", 0.82)) - float(index) * float(options.get("alpha_step", 0.12)))
61 var bg = _panel_node(parent, _indexed_name("%sPanel" % node_prefix, index))
62 bg.position = Vector2(rect.position.x, y)
63 bg.size = Vector2(rect.size.x, height)
64 bg.color = Color(str(options.get("background", "#0a0d10")))
65 bg.color.a = alpha
66 bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
67 bg.visible = true
68 var label = _label_node(parent, _indexed_name("%sText" % node_prefix, index))
69 label.text = str(wrapped.get("text", ""))
70 label.position = bg.position + Vector2(padding, padding * 0.6)
71 label.size = Vector2(rect.size.x - padding * 2.0, height - padding)
72 label.clip_contents = true
73 label.autowrap_mode = TextServer.AUTOWRAP_OFF
74 label.max_lines_visible = max_lines
75 label.add_theme_font_size_override("font_size", font_size)
76 label.mouse_filter = Control.MOUSE_FILTER_IGNORE
77 label.visible = true
78 y += height + gap
79 _hide_unused_nodes(parent, rendered_count)
80
81
82func expire(now_ms:int) -> void:
83 for index in range(messages.size() - 1, -1, -1):
84 var entry = messages[index]
85 if not entry is Dictionary or now_ms > int(entry.get("until", 0)):
86 messages.remove_at(index)
87 if messages.is_empty():
88 last_message = ""
89
90
91func clear_nodes(parent:Node) -> void:
92 if parent == null:
93 return
94 _hide_unused_nodes(parent, 0)
95
96
97func get_messages() -> Array:
98 return messages.duplicate(true)
99
100
101func set_messages(value:Array) -> void:
102 messages = value.duplicate(true)
103
104
105func wrap_text(message:String, width:float, options:Dictionary = {}) -> Dictionary:
106 var chars_per_line = max(12, int(floor(width / float(options.get("char_width", 8.0)))))
107 var max_lines = int(options.get("max_lines", 4))
108 var lines := []
109 var current := ""
110 for raw_word in _normalize(message).split(" ", false):
111 var word = str(raw_word)
112 if current == "":
113 current = word
114 elif current.length() + 1 + word.length() <= chars_per_line:
115 current += " " + word
116 else:
117 lines.append(current)
118 current = word
119 if current != "":
120 lines.append(current)
121 if lines.size() > max_lines:
122 lines = lines.slice(0, max_lines)
123 lines[max_lines - 1] = str(lines[max_lines - 1]).trim_suffix(".") + "..."
124 return {
125 "text": "\n".join(lines),
126 "line_count": max(1, lines.size())
127 }
128
129
130func _normalize(message:String) -> String:
131 return " ".join(message.replace("\n", " ").split(" ", false)).strip_edges()
132
133
134func _indexed_name(base_name:String, index:int) -> String:
135 return base_name if index == 0 else "%s%d" % [base_name, index]
136
137
138func _panel_node(parent:Control, node_name:String) -> ColorRect:
139 var node = parent.get_node_or_null(node_name)
140 if node is ColorRect:
141 return node
142 var created = ColorRect.new()
143 created.name = node_name
144 created.mouse_filter = Control.MOUSE_FILTER_IGNORE
145 parent.add_child(created)
146 return created
147
148
149func _label_node(parent:Control, node_name:String) -> Label:
150 var node = parent.get_node_or_null(node_name)
151 if node is Label:
152 return node
153 var created = Label.new()
154 created.name = node_name
155 created.mouse_filter = Control.MOUSE_FILTER_IGNORE
156 parent.add_child(created)
157 return created
158
159
160func _hide_unused_nodes(parent:Node, used_count:int) -> void:
161 for child in parent.get_children():
162 var child_name = str(child.name)
163 if not child_name.begins_with(node_prefix):
164 continue
165 var index = _node_index(child_name)
166 if index >= used_count and child is CanvasItem:
167 child.visible = false
168
169
170func _node_index(child_name:String) -> int:
171 var suffix = child_name.trim_prefix("%sPanel" % node_prefix)
172 if child_name.begins_with("%sText" % node_prefix):
173 suffix = child_name.trim_prefix("%sText" % node_prefix)
174 return int(suffix) if suffix.is_valid_int() else 0