Colours and practise dummy

main
Twirpytherobot 6 days ago
parent ddc1522174
commit fab47fb1e0
  1. 2
      level/resources/weapon_testsword.tres
  2. 49
      level/scenes/Player_Lilguy.tscn
  3. 51
      level/scenes/enemies/practice_dummy.tscn
  4. 6
      level/scenes/level.tscn
  5. 108
      level/scripts/base_enemy.gd
  6. 1
      level/scripts/base_enemy.gd.uid
  7. 85
      level/scripts/level.gd
  8. 43
      level/scripts/lilguy_body.gd
  9. 48
      level/scripts/player.gd
  10. 112
      level/scripts/practice_dummy.gd
  11. 1
      level/scripts/practice_dummy.gd.uid

@ -11,7 +11,7 @@ damage = 20.0
attack_range = 3.5
attack_cooldown = 0.6
knockback_force = 12.0
startup_time = 0.12
startup_time = 0.5
active_time = 1.0
mesh_scene = ExtResource("1_gdc1w")
weight = 2.0

@ -47,54 +47,54 @@ _character = NodePath("../..")
animation_player = NodePath("../AnimationPlayer")
[node name="Skeleton3D" parent="LilguyRigged/Armature" index="0"]
bones/0/position = Vector3(-0.32852697, 2.914154, -546.76843)
bones/0/rotation = Quaternion(-0.6608288, 0.28933647, -0.19178489, 0.6654384)
bones/0/position = Vector3(-0.32859802, 2.9141626, -546.76843)
bones/0/rotation = Quaternion(-0.6608289, 0.28933656, -0.19178493, 0.66543835)
bones/1/position = Vector3(0.054167695, 63.219894, -3.33786e-06)
bones/1/rotation = Quaternion(0.015321622, 0.025352472, 0.09471857, 0.99506325)
bones/1/rotation = Quaternion(0.015321612, 0.025352655, 0.0947179, 0.99506336)
bones/2/position = Vector3(-1.8112361e-05, 73.7566, -1.621247e-05)
bones/2/rotation = Quaternion(0.03465983, 0.05047194, 0.051301006, 0.99680465)
bones/2/rotation = Quaternion(0.034659874, 0.050472155, 0.051301125, 0.99680465)
bones/3/position = Vector3(-3.0510128e-05, 84.29319, 9.059899e-06)
bones/3/rotation = Quaternion(0.029244617, 0.05379084, -0.051849358, 0.9967763)
bones/3/rotation = Quaternion(0.029244598, 0.05379088, -0.051849354, 0.99677634)
bones/4/position = Vector3(3.8038404e-05, 94.83001, 1.9073414e-06)
bones/4/rotation = Quaternion(0.0006216668, 0.08164933, 0.020402616, 0.99645215)
bones/4/rotation = Quaternion(0.0006217413, 0.08164966, 0.020404326, 0.9964521)
bones/5/position = Vector3(-0.25257444, 72.84532, -7.644296e-06)
bones/5/rotation = Quaternion(0.03735582, 0.19943666, -0.04159315, 0.97831464)
bones/5/rotation = Quaternion(0.037355006, 0.19943582, -0.04159446, 0.9783148)
bones/6/position = Vector3(-0.606337, 174.89494, 7.152558e-06)
bones/7/position = Vector3(-0.19949026, 76.75483, 52.286175)
bones/7/rotation = Quaternion(0.8036073, -0.09628729, 0.106725484, 0.57754105)
bones/7/rotation = Quaternion(0.80360717, -0.09628771, 0.10672592, 0.57754105)
bones/8/position = Vector3(4.5403274e-05, 110.91907, 9.404198e-05)
bones/8/rotation = Quaternion(0.25522032, -0.08967137, 0.029357875, 0.962268)
bones/8/rotation = Quaternion(0.25522023, -0.08967148, 0.029356971, 0.9622681)
bones/9/position = Vector3(2.3064584e-05, 173.66367, 5.063071e-05)
bones/9/rotation = Quaternion(0.0878422, -0.16096674, 0.2433837, 0.9524378)
bones/9/rotation = Quaternion(0.08784258, -0.16096693, 0.24338366, 0.95243776)
bones/10/position = Vector3(-2.2947788e-05, 166.48767, -1.2734416e-05)
bones/11/position = Vector3(0.23053212, 76.75536, -52.28617)
bones/11/rotation = Quaternion(0.14271575, -0.5852634, 0.782287, 0.15851216)
bones/11/rotation = Quaternion(0.14271267, -0.5852636, 0.7822879, 0.15851)
bones/12/position = Vector3(1.532285e-05, 110.91911, 4.0430357e-05)
bones/12/rotation = Quaternion(0.32196987, 0.13412467, 0.27112576, 0.8971269)
bones/12/rotation = Quaternion(0.32197043, 0.13412262, 0.2711233, 0.89712787)
bones/13/position = Vector3(1.5523525e-05, 173.6661, 0.00010698747)
bones/13/rotation = Quaternion(0.09037647, 0.101556264, -0.39819276, 0.90717196)
bones/13/rotation = Quaternion(0.090376236, 0.10155637, -0.39819276, 0.90717196)
bones/14/position = Vector3(-2.0682812e-05, 166.48976, 3.939679e-05)
bones/15/position = Vector3(0.6496186, -35.1185, 49.84838)
bones/15/rotation = Quaternion(0.38543195, 0.163806, 0.8217493, 0.3864424)
bones/15/rotation = Quaternion(0.38543156, 0.16380574, 0.82174975, 0.38644233)
bones/16/position = Vector3(8.771768e-06, 312.91962, 7.4840264e-06)
bones/16/rotation = Quaternion(-0.05300442, 0.17209676, 0.3905684, 0.90278995)
bones/16/rotation = Quaternion(-0.053004134, 0.17209636, 0.39056766, 0.90279037)
bones/17/position = Vector3(-1.8137518e-05, 301.05597, -2.1670077e-05)
bones/17/rotation = Quaternion(0.24982396, 0.64725155, -0.6711768, 0.2611037)
bones/17/rotation = Quaternion(0.2498236, 0.64725155, -0.67117685, 0.26110402)
bones/18/position = Vector3(-3.026353e-05, 14.185886, -1.4917823e-06)
bones/18/rotation = Quaternion(0.11539763, 0.017187234, -0.0100022685, 0.9931203)
bones/18/rotation = Quaternion(0.11539694, 0.017187497, -0.010002339, 0.9931204)
bones/19/position = Vector3(-4.351055e-06, 11.391233, -2.5032205e-06)
bones/20/position = Vector3(0.014209064, -35.118507, -49.848385)
bones/20/rotation = Quaternion(-0.07370265, -0.18747172, 0.9412239, 0.2711456)
bones/20/rotation = Quaternion(-0.07370261, -0.18747209, 0.94122386, 0.27114522)
bones/21/position = Vector3(2.8756085e-05, 312.91974, 5.14377e-06)
bones/21/rotation = Quaternion(-0.03735361, -0.04220169, 0.46135134, 0.8854257)
bones/21/rotation = Quaternion(-0.037353504, -0.04220154, 0.46134973, 0.88542664)
bones/22/position = Vector3(2.2092872e-05, 301.0575, 1.8114511e-05)
bones/22/rotation = Quaternion(0.7933202, 0.12858748, -0.3622723, 0.47208822)
bones/22/rotation = Quaternion(0.79332, 0.1285891, -0.36227074, 0.47208923)
bones/23/position = Vector3(1.3624241e-05, 15.034077, 9.790485e-06)
bones/23/rotation = Quaternion(0.11885707, 0.009521758, -0.0077993367, 0.9928351)
bones/23/rotation = Quaternion(0.11885707, 0.009522018, -0.0077985795, 0.9928351)
bones/24/position = Vector3(-2.4847686e-06, 11.913359, -6.198885e-06)
[node name="WeaponPoint" type="BoneAttachment3D" parent="LilguyRigged/Armature/Skeleton3D" index="1"]
transform = Transform3D(-0.43292555, -0.61284775, 0.6610542, 0.7782953, 0.11585887, 0.61711675, -0.45478758, 0.78166103, 0.42681834, -352.38528, -73.5694, -531.96124)
transform = Transform3D(-0.4329258, -0.61284786, 0.6610537, 0.7782944, 0.11585927, 0.6171174, -0.45478824, 0.78166056, 0.42681772, -352.385, -73.56995, -531.9614)
bone_name = "mixamorig_RightHand"
bone_idx = 14
@ -102,13 +102,16 @@ bone_idx = 14
transform = Transform3D(36.6912, 297.2667, 16.921356, 46.72698, 11.0892515, -296.13126, -294.05847, 38.85366, -44.94499, 24.08223, -7.4241333, 7.098694)
[node name="OffhandPoint" type="BoneAttachment3D" parent="LilguyRigged/Armature/Skeleton3D" index="2"]
transform = Transform3D(0.6212382, -0.004605584, -0.7836083, -0.620316, 0.60813713, -0.49535576, 0.47882265, 0.7938187, 0.37494114, 135.65903, 334.35764, -511.2708)
transform = Transform3D(0.62123704, -0.004605159, -0.7836091, -0.62031674, 0.6081372, -0.49535444, 0.4788229, 0.79381835, 0.3749406, 135.65929, 334.35745, -511.27094)
bone_name = "mixamorig_LeftHand"
bone_idx = 10
[node name="OffhandContainer" type="Node3D" parent="LilguyRigged/Armature/Skeleton3D/OffhandPoint"]
transform = Transform3D(-17.74905, -295.46814, -48.82108, 21.019196, -50.01525, 295.05362, -298.73593, 14.035805, 23.660797, 0.005859375, 0.39337158, 0.06616211)
[node name="AnimationPlayer" parent="LilguyRigged" index="1"]
speed_scale = 2.0
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
transform = Transform3D(2, 0, 0, 0, 2, 0, 0, 0, 2, -0.066, 1.647685, 0.01)
shape = SubResource("CapsuleShape3D_yxyay")

@ -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

@ -1,7 +1,8 @@
[gd_scene load_steps=20 format=3 uid="uid://dugaivbj1o66n"]
[gd_scene load_steps=21 format=3 uid="uid://dugaivbj1o66n"]
[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" 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="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"]
@ -47,6 +48,7 @@ color = Color(0, 0, 0, 0)
[node name="Level" type="Node3D"]
script = ExtResource("1_e1sh7")
player_scene = ExtResource("1_uvcbi")
practice_dummy_scene = ExtResource("3_i7s07")
[node name="Environment" type="Node3D" parent="."]
@ -356,6 +358,8 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0268106, 2.6057472, 8.8369
[node name="WorldWeaponSword2" parent="WeaponsContainer" instance=ExtResource("6_xerh7")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.32151043, 5.2709904)
[node name="EnemiesContainer" type="Node3D" parent="."]
[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/Option4/Quit" to="." method="_on_quit_pressed"]

@ -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

@ -4,10 +4,12 @@ extends Node3D
@onready var nick_input: LineEdit = $Menu/MainContainer/MainMenu/Option1/NickInput
@onready var address_input: LineEdit = $Menu/MainContainer/MainMenu/Option3/AddressInput
@onready var players_container: Node3D = $PlayersContainer
@onready var weapons_container: Node3D = null # Will be created if doesn't exist
@onready var weapons_container: Node3D = $WeaponsContainer
@onready var enemies_container: Node3D = $EnemiesContainer
@onready var menu: Control = $Menu
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu
@export var player_scene: PackedScene
@export var practice_dummy_scene: PackedScene
# Weapon spawning counter (server-side only)
var _weapon_spawn_counter: int = 0
@ -43,6 +45,15 @@ func _ready():
add_child(weapons_container)
print("Created WeaponsContainer")
# Create or find enemies container
if has_node("EnemiesContainer"):
enemies_container = get_node("EnemiesContainer")
else:
enemies_container = Node3D.new()
enemies_container.name = "EnemiesContainer"
add_child(enemies_container)
print("Created EnemiesContainer")
# Don't initialize weapons in _ready() - wait for Host/Join to be pressed
# This is handled in initialize_multiplayer() which is called after the
# multiplayer peer is properly set up
@ -74,6 +85,9 @@ func initialize_multiplayer():
# Spawn initial weapons when server starts
_spawn_initial_weapons()
# Spawn practice dummies
_spawn_practice_dummies()
# Spawn the host player (peer ID 1)
print("[Level] Spawning host player")
var host_info = Network.players.get(1, {"nick": "Host", "skin": "blue"})
@ -219,6 +233,18 @@ func _on_player_connected(peer_id, player_info):
else:
print("[Server] Skipping invalid weapon ", weapon_id)
# Sync existing enemies to the newly joined player
print("[Server] Syncing enemies to newly connected peer: ", peer_id)
if enemies_container:
for enemy in enemies_container.get_children():
if enemy is BaseEnemy:
# Extract ID from name (e.g., "PracticeDummy_1" -> 1)
var enemy_name_parts = enemy.name.split("_")
if enemy_name_parts.size() >= 2:
var enemy_id = enemy_name_parts[-1].to_int()
print("[Server] Syncing enemy ", enemy.name, " at position ", enemy.global_position, " to peer ", peer_id)
rpc_id(peer_id, "_spawn_dummy_local", enemy_id, enemy.global_position)
# Sync equipped weapons for all existing players to the newly joined player
print("[Server] Syncing equipped weapons to newly connected peer: ", peer_id)
for player_node in players_container.get_children():
@ -414,6 +440,63 @@ func _on_jemz_preset():
skin_input.text = "Red"
address_input.text = "127.0.0.1"
# ---------- ENEMY SPAWNING ----------
func _spawn_practice_dummies():
if not multiplayer.is_server():
return
if not practice_dummy_scene:
push_warning("Practice dummy scene not assigned!")
return
# Wait a frame for everything to be ready
await get_tree().process_frame
print("[Server] Spawning practice dummies")
# Spawn dummies at different positions
var dummy_positions = [
Vector3(10, 0, 0),
Vector3(-10, 0, 0),
Vector3(0, 0, 10),
Vector3(0, 0, -10),
]
var dummy_counter = 0
for pos in dummy_positions:
dummy_counter += 1
rpc("_spawn_dummy_local", dummy_counter, pos)
## Spawn a practice dummy on all clients
@rpc("any_peer", "call_local", "reliable")
func _spawn_dummy_local(dummy_id: int, spawn_pos: Vector3):
if not practice_dummy_scene:
push_error("Practice dummy scene not loaded!")
return
if not enemies_container:
push_error("EnemiesContainer not found!")
return
var dummy_name = "PracticeDummy_" + str(dummy_id)
# Don't spawn duplicates
if enemies_container.has_node(dummy_name):
print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " already exists")
return
print("[Peer ", multiplayer.get_unique_id(), "] Spawning dummy ", dummy_name, " at ", spawn_pos)
var dummy = practice_dummy_scene.instantiate()
dummy.name = dummy_name
dummy.position = spawn_pos
# Set multiplayer authority to server
dummy.set_multiplayer_authority(1)
enemies_container.add_child(dummy, true)
print("[Peer ", multiplayer.get_unique_id(), "] Dummy ", dummy_name, " spawned successfully")
# ---------- WEAPON SPAWNING ----------
## Spawn a weapon in the world (called from server, syncs to all clients)
@rpc("any_peer", "call_local", "reliable")

@ -65,3 +65,46 @@ func play_attack(anim_name: String = "Attack_OneHand") -> void:
#push_warning("Animation '%s' not found, using Attack_OneHand" % anim_name)
animation_player.play("Attack_OneHand", -1, 1.0)
animation_player.animation_set_next("Attack_OneHand", "")
## Apply hue shift to all mesh instances in the character
func set_character_color(hue_shift: float) -> void:
# Find all MeshInstance3D children recursively
var mesh_instances = _find_mesh_instances(self)
for mesh in mesh_instances:
_apply_hue_to_mesh(mesh, hue_shift)
## Recursively find all MeshInstance3D nodes
func _find_mesh_instances(node: Node) -> Array[MeshInstance3D]:
var meshes: Array[MeshInstance3D] = []
if node is MeshInstance3D:
meshes.append(node)
for child in node.get_children():
meshes.append_array(_find_mesh_instances(child))
return meshes
## Apply hue shift to a mesh instance
func _apply_hue_to_mesh(mesh_instance: MeshInstance3D, hue_shift: float) -> void:
if not mesh_instance:
return
# Get or create material for each surface
for i in range(mesh_instance.get_surface_override_material_count()):
var material = mesh_instance.get_surface_override_material(i)
# If no override material, get the base material and duplicate it
if not material:
material = mesh_instance.mesh.surface_get_material(i)
if material:
material = material.duplicate()
mesh_instance.set_surface_override_material(i, material)
# Apply hue shift if it's a StandardMaterial3D
if material and material is StandardMaterial3D:
var std_mat = material as StandardMaterial3D
# Create a modulate color from hue
var color = Color.from_hsv(hue_shift, 0.6, 1.0)
std_mat.albedo_color = color

@ -348,12 +348,27 @@ func get_texture_from_name(skin_color: SkinColor) -> CompressedTexture2D:
@rpc("any_peer", "reliable")
func set_player_skin(skin_name: SkinColor) -> void:
var texture = get_texture_from_name(skin_name)
set_mesh_texture(_bottom_mesh, texture)
set_mesh_texture(_chest_mesh, texture)
set_mesh_texture(_face_mesh, texture)
set_mesh_texture(_limbs_head_mesh, texture)
# Check if we're using the LilguyRigged model
if _body is LilguyBody:
# Use hue-based color system for Lilguy
var hue = _get_hue_from_skin_color(skin_name)
_body.set_character_color(hue)
else:
# Use texture-based system for 3DGodotRobot
var texture = get_texture_from_name(skin_name)
set_mesh_texture(_bottom_mesh, texture)
set_mesh_texture(_chest_mesh, texture)
set_mesh_texture(_face_mesh, texture)
set_mesh_texture(_limbs_head_mesh, texture)
## Convert SkinColor enum to hue value (0.0 to 1.0)
func _get_hue_from_skin_color(skin_color: SkinColor) -> float:
match skin_color:
SkinColor.BLUE: return 0.6 # Blue hue
SkinColor.GREEN: return 0.33 # Green hue
SkinColor.RED: return 0.0 # Red hue
SkinColor.YELLOW: return 0.16 # Yellow hue
_: return 0.6 # Default to blue
func set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2D) -> void:
if mesh_instance:
@ -435,16 +450,25 @@ func _server_apply_damage(target_name: String, damage: float, attacker_id: int,
if not multiplayer.is_server():
return
# Get the target from the players container
var level = get_tree().get_current_scene()
if not level or not level.has_node("PlayersContainer"):
if not level:
return
var players_container = level.get_node("PlayersContainer")
if not players_container.has_node(target_name):
return
var target = null
# Check players container first
if level.has_node("PlayersContainer"):
var players_container = level.get_node("PlayersContainer")
if players_container.has_node(target_name):
target = players_container.get_node(target_name)
# If not found in players, check enemies container
if not target and level.has_node("EnemiesContainer"):
var enemies_container = level.get_node("EnemiesContainer")
if enemies_container.has_node(target_name):
target = enemies_container.get_node(target_name)
var target = players_container.get_node(target_name)
# Apply damage if target found
if target and target is BaseUnit:
target.take_damage(damage, attacker_id, knockback, attacker_pos)

@ -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…
Cancel
Save