New player model with anims!!!!!!

This commit is contained in:
Twirpytherobot
2025-11-16 18:42:06 +00:00
parent 9d5feb6f0e
commit 3431c12dc2
10 changed files with 301 additions and 49 deletions
+4 -2
View File
@@ -48,8 +48,10 @@ func perform_attack() -> bool:
if owner_character._body:
owner_character._body.play_attack()
# Find targets in range
_find_and_damage_targets()
# Delay damage until animation hits (roughly 70% through the animation)
# This makes the damage apply when the swing actually connects
var damage_delay = weapon_data.attack_cooldown * 0.4 # Adjust this multiplier to change when damage happens
get_tree().create_timer(damage_delay).timeout.connect(_find_and_damage_targets)
attack_performed.emit()
return true
+94 -9
View File
@@ -11,6 +11,8 @@ extends Node3D
# Weapon spawning counter (server-side only)
var _weapon_spawn_counter: int = 0
# Track active weapons for late-join sync (server-side only)
var _active_weapons: Dictionary = {} # weapon_id -> WorldWeapon reference
# multiplayer chat
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message
@@ -53,8 +55,11 @@ func _spawn_initial_weapons():
# Wait a frame for everything to be ready
await get_tree().process_frame
print("[Server] _spawn_initial_weapons - Connected peers: ", multiplayer.get_peers())
# Spawn a sword
_weapon_spawn_counter += 1
print("[Server] Calling RPC to spawn sword with ID: ", _weapon_spawn_counter)
rpc("spawn_world_weapon",
"res://level/resources/weapon_sword.tres",
Vector3(5, 1, 0),
@@ -64,6 +69,7 @@ func _spawn_initial_weapons():
# Spawn a shield
_weapon_spawn_counter += 1
print("[Server] Calling RPC to spawn shield with ID: ", _weapon_spawn_counter)
rpc("spawn_world_weapon",
"res://level/resources/weapon_shield.tres",
Vector3(-5, 1, 0),
@@ -72,13 +78,24 @@ func _spawn_initial_weapons():
)
func _on_player_connected(peer_id, player_info):
# for id in Network.players.keys():
# var player_data = Network.players[id]
# if id != peer_id:
# rpc_id(peer_id, "sync_player_skin", id, player_data["skin"])
_add_player(peer_id, player_info)
# Sync existing weapons to the newly joined player (but not to server itself)
if multiplayer.is_server() and peer_id != 1:
print("[Server] Syncing weapons to newly connected peer: ", peer_id)
print("[Server] Active weapons count: ", _active_weapons.size())
for weapon_id in _active_weapons.keys():
var weapon = _active_weapons[weapon_id]
if is_instance_valid(weapon) and weapon.weapon_data:
print("[Server] Sending weapon ", weapon_id, " to peer ", peer_id)
# Send current position and zero velocity for syncing
rpc_id(peer_id, "_client_spawn_weapon",
weapon.weapon_data.resource_path,
weapon.global_position,
Vector3.ZERO,
weapon_id
)
func _on_host_pressed():
menu.hide()
Network.start_host(nick_input.text.strip_edges(), skin_input.text.strip_edges().to_lower())
@@ -206,8 +223,10 @@ func _on_jemz_preset():
# ---------- WEAPON SPAWNING ----------
## Spawn a weapon in the world (called from server, syncs to all clients)
@rpc("authority", "call_local", "reliable")
@rpc("any_peer", "call_local", "reliable")
func spawn_world_weapon(weapon_data_path: String, spawn_position: Vector3, initial_velocity: Vector3, weapon_id: int):
print("[Client ", multiplayer.get_unique_id(), "] spawn_world_weapon called for weapon_id: ", weapon_id)
if not weapons_container:
push_error("WeaponsContainer not found in level!")
return
@@ -221,15 +240,81 @@ func spawn_world_weapon(weapon_data_path: String, spawn_position: Vector3, initi
# Create WorldWeapon instance
var world_weapon = WorldWeapon.new()
world_weapon.weapon_data = weapon_data
print("[DEBUG] About to set weapon_id. Parameter weapon_id = ", weapon_id)
world_weapon.weapon_id = weapon_id # Store the ID
print("[DEBUG] After setting weapon_id. world_weapon.weapon_id = ", world_weapon.weapon_id)
world_weapon.name = "WorldWeapon_" + str(weapon_id) # Deterministic name
print("[DEBUG] Set weapon name to: ", world_weapon.name, " using weapon_id: ", weapon_id)
world_weapon.position = spawn_position
# Add to weapons container
weapons_container.add_child(world_weapon, true)
# Remove existing weapon with same name if it exists (prevents duplicates)
var weapon_path = NodePath(world_weapon.name)
if weapons_container.has_node(weapon_path):
print("[DEBUG] Weapon ", world_weapon.name, " already exists, removing old one first")
var old_weapon = weapons_container.get_node(weapon_path)
weapons_container.remove_child(old_weapon)
old_weapon.queue_free()
print("Spawned weapon: ", world_weapon.name, " at ", spawn_position)
# Add to weapons container
weapons_container.add_child(world_weapon)
print("[DEBUG] After add_child, weapon name in tree: ", world_weapon.name, " weapon_id: ", world_weapon.weapon_id)
# Track this weapon on the server (AFTER adding to tree so signals work)
if multiplayer.is_server():
_active_weapons[weapon_id] = world_weapon
print("[Server] Added weapon ", weapon_id, " to _active_weapons. Total: ", _active_weapons.size())
# Connect to the weapon's removal signal
world_weapon.tree_exiting.connect(_on_weapon_removed.bind(weapon_id))
print("[Peer ", multiplayer.get_unique_id(), "] Spawned weapon: ", world_weapon.name, " at ", spawn_position)
# Apply velocity after physics is ready
if initial_velocity != Vector3.ZERO:
await get_tree().process_frame
world_weapon.linear_velocity = initial_velocity
## Remove a world weapon from all clients (called by WorldWeapon when picked up)
func remove_world_weapon(weapon_id: int):
print("[Server] remove_world_weapon called for weapon_id: ", weapon_id)
if not multiplayer.is_server():
print("[ERROR] remove_world_weapon called on client!")
return
# Broadcast removal to all clients
print("[Server] Broadcasting removal RPC to all clients")
rpc("_remove_weapon_on_clients", weapon_id)
## RPC to remove weapon on all clients
@rpc("any_peer", "call_local", "reliable")
func _remove_weapon_on_clients(weapon_id: int):
print("[Peer ", multiplayer.get_unique_id(), "] _remove_weapon_on_clients called for weapon_id: ", weapon_id)
var weapon_name = "WorldWeapon_" + str(weapon_id)
if weapons_container and weapons_container.has_node(weapon_name):
var weapon = weapons_container.get_node(weapon_name)
print("[Peer ", multiplayer.get_unique_id(), "] Removing weapon ", weapon_name)
weapon.queue_free()
else:
print("[Peer ", multiplayer.get_unique_id(), "] WARNING: Weapon ", weapon_name, " not found in WeaponsContainer")
## Called when a weapon is removed (picked up or destroyed)
func _on_weapon_removed(weapon_id: int):
if not multiplayer.is_server():
return
# Remove from tracking
if _active_weapons.has(weapon_id):
_active_weapons.erase(weapon_id)
print("Removed weapon ", weapon_id, " from active tracking")
## Client-only spawn (called via rpc_id for late-join sync)
@rpc("any_peer", "reliable")
func _client_spawn_weapon(weapon_data_path: String, spawn_position: Vector3, initial_velocity: Vector3, weapon_id: int):
print("[Client ", multiplayer.get_unique_id(), "] _client_spawn_weapon received for weapon_id: ", weapon_id)
# This only runs on clients, not server (no call_local)
if multiplayer.is_server():
print("[ERROR] _client_spawn_weapon called on server!")
return
# Call the regular spawn function to create the weapon
print("[Client ", multiplayer.get_unique_id(), "] Calling spawn_world_weapon locally")
spawn_world_weapon(weapon_data_path, spawn_position, initial_velocity, weapon_id)
+62
View File
@@ -0,0 +1,62 @@
extends Node3D
class_name LilguyBody
const LERP_VELOCITY: float = 0.15
@export_category("Objects")
@export var _character: CharacterBody3D = null
@export var animation_player: AnimationPlayer = null
func apply_rotation(_velocity: Vector3) -> void:
var new_rotation_y = lerp_angle(rotation.y, atan2(_velocity.x, _velocity.z), LERP_VELOCITY)
rotation.y = new_rotation_y
func animate(_velocity: Vector3) -> void:
if not animation_player:
return
# Don't override attack animation if it's playing
if animation_player.is_playing() and animation_player.current_animation == "Attack_OneHand":
return
# Check if we're dashing
if _character._is_dashing:
if animation_player.current_animation != "Jump":
_play_animation("Jump")
return
if not _character.is_on_floor():
if _velocity.y < 0:
# Falling - use FallIdle animation
_play_animation("FallIdle")
else:
_play_animation("Jump")
return
if _velocity:
if _character.is_running() and _character.is_on_floor():
# Sprint animation = Run for Lilguy
_play_animation("Run")
return
# Walk animation for normal movement
_play_animation("Walk")
return
# Idle - use FallIdle or Idle if it exists
if animation_player.has_animation("Idle"):
_play_animation("Idle")
else:
_play_animation("FallIdle")
func _play_animation(anim_name: String):
if animation_player and animation_player.has_animation(anim_name):
animation_player.play(anim_name)
# Silently ignore if animation doesn't exist
func play_attack() -> void:
if animation_player:
# Play attack animation once (don't loop)
animation_player.play("Attack_OneHand", -1, 1.0)
# Ensure it doesn't loop
animation_player.animation_set_next("Attack_OneHand", "")
+1
View File
@@ -0,0 +1 @@
uid://cf7jky1bcs560
+76 -12
View File
@@ -24,10 +24,10 @@ enum SkinColor { BLUE, YELLOW, GREEN, RED }
@export var green_texture : CompressedTexture2D
@export var red_texture : CompressedTexture2D
@onready var _bottom_mesh: MeshInstance3D = get_node("3DGodotRobot/RobotArmature/Skeleton3D/Bottom")
@onready var _chest_mesh: MeshInstance3D = get_node("3DGodotRobot/RobotArmature/Skeleton3D/Chest")
@onready var _face_mesh: MeshInstance3D = get_node("3DGodotRobot/RobotArmature/Skeleton3D/Face")
@onready var _limbs_head_mesh: MeshInstance3D = get_node("3DGodotRobot/RobotArmature/Skeleton3D/Llimbs and head")
@onready var _bottom_mesh: MeshInstance3D = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/Bottom")
@onready var _chest_mesh: MeshInstance3D = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/Chest")
@onready var _face_mesh: MeshInstance3D = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/Face")
@onready var _limbs_head_mesh: MeshInstance3D = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/Llimbs and head")
var _current_speed: float
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
@@ -61,6 +61,65 @@ func _ready():
super._ready()
set_respawn_point(Vector3(0, 5, 0))
# Auto-find body node (needed for instanced scenes where @export NodePath doesn't work reliably)
if _body == null:
for child in get_children():
if child.name == "Armature" or child.name == "3DGodotRobot":
_body = child
print("Auto-found _body: ", child.name)
break
if _body == null:
push_error("Could not find body node (Armature or 3DGodotRobot)!")
# Auto-find spring arm offset
if _spring_arm_offset == null:
if has_node("SpringArmOffset"):
_spring_arm_offset = get_node("SpringArmOffset")
print("Auto-found _spring_arm_offset")
else:
push_error("Could not find SpringArmOffset!")
# Auto-find weapon attachments if not set
if _weapon_attachment == null:
if has_node("Armature/Skeleton3D/WeaponPoint"):
_weapon_attachment = get_node("Armature/Skeleton3D/WeaponPoint")
print("Auto-found _weapon_attachment")
elif has_node("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D"):
_weapon_attachment = get_node("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D")
print("Auto-found _weapon_attachment (robot)")
else:
print("WARNING: WeaponPoint not found! Check if you've added BoneAttachment3D nodes.")
if has_node("Armature/Skeleton3D"):
print("Skeleton3D children:")
var skeleton = get_node("Armature/Skeleton3D")
for child in skeleton.get_children():
print(" - ", child.name, " (", child.get_class(), ")")
# Auto-find weapon container
if _weapon_container == null and _weapon_attachment:
var container = _weapon_attachment.get_node_or_null("WeaponContainer")
if container:
_weapon_container = container
print("Auto-found _weapon_container")
# Auto-find offhand attachment
if _offhand_attachment == null:
if has_node("Armature/Skeleton3D/OffhandPoint"):
_offhand_attachment = get_node("Armature/Skeleton3D/OffhandPoint")
print("Auto-found _offhand_attachment")
elif has_node("3DGodotRobot/RobotArmature/Skeleton3D/OffHandPoint"):
_offhand_attachment = get_node("3DGodotRobot/RobotArmature/Skeleton3D/OffHandPoint")
print("Auto-found _offhand_attachment (robot)")
else:
print("WARNING: OffhandPoint not found!")
# Auto-find offhand container
if _offhand_container == null and _offhand_attachment:
var container = _offhand_attachment.get_node_or_null("OffhandContainer")
if container:
_offhand_container = container
print("Auto-found _offhand_container")
# Try to get optional health label
if has_node("PlayerNick/HealthLabel"):
health_label = get_node("PlayerNick/HealthLabel")
@@ -539,16 +598,19 @@ func _flash_hurt():
if not _body:
return
# Store original modulate
var original_modulate = _body.modulate
# Only works if _body has a modulate property (CanvasItem or some Node3D with visual children)
if "modulate" in _body:
# Store original modulate
var original_modulate = _body.modulate
# Flash red
_body.modulate = Color(1.5, 0.5, 0.5, 1.0)
# Flash red
_body.modulate = Color(1.5, 0.5, 0.5, 1.0)
# Return to normal after a brief moment
await get_tree().create_timer(0.15).timeout
if _body:
_body.modulate = original_modulate
# Return to normal after a brief moment
await get_tree().create_timer(0.15).timeout
if _body:
_body.modulate = original_modulate
# If no modulate, just skip the visual effect
## Weapon System
func _setup_weapon_pickup_area():
@@ -589,9 +651,11 @@ func _on_weapon_area_exited(area: Area3D):
## Equip a weapon from WorldWeapon data (receives resource path)
@rpc("any_peer", "call_local", "reliable")
func equip_weapon_from_world(weapon_data_path: String):
print("[Client ", multiplayer.get_unique_id(), "] equip_weapon_from_world called for: ", weapon_data_path)
var data = load(weapon_data_path) as WeaponData
if data:
equip_weapon(data)
print("[Client ", multiplayer.get_unique_id(), "] Equipped weapon: ", data.weapon_name)
else:
push_error("Failed to load weapon data from: " + weapon_data_path)
+18 -2
View File
@@ -7,6 +7,7 @@ class_name WorldWeapon
@export var weapon_data: WeaponData
var weapon_id: int = -1 # Set by Level when spawned
var _mesh_instance: Node3D = null
var _collision_shape: CollisionShape3D = null
var _pickup_area: Area3D = null
@@ -119,15 +120,30 @@ func try_pickup(player_id: int):
push_error("WeaponData has no resource path!")
return
# Check weapon_id is valid
if weapon_id == -1:
push_error("WorldWeapon.try_pickup: weapon_id is -1! This weapon was not spawned correctly.")
print(" Weapon name: ", name)
print(" Weapon data: ", weapon_data.resource_path if weapon_data else "null")
return
# Tell the player to equip this weapon (on all clients)
print("[Server] Telling player ", player_id, " to equip weapon via RPC")
player.rpc("equip_weapon_from_world", resource_path)
# Remove this world weapon from all clients
rpc("_remove_from_all_clients")
# Remove this world weapon from all clients using level's centralized system
print("[Server] Removing world weapon ", name, " (ID: ", weapon_id, ") via level.remove_world_weapon")
if level.has_method("remove_world_weapon"):
level.remove_world_weapon(weapon_id)
else:
push_error("Level doesn't have remove_world_weapon method!")
## Remove weapon from all clients
@rpc("any_peer", "call_local", "reliable")
func _remove_from_all_clients():
print("[Client ", multiplayer.get_unique_id(), "] Removing weapon ", name)
# Delay removal to ensure RPC is fully processed
await get_tree().process_frame
queue_free()
## Set weapon data and refresh