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:
|
||||
if not _references_resolved:
|
||||
_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)
|
||||
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:
|
||||
player.velocity = dash_velocity
|
||||
if dash_timer <= 0.0:
|
||||
@@ -58,6 +65,14 @@ func _physics_process(delta: float) -> void:
|
||||
_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:
|
||||
attack_timer = attack_cooldown_seconds
|
||||
_play_swing()
|
||||
|
||||
@@ -492,7 +492,7 @@ func _create_task_glyph_mesh(action_id: StringName) -> PrimitiveMesh:
|
||||
var crate := BoxMesh.new()
|
||||
crate.size = Vector3(0.34, 0.22, 0.34)
|
||||
return crate
|
||||
SimulationIds.ACTION_PATROL:
|
||||
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_DEFEND:
|
||||
var shield := CylinderMesh.new()
|
||||
shield.top_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)
|
||||
SimulationIds.ACTION_DEPOSIT_FOOD:
|
||||
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)
|
||||
SimulationIds.ACTION_STUDY:
|
||||
return Color(0.62, 0.46, 0.9, 1.0)
|
||||
|
||||
@@ -25,6 +25,11 @@ func _ready() -> 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 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)
|
||||
|
||||
|
||||
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:
|
||||
if event.is_action_pressed("interact"):
|
||||
try_interact()
|
||||
|
||||
@@ -33,6 +33,7 @@ signal combatant_died(combatant_id: StringName)
|
||||
signal raid_started(raider_ids: Array[StringName])
|
||||
signal war_resolved(outcome: StringName)
|
||||
signal war_aborted
|
||||
signal player_downed
|
||||
|
||||
var village := SimVillage.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.war_resolved.connect(func(outcome): war_resolved.emit(outcome))
|
||||
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
|
||||
var definition_errors := SimulationDefinitions.validate()
|
||||
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)
|
||||
var helper := get_active_opportunity_helper()
|
||||
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(
|
||||
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:
|
||||
return
|
||||
@@ -390,6 +401,8 @@ func _apply_action_completion(
|
||||
pass
|
||||
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY, SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
|
||||
village.apply_npc_task(npc)
|
||||
SimulationIds.ACTION_DEFEND:
|
||||
village.apply_metric_delta(&"safety", 0.5)
|
||||
if debug_logs and completed_task == SimulationIds.ACTION_SLEEP:
|
||||
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:
|
||||
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:
|
||||
|
||||
@@ -24,7 +24,8 @@ func select_action(
|
||||
all_npcs: Array = [],
|
||||
opportunity_helper: OpportunityHelperResult = null,
|
||||
population_view: SimulationPopulationView = null,
|
||||
animal_feed_available: bool = false
|
||||
animal_feed_available: bool = false,
|
||||
war_raid_active: bool = false
|
||||
) -> ActionSelectionResult:
|
||||
if npc.is_dead:
|
||||
return null
|
||||
@@ -115,6 +116,10 @@ func select_action(
|
||||
-1.0,
|
||||
"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:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
|
||||
@@ -297,6 +302,10 @@ func _calculate_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:
|
||||
if _in_wrap_range(time_of_day, SLEEP_BEGIN, SLEEP_END):
|
||||
return SchedulePeriod.SLEEP
|
||||
|
||||
@@ -15,6 +15,8 @@ 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
|
||||
@@ -24,9 +26,7 @@ const WOLF_HUNT_RADIUS := 18.0
|
||||
const RAID_SPAWN_OFFSET := Vector3(42.0, 0.0, 42.0)
|
||||
const RAID_SPEED := 1.2
|
||||
const RAID_SIZE := 4
|
||||
const RAIDER_HEALTH := 60.0
|
||||
const WOLF_HEALTH := 40.0
|
||||
const RAIDER_REACH_FACTOR := 1.6
|
||||
const PLAYER_COMBATANT_ID := &"player_combatant"
|
||||
|
||||
var combatants: Dictionary = {}
|
||||
var factions: Dictionary = {}
|
||||
@@ -52,6 +52,53 @@ func initialize_factions() -> void:
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
@@ -100,7 +147,7 @@ func advance(simulation_tick: int) -> void:
|
||||
|
||||
|
||||
func _advance_raider_positions() -> void:
|
||||
if not _is_raiding():
|
||||
if not is_raiding():
|
||||
return
|
||||
for combatant_id in _sorted_combatant_ids():
|
||||
var combatant := combatants[combatant_id] as CombatantStateRecord
|
||||
@@ -132,16 +179,17 @@ 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,
|
||||
"Wolf",
|
||||
definition.display_name if definition != null else "Wolf",
|
||||
position,
|
||||
WOLF_HEALTH,
|
||||
SimulationIds.ITEM_CLAW,
|
||||
definition.health if definition != null else 40.0,
|
||||
definition.weapon_id if definition != null else SimulationIds.ITEM_CLAW,
|
||||
CombatantStateRecord.NO_NPC_ID,
|
||||
true
|
||||
)
|
||||
@@ -165,6 +213,11 @@ func _apply_damage(target: CombatantStateRecord, damage: float, actor_id: int) -
|
||||
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
|
||||
@@ -313,6 +366,7 @@ 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] = []
|
||||
@@ -324,10 +378,10 @@ func _start_raid(tribe: FactionStateRecord, simulation_tick: int) -> void:
|
||||
raider_id,
|
||||
SimulationIds.COMBATANT_KIND_RAIDER,
|
||||
SimulationIds.FACTION_TRIBE,
|
||||
"Raider",
|
||||
definition.display_name if definition != null else "Raider",
|
||||
village_center + RAID_SPAWN_OFFSET + offset,
|
||||
RAIDER_HEALTH,
|
||||
SimulationIds.ITEM_SWORD,
|
||||
definition.health if definition != null else 60.0,
|
||||
definition.weapon_id if definition != null else SimulationIds.ITEM_SWORD,
|
||||
CombatantStateRecord.NO_NPC_ID,
|
||||
true
|
||||
)
|
||||
@@ -347,7 +401,7 @@ func _start_raid(tribe: FactionStateRecord, simulation_tick: int) -> void:
|
||||
|
||||
|
||||
func _run_battle() -> void:
|
||||
var raid_active := _is_raiding()
|
||||
var raid_active := is_raiding()
|
||||
var living_hostiles: Array[CombatantStateRecord] = []
|
||||
for combatant_id in _sorted_combatant_ids():
|
||||
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())
|
||||
if weapon == null:
|
||||
return 1.5
|
||||
return (
|
||||
weapon.reach * RAIDER_REACH_FACTOR
|
||||
if combatant.get_kind() == SimulationIds.COMBATANT_KIND_WOLF
|
||||
else weapon.reach
|
||||
)
|
||||
return weapon.reach
|
||||
|
||||
|
||||
func _actor_id_for(combatant: CombatantStateRecord) -> int:
|
||||
@@ -506,7 +556,7 @@ func _living_village_npc_count() -> int:
|
||||
return count
|
||||
|
||||
|
||||
func _is_raiding() -> bool:
|
||||
func is_raiding() -> bool:
|
||||
var tribe := _get_faction(SimulationIds.FACTION_TRIBE)
|
||||
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:
|
||||
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:
|
||||
|
||||
@@ -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/withdraw_food.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 := [
|
||||
|
||||
@@ -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_WITHDRAW_FOOD := &"withdraw_food"
|
||||
const ACTION_DEPOSIT_WOOD := &"deposit_wood"
|
||||
const ACTION_DEFEND := &"defend"
|
||||
|
||||
const PROFESSION_FARMER := &"farmer"
|
||||
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
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const SCHEMA_VERSION := 2
|
||||
const LEGACY_SCHEMA_VERSION := 1
|
||||
const MAX_HEALTH := 100.0
|
||||
|
||||
var data: Dictionary
|
||||
|
||||
@@ -18,6 +20,10 @@ static func create_default() -> PlayerStateRecord:
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"hunger": 40.0,
|
||||
"energy": 100.0,
|
||||
"health": MAX_HEALTH,
|
||||
"max_health": MAX_HEALTH,
|
||||
"downed": false,
|
||||
"downed_ticks": 0,
|
||||
"inventory": {},
|
||||
}
|
||||
)
|
||||
@@ -25,15 +31,27 @@ static func create_default() -> 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
|
||||
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
|
||||
if not record_data["inventory"] is Dictionary:
|
||||
return null
|
||||
var hunger := float(record_data["hunger"])
|
||||
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
|
||||
var normalized_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,
|
||||
"hunger": clampf(hunger, 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,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
var manager := _create_manager(981)
|
||||
manager.update_player_combatant(Vector3(100.0, 0.0, 100.0))
|
||||
var npc: SimNPC = manager.npcs[0]
|
||||
npc.position = Vector3.ZERO
|
||||
var npc_combatant: CombatantStateRecord = manager.get_combatant(
|
||||
|
||||
@@ -35,7 +35,7 @@ func _test_registry_integrity() -> void:
|
||||
"Action lookup should return '%s'" % definition.action_id
|
||||
)
|
||||
_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()
|
||||
|
||||
@@ -48,7 +48,12 @@ func get_interaction_position() -> Vector3:
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -1,48 +1,39 @@
|
||||
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 simulation_manager: Node
|
||||
var is_dead_visual := false
|
||||
var flash_tween: Tween
|
||||
var spawn_scale := Vector3.ONE
|
||||
var _flash_tween: Tween
|
||||
var _spawn_tween: Tween
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
spawn_scale = Vector3.ONE
|
||||
add_to_group("grass_interactors")
|
||||
scale = Vector3.ONE * 0.001
|
||||
var tween := create_tween()
|
||||
tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tween.tween_property(self, "scale", Vector3.ONE, 0.28)
|
||||
_spawn_tween = create_tween()
|
||||
_spawn_tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
_spawn_tween.tween_property(self, "scale", Vector3.ONE, 0.28)
|
||||
|
||||
|
||||
func bind(combatant_id_value: StringName, manager: Node) -> void:
|
||||
combatant_id = combatant_id_value
|
||||
creature_id = combatant_id_value
|
||||
simulation_manager = manager
|
||||
combatant = manager.get_combatant(combatant_id) as CombatantStateRecord
|
||||
combatant = manager.get_combatant(creature_id) as CombatantStateRecord
|
||||
if combatant == null:
|
||||
return
|
||||
global_position = combatant.get_position()
|
||||
_build_body(combatant.get_kind())
|
||||
sim_position = combatant.get_position()
|
||||
_build_from_definition()
|
||||
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):
|
||||
return
|
||||
if not combatant.is_alive():
|
||||
_play_death()
|
||||
play_death()
|
||||
return
|
||||
var target_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)
|
||||
set_sim_position(combatant.get_position())
|
||||
|
||||
|
||||
func _on_combatant_changed(_state: CombatantStateRecord) -> void:
|
||||
@@ -52,42 +43,54 @@ func _on_combatant_changed(_state: CombatantStateRecord) -> void:
|
||||
|
||||
|
||||
func _flash_hit() -> void:
|
||||
if flash_tween != null and flash_tween.is_valid():
|
||||
flash_tween.kill()
|
||||
flash_tween = create_tween()
|
||||
flash_tween.tween_property(self, "scale", Vector3.ONE * 0.85, 0.06)
|
||||
flash_tween.tween_property(self, "scale", Vector3.ONE, 0.12)
|
||||
if _flash_tween != null and _flash_tween.is_valid():
|
||||
_flash_tween.kill()
|
||||
_flash_tween = create_tween()
|
||||
_flash_tween.tween_property(self, "scale", _definition_scale() * 0.85, 0.06)
|
||||
_flash_tween.tween_property(self, "scale", _definition_scale(), 0.12)
|
||||
|
||||
|
||||
func _play_death() -> void:
|
||||
func play_death() -> void:
|
||||
is_dead_visual = true
|
||||
stop_travel_visual()
|
||||
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)
|
||||
super.play_death()
|
||||
|
||||
|
||||
func stop_travel_visual() -> void:
|
||||
pass
|
||||
func _definition_scale() -> Vector3:
|
||||
var definition := _enemy_definition()
|
||||
return definition.visual_scale if definition != null else Vector3.ONE
|
||||
|
||||
|
||||
func _build_body(kind: StringName) -> void:
|
||||
if kind == SimulationIds.COMBATANT_KIND_WOLF:
|
||||
_build_wolf()
|
||||
else:
|
||||
_build_raider()
|
||||
func _build_from_definition() -> void:
|
||||
var definition := _enemy_definition()
|
||||
if definition != null:
|
||||
move_speed = definition.move_speed
|
||||
scale = definition.visual_scale
|
||||
if definition.kind == SimulationIds.COMBATANT_KIND_WOLF:
|
||||
_build_wolf(definition)
|
||||
else:
|
||||
_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()
|
||||
tunic.albedo_color = Color(0.4, 0.13, 0.1, 1.0)
|
||||
tunic.albedo_color = definition.body_color
|
||||
tunic.roughness = 0.85
|
||||
var skin := StandardMaterial3D.new()
|
||||
skin.albedo_color = Color(0.8, 0.58, 0.36, 1.0)
|
||||
skin.roughness = 0.9
|
||||
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
|
||||
|
||||
_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()
|
||||
fur.albedo_color = Color(0.32, 0.32, 0.34, 1.0)
|
||||
fur.albedo_color = definition.body_color
|
||||
fur.roughness = 0.95
|
||||
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
|
||||
|
||||
var body := SphereMesh.new()
|
||||
|
||||
@@ -66,7 +66,7 @@ func _on_combatant_spawned(combatant_id: StringName) -> void:
|
||||
func _on_combatant_died(combatant_id: StringName) -> void:
|
||||
var scene := hostiles.get(combatant_id) as HostileCombatant
|
||||
if scene != null:
|
||||
scene._play_death()
|
||||
scene.play_death()
|
||||
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