feat: shared creature root, enemy definitions, defend duty, and player downing
This commit is contained in:
@@ -44,8 +44,15 @@ func _resolve_references() -> void:
|
|||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
if not _references_resolved:
|
if not _references_resolved:
|
||||||
_resolve_references()
|
_resolve_references()
|
||||||
|
if simulation_manager != null and simulation_manager.has_method("update_player_combatant"):
|
||||||
|
simulation_manager.update_player_combatant(player.global_position)
|
||||||
attack_timer = maxf(attack_timer - delta, 0.0)
|
attack_timer = maxf(attack_timer - delta, 0.0)
|
||||||
dash_timer = maxf(dash_timer - delta, 0.0)
|
dash_timer = maxf(dash_timer - delta, 0.0)
|
||||||
|
if _player_is_downed():
|
||||||
|
attack_timer = 0.0
|
||||||
|
dash_timer = 0.0
|
||||||
|
dashing = false
|
||||||
|
return
|
||||||
if dashing:
|
if dashing:
|
||||||
player.velocity = dash_velocity
|
player.velocity = dash_velocity
|
||||||
if dash_timer <= 0.0:
|
if dash_timer <= 0.0:
|
||||||
@@ -58,6 +65,14 @@ func _physics_process(delta: float) -> void:
|
|||||||
_perform_dash()
|
_perform_dash()
|
||||||
|
|
||||||
|
|
||||||
|
func _player_is_downed() -> bool:
|
||||||
|
return (
|
||||||
|
simulation_manager != null
|
||||||
|
and simulation_manager.has_method("player_is_downed")
|
||||||
|
and simulation_manager.player_is_downed()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _perform_attack() -> void:
|
func _perform_attack() -> void:
|
||||||
attack_timer = attack_cooldown_seconds
|
attack_timer = attack_cooldown_seconds
|
||||||
_play_swing()
|
_play_swing()
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ func _create_task_glyph_mesh(action_id: StringName) -> PrimitiveMesh:
|
|||||||
var crate := BoxMesh.new()
|
var crate := BoxMesh.new()
|
||||||
crate.size = Vector3(0.34, 0.22, 0.34)
|
crate.size = Vector3(0.34, 0.22, 0.34)
|
||||||
return crate
|
return crate
|
||||||
SimulationIds.ACTION_PATROL:
|
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_DEFEND:
|
||||||
var shield := CylinderMesh.new()
|
var shield := CylinderMesh.new()
|
||||||
shield.top_radius = 0.2
|
shield.top_radius = 0.2
|
||||||
shield.bottom_radius = 0.2
|
shield.bottom_radius = 0.2
|
||||||
@@ -532,7 +532,7 @@ func _get_task_glyph_color(action_id: StringName) -> Color:
|
|||||||
return Color(0.58, 0.34, 0.16, 1.0)
|
return Color(0.58, 0.34, 0.16, 1.0)
|
||||||
SimulationIds.ACTION_DEPOSIT_FOOD:
|
SimulationIds.ACTION_DEPOSIT_FOOD:
|
||||||
return Color(0.95, 0.72, 0.32, 1.0)
|
return Color(0.95, 0.72, 0.32, 1.0)
|
||||||
SimulationIds.ACTION_PATROL:
|
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_DEFEND:
|
||||||
return Color(0.36, 0.58, 0.95, 1.0)
|
return Color(0.36, 0.58, 0.95, 1.0)
|
||||||
SimulationIds.ACTION_STUDY:
|
SimulationIds.ACTION_STUDY:
|
||||||
return Color(0.62, 0.46, 0.9, 1.0)
|
return Color(0.62, 0.46, 0.9, 1.0)
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
|
if _is_player_downed():
|
||||||
|
velocity.x = move_toward(velocity.x, 0.0, acceleration * delta)
|
||||||
|
velocity.z = move_toward(velocity.z, 0.0, acceleration * delta)
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
|
var input := Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
|
||||||
|
|
||||||
var forward := -camera_rig.global_transform.basis.z
|
var forward := -camera_rig.global_transform.basis.z
|
||||||
@@ -55,6 +60,14 @@ func _physics_process(delta: float) -> void:
|
|||||||
rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)
|
rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _is_player_downed() -> bool:
|
||||||
|
return (
|
||||||
|
simulation_manager != null
|
||||||
|
and simulation_manager.has_method("player_is_downed")
|
||||||
|
and simulation_manager.player_is_downed()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if event.is_action_pressed("interact"):
|
if event.is_action_pressed("interact"):
|
||||||
try_interact()
|
try_interact()
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ signal combatant_died(combatant_id: StringName)
|
|||||||
signal raid_started(raider_ids: Array[StringName])
|
signal raid_started(raider_ids: Array[StringName])
|
||||||
signal war_resolved(outcome: StringName)
|
signal war_resolved(outcome: StringName)
|
||||||
signal war_aborted
|
signal war_aborted
|
||||||
|
signal player_downed
|
||||||
|
|
||||||
var village := SimVillage.new()
|
var village := SimVillage.new()
|
||||||
var player_system := PlayerCitizenSystem.new()
|
var player_system := PlayerCitizenSystem.new()
|
||||||
@@ -98,6 +99,8 @@ func _ready() -> void:
|
|||||||
conflict_system.raid_started.connect(func(raider_ids): raid_started.emit(raider_ids))
|
conflict_system.raid_started.connect(func(raider_ids): raid_started.emit(raider_ids))
|
||||||
conflict_system.war_resolved.connect(func(outcome): war_resolved.emit(outcome))
|
conflict_system.war_resolved.connect(func(outcome): war_resolved.emit(outcome))
|
||||||
conflict_system.war_aborted.connect(func(): war_aborted.emit())
|
conflict_system.war_aborted.connect(func(): war_aborted.emit())
|
||||||
|
conflict_system.player_damaged.connect(_on_player_damaged)
|
||||||
|
conflict_system.player_downed.connect(func(): player_downed.emit())
|
||||||
action_selector.relationship_system = relationship_system
|
action_selector.relationship_system = relationship_system
|
||||||
var definition_errors := SimulationDefinitions.validate()
|
var definition_errors := SimulationDefinitions.validate()
|
||||||
if not definition_errors.is_empty():
|
if not definition_errors.is_empty():
|
||||||
@@ -258,8 +261,16 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
|
|||||||
record_narrative_event(SimulationIds.EVENT_VILLAGER_WEAK, npc.id)
|
record_narrative_event(SimulationIds.EVENT_VILLAGER_WEAK, npc.id)
|
||||||
var helper := get_active_opportunity_helper()
|
var helper := get_active_opportunity_helper()
|
||||||
var animal_feed_available := animal_care.has_available_feed_target(npc.id)
|
var animal_feed_available := animal_care.has_available_feed_target(npc.id)
|
||||||
|
var war_raid_active := conflict_system.is_raiding()
|
||||||
var selection := action_selector.select_action(
|
var selection := action_selector.select_action(
|
||||||
npc, village, clock.time_of_day(), npcs, helper, _population_view, animal_feed_available
|
npc,
|
||||||
|
village,
|
||||||
|
clock.time_of_day(),
|
||||||
|
npcs,
|
||||||
|
helper,
|
||||||
|
_population_view,
|
||||||
|
animal_feed_available,
|
||||||
|
war_raid_active
|
||||||
)
|
)
|
||||||
if selection == null:
|
if selection == null:
|
||||||
return
|
return
|
||||||
@@ -390,6 +401,8 @@ func _apply_action_completion(
|
|||||||
pass
|
pass
|
||||||
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY, SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
|
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY, SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
|
||||||
village.apply_npc_task(npc)
|
village.apply_npc_task(npc)
|
||||||
|
SimulationIds.ACTION_DEFEND:
|
||||||
|
village.apply_metric_delta(&"safety", 0.5)
|
||||||
if debug_logs and completed_task == SimulationIds.ACTION_SLEEP:
|
if debug_logs and completed_task == SimulationIds.ACTION_SLEEP:
|
||||||
print("[SimulationManager] ", npc.npc_name, " completed sleep at home")
|
print("[SimulationManager] ", npc.npc_name, " completed sleep at home")
|
||||||
|
|
||||||
@@ -1138,6 +1151,31 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
|||||||
|
|
||||||
func advance_player_needs() -> void:
|
func advance_player_needs() -> void:
|
||||||
player_system.advance_needs(village.food_modifier)
|
player_system.advance_needs(village.food_modifier)
|
||||||
|
player_system.player_state.advance_health()
|
||||||
|
_sync_player_combatant()
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_player_combatant() -> void:
|
||||||
|
conflict_system.set_player_health(
|
||||||
|
player_system.player_state.get_health(), player_system.player_state.is_downed()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_player_damaged(amount: float) -> void:
|
||||||
|
player_system.player_state.take_damage(amount)
|
||||||
|
_sync_player_combatant()
|
||||||
|
|
||||||
|
|
||||||
|
func update_player_combatant(position: Vector3) -> void:
|
||||||
|
conflict_system.set_player_combatant_position(position)
|
||||||
|
|
||||||
|
|
||||||
|
func player_is_downed() -> bool:
|
||||||
|
return player_system.player_state.is_downed()
|
||||||
|
|
||||||
|
|
||||||
|
func get_player_health() -> float:
|
||||||
|
return player_system.player_state.get_health()
|
||||||
|
|
||||||
|
|
||||||
func get_season() -> StringName:
|
func get_season() -> StringName:
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ func select_action(
|
|||||||
all_npcs: Array = [],
|
all_npcs: Array = [],
|
||||||
opportunity_helper: OpportunityHelperResult = null,
|
opportunity_helper: OpportunityHelperResult = null,
|
||||||
population_view: SimulationPopulationView = null,
|
population_view: SimulationPopulationView = null,
|
||||||
animal_feed_available: bool = false
|
animal_feed_available: bool = false,
|
||||||
|
war_raid_active: bool = false
|
||||||
) -> ActionSelectionResult:
|
) -> ActionSelectionResult:
|
||||||
if npc.is_dead:
|
if npc.is_dead:
|
||||||
return null
|
return null
|
||||||
@@ -115,6 +116,10 @@ func select_action(
|
|||||||
-1.0,
|
-1.0,
|
||||||
"Responding to village need: %s" % opportunity_helper.reason
|
"Responding to village need: %s" % opportunity_helper.reason
|
||||||
)
|
)
|
||||||
|
if war_raid_active and period == SchedulePeriod.WORK and _is_eligible_defender(npc):
|
||||||
|
return ActionSelectionResult.new(
|
||||||
|
SimulationIds.ACTION_DEFEND, -1.0, "Rallying to defend the village"
|
||||||
|
)
|
||||||
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
|
||||||
return ActionSelectionResult.new(
|
return ActionSelectionResult.new(
|
||||||
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
|
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
|
||||||
@@ -297,6 +302,10 @@ func _calculate_score(
|
|||||||
return score
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
static func _is_eligible_defender(npc: SimNPC) -> bool:
|
||||||
|
return npc.strength >= 5.0 or npc.profession == SimulationIds.PROFESSION_GUARD
|
||||||
|
|
||||||
|
|
||||||
static func _get_period(time_of_day: float) -> int:
|
static func _get_period(time_of_day: float) -> int:
|
||||||
if _in_wrap_range(time_of_day, SLEEP_BEGIN, SLEEP_END):
|
if _in_wrap_range(time_of_day, SLEEP_BEGIN, SLEEP_END):
|
||||||
return SchedulePeriod.SLEEP
|
return SchedulePeriod.SLEEP
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ signal combatant_died(combatant_id: StringName)
|
|||||||
signal raid_started(raider_ids: Array[StringName])
|
signal raid_started(raider_ids: Array[StringName])
|
||||||
signal war_resolved(outcome: StringName)
|
signal war_resolved(outcome: StringName)
|
||||||
signal war_aborted
|
signal war_aborted
|
||||||
|
signal player_damaged(amount: float)
|
||||||
|
signal player_downed
|
||||||
|
|
||||||
const NPC_MAX_HEALTH := 100.0
|
const NPC_MAX_HEALTH := 100.0
|
||||||
const STRONG_STRENGTH := 5.0
|
const STRONG_STRENGTH := 5.0
|
||||||
@@ -24,9 +26,7 @@ const WOLF_HUNT_RADIUS := 18.0
|
|||||||
const RAID_SPAWN_OFFSET := Vector3(42.0, 0.0, 42.0)
|
const RAID_SPAWN_OFFSET := Vector3(42.0, 0.0, 42.0)
|
||||||
const RAID_SPEED := 1.2
|
const RAID_SPEED := 1.2
|
||||||
const RAID_SIZE := 4
|
const RAID_SIZE := 4
|
||||||
const RAIDER_HEALTH := 60.0
|
const PLAYER_COMBATANT_ID := &"player_combatant"
|
||||||
const WOLF_HEALTH := 40.0
|
|
||||||
const RAIDER_REACH_FACTOR := 1.6
|
|
||||||
|
|
||||||
var combatants: Dictionary = {}
|
var combatants: Dictionary = {}
|
||||||
var factions: Dictionary = {}
|
var factions: Dictionary = {}
|
||||||
@@ -52,6 +52,53 @@ func initialize_factions() -> void:
|
|||||||
factions[SimulationIds.FACTION_TRIBE] = FactionStateRecord.create(
|
factions[SimulationIds.FACTION_TRIBE] = FactionStateRecord.create(
|
||||||
SimulationIds.FACTION_TRIBE, "Hill Tribe", 4.0, RAID_SIZE, 0.7, 0.7
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
if not downed and player_combatant.is_alive():
|
||||||
|
var target := clampf(health, 0.0, player_combatant.get_max_health())
|
||||||
|
var current := player_combatant.get_health()
|
||||||
|
if target < current:
|
||||||
|
player_combatant.take_damage(current - target)
|
||||||
|
elif target > current:
|
||||||
|
player_combatant.data["health"] = target
|
||||||
|
|
||||||
|
|
||||||
|
func is_player_downed() -> bool:
|
||||||
|
var player_combatant := get_combatant(PLAYER_COMBATANT_ID)
|
||||||
|
return player_combatant != null and not player_combatant.is_alive()
|
||||||
|
|
||||||
|
|
||||||
func register_npc_combatants(npc_list: Array[SimNPC]) -> void:
|
func register_npc_combatants(npc_list: Array[SimNPC]) -> void:
|
||||||
@@ -100,7 +147,7 @@ func advance(simulation_tick: int) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _advance_raider_positions() -> void:
|
func _advance_raider_positions() -> void:
|
||||||
if not _is_raiding():
|
if not is_raiding():
|
||||||
return
|
return
|
||||||
for combatant_id in _sorted_combatant_ids():
|
for combatant_id in _sorted_combatant_ids():
|
||||||
var combatant := combatants[combatant_id] as CombatantStateRecord
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
||||||
@@ -132,16 +179,17 @@ func player_attack(combatant_id: StringName) -> float:
|
|||||||
|
|
||||||
|
|
||||||
func spawn_wolf(position: Vector3) -> StringName:
|
func spawn_wolf(position: Vector3) -> StringName:
|
||||||
|
var definition := SimulationEnemies.get_enemy(&"enemy_wolf")
|
||||||
var wolf_id := StringName("wolf_%d" % next_wolf_id)
|
var wolf_id := StringName("wolf_%d" % next_wolf_id)
|
||||||
next_wolf_id += 1
|
next_wolf_id += 1
|
||||||
var wolf := CombatantStateRecord.create(
|
var wolf := CombatantStateRecord.create(
|
||||||
wolf_id,
|
wolf_id,
|
||||||
SimulationIds.COMBATANT_KIND_WOLF,
|
SimulationIds.COMBATANT_KIND_WOLF,
|
||||||
SimulationIds.FACTION_TRIBE,
|
SimulationIds.FACTION_TRIBE,
|
||||||
"Wolf",
|
definition.display_name if definition != null else "Wolf",
|
||||||
position,
|
position,
|
||||||
WOLF_HEALTH,
|
definition.health if definition != null else 40.0,
|
||||||
SimulationIds.ITEM_CLAW,
|
definition.weapon_id if definition != null else SimulationIds.ITEM_CLAW,
|
||||||
CombatantStateRecord.NO_NPC_ID,
|
CombatantStateRecord.NO_NPC_ID,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@@ -165,6 +213,11 @@ func _apply_damage(target: CombatantStateRecord, damage: float, actor_id: int) -
|
|||||||
target.get_weapon_id(),
|
target.get_weapon_id(),
|
||||||
applied
|
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():
|
if not target.is_alive():
|
||||||
_kill_combatant(target, actor_id)
|
_kill_combatant(target, actor_id)
|
||||||
return applied
|
return applied
|
||||||
@@ -313,6 +366,7 @@ func village_defender_count() -> int:
|
|||||||
|
|
||||||
|
|
||||||
func _start_raid(tribe: FactionStateRecord, simulation_tick: int) -> void:
|
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_war_plan(SimulationIds.WAR_PLAN_RAIDING, simulation_tick)
|
||||||
tribe.set_stance(SimulationIds.STANCE_HOSTILE)
|
tribe.set_stance(SimulationIds.STANCE_HOSTILE)
|
||||||
var raider_ids: Array[StringName] = []
|
var raider_ids: Array[StringName] = []
|
||||||
@@ -324,10 +378,10 @@ func _start_raid(tribe: FactionStateRecord, simulation_tick: int) -> void:
|
|||||||
raider_id,
|
raider_id,
|
||||||
SimulationIds.COMBATANT_KIND_RAIDER,
|
SimulationIds.COMBATANT_KIND_RAIDER,
|
||||||
SimulationIds.FACTION_TRIBE,
|
SimulationIds.FACTION_TRIBE,
|
||||||
"Raider",
|
definition.display_name if definition != null else "Raider",
|
||||||
village_center + RAID_SPAWN_OFFSET + offset,
|
village_center + RAID_SPAWN_OFFSET + offset,
|
||||||
RAIDER_HEALTH,
|
definition.health if definition != null else 60.0,
|
||||||
SimulationIds.ITEM_SWORD,
|
definition.weapon_id if definition != null else SimulationIds.ITEM_SWORD,
|
||||||
CombatantStateRecord.NO_NPC_ID,
|
CombatantStateRecord.NO_NPC_ID,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
@@ -347,7 +401,7 @@ func _start_raid(tribe: FactionStateRecord, simulation_tick: int) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _run_battle() -> void:
|
func _run_battle() -> void:
|
||||||
var raid_active := _is_raiding()
|
var raid_active := is_raiding()
|
||||||
var living_hostiles: Array[CombatantStateRecord] = []
|
var living_hostiles: Array[CombatantStateRecord] = []
|
||||||
for combatant_id in _sorted_combatant_ids():
|
for combatant_id in _sorted_combatant_ids():
|
||||||
var combatant := combatants[combatant_id] as CombatantStateRecord
|
var combatant := combatants[combatant_id] as CombatantStateRecord
|
||||||
@@ -483,11 +537,7 @@ func _reach(combatant: CombatantStateRecord) -> float:
|
|||||||
var weapon := SimulationItems.get_weapon(combatant.get_weapon_id())
|
var weapon := SimulationItems.get_weapon(combatant.get_weapon_id())
|
||||||
if weapon == null:
|
if weapon == null:
|
||||||
return 1.5
|
return 1.5
|
||||||
return (
|
return weapon.reach
|
||||||
weapon.reach * RAIDER_REACH_FACTOR
|
|
||||||
if combatant.get_kind() == SimulationIds.COMBATANT_KIND_WOLF
|
|
||||||
else weapon.reach
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _actor_id_for(combatant: CombatantStateRecord) -> int:
|
func _actor_id_for(combatant: CombatantStateRecord) -> int:
|
||||||
@@ -506,7 +556,7 @@ func _living_village_npc_count() -> int:
|
|||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
||||||
func _is_raiding() -> bool:
|
func is_raiding() -> bool:
|
||||||
var tribe := _get_faction(SimulationIds.FACTION_TRIBE)
|
var tribe := _get_faction(SimulationIds.FACTION_TRIBE)
|
||||||
return tribe != null and tribe.get_war_plan() == SimulationIds.WAR_PLAN_RAIDING
|
return tribe != null and tribe.get_war_plan() == SimulationIds.WAR_PLAN_RAIDING
|
||||||
|
|
||||||
@@ -596,6 +646,8 @@ func _narrative_event_requested(
|
|||||||
func append_state_records(record: SimulationStateRecord) -> void:
|
func append_state_records(record: SimulationStateRecord) -> void:
|
||||||
var combatant_keys: Array[String] = []
|
var combatant_keys: Array[String] = []
|
||||||
for combatant_id in combatants.keys():
|
for combatant_id in combatants.keys():
|
||||||
|
if String(combatant_id) == String(PLAYER_COMBATANT_ID):
|
||||||
|
continue
|
||||||
combatant_keys.append(String(combatant_id))
|
combatant_keys.append(String(combatant_id))
|
||||||
combatant_keys.sort()
|
combatant_keys.sort()
|
||||||
for combatant_key in combatant_keys:
|
for combatant_key in combatant_keys:
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
class_name EnemyDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var enemy_id: StringName
|
||||||
|
@export var display_name: String
|
||||||
|
@export var kind: StringName = SimulationIds.COMBATANT_KIND_RAIDER
|
||||||
|
@export var weapon_id: StringName = SimulationIds.ITEM_SWORD
|
||||||
|
@export var health := 60.0
|
||||||
|
@export var move_speed := 3.5
|
||||||
|
@export var body_color := Color(0.4, 0.13, 0.1, 1.0)
|
||||||
|
@export var accent_color := Color(0.55, 0.55, 0.58, 1.0)
|
||||||
|
@export var visual_scale := Vector3.ONE
|
||||||
|
|
||||||
|
|
||||||
|
func validate() -> Array[String]:
|
||||||
|
var errors: Array[String] = []
|
||||||
|
if enemy_id.is_empty():
|
||||||
|
errors.append("enemy_id is empty")
|
||||||
|
if display_name.is_empty():
|
||||||
|
errors.append("display_name is empty for '%s'" % enemy_id)
|
||||||
|
if (
|
||||||
|
kind
|
||||||
|
not in [
|
||||||
|
SimulationIds.COMBATANT_KIND_RAIDER,
|
||||||
|
SimulationIds.COMBATANT_KIND_WOLF,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
errors.append("kind '%s' is invalid for '%s'" % [kind, enemy_id])
|
||||||
|
if health <= 0.0:
|
||||||
|
errors.append("health must be positive for '%s'" % enemy_id)
|
||||||
|
return errors
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://5tfgb1oww0e0
|
||||||
@@ -13,7 +13,8 @@ const ACTION_PATHS := [
|
|||||||
"res://simulation/definitions/actions/deposit_food.tres",
|
"res://simulation/definitions/actions/deposit_food.tres",
|
||||||
"res://simulation/definitions/actions/withdraw_food.tres",
|
"res://simulation/definitions/actions/withdraw_food.tres",
|
||||||
"res://simulation/definitions/actions/sleep.tres",
|
"res://simulation/definitions/actions/sleep.tres",
|
||||||
"res://simulation/definitions/actions/deposit_wood.tres"
|
"res://simulation/definitions/actions/deposit_wood.tres",
|
||||||
|
"res://simulation/definitions/actions/defend.tres"
|
||||||
]
|
]
|
||||||
|
|
||||||
const PROFESSION_PATHS := [
|
const PROFESSION_PATHS := [
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
class_name SimulationEnemies
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
static var _enemies: Dictionary = {}
|
||||||
|
static var _cache_valid := false
|
||||||
|
|
||||||
|
|
||||||
|
static func _ensure_cache() -> void:
|
||||||
|
if _cache_valid:
|
||||||
|
return
|
||||||
|
_enemies = {}
|
||||||
|
var raider := EnemyDefinition.new()
|
||||||
|
raider.enemy_id = &"enemy_raider"
|
||||||
|
raider.display_name = "Raider"
|
||||||
|
raider.kind = SimulationIds.COMBATANT_KIND_RAIDER
|
||||||
|
raider.weapon_id = SimulationIds.ITEM_SWORD
|
||||||
|
raider.health = 60.0
|
||||||
|
raider.move_speed = 3.5
|
||||||
|
raider.body_color = Color(0.4, 0.13, 0.1, 1.0)
|
||||||
|
raider.accent_color = Color(0.55, 0.55, 0.58, 1.0)
|
||||||
|
_enemies[raider.enemy_id] = raider
|
||||||
|
|
||||||
|
var wolf := EnemyDefinition.new()
|
||||||
|
wolf.enemy_id = &"enemy_wolf"
|
||||||
|
wolf.display_name = "Wolf"
|
||||||
|
wolf.kind = SimulationIds.COMBATANT_KIND_WOLF
|
||||||
|
wolf.weapon_id = SimulationIds.ITEM_CLAW
|
||||||
|
wolf.health = 40.0
|
||||||
|
wolf.move_speed = 4.2
|
||||||
|
wolf.body_color = Color(0.32, 0.32, 0.34, 1.0)
|
||||||
|
wolf.accent_color = Color(0.95, 0.5, 0.12, 1.0)
|
||||||
|
wolf.visual_scale = Vector3(0.8, 0.8, 0.8)
|
||||||
|
_enemies[wolf.enemy_id] = wolf
|
||||||
|
_cache_valid = true
|
||||||
|
|
||||||
|
|
||||||
|
static func invalidate_cache() -> void:
|
||||||
|
_cache_valid = false
|
||||||
|
|
||||||
|
|
||||||
|
static func get_enemy(enemy_id: StringName) -> EnemyDefinition:
|
||||||
|
_ensure_cache()
|
||||||
|
return _enemies.get(enemy_id) as EnemyDefinition
|
||||||
|
|
||||||
|
|
||||||
|
static func get_all() -> Array[EnemyDefinition]:
|
||||||
|
_ensure_cache()
|
||||||
|
var values: Array[EnemyDefinition] = []
|
||||||
|
for enemy in _enemies.values():
|
||||||
|
values.append(enemy)
|
||||||
|
return values
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://daurjkbl4hcd8
|
||||||
@@ -18,6 +18,7 @@ const ACTION_WANDER := &"wander"
|
|||||||
const ACTION_DEPOSIT_FOOD := &"deposit_food"
|
const ACTION_DEPOSIT_FOOD := &"deposit_food"
|
||||||
const ACTION_WITHDRAW_FOOD := &"withdraw_food"
|
const ACTION_WITHDRAW_FOOD := &"withdraw_food"
|
||||||
const ACTION_DEPOSIT_WOOD := &"deposit_wood"
|
const ACTION_DEPOSIT_WOOD := &"deposit_wood"
|
||||||
|
const ACTION_DEFEND := &"defend"
|
||||||
|
|
||||||
const PROFESSION_FARMER := &"farmer"
|
const PROFESSION_FARMER := &"farmer"
|
||||||
const PROFESSION_WOODCUTTER := &"woodcutter"
|
const PROFESSION_WOODCUTTER := &"woodcutter"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
action_id = &"defend"
|
||||||
|
display_name = "Defend"
|
||||||
|
default_duration = 5.0
|
||||||
|
preferred_profession_id = &"guard"
|
||||||
|
target_type = &"activity"
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
class_name PlayerStateRecord
|
class_name PlayerStateRecord
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_VERSION := 1
|
const SCHEMA_VERSION := 2
|
||||||
|
const LEGACY_SCHEMA_VERSION := 1
|
||||||
|
const MAX_HEALTH := 100.0
|
||||||
|
|
||||||
var data: Dictionary
|
var data: Dictionary
|
||||||
|
|
||||||
@@ -18,6 +20,10 @@ static func create_default() -> PlayerStateRecord:
|
|||||||
"schema_version": SCHEMA_VERSION,
|
"schema_version": SCHEMA_VERSION,
|
||||||
"hunger": 40.0,
|
"hunger": 40.0,
|
||||||
"energy": 100.0,
|
"energy": 100.0,
|
||||||
|
"health": MAX_HEALTH,
|
||||||
|
"max_health": MAX_HEALTH,
|
||||||
|
"downed": false,
|
||||||
|
"downed_ticks": 0,
|
||||||
"inventory": {},
|
"inventory": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -25,15 +31,27 @@ static func create_default() -> PlayerStateRecord:
|
|||||||
|
|
||||||
|
|
||||||
static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
|
static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
|
||||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
var version := int(record_data.get("schema_version", -1))
|
||||||
|
if version == LEGACY_SCHEMA_VERSION:
|
||||||
|
record_data = _migrate_v1(record_data)
|
||||||
|
elif version != SCHEMA_VERSION:
|
||||||
return null
|
return null
|
||||||
if not record_data.has_all(["hunger", "energy", "inventory"]):
|
if not record_data.has_all(
|
||||||
|
["hunger", "energy", "health", "max_health", "downed", "downed_ticks", "inventory"]
|
||||||
|
):
|
||||||
return null
|
return null
|
||||||
if not record_data["inventory"] is Dictionary:
|
if not record_data["inventory"] is Dictionary:
|
||||||
return null
|
return null
|
||||||
var hunger := float(record_data["hunger"])
|
var hunger := float(record_data["hunger"])
|
||||||
var energy := float(record_data["energy"])
|
var energy := float(record_data["energy"])
|
||||||
if not is_finite(hunger) or not is_finite(energy):
|
var health := float(record_data["health"])
|
||||||
|
var max_health := float(record_data["max_health"])
|
||||||
|
if (
|
||||||
|
not is_finite(hunger)
|
||||||
|
or not is_finite(energy)
|
||||||
|
or not is_finite(health)
|
||||||
|
or not is_finite(max_health)
|
||||||
|
):
|
||||||
return null
|
return null
|
||||||
var normalized_inventory := {}
|
var normalized_inventory := {}
|
||||||
for raw_item_id in record_data["inventory"]:
|
for raw_item_id in record_data["inventory"]:
|
||||||
@@ -54,12 +72,26 @@ static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
|
|||||||
"schema_version": SCHEMA_VERSION,
|
"schema_version": SCHEMA_VERSION,
|
||||||
"hunger": clampf(hunger, 0.0, 100.0),
|
"hunger": clampf(hunger, 0.0, 100.0),
|
||||||
"energy": clampf(energy, 0.0, 100.0),
|
"energy": clampf(energy, 0.0, 100.0),
|
||||||
|
"health": clampf(health, 0.0, max_health),
|
||||||
|
"max_health": max_health,
|
||||||
|
"downed": bool(record_data["downed"]),
|
||||||
|
"downed_ticks": int(record_data["downed_ticks"]),
|
||||||
"inventory": normalized_inventory,
|
"inventory": normalized_inventory,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _migrate_v1(legacy_data: Dictionary) -> Dictionary:
|
||||||
|
var migrated := legacy_data.duplicate(true)
|
||||||
|
migrated["schema_version"] = SCHEMA_VERSION
|
||||||
|
migrated["health"] = MAX_HEALTH
|
||||||
|
migrated["max_health"] = MAX_HEALTH
|
||||||
|
migrated["downed"] = false
|
||||||
|
migrated["downed_ticks"] = 0
|
||||||
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
func get_hunger() -> float:
|
func get_hunger() -> float:
|
||||||
return clampf(float(data["hunger"]), 0.0, 100.0)
|
return clampf(float(data["hunger"]), 0.0, 100.0)
|
||||||
|
|
||||||
@@ -68,6 +100,22 @@ func get_energy() -> float:
|
|||||||
return clampf(float(data["energy"]), 0.0, 100.0)
|
return clampf(float(data["energy"]), 0.0, 100.0)
|
||||||
|
|
||||||
|
|
||||||
|
func get_health() -> float:
|
||||||
|
return clampf(float(data["health"]), 0.0, get_max_health())
|
||||||
|
|
||||||
|
|
||||||
|
func get_max_health() -> float:
|
||||||
|
return maxf(float(data["max_health"]), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func is_downed() -> bool:
|
||||||
|
return bool(data["downed"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_downed_ticks() -> int:
|
||||||
|
return int(data["downed_ticks"])
|
||||||
|
|
||||||
|
|
||||||
func set_hunger(amount: float) -> void:
|
func set_hunger(amount: float) -> void:
|
||||||
data["hunger"] = clampf(amount, 0.0, 100.0)
|
data["hunger"] = clampf(amount, 0.0, 100.0)
|
||||||
|
|
||||||
@@ -76,6 +124,28 @@ func set_energy(amount: float) -> void:
|
|||||||
data["energy"] = clampf(amount, 0.0, 100.0)
|
data["energy"] = clampf(amount, 0.0, 100.0)
|
||||||
|
|
||||||
|
|
||||||
|
func take_damage(amount: float) -> float:
|
||||||
|
if not is_finite(amount) or amount <= 0.0 or is_downed():
|
||||||
|
return 0.0
|
||||||
|
var previous := get_health()
|
||||||
|
data["health"] = clampf(previous - amount, 0.0, get_max_health())
|
||||||
|
if get_health() <= 0.0:
|
||||||
|
data["downed"] = true
|
||||||
|
data["downed_ticks"] = 20
|
||||||
|
return previous - get_health()
|
||||||
|
|
||||||
|
|
||||||
|
func advance_health() -> void:
|
||||||
|
if is_downed():
|
||||||
|
data["downed_ticks"] = maxi(get_downed_ticks() - 1, 0)
|
||||||
|
if get_downed_ticks() <= 0:
|
||||||
|
data["downed"] = false
|
||||||
|
data["health"] = get_max_health() * 0.5
|
||||||
|
return
|
||||||
|
if get_health() < get_max_health():
|
||||||
|
data["health"] = clampf(get_health() + 0.25, 0.0, get_max_health())
|
||||||
|
|
||||||
|
|
||||||
func get_inventory_amount(item_id: StringName) -> float:
|
func get_inventory_amount(item_id: StringName) -> float:
|
||||||
return float(data["inventory"].get(String(item_id), 0.0))
|
return float(data["inventory"].get(String(item_id), 0.0))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_test_enemy_definitions()
|
||||||
|
_test_player_downing()
|
||||||
|
_test_defend_selection()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_enemy_definitions() -> void:
|
||||||
|
var raider: EnemyDefinition = SimulationEnemies.get_enemy(&"enemy_raider")
|
||||||
|
var wolf: EnemyDefinition = SimulationEnemies.get_enemy(&"enemy_wolf")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
raider != null
|
||||||
|
and wolf != null
|
||||||
|
and raider.kind == SimulationIds.COMBATANT_KIND_RAIDER
|
||||||
|
and wolf.kind == SimulationIds.COMBATANT_KIND_WOLF
|
||||||
|
and raider.weapon_id == SimulationIds.ITEM_SWORD
|
||||||
|
and wolf.weapon_id == SimulationIds.ITEM_CLAW
|
||||||
|
),
|
||||||
|
"Enemy definitions should describe raiders and wolves as data"
|
||||||
|
)
|
||||||
|
var manager := _create_manager(991)
|
||||||
|
var wolf_id: StringName = manager.spawn_wolf(Vector3.ZERO)
|
||||||
|
var wolf_combatant: CombatantStateRecord = manager.get_combatant(wolf_id)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
is_equal_approx(wolf_combatant.get_health(), wolf.health)
|
||||||
|
and wolf_combatant.get_weapon_id() == wolf.weapon_id
|
||||||
|
),
|
||||||
|
"Spawned enemies should inherit their definition's health and weapon"
|
||||||
|
)
|
||||||
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_player_downing() -> void:
|
||||||
|
var manager := _create_manager(992)
|
||||||
|
var player_state: PlayerStateRecord = manager.get_player_state()
|
||||||
|
_check(
|
||||||
|
is_equal_approx(player_state.get_health(), 100.0) and not player_state.is_downed(),
|
||||||
|
"The player should begin healthy and standing"
|
||||||
|
)
|
||||||
|
var wolf_id: StringName = manager.spawn_wolf(Vector3.ZERO)
|
||||||
|
manager.update_player_combatant(Vector3.ZERO)
|
||||||
|
for _tick in 3:
|
||||||
|
manager.simulate_tick()
|
||||||
|
_check(
|
||||||
|
player_state.get_health() < 100.0 and not player_state.is_downed(),
|
||||||
|
"A hungry wolf should wound the player during a hunt"
|
||||||
|
)
|
||||||
|
var downed := false
|
||||||
|
for _tick in 60:
|
||||||
|
manager.simulate_tick()
|
||||||
|
if player_state.is_downed():
|
||||||
|
downed = true
|
||||||
|
break
|
||||||
|
_check(downed, "A prolonged wolf attack should eventually down the player")
|
||||||
|
if downed:
|
||||||
|
for _tick in 22:
|
||||||
|
manager.simulate_tick()
|
||||||
|
_check(not player_state.is_downed(), "The downed player should recover and stand again")
|
||||||
|
_check(
|
||||||
|
player_state.get_health() > 0.0, "Recovery should restore the player to a fighting baseline"
|
||||||
|
)
|
||||||
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_defend_selection() -> void:
|
||||||
|
var manager := _create_manager(993)
|
||||||
|
manager.clock.elapsed_ticks = 100
|
||||||
|
var tribe: FactionStateRecord = manager.get_faction(SimulationIds.FACTION_TRIBE)
|
||||||
|
tribe.set_war_plan(SimulationIds.WAR_PLAN_RAIDING, 1)
|
||||||
|
var defender: SimNPC = manager.npcs[0]
|
||||||
|
defender.strength = 9.0
|
||||||
|
defender.hunger = 20.0
|
||||||
|
defender.energy = 90.0
|
||||||
|
defender.is_starving = false
|
||||||
|
defender.mourning_ticks = 0
|
||||||
|
defender.inventory.clear()
|
||||||
|
defender.task_state = SimNPC.TASK_STATE_IDLE
|
||||||
|
defender.task_complete = true
|
||||||
|
manager.call("_select_action_if_idle", defender, SimNPC.TASK_STATE_IDLE)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
defender.current_task == SimulationIds.ACTION_DEFEND
|
||||||
|
and manager.conflict_system.is_raiding()
|
||||||
|
),
|
||||||
|
"A strong idle villager should rally to defend during an active raid"
|
||||||
|
)
|
||||||
|
var worker: SimNPC = manager.npcs[1]
|
||||||
|
worker.strength = 1.0
|
||||||
|
worker.hunger = 20.0
|
||||||
|
worker.energy = 90.0
|
||||||
|
worker.is_starving = false
|
||||||
|
worker.mourning_ticks = 0
|
||||||
|
worker.inventory.clear()
|
||||||
|
worker.task_state = SimNPC.TASK_STATE_IDLE
|
||||||
|
worker.task_complete = true
|
||||||
|
manager.call("_select_action_if_idle", worker, SimNPC.TASK_STATE_IDLE)
|
||||||
|
_check(
|
||||||
|
worker.current_task != SimulationIds.ACTION_DEFEND,
|
||||||
|
"A weak non-guard villager should not be conscripted as a defender"
|
||||||
|
)
|
||||||
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_manager(seed_value: int = 991) -> Node:
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.simulation_seed = seed_value
|
||||||
|
manager.debug_logs = false
|
||||||
|
var home_positions: Array[Vector3] = [
|
||||||
|
Vector3(0.0, 0.0, 0.0),
|
||||||
|
Vector3(2.0, 0.0, 0.0),
|
||||||
|
Vector3(10.0, 0.0, 0.0),
|
||||||
|
Vector3(12.0, 0.0, 0.0),
|
||||||
|
Vector3(30.0, 0.0, 30.0),
|
||||||
|
Vector3(40.0, 0.0, 40.0),
|
||||||
|
]
|
||||||
|
manager.home_positions = home_positions
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.set_process(false)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Combat patterns passed: enemy data, player downing, defend duty")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c5x8usbsl6aku
|
||||||
@@ -17,6 +17,7 @@ func _run() -> void:
|
|||||||
|
|
||||||
func _test_combat_and_wolf() -> void:
|
func _test_combat_and_wolf() -> void:
|
||||||
var manager := _create_manager(981)
|
var manager := _create_manager(981)
|
||||||
|
manager.update_player_combatant(Vector3(100.0, 0.0, 100.0))
|
||||||
var npc: SimNPC = manager.npcs[0]
|
var npc: SimNPC = manager.npcs[0]
|
||||||
npc.position = Vector3.ZERO
|
npc.position = Vector3.ZERO
|
||||||
var npc_combatant: CombatantStateRecord = manager.get_combatant(
|
var npc_combatant: CombatantStateRecord = manager.get_combatant(
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func _test_registry_integrity() -> void:
|
|||||||
"Action lookup should return '%s'" % definition.action_id
|
"Action lookup should return '%s'" % definition.action_id
|
||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
action_ids.size() == 12, "Registry should contain all twelve executable prototype actions"
|
action_ids.size() == 13, "Registry should contain all thirteen executable prototype actions"
|
||||||
)
|
)
|
||||||
|
|
||||||
var profession_ids := SimulationDefinitions.get_profession_ids()
|
var profession_ids := SimulationDefinitions.get_profession_ids()
|
||||||
|
|||||||
@@ -48,7 +48,12 @@ func get_interaction_position() -> Vector3:
|
|||||||
|
|
||||||
|
|
||||||
func supports_action(candidate_action_id: StringName) -> bool:
|
func supports_action(candidate_action_id: StringName) -> bool:
|
||||||
return action_id == candidate_action_id
|
if action_id == candidate_action_id:
|
||||||
|
return true
|
||||||
|
return (
|
||||||
|
action_id == SimulationIds.ACTION_PATROL
|
||||||
|
and candidate_action_id == SimulationIds.ACTION_DEFEND
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _update_presentation() -> void:
|
func _update_presentation() -> void:
|
||||||
|
|||||||
@@ -1,48 +1,39 @@
|
|||||||
class_name HostileCombatant
|
class_name HostileCombatant
|
||||||
extends Node3D
|
extends CreatureVisual
|
||||||
|
|
||||||
const FOLLOW_SPEED := 6.0
|
|
||||||
const ARRIVE_DISTANCE := 0.5
|
|
||||||
|
|
||||||
var combatant_id: StringName
|
|
||||||
var combatant: CombatantStateRecord
|
var combatant: CombatantStateRecord
|
||||||
var simulation_manager: Node
|
var simulation_manager: Node
|
||||||
var is_dead_visual := false
|
var _flash_tween: Tween
|
||||||
var flash_tween: Tween
|
var _spawn_tween: Tween
|
||||||
var spawn_scale := Vector3.ONE
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
spawn_scale = Vector3.ONE
|
add_to_group("grass_interactors")
|
||||||
scale = Vector3.ONE * 0.001
|
scale = Vector3.ONE * 0.001
|
||||||
var tween := create_tween()
|
_spawn_tween = create_tween()
|
||||||
tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
_spawn_tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
tween.tween_property(self, "scale", Vector3.ONE, 0.28)
|
_spawn_tween.tween_property(self, "scale", Vector3.ONE, 0.28)
|
||||||
|
|
||||||
|
|
||||||
func bind(combatant_id_value: StringName, manager: Node) -> void:
|
func bind(combatant_id_value: StringName, manager: Node) -> void:
|
||||||
combatant_id = combatant_id_value
|
creature_id = combatant_id_value
|
||||||
simulation_manager = manager
|
simulation_manager = manager
|
||||||
combatant = manager.get_combatant(combatant_id) as CombatantStateRecord
|
combatant = manager.get_combatant(creature_id) as CombatantStateRecord
|
||||||
if combatant == null:
|
if combatant == null:
|
||||||
return
|
return
|
||||||
global_position = combatant.get_position()
|
global_position = combatant.get_position()
|
||||||
_build_body(combatant.get_kind())
|
sim_position = combatant.get_position()
|
||||||
|
_build_from_definition()
|
||||||
combatant.changed.connect(_on_combatant_changed)
|
combatant.changed.connect(_on_combatant_changed)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
if is_dead_visual or combatant == null or not is_instance_valid(combatant):
|
if is_dead_visual or combatant == null or not is_instance_valid(combatant):
|
||||||
return
|
return
|
||||||
if not combatant.is_alive():
|
if not combatant.is_alive():
|
||||||
_play_death()
|
play_death()
|
||||||
return
|
return
|
||||||
var target_position := combatant.get_position()
|
set_sim_position(combatant.get_position())
|
||||||
var to_target := target_position - global_position
|
|
||||||
if to_target.length() <= ARRIVE_DISTANCE:
|
|
||||||
return
|
|
||||||
global_position += to_target.normalized() * minf(FOLLOW_SPEED * delta, to_target.length())
|
|
||||||
rotation.y = lerp_angle(rotation.y, atan2(to_target.x, to_target.z), delta * 8.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_combatant_changed(_state: CombatantStateRecord) -> void:
|
func _on_combatant_changed(_state: CombatantStateRecord) -> void:
|
||||||
@@ -52,42 +43,54 @@ func _on_combatant_changed(_state: CombatantStateRecord) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _flash_hit() -> void:
|
func _flash_hit() -> void:
|
||||||
if flash_tween != null and flash_tween.is_valid():
|
if _flash_tween != null and _flash_tween.is_valid():
|
||||||
flash_tween.kill()
|
_flash_tween.kill()
|
||||||
flash_tween = create_tween()
|
_flash_tween = create_tween()
|
||||||
flash_tween.tween_property(self, "scale", Vector3.ONE * 0.85, 0.06)
|
_flash_tween.tween_property(self, "scale", _definition_scale() * 0.85, 0.06)
|
||||||
flash_tween.tween_property(self, "scale", Vector3.ONE, 0.12)
|
_flash_tween.tween_property(self, "scale", _definition_scale(), 0.12)
|
||||||
|
|
||||||
|
|
||||||
func _play_death() -> void:
|
func play_death() -> void:
|
||||||
is_dead_visual = true
|
is_dead_visual = true
|
||||||
stop_travel_visual()
|
super.play_death()
|
||||||
var tween := create_tween()
|
|
||||||
tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
|
||||||
tween.tween_property(self, "scale", Vector3(0.001, 0.001, 0.001), 0.45)
|
|
||||||
tween.tween_callback(queue_free)
|
|
||||||
|
|
||||||
|
|
||||||
func stop_travel_visual() -> void:
|
func _definition_scale() -> Vector3:
|
||||||
pass
|
var definition := _enemy_definition()
|
||||||
|
return definition.visual_scale if definition != null else Vector3.ONE
|
||||||
|
|
||||||
|
|
||||||
func _build_body(kind: StringName) -> void:
|
func _build_from_definition() -> void:
|
||||||
if kind == SimulationIds.COMBATANT_KIND_WOLF:
|
var definition := _enemy_definition()
|
||||||
_build_wolf()
|
if definition != null:
|
||||||
|
move_speed = definition.move_speed
|
||||||
|
scale = definition.visual_scale
|
||||||
|
if definition.kind == SimulationIds.COMBATANT_KIND_WOLF:
|
||||||
|
_build_wolf(definition)
|
||||||
else:
|
else:
|
||||||
_build_raider()
|
_build_raider(definition)
|
||||||
|
|
||||||
|
|
||||||
func _build_raider() -> void:
|
func _enemy_definition() -> EnemyDefinition:
|
||||||
|
if combatant == null:
|
||||||
|
return null
|
||||||
|
var enemy_id: StringName = (
|
||||||
|
&"enemy_wolf"
|
||||||
|
if combatant.get_kind() == SimulationIds.COMBATANT_KIND_WOLF
|
||||||
|
else &"enemy_raider"
|
||||||
|
)
|
||||||
|
return SimulationEnemies.get_enemy(enemy_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_raider(definition: EnemyDefinition) -> void:
|
||||||
var tunic := StandardMaterial3D.new()
|
var tunic := StandardMaterial3D.new()
|
||||||
tunic.albedo_color = Color(0.4, 0.13, 0.1, 1.0)
|
tunic.albedo_color = definition.body_color
|
||||||
tunic.roughness = 0.85
|
tunic.roughness = 0.85
|
||||||
var skin := StandardMaterial3D.new()
|
var skin := StandardMaterial3D.new()
|
||||||
skin.albedo_color = Color(0.8, 0.58, 0.36, 1.0)
|
skin.albedo_color = Color(0.8, 0.58, 0.36, 1.0)
|
||||||
skin.roughness = 0.9
|
skin.roughness = 0.9
|
||||||
var metal := StandardMaterial3D.new()
|
var metal := StandardMaterial3D.new()
|
||||||
metal.albedo_color = Color(0.55, 0.55, 0.58, 1.0)
|
metal.albedo_color = definition.accent_color
|
||||||
metal.roughness = 0.35
|
metal.roughness = 0.35
|
||||||
|
|
||||||
_add_mesh("Body", CylinderMesh.new(), Vector3(0.0, 1.1, 0.0), tunic)
|
_add_mesh("Body", CylinderMesh.new(), Vector3(0.0, 1.1, 0.0), tunic)
|
||||||
@@ -104,12 +107,12 @@ func _build_raider() -> void:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _build_wolf() -> void:
|
func _build_wolf(definition: EnemyDefinition) -> void:
|
||||||
var fur := StandardMaterial3D.new()
|
var fur := StandardMaterial3D.new()
|
||||||
fur.albedo_color = Color(0.32, 0.32, 0.34, 1.0)
|
fur.albedo_color = definition.body_color
|
||||||
fur.roughness = 0.95
|
fur.roughness = 0.95
|
||||||
var eye := StandardMaterial3D.new()
|
var eye := StandardMaterial3D.new()
|
||||||
eye.albedo_color = Color(0.95, 0.5, 0.12, 1.0)
|
eye.albedo_color = definition.accent_color
|
||||||
eye.roughness = 0.3
|
eye.roughness = 0.3
|
||||||
|
|
||||||
var body := SphereMesh.new()
|
var body := SphereMesh.new()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func _on_combatant_spawned(combatant_id: StringName) -> void:
|
|||||||
func _on_combatant_died(combatant_id: StringName) -> void:
|
func _on_combatant_died(combatant_id: StringName) -> void:
|
||||||
var scene := hostiles.get(combatant_id) as HostileCombatant
|
var scene := hostiles.get(combatant_id) as HostileCombatant
|
||||||
if scene != null:
|
if scene != null:
|
||||||
scene._play_death()
|
scene.play_death()
|
||||||
hostiles.erase(combatant_id)
|
hostiles.erase(combatant_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
class_name CreatureVisual
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
signal position_changed(creature_id: StringName, active_position: Vector3)
|
||||||
|
signal arrived_at_sim_position(creature_id: StringName)
|
||||||
|
|
||||||
|
const ARRIVAL_DISTANCE := 0.5
|
||||||
|
const PATH_RETARGET_DISTANCE := 0.8
|
||||||
|
|
||||||
|
@export_range(0.5, 12.0, 0.1) var move_speed := 3.5
|
||||||
|
@export_range(0.05, 1.0, 0.05) var waypoint_reached_distance := 0.3
|
||||||
|
@export_range(1.0, 20.0, 0.5) var rotation_speed := 8.0
|
||||||
|
|
||||||
|
var creature_id: StringName
|
||||||
|
var sim_position := Vector3.INF
|
||||||
|
var is_dead_visual := false
|
||||||
|
|
||||||
|
var current_path := PackedVector3Array()
|
||||||
|
var path_index := 0
|
||||||
|
var path_target := Vector3.INF
|
||||||
|
var path_request_id := 0
|
||||||
|
var path_pending := false
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if is_dead_visual:
|
||||||
|
return
|
||||||
|
if not sim_position.is_finite():
|
||||||
|
return
|
||||||
|
if _horizontal_distance_to(sim_position) <= ARRIVAL_DISTANCE:
|
||||||
|
_stop_moving()
|
||||||
|
return
|
||||||
|
_ensure_path()
|
||||||
|
if path_pending:
|
||||||
|
return
|
||||||
|
if current_path.is_empty() or path_index >= current_path.size():
|
||||||
|
_request_path()
|
||||||
|
return
|
||||||
|
var next_position := current_path[path_index]
|
||||||
|
var offset := next_position - global_position
|
||||||
|
if offset.length() <= waypoint_reached_distance:
|
||||||
|
path_index += 1
|
||||||
|
return
|
||||||
|
var direction := offset.normalized()
|
||||||
|
global_position = global_position.move_toward(next_position, move_speed * delta)
|
||||||
|
position_changed.emit(creature_id, global_position)
|
||||||
|
var target_angle := atan2(direction.x, direction.z)
|
||||||
|
rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)
|
||||||
|
|
||||||
|
|
||||||
|
func set_sim_position(position: Vector3) -> void:
|
||||||
|
if not position.is_finite():
|
||||||
|
return
|
||||||
|
var moved := (
|
||||||
|
not path_target.is_finite()
|
||||||
|
or _horizontal_distance_to_position(sim_position, position) > PATH_RETARGET_DISTANCE
|
||||||
|
)
|
||||||
|
sim_position = position
|
||||||
|
if moved and not is_dead_visual:
|
||||||
|
_request_path()
|
||||||
|
|
||||||
|
|
||||||
|
func play_death() -> void:
|
||||||
|
is_dead_visual = true
|
||||||
|
_stop_moving()
|
||||||
|
var tween := create_tween()
|
||||||
|
tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||||
|
tween.tween_property(self, "scale", Vector3(0.001, 0.001, 0.001), 0.45)
|
||||||
|
tween.tween_callback(queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
func build_visual() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_path() -> void:
|
||||||
|
if (
|
||||||
|
path_pending
|
||||||
|
or (
|
||||||
|
path_target.is_finite()
|
||||||
|
and (
|
||||||
|
_horizontal_distance_to_position(path_target, sim_position)
|
||||||
|
<= PATH_RETARGET_DISTANCE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_request_path()
|
||||||
|
|
||||||
|
|
||||||
|
func _request_path() -> void:
|
||||||
|
path_request_id += 1
|
||||||
|
var request_id := path_request_id
|
||||||
|
path_target = sim_position
|
||||||
|
current_path = PackedVector3Array()
|
||||||
|
path_index = 0
|
||||||
|
path_pending = true
|
||||||
|
await get_tree().physics_frame
|
||||||
|
if request_id != path_request_id or is_dead_visual:
|
||||||
|
return
|
||||||
|
if _horizontal_distance_to(sim_position) <= ARRIVAL_DISTANCE:
|
||||||
|
path_pending = false
|
||||||
|
return
|
||||||
|
current_path = NavigationServer3D.map_get_path(
|
||||||
|
get_world_3d().navigation_map, global_position, sim_position, true
|
||||||
|
)
|
||||||
|
path_pending = false
|
||||||
|
if current_path.is_empty():
|
||||||
|
_stop_moving()
|
||||||
|
|
||||||
|
|
||||||
|
func _stop_moving() -> void:
|
||||||
|
path_request_id += 1
|
||||||
|
current_path = PackedVector3Array()
|
||||||
|
path_index = 0
|
||||||
|
path_pending = false
|
||||||
|
path_target = Vector3.INF
|
||||||
|
|
||||||
|
|
||||||
|
func _horizontal_distance_to(target_position: Vector3) -> float:
|
||||||
|
return _horizontal_distance_to_position(global_position, target_position)
|
||||||
|
|
||||||
|
|
||||||
|
static func _horizontal_distance_to_position(from_position: Vector3, to_position: Vector3) -> float:
|
||||||
|
return Vector2(from_position.x, from_position.z).distance_to(
|
||||||
|
Vector2(to_position.x, to_position.z)
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://6r66un51bane
|
||||||
Reference in New Issue
Block a user