|
|
|
|
extends BaseUnit
|
|
|
|
|
class_name Character
|
|
|
|
|
|
|
|
|
|
const NORMAL_SPEED = 6.0
|
|
|
|
|
const SPRINT_SPEED = 10.0
|
|
|
|
|
const JUMP_VELOCITY = 10
|
|
|
|
|
|
|
|
|
|
enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
|
|
|
|
|
|
|
|
|
@onready var nickname: Label3D = $PlayerNick/Nickname
|
|
|
|
|
@onready var health_label: Label3D = null # Optional 3D health label
|
|
|
|
|
|
|
|
|
|
@export_category("Objects")
|
|
|
|
|
@export var _body: Node3D = null
|
|
|
|
|
@export var _spring_arm_offset: Node3D = null
|
|
|
|
|
|
|
|
|
|
@export_category("Skin Colors")
|
|
|
|
|
@export var blue_texture : CompressedTexture2D
|
|
|
|
|
@export var yellow_texture : CompressedTexture2D
|
|
|
|
|
@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")
|
|
|
|
|
|
|
|
|
|
var _current_speed: float
|
|
|
|
|
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
|
|
|
|
|
|
|
|
|
|
# Attack system
|
|
|
|
|
@export var attack_damage: float = 10.0
|
|
|
|
|
@export var attack_range: float = 3.0
|
|
|
|
|
@export var attack_cooldown: float = 0.5
|
|
|
|
|
var _attack_timer: float = 0.0
|
|
|
|
|
|
|
|
|
|
func _enter_tree():
|
|
|
|
|
super._enter_tree()
|
|
|
|
|
$SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
|
|
|
|
|
|
|
|
|
|
func _ready():
|
|
|
|
|
super._ready()
|
|
|
|
|
set_respawn_point(Vector3(0, 5, 0))
|
|
|
|
|
|
|
|
|
|
# Try to get optional health label
|
|
|
|
|
if has_node("PlayerNick/HealthLabel"):
|
|
|
|
|
health_label = get_node("PlayerNick/HealthLabel")
|
|
|
|
|
|
|
|
|
|
# Connect health signals
|
|
|
|
|
health_changed.connect(_on_health_changed)
|
|
|
|
|
died.connect(_on_died)
|
|
|
|
|
respawned.connect(_on_respawned)
|
|
|
|
|
|
|
|
|
|
# Update health display
|
|
|
|
|
_update_health_display()
|
|
|
|
|
|
|
|
|
|
# Create 2D UI health bar for local player
|
|
|
|
|
if is_multiplayer_authority():
|
|
|
|
|
_create_health_ui()
|
|
|
|
|
|
|
|
|
|
func _physics_process(delta):
|
|
|
|
|
# Check if multiplayer is ready
|
|
|
|
|
if multiplayer.multiplayer_peer == null:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not is_multiplayer_authority():
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var current_scene = get_tree().get_current_scene()
|
|
|
|
|
if current_scene and current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible() and is_on_floor():
|
|
|
|
|
freeze()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not is_on_floor():
|
|
|
|
|
velocity.y -= gravity * delta
|
|
|
|
|
_body.animate(velocity)
|
|
|
|
|
|
|
|
|
|
if is_on_floor():
|
|
|
|
|
if Input.is_action_just_pressed("jump"):
|
|
|
|
|
velocity.y = JUMP_VELOCITY
|
|
|
|
|
else:
|
|
|
|
|
velocity.y -= gravity * delta
|
|
|
|
|
|
|
|
|
|
_move()
|
|
|
|
|
move_and_slide()
|
|
|
|
|
_body.animate(velocity)
|
|
|
|
|
|
|
|
|
|
func _process(delta):
|
|
|
|
|
# Check if multiplayer is ready
|
|
|
|
|
if multiplayer.multiplayer_peer == null:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if not is_multiplayer_authority():
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_check_fall_and_respawn()
|
|
|
|
|
|
|
|
|
|
# Update attack cooldown
|
|
|
|
|
if _attack_timer > 0:
|
|
|
|
|
_attack_timer -= delta
|
|
|
|
|
|
|
|
|
|
# Handle attack input
|
|
|
|
|
if Input.is_action_just_pressed("attack") and _attack_timer <= 0 and not is_dead:
|
|
|
|
|
_perform_attack()
|
|
|
|
|
|
|
|
|
|
func freeze():
|
|
|
|
|
velocity.x = 0
|
|
|
|
|
velocity.z = 0
|
|
|
|
|
_current_speed = 0
|
|
|
|
|
_body.animate(Vector3.ZERO)
|
|
|
|
|
|
|
|
|
|
func _move() -> void:
|
|
|
|
|
var _input_direction: Vector2 = Vector2.ZERO
|
|
|
|
|
if is_multiplayer_authority():
|
|
|
|
|
_input_direction = Input.get_vector(
|
|
|
|
|
"move_left", "move_right",
|
|
|
|
|
"move_forward", "move_backward"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var _direction: Vector3 = transform.basis * Vector3(_input_direction.x, 0, _input_direction.y).normalized()
|
|
|
|
|
|
|
|
|
|
is_running()
|
|
|
|
|
_direction = _direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y)
|
|
|
|
|
|
|
|
|
|
if _direction:
|
|
|
|
|
velocity.x = _direction.x * _current_speed
|
|
|
|
|
velocity.z = _direction.z * _current_speed
|
|
|
|
|
_body.apply_rotation(velocity)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
velocity.x = move_toward(velocity.x, 0, _current_speed)
|
|
|
|
|
velocity.z = move_toward(velocity.z, 0, _current_speed)
|
|
|
|
|
|
|
|
|
|
func is_running() -> bool:
|
|
|
|
|
if Input.is_action_pressed("shift"):
|
|
|
|
|
_current_speed = SPRINT_SPEED
|
|
|
|
|
return true
|
|
|
|
|
else:
|
|
|
|
|
_current_speed = NORMAL_SPEED
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
func _check_fall_and_respawn():
|
|
|
|
|
if global_transform.origin.y < -15.0 and multiplayer.is_server():
|
|
|
|
|
# Use BaseUnit's respawn system
|
|
|
|
|
_respawn()
|
|
|
|
|
|
|
|
|
|
@rpc("any_peer", "reliable")
|
|
|
|
|
func change_nick(new_nick: String):
|
|
|
|
|
if nickname:
|
|
|
|
|
nickname.text = new_nick
|
|
|
|
|
|
|
|
|
|
func get_texture_from_name(skin_color: SkinColor) -> CompressedTexture2D:
|
|
|
|
|
match skin_color:
|
|
|
|
|
SkinColor.BLUE: return blue_texture
|
|
|
|
|
SkinColor.GREEN: return green_texture
|
|
|
|
|
SkinColor.RED: return red_texture
|
|
|
|
|
SkinColor.YELLOW: return yellow_texture
|
|
|
|
|
_: return blue_texture
|
|
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
|
|
|
|
func set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2D) -> void:
|
|
|
|
|
if mesh_instance:
|
|
|
|
|
var material := mesh_instance.get_surface_override_material(0)
|
|
|
|
|
if material and material is StandardMaterial3D:
|
|
|
|
|
var new_material := material
|
|
|
|
|
new_material.albedo_texture = texture
|
|
|
|
|
mesh_instance.set_surface_override_material(0, new_material)
|
|
|
|
|
|
|
|
|
|
## Attack system
|
|
|
|
|
func _perform_attack():
|
|
|
|
|
if not is_multiplayer_authority() or is_dead:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Don't attack if already attacking
|
|
|
|
|
if _body and _body.animation_player and _body.animation_player.current_animation == "Attack1":
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_attack_timer = attack_cooldown
|
|
|
|
|
|
|
|
|
|
# Play attack animation once
|
|
|
|
|
if _body:
|
|
|
|
|
_body.play_attack()
|
|
|
|
|
|
|
|
|
|
# Find nearest enemy in range
|
|
|
|
|
var space_state = get_world_3d().direct_space_state
|
|
|
|
|
var query = PhysicsShapeQueryParameters3D.new()
|
|
|
|
|
var sphere = SphereShape3D.new()
|
|
|
|
|
sphere.radius = attack_range
|
|
|
|
|
query.shape = sphere
|
|
|
|
|
query.transform = global_transform
|
|
|
|
|
query.collision_mask = 1 # Player layer
|
|
|
|
|
|
|
|
|
|
var results = space_state.intersect_shape(query)
|
|
|
|
|
|
|
|
|
|
for result in results:
|
|
|
|
|
var hit_body = result["collider"]
|
|
|
|
|
if hit_body != self and hit_body is BaseUnit:
|
|
|
|
|
var attacker_id = multiplayer.get_unique_id()
|
|
|
|
|
|
|
|
|
|
# If we're the server, apply damage directly
|
|
|
|
|
if multiplayer.is_server():
|
|
|
|
|
_server_apply_damage(hit_body.name, attack_damage, attacker_id)
|
|
|
|
|
else:
|
|
|
|
|
# Otherwise, request server to apply damage
|
|
|
|
|
rpc_id(1, "_server_apply_damage", hit_body.name, attack_damage, attacker_id)
|
|
|
|
|
break # Only hit one target per attack
|
|
|
|
|
|
|
|
|
|
## Server-side damage application
|
|
|
|
|
@rpc("any_peer", "reliable")
|
|
|
|
|
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"):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var players_container = level.get_node("PlayersContainer")
|
|
|
|
|
if not players_container.has_node(target_name):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var target = players_container.get_node(target_name)
|
|
|
|
|
if target and target is BaseUnit:
|
|
|
|
|
target.take_damage(damage, attacker_id)
|
|
|
|
|
|
|
|
|
|
## Health display and callbacks
|
|
|
|
|
func _create_health_ui():
|
|
|
|
|
# Create a 2D UI for the local player's health
|
|
|
|
|
var canvas = CanvasLayer.new()
|
|
|
|
|
canvas.name = "HealthUI"
|
|
|
|
|
add_child(canvas)
|
|
|
|
|
|
|
|
|
|
# Health bar background
|
|
|
|
|
var health_bg = ColorRect.new()
|
|
|
|
|
health_bg.name = "HealthBG"
|
|
|
|
|
health_bg.color = Color(0.2, 0.2, 0.2, 0.8)
|
|
|
|
|
health_bg.position = Vector2(20, 20)
|
|
|
|
|
health_bg.size = Vector2(200, 30)
|
|
|
|
|
canvas.add_child(health_bg)
|
|
|
|
|
|
|
|
|
|
# Health bar (current health)
|
|
|
|
|
var health_bar = ColorRect.new()
|
|
|
|
|
health_bar.name = "HealthBar"
|
|
|
|
|
health_bar.color = Color(0.0, 0.8, 0.0, 1.0) # Green
|
|
|
|
|
health_bar.position = Vector2(22, 22)
|
|
|
|
|
health_bar.size = Vector2(196, 26)
|
|
|
|
|
canvas.add_child(health_bar)
|
|
|
|
|
|
|
|
|
|
# Health text
|
|
|
|
|
var health_text = Label.new()
|
|
|
|
|
health_text.name = "HealthText"
|
|
|
|
|
health_text.position = Vector2(20, 20)
|
|
|
|
|
health_text.size = Vector2(200, 30)
|
|
|
|
|
health_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
|
|
|
health_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
|
|
|
health_text.add_theme_color_override("font_color", Color.WHITE)
|
|
|
|
|
health_text.add_theme_color_override("font_outline_color", Color.BLACK)
|
|
|
|
|
health_text.add_theme_constant_override("outline_size", 2)
|
|
|
|
|
health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
|
|
|
|
canvas.add_child(health_text)
|
|
|
|
|
|
|
|
|
|
func _update_health_display():
|
|
|
|
|
# Update 3D label if it exists
|
|
|
|
|
if health_label:
|
|
|
|
|
health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
|
|
|
|
|
|
|
|
|
# Update 2D UI for local player
|
|
|
|
|
if is_multiplayer_authority() and has_node("HealthUI"):
|
|
|
|
|
var health_bar = get_node_or_null("HealthUI/HealthBar")
|
|
|
|
|
var health_text = get_node_or_null("HealthUI/HealthText")
|
|
|
|
|
|
|
|
|
|
if health_bar:
|
|
|
|
|
var health_percent = get_health_percent()
|
|
|
|
|
health_bar.size.x = 196 * health_percent
|
|
|
|
|
|
|
|
|
|
# Change color based on health
|
|
|
|
|
if health_percent > 0.6:
|
|
|
|
|
health_bar.color = Color(0.0, 0.8, 0.0) # Green
|
|
|
|
|
elif health_percent > 0.3:
|
|
|
|
|
health_bar.color = Color(1.0, 0.8, 0.0) # Yellow
|
|
|
|
|
else:
|
|
|
|
|
health_bar.color = Color(0.8, 0.0, 0.0) # Red
|
|
|
|
|
|
|
|
|
|
if health_text:
|
|
|
|
|
health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)]
|
|
|
|
|
|
|
|
|
|
func _on_health_changed(_old_health: float, _new_health: float):
|
|
|
|
|
_update_health_display()
|
|
|
|
|
|
|
|
|
|
func _on_died(killer_id: int):
|
|
|
|
|
# Disable player when dead
|
|
|
|
|
set_physics_process(false)
|
|
|
|
|
set_process(false)
|
|
|
|
|
|
|
|
|
|
# Visual feedback - could add death animation here
|
|
|
|
|
if _body:
|
|
|
|
|
_body.visible = false
|
|
|
|
|
|
|
|
|
|
# Print death message
|
|
|
|
|
var killer_name = "Unknown"
|
|
|
|
|
if Network.players.has(killer_id):
|
|
|
|
|
killer_name = Network.players[killer_id]["nick"]
|
|
|
|
|
|
|
|
|
|
if is_multiplayer_authority():
|
|
|
|
|
print("You were killed by ", killer_name)
|
|
|
|
|
|
|
|
|
|
# Show death message on UI
|
|
|
|
|
if has_node("HealthUI/HealthText"):
|
|
|
|
|
get_node("HealthUI/HealthText").text = "DEAD - Respawning..."
|
|
|
|
|
|
|
|
|
|
func _on_respawned():
|
|
|
|
|
# Re-enable player
|
|
|
|
|
set_physics_process(true)
|
|
|
|
|
set_process(true)
|
|
|
|
|
|
|
|
|
|
if _body:
|
|
|
|
|
_body.visible = true
|
|
|
|
|
|
|
|
|
|
_update_health_display()
|
|
|
|
|
|
|
|
|
|
if is_multiplayer_authority():
|
|
|
|
|
print("You respawned!")
|