RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
entity_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
7
8var entities := {}
9var entity_order := []
10
11
12
13func configure(next_entities:Array = []) -> void:
14 entities.clear()
15 entity_order.clear()
16 for raw_entity in next_entities:
17 if raw_entity is Dictionary:
18 add_entity(raw_entity)
19
20
21
22func create_entity(data:Dictionary = {}) -> Dictionary:
23 var id = str(data.get("id", ""))
24 var entity = data.duplicate(true)
25 entity["id"] = id
26 entity["kind"] = str(entity.get("kind", entity.get("type", "entity")))
27 entity["type"] = str(entity.get("type", entity.get("kind", "entity")))
28 entity["name"] = str(entity.get("name", id.capitalize() if id != "" else "Entity"))
29 entity["team"] = str(entity.get("team", "neutral"))
30 entity["owner"] = str(entity.get("owner", ""))
31 entity["tags"] = _normalize_string_array(entity.get("tags", []))
32 entity["state"] = str(entity.get("state", "idle"))
33 entity["alive"] = bool(entity.get("alive", int(entity.get("hp", 1)) > 0))
34 entity["hp"] = int(entity.get("hp", entity.get("health", 1)))
35 entity["max_hp"] = int(entity.get("max_hp", entity.get("max_health", max(1, int(entity.get("hp", entity.get("health", 1)))))))
36 entity["cell"] = _cell_to_array(_cell_from_any(entity.get("cell", [0, 0])))
37 entity["position"] = _vector_to_array(_vector_from_any(entity.get("position", Vector2.ZERO)))
38 entity["velocity"] = _vector_to_array(_vector_from_any(entity.get("velocity", Vector2.ZERO)))
39 entity["facing"] = _cell_to_array(_cell_from_any(entity.get("facing", [0, 1])))
40 entity["status_effects"] = _normalize_statuses(entity.get("status_effects", entity.get("statuses", [])))
41 entity["metadata"] = entity.get("metadata", {}).duplicate(true) if entity.get("metadata", {}) is Dictionary else {}
42 return entity
43
44
45
46func add_entity(data:Dictionary) -> Dictionary:
47 var entity = create_entity(data)
48 var id = str(entity.get("id", ""))
49 if id == "":
50 id = "entity_%d" % (entity_order.size() + 1)
51 entity["id"] = id
52 if not entities.has(id):
53 entity_order.append(id)
54 entities[id] = entity
55 return entity.duplicate(true)
56
57
58
59func remove_entity(entity_id:String) -> bool:
60 if not entities.has(entity_id):
61 return false
62 entities.erase(entity_id)
63 entity_order.erase(entity_id)
64 return true
65
66
67
68func has_entity(entity_id:String) -> bool:
69 return entities.has(entity_id)
70
71
72
73func get_entity(entity_id:String) -> Dictionary:
74 return entities.get(entity_id, {}).duplicate(true)
75
76
77
78func get_entities(filters:Dictionary = {}) -> Array:
79 var result := []
80 for id in entity_order:
81 if not entities.has(id):
82 continue
83 var entity = entities[id]
84 if _matches_filters(entity, filters):
85 result.append(entity.duplicate(true))
86 return result
87
88
89
90func update_entity(entity_id:String, patch:Dictionary) -> Dictionary:
91 if not entities.has(entity_id):
92 return {}
93 var entity = entities[entity_id].duplicate(true)
94 for key in patch.keys():
95 entity[key] = patch[key]
96 entities[entity_id] = create_entity(entity)
97 return entities[entity_id].duplicate(true)
98
99
100
101func set_cell(entity_id:String, cell) -> Dictionary:
102 return update_entity(entity_id, {"cell": _cell_to_array(_cell_from_any(cell))})
103
104
105
106func set_position(entity_id:String, position) -> Dictionary:
107 return update_entity(entity_id, {"position": _vector_to_array(_vector_from_any(position))})
108
109
110
111func add_status(entity_id:String, status:Dictionary) -> Dictionary:
112 if not entities.has(entity_id):
113 return {}
114 var entity = entities[entity_id].duplicate(true)
115 var statuses = _normalize_statuses(entity.get("status_effects", []))
116 var next_status = status.duplicate(true)
117 next_status["id"] = str(next_status.get("id", "status"))
118 var replaced := false
119 for index in range(statuses.size()):
120 if str(statuses[index].get("id", "")) == str(next_status.get("id", "")):
121 statuses[index] = next_status
122 replaced = true
123 break
124 if not replaced:
125 statuses.append(next_status)
126 entity["status_effects"] = statuses
127 entities[entity_id] = create_entity(entity)
128 return entities[entity_id].duplicate(true)
129
130
131
132func tick_statuses(delta:float) -> void:
133 for id in entity_order:
134 if not entities.has(id):
135 continue
136 var entity = entities[id].duplicate(true)
137 var active_statuses := []
138 for status in _normalize_statuses(entity.get("status_effects", [])):
139 var next_status = status.duplicate(true)
140 if next_status.has("duration"):
141 next_status["duration"] = max(0.0, float(next_status.get("duration", 0.0)) - delta)
142 if float(next_status.get("duration", 0.0)) <= 0.0:
143 continue
144 active_statuses.append(next_status)
145 entity["status_effects"] = active_statuses
146 entities[id] = create_entity(entity)
147
148
149
150func apply_damage(entity_id:String, amount:int, options:Dictionary = {}) -> Dictionary:
151 if not entities.has(entity_id):
152 return {}
153 var entity = entities[entity_id].duplicate(true)
154 var defense = int(entity.get("defense", 0))
155 var dealt = max(0, amount - defense) if bool(options.get("use_defense", true)) else max(0, amount)
156 entity["hp"] = max(0, int(entity.get("hp", 0)) - dealt)
157 entity["alive"] = int(entity.get("hp", 0)) > 0
158 if not bool(entity.get("alive", true)):
159 entity["state"] = str(options.get("death_state", "dead"))
160 entities[entity_id] = create_entity(entity)
161 return {
162 "entity": entities[entity_id].duplicate(true),
163 "damage": dealt,
164 "defeated": not bool(entities[entity_id].get("alive", true))
165 }
166
167
168
169func heal(entity_id:String, amount:int) -> Dictionary:
170 if not entities.has(entity_id):
171 return {}
172 var entity = entities[entity_id].duplicate(true)
173 entity["hp"] = min(int(entity.get("max_hp", 1)), int(entity.get("hp", 0)) + max(0, amount))
174 entity["alive"] = int(entity.get("hp", 0)) > 0
175 entities[entity_id] = create_entity(entity)
176 return entities[entity_id].duplicate(true)
177
178
179
180func get_entities_at_cell(cell, filters:Dictionary = {}) -> Array:
181 var target = _cell_from_any(cell)
182 var result := []
183 for entity in get_entities(filters):
184 if _cell_from_any(entity.get("cell", [0, 0])) == target:
185 result.append(entity)
186 return result
187
188
189
190func get_occupied_cells(filters:Dictionary = {}) -> Array:
191 var cells := []
192 for entity in get_entities(filters):
193 cells.append(_cell_from_any(entity.get("cell", [0, 0])))
194 return cells
195
196
197
198func get_state() -> Dictionary:
199 return {
200 "entities": get_entities(),
201 "order": entity_order.duplicate(true)
202 }
203
204
205
206func apply_state(state:Dictionary) -> void:
207 configure(state.get("entities", []) if state.get("entities", []) is Array else [])
208 var order = state.get("order", [])
209 if order is Array and not order.is_empty():
210 var normalized_order := []
211 for id in order:
212 if entities.has(str(id)):
213 normalized_order.append(str(id))
214 for id in entity_order:
215 if not normalized_order.has(id):
216 normalized_order.append(id)
217 entity_order = normalized_order
218
219
220func _matches_filters(entity:Dictionary, filters:Dictionary) -> bool:
221 if filters.has("kind") and str(entity.get("kind", "")) != str(filters.get("kind", "")):
222 return false
223 if filters.has("type") and str(entity.get("type", "")) != str(filters.get("type", "")):
224 return false
225 if filters.has("team") and str(entity.get("team", "")) != str(filters.get("team", "")):
226 return false
227 if filters.has("owner") and str(entity.get("owner", "")) != str(filters.get("owner", "")):
228 return false
229 if filters.has("alive") and bool(entity.get("alive", true)) != bool(filters.get("alive", true)):
230 return false
231 if filters.has("tags"):
232 var tags = _normalize_string_array(entity.get("tags", []))
233 for tag in _normalize_string_array(filters.get("tags", [])):
234 if not tags.has(tag):
235 return false
236 return true
237
238
239func _normalize_string_array(value) -> Array:
240 var result := []
241 if not value is Array:
242 return result
243 for item in value:
244 result.append(str(item))
245 return result
246
247
248func _normalize_statuses(value) -> Array:
249 var result := []
250 if not value is Array:
251 return result
252 for status in value:
253 if status is Dictionary:
254 var entry = status.duplicate(true)
255 entry["id"] = str(entry.get("id", "status"))
256 result.append(entry)
257 return result
258
259
260func _cell_from_any(value) -> Vector2i:
261 if value is Vector2i:
262 return value
263 if value is Vector2:
264 return Vector2i(int(value.x), int(value.y))
265 if value is Array and value.size() >= 2:
266 return Vector2i(int(value[0]), int(value[1]))
267 if value is Dictionary and value.has("cell"):
268 return _cell_from_any(value.get("cell"))
269 if value is Dictionary and value.has("x") and value.has("y"):
270 return Vector2i(int(value.get("x", 0)), int(value.get("y", 0)))
271 return Vector2i.ZERO
272
273
274func _vector_from_any(value) -> Vector2:
275 if value is Vector2:
276 return value
277 if value is Vector2i:
278 return Vector2(float(value.x), float(value.y))
279 if value is Array and value.size() >= 2:
280 return Vector2(float(value[0]), float(value[1]))
281 if value is Dictionary and value.has("position"):
282 return _vector_from_any(value.get("position"))
283 if value is Dictionary and value.has("x") and value.has("y"):
284 return Vector2(float(value.get("x", 0.0)), float(value.get("y", 0.0)))
285 return Vector2.ZERO
286
287
288func _cell_to_array(cell:Vector2i) -> Array:
289 return [cell.x, cell.y]
290
291
292func _vector_to_array(vector:Vector2) -> Array:
293 return [vector.x, vector.y]