This commit is contained in:
Marek Le
2026-05-09 23:37:26 +02:00
parent 6d28b04c12
commit 2d4002bd3f
263 changed files with 5250 additions and 4597 deletions

View File

@@ -0,0 +1,170 @@
extends CharacterBody3D
const GRAVITY: float = 18.0
@export var stats_resource: EnemyStats
@export var is_boss: bool = false
@onready var nav: NavigationAgent3D = $NavAgent
@onready var mesh_holder: Node3D = $MeshHolder
@onready var collision: CollisionShape3D = $Collision
@onready var detection: Area3D = $DetectionArea
@onready var sync: MultiplayerSynchronizer = $Synchronizer
@onready var healthbar: MeshInstance3D = $Healthbar
@onready var name_label: Label3D = $NameLabel
var origin: Vector3 = Vector3.ZERO
var attack_cd: float = 0.0
var dead: bool = false
var invasion_target: Node = null
@export var sync_position: Vector3 = Vector3.ZERO
@export var sync_yaw: float = 0.0
func _enter_tree() -> void:
set_multiplayer_authority(1)
func _ready() -> void:
if is_boss:
add_to_group("boss")
add_to_group("enemies")
if stats_resource == null:
stats_resource = EnemyStats.new()
Stats.register(self, stats_resource)
origin = global_position
detection.body_entered.connect(_on_body_entered)
EventBus.health_changed.connect(_on_health_changed)
EventBus.entity_died.connect(_on_entity_died)
name_label.text = "Boss" if is_boss else "Enemy"
if is_boss:
name_label.text = "Boss"
var mesh: MeshInstance3D = mesh_holder.get_node("Mesh")
mesh.scale = Vector3(1.5, 1.5, 1.5)
var mat: StandardMaterial3D = StandardMaterial3D.new()
mat.albedo_color = Color(0.6, 0.2, 0.8)
mesh.material_override = mat
func _exit_tree() -> void:
Stats.deregister(self)
func _physics_process(delta: float) -> void:
if not is_multiplayer_authority():
global_position = global_position.lerp(sync_position, clamp(delta * 20.0, 0.0, 1.0))
rotation.y = lerp_angle(rotation.y, sync_yaw, clamp(delta * 20.0, 0.0, 1.0))
return
if dead:
return
if not is_on_floor():
velocity.y -= GRAVITY * delta
attack_cd = max(0.0, attack_cd - delta)
var target: Node = _get_target()
if target == null:
_return_to_origin(delta)
else:
_chase_or_attack(target, delta)
move_and_slide()
sync_position = global_position
sync_yaw = rotation.y
func _get_target() -> Node:
var aggro: Node = get_node_or_null("/root/World/Systems/AggroSystem")
if aggro == null:
aggro = get_node_or_null("/root/Dungeon/Systems/AggroSystem")
if aggro and aggro.has_method("target_for"):
var t: Node = aggro.target_for(self)
if t:
return t
if invasion_target and is_instance_valid(invasion_target):
return invasion_target
return null
func _chase_or_attack(target: Node, delta: float) -> void:
var t_pos: Vector3 = (target as Node3D).global_position
var d: float = global_position.distance_to(t_pos)
var attack_range: float = float(Stats.get_stat(self, "attack_range", 2.0))
if d <= attack_range:
velocity.x = move_toward(velocity.x, 0.0, 20.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, 20.0 * delta)
var look := Vector3(t_pos.x - global_position.x, 0.0, t_pos.z - global_position.z)
if look.length() > 0.01:
rotation.y = atan2(look.x, look.z)
if attack_cd <= 0.0:
attack_cd = float(Stats.get_stat(self, "attack_cooldown", 1.5))
var dmg: float = float(Stats.get_stat(self, "attack_damage", 5.0))
EventBus.damage_requested.emit(self, target, dmg, Element.NONE)
else:
var dir := Vector3(t_pos.x - global_position.x, 0.0, t_pos.z - global_position.z).normalized()
var speed: float = float(Stats.get_stat(self, "speed", 3.0))
velocity.x = dir.x * speed
velocity.z = dir.z * speed
if dir.length() > 0.01:
rotation.y = atan2(dir.x, dir.z)
func _return_to_origin(delta: float) -> void:
var d: float = global_position.distance_to(origin)
if d < 0.5:
velocity.x = move_toward(velocity.x, 0.0, 20.0 * delta)
velocity.z = move_toward(velocity.z, 0.0, 20.0 * delta)
var max_hp: float = float(Stats.get_stat(self, "max_health", 100.0))
var hp: float = float(Stats.get_stat(self, "health", 0.0))
if hp > 0.0 and hp < max_hp:
Stats.set_stat(self, "health", min(max_hp, hp + max_hp * 0.20 * delta))
EventBus.health_changed.emit(self, Stats.get_stat(self, "health"), max_hp)
else:
var dir: Vector3 = Vector3(origin.x - global_position.x, 0.0, origin.z - global_position.z).normalized()
var speed: float = float(Stats.get_stat(self, "speed", 3.0)) * 1.5
velocity.x = dir.x * speed
velocity.z = dir.z * speed
if dir.length() > 0.01:
rotation.y = atan2(dir.x, dir.z)
func _on_body_entered(body: Node) -> void:
if not multiplayer.is_server() and multiplayer.multiplayer_peer != null:
return
if body.is_in_group("player") and not dead:
EventBus.enemy_detected.emit(self, body)
func _on_health_changed(entity: Node, current: float, max: float) -> void:
if entity != self:
return
var ratio: float = clamp(current / max if max > 0 else 0.0, 0.0, 1.0)
if healthbar:
healthbar.scale.x = max(0.01, ratio)
func _on_entity_died(entity: Node) -> void:
if entity != self or dead:
return
dead = true
if multiplayer.is_server() or multiplayer.multiplayer_peer == null:
_on_death.rpc()
var loot: Node = get_node_or_null("/root/World/Systems/LootSystem")
if loot == null:
loot = get_node_or_null("/root/Dungeon/Systems/LootSystem")
if loot and loot.has_method("drop_loot_for"):
loot.drop_loot_for(self, global_position)
var xp_sys: Node = get_node_or_null("/root/World/Systems/XpSystem")
if xp_sys == null:
xp_sys = get_node_or_null("/root/Dungeon/Systems/XpSystem")
if xp_sys and xp_sys.has_method("award_for_enemy"):
xp_sys.award_for_enemy(self)
if is_boss:
EventBus.boss_defeated.emit(self)
var t := get_tree().create_timer(2.0)
t.timeout.connect(func():
if is_instance_valid(self):
queue_free())
@rpc("any_peer", "reliable", "call_local")
func _on_death() -> void:
if multiplayer.multiplayer_peer != null and not (multiplayer.multiplayer_peer is OfflineMultiplayerPeer) and multiplayer.get_remote_sender_id() != 0 and multiplayer.get_remote_sender_id() != 1:
return
dead = true
collision.disabled = true
modulate_alpha(0.4)
func modulate_alpha(a: float) -> void:
for child in mesh_holder.get_children():
if child is MeshInstance3D and child.material_override:
var c: Color = child.material_override.albedo_color
c.a = a
child.material_override.albedo_color = c

View File

@@ -0,0 +1 @@
uid://t28sckpnef15

View File

@@ -0,0 +1,76 @@
[gd_scene load_steps=8 format=3 uid="uid://b0enemy00001"]
[ext_resource type="Script" path="res://scenes/entities/enemy/enemy.gd" id="1"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
height = 1.6
radius = 0.4
[sub_resource type="CapsuleMesh" id="CapsuleMesh_1"]
height = 1.6
radius = 0.4
[sub_resource type="StandardMaterial3D" id="Mat_1"]
albedo_color = Color(0.6, 0.6, 0.6, 1)
[sub_resource type="SphereShape3D" id="SphereShape3D_1"]
radius = 12.0
[sub_resource type="QuadMesh" id="QuadMesh_1"]
size = Vector2(1.0, 0.12)
[sub_resource type="StandardMaterial3D" id="HBMat"]
shading_mode = 0
no_depth_test = true
albedo_color = Color(0.9, 0.2, 0.2, 1)
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_1"]
properties/0/path = NodePath(".:sync_position")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:sync_yaw")
properties/1/spawn = true
properties/1/replication_mode = 1
[node name="Enemy" type="CharacterBody3D"]
collision_layer = 4
collision_mask = 1
script = ExtResource("1")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource("CapsuleShape3D_1")
[node name="MeshHolder" type="Node3D" parent="."]
[node name="Mesh" type="MeshInstance3D" parent="MeshHolder"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
mesh = SubResource("CapsuleMesh_1")
material_override = SubResource("Mat_1")
[node name="NameLabel" type="Label3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.2, 0)
billboard = 1
text = "Enemy"
font_size = 20
outline_size = 3
[node name="Healthbar" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.95, 0)
mesh = SubResource("QuadMesh_1")
material_override = SubResource("HBMat")
gi_mode = 0
[node name="DetectionArea" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 2
[node name="DetectionShape" type="CollisionShape3D" parent="DetectionArea"]
shape = SubResource("SphereShape3D_1")
[node name="NavAgent" type="NavigationAgent3D" parent="."]
path_desired_distance = 0.5
target_desired_distance = 0.5
[node name="Synchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_1")