Compare commits
No commits in common. 'main' and 'Dashfix' have entirely different histories.
38 changed files with 276 additions and 2028 deletions
Binary file not shown.
@ -1,18 +0,0 @@ |
||||
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3 uid="uid://b2q62xc0jw4w3"] |
||||
|
||||
[ext_resource type="PackedScene" uid="uid://cehc5ckhq2byd" path="res://level/scenes/weapons/Applecoremesh.tscn" id="1_1ytxi"] |
||||
[ext_resource type="Script" uid="uid://d2homvlmrg6xs" path="res://level/scripts/weapon_data.gd" id="2_hfi3c"] |
||||
|
||||
[resource] |
||||
script = ExtResource("2_hfi3c") |
||||
weapon_name = "Apple Sword" |
||||
description = "yum" |
||||
damage = 20.0 |
||||
attack_range = 3.5 |
||||
attack_cooldown = 0.6 |
||||
attack_animation = "Attack_TwoHandSwing" |
||||
knockback_force = 12.0 |
||||
startup_time = 0.2 |
||||
active_time = 0.15 |
||||
mesh_scene = ExtResource("1_1ytxi") |
||||
weight = 2.0 |
||||
@ -1,19 +0,0 @@ |
||||
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3] |
||||
|
||||
[ext_resource type="Script" path="res://level/scripts/weapon_data.gd" id="1"] |
||||
[ext_resource type="PackedScene" path="res://level/scenes/weapons/LobsterAxeMesh.tscn" id="2"] |
||||
|
||||
[resource] |
||||
script = ExtResource("1") |
||||
weapon_name = "Lobster Axe" |
||||
description = "A heavy-hitting axe shaped like a lobster claw. Surprisingly quick for its size." |
||||
damage = 18.0 |
||||
attack_range = 3.0 |
||||
attack_cooldown = 0.7 |
||||
knockback_force = 14.0 |
||||
attack_animation = "Attack1" |
||||
startup_time = 0.18 |
||||
active_time = 0.18 |
||||
mesh_scene = ExtResource("2") |
||||
pickup_radius = 1.5 |
||||
weight = 2.5 |
||||
@ -1,63 +0,0 @@ |
||||
[gd_scene load_steps=8 format=3 uid="uid://byknup31d2b53"] |
||||
|
||||
[ext_resource type="Script" uid="uid://cd87rsuiqhdav" path="res://level/scripts/basic_enemy.gd" id="1_basic_enemy"] |
||||
[ext_resource type="Script" uid="uid://bj3uepduxvgju" path="res://level/scripts/hurt_box.gd" id="2_hurtbox"] |
||||
[ext_resource type="ArrayMesh" uid="uid://dy0xld0fpulmk" path="res://assets/characters/Lobster/10029_Lobster_v1_iterations-2.obj" id="2_obxet"] |
||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="3_hitbox"] |
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"] |
||||
height = 1.869873 |
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_hitbox"] |
||||
radius = 2.0 |
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_enemy"] |
||||
properties/0/path = NodePath(".:position") |
||||
properties/0/spawn = true |
||||
properties/0/replication_mode = 1 |
||||
properties/1/path = NodePath(".:rotation") |
||||
properties/1/spawn = true |
||||
properties/1/replication_mode = 1 |
||||
|
||||
[node name="BasicEnemy" type="CharacterBody3D"] |
||||
collision_mask = 3 |
||||
script = ExtResource("1_basic_enemy") |
||||
move_speed = 3.5 |
||||
attack_damage = 5.0 |
||||
detection_range = 500.0 |
||||
max_health = 20.0 |
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="."] |
||||
transform = Transform3D(-0.29981995, -0.01038597, -0.00033419454, 0.00636219, -0.19110087, 0.23117083, -0.008215994, 0.23102501, 0.19120643, 0, 0.41641736, 0) |
||||
mesh = ExtResource("2_obxet") |
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, 1, 0) |
||||
shape = SubResource("CapsuleShape3D_body") |
||||
|
||||
[node name="HurtBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")] |
||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, -0.47265148, 0) |
||||
collision_layer = 16 |
||||
collision_mask = 0 |
||||
script = ExtResource("2_hurtbox") |
||||
owner_entity = NodePath("..") |
||||
|
||||
[node name="HurtBoxShape" type="CollisionShape3D" parent="HurtBox"] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9349365, 0) |
||||
shape = SubResource("CapsuleShape3D_body") |
||||
|
||||
[node name="HitBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, -1.2) |
||||
collision_layer = 0 |
||||
collision_mask = 16 |
||||
script = ExtResource("3_hitbox") |
||||
damage = 15.0 |
||||
knockback = 10.0 |
||||
owner_entity = NodePath("..") |
||||
|
||||
[node name="HitBoxShape" type="CollisionShape3D" parent="HitBox"] |
||||
transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 0, 0.13992286, 0) |
||||
shape = SubResource("SphereShape3D_hitbox") |
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] |
||||
replication_config = SubResource("SceneReplicationConfig_enemy") |
||||
@ -1,51 +0,0 @@ |
||||
[gd_scene load_steps=7 format=3 uid="uid://dif4t1y3c07ax"] |
||||
|
||||
[ext_resource type="Script" path="res://level/scripts/practice_dummy.gd" id="1_dummy"] |
||||
[ext_resource type="Script" uid="uid://bj3uepduxvgju" path="res://level/scripts/hurt_box.gd" id="2_hurtbox"] |
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_dummy"] |
||||
albedo_color = Color(0.8, 0.6, 0.4, 1) |
||||
metallic = 0.2 |
||||
roughness = 0.8 |
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_dummy"] |
||||
material = SubResource("StandardMaterial3D_dummy") |
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"] |
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_hurtbox"] |
||||
radius = 0.6 |
||||
height = 2.2 |
||||
|
||||
[node name="PracticeDummy" type="CharacterBody3D"] |
||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, 0, 0, 0) |
||||
collision_mask = 2 |
||||
script = ExtResource("1_dummy") |
||||
detection_range = 0.0 |
||||
is_aggressive = false |
||||
respawn_delay = 5.0 |
||||
|
||||
[node name="Mesh" type="MeshInstance3D" parent="."] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) |
||||
mesh = SubResource("CapsuleMesh_dummy") |
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) |
||||
shape = SubResource("CapsuleShape3D_body") |
||||
|
||||
[node name="HurtBox" type="Area3D" parent="." node_paths=PackedStringArray("owner_entity")] |
||||
collision_layer = 16 |
||||
collision_mask = 0 |
||||
script = ExtResource("2_hurtbox") |
||||
owner_entity = NodePath("..") |
||||
|
||||
[node name="HurtBoxShape" type="CollisionShape3D" parent="HurtBox"] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) |
||||
shape = SubResource("CapsuleShape3D_hurtbox") |
||||
|
||||
[node name="HealthLabel" type="Label3D" parent="."] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0) |
||||
billboard = 1 |
||||
text = "HP: 100/100" |
||||
font_size = 24 |
||||
outline_size = 8 |
||||
@ -1,27 +0,0 @@ |
||||
[gd_scene load_steps=4 format=3 uid="uid://blm8lav3xh2yw"] |
||||
|
||||
[ext_resource type="Script" path="res://level/scripts/enemy_spawner.gd" id="1_spawner"] |
||||
[ext_resource type="PackedScene" uid="uid://byknup31d2b53" path="res://level/scenes/enemies/basic_enemy.tscn" id="2_basic_enemy"] |
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_indicator"] |
||||
albedo_color = Color(1, 0.5, 0, 0.3) |
||||
transparency = 1 |
||||
cull_mode = 2 |
||||
shading_mode = 0 |
||||
|
||||
[node name="EnemySpawner" type="Node3D"] |
||||
script = ExtResource("1_spawner") |
||||
spawn_radius = 20.0 |
||||
spawn_height = 0.5 |
||||
enemies_per_wave = 3 |
||||
auto_start_next_wave = false |
||||
wave_delay = 5.0 |
||||
enemy_scenes = Array[PackedScene]([ExtResource("2_basic_enemy")]) |
||||
|
||||
[node name="SpawnRadiusIndicator" type="MeshInstance3D" parent="."] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 0) |
||||
visible = false |
||||
material_override = SubResource("StandardMaterial3D_indicator") |
||||
|
||||
[node name="CenterMarker" type="MeshInstance3D" parent="."] |
||||
transform = Transform3D(0.5, 0, 0, 0, 2, 0, 0, 0, 0.5, 0, 1, 0) |
||||
@ -1,19 +0,0 @@ |
||||
[gd_scene load_steps=4 format=3 uid="uid://cehc5ckhq2byd"] |
||||
|
||||
[ext_resource type="PackedScene" uid="uid://c3e6e3s2q0uro" path="res://assets/Objects/Applecorer.glb" id="1_yadub"] |
||||
[ext_resource type="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_lq7hu"] |
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ei1hh"] |
||||
size = Vector3(0.19293976, 2.9722443, 0.5605469) |
||||
|
||||
[node name="TestSwordMesh" type="Node3D"] |
||||
|
||||
[node name="Applecorer" parent="." instance=ExtResource("1_yadub")] |
||||
transform = Transform3D(-0.3, 0, -2.6226834e-08, 0, 0.3, 0, 2.6226834e-08, 0, -0.3, 0, 0, 0) |
||||
|
||||
[node name="HitBox" type="Area3D" parent="."] |
||||
script = ExtResource("2_lq7hu") |
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0034065247, 2.15522, 0.0234375) |
||||
shape = SubResource("BoxShape3D_ei1hh") |
||||
Binary file not shown.
@ -1,19 +0,0 @@ |
||||
[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="Script" uid="uid://jyas86y3f0jp" path="res://level/scripts/hit_box.gd" id="2_hitbox"] |
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_lobster"] |
||||
size = Vector3(2.0, 3.2, 0.6) |
||||
|
||||
[node name="LobsterAxeMesh" type="Node3D"] |
||||
|
||||
[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) |
||||
|
||||
[node name="HitBox" type="Area3D" parent="."] |
||||
script = ExtResource("2_hitbox") |
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="HitBox"] |
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1840072, 0) |
||||
shape = SubResource("BoxShape3D_lobster") |
||||
@ -1,17 +0,0 @@ |
||||
[gd_scene load_steps=4 format=3 uid="uid://8c4l6s6x67vh"] |
||||
|
||||
[ext_resource type="Script" uid="uid://ccnnd0y4jqiot" path="res://level/scripts/world_weapon.gd" id="1_7688s"] |
||||
[ext_resource type="Resource" uid="uid://b2q62xc0jw4w3" path="res://level/resources/weapon_applecorer.tres" id="2_7688s"] |
||||
|
||||
[sub_resource type="BoxShape3D" id="1"] |
||||
size = Vector3(0.3, 0.3, 1.2) |
||||
|
||||
[node name="WorldWeaponSword" type="RigidBody3D"] |
||||
collision_layer = 4 |
||||
collision_mask = 2 |
||||
mass = 2.0 |
||||
script = ExtResource("1_7688s") |
||||
weapon_data = ExtResource("2_7688s") |
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
||||
shape = SubResource("1") |
||||
@ -1,17 +0,0 @@ |
||||
[gd_scene load_steps=4 format=3 uid="uid://dpk7n3q8mwx2r"] |
||||
|
||||
[ext_resource type="Script" uid="uid://ccnnd0y4jqiot" path="res://level/scripts/world_weapon.gd" id="1"] |
||||
[ext_resource type="Resource" path="res://level/resources/weapon_lobsteraxe.tres" id="2"] |
||||
|
||||
[sub_resource type="BoxShape3D" id="1"] |
||||
size = Vector3(0.4, 0.4, 0.8) |
||||
|
||||
[node name="WorldWeaponLobsterAxe" type="RigidBody3D"] |
||||
collision_layer = 4 |
||||
collision_mask = 2 |
||||
mass = 2.5 |
||||
script = ExtResource("1") |
||||
weapon_data = ExtResource("2") |
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] |
||||
shape = SubResource("1") |
||||
@ -1,108 +0,0 @@ |
||||
extends BaseUnit |
||||
class_name BaseEnemy |
||||
|
||||
## Base class for all enemies in the game |
||||
## Provides common enemy functionality like AI, pathfinding, and targeting |
||||
|
||||
signal target_changed(new_target: Node) |
||||
|
||||
## Current target (usually a player) |
||||
var current_target: Node = null |
||||
## Enemy detection/aggro range |
||||
@export var detection_range: float = 10.0 |
||||
## Whether this enemy is aggressive (will attack players) |
||||
@export var is_aggressive: bool = true |
||||
|
||||
func _ready(): |
||||
super._ready() |
||||
|
||||
# Enemies should respawn by default |
||||
can_respawn = true |
||||
|
||||
# Connect to health signals for AI reactions |
||||
health_changed.connect(_on_enemy_health_changed) |
||||
died.connect(_on_enemy_died) |
||||
respawned.connect(_on_enemy_respawned) |
||||
|
||||
func _physics_process(delta): |
||||
# Only server handles enemy AI |
||||
if not multiplayer.is_server(): |
||||
return |
||||
|
||||
if is_dead: |
||||
return |
||||
|
||||
# Update target if needed |
||||
_update_target() |
||||
|
||||
## Find and update current target |
||||
func _update_target(): |
||||
# Subclasses can override this to implement custom targeting logic |
||||
pass |
||||
|
||||
## Get all players in range |
||||
func get_players_in_range(range_dist: float) -> Array[Node]: |
||||
var players_in_range: Array[Node] = [] |
||||
|
||||
# Find the players container |
||||
var level = get_tree().get_current_scene() |
||||
if not level or not level.has_node("PlayersContainer"): |
||||
return players_in_range |
||||
|
||||
var players_container = level.get_node("PlayersContainer") |
||||
|
||||
for player in players_container.get_children(): |
||||
if player is Character and not player.is_dead: |
||||
var distance = global_position.distance_to(player.global_position) |
||||
if distance <= range_dist: |
||||
players_in_range.append(player) |
||||
|
||||
return players_in_range |
||||
|
||||
## Get nearest player |
||||
func get_nearest_player() -> Node: |
||||
var players = get_players_in_range(detection_range) |
||||
|
||||
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 |
||||
|
||||
## Health changed callback |
||||
func _on_enemy_health_changed(old_health: float, new_health: float): |
||||
# Subclasses can override to react to damage |
||||
pass |
||||
|
||||
## Death callback |
||||
func _on_enemy_died(killer_id: int): |
||||
print("[Enemy ", name, "] died. Killer ID: ", killer_id) |
||||
|
||||
# Subclasses can override for death effects |
||||
pass |
||||
|
||||
## Respawn callback |
||||
func _on_enemy_respawned(): |
||||
print("[Enemy ", name, "] respawned at ", global_position) |
||||
|
||||
# Clear target on respawn |
||||
current_target = null |
||||
target_changed.emit(null) |
||||
|
||||
# Subclasses can override for respawn effects |
||||
pass |
||||
|
||||
## Override hurt animation |
||||
@rpc("any_peer", "call_local", "reliable") |
||||
func _play_hurt_animation(): |
||||
# Flash red or play hurt animation |
||||
# Subclasses should implement this with their specific animations |
||||
pass |
||||
@ -1 +0,0 @@ |
||||
uid://base_enemy_script |
||||
@ -1,285 +0,0 @@ |
||||
extends BaseEnemy |
||||
class_name BasicEnemy |
||||
|
||||
## A basic melee enemy that chases and attacks players |
||||
## Server-authoritative AI with multiplayer support |
||||
|
||||
## Movement |
||||
@export var move_speed: float = 3.0 |
||||
@export var chase_range: float = 15.0 |
||||
@export var attack_range: float = 2.5 |
||||
|
||||
## Combat |
||||
@export var attack_damage: float = 15.0 |
||||
@export var attack_knockback: float = 10.0 |
||||
@export var attack_cooldown: float = 1.5 |
||||
@export_category("Attack Timing") |
||||
@export var attack_startup: float = 0.3 # Wind-up before hit |
||||
@export var attack_active: float = 0.4 # Hit window duration |
||||
|
||||
## References |
||||
var _hitbox: HitBox = null |
||||
var _hurtbox: HurtBox = null |
||||
var _mesh: MeshInstance3D = null |
||||
|
||||
## AI State |
||||
var _attack_timer: float = 0.0 |
||||
var _is_attacking: bool = false |
||||
var _original_material: Material = null |
||||
var _hit_flash_timer: float = 0.0 |
||||
const HIT_FLASH_DURATION: float = 0.2 |
||||
|
||||
func _enter_tree(): |
||||
# Enemies are always server-authoritative |
||||
set_multiplayer_authority(1) |
||||
|
||||
func _ready(): |
||||
super._ready() |
||||
|
||||
# Find mesh, hitbox, and hurtbox |
||||
_mesh = get_node_or_null("Mesh") |
||||
_hitbox = get_node_or_null("HitBox") |
||||
_hurtbox = get_node_or_null("HurtBox") |
||||
|
||||
# Store original material for hit flash |
||||
if _mesh: |
||||
_original_material = _mesh.get_surface_override_material(0) |
||||
if not _original_material and _mesh.mesh: |
||||
_original_material = _mesh.mesh.surface_get_material(0) |
||||
|
||||
# Setup hitbox |
||||
if _hitbox: |
||||
_hitbox.owner_entity = self |
||||
_hitbox.set_stats(attack_damage, attack_knockback) |
||||
_hitbox.hit_landed.connect(_on_hitbox_hit) |
||||
|
||||
# Setup hurtbox |
||||
if _hurtbox: |
||||
_hurtbox.owner_entity = self |
||||
|
||||
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 |
||||
if 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() |
||||
|
||||
## Override to find nearest player without range limit |
||||
func get_nearest_player() -> Node: |
||||
# Find all players in a very large range (essentially unlimited) |
||||
var players = get_players_in_range(1000.0) # 1000m range - basically 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 - called by BaseEnemy |
||||
func _update_target(): |
||||
if not is_aggressive: |
||||
return |
||||
|
||||
# Always find and target the nearest player (no range limit) |
||||
var nearest = get_nearest_player() |
||||
|
||||
if nearest: |
||||
# Update target if it changed |
||||
if current_target != nearest: |
||||
current_target = nearest |
||||
target_changed.emit(nearest) |
||||
else: |
||||
# No players exist |
||||
if current_target != null: |
||||
current_target = null |
||||
target_changed.emit(null) |
||||
|
||||
## Combat AI behavior |
||||
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) |
||||
|
||||
# Face the target |
||||
if direction.length() > 0.01: |
||||
var look_dir = Vector3(direction.x, 0, direction.z) |
||||
if look_dir.length() > 0.01: |
||||
look_at(global_position + look_dir, Vector3.UP) |
||||
|
||||
# If in attack range, attack |
||||
if distance <= attack_range: |
||||
# Stop moving when attacking |
||||
velocity.x = 0 |
||||
velocity.z = 0 |
||||
|
||||
# Try to attack |
||||
if _attack_timer <= 0 and not _is_attacking: |
||||
_perform_attack() |
||||
else: |
||||
# Always chase the target (no range limit) |
||||
velocity.x = direction.x * move_speed |
||||
velocity.z = direction.z * move_speed |
||||
|
||||
## Perform melee attack |
||||
func _perform_attack(): |
||||
if _is_attacking or not multiplayer.is_server(): |
||||
return |
||||
|
||||
# Calculate total attack duration |
||||
var total_duration = attack_startup + attack_active |
||||
var cooldown = max(attack_cooldown, total_duration) |
||||
|
||||
_attack_timer = cooldown |
||||
_is_attacking = true |
||||
|
||||
# Play attack animation on all clients |
||||
rpc("_sync_attack_animation") |
||||
|
||||
# Activate hitbox |
||||
_activate_hitbox() |
||||
|
||||
## Activate hitbox for attack |
||||
func _activate_hitbox(): |
||||
if not _hitbox or not multiplayer.is_server(): |
||||
_is_attacking = false |
||||
return |
||||
|
||||
# STARTUP PHASE - Wait before activating |
||||
if attack_startup > 0: |
||||
await get_tree().create_timer(attack_startup).timeout |
||||
|
||||
if not _hitbox or not is_instance_valid(_hitbox) or is_dead: |
||||
_is_attacking = false |
||||
return |
||||
|
||||
# ACTIVE PHASE - Hitbox on |
||||
_hitbox.activate() |
||||
|
||||
await get_tree().create_timer(attack_active).timeout |
||||
|
||||
# RECOVERY PHASE - Hitbox off |
||||
if _hitbox and is_instance_valid(_hitbox): |
||||
_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() |
||||
|
||||
# Server applies damage directly |
||||
if target is BaseUnit: |
||||
target.take_damage(damage_amount, 1, knockback_amount, global_position) |
||||
|
||||
## Play attack animation (synced to all clients) |
||||
@rpc("any_peer", "call_local", "reliable") |
||||
func _sync_attack_animation(): |
||||
# Visual feedback for attack |
||||
# Could play animation here if you have an AnimationPlayer |
||||
pass |
||||
|
||||
## Override hurt animation to flash red |
||||
@rpc("any_peer", "call_local", "reliable") |
||||
func _play_hurt_animation(): |
||||
if _mesh: |
||||
_flash_red() |
||||
|
||||
## Flash red when hit |
||||
func _flash_red(): |
||||
if not _mesh: |
||||
return |
||||
|
||||
_hit_flash_timer = HIT_FLASH_DURATION |
||||
|
||||
# Create red material |
||||
var red_material = StandardMaterial3D.new() |
||||
red_material.albedo_color = Color(1.5, 0.3, 0.3) |
||||
|
||||
# Copy properties from original if exists |
||||
if _original_material and _original_material is StandardMaterial3D: |
||||
var orig = _original_material as StandardMaterial3D |
||||
red_material.metallic = orig.metallic |
||||
red_material.roughness = orig.roughness |
||||
red_material.albedo_texture = orig.albedo_texture |
||||
|
||||
_mesh.set_surface_override_material(0, red_material) |
||||
|
||||
## Reset material |
||||
func _reset_material(): |
||||
if _mesh and _original_material: |
||||
_mesh.set_surface_override_material(0, _original_material.duplicate()) |
||||
|
||||
## Death callback |
||||
func _on_enemy_died(killer_id: int): |
||||
super._on_enemy_died(killer_id) |
||||
|
||||
# Hide when dead |
||||
if _mesh: |
||||
_mesh.visible = false |
||||
|
||||
# Deactivate hitbox |
||||
if _hitbox: |
||||
_hitbox.deactivate() |
||||
|
||||
print("[BasicEnemy ", name, "] killed by ", killer_id) |
||||
|
||||
## Respawn callback |
||||
func _on_enemy_respawned(): |
||||
super._on_enemy_respawned() |
||||
|
||||
# Show mesh |
||||
if _mesh: |
||||
_mesh.visible = true |
||||
_reset_material() |
||||
|
||||
# Reset state |
||||
_attack_timer = 0.0 |
||||
_is_attacking = false |
||||
|
||||
print("[BasicEnemy ", name, "] respawned") |
||||
@ -1 +0,0 @@ |
||||
uid://cd87rsuiqhdav |
||||
@ -1,251 +0,0 @@ |
||||
extends Node3D |
||||
class_name EnemySpawner |
||||
|
||||
## Spawns enemies in waves around the arena edge |
||||
## Server-authoritative spawning with multiplayer sync |
||||
|
||||
signal wave_started(wave_number: int) |
||||
signal wave_completed(wave_number: int) |
||||
signal enemy_spawned(enemy: Node) |
||||
signal all_enemies_defeated() |
||||
|
||||
## Spawn configuration |
||||
@export_category("Spawn Settings") |
||||
@export var spawn_radius: float = 20.0 ## Distance from spawner center to spawn enemies |
||||
@export var spawn_height: float = 0.5 ## Height above ground to spawn |
||||
@export var enemies_per_wave: int = 3 ## How many enemies spawn per wave |
||||
@export var auto_start_next_wave: bool = false ## Auto-start next wave when all defeated |
||||
@export var wave_delay: float = 5.0 ## Delay before next wave auto-starts |
||||
|
||||
@export_category("Enemy Pool") |
||||
@export var enemy_scenes: Array[PackedScene] = [] ## List of enemy scenes to spawn from |
||||
|
||||
## Wave tracking |
||||
var current_wave: int = 0 |
||||
var active_enemies: Array[Node] = [] |
||||
var _wave_delay_timer: float = 0.0 |
||||
var _waiting_for_next_wave: bool = false |
||||
var _enemy_id_counter: int = 0 |
||||
|
||||
## Debug visualization |
||||
@export var show_spawn_radius: bool = false ## Show spawn radius circle in game |
||||
var _debug_circle: MeshInstance3D = null |
||||
|
||||
func _ready(): |
||||
print("[EnemySpawner] Ready. Server: ", multiplayer.is_server()) |
||||
_create_debug_circle() |
||||
|
||||
## Start a new wave |
||||
func start_wave(): |
||||
if not multiplayer.is_server(): |
||||
return |
||||
|
||||
if not is_inside_tree(): |
||||
push_error("[EnemySpawner] Cannot start wave - spawner not in scene tree!") |
||||
return |
||||
|
||||
if enemy_scenes.is_empty(): |
||||
push_error("[EnemySpawner] No enemy scenes configured!") |
||||
return |
||||
|
||||
current_wave += 1 |
||||
print("[EnemySpawner] Starting wave ", current_wave) |
||||
wave_started.emit(current_wave) |
||||
|
||||
# Spawn enemies |
||||
for i in range(enemies_per_wave): |
||||
_spawn_enemy(i, enemies_per_wave) |
||||
|
||||
## Spawn a single enemy at a position around the circle |
||||
func _spawn_enemy(index: int, total: int): |
||||
if not multiplayer.is_server(): |
||||
return |
||||
|
||||
if not is_inside_tree(): |
||||
push_error("[EnemySpawner] Cannot spawn enemy - spawner not in scene tree!") |
||||
return |
||||
|
||||
if enemy_scenes.is_empty(): |
||||
return |
||||
|
||||
# Pick a random enemy scene from the pool |
||||
var enemy_scene = enemy_scenes[randi() % enemy_scenes.size()] |
||||
|
||||
# Calculate spawn position in a circle |
||||
var angle = (TAU / total) * index # Evenly distribute around circle |
||||
var offset = Vector3( |
||||
cos(angle) * spawn_radius, |
||||
spawn_height, |
||||
sin(angle) * spawn_radius |
||||
) |
||||
var spawn_pos = global_position + offset |
||||
|
||||
# Generate unique name for multiplayer sync |
||||
_enemy_id_counter += 1 |
||||
var enemy_name = "Enemy_" + str(current_wave) + "_" + str(_enemy_id_counter) |
||||
|
||||
# Spawn on all clients via RPC (call_local will spawn on server too) |
||||
var enemy_scene_path = enemy_scene.resource_path |
||||
rpc("_spawn_enemy_on_client", enemy_name, spawn_pos, enemy_scene_path) |
||||
|
||||
## RPC to spawn enemy on all clients (including server via call_local) |
||||
@rpc("any_peer", "call_local", "reliable") |
||||
func _spawn_enemy_on_client(enemy_name: String, spawn_pos: Vector3, scene_path: String): |
||||
# Load the enemy scene |
||||
var enemy_scene = load(scene_path) |
||||
if not enemy_scene: |
||||
push_error("[EnemySpawner] Failed to load enemy scene: ", scene_path) |
||||
return |
||||
|
||||
# Instantiate enemy |
||||
var enemy = enemy_scene.instantiate() |
||||
enemy.name = enemy_name |
||||
enemy.position = spawn_pos |
||||
|
||||
# Find enemies container |
||||
var level = get_tree().get_current_scene() |
||||
var enemies_container = null |
||||
if level and level.has_node("EnemiesContainer"): |
||||
enemies_container = level.get_node("EnemiesContainer") |
||||
else: |
||||
push_error("[EnemySpawner] EnemiesContainer not found!") |
||||
return |
||||
|
||||
# Add to scene |
||||
enemies_container.add_child(enemy, true) |
||||
|
||||
# Only server tracks active enemies and connects signals |
||||
if multiplayer.is_server(): |
||||
active_enemies.append(enemy) |
||||
|
||||
# Connect death signal if enemy is BaseEnemy |
||||
if enemy is BaseEnemy: |
||||
enemy.died.connect(_on_enemy_died.bind(enemy)) |
||||
|
||||
enemy_spawned.emit(enemy) |
||||
|
||||
print("[EnemySpawner] Spawned ", enemy.name, " at ", enemy.global_position, " on peer ", multiplayer.get_unique_id()) |
||||
|
||||
## Called when an enemy dies |
||||
func _on_enemy_died(killer_id: int, enemy: Node): |
||||
if not multiplayer.is_server(): |
||||
return |
||||
|
||||
print("[EnemySpawner] Enemy ", enemy.name, " defeated by ", killer_id) |
||||
|
||||
# Will be cleaned up in _update_active_enemies |
||||
|
||||
## Update list of active enemies and check if wave complete |
||||
func _update_active_enemies(): |
||||
if not multiplayer.is_server(): |
||||
return |
||||
|
||||
# Remove dead/invalid enemies from active list |
||||
var alive_count = 0 |
||||
for enemy in active_enemies: |
||||
if is_instance_valid(enemy) and enemy is BaseEnemy and not enemy.is_dead: |
||||
alive_count += 1 |
||||
|
||||
# Check if wave is complete |
||||
if active_enemies.size() > 0 and alive_count == 0: |
||||
_on_wave_completed() |
||||
|
||||
## Called when all enemies in wave are defeated |
||||
func _on_wave_completed(): |
||||
if not multiplayer.is_server(): |
||||
return |
||||
|
||||
print("[EnemySpawner] Wave ", current_wave, " completed!") |
||||
wave_completed.emit(current_wave) |
||||
|
||||
# Clean up dead enemies after a delay |
||||
await get_tree().create_timer(2.0).timeout |
||||
_cleanup_dead_enemies() |
||||
|
||||
all_enemies_defeated.emit() |
||||
|
||||
# Auto-start next wave if enabled |
||||
if auto_start_next_wave: |
||||
_waiting_for_next_wave = true |
||||
_wave_delay_timer = wave_delay |
||||
print("[EnemySpawner] Next wave starts in ", wave_delay, " seconds") |
||||
|
||||
## Remove dead enemies from the scene |
||||
func _cleanup_dead_enemies(): |
||||
var cleaned = 0 |
||||
for enemy in active_enemies: |
||||
if is_instance_valid(enemy) and enemy is BaseEnemy and enemy.is_dead: |
||||
enemy.queue_free() |
||||
cleaned += 1 |
||||
|
||||
active_enemies.clear() |
||||
print("[EnemySpawner] Cleaned up ", cleaned, " defeated enemies") |
||||
|
||||
## Manual wave control |
||||
func start_next_wave(): |
||||
if not multiplayer.is_server(): |
||||
return |
||||
start_wave() |
||||
|
||||
func stop_waves(): |
||||
_waiting_for_next_wave = false |
||||
|
||||
## Get current wave info |
||||
func get_alive_enemy_count() -> int: |
||||
var count = 0 |
||||
for enemy in active_enemies: |
||||
if is_instance_valid(enemy) and enemy is BaseEnemy and not enemy.is_dead: |
||||
count += 1 |
||||
return count |
||||
|
||||
func is_wave_active() -> bool: |
||||
return get_alive_enemy_count() > 0 |
||||
|
||||
## Create debug visualization circle |
||||
func _create_debug_circle(): |
||||
# Create a circle mesh to show spawn radius |
||||
_debug_circle = MeshInstance3D.new() |
||||
_debug_circle.name = "DebugSpawnCircle" |
||||
|
||||
# Create circle mesh using ImmediateMesh |
||||
var immediate_mesh = ImmediateMesh.new() |
||||
var material = StandardMaterial3D.new() |
||||
material.albedo_color = Color(1, 0.5, 0, 0.6) # Orange |
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA |
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED |
||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED |
||||
|
||||
# Draw circle |
||||
immediate_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP) |
||||
var segments = 64 |
||||
for i in range(segments + 1): |
||||
var angle = (TAU / segments) * i |
||||
var x = cos(angle) * spawn_radius |
||||
var z = sin(angle) * spawn_radius |
||||
immediate_mesh.surface_add_vertex(Vector3(x, spawn_height, z)) |
||||
immediate_mesh.surface_end() |
||||
|
||||
_debug_circle.mesh = immediate_mesh |
||||
_debug_circle.material_override = material |
||||
_debug_circle.visible = show_spawn_radius |
||||
add_child(_debug_circle) |
||||
|
||||
## Update debug visualization (useful when changing spawn_radius in editor) |
||||
func _process(_delta): |
||||
# Update circle visibility |
||||
if _debug_circle: |
||||
_debug_circle.visible = show_spawn_radius |
||||
|
||||
# Server spawning logic |
||||
if not multiplayer.is_server(): |
||||
return |
||||
|
||||
# Handle wave delay timer |
||||
if _waiting_for_next_wave: |
||||
_wave_delay_timer -= _delta |
||||
if _wave_delay_timer <= 0: |
||||
_waiting_for_next_wave = false |
||||
start_wave() |
||||
|
||||
# Check if all enemies defeated |
||||
_update_active_enemies() |
||||
@ -1 +0,0 @@ |
||||
uid://dim3dvik1fd27 |
||||
@ -1,145 +0,0 @@ |
||||
extends Area3D |
||||
class_name HitBox |
||||
|
||||
## A component that deals damage to HurtBoxes |
||||
## Attach to weapons or attack effects |
||||
## Uses direct physics queries for reliable hit detection |
||||
|
||||
signal hit_landed(target: Node, damage: float, knockback: float, attacker_pos: Vector3) |
||||
|
||||
## Damage dealt on hit |
||||
@export var damage: float = 10.0 |
||||
## Knockback force applied |
||||
@export var knockback: float = 5.0 |
||||
## Owner entity (used to prevent self-damage and identify attacker) |
||||
@export var owner_entity: Node = null |
||||
|
||||
## Global debug visibility toggle (static-like via class name access) |
||||
static var debug_visible: bool = true |
||||
|
||||
## Whether hitbox is currently active (only deals damage when active) |
||||
var is_active: bool = false |
||||
## Tracks entities hit this attack (prevents multi-hit) |
||||
var _hits_this_attack: Array[Node] = [] |
||||
## Shape for queries (extracted from child CollisionShape3D) |
||||
var _query_shape: Shape3D = null |
||||
## Debug mesh for visualization |
||||
var _debug_mesh: MeshInstance3D = null |
||||
var _debug_material: StandardMaterial3D = null |
||||
|
||||
func _ready(): |
||||
# Find the collision shape for queries |
||||
for child in get_children(): |
||||
if child is CollisionShape3D and child.shape: |
||||
_query_shape = child.shape |
||||
_create_debug_visualization(child) |
||||
break |
||||
|
||||
func _create_debug_visualization(collision_shape: CollisionShape3D): |
||||
# Create a semi-transparent red mesh to visualize the hitbox |
||||
_debug_mesh = MeshInstance3D.new() |
||||
_debug_material = StandardMaterial3D.new() |
||||
_debug_material.albedo_color = Color(1.0, 0.0, 0.0, 0.4) # Red, semi-transparent |
||||
_debug_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA |
||||
_debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED |
||||
_debug_material.cull_mode = BaseMaterial3D.CULL_DISABLED # Visible from both sides |
||||
|
||||
# Create mesh matching the collision shape |
||||
var mesh: Mesh = null |
||||
if collision_shape.shape is BoxShape3D: |
||||
var box_mesh = BoxMesh.new() |
||||
box_mesh.size = collision_shape.shape.size |
||||
mesh = box_mesh |
||||
elif collision_shape.shape is SphereShape3D: |
||||
var sphere_mesh = SphereMesh.new() |
||||
sphere_mesh.radius = collision_shape.shape.radius |
||||
sphere_mesh.height = collision_shape.shape.radius * 2 |
||||
mesh = sphere_mesh |
||||
elif collision_shape.shape is CapsuleShape3D: |
||||
var capsule_mesh = CapsuleMesh.new() |
||||
capsule_mesh.radius = collision_shape.shape.radius |
||||
capsule_mesh.height = collision_shape.shape.height |
||||
mesh = capsule_mesh |
||||
|
||||
if mesh: |
||||
_debug_mesh.mesh = mesh |
||||
_debug_mesh.material_override = _debug_material |
||||
_debug_mesh.visible = HitBox.debug_visible |
||||
# Don't set transform - it inherits from parent CollisionShape3D |
||||
collision_shape.add_child(_debug_mesh) |
||||
|
||||
func _physics_process(_delta): |
||||
# Update debug visibility |
||||
if _debug_mesh: |
||||
_debug_mesh.visible = HitBox.debug_visible |
||||
|
||||
if not is_active: |
||||
return |
||||
|
||||
_check_hits() |
||||
|
||||
func _check_hits(): |
||||
if not _query_shape: |
||||
# Fallback: create a default sphere |
||||
var sphere = SphereShape3D.new() |
||||
sphere.radius = 2.0 |
||||
_query_shape = sphere |
||||
|
||||
# Use physics server for reliable queries |
||||
var space_state = get_world_3d().direct_space_state |
||||
var query = PhysicsShapeQueryParameters3D.new() |
||||
query.shape = _query_shape |
||||
query.transform = global_transform |
||||
query.collision_mask = 16 # Layer 5 (hurtbox) |
||||
query.collide_with_areas = true |
||||
query.collide_with_bodies = false |
||||
|
||||
var results = space_state.intersect_shape(query, 32) |
||||
|
||||
for result in results: |
||||
var collider = result["collider"] |
||||
if collider is HurtBox: |
||||
_process_hit(collider) |
||||
|
||||
func _process_hit(hurtbox: HurtBox): |
||||
# Don't hit our own hurtbox |
||||
if hurtbox.owner_entity == owner_entity: |
||||
return |
||||
|
||||
# Don't hit same entity twice in one attack |
||||
if hurtbox.owner_entity in _hits_this_attack: |
||||
return |
||||
|
||||
# Register this hit |
||||
var target = hurtbox.owner_entity |
||||
if target: |
||||
_hits_this_attack.append(target) |
||||
|
||||
# Get attacker position for knockback direction |
||||
var attacker_pos = global_position |
||||
if owner_entity and owner_entity is Node3D: |
||||
attacker_pos = owner_entity.global_position |
||||
|
||||
# Emit signal - let the weapon/owner handle damage routing to server |
||||
hit_landed.emit(target, damage, knockback, attacker_pos) |
||||
|
||||
## Activate hitbox (call when attack starts) |
||||
func activate(): |
||||
is_active = true |
||||
_hits_this_attack.clear() |
||||
# Change to yellow when active |
||||
if _debug_material: |
||||
_debug_material.albedo_color = Color(1.0, 1.0, 0.0, 0.5) # Yellow, semi-transparent |
||||
|
||||
## Deactivate hitbox (call when attack ends) |
||||
func deactivate(): |
||||
is_active = false |
||||
_hits_this_attack.clear() |
||||
# Change back to red when inactive |
||||
if _debug_material: |
||||
_debug_material.albedo_color = Color(1.0, 0.0, 0.0, 0.4) # Red, semi-transparent |
||||
|
||||
## Set damage stats (usually from weapon data) |
||||
func set_stats(new_damage: float, new_knockback: float): |
||||
damage = new_damage |
||||
knockback = new_knockback |
||||
@ -1 +0,0 @@ |
||||
uid://jyas86y3f0jp |
||||
@ -1,94 +0,0 @@ |
||||
extends Area3D |
||||
class_name HurtBox |
||||
|
||||
## A component that receives damage from HitBoxes |
||||
## Attach to any entity that can be damaged (players, enemies, destructibles) |
||||
## NOTE: This is a passive detection zone - HitBox handles the collision detection |
||||
|
||||
## Global debug visibility toggle (static-like via class name access) |
||||
static var debug_visible: bool = true |
||||
|
||||
## The entity that owns this hurtbox (should be a BaseUnit or similar) |
||||
@export var owner_entity: Node = null |
||||
## Debug mesh for visualization |
||||
var _debug_mesh: MeshInstance3D = null |
||||
var _debug_material: StandardMaterial3D = null |
||||
var _hit_flash_timer: float = 0.0 |
||||
const HIT_FLASH_DURATION: float = 0.3 # seconds |
||||
|
||||
func _ready(): |
||||
# Auto-find owner if not set (traverse up to find BaseUnit) |
||||
if owner_entity == null: |
||||
var parent = get_parent() |
||||
while parent: |
||||
if parent is BaseUnit: |
||||
owner_entity = parent |
||||
break |
||||
parent = parent.get_parent() |
||||
|
||||
# Configure collision - hurtboxes are on layer 5, detect nothing (passive) |
||||
collision_layer = 16 # Layer 5 (hurtbox) |
||||
collision_mask = 0 # Don't detect anything - hitboxes detect us |
||||
|
||||
# Ensure we can be detected but don't detect others |
||||
monitorable = true |
||||
monitoring = false |
||||
|
||||
# Add debug visualization |
||||
_create_debug_visualization() |
||||
|
||||
func _process(delta): |
||||
# Update debug visibility |
||||
if _debug_mesh: |
||||
_debug_mesh.visible = HurtBox.debug_visible |
||||
|
||||
# Handle hit flash timer |
||||
if _hit_flash_timer > 0.0: |
||||
_hit_flash_timer -= delta |
||||
if _hit_flash_timer <= 0.0: |
||||
# Flash finished, return to green |
||||
if _debug_material: |
||||
_debug_material.albedo_color = Color(0.0, 1.0, 0.0, 0.3) # Green |
||||
|
||||
func _create_debug_visualization(): |
||||
# Find the collision shape to visualize |
||||
for child in get_children(): |
||||
if child is CollisionShape3D and child.shape: |
||||
# Create a semi-transparent green mesh to visualize the hurtbox |
||||
_debug_mesh = MeshInstance3D.new() |
||||
_debug_material = StandardMaterial3D.new() |
||||
_debug_material.albedo_color = Color(0.0, 1.0, 0.0, 0.3) # Green, semi-transparent |
||||
_debug_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA |
||||
_debug_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED |
||||
_debug_material.cull_mode = BaseMaterial3D.CULL_DISABLED # Visible from both sides |
||||
|
||||
# Create mesh matching the collision shape |
||||
var mesh: Mesh = null |
||||
if child.shape is BoxShape3D: |
||||
var box_mesh = BoxMesh.new() |
||||
box_mesh.size = child.shape.size |
||||
mesh = box_mesh |
||||
elif child.shape is SphereShape3D: |
||||
var sphere_mesh = SphereMesh.new() |
||||
sphere_mesh.radius = child.shape.radius |
||||
sphere_mesh.height = child.shape.radius * 2 |
||||
mesh = sphere_mesh |
||||
elif child.shape is CapsuleShape3D: |
||||
var capsule_mesh = CapsuleMesh.new() |
||||
capsule_mesh.radius = child.shape.radius |
||||
capsule_mesh.height = child.shape.height |
||||
mesh = capsule_mesh |
||||
|
||||
if mesh: |
||||
_debug_mesh.mesh = mesh |
||||
_debug_mesh.material_override = _debug_material |
||||
_debug_mesh.visible = HurtBox.debug_visible |
||||
# Don't set transform - it inherits from parent CollisionShape3D |
||||
child.add_child(_debug_mesh) |
||||
break |
||||
|
||||
## Call this when the hurtbox is hit to flash red |
||||
func flash_hit(): |
||||
_hit_flash_timer = HIT_FLASH_DURATION |
||||
if _debug_material: |
||||
_debug_material.albedo_color = Color(1.0, 0.0, 0.0, 0.5) # Red, semi-transparent |
||||
@ -1 +0,0 @@ |
||||
uid://bj3uepduxvgju |
||||
@ -1,112 +0,0 @@ |
||||
extends BaseEnemy |
||||
class_name PracticeDummy |
||||
|
||||
## A stationary practice dummy for testing combat |
||||
## Cannot move or attack - just takes damage and shows health |
||||
|
||||
## Visual mesh reference |
||||
@onready var _mesh: MeshInstance3D = null |
||||
## Health label above dummy |
||||
@onready var _health_label: Label3D = null |
||||
## Original material for hit flash effect |
||||
var _original_material: Material = null |
||||
## Hit flash effect |
||||
var _hit_flash_timer: float = 0.0 |
||||
const HIT_FLASH_DURATION: float = 0.2 |
||||
|
||||
func _ready(): |
||||
super._ready() |
||||
|
||||
# Practice dummy should not be aggressive |
||||
is_aggressive = false |
||||
|
||||
# Auto-find mesh and health label |
||||
_mesh = get_node_or_null("Mesh") |
||||
_health_label = get_node_or_null("HealthLabel") |
||||
|
||||
# Store original material for flash effect |
||||
if _mesh: |
||||
_original_material = _mesh.get_surface_override_material(0) |
||||
if not _original_material and _mesh.mesh: |
||||
_original_material = _mesh.mesh.surface_get_material(0) |
||||
|
||||
# Update initial health display |
||||
_update_health_display() |
||||
|
||||
# Connect health change to update display |
||||
health_changed.connect(_on_health_display_changed) |
||||
|
||||
func _process(delta): |
||||
# Handle hit flash timer |
||||
if _hit_flash_timer > 0: |
||||
_hit_flash_timer -= delta |
||||
if _hit_flash_timer <= 0: |
||||
_reset_material() |
||||
|
||||
func _physics_process(delta): |
||||
# Don't call super._physics_process since we don't move |
||||
# Just apply gravity |
||||
if not is_on_floor(): |
||||
velocity.y -= ProjectSettings.get_setting("physics/3d/default_gravity") * delta |
||||
move_and_slide() |
||||
|
||||
## Update health display |
||||
func _update_health_display(): |
||||
if _health_label: |
||||
_health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)] |
||||
|
||||
func _on_health_display_changed(_old_health: float, _new_health: float): |
||||
_update_health_display() |
||||
|
||||
## Override hurt animation to flash red |
||||
@rpc("any_peer", "call_local", "reliable") |
||||
func _play_hurt_animation(): |
||||
if _mesh: |
||||
_flash_red() |
||||
|
||||
## Flash the dummy red when hit |
||||
func _flash_red(): |
||||
if not _mesh: |
||||
return |
||||
|
||||
_hit_flash_timer = HIT_FLASH_DURATION |
||||
|
||||
# Create red material |
||||
var red_material = StandardMaterial3D.new() |
||||
red_material.albedo_color = Color(1.5, 0.3, 0.3) # Bright red |
||||
|
||||
# Copy properties from original if it exists |
||||
if _original_material and _original_material is StandardMaterial3D: |
||||
var orig = _original_material as StandardMaterial3D |
||||
red_material.metallic = orig.metallic |
||||
red_material.roughness = orig.roughness |
||||
red_material.albedo_texture = orig.albedo_texture |
||||
|
||||
_mesh.set_surface_override_material(0, red_material) |
||||
|
||||
## Reset to original material |
||||
func _reset_material(): |
||||
if _mesh and _original_material: |
||||
_mesh.set_surface_override_material(0, _original_material.duplicate()) |
||||
|
||||
## Override death to just hide the mesh |
||||
func _on_enemy_died(killer_id: int): |
||||
super._on_enemy_died(killer_id) |
||||
|
||||
# Hide mesh when dead |
||||
if _mesh: |
||||
_mesh.visible = false |
||||
|
||||
print("[PracticeDummy] Killed by player ", killer_id, ". Respawning in ", respawn_delay, " seconds...") |
||||
|
||||
## Override respawn to show mesh again |
||||
func _on_enemy_respawned(): |
||||
super._on_enemy_respawned() |
||||
|
||||
# Show mesh when respawned |
||||
if _mesh: |
||||
_mesh.visible = true |
||||
_reset_material() |
||||
|
||||
_update_health_display() |
||||
print("[PracticeDummy] Respawned!") |
||||
@ -1 +0,0 @@ |
||||
uid://practice_dummy_script |
||||
Loading…
Reference in new issue