From fab47fb1e0978770b654313008864f1e4eb00bc6 Mon Sep 17 00:00:00 2001 From: Twirpytherobot Date: Sat, 29 Nov 2025 18:10:23 +0000 Subject: [PATCH] Colours and practise dummy --- level/resources/weapon_testsword.tres | 2 +- level/scenes/Player_Lilguy.tscn | 49 +++++----- level/scenes/enemies/practice_dummy.tscn | 51 +++++++++++ level/scenes/level.tscn | 6 +- level/scripts/base_enemy.gd | 108 ++++++++++++++++++++++ level/scripts/base_enemy.gd.uid | 1 + level/scripts/level.gd | 85 ++++++++++++++++- level/scripts/lilguy_body.gd | 43 +++++++++ level/scripts/player.gd | 48 +++++++--- level/scripts/practice_dummy.gd | 112 +++++++++++++++++++++++ level/scripts/practice_dummy.gd.uid | 1 + 11 files changed, 468 insertions(+), 38 deletions(-) create mode 100644 level/scenes/enemies/practice_dummy.tscn create mode 100644 level/scripts/base_enemy.gd create mode 100644 level/scripts/base_enemy.gd.uid create mode 100644 level/scripts/practice_dummy.gd create mode 100644 level/scripts/practice_dummy.gd.uid diff --git a/level/resources/weapon_testsword.tres b/level/resources/weapon_testsword.tres index cd82667..f73452e 100644 --- a/level/resources/weapon_testsword.tres +++ b/level/resources/weapon_testsword.tres @@ -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 diff --git a/level/scenes/Player_Lilguy.tscn b/level/scenes/Player_Lilguy.tscn index 5277a36..ed36649 100644 --- a/level/scenes/Player_Lilguy.tscn +++ b/level/scenes/Player_Lilguy.tscn @@ -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") diff --git a/level/scenes/enemies/practice_dummy.tscn b/level/scenes/enemies/practice_dummy.tscn new file mode 100644 index 0000000..64d9050 --- /dev/null +++ b/level/scenes/enemies/practice_dummy.tscn @@ -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 diff --git a/level/scenes/level.tscn b/level/scenes/level.tscn index db2c6a8..53c5e89 100644 --- a/level/scenes/level.tscn +++ b/level/scenes/level.tscn @@ -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"] diff --git a/level/scripts/base_enemy.gd b/level/scripts/base_enemy.gd new file mode 100644 index 0000000..9cd2fd8 --- /dev/null +++ b/level/scripts/base_enemy.gd @@ -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 diff --git a/level/scripts/base_enemy.gd.uid b/level/scripts/base_enemy.gd.uid new file mode 100644 index 0000000..8ab3fb3 --- /dev/null +++ b/level/scripts/base_enemy.gd.uid @@ -0,0 +1 @@ +uid://base_enemy_script diff --git a/level/scripts/level.gd b/level/scripts/level.gd index ccf3130..f70b33c 100644 --- a/level/scripts/level.gd +++ b/level/scripts/level.gd @@ -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") diff --git a/level/scripts/lilguy_body.gd b/level/scripts/lilguy_body.gd index 5d3ca5f..0acf25e 100644 --- a/level/scripts/lilguy_body.gd +++ b/level/scripts/lilguy_body.gd @@ -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 diff --git a/level/scripts/player.gd b/level/scripts/player.gd index 290e5f8..8f7dbc7 100644 --- a/level/scripts/player.gd +++ b/level/scripts/player.gd @@ -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) diff --git a/level/scripts/practice_dummy.gd b/level/scripts/practice_dummy.gd new file mode 100644 index 0000000..27bc8fe --- /dev/null +++ b/level/scripts/practice_dummy.gd @@ -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!") diff --git a/level/scripts/practice_dummy.gd.uid b/level/scripts/practice_dummy.gd.uid new file mode 100644 index 0000000..f585811 --- /dev/null +++ b/level/scripts/practice_dummy.gd.uid @@ -0,0 +1 @@ +uid://practice_dummy_script