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 # Dash system @export var dash_speed_multiplier: float = 2.0 @export var dash_duration: float = 0.25 @export var dash_cooldown: float = 4.0 var _dash_timer: float = 0.0 var _dash_cooldown_timer: float = 0.0 var _is_dashing: bool = false var _dash_direction: Vector3 = Vector3.ZERO 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 # Update dash timers if _dash_timer > 0: _dash_timer -= delta if _dash_timer <= 0: _is_dashing = false if _dash_cooldown_timer > 0: _dash_cooldown_timer -= delta # Handle dash input if Input.is_action_just_pressed("dash") and _dash_cooldown_timer <= 0 and not is_dead and is_on_floor(): _perform_dash() # Handle attack input if Input.is_action_just_pressed("attack") and _attack_timer <= 0 and not is_dead and not _is_dashing: _perform_attack() func freeze(): velocity.x = 0 velocity.z = 0 _current_speed = 0 _body.animate(Vector3.ZERO) func _move() -> void: # If dashing, use dash movement if _is_dashing: velocity.x = _dash_direction.x * _current_speed * dash_speed_multiplier velocity.z = _dash_direction.z * _current_speed * dash_speed_multiplier _body.apply_rotation(velocity) return 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!") ## Dash system func _perform_dash(): if not is_multiplayer_authority() or is_dead or not is_on_floor(): return # Get current movement direction or use forward if no input var _input_direction: Vector2 = Input.get_vector( "move_left", "move_right", "move_forward", "move_backward" ) var _direction: Vector3 if _input_direction.length() > 0: # Dash in input direction _direction = transform.basis * Vector3(_input_direction.x, 0, _input_direction.y).normalized() _direction = _direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y) else: # Dash forward if no input _direction = -transform.basis.z _direction = _direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y) # Set dash parameters _dash_direction = _direction _is_dashing = true _dash_timer = dash_duration _dash_cooldown_timer = dash_cooldown # Rotate character slightly forward during dash if _body: _body.rotation.x = -0.3 # Tilt forward slightly # Animation is handled by the Body's animate function # Reset rotation after dash var tween = create_tween() tween.tween_method(_reset_dash_rotation, -0.3, 0.0, dash_duration) func _reset_dash_rotation(rotation_value: float): if _body: _body.rotation.x = rotation_value