feat: add player sword combat, dash, and hostile wolf/raider visuals
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
class_name HostileCombatant
|
||||
extends Node3D
|
||||
|
||||
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
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
spawn_scale = Vector3.ONE
|
||||
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)
|
||||
|
||||
|
||||
func bind(combatant_id_value: StringName, manager: Node) -> void:
|
||||
combatant_id = combatant_id_value
|
||||
simulation_manager = manager
|
||||
combatant = manager.get_combatant(combatant_id) as CombatantStateRecord
|
||||
if combatant == null:
|
||||
return
|
||||
global_position = combatant.get_position()
|
||||
_build_body(combatant.get_kind())
|
||||
combatant.changed.connect(_on_combatant_changed)
|
||||
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
|
||||
func _on_combatant_changed(_state: CombatantStateRecord) -> void:
|
||||
if combatant == null or combatant.get_health() >= combatant.get_max_health():
|
||||
return
|
||||
_flash_hit()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
func stop_travel_visual() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _build_body(kind: StringName) -> void:
|
||||
if kind == SimulationIds.COMBATANT_KIND_WOLF:
|
||||
_build_wolf()
|
||||
else:
|
||||
_build_raider()
|
||||
|
||||
|
||||
func _build_raider() -> void:
|
||||
var tunic := StandardMaterial3D.new()
|
||||
tunic.albedo_color = Color(0.4, 0.13, 0.1, 1.0)
|
||||
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.roughness = 0.35
|
||||
|
||||
_add_mesh("Body", CylinderMesh.new(), Vector3(0.0, 1.1, 0.0), tunic)
|
||||
_add_mesh("Head", SphereMesh.new(), Vector3(0.0, 1.7, 0.0), skin)
|
||||
var blade := BoxMesh.new()
|
||||
blade.size = Vector3(0.06, 0.72, 0.14)
|
||||
_add_mesh("Sword", blade, Vector3(0.42, 1.25, 0.0), metal, Vector3(0.0, 0.0, -0.2))
|
||||
var shield := CylinderMesh.new()
|
||||
shield.top_radius = 0.3
|
||||
shield.bottom_radius = 0.3
|
||||
shield.height = 0.12
|
||||
_add_mesh(
|
||||
"Shield", shield, Vector3(-0.42, 1.15, 0.0), metal, Vector3.ZERO, Vector3(1.57, 0.0, 0.0)
|
||||
)
|
||||
|
||||
|
||||
func _build_wolf() -> void:
|
||||
var fur := StandardMaterial3D.new()
|
||||
fur.albedo_color = Color(0.32, 0.32, 0.34, 1.0)
|
||||
fur.roughness = 0.95
|
||||
var eye := StandardMaterial3D.new()
|
||||
eye.albedo_color = Color(0.95, 0.5, 0.12, 1.0)
|
||||
eye.roughness = 0.3
|
||||
|
||||
var body := SphereMesh.new()
|
||||
body.radius = 0.34
|
||||
body.height = 0.68
|
||||
_add_mesh("Body", body, Vector3(0.0, 0.55, 0.0), fur, Vector3.ZERO, Vector3(1.57, 0.0, 0.0))
|
||||
var head := SphereMesh.new()
|
||||
head.radius = 0.2
|
||||
head.height = 0.4
|
||||
_add_mesh("Head", head, Vector3(0.0, 0.62, -0.42), fur, Vector3.ZERO, Vector3(1.57, 0.0, 0.0))
|
||||
var tail := BoxMesh.new()
|
||||
tail.size = Vector3(0.1, 0.1, 0.5)
|
||||
_add_mesh("Tail", tail, Vector3(0.0, 0.72, 0.42), fur, Vector3(0.6, 0.0, 0.0))
|
||||
var leg_mesh := CylinderMesh.new()
|
||||
leg_mesh.top_radius = 0.06
|
||||
leg_mesh.bottom_radius = 0.06
|
||||
leg_mesh.height = 0.42
|
||||
for leg_x in [-0.18, 0.18]:
|
||||
for leg_z in [-0.2, 0.2]:
|
||||
_add_mesh("Leg", leg_mesh, Vector3(leg_x, 0.21, leg_z), fur)
|
||||
|
||||
|
||||
func _add_mesh(
|
||||
node_name: String,
|
||||
mesh: PrimitiveMesh,
|
||||
local_position: Vector3,
|
||||
material: Material,
|
||||
rotation_offset: Vector3 = Vector3.ZERO,
|
||||
mesh_rotation: Vector3 = Vector3.ZERO
|
||||
) -> void:
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = node_name
|
||||
mesh_instance.mesh = mesh
|
||||
mesh_instance.material_override = material
|
||||
mesh_instance.position = local_position
|
||||
if not rotation_offset.is_zero_approx():
|
||||
mesh_instance.rotation = rotation_offset
|
||||
if not mesh_rotation.is_zero_approx():
|
||||
mesh_instance.rotation += mesh_rotation
|
||||
add_child(mesh_instance)
|
||||
@@ -0,0 +1 @@
|
||||
uid://8mx2at53whud
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/combat/HostileCombatant.gd" id="1_hostile"]
|
||||
|
||||
[node name="HostileCombatant" type="Node3D"]
|
||||
script = ExtResource("1_hostile")
|
||||
@@ -0,0 +1,110 @@
|
||||
class_name CombatPresentation
|
||||
extends Node
|
||||
|
||||
const HOSTILE_SCENE := preload("res://world/combat/HostileCombatant.tscn")
|
||||
|
||||
@export var simulation_manager: Node
|
||||
@export var hostiles_parent: Node3D
|
||||
@export var wolf_spawn_positions: PackedVector3Array = PackedVector3Array()
|
||||
|
||||
var hostiles: Dictionary = {}
|
||||
var _initialized := false
|
||||
var _wolves_spawned := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("combat_presentation")
|
||||
if simulation_manager == null:
|
||||
push_error("CombatPresentation: simulation_manager missing")
|
||||
return
|
||||
simulation_manager.combatant_spawned.connect(_on_combatant_spawned)
|
||||
simulation_manager.combatant_died.connect(_on_combatant_died)
|
||||
simulation_manager.state_restored.connect(_on_state_restored)
|
||||
_initialized = true
|
||||
call_deferred("_spawn_wolves")
|
||||
_refresh_from_state()
|
||||
|
||||
|
||||
func _spawn_wolves() -> void:
|
||||
if _wolves_spawned or simulation_manager == null:
|
||||
return
|
||||
_wolves_spawned = true
|
||||
for position in wolf_spawn_positions:
|
||||
if simulation_manager.has_method("spawn_wolf"):
|
||||
simulation_manager.spawn_wolf(position)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _initialized:
|
||||
return
|
||||
for combatant_id in _hostile_ids():
|
||||
if not hostiles.has(combatant_id):
|
||||
_spawn_visual(combatant_id)
|
||||
|
||||
|
||||
func _hostile_ids() -> Array:
|
||||
var ids: Array[StringName] = []
|
||||
var living: Array = simulation_manager.get_living_hostile_combatants()
|
||||
for combatant in living:
|
||||
ids.append(combatant.get_combatant_id())
|
||||
return ids
|
||||
|
||||
|
||||
func _spawn_visual(combatant_id: StringName) -> void:
|
||||
if hostiles.has(combatant_id):
|
||||
return
|
||||
var scene := HOSTILE_SCENE.instantiate() as HostileCombatant
|
||||
hostiles_parent.add_child(scene)
|
||||
scene.bind(combatant_id, simulation_manager)
|
||||
hostiles[combatant_id] = scene
|
||||
|
||||
|
||||
func _on_combatant_spawned(combatant_id: StringName) -> void:
|
||||
_spawn_visual(combatant_id)
|
||||
|
||||
|
||||
func _on_combatant_died(combatant_id: StringName) -> void:
|
||||
var scene := hostiles.get(combatant_id) as HostileCombatant
|
||||
if scene != null:
|
||||
scene._play_death()
|
||||
hostiles.erase(combatant_id)
|
||||
|
||||
|
||||
func _on_state_restored() -> void:
|
||||
for combatant_id in hostiles.keys():
|
||||
var scene := hostiles[combatant_id] as Node
|
||||
if is_instance_valid(scene):
|
||||
scene.queue_free()
|
||||
hostiles.clear()
|
||||
_refresh_from_state()
|
||||
|
||||
|
||||
func _refresh_from_state() -> void:
|
||||
if simulation_manager == null:
|
||||
return
|
||||
for combatant_id in _hostile_ids():
|
||||
_spawn_visual(combatant_id)
|
||||
|
||||
|
||||
func find_hostile_in_arc(
|
||||
origin: Vector3, facing: Vector3, max_distance: float, angle_degrees: float
|
||||
) -> CombatantStateRecord:
|
||||
var living: Array = simulation_manager.get_living_hostile_combatants()
|
||||
var best: CombatantStateRecord
|
||||
var best_score := INF
|
||||
var facing_flat := Vector3(facing.x, 0.0, facing.z).normalized()
|
||||
var half_cos := cos(deg_to_rad(angle_degrees) * 0.5)
|
||||
for combatant in living:
|
||||
var candidate := combatant as CombatantStateRecord
|
||||
var offset := candidate.get_position() - origin
|
||||
var horizontal := Vector2(offset.x, offset.z)
|
||||
if horizontal.length() > max_distance:
|
||||
continue
|
||||
var direction := Vector3(offset.x, 0.0, offset.z).normalized()
|
||||
if facing_flat.dot(direction) < half_cos:
|
||||
continue
|
||||
var distance := horizontal.length()
|
||||
if distance < best_score:
|
||||
best = candidate
|
||||
best_score = distance
|
||||
return best
|
||||
@@ -0,0 +1 @@
|
||||
uid://qw0qb6mc8j7
|
||||
Reference in New Issue
Block a user