Armed guys!

main
Twirpytherobot 1 month ago
parent fce4c4a3e2
commit 7629342540
  1. 3
      level/resources/weapon_applecorer.tres
  2. 14
      level/resources/weapon_lobsteraxe.tres
  3. 10
      level/resources/weapon_sword.tres
  4. 124
      level/scenes/enemies/armed_enemy.tscn
  5. 6
      level/scenes/level.tscn
  6. 7
      level/scenes/weapons/LobsterAxeMesh.tscn
  7. 616
      level/scripts/armed_enemy.gd
  8. 1
      level/scripts/armed_enemy.gd.uid
  9. 4
      level/scripts/base_enemy.gd
  10. 35
      level/scripts/base_weapon.gd
  11. 4
      level/scripts/enemy_spawner.gd
  12. 95
      level/scripts/level.gd
  13. 18
      level/scripts/lilguy_body.gd
  14. 14
      level/scripts/player.gd

@ -10,9 +10,8 @@ description = "yum"
damage = 20.0 damage = 20.0
attack_range = 3.5 attack_range = 3.5
attack_cooldown = 0.6 attack_cooldown = 0.6
attack_animation = "Attack_TwoHandSwing"
knockback_force = 12.0 knockback_force = 12.0
startup_time = 0.2 startup_time = 0.2
active_time = 0.15 active_time = 1.0
mesh_scene = ExtResource("1_1ytxi") mesh_scene = ExtResource("1_1ytxi")
weight = 2.0 weight = 2.0

@ -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

@ -1,7 +1,7 @@
[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://cuwjmtrp4silp"]
[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/sword_mesh.tscn" id="2"] [ext_resource type="PackedScene" uid="uid://dyjfaq654xne3" path="res://level/scenes/weapons/sword_mesh.tscn" id="2"]
[resource] [resource]
script = ExtResource("1") script = ExtResource("1")
@ -11,9 +11,7 @@ damage = 15.0
attack_range = 3.5 attack_range = 3.5
attack_cooldown = 0.6 attack_cooldown = 0.6
knockback_force = 12.0 knockback_force = 12.0
attack_animation = "Attack1"
startup_time = 0.12 startup_time = 0.12
active_time = 0.2 active_time = 1.0
mesh_scene = ExtResource("2") mesh_scene = ExtResource("2")
pickup_radius = 1.5
weight = 2.0 weight = 2.0

@ -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"]

@ -1,14 +1,15 @@
[gd_scene load_steps=15 format=3 uid="uid://dugaivbj1o66n"] [gd_scene load_steps=16 format=3 uid="uid://dugaivbj1o66n"]
[ext_resource type="Script" uid="uid://d0dgljwwl463n" path="res://level/scripts/level.gd" id="1_e1sh7"] [ext_resource type="Script" uid="uid://d0dgljwwl463n" path="res://level/scripts/level.gd" id="1_e1sh7"]
[ext_resource type="PackedScene" uid="uid://db06e8q8f8bdq" path="res://level/scenes/Player_Lilguy.tscn" id="1_uvcbi"] [ext_resource type="PackedScene" uid="uid://db06e8q8f8bdq" path="res://level/scenes/Player_Lilguy.tscn" id="1_uvcbi"]
[ext_resource type="PackedScene" uid="uid://dif4t1y3c07ax" path="res://level/scenes/enemies/practice_dummy.tscn" id="3_i7s07"] [ext_resource type="PackedScene" uid="uid://dif4t1y3c07ax" path="res://level/scenes/enemies/practice_dummy.tscn" id="3_i7s07"]
[ext_resource type="PackedScene" path="res://level/scenes/enemies/armed_enemy.tscn" id="4_armed"]
[ext_resource type="FontFile" uid="uid://wipqjhfqeuwd" path="res://assets/fonts/Kurland.ttf" id="3_icc4p"] [ext_resource type="FontFile" uid="uid://wipqjhfqeuwd" path="res://assets/fonts/Kurland.ttf" id="3_icc4p"]
[ext_resource type="PackedScene" uid="uid://blm8lav3xh2yw" path="res://level/scenes/enemy_spawner.tscn" id="3_spawner"] [ext_resource type="PackedScene" uid="uid://blm8lav3xh2yw" path="res://level/scenes/enemy_spawner.tscn" id="3_spawner"]
[ext_resource type="PackedScene" uid="uid://chkrcwlprbn88" path="res://assets/Objects/Colosseum_10.fbx" id="4_u750a"] [ext_resource type="PackedScene" uid="uid://chkrcwlprbn88" path="res://assets/Objects/Colosseum_10.fbx" id="4_u750a"]
[ext_resource type="PackedScene" uid="uid://hd6pq287rgye" path="res://level/scenes/weapons/world_weapon_testsword.tscn" id="5_cwx4m"] [ext_resource type="PackedScene" uid="uid://hd6pq287rgye" path="res://level/scenes/weapons/world_weapon_testsword.tscn" id="5_cwx4m"]
[ext_resource type="PackedScene" uid="uid://8c4l6s6x67vh" path="res://level/scenes/weapons/world_weapon_applecorer.tscn" id="6_xerh7"] [ext_resource type="PackedScene" uid="uid://8c4l6s6x67vh" path="res://level/scenes/weapons/world_weapon_applecorer.tscn" id="6_xerh7"]
[ext_resource type="PackedScene" path="res://level/scenes/weapons/world_weapon_lobsteraxe.tscn" id="7_lobster"] [ext_resource type="PackedScene" uid="uid://dpk7n3q8mwx2r" path="res://level/scenes/weapons/world_weapon_lobsteraxe.tscn" id="7_lobster"]
[sub_resource type="PlaneMesh" id="PlaneMesh_r5xs5"] [sub_resource type="PlaneMesh" id="PlaneMesh_r5xs5"]
size = Vector2(90, 90) size = Vector2(90, 90)
@ -29,6 +30,7 @@ color = Color(0, 0, 0, 0)
script = ExtResource("1_e1sh7") script = ExtResource("1_e1sh7")
player_scene = ExtResource("1_uvcbi") player_scene = ExtResource("1_uvcbi")
practice_dummy_scene = ExtResource("3_i7s07") practice_dummy_scene = ExtResource("3_i7s07")
armed_enemy_scene = ExtResource("4_armed")
[node name="Environment" type="Node3D" parent="."] [node name="Environment" type="Node3D" parent="."]

@ -1,17 +1,18 @@
[gd_scene load_steps=4 format=3 uid="uid://cq8r5mkn3wvxj"] [gd_scene load_steps=4 format=3 uid="uid://cq8r5mkn3wvxj"]
[ext_resource type="PackedScene" uid="uid://bk5akj878m2a3" path="res://level/scenes/weapons/LobsterAxe.glb" id="1_lobster"] [ext_resource type="PackedScene" uid="uid://cejg4ixtc5xsf" path="res://level/scenes/weapons/LobsterAxe.glb" id="1_lobster"]
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_hitbox"] [ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_hitbox"]
[sub_resource type="BoxShape3D" id="BoxShape3D_lobster"] [sub_resource type="BoxShape3D" id="BoxShape3D_lobster"]
size = Vector3(2.0, 3.2, 0.6) size = Vector3(2, 3.2, 0.6)
[node name="LobsterAxeMesh" type="Node3D"] [node name="LobsterAxeMesh" type="Node3D"]
[node name="LobsterAxe" parent="." instance=ExtResource("1_lobster")] [node name="LobsterAxe" parent="." instance=ExtResource("1_lobster")]
transform = Transform3D(0.3, 0, 0, 0, 0.3, 0, 0, 0, 0.3, 0, -1.5884135, 0) transform = Transform3D(-1.3113416e-08, 0, 0.29999998, 0, 0.29999998, 0, -0.29999998, 0, -1.3113416e-08, 0, 0.72785115, 0)
[node name="HitBox" type="Area3D" parent="."] [node name="HitBox" type="Area3D" parent="."]
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 2.1950727, 0)
script = ExtResource("2_hitbox") script = ExtResource("2_hitbox")
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"] [node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"]

@ -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

@ -25,8 +25,8 @@ func _ready():
respawned.connect(_on_enemy_respawned) respawned.connect(_on_enemy_respawned)
func _physics_process(delta): func _physics_process(delta):
# Only server handles enemy AI # Only server handles enemy AI (check peer is assigned first)
if not multiplayer.is_server(): if multiplayer.multiplayer_peer == null or not multiplayer.is_server():
return return
if is_dead: if is_dead:

@ -11,7 +11,7 @@ signal hit_connected(target: Node)
@export var weapon_data: WeaponData @export var weapon_data: WeaponData
# Runtime references # Runtime references
var owner_character: Character = null var owner_character: Node = null # Can be Character or ArmedEnemy
var _mesh_instance: Node3D = null var _mesh_instance: Node3D = null
var _attack_timer: float = 0.0 var _attack_timer: float = 0.0
var _hitbox: HitBox = null var _hitbox: HitBox = null
@ -57,10 +57,8 @@ func _setup_hitbox():
# Create hitbox dynamically based on weapon range # Create hitbox dynamically based on weapon range
_hitbox = HitBox.new() _hitbox = HitBox.new()
_hitbox.name = "HitBox" _hitbox.name = "HitBox"
add_child(_hitbox)
# Add collision shape based on attack range - use sphere for consistent detection # Add collision shape BEFORE adding hitbox to tree (so _ready can find it)
# regardless of weapon orientation during animations
var collision = CollisionShape3D.new() var collision = CollisionShape3D.new()
var sphere = SphereShape3D.new() var sphere = SphereShape3D.new()
var range_val = weapon_data.attack_range if weapon_data else 1.5 var range_val = weapon_data.attack_range if weapon_data else 1.5
@ -68,6 +66,9 @@ func _setup_hitbox():
collision.shape = sphere collision.shape = sphere
_hitbox.add_child(collision) _hitbox.add_child(collision)
# Now add the fully configured hitbox to the scene
add_child(_hitbox)
_configure_hitbox() _configure_hitbox()
## Configure hitbox with weapon stats and owner ## Configure hitbox with weapon stats and owner
@ -102,6 +103,8 @@ func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float,
# Route damage through server # Route damage through server
var attacker_id = multiplayer.get_unique_id() var attacker_id = multiplayer.get_unique_id()
# Check if owner has _server_apply_damage (Character has it, ArmedEnemy doesn't)
if owner_character.has_method("_server_apply_damage"):
if multiplayer.is_server(): if multiplayer.is_server():
# We are server, apply directly # We are server, apply directly
owner_character._server_apply_damage( owner_character._server_apply_damage(
@ -120,6 +123,10 @@ func _on_hitbox_hit(target: Node, damage_amount: float, knockback_amount: float,
knockback_amount, knockback_amount,
attacker_pos attacker_pos
) )
else:
# ArmedEnemy or other entity - apply damage directly if we're server
if multiplayer.is_server() and target is BaseUnit:
target.take_damage(damage_amount, 1, knockback_amount, attacker_pos)
## Perform an attack with this weapon ## Perform an attack with this weapon
## Called by the character who owns this weapon ## Called by the character who owns this weapon
@ -141,20 +148,24 @@ func perform_attack() -> bool:
_attack_timer = cooldown _attack_timer = cooldown
_is_attacking = true _is_attacking = true
# Notify owner character of attack cooldown (for UI) # Notify owner character of attack cooldown (for UI) - only for Characters
if owner_character and owner_character.is_multiplayer_authority(): if owner_character.is_multiplayer_authority() and "_attack_timer" in owner_character:
owner_character._attack_timer = cooldown owner_character._attack_timer = cooldown
# Play attack animation on owner (use weapon's animation) # Play attack animation on owner (use weapon's animation)
if owner_character._body: if "_body" in owner_character and owner_character._body:
var anim_name = weapon_data.attack_animation if weapon_data.attack_animation else "Attack_OneHand" var anim_name = weapon_data.attack_animation if weapon_data.attack_animation else "Attack_OneHand"
if owner_character._body.has_method("play_attack"):
owner_character._body.play_attack(anim_name) owner_character._body.play_attack(anim_name)
# Sync animation to other clients # Sync animation to other clients if method exists
if owner_character.has_method("_sync_attack_animation"):
owner_character._sync_attack_animation.rpc(anim_name) owner_character._sync_attack_animation.rpc(anim_name)
# Activate hitbox for the attack duration # Activate hitbox for the attack duration
# Only activate on authority - they detect hits and send to server # For players: only activate on authority
if owner_character.is_multiplayer_authority(): # For enemies: they are server-authoritative, so check if we're server
var should_activate = owner_character.is_multiplayer_authority() or multiplayer.is_server()
if should_activate:
_activate_hitbox() _activate_hitbox()
attack_performed.emit() attack_performed.emit()
@ -197,8 +208,8 @@ func _activate_hitbox():
func can_attack() -> bool: func can_attack() -> bool:
return _attack_timer <= 0 and not _is_attacking return _attack_timer <= 0 and not _is_attacking
## Set the character who owns this weapon ## Set the character who owns this weapon (can be Character or ArmedEnemy)
func set_owner_character(character: Character): func set_owner_character(character: Node):
owner_character = character owner_character = character
# Update hitbox owner # Update hitbox owner
if _hitbox: if _hitbox:

@ -236,8 +236,8 @@ func _process(_delta):
if _debug_circle: if _debug_circle:
_debug_circle.visible = show_spawn_radius _debug_circle.visible = show_spawn_radius
# Server spawning logic # Server spawning logic (check peer is assigned first)
if not multiplayer.is_server(): if multiplayer.multiplayer_peer == null or not multiplayer.is_server():
return return
# Handle wave delay timer # Handle wave delay timer

@ -12,6 +12,7 @@ extends Node3D
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu @onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu
@export var player_scene: PackedScene @export var player_scene: PackedScene
@export var practice_dummy_scene: PackedScene @export var practice_dummy_scene: PackedScene
@export var armed_enemy_scene: PackedScene
# Weapon spawning counter (server-side only) # Weapon spawning counter (server-side only)
var _weapon_spawn_counter: int = 0 var _weapon_spawn_counter: int = 0
@ -92,6 +93,9 @@ func initialize_multiplayer():
# Spawn practice dummies # Spawn practice dummies
_spawn_practice_dummies() _spawn_practice_dummies()
# Spawn armed enemies
_spawn_armed_enemies()
# Spawn the host player (peer ID 1) # Spawn the host player (peer ID 1)
print("[Level] Spawning host player") print("[Level] Spawning host player")
var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"}) var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"})
@ -242,6 +246,18 @@ func _on_player_connected(peer_id, player_info):
if enemies_container: if enemies_container:
for enemy in enemies_container.get_children(): for enemy in enemies_container.get_children():
if enemy is BaseEnemy: if enemy is BaseEnemy:
# Check if it's an ArmedEnemy or PracticeDummy
if enemy.name.begins_with("ArmedEnemy_"):
# Sync armed enemy with its weapons
var main_weapon_path = ""
var offhand_weapon_path = ""
if "equipped_weapon" in enemy and enemy.equipped_weapon and enemy.equipped_weapon.weapon_data:
main_weapon_path = enemy.equipped_weapon.weapon_data.resource_path
if "equipped_offhand" in enemy and enemy.equipped_offhand and enemy.equipped_offhand.weapon_data:
offhand_weapon_path = enemy.equipped_offhand.weapon_data.resource_path
print("[Server] Syncing armed enemy ", enemy.name, " to peer ", peer_id)
rpc_id(peer_id, "_spawn_armed_enemy_local", enemy.name, enemy.global_position, main_weapon_path, offhand_weapon_path)
elif enemy.name.begins_with("PracticeDummy_"):
# Extract ID from name (e.g., "PracticeDummy_1" -> 1) # Extract ID from name (e.g., "PracticeDummy_1" -> 1)
var enemy_name_parts = enemy.name.split("_") var enemy_name_parts = enemy.name.split("_")
if enemy_name_parts.size() >= 2: if enemy_name_parts.size() >= 2:
@ -630,3 +646,82 @@ func _client_spawn_weapon(weapon_data_path: String, spawn_position: Vector3, ini
# Call the regular spawn function to create the weapon # Call the regular spawn function to create the weapon
print("[Client ", multiplayer.get_unique_id(), "] Calling spawn_world_weapon locally") print("[Client ", multiplayer.get_unique_id(), "] Calling spawn_world_weapon locally")
spawn_world_weapon(weapon_data_path, spawn_position, initial_velocity, weapon_id) spawn_world_weapon(weapon_data_path, spawn_position, initial_velocity, weapon_id)
# ---------- ARMED ENEMY SPAWNING ----------
var _armed_enemy_counter: int = 0
## Spawn an armed enemy (server only, replicates to all clients)
func spawn_armed_enemy(spawn_pos: Vector3, main_weapon_path: String = "", offhand_weapon_path: String = ""):
if not multiplayer.is_server():
return
if not armed_enemy_scene:
push_warning("[Level] Armed enemy scene not assigned!")
return
_armed_enemy_counter += 1
var enemy_name = "ArmedEnemy_" + str(_armed_enemy_counter)
rpc("_spawn_armed_enemy_local", enemy_name, spawn_pos, main_weapon_path, offhand_weapon_path)
## Spawn armed enemy on all clients
@rpc("any_peer", "call_local", "reliable")
func _spawn_armed_enemy_local(enemy_name: String, spawn_pos: Vector3, main_weapon_path: String, offhand_weapon_path: String):
if not armed_enemy_scene:
push_error("[Level] Armed enemy scene not loaded!")
return
if not enemies_container:
push_error("[Level] EnemiesContainer not found!")
return
# Don't spawn duplicates
if enemies_container.has_node(enemy_name):
print("[Peer ", multiplayer.get_unique_id(), "] Armed enemy ", enemy_name, " already exists")
return
print("[Peer ", multiplayer.get_unique_id(), "] Spawning armed enemy ", enemy_name, " at ", spawn_pos)
var enemy = armed_enemy_scene.instantiate()
enemy.name = enemy_name
enemy.position = spawn_pos
# Set multiplayer authority to server
enemy.set_multiplayer_authority(1)
# Set starting weapons (for server to trigger RPCs)
if main_weapon_path != "":
var weapon_data = load(main_weapon_path) as WeaponData
if weapon_data:
enemy.starting_weapon = weapon_data
if offhand_weapon_path != "":
var offhand_data = load(offhand_weapon_path) as WeaponData
if offhand_data:
enemy.starting_offhand = offhand_data
enemies_container.add_child(enemy, true)
print("[Peer ", multiplayer.get_unique_id(), "] Armed enemy ", enemy_name, " spawned successfully")
## Spawn initial armed enemies when server starts
func _spawn_armed_enemies():
if not multiplayer.is_server():
return
if not armed_enemy_scene:
push_warning("[Level] Armed enemy scene not assigned - skipping armed enemy spawn")
return
# Wait a frame for everything to be ready
await get_tree().process_frame
print("[Server] Spawning armed enemies")
# Spawn armed enemies at different positions with weapons
var enemy_configs = [
{"pos": Vector3(15, 0, 5), "weapon": "res://level/resources/weapon_sword.tres", "offhand": ""},
{"pos": Vector3(-15, 0, 5), "weapon": "res://level/resources/weapon_sword.tres", "offhand": "res://level/resources/weapon_shield.tres"},
]
for config in enemy_configs:
spawn_armed_enemy(config["pos"], config["weapon"], config["offhand"])

@ -19,13 +19,16 @@ func animate(_velocity: Vector3) -> void:
if animation_player.is_playing() and animation_player.current_animation.begins_with("Attack"): if animation_player.is_playing() and animation_player.current_animation.begins_with("Attack"):
return return
# Check if we're dashing # Check if we're dashing (defensive check for enemies that don't have this)
if _character._is_dashing: if _character and "_is_dashing" in _character and _character._is_dashing:
if animation_player.current_animation != "Jump": if animation_player.current_animation != "Jump":
_play_animation("Jump") _play_animation("Jump")
return return
if not _character.is_on_floor(): # Check if on floor (works for any CharacterBody3D)
var on_floor = _character.is_on_floor() if _character else true
if not on_floor:
if _velocity.y < 0: if _velocity.y < 0:
# Falling - use FallIdle animation # Falling - use FallIdle animation
_play_animation("FallIdle") _play_animation("FallIdle")
@ -34,7 +37,14 @@ func animate(_velocity: Vector3) -> void:
return return
if _velocity: if _velocity:
if _character.is_running() and _character.is_on_floor(): # Check if running (defensive check - enemies don't have is_running)
var is_running_val = false
if _character and _character.has_method("is_running"):
is_running_val = _character.is_running() and on_floor
elif _velocity.length() > 5.0: # Fallback: high speed = running
is_running_val = on_floor
if is_running_val:
# Sprint animation = Run for Lilguy # Sprint animation = Run for Lilguy
_play_animation("Run") _play_animation("Run")
return return

@ -623,13 +623,7 @@ func _setup_unarmed_hitbox():
_unarmed_hitbox.owner_entity = self _unarmed_hitbox.owner_entity = self
_unarmed_hitbox.set_stats(attack_damage, unarmed_knockback) _unarmed_hitbox.set_stats(attack_damage, unarmed_knockback)
# Attach to body so it rotates with player facing direction # Add collision shape BEFORE adding hitbox to tree (so _ready can find it)
if _body:
_body.add_child(_unarmed_hitbox)
else:
add_child(_unarmed_hitbox)
# Add collision shape - larger sphere in front of player
var collision = CollisionShape3D.new() var collision = CollisionShape3D.new()
var sphere = SphereShape3D.new() var sphere = SphereShape3D.new()
sphere.radius = attack_range # Full attack range as radius sphere.radius = attack_range # Full attack range as radius
@ -638,6 +632,12 @@ func _setup_unarmed_hitbox():
collision.position = Vector3(0, 0.8, -attack_range * 0.75) collision.position = Vector3(0, 0.8, -attack_range * 0.75)
_unarmed_hitbox.add_child(collision) _unarmed_hitbox.add_child(collision)
# Now attach the fully configured hitbox to body so it rotates with player facing direction
if _body:
_body.add_child(_unarmed_hitbox)
else:
add_child(_unarmed_hitbox)
# Connect hit signal # Connect hit signal
_unarmed_hitbox.hit_landed.connect(_on_unarmed_hit) _unarmed_hitbox.hit_landed.connect(_on_unarmed_hit)

Loading…
Cancel
Save