replace with multiplayer-coop files
This commit is contained in:
508
src/scripts/interactable_object.gd
Normal file
508
src/scripts/interactable_object.gd
Normal 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
|
||||
Reference in New Issue
Block a user