parent
fce4c4a3e2
commit
7629342540
14 changed files with 928 additions and 73 deletions
@ -1,19 +1,17 @@ |
|||||||
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3] |
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3 uid="uid://dyae861vxd8it"] |
||||||
|
|
||||||
[ext_resource type="Script" path="res://level/scripts/weapon_data.gd" id="1"] |
[ext_resource type="Script" uid="uid://d2homvlmrg6xs" path="res://level/scripts/weapon_data.gd" id="1"] |
||||||
[ext_resource type="PackedScene" path="res://level/scenes/weapons/LobsterAxeMesh.tscn" id="2"] |
[ext_resource type="PackedScene" uid="uid://cq8r5mkn3wvxj" path="res://level/scenes/weapons/LobsterAxeMesh.tscn" id="2"] |
||||||
|
|
||||||
[resource] |
[resource] |
||||||
script = ExtResource("1") |
script = ExtResource("1") |
||||||
weapon_name = "Lobster Axe" |
weapon_name = "Lobster Axe" |
||||||
description = "A heavy-hitting axe shaped like a lobster claw. Surprisingly quick for its size." |
description = "A heavy-hitting axe shaped like a lobster claw. Surprisingly quick for its size." |
||||||
damage = 18.0 |
damage = 18.0 |
||||||
attack_range = 3.0 |
|
||||||
attack_cooldown = 0.7 |
attack_cooldown = 0.7 |
||||||
|
attack_animation = "Attack_TwoHandSwing" |
||||||
knockback_force = 14.0 |
knockback_force = 14.0 |
||||||
attack_animation = "Attack1" |
startup_time = 0.2 |
||||||
startup_time = 0.18 |
active_time = 1.0 |
||||||
active_time = 0.18 |
|
||||||
mesh_scene = ExtResource("2") |
mesh_scene = ExtResource("2") |
||||||
pickup_radius = 1.5 |
|
||||||
weight = 2.5 |
weight = 2.5 |
||||||
|
|||||||
@ -0,0 +1,124 @@ |
|||||||
|
[gd_scene load_steps=5 format=3] |
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://level/scripts/armed_enemy.gd" id="1_armed_enemy"] |
||||||
|
[ext_resource type="PackedScene" uid="uid://b22ou40sbkavj" path="res://assets/characters/player/LilguyRigged.glb" id="2_lilguy"] |
||||||
|
[ext_resource type="Script" uid="uid://cf7jky1bcs560" path="res://level/scripts/lilguy_body.gd" id="3_body"] |
||||||
|
[ext_resource type="Script" uid="uid://bj3uepduxvgju" path="res://level/scripts/hurt_box.gd" id="4_hurtbox"] |
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"] |
||||||
|
radius = 0.35796 |
||||||
|
height = 1.73092 |
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hurtbox"] |
||||||
|
radius = 0.4 |
||||||
|
height = 1.8 |
||||||
|
|
||||||
|
[node name="ArmedEnemy" type="CharacterBody3D" node_paths=PackedStringArray("_body", "_weapon_attachment", "_weapon_container", "_offhand_attachment", "_offhand_container")] |
||||||
|
collision_mask = 3 |
||||||
|
script = ExtResource("1_armed_enemy") |
||||||
|
_body = NodePath("LilguyRigged/Armature") |
||||||
|
_weapon_attachment = NodePath("LilguyRigged/Armature/Skeleton3D/WeaponPoint") |
||||||
|
_weapon_container = NodePath("LilguyRigged/Armature/Skeleton3D/WeaponPoint/WeaponContainer") |
||||||
|
_offhand_attachment = NodePath("LilguyRigged/Armature/Skeleton3D/OffhandPoint") |
||||||
|
_offhand_container = NodePath("LilguyRigged/Armature/Skeleton3D/OffhandPoint/OffhandContainer") |
||||||
|
move_speed = 4.0 |
||||||
|
detection_range = 100.0 |
||||||
|
max_health = 50.0 |
||||||
|
respawn_delay = 10.0 |
||||||
|
|
||||||
|
[node name="LilguyRigged" parent="." instance=ExtResource("2_lilguy")] |
||||||
|
|
||||||
|
[node name="Armature" parent="LilguyRigged" index="0" node_paths=PackedStringArray("_character", "animation_player")] |
||||||
|
transform = Transform3D(0.003, 0, 0, 0, -1.3113416e-10, -0.003, 0, 0.003, -1.3113416e-10, 0, 0, 0) |
||||||
|
script = ExtResource("3_body") |
||||||
|
_character = NodePath("../..") |
||||||
|
animation_player = NodePath("../AnimationPlayer") |
||||||
|
|
||||||
|
[node name="Skeleton3D" parent="LilguyRigged/Armature" index="0"] |
||||||
|
bones/0/position = Vector3(-0.32859802, 2.9141626, -546.76843) |
||||||
|
bones/0/rotation = Quaternion(-0.6608289, 0.28933656, -0.19178493, 0.66543835) |
||||||
|
bones/1/position = Vector3(0.054167695, 63.219894, -3.33786e-06) |
||||||
|
bones/1/rotation = Quaternion(0.015321612, 0.025352655, 0.0947179, 0.99506336) |
||||||
|
bones/2/position = Vector3(-1.8112361e-05, 73.7566, -1.621247e-05) |
||||||
|
bones/2/rotation = Quaternion(0.034659874, 0.050472155, 0.051301125, 0.99680465) |
||||||
|
bones/3/position = Vector3(-3.0510128e-05, 84.29319, 9.059899e-06) |
||||||
|
bones/3/rotation = Quaternion(0.029244598, 0.05379088, -0.051849354, 0.99677634) |
||||||
|
bones/4/position = Vector3(3.8038404e-05, 94.83001, 1.9073414e-06) |
||||||
|
bones/4/rotation = Quaternion(0.0006217413, 0.08164966, 0.020404326, 0.9964521) |
||||||
|
bones/5/position = Vector3(-0.25257444, 72.84532, -7.644296e-06) |
||||||
|
bones/5/rotation = Quaternion(0.037355006, 0.19943582, -0.04159446, 0.9783148) |
||||||
|
bones/6/position = Vector3(-0.606337, 174.89494, 7.152558e-06) |
||||||
|
bones/7/position = Vector3(-0.19949026, 76.75483, 52.286175) |
||||||
|
bones/7/rotation = Quaternion(0.80360717, -0.09628771, 0.10672592, 0.57754105) |
||||||
|
bones/8/position = Vector3(4.5403274e-05, 110.91907, 9.404198e-05) |
||||||
|
bones/8/rotation = Quaternion(0.25522023, -0.08967148, 0.029356971, 0.9622681) |
||||||
|
bones/9/position = Vector3(2.3064584e-05, 173.66367, 5.063071e-05) |
||||||
|
bones/9/rotation = Quaternion(0.08784258, -0.16096693, 0.24338366, 0.95243776) |
||||||
|
bones/10/position = Vector3(-2.2947788e-05, 166.48767, -1.2734416e-05) |
||||||
|
bones/11/position = Vector3(0.23053212, 76.75536, -52.28617) |
||||||
|
bones/11/rotation = Quaternion(0.14271267, -0.5852636, 0.7822879, 0.15851) |
||||||
|
bones/12/position = Vector3(1.532285e-05, 110.91911, 4.0430357e-05) |
||||||
|
bones/12/rotation = Quaternion(0.32197043, 0.13412262, 0.2711233, 0.89712787) |
||||||
|
bones/13/position = Vector3(1.5523525e-05, 173.6661, 0.00010698747) |
||||||
|
bones/13/rotation = Quaternion(0.090376236, 0.10155637, -0.39819276, 0.90717196) |
||||||
|
bones/14/position = Vector3(-2.0682812e-05, 166.48976, 3.939679e-05) |
||||||
|
bones/15/position = Vector3(0.6496186, -35.1185, 49.84838) |
||||||
|
bones/15/rotation = Quaternion(0.38543156, 0.16380574, 0.82174975, 0.38644233) |
||||||
|
bones/16/position = Vector3(8.771768e-06, 312.91962, 7.4840264e-06) |
||||||
|
bones/16/rotation = Quaternion(-0.053004134, 0.17209636, 0.39056766, 0.90279037) |
||||||
|
bones/17/position = Vector3(-1.8137518e-05, 301.05597, -2.1670077e-05) |
||||||
|
bones/17/rotation = Quaternion(0.2498236, 0.64725155, -0.67117685, 0.26110402) |
||||||
|
bones/18/position = Vector3(-3.026353e-05, 14.185886, -1.4917823e-06) |
||||||
|
bones/18/rotation = Quaternion(0.11539694, 0.017187497, -0.010002339, 0.9931204) |
||||||
|
bones/19/position = Vector3(-4.351055e-06, 11.391233, -2.5032205e-06) |
||||||
|
bones/20/position = Vector3(0.014209064, -35.118507, -49.848385) |
||||||
|
bones/20/rotation = Quaternion(-0.07370261, -0.18747209, 0.94122386, 0.27114522) |
||||||
|
bones/21/position = Vector3(2.8756085e-05, 312.91974, 5.14377e-06) |
||||||
|
bones/21/rotation = Quaternion(-0.037353504, -0.04220154, 0.46134973, 0.88542664) |
||||||
|
bones/22/position = Vector3(2.2092872e-05, 301.0575, 1.8114511e-05) |
||||||
|
bones/22/rotation = Quaternion(0.79332, 0.1285891, -0.36227074, 0.47208923) |
||||||
|
bones/23/position = Vector3(1.3624241e-05, 15.034077, 9.790485e-06) |
||||||
|
bones/23/rotation = Quaternion(0.11885707, 0.009522018, -0.0077985795, 0.9928351) |
||||||
|
bones/24/position = Vector3(-2.4847686e-06, 11.913359, -6.198885e-06) |
||||||
|
|
||||||
|
[node name="WeaponPoint" type="BoneAttachment3D" parent="LilguyRigged/Armature/Skeleton3D" index="1"] |
||||||
|
transform = Transform3D(-0.4329258, -0.61284786, 0.6610537, 0.7782944, 0.11585927, 0.6171174, -0.45478824, 0.78166056, 0.42681772, -352.385, -73.56995, -531.9614) |
||||||
|
bone_name = "mixamorig_RightHand" |
||||||
|
bone_idx = 14 |
||||||
|
|
||||||
|
[node name="WeaponContainer" type="Node3D" parent="LilguyRigged/Armature/Skeleton3D/WeaponPoint"] |
||||||
|
transform = Transform3D(36.6912, 297.2667, 16.921356, 46.72698, 11.0892515, -296.13126, -294.05847, 38.85366, -44.94499, 24.08223, -7.4241333, 7.098694) |
||||||
|
|
||||||
|
[node name="OffhandPoint" type="BoneAttachment3D" parent="LilguyRigged/Armature/Skeleton3D" index="2"] |
||||||
|
transform = Transform3D(0.62123704, -0.004605159, -0.7836091, -0.62031674, 0.6081372, -0.49535444, 0.4788229, 0.79381835, 0.3749406, 135.65929, 334.35745, -511.27094) |
||||||
|
bone_name = "mixamorig_LeftHand" |
||||||
|
bone_idx = 10 |
||||||
|
|
||||||
|
[node name="OffhandContainer" type="Node3D" parent="LilguyRigged/Armature/Skeleton3D/OffhandPoint"] |
||||||
|
transform = Transform3D(-17.74905, -295.46814, -48.82108, 21.019196, -50.01525, 295.05362, -298.73593, 14.035805, 23.660797, 0.005859375, 0.39337158, 0.06616211) |
||||||
|
|
||||||
|
[node name="AnimationPlayer" parent="LilguyRigged" index="1"] |
||||||
|
speed_scale = 2.0 |
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
||||||
|
transform = Transform3D(2, 0, 0, 0, 2, 0, 0, 0, 2, -0.066, 1.647685, 0.01) |
||||||
|
shape = SubResource("CapsuleShape3D_body") |
||||||
|
|
||||||
|
[node name="HurtBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")] |
||||||
|
collision_layer = 16 |
||||||
|
collision_mask = 0 |
||||||
|
script = ExtResource("4_hurtbox") |
||||||
|
owner_entity = NodePath("..") |
||||||
|
|
||||||
|
[node name="HurtBoxShape" type="CollisionShape3D" parent="HurtBox"] |
||||||
|
transform = Transform3D(1.9228287, 0, 0, 0, 1.4454772, 0, 0, 0, 1.4906956, -0.066, 2.0836046, 0.01) |
||||||
|
shape = SubResource("CapsuleShape3D_hurtbox") |
||||||
|
|
||||||
|
[node name="EnemyLabel" type="Label3D" parent="."] |
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.2, 0) |
||||||
|
billboard = 1 |
||||||
|
modulate = Color(1, 0.3, 0.3, 1) |
||||||
|
outline_modulate = Color(0, 0, 0, 0.4) |
||||||
|
text = "Armed Enemy" |
||||||
|
|
||||||
|
[editable path="LilguyRigged"] |
||||||
@ -0,0 +1,616 @@ |
|||||||
|
extends BaseEnemy |
||||||
|
class_name ArmedEnemy |
||||||
|
|
||||||
|
## An enemy that uses the player model, animations, and can equip weapons |
||||||
|
## Drops equipped weapons on death using the existing world weapon spawn system |
||||||
|
|
||||||
|
## Movement |
||||||
|
@export var move_speed: float = 4.0 |
||||||
|
@export var chase_range: float = 20.0 |
||||||
|
@export var attack_range: float = 2.5 |
||||||
|
|
||||||
|
## Combat (unarmed fallback) |
||||||
|
@export var unarmed_damage: float = 10.0 |
||||||
|
@export var unarmed_knockback: float = 5.0 |
||||||
|
@export var attack_cooldown: float = 1.0 |
||||||
|
@export_category("Unarmed Attack Timing") |
||||||
|
@export var unarmed_startup: float = 0.15 |
||||||
|
@export var unarmed_active: float = 0.2 |
||||||
|
|
||||||
|
## Weapon system |
||||||
|
@export_category("Weapons") |
||||||
|
@export var starting_weapon: WeaponData = null ## Weapon to equip on spawn |
||||||
|
@export var starting_offhand: WeaponData = null ## Off-hand weapon to equip on spawn |
||||||
|
|
||||||
|
## Body reference (LilguyBody for animations) |
||||||
|
@export var _body: Node3D = null |
||||||
|
@export var _weapon_attachment: BoneAttachment3D = null |
||||||
|
@export var _weapon_container: Node3D = null |
||||||
|
@export var _offhand_attachment: BoneAttachment3D = null |
||||||
|
@export var _offhand_container: Node3D = null |
||||||
|
|
||||||
|
## Runtime weapon state |
||||||
|
var equipped_weapon: BaseWeapon = null |
||||||
|
var equipped_offhand: BaseWeapon = null |
||||||
|
|
||||||
|
## AI State |
||||||
|
var _attack_timer: float = 0.0 |
||||||
|
var _is_attacking: bool = false |
||||||
|
var _unarmed_hitbox: HitBox = null |
||||||
|
|
||||||
|
## Visual feedback |
||||||
|
var _hit_flash_timer: float = 0.0 |
||||||
|
const HIT_FLASH_DURATION: float = 0.2 |
||||||
|
|
||||||
|
## Position sync (manual sync instead of MultiplayerSynchronizer for dynamic spawning) |
||||||
|
var _sync_timer: float = 0.0 |
||||||
|
const SYNC_INTERVAL: float = 0.05 # 20 times per second |
||||||
|
|
||||||
|
func _enter_tree(): |
||||||
|
# Enemies are always server-authoritative |
||||||
|
set_multiplayer_authority(1) |
||||||
|
|
||||||
|
func _ready(): |
||||||
|
super._ready() |
||||||
|
|
||||||
|
# Auto-find body if not set |
||||||
|
if _body == null: |
||||||
|
if has_node("LilguyRigged/Armature"): |
||||||
|
_body = get_node("LilguyRigged/Armature") |
||||||
|
|
||||||
|
# Auto-find weapon attachments |
||||||
|
if _weapon_attachment == null: |
||||||
|
_weapon_attachment = get_node_or_null("LilguyRigged/Armature/Skeleton3D/WeaponPoint") |
||||||
|
|
||||||
|
if _weapon_container == null and _weapon_attachment: |
||||||
|
_weapon_container = _weapon_attachment.get_node_or_null("WeaponContainer") |
||||||
|
|
||||||
|
if _offhand_attachment == null: |
||||||
|
_offhand_attachment = get_node_or_null("LilguyRigged/Armature/Skeleton3D/OffhandPoint") |
||||||
|
|
||||||
|
if _offhand_container == null and _offhand_attachment: |
||||||
|
_offhand_container = _offhand_attachment.get_node_or_null("OffhandContainer") |
||||||
|
|
||||||
|
# Setup unarmed hitbox |
||||||
|
call_deferred("_setup_unarmed_hitbox") |
||||||
|
|
||||||
|
# Equip starting weapons |
||||||
|
# Server will equip and send RPC to sync; clients also equip directly to handle late-join |
||||||
|
call_deferred("_equip_starting_weapons_local") |
||||||
|
|
||||||
|
## Equip starting weapons - server uses RPC to sync, clients equip directly |
||||||
|
func _equip_starting_weapons_local(): |
||||||
|
# Wait a frame to ensure everything is ready |
||||||
|
await get_tree().process_frame |
||||||
|
|
||||||
|
# Check if multiplayer peer is assigned |
||||||
|
if multiplayer.multiplayer_peer == null: |
||||||
|
# No multiplayer yet, just equip locally |
||||||
|
if starting_weapon: |
||||||
|
_equip_weapon(starting_weapon, false) |
||||||
|
if starting_offhand: |
||||||
|
_equip_weapon(starting_offhand, true) |
||||||
|
return |
||||||
|
|
||||||
|
if multiplayer.is_server(): |
||||||
|
# Server equips via RPC to sync to all clients |
||||||
|
if starting_weapon: |
||||||
|
print("[ArmedEnemy ", name, "] Server equipping starting weapon: ", starting_weapon.resource_path) |
||||||
|
rpc("_equip_weapon_sync", starting_weapon.resource_path, false) |
||||||
|
if starting_offhand: |
||||||
|
print("[ArmedEnemy ", name, "] Server equipping starting offhand: ", starting_offhand.resource_path) |
||||||
|
rpc("_equip_weapon_sync", starting_offhand.resource_path, true) |
||||||
|
else: |
||||||
|
# Client equips directly (for late-join clients who won't receive server's initial RPC) |
||||||
|
# Skip if already equipped (from server RPC) |
||||||
|
if starting_weapon and not equipped_weapon: |
||||||
|
print("[ArmedEnemy ", name, "] Client equipping starting weapon directly") |
||||||
|
_equip_weapon(starting_weapon, false) |
||||||
|
if starting_offhand and not equipped_offhand: |
||||||
|
print("[ArmedEnemy ", name, "] Client equipping starting offhand directly") |
||||||
|
_equip_weapon(starting_offhand, true) |
||||||
|
|
||||||
|
## Equip weapon on all clients |
||||||
|
@rpc("any_peer", "call_local", "reliable") |
||||||
|
func _equip_weapon_sync(weapon_data_path: String, is_offhand: bool): |
||||||
|
print("[ArmedEnemy ", name, "] _equip_weapon_sync called on peer ", multiplayer.get_unique_id(), " path: ", weapon_data_path) |
||||||
|
if weapon_data_path == "": |
||||||
|
push_error("[ArmedEnemy] Empty weapon path!") |
||||||
|
return |
||||||
|
var data = load(weapon_data_path) as WeaponData |
||||||
|
if data: |
||||||
|
_equip_weapon(data, is_offhand) |
||||||
|
else: |
||||||
|
push_error("[ArmedEnemy] Failed to load weapon data from: ", weapon_data_path) |
||||||
|
|
||||||
|
func _equip_weapon(data: WeaponData, is_offhand: bool = false): |
||||||
|
# Unequip current weapon in that hand first |
||||||
|
if is_offhand: |
||||||
|
if equipped_offhand: |
||||||
|
_unequip_weapon(true) |
||||||
|
else: |
||||||
|
if equipped_weapon: |
||||||
|
_unequip_weapon(false) |
||||||
|
|
||||||
|
# Determine attachment point |
||||||
|
var attach_point: Node3D |
||||||
|
if is_offhand: |
||||||
|
attach_point = _offhand_container if _offhand_container else _offhand_attachment |
||||||
|
else: |
||||||
|
attach_point = _weapon_container if _weapon_container else _weapon_attachment |
||||||
|
|
||||||
|
if not attach_point: |
||||||
|
push_error("[ArmedEnemy] No weapon attachment point found") |
||||||
|
return |
||||||
|
|
||||||
|
# Create weapon instance |
||||||
|
var weapon = BaseWeapon.new() |
||||||
|
weapon.weapon_data = data |
||||||
|
weapon.name = "EquippedOffHand" if is_offhand else "EquippedWeapon" |
||||||
|
|
||||||
|
# Add to scene first (so _ready is called and hitbox is set up) |
||||||
|
attach_point.add_child(weapon) |
||||||
|
|
||||||
|
# Set owner for damage routing (must be after add_child so hitbox exists) |
||||||
|
weapon.set_owner_character(self) |
||||||
|
|
||||||
|
# Store reference |
||||||
|
if is_offhand: |
||||||
|
equipped_offhand = weapon |
||||||
|
else: |
||||||
|
equipped_weapon = weapon |
||||||
|
|
||||||
|
print("[ArmedEnemy ", name, "] Equipped: ", data.weapon_name) |
||||||
|
|
||||||
|
func _unequip_weapon(is_offhand: bool = false): |
||||||
|
if is_offhand: |
||||||
|
if equipped_offhand: |
||||||
|
equipped_offhand.queue_free() |
||||||
|
equipped_offhand = null |
||||||
|
else: |
||||||
|
if equipped_weapon: |
||||||
|
equipped_weapon.queue_free() |
||||||
|
equipped_weapon = null |
||||||
|
|
||||||
|
func _process(delta): |
||||||
|
# Countdown timers |
||||||
|
if _attack_timer > 0: |
||||||
|
_attack_timer -= delta |
||||||
|
|
||||||
|
# Handle hit flash |
||||||
|
if _hit_flash_timer > 0: |
||||||
|
_hit_flash_timer -= delta |
||||||
|
if _hit_flash_timer <= 0: |
||||||
|
_reset_material() |
||||||
|
|
||||||
|
func _physics_process(delta): |
||||||
|
super._physics_process(delta) |
||||||
|
|
||||||
|
# Only server runs AI and movement (check peer is assigned first) |
||||||
|
if multiplayer.multiplayer_peer == null or not multiplayer.is_server(): |
||||||
|
return |
||||||
|
|
||||||
|
if is_dead: |
||||||
|
return |
||||||
|
|
||||||
|
# Apply gravity |
||||||
|
if not is_on_floor(): |
||||||
|
velocity.y -= ProjectSettings.get_setting("physics/3d/default_gravity") * delta |
||||||
|
|
||||||
|
# AI behavior |
||||||
|
if current_target and is_instance_valid(current_target): |
||||||
|
_ai_combat(delta) |
||||||
|
else: |
||||||
|
# Stop moving if no target |
||||||
|
velocity.x = 0 |
||||||
|
velocity.z = 0 |
||||||
|
|
||||||
|
move_and_slide() |
||||||
|
|
||||||
|
# Rotate body to face movement direction (like player does) |
||||||
|
var body_rotation_y: float = 0.0 |
||||||
|
if _body and _body.has_method("apply_rotation") and velocity.length() > 0.1: |
||||||
|
_body.apply_rotation(velocity) |
||||||
|
if _body: |
||||||
|
body_rotation_y = _body.rotation.y |
||||||
|
|
||||||
|
# Animate body |
||||||
|
if _body and _body.has_method("animate"): |
||||||
|
_body.animate(velocity) |
||||||
|
|
||||||
|
# Sync position, rotation, and animation to clients periodically |
||||||
|
_sync_timer -= delta |
||||||
|
if _sync_timer <= 0: |
||||||
|
_sync_timer = SYNC_INTERVAL |
||||||
|
var current_anim = "" |
||||||
|
if _body: |
||||||
|
var anim_player = _body.get_node_or_null("../AnimationPlayer") as AnimationPlayer |
||||||
|
if anim_player: |
||||||
|
current_anim = anim_player.current_animation |
||||||
|
rpc("_sync_transform", global_position, body_rotation_y, current_anim) |
||||||
|
|
||||||
|
## Sync position, body rotation, and animation from server to clients |
||||||
|
@rpc("authority", "call_remote", "unreliable") |
||||||
|
func _sync_transform(pos: Vector3, body_rot_y: float, anim_name: String = ""): |
||||||
|
# Only apply on clients (server is authoritative) |
||||||
|
if multiplayer.is_server(): |
||||||
|
return |
||||||
|
|
||||||
|
global_position = pos |
||||||
|
if _body: |
||||||
|
_body.rotation.y = body_rot_y |
||||||
|
|
||||||
|
# Sync animation |
||||||
|
if anim_name != "": |
||||||
|
var anim_player = _body.get_node_or_null("../AnimationPlayer") as AnimationPlayer |
||||||
|
if anim_player and anim_player.has_animation(anim_name): |
||||||
|
if anim_player.current_animation != anim_name: |
||||||
|
anim_player.play(anim_name) |
||||||
|
|
||||||
|
## Override to find nearest player |
||||||
|
func get_nearest_player() -> Node: |
||||||
|
var players = get_players_in_range(1000.0) # Essentially unlimited |
||||||
|
|
||||||
|
if players.is_empty(): |
||||||
|
return null |
||||||
|
|
||||||
|
var nearest_player = null |
||||||
|
var nearest_distance = INF |
||||||
|
|
||||||
|
for player in players: |
||||||
|
var distance = global_position.distance_to(player.global_position) |
||||||
|
if distance < nearest_distance: |
||||||
|
nearest_distance = distance |
||||||
|
nearest_player = player |
||||||
|
|
||||||
|
return nearest_player |
||||||
|
|
||||||
|
## Update target |
||||||
|
func _update_target(): |
||||||
|
if not is_aggressive: |
||||||
|
return |
||||||
|
|
||||||
|
var nearest = get_nearest_player() |
||||||
|
|
||||||
|
if nearest: |
||||||
|
if current_target != nearest: |
||||||
|
current_target = nearest |
||||||
|
target_changed.emit(nearest) |
||||||
|
else: |
||||||
|
if current_target != null: |
||||||
|
current_target = null |
||||||
|
target_changed.emit(null) |
||||||
|
|
||||||
|
## Combat AI |
||||||
|
func _ai_combat(delta): |
||||||
|
if not current_target or not is_instance_valid(current_target): |
||||||
|
return |
||||||
|
|
||||||
|
var target_pos = current_target.global_position |
||||||
|
var direction = (target_pos - global_position).normalized() |
||||||
|
var distance = global_position.distance_to(target_pos) |
||||||
|
|
||||||
|
# Get attack range - use a close fixed range to ensure hits connect |
||||||
|
# The hitbox is on the weapon in the enemy's hand, so we need to be close |
||||||
|
var current_attack_range = 2.0 # Fixed close range for melee |
||||||
|
|
||||||
|
# If in attack range, attack |
||||||
|
if distance <= current_attack_range: |
||||||
|
velocity.x = 0 |
||||||
|
velocity.z = 0 |
||||||
|
|
||||||
|
# Face target while attacking (use body rotation like player does) |
||||||
|
if _body and _body.has_method("apply_rotation"): |
||||||
|
var face_dir = Vector3(direction.x, 0, direction.z) * move_speed |
||||||
|
_body.apply_rotation(face_dir) |
||||||
|
|
||||||
|
if _attack_timer <= 0 and not _is_attacking: |
||||||
|
_perform_attack() |
||||||
|
else: |
||||||
|
# Chase target - velocity direction will be used for body rotation |
||||||
|
velocity.x = direction.x * move_speed |
||||||
|
velocity.z = direction.z * move_speed |
||||||
|
|
||||||
|
## Perform attack |
||||||
|
func _perform_attack(): |
||||||
|
if _is_attacking or not multiplayer.is_server(): |
||||||
|
return |
||||||
|
|
||||||
|
# Use weapon if equipped |
||||||
|
if equipped_weapon and equipped_weapon.weapon_data: |
||||||
|
_perform_weapon_attack() |
||||||
|
else: |
||||||
|
_perform_unarmed_attack() |
||||||
|
|
||||||
|
func _perform_weapon_attack(): |
||||||
|
var weapon = equipped_weapon |
||||||
|
var data = weapon.weapon_data |
||||||
|
|
||||||
|
var total_duration = data.startup_time + data.active_time |
||||||
|
var cooldown = max(data.attack_cooldown, total_duration) |
||||||
|
|
||||||
|
_attack_timer = cooldown |
||||||
|
_is_attacking = true |
||||||
|
|
||||||
|
# Play animation on all clients |
||||||
|
var anim_name = data.attack_animation if data.attack_animation else "Attack_OneHand" |
||||||
|
rpc("_sync_attack_animation", anim_name) |
||||||
|
|
||||||
|
# Use weapon's built-in attack activation |
||||||
|
_activate_weapon_hitbox_direct(weapon) |
||||||
|
|
||||||
|
func _perform_unarmed_attack(): |
||||||
|
var total_duration = unarmed_startup + unarmed_active |
||||||
|
var cooldown = max(attack_cooldown, total_duration) |
||||||
|
|
||||||
|
_attack_timer = cooldown |
||||||
|
_is_attacking = true |
||||||
|
|
||||||
|
# Play animation |
||||||
|
rpc("_sync_attack_animation", "Attack_OneHand") |
||||||
|
|
||||||
|
# Activate unarmed hitbox |
||||||
|
_activate_unarmed_hitbox() |
||||||
|
|
||||||
|
## Activate weapon hitbox for attack (direct access to weapon's internal hitbox) |
||||||
|
func _activate_weapon_hitbox_direct(weapon: BaseWeapon): |
||||||
|
if not weapon or not multiplayer.is_server(): |
||||||
|
_is_attacking = false |
||||||
|
return |
||||||
|
|
||||||
|
var data = weapon.weapon_data |
||||||
|
|
||||||
|
# Access weapon's internal hitbox |
||||||
|
var hitbox = weapon._hitbox |
||||||
|
|
||||||
|
if not hitbox: |
||||||
|
print("[ArmedEnemy] No hitbox found on weapon, trying to find it") |
||||||
|
hitbox = weapon.get_node_or_null("HitBox") as HitBox |
||||||
|
|
||||||
|
if not hitbox: |
||||||
|
push_error("[ArmedEnemy] Cannot find hitbox on weapon!") |
||||||
|
_is_attacking = false |
||||||
|
return |
||||||
|
|
||||||
|
# Make sure hitbox has correct owner |
||||||
|
hitbox.owner_entity = self |
||||||
|
|
||||||
|
# STARTUP PHASE |
||||||
|
if data.startup_time > 0: |
||||||
|
await get_tree().create_timer(data.startup_time).timeout |
||||||
|
|
||||||
|
if not is_instance_valid(hitbox) or is_dead: |
||||||
|
_is_attacking = false |
||||||
|
return |
||||||
|
|
||||||
|
# ACTIVE PHASE |
||||||
|
hitbox.activate() |
||||||
|
|
||||||
|
await get_tree().create_timer(data.active_time).timeout |
||||||
|
|
||||||
|
# RECOVERY PHASE |
||||||
|
if hitbox and is_instance_valid(hitbox): |
||||||
|
hitbox.deactivate() |
||||||
|
|
||||||
|
_is_attacking = false |
||||||
|
|
||||||
|
## Setup unarmed hitbox |
||||||
|
func _setup_unarmed_hitbox(): |
||||||
|
_unarmed_hitbox = HitBox.new() |
||||||
|
_unarmed_hitbox.name = "UnarmedHitBox" |
||||||
|
_unarmed_hitbox.owner_entity = self |
||||||
|
_unarmed_hitbox.set_stats(unarmed_damage, unarmed_knockback) |
||||||
|
|
||||||
|
# Add collision shape BEFORE adding hitbox to tree (so _ready can find it) |
||||||
|
var collision = CollisionShape3D.new() |
||||||
|
var sphere = SphereShape3D.new() |
||||||
|
sphere.radius = attack_range |
||||||
|
collision.shape = sphere |
||||||
|
collision.position = Vector3(0, 0.8, -attack_range * 0.75) |
||||||
|
_unarmed_hitbox.add_child(collision) |
||||||
|
|
||||||
|
# Now attach the fully configured hitbox to body |
||||||
|
if _body: |
||||||
|
_body.add_child(_unarmed_hitbox) |
||||||
|
else: |
||||||
|
add_child(_unarmed_hitbox) |
||||||
|
|
||||||
|
# Connect hit signal |
||||||
|
_unarmed_hitbox.hit_landed.connect(_on_hitbox_hit) |
||||||
|
|
||||||
|
func _activate_unarmed_hitbox(): |
||||||
|
if not _unarmed_hitbox or not multiplayer.is_server(): |
||||||
|
_is_attacking = false |
||||||
|
return |
||||||
|
|
||||||
|
# STARTUP PHASE |
||||||
|
if unarmed_startup > 0: |
||||||
|
await get_tree().create_timer(unarmed_startup).timeout |
||||||
|
|
||||||
|
if not _unarmed_hitbox or not is_instance_valid(_unarmed_hitbox) or is_dead: |
||||||
|
_is_attacking = false |
||||||
|
return |
||||||
|
|
||||||
|
# ACTIVE PHASE |
||||||
|
_unarmed_hitbox.activate() |
||||||
|
|
||||||
|
await get_tree().create_timer(unarmed_active).timeout |
||||||
|
|
||||||
|
# RECOVERY PHASE |
||||||
|
if _unarmed_hitbox and is_instance_valid(_unarmed_hitbox): |
||||||
|
_unarmed_hitbox.deactivate() |
||||||
|
|
||||||
|
_is_attacking = false |
||||||
|
|
||||||
|
## Called when hitbox hits something |
||||||
|
func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float, attacker_pos: Vector3): |
||||||
|
if not target or not multiplayer.is_server(): |
||||||
|
return |
||||||
|
|
||||||
|
# Flash target's hurtbox |
||||||
|
if target is Node: |
||||||
|
var hurtbox = target.find_child("HurtBox", true, false) |
||||||
|
if hurtbox and hurtbox.has_method("flash_hit"): |
||||||
|
hurtbox.flash_hit() |
||||||
|
|
||||||
|
# Apply damage directly (we're server) |
||||||
|
if target is BaseUnit: |
||||||
|
target.take_damage(damage_amount, 1, knockback_amount, global_position) |
||||||
|
|
||||||
|
## Sync attack animation |
||||||
|
@rpc("any_peer", "call_local", "reliable") |
||||||
|
func _sync_attack_animation(anim_name: String): |
||||||
|
if _body and _body.has_method("play_attack"): |
||||||
|
_body.play_attack(anim_name) |
||||||
|
|
||||||
|
## Override hurt animation |
||||||
|
@rpc("any_peer", "call_local", "reliable") |
||||||
|
func _play_hurt_animation(): |
||||||
|
_flash_red() |
||||||
|
|
||||||
|
## Flash red when hit |
||||||
|
func _flash_red(): |
||||||
|
_hit_flash_timer = HIT_FLASH_DURATION |
||||||
|
|
||||||
|
# Flash all mesh instances in body |
||||||
|
if _body: |
||||||
|
var meshes = _find_mesh_instances(_body) |
||||||
|
for mesh in meshes: |
||||||
|
_apply_red_flash(mesh) |
||||||
|
|
||||||
|
func _find_mesh_instances(node: Node) -> Array[MeshInstance3D]: |
||||||
|
var meshes: Array[MeshInstance3D] = [] |
||||||
|
if node is MeshInstance3D: |
||||||
|
meshes.append(node) |
||||||
|
for child in node.get_children(): |
||||||
|
meshes.append_array(_find_mesh_instances(child)) |
||||||
|
return meshes |
||||||
|
|
||||||
|
func _apply_red_flash(mesh: MeshInstance3D): |
||||||
|
if not mesh: |
||||||
|
return |
||||||
|
|
||||||
|
for i in range(mesh.get_surface_override_material_count()): |
||||||
|
var material = mesh.get_surface_override_material(i) |
||||||
|
if not material: |
||||||
|
material = mesh.mesh.surface_get_material(i) |
||||||
|
if material: |
||||||
|
material = material.duplicate() |
||||||
|
mesh.set_surface_override_material(i, material) |
||||||
|
|
||||||
|
if material and material is StandardMaterial3D: |
||||||
|
material.albedo_color = Color(1.5, 0.3, 0.3) |
||||||
|
|
||||||
|
func _reset_material(): |
||||||
|
# Reset to a neutral color after flash |
||||||
|
if _body: |
||||||
|
var meshes = _find_mesh_instances(_body) |
||||||
|
for mesh in meshes: |
||||||
|
_reset_mesh_material(mesh) |
||||||
|
|
||||||
|
func _reset_mesh_material(mesh: MeshInstance3D): |
||||||
|
if not mesh: |
||||||
|
return |
||||||
|
|
||||||
|
for i in range(mesh.get_surface_override_material_count()): |
||||||
|
var material = mesh.get_surface_override_material(i) |
||||||
|
if material and material is StandardMaterial3D: |
||||||
|
# Reset to a default enemy color (reddish) |
||||||
|
material.albedo_color = Color(0.8, 0.3, 0.3) |
||||||
|
|
||||||
|
## Death callback - drop weapons |
||||||
|
func _on_enemy_died(killer_id: int): |
||||||
|
super._on_enemy_died(killer_id) |
||||||
|
|
||||||
|
# Hide body |
||||||
|
if _body: |
||||||
|
_body.visible = false |
||||||
|
|
||||||
|
# Deactivate hitboxes |
||||||
|
if _unarmed_hitbox: |
||||||
|
_unarmed_hitbox.deactivate() |
||||||
|
|
||||||
|
# Drop equipped weapons (server only) |
||||||
|
if multiplayer.is_server(): |
||||||
|
_drop_all_weapons() |
||||||
|
|
||||||
|
print("[ArmedEnemy ", name, "] killed by ", killer_id) |
||||||
|
|
||||||
|
## Drop all equipped weapons |
||||||
|
func _drop_all_weapons(): |
||||||
|
if not multiplayer.is_server(): |
||||||
|
return |
||||||
|
|
||||||
|
# Drop main hand weapon |
||||||
|
if equipped_weapon and equipped_weapon.weapon_data: |
||||||
|
_spawn_dropped_weapon(equipped_weapon.weapon_data, false) |
||||||
|
|
||||||
|
# Drop off-hand weapon |
||||||
|
if equipped_offhand and equipped_offhand.weapon_data: |
||||||
|
_spawn_dropped_weapon(equipped_offhand.weapon_data, true) |
||||||
|
|
||||||
|
# Clear equipped weapons on all clients |
||||||
|
rpc("_clear_equipped_weapons") |
||||||
|
|
||||||
|
## Spawn a dropped weapon in the world |
||||||
|
func _spawn_dropped_weapon(data: WeaponData, is_offhand: bool): |
||||||
|
if not multiplayer.is_server(): |
||||||
|
return |
||||||
|
|
||||||
|
var resource_path = data.resource_path |
||||||
|
if resource_path == "": |
||||||
|
push_error("[ArmedEnemy] WeaponData has no resource path!") |
||||||
|
return |
||||||
|
|
||||||
|
# Calculate spawn position with slight offset and upward velocity |
||||||
|
var offset = Vector3.ZERO |
||||||
|
if is_offhand: |
||||||
|
offset = transform.basis.x * -0.5 # Left side |
||||||
|
else: |
||||||
|
offset = transform.basis.x * 0.5 # Right side |
||||||
|
|
||||||
|
var spawn_pos = global_position + offset |
||||||
|
spawn_pos.y += 1.5 # Spawn above death position |
||||||
|
|
||||||
|
# Random velocity to scatter weapons |
||||||
|
var velocity = Vector3( |
||||||
|
randf_range(-2.0, 2.0), |
||||||
|
randf_range(3.0, 5.0), # Upward |
||||||
|
randf_range(-2.0, 2.0) |
||||||
|
) |
||||||
|
|
||||||
|
# Use level's weapon spawning system |
||||||
|
var level = get_tree().get_current_scene() |
||||||
|
if level and level.has_method("spawn_world_weapon"): |
||||||
|
level._weapon_spawn_counter += 1 |
||||||
|
level.rpc("spawn_world_weapon", resource_path, spawn_pos, velocity, level._weapon_spawn_counter) |
||||||
|
print("[ArmedEnemy ", name, "] Dropped weapon: ", data.weapon_name) |
||||||
|
|
||||||
|
## Clear equipped weapons on all clients |
||||||
|
@rpc("any_peer", "call_local", "reliable") |
||||||
|
func _clear_equipped_weapons(): |
||||||
|
_unequip_weapon(false) |
||||||
|
_unequip_weapon(true) |
||||||
|
|
||||||
|
## Respawn callback |
||||||
|
func _on_enemy_respawned(): |
||||||
|
super._on_enemy_respawned() |
||||||
|
|
||||||
|
# Show body |
||||||
|
if _body: |
||||||
|
_body.visible = true |
||||||
|
_reset_material() |
||||||
|
|
||||||
|
# Reset state |
||||||
|
_attack_timer = 0.0 |
||||||
|
_is_attacking = false |
||||||
|
|
||||||
|
# Re-equip starting weapons |
||||||
|
call_deferred("_equip_starting_weapons_local") |
||||||
|
|
||||||
|
print("[ArmedEnemy ", name, "] respawned") |
||||||
|
|
||||||
|
## Set enemy color (hue-based like player) |
||||||
|
func set_enemy_color(hue: float): |
||||||
|
if _body and _body.has_method("set_character_color"): |
||||||
|
_body.set_character_color(hue) |
||||||
@ -0,0 +1 @@ |
|||||||
|
uid://deefoag762nvc |
||||||
Loading…
Reference in new issue