7const DataCacheManager = preload(
"res://scripts/systems/dataCacheManager/data_cache_manager.gd")
12var active_projectiles := []
13var floating_numbers := []
15var data_cache_manager = DataCacheManager.new()
18func load_archetypes(file_path:String) -> void:
20 load_archetype_data(data_cache_manager.load_json(file_path))
23func get_cache_stats() -> Dictionary:
24 return data_cache_manager.get_cache_stats()
27func load_archetype_data(data) -> void:
29 if not data
is Dictionary:
31 for archetype
in data.get(
"archetypes", []):
32 if not archetype
is Dictionary:
34 var id = str(archetype.get(
"id",
""))
36 archetypes[id] = archetype.duplicate(true)
39func configure(next_enemies:Array) -> void:
43 active_projectiles.clear()
44 floating_numbers.clear()
45 for enemy
in next_enemies:
46 if not enemy
is Dictionary:
48 var entry = _with_archetype(enemy)
49 entry[
"hp"] = int(entry.get(
"hp", 1))
50 entry[
"max_hp"] = int(entry.get(
"max_hp", entry.get(
"hp", 1)))
51 entry[
"alive"] = int(entry.get(
"hp", 1)) > 0
52 entry[
"cooldown_remaining"] = int(entry.get(
"cooldown_remaining", 0))
53 entry[
"alerted"] = bool(entry.get(
"alerted", false))
57func step(delta:float) -> void:
58 _tick_timed(active_projectiles, delta)
59 _tick_timed(floating_numbers, delta)
62func resolve_card(card:Dictionary, affected_cells:Array, origin_cell := Vector2i.ZERO, direction := Vector2i.ZERO) -> Dictionary:
63 var damage = int(card.get(
"damage", 0))
65 for index
in range(enemies.size()):
66 var enemy = enemies[index]
67 if not bool(enemy.get(
"alive", true)):
69 var enemy_cell = _cell_from_value(enemy.get(
"cell", [0, 0]))
70 if affected_cells.has(enemy_cell):
71 var dealt = max(0, damage - int(enemy.get(
"defense", 0)))
72 enemy[
"hp"] = max(0, int(enemy.get(
"hp", 0)) - dealt)
73 enemy[
"alive"] = int(enemy.get(
"hp", 0)) > 0
74 enemy[
"alerted"] = true
75 enemies[index] = enemy
77 "enemy_id": str(enemy.get(
"id",
"")),
78 "name": str(enemy.get(
"name",
"Enemy")),
80 "remaining_hp": int(enemy.get(
"hp", 0)),
81 "defeated":
not bool(enemy.get(
"alive", true)),
85 damage_log.append(hit.duplicate(true))
86 floating_numbers.append({
87 "text":
"-%d" % dealt,
93 _spawn_projectile(card, affected_cells, origin_cell, direction, hits)
95 "card_id": str(card.get(
"id",
"")),
97 "hit":
not hits.is_empty(),
98 "affected_cells": affected_cells.duplicate()
102func take_enemy_turn(player_cell:Vector2i, bounds:Rect2, blocked_cells:Array, config := {}) -> Dictionary:
104 var total_damage := 0
105 for index
in range(enemies.size()):
106 var enemy = enemies[index]
107 if not bool(enemy.get(
"alive", true)):
109 if int(enemy.get(
"cooldown_remaining", 0)) > 0:
110 enemy[
"cooldown_remaining"] = int(enemy.get(
"cooldown_remaining", 0)) - 1
111 enemies[index] = enemy
113 var action = _enemy_action(enemy, player_cell, bounds, blocked_cells, config)
114 if action.is_empty():
116 if str(action.get(
"type",
"")) ==
"move":
117 enemy[
"cell"] = action.get(
"to", enemy.get(
"cell", [0, 0]))
118 enemies[index] = enemy
119 elif str(action.get(
"type",
"")) ==
"attack":
120 total_damage += int(action.get(
"damage", 0))
121 floating_numbers.append({
122 "text":
"-%d" % int(action.get(
"damage", 0)),
128 active_projectiles.append({
129 "card_id":
"enemy_attack",
130 "type": str(action.get(
"attack_type",
"melee")),
131 "shape": str(action.get(
"attack_type",
"melee")),
132 "color": str(action.get(
"color",
"#ff7777")),
133 "origin_cell": action.get(
"from", player_cell),
134 "end_cell": player_cell,
135 "direction": Vector2i.ZERO,
136 "affected_cells": [player_cell],
140 enemy[
"cooldown_remaining"] = _cooldown_for(enemy)
141 enemies[index] = enemy
142 actions.append(action)
143 action_log.append(action.duplicate(true))
146 "damage": total_damage
150func get_enemies() -> Array:
151 return enemies.duplicate(true)
154func get_alive_enemies() -> Array:
156 for enemy
in enemies:
157 if bool(enemy.get(
"alive", true)):
158 alive.append(enemy.duplicate(true))
162func get_active_projectiles() -> Array:
163 return active_projectiles.duplicate(true)
166func get_floating_numbers() -> Array:
167 return floating_numbers.duplicate(true)
170func get_damage_log() -> Array:
171 return damage_log.duplicate(true)
174func get_action_log() -> Array:
175 return action_log.duplicate(true)
178func get_primary_enemy() -> Dictionary:
179 for enemy
in enemies:
180 if bool(enemy.get(
"alive", true)):
181 return enemy.duplicate(true)
182 if enemies.is_empty():
184 return enemies[0].duplicate(true)
187func get_summary() -> Dictionary:
190 for enemy
in enemies:
191 if bool(enemy.get(
"alive", true)):
196 "enemy_count": enemies.size(),
198 "defeated": defeated,
199 "damage_events": damage_log.size(),
200 "actions": action_log.size()
205func collect_defeat_rewards(hits:Array) -> Dictionary:
207 var enemy_lookup := {}
208 for enemy
in enemies:
209 if enemy
is Dictionary:
210 enemy_lookup[str(enemy.get(
"id",
""))] = enemy
212 if not hit
is Dictionary
or not bool(hit.get(
"defeated", false)):
214 var enemy_id = str(hit.get(
"enemy_id",
""))
215 var enemy = enemy_lookup.get(enemy_id, {})
216 for drop
in enemy.get(
"drops", []):
217 if not drop
is Dictionary:
219 var chance = float(drop.get(
"chance", 1.0))
220 if chance < 1.0
and randf() > chance:
222 var id = str(drop.get(
"id",
""))
223 var amount = int(drop.get(
"amount", 1))
224 if id ==
"" or amount <= 0:
226 gained[id] = int(gained.get(id, 0)) + amount
230func get_state() -> Dictionary:
232 "enemies": enemies.duplicate(true),
233 "damage_log": damage_log.duplicate(true),
234 "action_log": action_log.duplicate(true),
235 "active_projectiles": active_projectiles.duplicate(true),
236 "floating_numbers": floating_numbers.duplicate(true)
240func apply_state(state:Dictionary) -> void:
241 enemies = state.get(
"enemies", []).duplicate(true)
242 damage_log = state.get(
"damage_log", []).duplicate(true)
243 action_log = state.get(
"action_log", []).duplicate(true)
244 active_projectiles = state.get(
"active_projectiles", []).duplicate(true)
245 floating_numbers = state.get(
"floating_numbers", []).duplicate(true)
248func _with_archetype(enemy:Dictionary) -> Dictionary:
249 var archetype_id = str(enemy.get(
"archetype",
""))
251 if archetypes.has(archetype_id):
252 entry = archetypes[archetype_id].duplicate(true)
253 for key
in enemy.keys():
254 entry[key] = enemy[key]
255 if not entry.has(
"max_hp")
and entry.has(
"hp"):
256 entry[
"max_hp"] = entry[
"hp"]
260func _spawn_projectile(card:Dictionary, affected_cells:Array, origin_cell:Vector2i, direction:Vector2i, hits:Array) -> void:
261 var targeting = card.get(
"targeting", {})
262 var visual = card.get(
"visual", {})
263 var type = str(targeting.get(
"type",
"projectile"))
264 var duration = float(visual.get(
"duration", 0.25))
265 var end_cell = origin_cell
266 if not hits.is_empty():
267 end_cell = _cell_from_value(hits[0].get(
"cell", origin_cell))
268 elif not affected_cells.is_empty():
269 end_cell = _cell_from_value(affected_cells[affected_cells.size() - 1])
270 active_projectiles.append({
271 "card_id": str(card.get(
"id",
"")),
273 "shape": str(visual.get(
"shape", type)),
274 "color": str(visual.get(
"color",
"#ffffff")),
275 "origin_cell": origin_cell,
276 "end_cell": end_cell,
277 "direction": direction,
278 "affected_cells": affected_cells.duplicate(),
280 "duration": max(0.05, duration)
284func _enemy_action(enemy:Dictionary, player_cell:Vector2i, bounds:Rect2, blocked_cells:Array, config:Dictionary) -> Dictionary:
285 var enemy_cell = _cell_from_value(enemy.get(
"cell", [0, 0]))
286 var behavior = enemy.get(
"behavior", {})
287 var behavior_type = str(behavior.get(
"type",
"guard"))
288 var distance = _grid_distance(enemy_cell, player_cell)
289 var attack_range = int(behavior.get(
"range", 1))
290 var aggro_range = int(behavior.get(
"aggro_range", attack_range))
291 var alerted = bool(enemy.get(
"alerted", false))
or distance <= aggro_range
292 if alerted
and not bool(enemy.get(
"alerted", false)):
293 enemy[
"alerted"] = true
295 return _wander_action(enemy, enemy_cell, bounds, blocked_cells, config)
296 if behavior_type ==
"ranged" and _has_line(enemy_cell, player_cell, attack_range, blocked_cells):
297 return _attack_action(enemy, enemy_cell, player_cell,
"beam")
298 if distance <= attack_range
and behavior_type !=
"ranged":
299 return _attack_action(enemy, enemy_cell, player_cell,
"melee")
300 if behavior_type ==
"chase" or behavior_type ==
"ranged":
301 var move_distance = int(behavior.get(
"move_distance", 1))
302 if move_distance <= 0:
304 var next_cell = _step_toward(enemy_cell, player_cell, bounds, blocked_cells, config)
305 if next_cell != enemy_cell:
308 "enemy_id": str(enemy.get(
"id",
"")),
309 "name": str(enemy.get(
"name",
"Enemy")),
316func _wander_action(enemy:Dictionary, enemy_cell:Vector2i, bounds:Rect2, blocked_cells:Array, config:Dictionary) -> Dictionary:
317 var behavior = enemy.get(
"behavior", {})
318 if not bool(behavior.get(
"wander", true)):
320 var move_distance = int(behavior.get(
"move_distance", 1))
321 if move_distance <= 0:
323 var directions = [Vector2i.LEFT, Vector2i.RIGHT, Vector2i.UP, Vector2i.DOWN]
324 var start_index = randi() % directions.size()
325 for offset
in range(directions.size()):
326 var direction = directions[(start_index + offset) % directions.size()]
327 var candidate = enemy_cell + direction
328 if not _cell_blocked(candidate, bounds, blocked_cells, config, enemy_cell):
332 "enemy_id": str(enemy.get(
"id",
"")),
333 "name": str(enemy.get(
"name",
"Enemy")),
340func _attack_action(enemy:Dictionary, enemy_cell:Vector2i, player_cell:Vector2i, attack_type:String) -> Dictionary:
343 "attack_type": attack_type,
344 "enemy_id": str(enemy.get(
"id",
"")),
345 "name": str(enemy.get(
"name",
"Enemy")),
347 "target": player_cell,
348 "damage": int(enemy.get(
"attack", 1)),
349 "color": str(enemy.get(
"color",
"#ff7777"))
353func _cooldown_for(enemy:Dictionary) -> int:
354 var behavior = enemy.get(
"behavior", {})
355 if behavior
is Dictionary:
356 return max(0, int(behavior.get(
"cooldown_turns", 0)))
360func _step_toward(from_cell:Vector2i, target_cell:Vector2i, bounds:Rect2, blocked_cells:Array, config:Dictionary) -> Vector2i:
362 var delta = target_cell - from_cell
363 if abs(delta.x) >= abs(delta.y):
364 candidates.append(Vector2i(sign(delta.x), 0))
365 candidates.append(Vector2i(0, sign(delta.y)))
367 candidates.append(Vector2i(0, sign(delta.y)))
368 candidates.append(Vector2i(sign(delta.x), 0))
369 for fallback
in [Vector2i.LEFT, Vector2i.RIGHT, Vector2i.UP, Vector2i.DOWN]:
370 if not candidates.has(fallback):
371 candidates.append(fallback)
372 for direction
in candidates:
373 if direction == Vector2i.ZERO:
375 var candidate = from_cell + direction
376 if candidate == target_cell:
378 if not _cell_blocked(candidate, bounds, blocked_cells, config, from_cell):
383func _cell_blocked(cell:Vector2i, bounds:Rect2, blocked_cells:Array, config:Dictionary, moving_from:Vector2i) -> bool:
384 var tile_size = max(1.0, float(config.get(
"tile_size", 40.0)))
385 var world_position = Vector2(float(cell.x) * tile_size + tile_size * 0.5, float(cell.y) * tile_size + tile_size * 0.5)
386 if not bounds.has_point(world_position):
388 for blocked
in blocked_cells:
389 if _cell_from_value(blocked) == cell:
391 for enemy
in enemies:
392 if not bool(enemy.get(
"alive", true)):
394 var enemy_cell = _cell_from_value(enemy.get(
"cell", [0, 0]))
395 if enemy_cell != moving_from
and enemy_cell == cell:
400func _has_line(from_cell:Vector2i, target_cell:Vector2i, max_range:int, blocked_cells:Array) -> bool:
401 var delta = target_cell - from_cell
402 if delta.x != 0
and delta.y != 0:
404 var distance = _grid_distance(from_cell, target_cell)
405 if distance > max_range:
407 var direction = Vector2i(sign(delta.x), sign(delta.y))
408 var current = from_cell
409 for _index
in range(distance - 1):
411 if blocked_cells.has(current):
416func _grid_distance(a:Vector2i, b:Vector2i) -> int:
417 return abs(a.x - b.x) + abs(a.y - b.y)
420func _tick_timed(items:Array, delta:float) -> void:
421 for index
in range(items.size() - 1, -1, -1):
422 var item = items[index]
423 item[
"age"] = float(item.get(
"age", 0.0)) + delta
425 if float(item.get(
"age", 0.0)) >= float(item.get(
"duration", 0.1)):
426 items.remove_at(index)
429func _cell_from_value(value) -> Vector2i:
430 if value
is Vector2i:
433 return Vector2i(int(value.x), int(value.y))
434 if value
is Array
and value.size() >= 2:
435 return Vector2i(int(value[0]), int(value[1]))