feat: shared creature root, enemy definitions, defend duty, and player downing

This commit is contained in:
Rijad Zuzo
2026-08-12 00:16:48 +02:00
parent 6de6d25bf5
commit 661d53f31a
23 changed files with 653 additions and 76 deletions
+6 -1
View File
@@ -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:
+50 -47
View File
@@ -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()
+1 -1
View File
@@ -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)
+127
View File
@@ -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)
)
+1
View File
@@ -0,0 +1 @@
uid://6r66un51bane