parent
ddc1522174
commit
fab47fb1e0
11 changed files with 468 additions and 38 deletions
@ -0,0 +1,51 @@ |
|||||||
|
[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 |
||||||
@ -0,0 +1,108 @@ |
|||||||
|
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 |
||||||
@ -0,0 +1 @@ |
|||||||
|
uid://base_enemy_script |
||||||
@ -0,0 +1,112 @@ |
|||||||
|
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!") |
||||||
@ -0,0 +1 @@ |
|||||||
|
uid://practice_dummy_script |
||||||
Loading…
Reference in new issue