feat: add player sword combat, dash, and hostile wolf/raider visuals
This commit is contained in:
@@ -16,6 +16,8 @@
|
|||||||
[ext_resource type="Script" path="res://world/ui/player_interaction_hud.gd" id="16_interaction"]
|
[ext_resource type="Script" path="res://world/ui/player_interaction_hud.gd" id="16_interaction"]
|
||||||
[ext_resource type="Script" path="res://world/ui/villager_field_note_hud.gd" id="17_villager_note"]
|
[ext_resource type="Script" path="res://world/ui/villager_field_note_hud.gd" id="17_villager_note"]
|
||||||
[ext_resource type="Script" path="res://world/ui/player_status_hud.gd" id="18_status"]
|
[ext_resource type="Script" path="res://world/ui/player_status_hud.gd" id="18_status"]
|
||||||
|
[ext_resource type="Script" path="res://player/combat/player_combat_controller.gd" id="19_combat"]
|
||||||
|
[ext_resource type="Script" path="res://world/combat/combat_presentation.gd" id="20_combat_view"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
||||||
radius = 0.4
|
radius = 0.4
|
||||||
@@ -43,6 +45,13 @@ shape = SubResource("CapsuleShape3D_o5qli")
|
|||||||
|
|
||||||
[node name="Visual" parent="Player" instance=ExtResource("14_player_visual")]
|
[node name="Visual" parent="Player" instance=ExtResource("14_player_visual")]
|
||||||
|
|
||||||
|
[node name="PlayerCombatController" type="Node3D" parent="Player" unique_id=99761234 node_paths=PackedStringArray("player", "simulation_manager", "combat_presentation", "sword")]
|
||||||
|
script = ExtResource("19_combat")
|
||||||
|
player = NodePath("..")
|
||||||
|
simulation_manager = NodePath("../SimulationManager")
|
||||||
|
combat_presentation = NodePath("../CombatPresentation")
|
||||||
|
sword = NodePath("../Visual/Sword")
|
||||||
|
|
||||||
[node name="CameraRig" type="Node3D" parent="." unique_id=81378719 node_paths=PackedStringArray("target")]
|
[node name="CameraRig" type="Node3D" parent="." unique_id=81378719 node_paths=PackedStringArray("target")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 0.57357645, 0.81915206, 0, -0.81915206, 0.57357645, 0, 10, 8)
|
transform = Transform3D(1, 0, 0, 0, 0.57357645, 0.81915206, 0, -0.81915206, 0.57357645, 0, 10, 8)
|
||||||
script = ExtResource("2_1bvp3")
|
script = ExtResource("2_1bvp3")
|
||||||
@@ -435,3 +444,11 @@ camera_rig = NodePath("../CameraRig")
|
|||||||
player = NodePath("../Player")
|
player = NodePath("../Player")
|
||||||
pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
|
pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
|
||||||
crisis_caption = NodePath("../CrisisCaptionLayer/CrisisCaption")
|
crisis_caption = NodePath("../CrisisCaptionLayer/CrisisCaption")
|
||||||
|
|
||||||
|
[node name="CombatPresentation" type="Node" parent="." unique_id=8844112 node_paths=PackedStringArray("simulation_manager", "hostiles_parent")]
|
||||||
|
script = ExtResource("20_combat_view")
|
||||||
|
simulation_manager = NodePath("../SimulationManager")
|
||||||
|
hostiles_parent = NodePath("../HostilesParent")
|
||||||
|
wolf_spawn_positions = PackedVector3Array(24, 0, 10, 30, 0, -4, 38, 0, 22)
|
||||||
|
|
||||||
|
[node name="HostilesParent" type="Node3D" parent="." unique_id=8844113]
|
||||||
|
|||||||
@@ -67,6 +67,24 @@ roughness = 0.82
|
|||||||
material = SubResource("Material_accent")
|
material = SubResource("Material_accent")
|
||||||
size = Vector3(0.48, 0.1, 0.38)
|
size = Vector3(0.48, 0.1, 0.38)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_sword"]
|
||||||
|
albedo_color = Color(0.72, 0.74, 0.78, 1)
|
||||||
|
roughness = 0.32
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="Mesh_blade"]
|
||||||
|
material = SubResource("Material_sword")
|
||||||
|
size = Vector3(0.055, 0.78, 0.11)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_grip"]
|
||||||
|
albedo_color = Color(0.3, 0.2, 0.12, 1)
|
||||||
|
roughness = 0.6
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="Mesh_grip"]
|
||||||
|
material = SubResource("Material_grip")
|
||||||
|
top_radius = 0.045
|
||||||
|
bottom_radius = 0.045
|
||||||
|
height = 0.16
|
||||||
|
|
||||||
[sub_resource type="SphereMesh" id="Mesh_satchel"]
|
[sub_resource type="SphereMesh" id="Mesh_satchel"]
|
||||||
material = SubResource("Material_accent")
|
material = SubResource("Material_accent")
|
||||||
radius = 0.22
|
radius = 0.22
|
||||||
@@ -128,3 +146,15 @@ mesh = SubResource("Mesh_foot")
|
|||||||
[node name="Satchel" type="MeshInstance3D" parent="."]
|
[node name="Satchel" type="MeshInstance3D" parent="."]
|
||||||
position = Vector3(-0.43, 0.87, -0.04)
|
position = Vector3(-0.43, 0.87, -0.04)
|
||||||
mesh = SubResource("Mesh_satchel")
|
mesh = SubResource("Mesh_satchel")
|
||||||
|
|
||||||
|
[node name="Sword" type="Node3D" parent="."]
|
||||||
|
position = Vector3(0.52, 1.22, 0.12)
|
||||||
|
rotation = Vector3(-0.35, 0, 0)
|
||||||
|
|
||||||
|
[node name="Blade" type="MeshInstance3D" parent="Sword"]
|
||||||
|
position = Vector3(0, 0.42, 0)
|
||||||
|
mesh = SubResource("Mesh_blade")
|
||||||
|
|
||||||
|
[node name="Grip" type="MeshInstance3D" parent="Sword"]
|
||||||
|
position = Vector3(0, -0.05, 0)
|
||||||
|
mesh = SubResource("Mesh_grip")
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
class_name PlayerCombatController
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const SLASH_REST_ANGLE := Vector3(-0.35, 0.0, 0.0)
|
||||||
|
|
||||||
|
@export var player: CharacterBody3D
|
||||||
|
@export var simulation_manager: Node
|
||||||
|
@export var combat_presentation: Node
|
||||||
|
@export var sword: Node3D
|
||||||
|
|
||||||
|
@export_range(0.5, 4.0, 0.1) var attack_range := 2.6
|
||||||
|
@export_range(30.0, 160.0, 5.0) var attack_angle := 90.0
|
||||||
|
@export var dash_speed := 22.0
|
||||||
|
@export var dash_duration := 0.18
|
||||||
|
@export var dash_cooldown_seconds := 1.1
|
||||||
|
|
||||||
|
var attack_cooldown_seconds := 0.55
|
||||||
|
var attack_timer := 0.0
|
||||||
|
var dash_timer := 0.0
|
||||||
|
var dashing := false
|
||||||
|
var dash_velocity := Vector3.ZERO
|
||||||
|
var _swing_tween: Tween
|
||||||
|
var _references_resolved := false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if sword == null:
|
||||||
|
return
|
||||||
|
sword.rotation = SLASH_REST_ANGLE
|
||||||
|
var definition := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD)
|
||||||
|
if definition != null:
|
||||||
|
attack_cooldown_seconds = definition.attack_cooldown
|
||||||
|
attack_range = maxf(definition.reach + 0.2, attack_range)
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_references() -> void:
|
||||||
|
if simulation_manager == null:
|
||||||
|
simulation_manager = get_tree().get_first_node_in_group("simulation_manager")
|
||||||
|
if combat_presentation == null:
|
||||||
|
combat_presentation = get_tree().get_first_node_in_group("combat_presentation")
|
||||||
|
_references_resolved = true
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if not _references_resolved:
|
||||||
|
_resolve_references()
|
||||||
|
attack_timer = maxf(attack_timer - delta, 0.0)
|
||||||
|
dash_timer = maxf(dash_timer - delta, 0.0)
|
||||||
|
if dashing:
|
||||||
|
player.velocity = dash_velocity
|
||||||
|
if dash_timer <= 0.0:
|
||||||
|
dashing = false
|
||||||
|
player.move_and_slide()
|
||||||
|
return
|
||||||
|
if Input.is_action_just_pressed("attack") and attack_timer <= 0.0:
|
||||||
|
_perform_attack()
|
||||||
|
if Input.is_action_just_pressed("dash") and dash_timer <= 0.0:
|
||||||
|
_perform_dash()
|
||||||
|
|
||||||
|
|
||||||
|
func _perform_attack() -> void:
|
||||||
|
attack_timer = attack_cooldown_seconds
|
||||||
|
_play_swing()
|
||||||
|
var facing := -player.global_transform.basis.z
|
||||||
|
var hostile: CombatantStateRecord = null
|
||||||
|
if combat_presentation != null and combat_presentation.has_method("find_hostile_in_arc"):
|
||||||
|
hostile = combat_presentation.find_hostile_in_arc(
|
||||||
|
player.global_position, facing, attack_range, attack_angle
|
||||||
|
)
|
||||||
|
if hostile == null:
|
||||||
|
return
|
||||||
|
if simulation_manager != null and simulation_manager.has_method("player_attack"):
|
||||||
|
var damage: float = simulation_manager.player_attack(hostile.get_combatant_id())
|
||||||
|
if damage > 0.0:
|
||||||
|
_emit_combat_feedback("Struck %s" % hostile.get_display_name(), damage)
|
||||||
|
|
||||||
|
|
||||||
|
func _perform_dash() -> void:
|
||||||
|
dash_timer = dash_duration
|
||||||
|
dashing = true
|
||||||
|
var facing := -player.global_transform.basis.z
|
||||||
|
facing.y = 0.0
|
||||||
|
facing = facing.normalized()
|
||||||
|
dash_velocity = facing * dash_speed
|
||||||
|
dash_velocity.y = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _play_swing() -> void:
|
||||||
|
if sword == null:
|
||||||
|
return
|
||||||
|
if _swing_tween != null and _swing_tween.is_valid():
|
||||||
|
_swing_tween.kill()
|
||||||
|
sword.rotation = SLASH_REST_ANGLE
|
||||||
|
_swing_tween = create_tween()
|
||||||
|
_swing_tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||||
|
_swing_tween.tween_property(sword, "rotation", SLASH_REST_ANGLE + Vector3(-1.6, 0.6, 0.0), 0.12)
|
||||||
|
_swing_tween.tween_property(sword, "rotation", SLASH_REST_ANGLE, 0.2)
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_combat_feedback(heading: String, damage: float) -> void:
|
||||||
|
if player != null and player.has_signal("interaction_feedback"):
|
||||||
|
player.interaction_feedback.emit(heading, "Dealt %.0f damage." % damage, true)
|
||||||
|
|
||||||
|
|
||||||
|
func is_attacking() -> bool:
|
||||||
|
return attack_timer > 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func is_dashing() -> bool:
|
||||||
|
return dashing
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bfysko6pf0tnt
|
||||||
+11
-1
@@ -50,8 +50,18 @@ move_right={
|
|||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
interact={
|
interact={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
attack={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
dash={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":true,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var main_scene: Node = load("res://main.tscn").instantiate()
|
||||||
|
root.add_child(main_scene)
|
||||||
|
await process_frame
|
||||||
|
for _frame in 12:
|
||||||
|
await physics_frame
|
||||||
|
|
||||||
|
var simulation_manager: Node = main_scene.get_node("SimulationManager")
|
||||||
|
simulation_manager.set_process(false)
|
||||||
|
var combat_view: Node = main_scene.get_node("CombatPresentation")
|
||||||
|
var player := main_scene.get_node("Player") as Node3D
|
||||||
|
var controller := main_scene.get_node("Player/PlayerCombatController")
|
||||||
|
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
_check(
|
||||||
|
combat_view.hostiles.size() >= 1,
|
||||||
|
"Authored wolves should spawn as hostile visuals in the loaded world"
|
||||||
|
)
|
||||||
|
var hostiles: Array = simulation_manager.get_living_hostile_combatants()
|
||||||
|
_check(not hostiles.is_empty(), "The simulation should own at least one living hostile")
|
||||||
|
if hostiles.is_empty():
|
||||||
|
main_scene.queue_free()
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
var wolf: CombatantStateRecord = hostiles[0]
|
||||||
|
_check(
|
||||||
|
combat_view.hostiles.has(wolf.get_combatant_id()),
|
||||||
|
"The hostile visual should bind to the authoritative combatant record"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
wolf.get_kind() == SimulationIds.COMBATANT_KIND_WOLF
|
||||||
|
and wolf.is_hostile()
|
||||||
|
and wolf.get_faction_id() == SimulationIds.FACTION_TRIBE
|
||||||
|
),
|
||||||
|
"Spawned wolves should be hostile tribe-affiliated combatants"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wolf_visual: Node3D = combat_view.hostiles[wolf.get_combatant_id()]
|
||||||
|
player.global_position = wolf_visual.global_position + Vector3(-2.0, 0.0, 0.0)
|
||||||
|
player.look_at(wolf_visual.global_position, Vector3.UP)
|
||||||
|
player.rotation.x = 0.0
|
||||||
|
var wolf_health: float = wolf.get_health()
|
||||||
|
controller.call("_perform_attack")
|
||||||
|
_check(
|
||||||
|
wolf.get_health() < wolf_health,
|
||||||
|
"A player sword strike should damage the hostile wolf through the simulation"
|
||||||
|
)
|
||||||
|
var struck := 0
|
||||||
|
for _attempt in 6:
|
||||||
|
if not wolf.is_alive():
|
||||||
|
break
|
||||||
|
controller.call("_perform_attack")
|
||||||
|
struck += 1
|
||||||
|
await process_frame
|
||||||
|
_check(not wolf.is_alive(), "Repeated sword strikes should kill the hostile wolf")
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
_check(
|
||||||
|
not combat_view.hostiles.has(wolf.get_combatant_id()),
|
||||||
|
"Killing a hostile should remove its presentation visual"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
_has_event(simulation_manager, SimulationIds.EVENT_COMBATANT_KILLED),
|
||||||
|
"The player's kill should be recorded as a combatant_killed fact"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tribe: FactionStateRecord = simulation_manager.get_faction(SimulationIds.FACTION_TRIBE)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
simulation_manager.get_village_defender_count() >= 0
|
||||||
|
and tribe != null
|
||||||
|
and (
|
||||||
|
simulation_manager.get_tribe_war_plan()
|
||||||
|
in [
|
||||||
|
SimulationIds.WAR_PLAN_NONE,
|
||||||
|
SimulationIds.WAR_PLAN_PLANNED,
|
||||||
|
SimulationIds.WAR_PLAN_RAIDING,
|
||||||
|
SimulationIds.WAR_PLAN_ABORTED,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"The runtime should expose village defense and the tribe war plan"
|
||||||
|
)
|
||||||
|
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Jajce combat presentation passed: wolves -> sword -> kill -> cleanup")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _has_event(manager: Node, event_type: StringName) -> bool:
|
||||||
|
for event in manager.economic_events:
|
||||||
|
if StringName(event.data["event_type"]) == event_type:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Jajce combat presentation passed: wolves -> sword -> kill -> cleanup")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bneh10pvbywgr
|
||||||
@@ -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