replace with multiplayer-coop files

This commit is contained in:
2026-01-08 16:47:52 +01:00
parent 1725c615ce
commit 22c7025ac4
1230 changed files with 20555 additions and 17232 deletions

480
src/scripts/loot.gd Normal file
View File

@@ -0,0 +1,480 @@
extends CharacterBody2D
# Loot Item - Coins and food items that drop from enemies
enum LootType {
COIN,
APPLE,
BANANA,
CHERRY,
KEY
}
@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
@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_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) + 11 # vframe 9, hframe 11
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) + 12 # vframe 9, hframe 12
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) + 13 # vframe 9, hframe 13
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) + 11 # vframe 9, hframe 13
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
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)
if not collected and abs(velocity_z) > min_bounce_velocity:
# Bounce on floor
if loot_type == LootType.COIN and bounce_timer == 0.0:
# Play bounce sound for coins
if sfx_coin_bounce:
# Adjust volume based on bounce velocity
sfx_coin_bounce.volume_db = -1.0 + (-10.0 - (velocity_z * 0.1))
sfx_coin_bounce.play()
bounce_timer = 0.08 # Prevent rapid bounce sounds
velocity_z = - velocity_z * bounce_restitution
is_airborne = true # Still bouncing
else:
velocity_z = 0.0
is_airborne = false
else:
is_airborne = false
# 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:
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 (gold color)
_show_floating_text(player, "+1 coin", Color(1.0, 0.84, 0.0)) # Gold color
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, LootType.BANANA, LootType.CHERRY:
if sfx_potion_collect:
sfx_potion_collect.play()
# Heal player
if player.has_method("heal"):
player.heal(heal_amount)
# Show floating text
_show_floating_text(player, "+" + str(int(heal_amount)), Color.GREEN)
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.KEY:
if sfx_key_collect:
sfx_key_collect.play()
# TODO: GIVE PLAYER KEY IN INVENTORY!
# Show floating text
_show_floating_text(player, "+1 key", Color.YELLOW)
self.visible = false
# Wait for sound to finish before removing
if sfx_key_collect and sfx_key_collect.playing:
await $SfxKeyCollect.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:
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] 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()
func _show_floating_text(player: Node, text: String, color: Color):
# Create floating text above player
var floating_text_scene = preload("res://scenes/floating_text.tscn")
if floating_text_scene:
var floating_text = floating_text_scene.instantiate()
player.get_parent().add_child(floating_text)
floating_text.global_position = player.global_position + Vector2(0, -20)
floating_text.setup(text, color)