eNEMIES!!!!!!!!!
Enemys and enemy spawner - Press N to spawn wave - Press H to togglt hitboxs
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
[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")
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[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)
|
||||||
+25
-77
@@ -1,9 +1,10 @@
|
|||||||
[gd_scene load_steps=21 format=3 uid="uid://dugaivbj1o66n"]
|
[gd_scene load_steps=14 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" 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="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" 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"]
|
||||||
@@ -15,29 +16,7 @@ size = Vector2(90, 90)
|
|||||||
albedo_color = Color(0, 0.321569, 0.172549, 1)
|
albedo_color = Color(0, 0.321569, 0.172549, 1)
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_x3h1o"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_x3h1o"]
|
||||||
size = Vector3(90, 0.05, 90)
|
size = Vector3(288.0893, 0.05, 350.08374)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_lc35d"]
|
|
||||||
albedo_color = Color(0, 0, 0, 1)
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_8pl0k"]
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_f43m5"]
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_womqi"]
|
|
||||||
albedo_color = Color(0, 0, 0, 1)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_taagp"]
|
|
||||||
albedo_color = Color(0, 0, 0, 1)
|
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_q5fs2"]
|
|
||||||
size = Vector3(25, 1, 1.5)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_fs7ud"]
|
|
||||||
albedo_color = Color(0, 0, 0, 1)
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_epsao"]
|
|
||||||
size = Vector3(25, 1, 1.5)
|
|
||||||
|
|
||||||
[sub_resource type="Environment" id="Environment_qb4jd"]
|
[sub_resource type="Environment" id="Environment_qb4jd"]
|
||||||
fog_enabled = true
|
fog_enabled = true
|
||||||
@@ -56,67 +35,19 @@ practice_dummy_scene = ExtResource("3_i7s07")
|
|||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Environment/Floor"]
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Environment/Floor"]
|
||||||
|
transform = Transform3D(3.140456, 0, 0, 0, 1, 0, 0, 0, 3.8321724, 0, 0, 0)
|
||||||
mesh = SubResource("PlaneMesh_r5xs5")
|
mesh = SubResource("PlaneMesh_r5xs5")
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_o02aj")
|
surface_material_override/0 = SubResource("StandardMaterial3D_o02aj")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Environment/Floor"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Environment/Floor"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6853943, 0, 1.6468506)
|
||||||
shape = SubResource("BoxShape3D_x3h1o")
|
shape = SubResource("BoxShape3D_x3h1o")
|
||||||
|
|
||||||
[node name="Box_1" type="StaticBody3D" parent="Environment"]
|
|
||||||
collision_layer = 2
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Environment/Box_1"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, 2.91206, 0.456, 6.91607)
|
|
||||||
material_override = SubResource("StandardMaterial3D_lc35d")
|
|
||||||
mesh = SubResource("BoxMesh_8pl0k")
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Environment/Box_1"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, 2.91167, 0.456274, 6.91607)
|
|
||||||
shape = SubResource("BoxShape3D_f43m5")
|
|
||||||
|
|
||||||
[node name="Box_2" type="StaticBody3D" parent="Environment"]
|
|
||||||
collision_layer = 2
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Environment/Box_2"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, 2.91206, 2.456, 9.916)
|
|
||||||
mesh = SubResource("BoxMesh_8pl0k")
|
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_womqi")
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Environment/Box_2"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, 2.91167, 2.456, 9.916)
|
|
||||||
shape = SubResource("BoxShape3D_f43m5")
|
|
||||||
|
|
||||||
[node name="Box_3" type="StaticBody3D" parent="Environment"]
|
|
||||||
collision_layer = 2
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Environment/Box_3"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, 2.91206, 4.456, 12.916)
|
|
||||||
mesh = SubResource("BoxMesh_8pl0k")
|
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_taagp")
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Environment/Box_3"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, 2.91167, 4.456, 12.916)
|
|
||||||
shape = SubResource("BoxShape3D_f43m5")
|
|
||||||
|
|
||||||
[node name="Box_4" type="StaticBody3D" parent="Environment"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, 0, 0, 0)
|
|
||||||
collision_layer = 2
|
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Environment/Box_4"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, -3.52997, 5.947, 17.398)
|
|
||||||
layers = 2
|
|
||||||
mesh = SubResource("BoxMesh_q5fs2")
|
|
||||||
surface_material_override/0 = SubResource("StandardMaterial3D_fs7ud")
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Environment/Box_4"]
|
|
||||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, -3.52997, 5.947, 17.398)
|
|
||||||
shape = SubResource("BoxShape3D_epsao")
|
|
||||||
|
|
||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="Environment"]
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="Environment"]
|
||||||
environment = SubResource("Environment_qb4jd")
|
environment = SubResource("Environment_qb4jd")
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Environment"]
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="Environment"]
|
||||||
transform = Transform3D(1, 0, 0, 0, -0.5, 0.866025, 0, -0.866025, -0.5, 0, 4, 0)
|
transform = Transform3D(1, 0, 0, 0, -0.50000024, 0.8660253, 0, -0.8660253, -0.50000024, 0, 18.907759, 0)
|
||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
shadow_blur = 0.5
|
shadow_blur = 0.5
|
||||||
|
|
||||||
@@ -348,7 +279,7 @@ offset_right = 40.0
|
|||||||
offset_bottom = 40.0
|
offset_bottom = 40.0
|
||||||
|
|
||||||
[node name="Colosseum_10" parent="." instance=ExtResource("4_u750a")]
|
[node name="Colosseum_10" parent="." instance=ExtResource("4_u750a")]
|
||||||
transform = Transform3D(15, 0, 0, 0, 15, 0, 0, 0, 15, 1.301034, -1.2294581, 2.0630608)
|
transform = Transform3D(30, 0, 0, 0, 30, 0, 0, 0, 30, 1.301034, -2.3844016, 2.0630608)
|
||||||
|
|
||||||
[node name="WeaponsContainer" type="Node3D" parent="."]
|
[node name="WeaponsContainer" type="Node3D" parent="."]
|
||||||
|
|
||||||
@@ -360,6 +291,23 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.32151043, 5.2709904)
|
|||||||
|
|
||||||
[node name="EnemiesContainer" type="Node3D" parent="."]
|
[node name="EnemiesContainer" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="EnemySpawner" parent="." instance=ExtResource("3_spawner")]
|
||||||
|
spawn_radius = 100.0
|
||||||
|
|
||||||
|
[node name="PlayerSpawnPoints" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="PlayerSpawn1" type="Node3D" parent="PlayerSpawnPoints"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 21.496109, 0, 12.60405)
|
||||||
|
|
||||||
|
[node name="PlayerSpawn2" type="Node3D" parent="PlayerSpawnPoints"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 21.496109, 0, -13.144485)
|
||||||
|
|
||||||
|
[node name="PlayerSpawn3" type="Node3D" parent="PlayerSpawnPoints"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -16.29047, 0, -13.144485)
|
||||||
|
|
||||||
|
[node name="PlayerSpawn4" type="Node3D" parent="PlayerSpawnPoints"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -16.29047, 0, 13.986315)
|
||||||
|
|
||||||
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Buttons/Host" to="." method="_on_host_pressed"]
|
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Buttons/Host" to="." method="_on_host_pressed"]
|
||||||
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Buttons/Join" to="." method="_on_join_pressed"]
|
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Buttons/Join" to="." method="_on_join_pressed"]
|
||||||
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Option4/Quit" to="." method="_on_quit_pressed"]
|
[connection signal="pressed" from="Menu/MainContainer/MainMenu/Option4/Quit" to="." method="_on_quit_pressed"]
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
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")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cd87rsuiqhdav
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dim3dvik1fd27
|
||||||
@@ -14,6 +14,9 @@ signal hit_landed(target: Node, damage: float, knockback: float, attacker_pos: V
|
|||||||
## Owner entity (used to prevent self-damage and identify attacker)
|
## Owner entity (used to prevent self-damage and identify attacker)
|
||||||
@export var owner_entity: Node = null
|
@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)
|
## Whether hitbox is currently active (only deals damage when active)
|
||||||
var is_active: bool = false
|
var is_active: bool = false
|
||||||
## Tracks entities hit this attack (prevents multi-hit)
|
## Tracks entities hit this attack (prevents multi-hit)
|
||||||
@@ -61,10 +64,15 @@ func _create_debug_visualization(collision_shape: CollisionShape3D):
|
|||||||
if mesh:
|
if mesh:
|
||||||
_debug_mesh.mesh = mesh
|
_debug_mesh.mesh = mesh
|
||||||
_debug_mesh.material_override = _debug_material
|
_debug_mesh.material_override = _debug_material
|
||||||
|
_debug_mesh.visible = HitBox.debug_visible
|
||||||
# Don't set transform - it inherits from parent CollisionShape3D
|
# Don't set transform - it inherits from parent CollisionShape3D
|
||||||
collision_shape.add_child(_debug_mesh)
|
collision_shape.add_child(_debug_mesh)
|
||||||
|
|
||||||
func _physics_process(_delta):
|
func _physics_process(_delta):
|
||||||
|
# Update debug visibility
|
||||||
|
if _debug_mesh:
|
||||||
|
_debug_mesh.visible = HitBox.debug_visible
|
||||||
|
|
||||||
if not is_active:
|
if not is_active:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ class_name HurtBox
|
|||||||
## Attach to any entity that can be damaged (players, enemies, destructibles)
|
## Attach to any entity that can be damaged (players, enemies, destructibles)
|
||||||
## NOTE: This is a passive detection zone - HitBox handles the collision detection
|
## 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)
|
## The entity that owns this hurtbox (should be a BaseUnit or similar)
|
||||||
@export var owner_entity: Node = null
|
@export var owner_entity: Node = null
|
||||||
## Debug mesh for visualization
|
## Debug mesh for visualization
|
||||||
@@ -35,6 +38,10 @@ func _ready():
|
|||||||
_create_debug_visualization()
|
_create_debug_visualization()
|
||||||
|
|
||||||
func _process(delta):
|
func _process(delta):
|
||||||
|
# Update debug visibility
|
||||||
|
if _debug_mesh:
|
||||||
|
_debug_mesh.visible = HurtBox.debug_visible
|
||||||
|
|
||||||
# Handle hit flash timer
|
# Handle hit flash timer
|
||||||
if _hit_flash_timer > 0.0:
|
if _hit_flash_timer > 0.0:
|
||||||
_hit_flash_timer -= delta
|
_hit_flash_timer -= delta
|
||||||
@@ -75,6 +82,7 @@ func _create_debug_visualization():
|
|||||||
if mesh:
|
if mesh:
|
||||||
_debug_mesh.mesh = mesh
|
_debug_mesh.mesh = mesh
|
||||||
_debug_mesh.material_override = _debug_material
|
_debug_mesh.material_override = _debug_material
|
||||||
|
_debug_mesh.visible = HurtBox.debug_visible
|
||||||
# Don't set transform - it inherits from parent CollisionShape3D
|
# Don't set transform - it inherits from parent CollisionShape3D
|
||||||
child.add_child(_debug_mesh)
|
child.add_child(_debug_mesh)
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ extends Node3D
|
|||||||
@onready var players_container: Node3D = $PlayersContainer
|
@onready var players_container: Node3D = $PlayersContainer
|
||||||
@onready var weapons_container: Node3D = $WeaponsContainer
|
@onready var weapons_container: Node3D = $WeaponsContainer
|
||||||
@onready var enemies_container: Node3D = $EnemiesContainer
|
@onready var enemies_container: Node3D = $EnemiesContainer
|
||||||
|
@onready var enemy_spawner: EnemySpawner = $EnemySpawner
|
||||||
|
@onready var player_spawn_points: Node3D = $PlayerSpawnPoints
|
||||||
@onready var menu: Control = $Menu
|
@onready var menu: Control = $Menu
|
||||||
@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
|
||||||
@@ -17,6 +19,8 @@ var _weapon_spawn_counter: int = 0
|
|||||||
var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference
|
var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference
|
||||||
# Track if we've already initialized to prevent double-spawning
|
# Track if we've already initialized to prevent double-spawning
|
||||||
var _multiplayer_initialized: bool = false
|
var _multiplayer_initialized: bool = false
|
||||||
|
# Track next spawn point for round-robin spawning (server-side only)
|
||||||
|
var _next_spawn_index: int = 0
|
||||||
|
|
||||||
# multiplayer chat
|
# multiplayer chat
|
||||||
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message
|
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message
|
||||||
@@ -342,6 +346,20 @@ func _spawn_player_local(id: int, spawn_pos: Vector3):
|
|||||||
# rpc("sync_player_position", id, player.position)
|
# rpc("sync_player_position", id, player.position)
|
||||||
|
|
||||||
func get_spawn_point() -> Vector3:
|
func get_spawn_point() -> Vector3:
|
||||||
|
# Use PlayerSpawnPoint container if available
|
||||||
|
if player_spawn_points and player_spawn_points.get_child_count() > 0:
|
||||||
|
var spawn_children = player_spawn_points.get_children()
|
||||||
|
|
||||||
|
# Use round-robin to distribute players across spawn points
|
||||||
|
var spawn_index = _next_spawn_index % spawn_children.size()
|
||||||
|
_next_spawn_index = (spawn_index + 1) % spawn_children.size()
|
||||||
|
|
||||||
|
var spawn_node = spawn_children[spawn_index]
|
||||||
|
print("[Level] Using spawn point ", spawn_index, " at ", spawn_node.global_position)
|
||||||
|
return spawn_node.global_position
|
||||||
|
else:
|
||||||
|
# Fallback to random circle spawn if no spawn points defined
|
||||||
|
print("[Level] Warning: No PlayerSpawnPoint container found, using fallback random spawn")
|
||||||
var spawn_point = Vector2.from_angle(randf() * 2 * PI) * 10 # spawn radius
|
var spawn_point = Vector2.from_angle(randf() * 2 * PI) * 10 # spawn radius
|
||||||
return Vector3(spawn_point.x, 0, spawn_point.y)
|
return Vector3(spawn_point.x, 0, spawn_point.y)
|
||||||
|
|
||||||
@@ -388,6 +406,16 @@ func _input(event):
|
|||||||
toggle_chat()
|
toggle_chat()
|
||||||
elif event is InputEventKey and event.keycode == KEY_ENTER:
|
elif event is InputEventKey and event.keycode == KEY_ENTER:
|
||||||
_on_send_pressed()
|
_on_send_pressed()
|
||||||
|
elif event is InputEventKey and event.keycode == KEY_N and event.pressed:
|
||||||
|
# Start next wave (server only, press N)
|
||||||
|
if multiplayer.is_server() and enemy_spawner:
|
||||||
|
enemy_spawner.start_next_wave()
|
||||||
|
print("[Level] Starting wave via N key")
|
||||||
|
elif event is InputEventKey and event.keycode == KEY_H and event.pressed:
|
||||||
|
# Toggle hitbox/hurtbox debug visualization
|
||||||
|
HitBox.debug_visible = not HitBox.debug_visible
|
||||||
|
HurtBox.debug_visible = not HurtBox.debug_visible
|
||||||
|
print("[Level] Hitbox/Hurtbox debug visibility: ", HitBox.debug_visible)
|
||||||
|
|
||||||
func _on_send_pressed() -> void:
|
func _on_send_pressed() -> void:
|
||||||
var trimmed_message = message.text.strip_edges()
|
var trimmed_message = message.text.strip_edges()
|
||||||
|
|||||||
Reference in New Issue
Block a user