|
|
|
|
@ -17,6 +17,12 @@ class_name ArmedEnemy |
|
|
|
|
@export var unarmed_startup: float = 0.15 |
|
|
|
|
@export var unarmed_active: float = 0.2 |
|
|
|
|
|
|
|
|
|
## Weapon seeking (when unarmed) |
|
|
|
|
@export_category("Weapon Seeking") |
|
|
|
|
@export var seek_weapons_when_unarmed: bool = true ## If true, unarmed enemies will seek nearby weapons |
|
|
|
|
@export var weapon_seek_range: float = 30.0 ## How far to look for weapons |
|
|
|
|
@export var weapon_pickup_range: float = 1.5 ## How close to get before picking up |
|
|
|
|
|
|
|
|
|
## Weapon system |
|
|
|
|
@export_category("Weapons") |
|
|
|
|
@export var starting_weapon: WeaponData = null ## Weapon to equip on spawn |
|
|
|
|
@ -197,13 +203,19 @@ func _physics_process(delta): |
|
|
|
|
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 |
|
|
|
|
# AI behavior - prioritize weapon seeking when unarmed |
|
|
|
|
var seeking_weapon = false |
|
|
|
|
if seek_weapons_when_unarmed and equipped_weapon == null: |
|
|
|
|
seeking_weapon = _ai_seek_weapon(delta) |
|
|
|
|
|
|
|
|
|
# If not seeking a weapon (or armed), do normal combat |
|
|
|
|
if not seeking_weapon: |
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
@ -265,6 +277,28 @@ func get_nearest_player() -> Node: |
|
|
|
|
|
|
|
|
|
return nearest_player |
|
|
|
|
|
|
|
|
|
## Find the nearest WorldWeapon within range |
|
|
|
|
func get_nearest_world_weapon() -> WorldWeapon: |
|
|
|
|
var level = get_tree().get_current_scene() |
|
|
|
|
if not level: |
|
|
|
|
return null |
|
|
|
|
|
|
|
|
|
var weapons_container = level.get_node_or_null("WeaponsContainer") |
|
|
|
|
if not weapons_container: |
|
|
|
|
return null |
|
|
|
|
|
|
|
|
|
var nearest_weapon: WorldWeapon = null |
|
|
|
|
var nearest_distance = weapon_seek_range |
|
|
|
|
|
|
|
|
|
for child in weapons_container.get_children(): |
|
|
|
|
if child is WorldWeapon: |
|
|
|
|
var distance = global_position.distance_to(child.global_position) |
|
|
|
|
if distance < nearest_distance: |
|
|
|
|
nearest_distance = distance |
|
|
|
|
nearest_weapon = child |
|
|
|
|
|
|
|
|
|
return nearest_weapon |
|
|
|
|
|
|
|
|
|
## Update target |
|
|
|
|
func _update_target(): |
|
|
|
|
if not is_aggressive: |
|
|
|
|
@ -311,6 +345,62 @@ func _ai_combat(delta): |
|
|
|
|
velocity.x = direction.x * move_speed |
|
|
|
|
velocity.z = direction.z * move_speed |
|
|
|
|
|
|
|
|
|
## Weapon seeking AI - returns true if actively seeking a weapon |
|
|
|
|
func _ai_seek_weapon(delta) -> bool: |
|
|
|
|
var nearest_weapon = get_nearest_world_weapon() |
|
|
|
|
if not nearest_weapon or not is_instance_valid(nearest_weapon): |
|
|
|
|
return false |
|
|
|
|
|
|
|
|
|
var weapon_pos = nearest_weapon.global_position |
|
|
|
|
var direction = (weapon_pos - global_position).normalized() |
|
|
|
|
var distance = global_position.distance_to(weapon_pos) |
|
|
|
|
|
|
|
|
|
# If close enough, pick up the weapon |
|
|
|
|
if distance <= weapon_pickup_range: |
|
|
|
|
velocity.x = 0 |
|
|
|
|
velocity.z = 0 |
|
|
|
|
_pickup_world_weapon(nearest_weapon) |
|
|
|
|
return true |
|
|
|
|
|
|
|
|
|
# Move towards the weapon |
|
|
|
|
velocity.x = direction.x * move_speed |
|
|
|
|
velocity.z = direction.z * move_speed |
|
|
|
|
return true |
|
|
|
|
|
|
|
|
|
## Pick up a world weapon (server only) |
|
|
|
|
func _pickup_world_weapon(world_weapon: WorldWeapon): |
|
|
|
|
if not multiplayer.is_server(): |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
if not world_weapon or not is_instance_valid(world_weapon): |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
var weapon_data = world_weapon.weapon_data |
|
|
|
|
if not weapon_data: |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
var resource_path = weapon_data.resource_path |
|
|
|
|
if resource_path == "": |
|
|
|
|
push_error("[ArmedEnemy] WorldWeapon has no resource path!") |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
var weapon_id = world_weapon.weapon_id |
|
|
|
|
if weapon_id == -1: |
|
|
|
|
push_error("[ArmedEnemy] WorldWeapon has invalid weapon_id!") |
|
|
|
|
return |
|
|
|
|
|
|
|
|
|
print("[ArmedEnemy ", name, "] Picking up weapon: ", weapon_data.weapon_name) |
|
|
|
|
|
|
|
|
|
# Equip the weapon on all clients |
|
|
|
|
rpc("_equip_weapon_sync", resource_path, false) |
|
|
|
|
|
|
|
|
|
# Remove the world weapon from all clients using level's system |
|
|
|
|
var level = get_tree().get_current_scene() |
|
|
|
|
if level and level.has_method("remove_world_weapon"): |
|
|
|
|
level.remove_world_weapon(weapon_id) |
|
|
|
|
else: |
|
|
|
|
push_error("[ArmedEnemy] Level doesn't have remove_world_weapon method!") |
|
|
|
|
|
|
|
|
|
## Perform attack |
|
|
|
|
func _perform_attack(): |
|
|
|
|
if _is_attacking or not multiplayer.is_server(): |
|
|
|
|
@ -526,6 +616,17 @@ func _on_enemy_died(killer_id: int): |
|
|
|
|
if _body: |
|
|
|
|
_body.visible = false |
|
|
|
|
|
|
|
|
|
# Disable collision so players can walk through |
|
|
|
|
var collision_shape = get_node_or_null("CollisionShape3D") |
|
|
|
|
if collision_shape: |
|
|
|
|
collision_shape.disabled = true |
|
|
|
|
|
|
|
|
|
# Disable hurtbox |
|
|
|
|
var hurtbox = get_node_or_null("HurtBox") |
|
|
|
|
if hurtbox: |
|
|
|
|
hurtbox.monitoring = false |
|
|
|
|
hurtbox.monitorable = false |
|
|
|
|
|
|
|
|
|
# Deactivate hitboxes |
|
|
|
|
if _unarmed_hitbox: |
|
|
|
|
_unarmed_hitbox.deactivate() |
|
|
|
|
@ -601,6 +702,17 @@ func _on_enemy_respawned(): |
|
|
|
|
_body.visible = true |
|
|
|
|
_reset_material() |
|
|
|
|
|
|
|
|
|
# Re-enable collision |
|
|
|
|
var collision_shape = get_node_or_null("CollisionShape3D") |
|
|
|
|
if collision_shape: |
|
|
|
|
collision_shape.disabled = false |
|
|
|
|
|
|
|
|
|
# Re-enable hurtbox |
|
|
|
|
var hurtbox = get_node_or_null("HurtBox") |
|
|
|
|
if hurtbox: |
|
|
|
|
hurtbox.monitoring = false # Hurtbox doesn't monitor, it's monitored |
|
|
|
|
hurtbox.monitorable = true |
|
|
|
|
|
|
|
|
|
# Reset state |
|
|
|
|
_attack_timer = 0.0 |
|
|
|
|
_is_attacking = false |
|
|
|
|
|