741 lines
23 KiB
GDScript
741 lines
23 KiB
GDScript
class_name ConflictSystem
|
|
extends RefCounted
|
|
|
|
signal narrative_event_requested(
|
|
event_type: StringName,
|
|
actor_id: int,
|
|
source_id: StringName,
|
|
action_display: String,
|
|
action_id: StringName,
|
|
item_id: StringName,
|
|
required_amount: float
|
|
)
|
|
signal combatant_spawned(combatant_id: StringName)
|
|
signal combatant_died(combatant_id: StringName)
|
|
signal raid_started(raider_ids: Array[StringName])
|
|
signal war_resolved(outcome: StringName)
|
|
signal war_aborted
|
|
signal player_damaged(amount: float)
|
|
signal player_downed
|
|
|
|
const NPC_MAX_HEALTH := 100.0
|
|
const STRONG_STRENGTH := 5.0
|
|
const DEFENSE_DECISION_INTERVAL := 200
|
|
const TRIBE_FOOD_DECAY := 0.05
|
|
const WOLF_HUNT_RADIUS := 18.0
|
|
const DEFAULT_TICK_INTERVAL := 1.2
|
|
const RAID_SPAWN_OFFSET := Vector3(42.0, 0.0, 42.0)
|
|
const RAID_SPEED := 1.2
|
|
const RAID_SIZE := 4
|
|
const PLAYER_COMBATANT_ID := &"player_combatant"
|
|
|
|
var combatants: Dictionary = {}
|
|
var factions: Dictionary = {}
|
|
var economy: RefCounted
|
|
var npcs: Array[SimNPC] = []
|
|
var _npcs_by_id: Dictionary = {}
|
|
var combatant_factory := CombatantFactory.new()
|
|
var current_tick := 0
|
|
var village_center := Vector3.ZERO
|
|
var last_war_resolved_tick := -1
|
|
var tick_interval := DEFAULT_TICK_INTERVAL
|
|
var player_attack_cooldown_remaining := 0.0
|
|
var _sorted_combatant_ids_cache: Array[StringName] = []
|
|
var _combatant_ids_dirty := true
|
|
|
|
|
|
func configure(
|
|
economy_service: RefCounted, simulation_tick_interval: float = DEFAULT_TICK_INTERVAL
|
|
) -> void:
|
|
economy = economy_service
|
|
tick_interval = maxf(simulation_tick_interval, 0.0001)
|
|
|
|
|
|
func initialize_factions() -> void:
|
|
if not factions.has(SimulationIds.FACTION_VILLAGE):
|
|
factions[SimulationIds.FACTION_VILLAGE] = FactionStateRecord.create(
|
|
SimulationIds.FACTION_VILLAGE, "Village", 20.0, 0, 0.2, 0.8
|
|
)
|
|
if not factions.has(SimulationIds.FACTION_TRIBE):
|
|
factions[SimulationIds.FACTION_TRIBE] = FactionStateRecord.create(
|
|
SimulationIds.FACTION_TRIBE, "Hill Tribe", 4.0, RAID_SIZE, 0.7, 0.7
|
|
)
|
|
_ensure_player_combatant()
|
|
|
|
|
|
func _ensure_player_combatant() -> void:
|
|
if combatants.has(PLAYER_COMBATANT_ID):
|
|
return
|
|
var sword := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
|
combatants[PLAYER_COMBATANT_ID] = CombatantStateRecord.create(
|
|
PLAYER_COMBATANT_ID,
|
|
SimulationIds.COMBATANT_KIND_PLAYER,
|
|
SimulationIds.FACTION_VILLAGE,
|
|
"Player",
|
|
village_center,
|
|
100.0,
|
|
sword.item_id if sword != null else SimulationIds.ITEM_SWORD,
|
|
CombatantStateRecord.NO_NPC_ID
|
|
)
|
|
_combatant_ids_dirty = true
|
|
|
|
|
|
func set_player_combatant_position(position: Vector3) -> void:
|
|
var player_combatant := get_combatant(PLAYER_COMBATANT_ID)
|
|
if player_combatant == null:
|
|
_ensure_player_combatant()
|
|
player_combatant = get_combatant(PLAYER_COMBATANT_ID)
|
|
if player_combatant != null:
|
|
player_combatant.set_position(position)
|
|
|
|
|
|
func set_player_health(health: float, downed: bool) -> void:
|
|
var player_combatant := get_combatant(PLAYER_COMBATANT_ID)
|
|
if player_combatant == null:
|
|
_ensure_player_combatant()
|
|
player_combatant = get_combatant(PLAYER_COMBATANT_ID)
|
|
if player_combatant == null:
|
|
return
|
|
player_combatant.set_health_and_alive(health, not downed)
|
|
if downed:
|
|
player_attack_cooldown_remaining = 0.0
|
|
|
|
|
|
func is_player_downed() -> bool:
|
|
var player_combatant := get_combatant(PLAYER_COMBATANT_ID)
|
|
return player_combatant != null and not player_combatant.is_alive()
|
|
|
|
|
|
func advance_realtime(delta: float) -> void:
|
|
if not is_finite(delta) or delta <= 0.0:
|
|
return
|
|
player_attack_cooldown_remaining = maxf(player_attack_cooldown_remaining - delta, 0.0)
|
|
|
|
|
|
func is_player_attack_ready() -> bool:
|
|
var player_combatant := get_combatant(PLAYER_COMBATANT_ID)
|
|
return (
|
|
player_combatant != null
|
|
and player_combatant.is_alive()
|
|
and player_attack_cooldown_remaining <= 0.0
|
|
)
|
|
|
|
|
|
func get_player_attack_cooldown_remaining() -> float:
|
|
return player_attack_cooldown_remaining
|
|
|
|
|
|
func register_npc_combatants(npc_list: Array[SimNPC]) -> void:
|
|
npcs = npc_list
|
|
_npcs_by_id.clear()
|
|
for npc in npcs:
|
|
_npcs_by_id[npc.id] = npc
|
|
var existing_ids := {}
|
|
for combatant in combatants.values():
|
|
var candidate := combatant as CombatantStateRecord
|
|
if candidate != null and candidate.get_kind() == SimulationIds.COMBATANT_KIND_NPC:
|
|
existing_ids[candidate.get_npc_id()] = true
|
|
for npc in npcs:
|
|
if npc.is_dead:
|
|
continue
|
|
if existing_ids.has(npc.id):
|
|
continue
|
|
_create_npc_combatant(npc)
|
|
|
|
|
|
func _create_npc_combatant(npc: SimNPC) -> CombatantStateRecord:
|
|
var weapon := (
|
|
SimulationIds.ITEM_SWORD
|
|
if npc.profession == SimulationIds.PROFESSION_GUARD
|
|
else SimulationIds.ITEM_CLAW
|
|
)
|
|
var combatant := CombatantStateRecord.create(
|
|
SimulationIds.npc_combatant_id(npc.id),
|
|
SimulationIds.COMBATANT_KIND_NPC,
|
|
SimulationIds.FACTION_VILLAGE,
|
|
npc.npc_name,
|
|
npc.position,
|
|
NPC_MAX_HEALTH,
|
|
weapon,
|
|
npc.id
|
|
)
|
|
combatants[combatant.get_combatant_id()] = combatant
|
|
_combatant_ids_dirty = true
|
|
return combatant
|
|
|
|
|
|
func advance(simulation_tick: int) -> void:
|
|
current_tick = simulation_tick
|
|
_sync_npc_positions()
|
|
_tick_cooldowns()
|
|
_advance_raider_positions()
|
|
_advance_wolves()
|
|
_advance_war(simulation_tick)
|
|
_run_battle()
|
|
|
|
|
|
func _advance_raider_positions() -> void:
|
|
if not is_raiding():
|
|
return
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if (
|
|
combatant == null
|
|
or not combatant.is_alive()
|
|
or combatant.get_kind() != SimulationIds.COMBATANT_KIND_RAIDER
|
|
):
|
|
continue
|
|
var target := _find_nearest_defender(combatant.get_position(), 100000.0)
|
|
var target_position := village_center
|
|
if target != null:
|
|
target_position = target.get_position()
|
|
var to_target := target_position - combatant.get_position()
|
|
to_target.y = 0.0
|
|
if to_target.length() <= 0.5:
|
|
continue
|
|
var step := to_target.normalized() * minf(RAID_SPEED, to_target.length())
|
|
combatant.set_position(combatant.get_position() + step)
|
|
|
|
|
|
func player_attack(combatant_id: StringName) -> float:
|
|
var player_combatant := get_combatant(PLAYER_COMBATANT_ID)
|
|
var combatant := get_combatant(combatant_id)
|
|
if (
|
|
not is_player_attack_ready()
|
|
or combatant == null
|
|
or not combatant.is_alive()
|
|
or not combatant.is_hostile()
|
|
or combatant.get_faction_id() == player_combatant.get_faction_id()
|
|
or (
|
|
_horizontal_distance(player_combatant.get_position(), combatant.get_position())
|
|
> _reach(player_combatant)
|
|
)
|
|
):
|
|
return 0.0
|
|
var sword := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
|
var damage := sword.damage if sword != null else 24.0
|
|
var applied := _apply_damage(combatant, damage, SimulationIds.PLAYER_ACTOR_ID)
|
|
if applied > 0.0:
|
|
player_attack_cooldown_remaining = sword.attack_cooldown if sword != null else 0.6
|
|
return applied
|
|
|
|
|
|
func spawn_wolf(position: Vector3) -> StringName:
|
|
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(combatant_id)
|
|
return combatant_id
|
|
|
|
|
|
func _apply_damage(target: CombatantStateRecord, damage: float, actor_id: int) -> float:
|
|
if target == null or not target.is_alive() or damage <= 0.0:
|
|
return 0.0
|
|
var applied := target.take_damage(damage)
|
|
if applied <= 0.0:
|
|
return 0.0
|
|
_narrative_event_requested(
|
|
SimulationIds.EVENT_COMBATANT_HURT,
|
|
actor_id,
|
|
target.get_combatant_id(),
|
|
"",
|
|
"",
|
|
target.get_weapon_id(),
|
|
applied
|
|
)
|
|
if target.get_kind() == SimulationIds.COMBATANT_KIND_PLAYER:
|
|
player_damaged.emit(applied)
|
|
if not target.is_alive():
|
|
player_downed.emit()
|
|
return applied
|
|
if not target.is_alive():
|
|
_kill_combatant(target, actor_id)
|
|
return applied
|
|
|
|
|
|
func _kill_combatant(target: CombatantStateRecord, killer_id: int) -> void:
|
|
var npc_id := target.get_npc_id()
|
|
if npc_id >= 0:
|
|
var npc := _find_npc(npc_id)
|
|
if npc != null and not npc.is_dead:
|
|
npc.last_task = npc.current_task
|
|
npc.die_from_combat()
|
|
_narrative_event_requested(
|
|
SimulationIds.EVENT_COMBATANT_KILLED,
|
|
killer_id,
|
|
target.get_combatant_id(),
|
|
target.get_display_name(),
|
|
"",
|
|
target.get_weapon_id(),
|
|
0.0
|
|
)
|
|
combatant_died.emit(target.get_combatant_id())
|
|
|
|
|
|
func _advance_wolves() -> void:
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if combatant == null or not combatant.is_alive():
|
|
continue
|
|
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:
|
|
continue
|
|
if not combatant.is_hostile():
|
|
combatant.set_hostile(true)
|
|
var reach := _reach(combatant)
|
|
var to_target := target.get_position() - combatant.get_position()
|
|
to_target.y = 0.0
|
|
if to_target.length() > reach:
|
|
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)
|
|
)
|
|
continue
|
|
_attack_target(combatant, target)
|
|
|
|
|
|
func _advance_war(simulation_tick: int) -> void:
|
|
var tribe := _get_faction(SimulationIds.FACTION_TRIBE)
|
|
var village := _get_faction(SimulationIds.FACTION_VILLAGE)
|
|
if tribe == null:
|
|
return
|
|
if economy != null:
|
|
var pantry: StorageStateRecord = economy.get_pantry()
|
|
if pantry != null:
|
|
if village != null:
|
|
village.set_food(pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
|
tribe.set_food(tribe.get_food() - TRIBE_FOOD_DECAY)
|
|
if tribe.get_war_plan() == SimulationIds.WAR_PLAN_RAIDING:
|
|
return
|
|
if (
|
|
last_war_resolved_tick >= 0
|
|
and simulation_tick - last_war_resolved_tick < DEFENSE_DECISION_INTERVAL
|
|
):
|
|
return
|
|
if simulation_tick % DEFENSE_DECISION_INTERVAL != 0:
|
|
return
|
|
_evaluate_war(simulation_tick)
|
|
|
|
|
|
func _evaluate_war(simulation_tick: int) -> void:
|
|
var tribe := _get_faction(SimulationIds.FACTION_TRIBE)
|
|
var village := _get_faction(SimulationIds.FACTION_VILLAGE)
|
|
if tribe == null or village == null:
|
|
return
|
|
var desire := _tribe_desire(tribe, village)
|
|
var confidence := _tribe_confidence(tribe)
|
|
if desire < 0.5:
|
|
tribe.set_war_plan(SimulationIds.WAR_PLAN_NONE, simulation_tick, confidence)
|
|
return
|
|
if confidence >= 0.55:
|
|
if tribe.get_war_plan() == SimulationIds.WAR_PLAN_PLANNED:
|
|
_start_raid(tribe, simulation_tick)
|
|
else:
|
|
tribe.set_war_plan(SimulationIds.WAR_PLAN_PLANNED, simulation_tick, confidence)
|
|
return
|
|
if confidence < 0.35:
|
|
var had_plan := (
|
|
tribe.get_war_plan()
|
|
in [
|
|
SimulationIds.WAR_PLAN_PLANNED,
|
|
SimulationIds.WAR_PLAN_RAIDING,
|
|
]
|
|
)
|
|
tribe.set_war_plan(SimulationIds.WAR_PLAN_ABORTED, simulation_tick, confidence)
|
|
tribe.set_stance(SimulationIds.STANCE_NEUTRAL)
|
|
if had_plan:
|
|
war_aborted.emit()
|
|
_narrative_event_requested(
|
|
SimulationIds.EVENT_WAR_ABORTED,
|
|
SimulationIds.PLAYER_ACTOR_ID,
|
|
SimulationIds.FACTION_TRIBE,
|
|
"The hill tribe read the village's strength and turned away.",
|
|
"",
|
|
"",
|
|
0.0
|
|
)
|
|
return
|
|
if tribe.get_war_plan() != SimulationIds.WAR_PLAN_PLANNED:
|
|
tribe.set_war_plan(SimulationIds.WAR_PLAN_PLANNED, simulation_tick, confidence)
|
|
|
|
|
|
func _tribe_desire(tribe: FactionStateRecord, village: FactionStateRecord) -> float:
|
|
var hunger_need := clampf((30.0 - tribe.get_food()) / 30.0, 0.0, 1.0)
|
|
var surplus_lure := clampf(village.get_food() / 30.0, 0.0, 1.0)
|
|
return clampf((hunger_need + surplus_lure * 0.5) / 1.5, 0.0, 1.0) * tribe.get_aggression()
|
|
|
|
|
|
func _tribe_confidence(tribe: FactionStateRecord) -> float:
|
|
var morale_factor := 0.6 + 0.4 * tribe.get_morale()
|
|
var tribe_strength := float(tribe.get_warriors()) * morale_factor
|
|
var defense := village_defense()
|
|
if defense <= 0.0:
|
|
return 1.0
|
|
return clampf(tribe_strength / (defense * 1.1), 0.0, 1.0)
|
|
|
|
|
|
func village_defense() -> float:
|
|
var defense := 0.0
|
|
var eligible := 0
|
|
for npc in npcs:
|
|
if npc.is_dead:
|
|
continue
|
|
var is_guard := npc.profession == SimulationIds.PROFESSION_GUARD
|
|
if npc.strength < STRONG_STRENGTH and not is_guard:
|
|
continue
|
|
eligible += 1
|
|
defense += (
|
|
1.0 + (0.5 if is_guard else 0.0) + maxf(npc.strength - STRONG_STRENGTH, 0.0) * 0.1
|
|
)
|
|
var village := _get_faction(SimulationIds.FACTION_VILLAGE)
|
|
if village != null:
|
|
defense += village.get_food() / 100.0
|
|
return defense
|
|
|
|
|
|
func village_defender_count() -> int:
|
|
var count := 0
|
|
for npc in npcs:
|
|
if npc.is_dead:
|
|
continue
|
|
if npc.strength >= STRONG_STRENGTH or npc.profession == SimulationIds.PROFESSION_GUARD:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func _start_raid(tribe: FactionStateRecord, simulation_tick: int) -> void:
|
|
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 offset := Vector3(float(index * 2 - tribe.get_warriors()), 0.0, 0.0)
|
|
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)
|
|
combatant_spawned.emit(raider_id)
|
|
raid_started.emit(raider_ids)
|
|
_narrative_event_requested(
|
|
SimulationIds.EVENT_RAID_STARTED,
|
|
SimulationIds.PLAYER_ACTOR_ID,
|
|
SimulationIds.FACTION_TRIBE,
|
|
"The hill tribe, short of food, raids the village.",
|
|
"",
|
|
"",
|
|
0.0
|
|
)
|
|
|
|
|
|
func _run_battle() -> void:
|
|
var raid_active := is_raiding()
|
|
var living_hostiles: Array[CombatantStateRecord] = []
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if combatant == null or not combatant.is_alive() or not combatant.is_hostile():
|
|
continue
|
|
living_hostiles.append(combatant)
|
|
if not raid_active and living_hostiles.is_empty():
|
|
return
|
|
for hostile in living_hostiles:
|
|
var target := _find_nearest_defender(hostile.get_position(), _reach(hostile))
|
|
if target != null and hostile.is_attack_ready():
|
|
_attack_target(hostile, target)
|
|
if raid_active:
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if (
|
|
combatant == null
|
|
or not combatant.is_alive()
|
|
or combatant.get_kind() != SimulationIds.COMBATANT_KIND_NPC
|
|
):
|
|
continue
|
|
var hostile := _find_nearest_hostile(combatant.get_position(), _reach(combatant))
|
|
if hostile != null and combatant.is_attack_ready():
|
|
_attack_target(combatant, hostile)
|
|
_resolve_raid_outcome()
|
|
|
|
|
|
func _resolve_raid_outcome() -> void:
|
|
var tribe := _get_faction(SimulationIds.FACTION_TRIBE)
|
|
if tribe == null or tribe.get_war_plan() != SimulationIds.WAR_PLAN_RAIDING:
|
|
return
|
|
var living_raiders := 0
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if (
|
|
combatant != null
|
|
and combatant.is_alive()
|
|
and combatant.get_kind() == SimulationIds.COMBATANT_KIND_RAIDER
|
|
):
|
|
living_raiders += 1
|
|
var living_defenders := _living_village_npc_count()
|
|
if living_raiders == 0:
|
|
_finish_war(SimulationIds.WAR_PLAN_NONE, &"village_won")
|
|
elif living_defenders <= 0:
|
|
_finish_war(SimulationIds.WAR_PLAN_ABORTED, &"village_lost")
|
|
|
|
|
|
func _finish_war(next_plan: StringName, outcome: StringName) -> void:
|
|
var tribe := _get_faction(SimulationIds.FACTION_TRIBE)
|
|
var village := _get_faction(SimulationIds.FACTION_VILLAGE)
|
|
if tribe != null:
|
|
tribe.set_war_plan(next_plan, current_tick)
|
|
tribe.set_stance(SimulationIds.STANCE_NEUTRAL)
|
|
if village != null and outcome == &"village_lost":
|
|
var stolen := 0.0
|
|
if economy != null:
|
|
var pantry: StorageStateRecord = economy.get_pantry()
|
|
if pantry != null:
|
|
var requested := minf(pantry.get_amount(SimulationIds.RESOURCE_FOOD) * 0.6, 15.0)
|
|
stolen = economy.withdraw_resource(SimulationIds.RESOURCE_FOOD, requested)
|
|
village.set_food(pantry.get_amount(SimulationIds.RESOURCE_FOOD))
|
|
if tribe != null:
|
|
tribe.set_food(tribe.get_food() + stolen)
|
|
elif tribe != null:
|
|
tribe.set_food(tribe.get_food() + 2.0)
|
|
tribe.set_morale(tribe.get_morale() * 0.6)
|
|
last_war_resolved_tick = current_tick
|
|
war_resolved.emit(outcome)
|
|
_narrative_event_requested(
|
|
SimulationIds.EVENT_WAR_RESOLVED,
|
|
SimulationIds.PLAYER_ACTOR_ID,
|
|
SimulationIds.FACTION_TRIBE,
|
|
(
|
|
"The village drove off the raiders."
|
|
if outcome == &"village_won"
|
|
else "The raiders plundered the village."
|
|
),
|
|
"",
|
|
"",
|
|
0.0
|
|
)
|
|
|
|
|
|
func _attack_target(attacker: CombatantStateRecord, target: CombatantStateRecord) -> void:
|
|
if not attacker.is_attack_ready() or target == null:
|
|
return
|
|
var damage := _attack_damage(attacker)
|
|
var applied := _apply_damage(target, damage, _actor_id_for(attacker))
|
|
if applied > 0.0:
|
|
attacker.mark_attacked(tick_interval)
|
|
|
|
|
|
func _attack_damage(attacker: CombatantStateRecord) -> float:
|
|
var weapon := SimulationItems.get_weapon(attacker.get_weapon_id())
|
|
var base := weapon.damage if weapon != null else 5.0
|
|
if attacker.get_kind() != SimulationIds.COMBATANT_KIND_NPC:
|
|
return base
|
|
var npc := _find_npc(attacker.get_npc_id())
|
|
return base * (1.0 + (npc.strength * 0.04 if npc != null else 0.0))
|
|
|
|
|
|
func _find_nearest_defender(origin: Vector3, max_distance: float) -> CombatantStateRecord:
|
|
var best: CombatantStateRecord
|
|
var best_distance := max_distance * max_distance
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if combatant == null or not combatant.is_alive() or combatant.is_hostile():
|
|
continue
|
|
if (
|
|
combatant.get_kind()
|
|
not in [SimulationIds.COMBATANT_KIND_NPC, SimulationIds.COMBATANT_KIND_PLAYER]
|
|
):
|
|
continue
|
|
var distance := origin.distance_squared_to(combatant.get_position())
|
|
if distance > best_distance:
|
|
continue
|
|
best = combatant
|
|
best_distance = distance
|
|
return best
|
|
|
|
|
|
func _find_nearest_hostile(origin: Vector3, max_distance: float) -> CombatantStateRecord:
|
|
var best: CombatantStateRecord
|
|
var best_distance := max_distance * max_distance
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if combatant == null or not combatant.is_alive() or not combatant.is_hostile():
|
|
continue
|
|
var distance := origin.distance_squared_to(combatant.get_position())
|
|
if distance > best_distance:
|
|
continue
|
|
best = combatant
|
|
best_distance = distance
|
|
return best
|
|
|
|
|
|
func _reach(combatant: CombatantStateRecord) -> float:
|
|
var weapon := SimulationItems.get_weapon(combatant.get_weapon_id())
|
|
if weapon == null:
|
|
return 1.5
|
|
return weapon.reach
|
|
|
|
|
|
static func _horizontal_distance(first: Vector3, second: Vector3) -> float:
|
|
return Vector2(first.x, first.z).distance_to(Vector2(second.x, second.z))
|
|
|
|
|
|
func _actor_id_for(combatant: CombatantStateRecord) -> int:
|
|
if combatant.get_npc_id() >= 0:
|
|
return combatant.get_npc_id()
|
|
if combatant.get_kind() == SimulationIds.COMBATANT_KIND_RAIDER:
|
|
return SimulationIds.PLAYER_ACTOR_ID - 1
|
|
return SimulationIds.PLAYER_ACTOR_ID - 2
|
|
|
|
|
|
func _living_village_npc_count() -> int:
|
|
var count := 0
|
|
for npc in npcs:
|
|
if not npc.is_dead:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
func is_raiding() -> bool:
|
|
var tribe := _get_faction(SimulationIds.FACTION_TRIBE)
|
|
return tribe != null and tribe.get_war_plan() == SimulationIds.WAR_PLAN_RAIDING
|
|
|
|
|
|
func _sync_npc_positions() -> void:
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if combatant == null or combatant.get_kind() != SimulationIds.COMBATANT_KIND_NPC:
|
|
continue
|
|
var npc := _find_npc(combatant.get_npc_id())
|
|
if npc == null:
|
|
continue
|
|
if npc.is_dead:
|
|
if combatant.is_alive():
|
|
combatant.take_damage(combatant.get_health())
|
|
continue
|
|
combatant.set_position(npc.position)
|
|
|
|
|
|
func _tick_cooldowns() -> void:
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if combatant != null:
|
|
combatant.tick_cooldown()
|
|
|
|
|
|
func get_combatant(combatant_id: StringName) -> CombatantStateRecord:
|
|
return combatants.get(combatant_id) as CombatantStateRecord
|
|
|
|
|
|
func get_faction(faction_id: StringName) -> FactionStateRecord:
|
|
return factions.get(faction_id) as FactionStateRecord
|
|
|
|
|
|
func get_all_combatants() -> Array[CombatantStateRecord]:
|
|
var values: Array[CombatantStateRecord] = []
|
|
for combatant in combatants.values():
|
|
values.append(combatant)
|
|
return values
|
|
|
|
|
|
func get_living_hostiles() -> Array[CombatantStateRecord]:
|
|
var hostiles: Array[CombatantStateRecord] = []
|
|
for combatant_id in _sorted_combatant_ids():
|
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
|
if combatant != null and combatant.is_alive() and combatant.is_hostile():
|
|
hostiles.append(combatant)
|
|
return hostiles
|
|
|
|
|
|
func _sorted_combatant_ids() -> Array[StringName]:
|
|
if not _combatant_ids_dirty:
|
|
return _sorted_combatant_ids_cache
|
|
var keys: Array[String] = []
|
|
for combatant_id in combatants.keys():
|
|
keys.append(String(combatant_id))
|
|
keys.sort()
|
|
_sorted_combatant_ids_cache.clear()
|
|
for key in keys:
|
|
_sorted_combatant_ids_cache.append(StringName(key))
|
|
_combatant_ids_dirty = false
|
|
return _sorted_combatant_ids_cache
|
|
|
|
|
|
func _find_npc(npc_id: int) -> SimNPC:
|
|
return _npcs_by_id.get(npc_id) as SimNPC
|
|
|
|
|
|
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,
|
|
source_id: StringName,
|
|
action_display: String,
|
|
action_id: StringName,
|
|
item_id: StringName,
|
|
required_amount: float
|
|
) -> void:
|
|
narrative_event_requested.emit(
|
|
event_type, actor_id, source_id, action_display, action_id, item_id, required_amount
|
|
)
|
|
|
|
|
|
func append_state_records(record: SimulationStateRecord) -> void:
|
|
var combatant_keys: Array[String] = []
|
|
for combatant_id in combatants.keys():
|
|
if String(combatant_id) == String(PLAYER_COMBATANT_ID):
|
|
continue
|
|
combatant_keys.append(String(combatant_id))
|
|
combatant_keys.sort()
|
|
for combatant_key in combatant_keys:
|
|
var combatant := combatants[StringName(combatant_key)] as CombatantStateRecord
|
|
record.combatants.append(combatant)
|
|
var faction_keys: Array[String] = []
|
|
for faction_id in factions.keys():
|
|
faction_keys.append(String(faction_id))
|
|
faction_keys.sort()
|
|
for faction_key in faction_keys:
|
|
var faction := factions[StringName(faction_key)] as FactionStateRecord
|
|
record.factions.append(faction)
|
|
|
|
|
|
func restore_state_records(
|
|
combatant_records: Array[CombatantStateRecord],
|
|
faction_records: Array[FactionStateRecord],
|
|
restored_tick: int
|
|
) -> void:
|
|
current_tick = restored_tick
|
|
player_attack_cooldown_remaining = 0.0
|
|
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:
|
|
factions[faction.get_faction_id()] = faction
|
|
initialize_factions()
|