feat: spawn enemies from reusable definitions

This commit is contained in:
Rijad Zuzo
2026-08-12 20:34:55 +02:00
parent 41b37a4962
commit 809e7c008d
12 changed files with 332 additions and 57 deletions
+81
View File
@@ -0,0 +1,81 @@
class_name CombatantFactory
extends RefCounted
var _occupied_ids: Dictionary = {}
var _next_suffix_by_prefix: Dictionary = {}
func reset_allocator() -> void:
_occupied_ids.clear()
_next_suffix_by_prefix.clear()
func restore_allocator(records: Array[CombatantStateRecord]) -> void:
reset_allocator()
for record in records:
if record != null:
reserve_id(record.get_combatant_id())
func reserve_id(combatant_id: StringName) -> bool:
if combatant_id.is_empty() or _occupied_ids.has(combatant_id):
return false
_occupied_ids[combatant_id] = true
_observe_numeric_suffix(combatant_id)
return true
func create_enemy(enemy_definition_id: StringName, position: Vector3) -> CombatantStateRecord:
return create_from_definition(SimulationEnemies.get_enemy(enemy_definition_id), position)
func create_from_definition(definition: EnemyDefinition, position: Vector3) -> CombatantStateRecord:
if definition == null or not definition.validate().is_empty() or not position.is_finite():
return null
var combatant_id := _allocate_id(definition.combatant_id_prefix)
return CombatantStateRecord.create(
combatant_id,
definition.kind,
definition.faction_id,
definition.display_name,
position,
definition.health,
definition.weapon_id,
CombatantStateRecord.NO_NPC_ID,
definition.hostile,
definition.enemy_id
)
func get_next_suffix(prefix: String) -> int:
return int(_next_suffix_by_prefix.get(prefix, 0))
func is_reserved(combatant_id: StringName) -> bool:
return _occupied_ids.has(combatant_id)
func _allocate_id(prefix: String) -> StringName:
var suffix := get_next_suffix(prefix)
var candidate := StringName("%s_%d" % [prefix, suffix])
while _occupied_ids.has(candidate):
suffix += 1
candidate = StringName("%s_%d" % [prefix, suffix])
_occupied_ids[candidate] = true
_next_suffix_by_prefix[prefix] = suffix + 1
return candidate
func _observe_numeric_suffix(combatant_id: StringName) -> void:
var text_id := String(combatant_id)
var separator_index := text_id.rfind("_")
if separator_index <= 0 or separator_index >= text_id.length() - 1:
return
var suffix_text := text_id.substr(separator_index + 1)
if not suffix_text.is_valid_int():
return
var suffix := int(suffix_text)
if suffix < 0:
return
var prefix := text_id.substr(0, separator_index)
_next_suffix_by_prefix[prefix] = maxi(get_next_suffix(prefix), suffix + 1)
@@ -0,0 +1 @@
uid://dfxudn0m53uai
+30 -35
View File
@@ -34,8 +34,7 @@ var factions: Dictionary = {}
var economy: RefCounted
var npcs: Array[SimNPC] = []
var _npcs_by_id: Dictionary = {}
var next_combatant_id := 0
var next_wolf_id := 0
var combatant_factory := CombatantFactory.new()
var current_tick := 0
var village_center := Vector3.ZERO
var last_war_resolved_tick := -1
@@ -222,24 +221,18 @@ func player_attack(combatant_id: StringName) -> float:
func spawn_wolf(position: Vector3) -> StringName:
var definition := SimulationEnemies.get_enemy(&"enemy_wolf")
var wolf_id := StringName("wolf_%d" % next_wolf_id)
next_wolf_id += 1
var wolf := CombatantStateRecord.create(
wolf_id,
SimulationIds.COMBATANT_KIND_WOLF,
SimulationIds.FACTION_TRIBE,
definition.display_name if definition != null else "Wolf",
position,
definition.health if definition != null else 40.0,
definition.weapon_id if definition != null else SimulationIds.ITEM_CLAW,
CombatantStateRecord.NO_NPC_ID,
true
)
combatants[wolf_id] = wolf
return spawn_enemy(&"enemy_wolf", position)
func spawn_enemy(enemy_definition_id: StringName, position: Vector3) -> StringName:
var enemy := combatant_factory.create_enemy(enemy_definition_id, position)
if enemy == null:
return &""
var combatant_id := enemy.get_combatant_id()
combatants[combatant_id] = enemy
_combatant_ids_dirty = true
combatant_spawned.emit(wolf_id)
return wolf_id
combatant_spawned.emit(combatant_id)
return combatant_id
func _apply_damage(target: CombatantStateRecord, damage: float, actor_id: int) -> float:
@@ -291,7 +284,11 @@ func _advance_wolves() -> void:
var combatant := combatants[combatant_id] as CombatantStateRecord
if combatant == null or not combatant.is_alive():
continue
if combatant.get_kind() != SimulationIds.COMBATANT_KIND_WOLF:
var definition := _enemy_definition_for(combatant)
if (
definition == null
or definition.behavior_profile_id != EnemyDefinition.BEHAVIOR_PROFILE_WOLF_HUNT
):
continue
var target := _find_nearest_defender(combatant.get_position(), WOLF_HUNT_RADIUS)
if target == null:
@@ -302,8 +299,7 @@ func _advance_wolves() -> void:
var to_target := target.get_position() - combatant.get_position()
to_target.y = 0.0
if to_target.length() > reach:
var definition := SimulationEnemies.get_enemy(&"enemy_wolf")
var speed := definition.move_speed if definition != null else 3.2
var speed := definition.move_speed
var step_distance := minf(speed * tick_interval, to_target.length() - reach)
combatant.set_position(
combatant.get_position() + to_target.normalized() * maxf(step_distance, 0.0)
@@ -422,25 +418,17 @@ func village_defender_count() -> int:
func _start_raid(tribe: FactionStateRecord, simulation_tick: int) -> void:
var definition := SimulationEnemies.get_enemy(&"enemy_raider")
tribe.set_war_plan(SimulationIds.WAR_PLAN_RAIDING, simulation_tick)
tribe.set_stance(SimulationIds.STANCE_HOSTILE)
var raider_ids: Array[StringName] = []
for index in range(tribe.get_warriors()):
var raider_id := StringName("raider_%d" % next_combatant_id)
next_combatant_id += 1
var offset := Vector3(float(index * 2 - tribe.get_warriors()), 0.0, 0.0)
var raider := CombatantStateRecord.create(
raider_id,
SimulationIds.COMBATANT_KIND_RAIDER,
SimulationIds.FACTION_TRIBE,
definition.display_name if definition != null else "Raider",
village_center + RAID_SPAWN_OFFSET + offset,
definition.health if definition != null else 60.0,
definition.weapon_id if definition != null else SimulationIds.ITEM_SWORD,
CombatantStateRecord.NO_NPC_ID,
true
var raider := combatant_factory.create_enemy(
&"enemy_raider", village_center + RAID_SPAWN_OFFSET + offset
)
if raider == null:
continue
var raider_id := raider.get_combatant_id()
combatants[raider_id] = raider
_combatant_ids_dirty = true
raider_ids.append(raider_id)
@@ -695,6 +683,12 @@ func _get_faction(faction_id: StringName) -> FactionStateRecord:
return factions.get(faction_id) as FactionStateRecord
func _enemy_definition_for(combatant: CombatantStateRecord) -> EnemyDefinition:
if combatant == null or combatant.get_enemy_definition_id().is_empty():
return null
return SimulationEnemies.get_enemy(combatant.get_enemy_definition_id())
func _narrative_event_requested(
event_type: StringName,
actor_id: int,
@@ -738,6 +732,7 @@ func restore_state_records(
combatants.clear()
for combatant in combatant_records:
combatants[combatant.get_combatant_id()] = combatant
combatant_factory.restore_allocator(combatant_records)
_combatant_ids_dirty = true
factions.clear()
for faction in faction_records: