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

View File

@@ -0,0 +1,508 @@
extends CharacterBody2D
var tileParticleScene = preload("res://scenes/tile_particle.tscn")
# Interactable Object - Can be grabbed, pushed, pulled, lifted, and thrown
@export var is_grabbable: bool = true
@export var can_be_pushed: bool = true
@export var is_destroyable: bool = true
@export var is_liftable: bool = true
@export var weight: float = 1.0 # Affects throw distance and friction
@export var health: float = 1.0 # How many hits before breaking
const BASE_SCALE = Vector2(0.25, 0.25) # Base scale for box sprites
var is_being_held: bool = false
var held_by_player = null
var is_frozen: bool = false
var thrown_by_player = null # Track who threw this box
# Physics for thrown objects
var throw_velocity: Vector2 = Vector2.ZERO
var friction: float = 0.92 # Deceleration when sliding
# Z-axis simulation (for throwing arc)
var position_z: float = 0.0
var velocity_z: float = 0.0
var gravity_z: float = 500.0 # Gravity pulls down (scaled for 1x scale)
var is_airborne: bool = false
@onready var sprite = $Sprite2D
@onready var sprite_above = $Sprite2DAbove
@onready var shadow = $Shadow
# Object type tracking
var object_type: String = ""
var chest_closed_frame: int = -1
var chest_opened_frame: int = -1
var is_chest_opened: bool = false
# Network sync timer
var sync_timer: float = 0.0
var sync_interval: float = 0.05 # Sync 20 times per second
func _ready():
# Make sure it's on the interactable layer
collision_layer = 2 # Layer 2 for objects
collision_mask = 1 | 2 | 4 # Collide with players, other objects, and walls
# No gravity in top-down
motion_mode = MOTION_MODE_FLOATING
# Setup shadow
if shadow:
shadow.modulate = Color(0, 0, 0, 0.5)
shadow.z_index = -1
func _physics_process(delta):
# All clients simulate physics locally for smooth visuals
# Initial throw state is synced via player's _sync_throw RPC
# Don't update physics if being held (player controls position)
if is_being_held:
return
if not is_frozen:
# Z-axis physics for airborne boxes
if is_airborne:
# Apply gravity to Z velocity
velocity_z -= gravity_z * delta
position_z += velocity_z * delta
# Update sprite position and scale based on height
if sprite:
sprite.position.y = - position_z * 0.5
var height_scale = 1.0 - (position_z / 50.0) * 0.2 # Scaled down for smaller Z values
sprite.scale = Vector2(1.0, 1.0) * max(0.8, height_scale)
# Update shadow based on height
if shadow:
var shadow_scale = 1.0 - (position_z / 75.0) * 0.5 # Scaled down for smaller Z values
shadow.scale = Vector2(1.0, 1.0) * max(0.5, shadow_scale)
shadow.modulate.a = 0.5 - (position_z / 100.0) * 0.3
# Apply throw velocity (NO friction while airborne, just like players)
velocity = throw_velocity
# Check if landed
if position_z <= 0:
_land()
else:
# Ground physics - apply friction when on ground
if shadow:
shadow.scale = Vector2(1.0, 1.0) # Set to 1,1 scale instead of BASE_SCALE
shadow.modulate.a = 0.5
# Apply throw velocity
velocity = throw_velocity
# Apply friction only on ground
throw_velocity = throw_velocity * friction
# Stop if very slow
if throw_velocity.length() < 5.0:
throw_velocity = Vector2.ZERO
var _collision = move_and_slide()
# Check collisions while airborne (only check if moving fast enough)
if is_airborne and throw_velocity.length() > 50.0 and get_slide_collision_count() > 0:
_handle_air_collision()
func _land():
is_airborne = false
position_z = 0.0
velocity_z = 0.0
is_frozen = false
is_being_held = false # Make sure it can be grabbed again
held_by_player = null
thrown_by_player = null # Clear who threw it
# Re-enable collision when landing
set_collision_layer_value(2, true)
set_collision_mask_value(1, true)
set_collision_mask_value(2, true)
# Reset sprite
if sprite:
sprite.position.y = 0
sprite.scale = Vector2(1.0, 1.0) # Set to 1,1 scale instead of BASE_SCALE
# Reset shadow
if shadow:
shadow.scale = Vector2(1.0, 1.0) # Set to 1,1 scale instead of BASE_SCALE
shadow.modulate.a = 0.5
# Landing squash effect
if sprite:
var tween = create_tween()
tween.tween_property(sprite, "scale", Vector2(1.2, 0.8), 0.1)
tween.tween_property(sprite, "scale", Vector2(1.0, 1.0), 0.1)
print(name, " landed!")
func _handle_air_collision():
# Handle collision while airborne
for i in get_slide_collision_count():
var collision = get_slide_collision(i)
var collider = collision.get_collider()
# Hit an enemy! Damage them
if collider.is_in_group("enemy"):
# Only process collision on server to prevent duplicates
if not multiplayer.is_server():
continue
# Damage enemy
if collider.has_method("take_damage"):
var attacker_pos = thrown_by_player.global_position if thrown_by_player and is_instance_valid(thrown_by_player) else global_position
collider.take_damage(15.0, attacker_pos)
print(name, " hit enemy ", collider.name, "!")
# Box breaks (only if destroyable)
if is_destroyable:
_break_into_pieces()
if multiplayer.has_multiplayer_peer():
_sync_break.rpc()
return
if collider.is_in_group("player"):
# Ignore collision with the player who threw this box
if collider == thrown_by_player:
continue
# Only process collision on server to prevent duplicates
if not multiplayer.is_server():
continue
# Hit a player! Break locally and sync to others (only if destroyable)
if is_destroyable:
_break_into_pieces()
# Sync break to OTHER clients
if multiplayer.has_multiplayer_peer():
_sync_break.rpc()
# Damage and knockback player using RPC
# Pass the thrower's position for accurate direction
if collider.has_method("rpc_take_damage"):
var attacker_pos = thrown_by_player.global_position if thrown_by_player and is_instance_valid(thrown_by_player) else global_position
var player_peer_id = collider.get_multiplayer_authority()
if player_peer_id != 0:
# If target peer is the same as server (us), call directly
# rpc_id() might not execute locally when called to same peer
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
# Call directly on the same peer
collider.rpc_take_damage(10.0, attacker_pos)
else:
# Send RPC to remote peer
collider.rpc_take_damage.rpc_id(player_peer_id, 10.0, attacker_pos)
else:
# Fallback: broadcast if we can't get peer_id
collider.rpc_take_damage.rpc(10.0, attacker_pos)
print(name, " hit player ", collider.name, "!")
return
elif "throw_velocity" in collider and "is_grabbable" in collider: # Another box
# Only process collision on server to prevent duplicates
if not multiplayer.is_server():
continue
# Hit another box! Break both locally (only if destroyable)
if is_destroyable:
_break_into_pieces()
if collider.has_method("_break_into_pieces") and collider.has_method("can_be_destroyed") and collider.can_be_destroyed():
collider._break_into_pieces()
# Sync break to OTHER clients
if multiplayer.has_multiplayer_peer():
_sync_break.rpc()
# Tell the other box to break too
if collider.has_method("_sync_break") and collider.has_method("can_be_destroyed") and collider.can_be_destroyed():
collider._sync_break.rpc()
print(name, " hit another box!")
return
func _break_into_pieces():
# Only break if destroyable
if not is_destroyable:
return
var sprite_texture = $Sprite2D.texture
var frame_width = sprite_texture.get_width() / $Sprite2D.hframes
var frame_height = sprite_texture.get_height() / $Sprite2D.vframes
var frame_x = ($Sprite2D.frame % $Sprite2D.hframes) * frame_width
var frame_y = ($Sprite2D.frame / $Sprite2D.hframes) * frame_height
# Create 4 particles with different directions and different parts of the texture
var directions = [
Vector2(-1, -1).normalized(), # Top-left
Vector2(1, -1).normalized(), # Top-right
Vector2(-1, 1).normalized(), # Bottom-left
Vector2(1, 1).normalized() # Bottom-right
]
var regions = [
Rect2(frame_x, frame_y, frame_width / 2, frame_height / 2), # Top-left
Rect2(frame_x + frame_width / 2, frame_y, frame_width / 2, frame_height / 2), # Top-right
Rect2(frame_x, frame_y + frame_height / 2, frame_width / 2, frame_height / 2), # Bottom-left
Rect2(frame_x + frame_width / 2, frame_y + frame_height / 2, frame_width / 2, frame_height / 2) # Bottom-right
]
for i in range(4):
var tp = tileParticleScene.instantiate() as CharacterBody2D
var spr2D = tp.get_node("Sprite2D") as Sprite2D
tp.global_position = global_position
# Set up the sprite's texture and region
spr2D.texture = sprite_texture
spr2D.region_enabled = true
spr2D.region_rect = regions[i]
# Add some randomness to the velocity
var speed = randf_range(170, 200)
var dir = directions[i] + Vector2(randf_range(-0.2, 0.2), randf_range(-0.2, 0.2))
tp.velocity = dir * speed
# Add some rotation
tp.angular_velocity = randf_range(-7, 7)
get_parent().call_deferred("add_child", tp)
# Remove self
queue_free()
func can_be_grabbed() -> bool:
return is_grabbable and not is_being_held
func can_be_lifted() -> bool:
# Can be lifted if it's liftable (being held is OK - we're checking if it CAN be lifted)
return is_liftable
func can_be_thrown() -> bool:
# Can be thrown if it's liftable (being held is OK - we're checking if it CAN be thrown)
return is_liftable
func can_be_destroyed() -> bool:
return is_destroyable
func on_grabbed(by_player):
# Special handling for chests - open instead of grab
if object_type == "Chest" and not is_chest_opened:
_open_chest()
return
is_being_held = true
held_by_player = by_player
print(name, " grabbed by ", by_player.name)
func on_lifted(by_player):
# Called when object is lifted above head
# Note: The check for is_liftable is done in can_be_lifted(), not here
# This function is called after the check passes, so we can proceed
is_frozen = true
throw_velocity = Vector2.ZERO
print(name, " lifted by ", by_player.name)
func on_released(by_player):
is_being_held = false
held_by_player = null
is_frozen = false
is_airborne = false
position_z = 0.0
velocity_z = 0.0
throw_velocity = Vector2.ZERO
# Re-enable collision (in case it was disabled)
set_collision_layer_value(2, true)
set_collision_mask_value(1, true)
set_collision_mask_value(2, true)
# Reset sprite and shadow visuals
if sprite:
sprite.position.y = 0
sprite.scale = Vector2(1.0, 1.0) # Set to 1,1 scale instead of BASE_SCALE
if shadow:
shadow.scale = Vector2(1.0, 1.0) # Set to 1,1 scale instead of BASE_SCALE
shadow.modulate.a = 0.5
print(name, " released by ", by_player.name)
func on_thrown(by_player, force: Vector2):
# Only allow throwing if object is liftable
if not is_liftable:
return
is_being_held = false
held_by_player = null
thrown_by_player = by_player # Remember who threw this
is_frozen = false
# Set throw velocity (affected by weight) - increased for longer arc
throw_velocity = force / weight
# Make airborne with same physics as players
is_airborne = true
position_z = 2.5
velocity_z = 100.0 # Scaled down for 1x scale
print(name, " thrown with velocity ", throw_velocity)
@rpc("authority", "unreliable")
func _sync_box_state(pos: Vector2, vel: Vector2, z_pos: float, z_vel: float, airborne: bool):
# Only update on clients (server already has correct state)
if not multiplayer.is_server():
# Only update if we're not holding this box
if not is_being_held:
global_position = pos
throw_velocity = vel
position_z = z_pos
velocity_z = z_vel
is_airborne = airborne
@rpc("any_peer", "reliable")
func _sync_break():
# Sync break to all clients including server (called by whoever breaks the box)
if not is_queued_for_deletion():
_break_into_pieces()
# Object type setup functions
func setup_pot():
object_type = "Pot"
is_grabbable = true
can_be_pushed = true
is_destroyable = true
is_liftable = true
weight = 1.0
var pot_frames = [1, 2, 3, 20, 21, 22, 58]
if sprite:
sprite.frame = pot_frames[randi() % pot_frames.size()]
func setup_liftable_barrel():
object_type = "LiftableBarrel"
is_grabbable = true
can_be_pushed = true
is_destroyable = true
is_liftable = true
weight = 1.0
var barrel_frames = [4, 23]
if sprite:
sprite.frame = barrel_frames[randi() % barrel_frames.size()]
func setup_pushable_barrel():
object_type = "PushableBarrel"
is_grabbable = true
can_be_pushed = true
is_destroyable = true
is_liftable = false
weight = 1.0
if sprite:
sprite.frame = 42
func setup_box():
object_type = "Box"
is_grabbable = true
can_be_pushed = true
is_destroyable = true
is_liftable = true
weight = 1.0
var box_frames = [7, 26]
if sprite:
sprite.frame = box_frames[randi() % box_frames.size()]
func setup_chest():
object_type = "Chest"
is_grabbable = true
can_be_pushed = false
is_destroyable = false
is_liftable = false
weight = 1.0
var chest_frames = [12, 31]
var opened_frames = [13, 32]
var index = randi() % chest_frames.size()
chest_closed_frame = chest_frames[index]
chest_opened_frame = opened_frames[index]
if sprite:
sprite.frame = chest_closed_frame
func setup_pillar():
object_type = "Pillar"
is_grabbable = true
can_be_pushed = true
is_destroyable = false
is_liftable = false
weight = 5.0
if sprite:
sprite.frame = 30
if sprite_above:
sprite_above.frame = 11
func setup_pushable_high_box():
object_type = "PushableHighBox"
is_grabbable = true
can_be_pushed = true
is_destroyable = true
is_liftable = false
weight = 1.0
var bottom_frames = [24, 25]
var top_frames = [5, 6]
var index = randi() % bottom_frames.size()
if sprite:
sprite.frame = bottom_frames[index]
if sprite_above:
sprite_above.frame = top_frames[index]
func _open_chest():
if is_chest_opened:
return
is_chest_opened = true
if sprite and chest_opened_frame >= 0:
sprite.frame = chest_opened_frame
# Spawn loot item
var loot_scene = preload("res://scenes/loot.tscn")
if loot_scene:
var loot = loot_scene.instantiate()
if loot:
# Random loot type
var loot_types = loot.LootType.values()
loot.loot_type = loot_types[randi() % loot_types.size()]
# Position above chest with some randomness
var spawn_pos = global_position + Vector2(randf_range(-10, 10), randf_range(-20, -10))
loot.global_position = spawn_pos
# Set initial velocity to fly out
var random_angle = randf() * PI * 2
var random_force = randf_range(80.0, 120.0)
loot.velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
loot.velocity_z = randf_range(100.0, 150.0)
loot.is_airborne = true
loot.velocity_set_by_spawner = true
get_parent().call_deferred("add_child", loot)
# Sync to network if multiplayer
if multiplayer.has_multiplayer_peer():
_sync_chest_open.rpc()
print(name, " opened! Loot spawned: ", loot_types[loot.loot_type])
@rpc("any_peer", "reliable")
func _sync_chest_open():
# Sync chest opening to all clients
if not is_chest_opened and sprite and chest_opened_frame >= 0:
is_chest_opened = true
sprite.frame = chest_opened_frame