693 lines
27 KiB
GDScript
693 lines
27 KiB
GDScript
extends CharacterBody2D
|
|
|
|
# Loot Item - Coins and food items that drop from enemies
|
|
|
|
enum LootType {
|
|
COIN,
|
|
APPLE,
|
|
BANANA,
|
|
CHERRY,
|
|
KEY,
|
|
ITEM # Item instance (equipment, consumables, etc.)
|
|
}
|
|
|
|
@export var loot_type: LootType = LootType.COIN
|
|
|
|
# Z-axis physics (like boxes and players)
|
|
var position_z: float = 0.0
|
|
var velocity_z: float = 0.0
|
|
var acceleration_z: float = 0.0
|
|
var is_airborne: bool = true
|
|
var velocity_set_by_spawner: bool = false # Track if velocity was set externally
|
|
|
|
# Bounce physics
|
|
var bounce_restitution: float = 0.6 # How much bounce energy is retained (0-1)
|
|
var min_bounce_velocity: float = 40.0 # Minimum velocity needed to bounce
|
|
var friction: float = 25.0 # Friction when on ground (increased to dampen faster)
|
|
var bounce_timer: float = 0.0 # Prevent rapid bounce sounds
|
|
|
|
# Loot properties
|
|
var coin_value: int = 1
|
|
var heal_amount: float = 20.0
|
|
var collected: bool = false
|
|
var item: Item = null # Item instance (for LootType.ITEM)
|
|
|
|
@onready var sprite = $Sprite2D
|
|
@onready var shadow = $Shadow
|
|
@onready var pickup_area = $PickupArea
|
|
@onready var collision_shape = $CollisionShape2D
|
|
@onready var sfx_coin_bounce = $SfxCoinBounce
|
|
@onready var sfx_coin_collect = $SfxCoinCollect
|
|
@onready var sfx_loot_collect = $SfxLootCollect
|
|
@onready var sfx_potion_collect = $SfxPotionCollect
|
|
@onready var sfx_banana_collect = $SfxBananaCollect
|
|
@onready var sfx_key_collect = $SfxKeyCollect
|
|
|
|
func _ready():
|
|
add_to_group("loot")
|
|
|
|
# Setup shadow
|
|
if shadow:
|
|
shadow.modulate = Color(0, 0, 0, 0.5)
|
|
shadow.z_index = -1
|
|
|
|
# Setup pickup area
|
|
if pickup_area:
|
|
pickup_area.body_entered.connect(_on_pickup_area_body_entered)
|
|
# Set collision mask to detect players (layer 1)
|
|
pickup_area.collision_mask = 1
|
|
|
|
# Top-down physics
|
|
motion_mode = MOTION_MODE_FLOATING
|
|
|
|
# Initial velocity is set by spawner (server) or synced via RPC (clients)
|
|
# If not set externally, use defaults (shouldn't happen in normal flow)
|
|
if not velocity_set_by_spawner:
|
|
velocity_z = randf_range(80.0, 120.0)
|
|
var random_angle = randf() * PI * 2
|
|
var random_force = randf_range(50.0, 100.0)
|
|
velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
|
|
is_airborne = true
|
|
|
|
# Setup sprite based on loot type (call after all properties are set)
|
|
call_deferred("_setup_sprite")
|
|
|
|
# Setup collision shape based on loot type
|
|
call_deferred("_setup_collision_shape")
|
|
|
|
# Adjust bounce properties based on loot type
|
|
if loot_type == LootType.COIN:
|
|
bounce_restitution = 0.4 # Reduced from 0.6 to dampen more
|
|
min_bounce_velocity = 40.0
|
|
else:
|
|
bounce_restitution = 0.2 # Reduced from 0.3 to dampen more
|
|
min_bounce_velocity = 60.0
|
|
|
|
func _setup_sprite():
|
|
if not sprite:
|
|
return
|
|
|
|
match loot_type:
|
|
LootType.COIN:
|
|
# Load coin texture
|
|
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
|
|
if coin_texture and sprite:
|
|
sprite.texture = coin_texture
|
|
sprite.hframes = 6
|
|
sprite.vframes = 1
|
|
sprite.frame = 0
|
|
print("Coin sprite setup: texture=", coin_texture != null, " hframes=", sprite.hframes, " vframes=", sprite.vframes, " frame=", sprite.frame)
|
|
else:
|
|
print("ERROR: Coin texture or sprite is null! texture=", coin_texture, " sprite=", sprite)
|
|
LootType.APPLE:
|
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
if items_texture:
|
|
sprite.texture = items_texture
|
|
sprite.hframes = 20
|
|
sprite.vframes = 14
|
|
sprite.frame = (8 * 20) + 10
|
|
LootType.BANANA:
|
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
if items_texture:
|
|
sprite.texture = items_texture
|
|
sprite.hframes = 20
|
|
sprite.vframes = 14
|
|
sprite.frame = (8 * 20) + 11
|
|
LootType.CHERRY:
|
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
if items_texture:
|
|
sprite.texture = items_texture
|
|
sprite.hframes = 20
|
|
sprite.vframes = 14
|
|
sprite.frame = (8 * 20) + 12
|
|
LootType.KEY:
|
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
if items_texture:
|
|
sprite.texture = items_texture
|
|
sprite.hframes = 20
|
|
sprite.vframes = 14
|
|
sprite.frame = (13 * 20) + 10
|
|
LootType.ITEM:
|
|
# Item instance - use item's spritePath and spriteFrame
|
|
if item and item.spritePath != "":
|
|
var items_texture = load(item.spritePath)
|
|
if items_texture:
|
|
sprite.texture = items_texture
|
|
sprite.hframes = item.spriteFrames.x if item.spriteFrames.x > 0 else 20
|
|
sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14
|
|
sprite.frame = item.spriteFrame
|
|
print("Loot: Set up item sprite for ", item.item_name, " frame=", sprite.frame)
|
|
else:
|
|
print("Loot: ERROR - Could not load texture from spritePath: ", item.spritePath)
|
|
else:
|
|
print("Loot: ERROR - Item loot has no item instance or spritePath! item=", item)
|
|
|
|
func _setup_collision_shape():
|
|
if not collision_shape:
|
|
# Try to find existing collision shape
|
|
collision_shape = get_node_or_null("CollisionShape2D")
|
|
if not collision_shape:
|
|
# Create collision shape if it doesn't exist
|
|
collision_shape = CollisionShape2D.new()
|
|
add_child(collision_shape)
|
|
|
|
# Get or create circle shape
|
|
var circle_shape: CircleShape2D = null
|
|
if collision_shape.shape and collision_shape.shape is CircleShape2D:
|
|
circle_shape = collision_shape.shape as CircleShape2D
|
|
else:
|
|
circle_shape = CircleShape2D.new()
|
|
|
|
# Set collision size based on loot type
|
|
if loot_type == LootType.COIN:
|
|
circle_shape.radius = 3.0 # 6x6 pixels (diameter)
|
|
else:
|
|
circle_shape.radius = 7.0 # 14x14 pixels (diameter)
|
|
|
|
collision_shape.shape = circle_shape
|
|
|
|
func _physics_process(delta):
|
|
# Stop all physics processing if collected
|
|
if collected:
|
|
return
|
|
|
|
# Update bounce timer
|
|
if bounce_timer > 0.0:
|
|
bounce_timer -= delta
|
|
if bounce_timer < 0:
|
|
bounce_timer = 0.0
|
|
|
|
# Update Z-axis physics
|
|
if is_airborne:
|
|
# Apply gravity to Z-axis
|
|
acceleration_z = -300.0 # Gravity
|
|
velocity_z += acceleration_z * delta
|
|
|
|
# CRITICAL: Apply damping to velocity_z to lerp it towards 0 (prevents infinite bouncing)
|
|
# Dampen more when velocity is small (closer to ground) but allow normal bounces first
|
|
var damping_factor = 8.0 # How quickly velocity_z approaches 0 (allow more visible bounces)
|
|
if abs(velocity_z) < 25.0: # More aggressive damping for very small velocities only
|
|
damping_factor = 20.0
|
|
velocity_z = lerpf(velocity_z, 0.0, 1.0 - exp(-damping_factor * delta))
|
|
|
|
position_z += velocity_z * delta
|
|
|
|
# Apply air resistance to slow down horizontal movement while airborne
|
|
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-8.0 * delta))
|
|
|
|
# Ground collision and bounce (skip if collected to prevent bounce sounds)
|
|
if position_z <= 0.0:
|
|
position_z = 0.0
|
|
|
|
# Apply friction when on ground (dampen X/Y momentum faster)
|
|
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
|
|
|
|
# Check if we should bounce (only if not collected and velocity is significant)
|
|
# Allow bouncing but ensure it eventually stops
|
|
if not collected and abs(velocity_z) > min_bounce_velocity:
|
|
# Bounce on floor
|
|
# Only play bounce sound if bounce is significant enough and timer has elapsed
|
|
# CRITICAL: Only play sound if velocity is large enough and coin is actually falling (downward)
|
|
if loot_type == LootType.COIN and bounce_timer == 0.0 and abs(velocity_z) > 50.0 and velocity_z < 0.0:
|
|
# Play bounce sound for coins (only for significant downward velocities)
|
|
if sfx_coin_bounce:
|
|
# Adjust volume based on bounce velocity (softer for smaller bounces)
|
|
var volume_multiplier = clamp(abs(velocity_z) / 100.0, 0.3, 1.0)
|
|
sfx_coin_bounce.volume_db = -3.0 + (-12.0 * (1.0 - volume_multiplier))
|
|
sfx_coin_bounce.play()
|
|
bounce_timer = 0.12 # Prevent rapid bounce sounds but allow reasonable bounce rate
|
|
|
|
velocity_z = - velocity_z * bounce_restitution
|
|
|
|
# CRITICAL: Force stop bouncing if velocity is too small after bounce (prevent micro-bounces)
|
|
# Use a lower threshold to allow a few more bounces before stopping
|
|
if abs(velocity_z) < min_bounce_velocity * 0.5:
|
|
velocity_z = 0.0
|
|
is_airborne = false
|
|
else:
|
|
is_airborne = true # Still bouncing
|
|
else:
|
|
# Velocity too small or collected - stop bouncing
|
|
velocity_z = 0.0
|
|
is_airborne = false
|
|
else:
|
|
is_airborne = false
|
|
# Ensure velocity_z is zero when on ground
|
|
velocity_z = 0.0
|
|
# Apply friction even when not airborne (on ground)
|
|
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
|
|
|
|
# Move and check for collisions
|
|
move_and_slide()
|
|
|
|
# Check for wall collisions (skip if collected to prevent bounce sounds)
|
|
if not collected:
|
|
for i in get_slide_collision_count():
|
|
var collision = get_slide_collision(i)
|
|
if collision:
|
|
var collider = collision.get_collider()
|
|
# Only bounce off walls, not players (players are detected via PickupArea)
|
|
if collider and not collider.is_in_group("player"):
|
|
# Bounce off walls
|
|
var normal = collision.get_normal()
|
|
velocity = velocity.bounce(normal) * 0.5 # Reduce velocity more after bounce (was 0.8)
|
|
|
|
# Play bounce sound for coins hitting walls
|
|
if loot_type == LootType.COIN and bounce_timer == 0.0:
|
|
if sfx_coin_bounce:
|
|
sfx_coin_bounce.volume_db = -5.0
|
|
sfx_coin_bounce.play()
|
|
bounce_timer = 0.08
|
|
|
|
# Update visual position based on Z
|
|
_update_visuals()
|
|
|
|
# Animate coin rotation (always animate, even when not airborne)
|
|
if loot_type == LootType.COIN:
|
|
_animate_coin(delta)
|
|
|
|
func _update_z_physics(delta):
|
|
position_z += velocity_z * delta
|
|
|
|
func _update_visuals():
|
|
# Update sprite position based on Z
|
|
if sprite:
|
|
sprite.position.y = - position_z * 0.5
|
|
|
|
# Update shadow scale and opacity based on Z
|
|
if shadow:
|
|
var shadow_scale = 1.0 - (position_z / 50.0) * 0.5
|
|
shadow.scale = Vector2.ONE * max(0.3, shadow_scale)
|
|
shadow.modulate.a = 0.5 - (position_z / 50.0) * 0.2
|
|
|
|
var coin_anim_time: float = 0.0
|
|
func _animate_coin(delta):
|
|
# Rotate coin animation
|
|
if sprite and loot_type == LootType.COIN:
|
|
# Make sure sprite is set up correctly (in case _setup_sprite hasn't run yet)
|
|
if sprite.hframes < 6 or sprite.texture == null:
|
|
_setup_sprite()
|
|
# Animate coin rotation
|
|
coin_anim_time += delta * 10.0 # Animation speed
|
|
var frame = int(coin_anim_time) % 6
|
|
sprite.frame = frame
|
|
|
|
func _on_pickup_area_body_entered(body):
|
|
if body and body.is_in_group("player") and not body.is_dead:
|
|
# Check if this item was dropped by this player recently (5 second cooldown)
|
|
if has_meta("dropped_by_peer_id") and has_meta("drop_time"):
|
|
var dropped_by_peer_id = get_meta("dropped_by_peer_id")
|
|
var drop_time = get_meta("drop_time")
|
|
var current_time = Time.get_ticks_msec()
|
|
var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds
|
|
|
|
# Check if this player dropped the item and cooldown hasn't expired
|
|
if body.has_method("get_multiplayer_authority"):
|
|
var player_peer_id = body.get_multiplayer_authority()
|
|
if player_peer_id == dropped_by_peer_id and time_since_drop < 5.0:
|
|
# Player can't pick up their own dropped item for 5 seconds
|
|
print("Loot: Player ", body.name, " cannot pick up item they dropped (", time_since_drop, "s ago, need 5s cooldown)")
|
|
return
|
|
|
|
print("Loot: Pickup area entered by player: ", body.name, " is_local: ", body.is_local_player if "is_local_player" in body else "unknown", " is_server: ", multiplayer.is_server())
|
|
_pickup(body)
|
|
|
|
func _pickup(player: Node):
|
|
# Prevent multiple pickups
|
|
if collected:
|
|
print("Loot: Already collected, ignoring pickup")
|
|
return
|
|
|
|
var player_auth_str = "N/A"
|
|
if "get_multiplayer_authority" in player:
|
|
player_auth_str = str(player.get_multiplayer_authority())
|
|
print("Loot: _pickup called by player: ", player.name, " is_server: ", multiplayer.is_server(), " has_peer: ", multiplayer.has_multiplayer_peer(), " player_authority: ", player_auth_str)
|
|
|
|
# In multiplayer, only process on server or if player has authority
|
|
# If client player picks it up, send RPC to server
|
|
if multiplayer.has_multiplayer_peer():
|
|
if not multiplayer.is_server():
|
|
# Client: send pickup request to server
|
|
if player.is_multiplayer_authority():
|
|
# This is the local player, send request to server
|
|
var player_peer_id = player.get_multiplayer_authority()
|
|
print("Loot: Client sending pickup request to server for player peer_id: ", player_peer_id)
|
|
# Route through game_world to avoid node path issues
|
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
|
var loot_id = get_meta("loot_id") if has_meta("loot_id") else -1
|
|
if game_world and game_world.has_method("_request_loot_pickup"):
|
|
game_world._request_loot_pickup.rpc_id(1, loot_id, global_position, player_peer_id)
|
|
else:
|
|
# Fallback: try direct RPC
|
|
rpc_id(1, "_request_pickup", player_peer_id)
|
|
else:
|
|
print("Loot: Client player does not have authority, cannot pickup")
|
|
return
|
|
else:
|
|
# Server: If player doesn't have authority, this is a client player
|
|
# The client will send _request_pickup RPC, so we should ignore this Area2D signal
|
|
# to avoid duplicate processing and errors
|
|
if not player.is_multiplayer_authority():
|
|
# Client player - they will send RPC, so ignore this signal completely
|
|
# This prevents race conditions where server's Area2D fires before client's RPC arrives
|
|
# Don't log as error since this is expected behavior
|
|
return
|
|
|
|
# Process the pickup (common code for both server and single-player)
|
|
_process_pickup_on_server(player)
|
|
|
|
func _process_pickup_on_server(player: Node):
|
|
# Internal function to process pickup on server (called from _request_pickup RPC)
|
|
# This skips the authority check since we've already validated the request
|
|
|
|
# Mark as collected immediately to prevent duplicate pickups
|
|
# (Note: This may already be set by _request_pickup, but set it here too for safety)
|
|
if not collected:
|
|
collected = true
|
|
|
|
# Disable pickup area immediately to prevent further collisions
|
|
# Use set_deferred() because we're in a signal callback
|
|
if pickup_area:
|
|
pickup_area.set_deferred("monitoring", false)
|
|
pickup_area.set_deferred("monitorable", false)
|
|
|
|
# Sync removal to all clients FIRST (before processing pickup)
|
|
# This ensures clients remove the loot even if host processes it
|
|
# Use game_world to route removal sync instead of direct RPC to avoid node path issues
|
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
|
var loot_id = get_meta("loot_id") if has_meta("loot_id") else -1
|
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
|
if game_world and game_world.has_method("_sync_loot_remove"):
|
|
print("Loot: Server syncing removal of loot id=", loot_id, " at ", global_position)
|
|
game_world._sync_loot_remove.rpc(loot_id, global_position)
|
|
else:
|
|
# Fallback: try direct RPC (may fail if node path doesn't match)
|
|
print("Loot: Server syncing removal via direct RPC (fallback)")
|
|
rpc("_sync_remove")
|
|
|
|
match loot_type:
|
|
LootType.COIN:
|
|
if sfx_coin_collect:
|
|
sfx_coin_collect.play()
|
|
# Give player coin
|
|
if player.has_method("add_coins"):
|
|
player.add_coins(coin_value)
|
|
# Show floating text with item graphic and text
|
|
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
|
|
_show_floating_text(player, "+" + str(coin_value) + " COIN", Color(1.0, 0.84, 0.0), 0.5, 0.5, coin_texture, 6, 1, 0)
|
|
# Sync floating text to client
|
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
|
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(coin_value) + " COIN", Color(1.0, 0.84, 0.0), coin_value, 0, player.get_multiplayer_authority())
|
|
|
|
self.visible = false
|
|
|
|
# Wait for sound to finish before removing
|
|
if sfx_coin_collect and sfx_coin_collect.playing:
|
|
await sfx_coin_collect.finished
|
|
queue_free()
|
|
LootType.APPLE:
|
|
if sfx_potion_collect:
|
|
sfx_potion_collect.play()
|
|
# Heal player
|
|
var actual_heal = 0.0
|
|
if player.has_method("heal"):
|
|
actual_heal = heal_amount
|
|
player.heal(heal_amount)
|
|
# Show floating text with item graphic and heal amount
|
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 10)
|
|
# Sync floating text to client
|
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
|
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, int(actual_heal), (8 * 20) + 10, player.get_multiplayer_authority())
|
|
|
|
self.visible = false
|
|
|
|
# Wait for sound to finish before removing
|
|
if sfx_potion_collect and sfx_potion_collect.playing:
|
|
await sfx_potion_collect.finished
|
|
queue_free()
|
|
LootType.BANANA:
|
|
if sfx_banana_collect:
|
|
sfx_banana_collect.play()
|
|
# Heal player
|
|
var actual_heal = 0.0
|
|
if player.has_method("heal"):
|
|
actual_heal = heal_amount
|
|
player.heal(heal_amount)
|
|
# Show floating text with item graphic and heal amount
|
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11)
|
|
# Sync floating text to client
|
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
|
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, int(actual_heal), (8 * 20) + 11, player.get_multiplayer_authority())
|
|
|
|
self.visible = false
|
|
|
|
# Wait for sound to finish before removing
|
|
if sfx_banana_collect and sfx_banana_collect.playing:
|
|
await sfx_banana_collect.finished
|
|
queue_free()
|
|
LootType.CHERRY:
|
|
if sfx_banana_collect:
|
|
sfx_banana_collect.play()
|
|
# Heal player
|
|
var actual_heal = 0.0
|
|
if player.has_method("heal"):
|
|
actual_heal = heal_amount
|
|
player.heal(heal_amount)
|
|
# Show floating text with item graphic and heal amount
|
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12)
|
|
# Sync floating text to client
|
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
|
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, int(actual_heal), (8 * 20) + 12, player.get_multiplayer_authority())
|
|
|
|
self.visible = false
|
|
|
|
# Wait for sound to finish before removing
|
|
if sfx_banana_collect and sfx_banana_collect.playing:
|
|
await sfx_banana_collect.finished
|
|
queue_free()
|
|
LootType.KEY:
|
|
if sfx_key_collect:
|
|
sfx_key_collect.play()
|
|
# Give player key in inventory
|
|
if player.has_method("add_key"):
|
|
player.add_key(1)
|
|
# Show floating text with item graphic and text
|
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
_show_floating_text(player, "+1 KEY", Color.YELLOW, 0.5, 0.5, items_texture, 20, 14, (13 * 20) + 10)
|
|
# Sync floating text to client
|
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
|
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+1 KEY", Color.YELLOW, 0, (13 * 20) + 10, player.get_multiplayer_authority())
|
|
|
|
self.visible = false
|
|
|
|
# Wait for sound to finish before removing
|
|
if sfx_key_collect and sfx_key_collect.playing:
|
|
await $SfxKeyCollect.finished
|
|
queue_free()
|
|
LootType.ITEM:
|
|
# Item instance pickup
|
|
if not item:
|
|
print("Loot: ERROR - Item loot has no item instance!")
|
|
queue_free()
|
|
return
|
|
|
|
if sfx_loot_collect:
|
|
sfx_loot_collect.play()
|
|
|
|
# Handle item pickup based on type
|
|
if item.item_type == Item.ItemType.Equippable:
|
|
# Equippable item - add to inventory
|
|
if player.character_stats:
|
|
player.character_stats.add_item(item)
|
|
print(name, " picked up item: ", item.item_name, " (added to inventory)")
|
|
elif item.item_type == Item.ItemType.Restoration:
|
|
# Consumable item - use immediately
|
|
if player.character_stats:
|
|
# Apply modifiers (hp, mp, etc.)
|
|
if item.modifiers.has("hp"):
|
|
var hp_heal = item.modifiers["hp"]
|
|
if player.has_method("heal"):
|
|
player.heal(hp_heal)
|
|
if item.modifiers.has("mp"):
|
|
var mana_amount = item.modifiers["mp"]
|
|
player.character_stats.restore_mana(mana_amount)
|
|
|
|
# TODO: Handle other modifiers (dodge_chance, res_all, etc.) - these would need duration tracking
|
|
|
|
print(name, " used item: ", item.item_name)
|
|
|
|
# Show floating text with item name
|
|
var items_texture = load(item.spritePath)
|
|
var display_text = item.item_name
|
|
var text_color = Color.WHITE
|
|
|
|
# Color code based on item type
|
|
if item.item_type == Item.ItemType.Equippable:
|
|
text_color = Color.CYAN # Cyan for equipment
|
|
elif item.item_type == Item.ItemType.Restoration:
|
|
text_color = Color.GREEN # Green for consumables
|
|
|
|
_show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, item.spriteFrames.x, item.spriteFrames.y, item.spriteFrame)
|
|
|
|
# Sync floating text to client
|
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
|
_sync_show_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, display_text, text_color, 0, item.spriteFrame, player.get_multiplayer_authority())
|
|
|
|
self.visible = false
|
|
|
|
# Wait for sound to finish before removing
|
|
if sfx_loot_collect and sfx_loot_collect.playing:
|
|
await sfx_loot_collect.finished
|
|
queue_free()
|
|
|
|
var processing_pickup: bool = false # Mutex to prevent concurrent pickup processing
|
|
|
|
@rpc("any_peer", "reliable")
|
|
func _request_pickup(player_peer_id: int):
|
|
# Server receives pickup request from client
|
|
print("Loot: _request_pickup RPC received on server for player_peer_id: ", player_peer_id, " is_server: ", multiplayer.is_server())
|
|
|
|
if not multiplayer.is_server():
|
|
print("Loot: _request_pickup called on non-server, ignoring")
|
|
return
|
|
|
|
# Use mutex to prevent concurrent processing (race condition protection)
|
|
if processing_pickup:
|
|
print("Loot: Pickup already being processed, ignoring duplicate request")
|
|
return
|
|
|
|
# Check if already collected (this prevents race conditions with Area2D signals)
|
|
if collected:
|
|
print("Loot: Already collected (collected=", collected, "), ignoring pickup request")
|
|
return
|
|
|
|
# Set mutex and mark as collected IMMEDIATELY to prevent any race conditions
|
|
processing_pickup = true
|
|
collected = true
|
|
print("Loot: Marked as collected=true and set processing_pickup=true at start of _request_pickup")
|
|
|
|
# Find the player by peer ID
|
|
var player = null
|
|
var players = get_tree().get_nodes_in_group("player")
|
|
print("Loot: Searching for player with peer_id ", player_peer_id, " among ", players.size(), " players")
|
|
for p in players:
|
|
var p_authority = p.get_multiplayer_authority() if "get_multiplayer_authority" in p else -1
|
|
print("Loot: Checking player ", p.name, " authority: ", p_authority)
|
|
if p_authority == player_peer_id:
|
|
player = p
|
|
break
|
|
|
|
if not player:
|
|
print("Loot: ERROR - Could not find player with peer_id ", player_peer_id, " for pickup")
|
|
# Clear mutex and reset collected if player not found (allow retry)
|
|
processing_pickup = false
|
|
collected = false
|
|
return
|
|
|
|
print("Loot: Found player ", player.name, " processing pickup on server")
|
|
# Process pickup on server directly (skip authority check since we've already validated the request came from the right client)
|
|
# Don't call _pickup() because it will check authority and reject client players
|
|
_process_pickup_on_server(player)
|
|
|
|
# Clear mutex after processing completes
|
|
processing_pickup = false
|
|
|
|
# This function can be called directly (not just via RPC) when game_world routes the update
|
|
func _sync_remove():
|
|
# Clients remove loot when any player picks it up
|
|
# Only process if we're not the authority (i.e., we're a client)
|
|
if multiplayer.is_server():
|
|
return # Server ignores its own updates
|
|
|
|
print("Loot: Client received removal sync for loot at ", global_position)
|
|
|
|
# Mark as collected immediately to stop physics and sounds
|
|
collected = true
|
|
|
|
# Play pickup sound on client so they can hear it
|
|
match loot_type:
|
|
LootType.COIN:
|
|
if sfx_coin_collect:
|
|
sfx_coin_collect.play()
|
|
LootType.APPLE, LootType.BANANA, LootType.CHERRY, LootType.ITEM:
|
|
if sfx_loot_collect:
|
|
sfx_loot_collect.play()
|
|
|
|
# Disable pickup area to prevent further collisions
|
|
if pickup_area:
|
|
pickup_area.monitoring = false
|
|
pickup_area.monitorable = false
|
|
|
|
# Hide immediately
|
|
visible = false
|
|
|
|
# Wait for sound to finish before removing (if sound is playing)
|
|
var _sound_playing = false
|
|
if loot_type == LootType.COIN and sfx_coin_collect and sfx_coin_collect.playing:
|
|
_sound_playing = true
|
|
await sfx_coin_collect.finished
|
|
elif loot_type in [LootType.APPLE, LootType.BANANA, LootType.CHERRY, LootType.ITEM] and sfx_loot_collect and sfx_loot_collect.playing:
|
|
_sound_playing = true
|
|
await sfx_loot_collect.finished
|
|
|
|
# Remove from scene
|
|
if not is_queued_for_deletion():
|
|
queue_free()
|
|
|
|
@rpc("any_peer", "reliable")
|
|
func _sync_show_floating_text(loot_type_value: int, text: String, color_value: Color, _value: int, sprite_frame_value: int, player_peer_id: int):
|
|
# Client receives floating text sync from server
|
|
if multiplayer.is_server():
|
|
return # Server ignores this (it's the sender)
|
|
|
|
# Find player by peer ID
|
|
var player = null
|
|
var players = get_tree().get_nodes_in_group("player")
|
|
for p in players:
|
|
if p.get_multiplayer_authority() == player_peer_id:
|
|
player = p
|
|
break
|
|
|
|
if not player or not is_instance_valid(player):
|
|
return # Can't find player
|
|
|
|
# Determine texture and parameters based on loot type
|
|
var item_texture: Texture2D = null
|
|
var sprite_hframes: int = 1
|
|
var sprite_vframes: int = 1
|
|
|
|
match loot_type_value:
|
|
LootType.COIN:
|
|
item_texture = load("res://assets/gfx/pickups/gold_coin.png")
|
|
sprite_hframes = 6
|
|
sprite_vframes = 1
|
|
LootType.APPLE, LootType.BANANA, LootType.CHERRY, LootType.KEY:
|
|
item_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
sprite_hframes = 20
|
|
sprite_vframes = 14
|
|
LootType.ITEM:
|
|
# Item instance - use item's sprite path
|
|
# Note: item data is not available on client in this sync, so we use default
|
|
# The actual item sprite is set when the loot is created
|
|
item_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
|
sprite_hframes = 20
|
|
sprite_vframes = 14
|
|
|
|
# Show floating text on client
|
|
_show_floating_text(player, text, color_value, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame_value)
|
|
|
|
func _show_floating_text(player: Node, text: String, color: Color, show_time: float = 0.5, fade_time: float = 0.5, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0):
|
|
# Create floating text and item graphic above player's head
|
|
# Shows for show_time seconds, then fades out over fade_time seconds
|
|
var floating_text_scene = preload("res://scenes/floating_text.tscn")
|
|
if floating_text_scene and player and is_instance_valid(player):
|
|
var floating_text = floating_text_scene.instantiate()
|
|
var parent = player.get_parent()
|
|
if parent:
|
|
parent.add_child(floating_text)
|
|
floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20)
|
|
floating_text.setup(text, color, show_time, fade_time, item_texture, sprite_hframes, sprite_vframes, sprite_frame)
|