You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
794 lines
25 KiB
794 lines
25 KiB
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 var _weapon_attachment: BoneAttachment3D = null # WeaponPoint bone attachment (main hand) |
|
@export var _weapon_container: Node3D = null # Container node for main hand weapons |
|
@export var _offhand_attachment: BoneAttachment3D = null # OffHandPoint bone attachment |
|
@export var _offhand_container: Node3D = null # Container node for off-hand weapons |
|
|
|
@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_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") |
|
|
|
# Weapon system |
|
var equipped_weapon: BaseWeapon = null # Main hand weapon |
|
var equipped_offhand: BaseWeapon = null # Off-hand weapon |
|
var _nearby_weapons: Array[WorldWeapon] = [] |
|
var is_blocking: bool = false |
|
|
|
# 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)) |
|
|
|
# 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") |
|
|
|
# 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() |
|
|
|
# Setup weapon pickup detection area |
|
_setup_weapon_pickup_area() |
|
|
|
# Auto-find weapon attachment if not set |
|
if _weapon_attachment == null: |
|
var bone_attach = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D") |
|
if bone_attach: |
|
_weapon_attachment = bone_attach |
|
print("Auto-found weapon attachment point") |
|
else: |
|
push_warning("Could not find BoneAttachment3D for weapons!") |
|
|
|
# Auto-find weapon container if not set |
|
if _weapon_container == null and _weapon_attachment: |
|
var container = _weapon_attachment.get_node_or_null("WeaponContainer") |
|
if container: |
|
_weapon_container = container |
|
print("Auto-found main hand weapon container") |
|
else: |
|
push_warning("Could not find WeaponContainer! Weapons will attach directly to BoneAttachment3D.") |
|
|
|
# Auto-find off-hand attachment if not set |
|
if _offhand_attachment == null: |
|
var offhand_attach = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/OffHandPoint") |
|
if offhand_attach: |
|
_offhand_attachment = offhand_attach |
|
print("Auto-found off-hand attachment point") |
|
|
|
# Auto-find off-hand container if not set |
|
if _offhand_container == null and _offhand_attachment: |
|
var container = _offhand_attachment.get_node_or_null("OffHandContainer") |
|
if container: |
|
_offhand_container = container |
|
print("Auto-found off-hand weapon container") |
|
|
|
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 block input |
|
if Input.is_action_pressed("block") and not is_dead: |
|
_try_block() |
|
else: |
|
is_blocking = false |
|
|
|
# Handle attack input |
|
if Input.is_action_just_pressed("attack") and not is_dead and not _is_dashing and not is_blocking: |
|
_perform_attack() |
|
|
|
# Handle weapon pickup/drop |
|
if Input.is_action_just_pressed("pickup") and not is_dead: |
|
print("Pickup pressed! Main: ", equipped_weapon != null, " OffHand: ", equipped_offhand != null, " Nearby: ", _nearby_weapons.size()) |
|
|
|
# Try to pickup nearby weapon |
|
if _nearby_weapons.size() > 0: |
|
_pickup_nearest_weapon() |
|
# If no nearby weapons, drop what we're holding (main hand first, then off-hand) |
|
elif equipped_weapon: |
|
# Tell server to drop (server will handle spawning) |
|
if multiplayer.is_server(): |
|
drop_weapon(false) # main hand |
|
else: |
|
rpc_id(1, "drop_weapon", false) # Only send to server |
|
elif equipped_offhand: |
|
# Drop off-hand |
|
if multiplayer.is_server(): |
|
drop_weapon(true) # off-hand |
|
else: |
|
rpc_id(1, "drop_weapon", true) |
|
else: |
|
print("No weapons nearby to pick up") |
|
|
|
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) |
|
|
|
## Block system |
|
func _try_block(): |
|
# Check if we have a weapon that can block |
|
var can_block_weapon = false |
|
|
|
if equipped_weapon and equipped_weapon.can_block(): |
|
can_block_weapon = true |
|
elif equipped_offhand and equipped_offhand.can_block(): |
|
can_block_weapon = true |
|
|
|
is_blocking = can_block_weapon |
|
|
|
# Could add blocking animation here |
|
# if is_blocking and _body: |
|
# _body.play_block_animation() |
|
|
|
## Get block reduction amount (0.0 to 1.0) |
|
func get_block_reduction() -> float: |
|
if not is_blocking: |
|
return 0.0 |
|
|
|
var reduction = 0.0 |
|
|
|
# Use the highest block reduction from equipped weapons |
|
if equipped_weapon and equipped_weapon.can_block(): |
|
reduction = max(reduction, equipped_weapon.get_block_reduction()) |
|
|
|
if equipped_offhand and equipped_offhand.can_block(): |
|
reduction = max(reduction, equipped_offhand.get_block_reduction()) |
|
|
|
return reduction |
|
|
|
## Attack system |
|
func _perform_attack(): |
|
if not is_multiplayer_authority() or is_dead: |
|
return |
|
|
|
# Use main hand weapon if available |
|
if equipped_weapon and equipped_weapon.can_attack(): |
|
equipped_weapon.perform_attack() |
|
return |
|
|
|
# Or use off-hand weapon if available |
|
if equipped_offhand and equipped_offhand.can_attack(): |
|
equipped_offhand.perform_attack() |
|
return |
|
|
|
# Fallback to default unarmed attack |
|
# Don't attack if already attacking |
|
if _body and _body.animation_player and _body.animation_player.current_animation == "Attack1": |
|
return |
|
|
|
if _attack_timer > 0: |
|
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 (default unarmed knockback) |
|
if multiplayer.is_server(): |
|
_server_apply_damage(hit_body.name, attack_damage, attacker_id, 5.0, global_position) |
|
else: |
|
# Otherwise, request server to apply damage |
|
rpc_id(1, "_server_apply_damage", hit_body.name, attack_damage, attacker_id, 5.0, global_position) |
|
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, knockback: float = 0.0, attacker_pos: Vector3 = Vector3.ZERO): |
|
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, knockback, attacker_pos) |
|
|
|
## 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 |
|
|
|
# Animation is handled by the Body's animate function (Jump animation plays during dash) |
|
|
|
## Override hurt animation from BaseUnit |
|
func _play_hurt_animation(): |
|
if _body and _body.animation_player: |
|
# Try to play a hurt/hit animation if it exists |
|
if _body.animation_player.has_animation("Hurt"): |
|
_body.animation_player.play("Hurt") |
|
elif _body.animation_player.has_animation("Hit"): |
|
_body.animation_player.play("Hit") |
|
elif _body.animation_player.has_animation("TakeDamage"): |
|
_body.animation_player.play("TakeDamage") |
|
else: |
|
# Fallback: briefly flash the character red |
|
_flash_hurt() |
|
|
|
## Flash effect when no hurt animation exists |
|
func _flash_hurt(): |
|
if not _body: |
|
return |
|
|
|
# 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) |
|
|
|
# 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(): |
|
# Create an Area3D to detect nearby weapons |
|
var pickup_area = Area3D.new() |
|
pickup_area.name = "WeaponPickupArea" |
|
pickup_area.collision_layer = 0 |
|
pickup_area.collision_mask = 4 # Will detect WorldWeapons (we'll use layer 3) |
|
add_child(pickup_area) |
|
|
|
# Create collision shape for pickup range |
|
var pickup_collision = CollisionShape3D.new() |
|
var sphere = SphereShape3D.new() |
|
sphere.radius = 2.0 # Pickup range |
|
pickup_collision.shape = sphere |
|
pickup_area.add_child(pickup_collision) |
|
|
|
# Connect signals to track nearby weapons |
|
pickup_area.area_entered.connect(_on_weapon_area_entered) |
|
pickup_area.area_exited.connect(_on_weapon_area_exited) |
|
|
|
func _on_weapon_area_entered(area: Area3D): |
|
# Check if the area belongs to a WorldWeapon |
|
var weapon = area.get_parent() |
|
if weapon is WorldWeapon and weapon not in _nearby_weapons: |
|
_nearby_weapons.append(weapon) |
|
if is_multiplayer_authority(): |
|
print("Weapon nearby: ", weapon.weapon_data.weapon_name if weapon.weapon_data else "Unknown") |
|
|
|
func _on_weapon_area_exited(area: Area3D): |
|
# Remove weapon from nearby list |
|
var weapon = area.get_parent() |
|
if weapon is WorldWeapon and weapon in _nearby_weapons: |
|
_nearby_weapons.erase(weapon) |
|
if is_multiplayer_authority(): |
|
print("Weapon left range: ", weapon.weapon_data.weapon_name if weapon.weapon_data else "Unknown") |
|
|
|
## 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) |
|
|
|
## Equip a weapon with given data |
|
func equip_weapon(data: WeaponData): |
|
# Determine which hand based on weapon type |
|
var is_offhand = (data.hand_type == WeaponData.Hand.OFF_HAND) |
|
|
|
# Unequip current weapon in that hand first |
|
if is_offhand: |
|
if equipped_offhand: |
|
unequip_weapon(true) |
|
else: |
|
if equipped_weapon: |
|
unequip_weapon(false) |
|
|
|
# Determine where to attach the weapon |
|
var attach_point: Node3D |
|
if is_offhand: |
|
attach_point = _offhand_container if _offhand_container else _offhand_attachment |
|
else: |
|
attach_point = _weapon_container if _weapon_container else _weapon_attachment |
|
|
|
if not attach_point: |
|
push_error("No weapon attachment point found for ", "off-hand" if is_offhand else "main hand") |
|
return |
|
|
|
# Create new weapon instance |
|
var weapon = BaseWeapon.new() |
|
weapon.weapon_data = data |
|
weapon.name = "EquippedOffHand" if is_offhand else "EquippedWeapon" |
|
weapon.set_owner_character(self) |
|
|
|
# Attach to weapon container (or bone attachment if no container) |
|
attach_point.add_child(weapon) |
|
|
|
# Store reference |
|
if is_offhand: |
|
equipped_offhand = weapon |
|
else: |
|
equipped_weapon = weapon |
|
|
|
if is_multiplayer_authority(): |
|
print("Equipped: ", data.weapon_name, " to ", attach_point.name) |
|
|
|
## Unequip current weapon (local only) |
|
func unequip_weapon(is_offhand: bool = false): |
|
if is_offhand: |
|
if equipped_offhand: |
|
equipped_offhand.queue_free() |
|
equipped_offhand = null |
|
else: |
|
if equipped_weapon: |
|
equipped_weapon.queue_free() |
|
equipped_weapon = null |
|
|
|
## Sync unequip across all clients |
|
@rpc("any_peer", "call_local", "reliable") |
|
func _unequip_weapon_sync(is_offhand: bool = false): |
|
unequip_weapon(is_offhand) |
|
|
|
## Drop currently equipped weapon |
|
@rpc("any_peer", "reliable") |
|
func drop_weapon(is_offhand: bool = false): |
|
var has_weapon = equipped_offhand if is_offhand else equipped_weapon |
|
print("drop_weapon called on peer ", multiplayer.get_unique_id(), " is_server: ", multiplayer.is_server(), " has weapon: ", has_weapon != null, " offhand: ", is_offhand) |
|
|
|
if not has_weapon: |
|
print("No weapon equipped in ", "off-hand" if is_offhand else "main hand", ", cannot drop") |
|
return |
|
|
|
# Only server spawns the world weapon |
|
if multiplayer.is_server(): |
|
print("Server spawning dropped weapon") |
|
_spawn_world_weapon(has_weapon.weapon_data) |
|
# Tell all clients to unequip |
|
rpc("_unequip_weapon_sync", is_offhand) |
|
|
|
# Unequip locally |
|
unequip_weapon(is_offhand) |
|
|
|
if is_multiplayer_authority(): |
|
print("Dropped weapon from ", "off-hand" if is_offhand else "main hand") |
|
|
|
## Spawn a weapon in the world (server only) |
|
func _spawn_world_weapon(data: WeaponData): |
|
if not multiplayer.is_server(): |
|
return |
|
|
|
# Get the resource path |
|
var resource_path = data.resource_path |
|
if resource_path == "": |
|
push_error("WeaponData has no resource path! Make sure to save it as a .tres file") |
|
return |
|
|
|
# Position in front of player |
|
var spawn_pos = global_position + (-transform.basis.z * 2.0) |
|
spawn_pos.y += 1.0 # Spawn at chest height |
|
|
|
# Calculate forward velocity |
|
var velocity = -transform.basis.z * 3.0 |
|
|
|
# Tell level to spawn weapon on all clients |
|
var level = get_tree().get_current_scene() |
|
if level and level.has_method("spawn_world_weapon"): |
|
# Increment the level's weapon counter |
|
level._weapon_spawn_counter += 1 |
|
level.rpc("spawn_world_weapon", resource_path, spawn_pos, velocity, level._weapon_spawn_counter) |
|
|
|
## Pick up nearest weapon |
|
func _pickup_nearest_weapon(): |
|
if _nearby_weapons.size() == 0: |
|
return |
|
|
|
# Find closest weapon |
|
var closest_weapon: WorldWeapon = null |
|
var closest_distance: float = INF |
|
|
|
for weapon in _nearby_weapons: |
|
if not is_instance_valid(weapon): |
|
continue |
|
|
|
var distance = global_position.distance_to(weapon.global_position) |
|
if distance < closest_distance: |
|
closest_distance = distance |
|
closest_weapon = weapon |
|
|
|
if closest_weapon: |
|
# Check what hand this weapon goes in |
|
var weapon_hand_type = closest_weapon.weapon_data.hand_type if closest_weapon.weapon_data else WeaponData.Hand.MAIN_HAND |
|
var is_offhand = (weapon_hand_type == WeaponData.Hand.OFF_HAND) |
|
var slot_occupied = equipped_offhand if is_offhand else equipped_weapon |
|
|
|
# If the slot is occupied, drop it first |
|
if slot_occupied: |
|
print("Slot occupied, dropping current weapon before picking up") |
|
if multiplayer.is_server(): |
|
drop_weapon(is_offhand) |
|
else: |
|
rpc_id(1, "drop_weapon", is_offhand) |
|
# Wait a tiny bit for the drop to complete |
|
await get_tree().create_timer(0.1).timeout |
|
|
|
# Request server to pickup (server validates) |
|
if multiplayer.is_server(): |
|
closest_weapon.try_pickup(multiplayer.get_unique_id()) |
|
else: |
|
closest_weapon.rpc_id(1, "try_pickup", multiplayer.get_unique_id())
|
|
|