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
var is_broken: bool = false
var has_dealt_damage: bool = false # Track if this thrown object has already damaged something

# 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
	
	# Ensure deterministic name for network sync
	if has_meta("object_index") and not name.begins_with("InteractableObject_"):
		name = "InteractableObject_%d" % get_meta("object_index")
	elif name.begins_with("InteractableObject_"):
		# Ensure meta matches name if it already has a consistent name
		var index_str = name.substr(20)
		if index_str.is_valid_int():
			var name_index = index_str.to_int()
			if not has_meta("object_index") or get_meta("object_index") != name_index:
				set_meta("object_index", name_index)
	
	# 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
	has_dealt_damage = false # Reset damage flag for next throw
	
	# 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!")
	$SfxLand.play()
	$DragParticles.emitting = true
	$DragParticles/TimerSmokeParticles.start()

func _handle_air_collision():
	# Handle collision while airborne
	# CRITICAL: Only allow ONE damage event per throw
	if has_dealt_damage:
		return
	
	for i in get_slide_collision_count():
		var collision = get_slide_collision(i)
		var collider = collision.get_collider()
		
		# Break on wall collision (pots and boxes)
		if (object_type == "Pot" or object_type == "Box") and _is_wall_collider(collider):
			# Only process on server to prevent duplicates
			if not multiplayer.is_server():
				continue
			has_dealt_damage = true # Mark as dealt damage (wall hit counts)
			if is_destroyable:
				if multiplayer.has_multiplayer_peer():
					var game_world = get_tree().get_first_node_in_group("game_world")
					if game_world and game_world.has_method("_rpc_to_ready_peers"):
						game_world._rpc_to_ready_peers("_sync_object_break", [name])
				_break_into_pieces()
			return
		
		# 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
			
			has_dealt_damage = true # Mark as dealt damage - can't damage anything else now
			
			# Damage enemy (pots deal less damage than boxes)
			# Enemy's take_damage() already handles defense calculation
			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
				var base_damage = 10.0 if object_type == "Pot" else 15.0
				collider.take_damage(base_damage, attacker_pos)
				print(name, " hit enemy ", collider.name, " with thrown object (", base_damage, " base damage, defense will reduce)!")
			
			# Box breaks (only if destroyable)
			if is_destroyable:
				# Sync break to OTHER clients via RPC BEFORE breaking locally
				# Use game_world to route the RPC to avoid node path resolution issues
				if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
					var game_world = get_tree().get_first_node_in_group("game_world")
					if game_world and game_world.has_method("_rpc_to_ready_peers"):
						game_world._rpc_to_ready_peers("_sync_object_break", [name])
				
				_break_into_pieces()
			
			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
			
			has_dealt_damage = true # Mark as dealt damage - can't damage anything else now
			
			# Hit a player! Break locally and sync to others (only if destroyable)
			if is_destroyable:
				# Sync break to OTHER clients via RPC BEFORE breaking locally
				# Use game_world to route the RPC to avoid node path resolution issues
				if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
					var game_world = get_tree().get_first_node_in_group("game_world")
					if game_world and game_world.has_method("_rpc_to_ready_peers"):
						game_world._rpc_to_ready_peers("_sync_object_break", [name])
				
				_break_into_pieces()
			
			# Damage and knockback player using RPC (any thrown object deals max 1 damage to other players)
			# Player's take_damage() already handles defense calculation
			# 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 base_damage = 1.0 # Max 1 damage to other players for any thrown object
				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(base_damage, attacker_pos)
					else:
						# Send RPC to remote peer
						collider.rpc_take_damage.rpc_id(player_peer_id, base_damage, attacker_pos)
				else:
					# Fallback: broadcast if we can't get peer_id
					collider.rpc_take_damage.rpc(base_damage, 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
			
			has_dealt_damage = true # Mark as dealt damage - can't damage anything else now
			
			# Hit another box! Break both locally (only if destroyable)
			if is_destroyable:
				# Sync break to OTHER clients via RPC BEFORE breaking locally
				# Use game_world to route the RPC to avoid node path resolution issues
				if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
					var game_world = get_tree().get_first_node_in_group("game_world")
					if game_world and game_world.has_method("_rpc_to_ready_peers"):
						game_world._rpc_to_ready_peers("_sync_object_break", [name])
						# Tell the other box to break too
						if collider.has_method("can_be_destroyed") and collider.can_be_destroyed():
							game_world._rpc_to_ready_peers("_sync_object_break", [collider.name])
				
				_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()
			
			print(name, " hit another box!")
			return

func _break_into_pieces(silent: bool = false):
	# Only break if destroyable
	if not is_destroyable or is_broken:
		return
	is_broken = true
	
	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
	]
	
	if not silent:
		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)
		
		play_destroy_sound()
	self.set_deferred("collision_layer", 0)
	$Shadow.visible = false
	$Sprite2DAbove.visible = false
	$Sprite2D.visible = false
	
	# Spawn item loot when breaking (30% chance)
	if is_multiplayer_authority():
		var drop_chance = randf()
		if drop_chance < 0.3: # 30% chance to drop item
			var item = ItemDatabase.get_random_container_item()
			if item:
				var entities_node = get_parent()
				var game_world = get_tree().get_first_node_in_group("game_world")
				if entities_node and game_world:
					ItemLootHelper.spawn_item_loot(item, global_position, entities_node, game_world)
					print(name, " dropped item: ", item.item_name, " when broken")
	
	if not silent:
		if ($SfxShatter.playing):
			await $SfxShatter.finished
		if ($SfxBreakCrate.playing):
			await $SfxBreakCrate.finished
	# Remove self
	queue_free()

func _is_wall_collider(collider) -> bool:
	if not collider:
		return false
	# TileMapLayer collisions
	if collider is TileMapLayer:
		return true
	# Any CollisionObject2D with wall layer (7) enabled
	if collider is CollisionObject2D and collider.get_collision_layer_value(7):
		return true
	return false

func can_be_grabbed() -> bool:
	return is_grabbable and not is_being_held

func _get_configured_object_type() -> String:
	# Prefer the configured type from dungeon data if available
	var idx = -1
	if name.begins_with("InteractableObject_"):
		var index_str = name.substr(20)
		if index_str.is_valid_int():
			idx = index_str.to_int()
	elif has_meta("object_index"):
		idx = get_meta("object_index")
	
	if idx >= 0:
		var game_world = get_tree().get_first_node_in_group("game_world")
		if game_world and "dungeon_data" in game_world and game_world.dungeon_data.has("interactable_objects"):
			var objects = game_world.dungeon_data.interactable_objects
			if idx < objects.size():
				var obj_data = objects[idx]
				if obj_data is Dictionary and obj_data.has("type"):
					return obj_data.type
	
	return object_type

func can_be_lifted() -> bool:
	# Can be lifted if it's liftable (being held is OK - we're checking if it CAN be lifted)
	var resolved_type = object_type
	if resolved_type == "":
		resolved_type = _get_configured_object_type()
	if resolved_type in ["Box", "Pot", "LiftableBarrel"]:
		return true
	if resolved_type in ["Chest", "Pillar", "PushableBarrel", "PushableHighBox"]:
		return false
	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 _is_wooden_burnable() -> bool:
	var t = object_type if object_type != "" else _get_configured_object_type()
	return t in ["Box", "Pot", "LiftableBarrel", "PushableBarrel", "PushableHighBox"]

func take_fire_damage(amount: float, _attacker_position: Vector2) -> void:
	if not is_destroyable or is_broken or not _is_wooden_burnable():
		return
	health -= amount
	if health > 0:
		return
	var game_world = get_tree().get_first_node_in_group("game_world")
	if multiplayer.has_multiplayer_peer():
		if multiplayer.is_server():
			if game_world and game_world.has_method("_rpc_to_ready_peers"):
				game_world._rpc_to_ready_peers("_sync_object_break", [name])
			_break_into_pieces()
		else:
			if game_world and game_world.has_method("_sync_object_break"):
				game_world._sync_object_break.rpc_id(1, name)
	else:
		_break_into_pieces()

func take_damage(amount: float, _from_position: Vector2) -> void:
	"""Generic damage from bomb, frost spike, etc. Any destroyable object."""
	if not is_destroyable or is_broken:
		return
	health -= amount
	if health > 0:
		return
	var game_world = get_tree().get_first_node_in_group("game_world")
	if multiplayer.has_multiplayer_peer():
		if multiplayer.is_server():
			if game_world and game_world.has_method("_rpc_to_ready_peers"):
				game_world._rpc_to_ready_peers("_sync_object_break", [name])
			_break_into_pieces()
		else:
			if game_world and game_world.has_method("_sync_object_break"):
				game_world._sync_object_break.rpc_id(1, name)
	else:
		_break_into_pieces()

func on_grabbed(by_player):
	# Special handling for chests - open instead of grab
	if object_type == "Chest" and not is_chest_opened:
		# In multiplayer, send RPC to server if client is opening
		if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
			# Client - send request to server
			if by_player:
				var player_peer_id = by_player.get_multiplayer_authority()
				# Use consistent object name based on object_index to avoid NodePath issues
				var chest_name = name
				if has_meta("object_index"):
					chest_name = "InteractableObject_%d" % get_meta("object_index")
				print("Chest: Client sending RPC to open chest, player_peer_id: ", player_peer_id, " chest_name: ", chest_name)
				var game_world = get_tree().get_first_node_in_group("game_world")
				if game_world and game_world.has_method("_request_chest_open_by_name"):
					game_world._request_chest_open_by_name.rpc_id(1, chest_name, player_peer_id)
				else:
					push_warning("Chest: GameWorld not ready, cannot send chest open request for " + chest_name)
		else:
			# Server or single player - open directly
			_open_chest(by_player)
		return # CRITICAL: Return early to prevent normal grab behavior
	
	is_being_held = true
	held_by_player = by_player
	has_dealt_damage = false # Reset damage flag when picked up
	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
	has_dealt_damage = false # Reset damage flag when released
	
	# 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):
	# Special handling for bombs - convert to projectile when thrown
	if object_type == "Bomb":
		_convert_to_bomb_projectile(by_player, force)
		return
	
	# 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
	has_dealt_damage = false # Reset damage flag - this throw can deal damage to ONE target
	
	# 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)

func _convert_to_bomb_projectile(by_player, force: Vector2):
	# Convert bomb object to projectile bomb when thrown
	var attack_bomb_scene = load("res://scenes/attack_bomb.tscn")
	if not attack_bomb_scene:
		push_error("ERROR: Could not load attack_bomb scene!")
		return
	
	# Only authority can spawn bombs
	if not is_multiplayer_authority():
		return
	
	# Store current position before freeing
	var current_pos = global_position
	
	# Spawn bomb projectile at current position
	var bomb = attack_bomb_scene.instantiate()
	bomb.name = "ThrownBomb_" + name
	get_parent().add_child(bomb)
	bomb.global_position = current_pos # Use current position, not target
	
	# Set multiplayer authority
	if multiplayer.has_multiplayer_peer():
		bomb.set_multiplayer_authority(by_player.get_multiplayer_authority())
	
	# Setup bomb with throw physics (pass force as throw_velocity)
	# The bomb will use throw_velocity for movement
	bomb.setup(current_pos, by_player, force, true) # true = is_thrown, force is throw_velocity
	
	# Make sure bomb sprite is visible
	if bomb.has_node("Sprite2D"):
		bomb.get_node("Sprite2D").visible = true
	
	# Sync bomb throw to other clients
	if multiplayer.has_multiplayer_peer() and by_player.is_multiplayer_authority():
		if by_player.has_method("_rpc_to_ready_peers") and by_player.is_inside_tree():
			# Player threw: sync via player RPC (pass our name so they can free the lifted bomb)
			by_player._rpc_to_ready_peers("_sync_throw_bomb", [name, current_pos, force])
		elif by_player.is_in_group("enemy"):
			# Enemy threw: sync via game_world so joiners see the bomb
			var game_world = get_tree().get_first_node_in_group("game_world")
			if game_world and game_world.has_method("_rpc_to_ready_peers"):
				var enemy_index = by_player.get_meta("enemy_index") if by_player.has_meta("enemy_index") else -1
				var bomb_name = "EnemyBomb_" + by_player.name + "_" + str(Time.get_ticks_msec())
				game_world._rpc_to_ready_peers("_sync_enemy_throw_bomb", [bomb_name, by_player.name, enemy_index, current_pos, force])
	
	# Remove the interactable object
	queue_free()
	
	print("Bomb object converted to projectile and thrown!")

@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(silent: bool = false):
	# Sync break to all clients including server (called by whoever breaks the box)
	if not is_queued_for_deletion() and not is_broken:
		_break_into_pieces(silent)

# Object type setup functions
func setup_pot():
	object_type = "Pot"
	is_grabbable = true
	can_be_pushed = true
	is_destroyable = true
	is_liftable = true
	weight = 0.8 # Pots are very light and easy to throw far!
	
	var pot_frames = [1, 2, 3, 20, 21, 22, 58]
	if sprite:
		var box_seed = 0
		var game_world = get_tree().get_first_node_in_group("game_world")
		if game_world and "dungeon_seed" in game_world:
			box_seed = game_world.dungeon_seed
		# Add position and object_index to seed to make each box unique but deterministic
		box_seed += int(global_position.x) * 1000 + int(global_position.y)
		if has_meta("object_index"):
			box_seed += get_meta("object_index") * 10000
		
		var rng = RandomNumberGenerator.new()
		rng.seed = box_seed
		var index = rng.randi() % pot_frames.size()
		sprite.frame = pot_frames[index]

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:
		var box_seed = 0
		var game_world = get_tree().get_first_node_in_group("game_world")
		if game_world and "dungeon_seed" in game_world:
			box_seed = game_world.dungeon_seed
		# Add position and object_index to seed to make each box unique but deterministic
		box_seed += int(global_position.x) * 1000 + int(global_position.y)
		if has_meta("object_index"):
			box_seed += get_meta("object_index") * 10000
		
		var rng = RandomNumberGenerator.new()
		rng.seed = box_seed
		var index = rng.randi() % barrel_frames.size()
		sprite.frame = barrel_frames[index]

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.5 # Boxes are heavier than pots
	
	var box_frames = [7, 26]
	if sprite:
		# Use deterministic randomness based on dungeon seed and position
		# This ensures host and clients get the same box variant
		var box_seed = 0
		var game_world = get_tree().get_first_node_in_group("game_world")
		if game_world and "dungeon_seed" in game_world:
			box_seed = game_world.dungeon_seed
		# Add position and object_index to seed to make each box unique but deterministic
		box_seed += int(global_position.x) * 1000 + int(global_position.y)
		if has_meta("object_index"):
			box_seed += get_meta("object_index") * 10000
		
		var rng = RandomNumberGenerator.new()
		rng.seed = box_seed
		var index = rng.randi() % box_frames.size()
		sprite.frame = box_frames[index]

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]
	
	# Use deterministic randomness based on dungeon seed and position
	# This ensures host and clients get the same chest variant
	var chest_seed = 0
	var game_world = get_tree().get_first_node_in_group("game_world")
	if game_world and "dungeon_seed" in game_world:
		chest_seed = game_world.dungeon_seed
	# Add position to seed to make each chest unique but deterministic
	chest_seed += int(global_position.x) * 1000 + int(global_position.y)
	
	var rng = RandomNumberGenerator.new()
	rng.seed = chest_seed
	var index = rng.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]
	
	# Use deterministic randomness based on dungeon seed and position
	# This ensures host and clients get the same chest variant
	var highbox_seed = 0
	var world = get_tree().get_first_node_in_group("game_world")
	if world and "dungeon_seed" in world:
		highbox_seed = world.dungeon_seed
	# Add position to seed to make each chest unique but deterministic
	highbox_seed += int(global_position.x) * 1000 + int(global_position.y)
	
	var rng = RandomNumberGenerator.new()
	rng.seed = highbox_seed
	var index = rng.randi() % bottom_frames.size()
	
	if sprite:
		sprite.frame = bottom_frames[index]
		if sprite_above:
			sprite_above.frame = top_frames[index]

func setup_bomb():
	object_type = "Bomb"
	is_grabbable = true
	can_be_pushed = false
	is_destroyable = false # Bombs don't break, they explode
	is_liftable = true
	weight = 0.5 # Light weight for easy throwing
	
	# Set bomb sprite (frame 199 from items_n_shit.png)
	if sprite:
		sprite.texture = load("res://assets/gfx/pickups/items_n_shit.png")
		sprite.hframes = 20
		sprite.vframes = 14
		sprite.frame = 199

func _open_chest(by_player: Node = null):
	if is_chest_opened:
		return
	
	# Only process on server (authority)
	if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
		return
	$SfxOpenChest.play()
	is_chest_opened = true
	
	# Track opened chest for syncing to new clients
	if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
		var world = get_tree().get_first_node_in_group("game_world")
		if world and has_meta("object_index"):
			var obj_index = get_meta("object_index")
			world.opened_chests[obj_index] = true
			LogManager.log("Chest: Tracked opened chest with index " + str(obj_index), LogManager.CATEGORY_NETWORK)
	if sprite and chest_opened_frame >= 0:
		sprite.frame = chest_opened_frame
	
	# Get random item from entire item database (using chest rarity weights)
	# Use deterministic randomness based on dungeon seed and chest position
	var chest_seed = 0
	var game_world = get_tree().get_first_node_in_group("game_world")
	if game_world and "dungeon_seed" in game_world:
		chest_seed = game_world.dungeon_seed
	# Add position to seed to make each chest unique but deterministic
	chest_seed += int(global_position.x) * 1000 + int(global_position.y)
	
	# Create deterministic RNG for this chest
	var chest_rng = RandomNumberGenerator.new()
	chest_rng.seed = chest_seed
	
	# Get random item using deterministic RNG
	# We need to manually select by rarity since get_random_chest_item() uses global randi()
	var rarity_roll = chest_rng.randf()
	var rarity: ItemDatabase.ItemRarity
	if rarity_roll < 0.4:
		rarity = ItemDatabase.ItemRarity.COMMON
	elif rarity_roll < 0.75:
		rarity = ItemDatabase.ItemRarity.UNCOMMON
	elif rarity_roll < 0.95:
		rarity = ItemDatabase.ItemRarity.RARE
	else:
		rarity = ItemDatabase.ItemRarity.EPIC if chest_rng.randf() < 0.5 else ItemDatabase.ItemRarity.CONSUMABLE
	
	# Get candidates for this rarity using deterministic RNG
	ItemDatabase._initialize()
	var candidates = []
	# Access static item_definitions directly
	for item_id in ItemDatabase.item_definitions.keys():
		var item_data = ItemDatabase.item_definitions[item_id]
		if item_data.has("rarity") and item_data["rarity"] == rarity:
			candidates.append(item_id)
	
	# Fallback to common if no candidates
	if candidates.is_empty():
		for item_id in ItemDatabase.item_definitions.keys():
			var item_data = ItemDatabase.item_definitions[item_id]
			if item_data.has("rarity") and item_data["rarity"] == ItemDatabase.ItemRarity.COMMON:
				candidates.append(item_id)
	
	# Select random item from candidates using deterministic RNG
	var random_item_id = candidates[chest_rng.randi() % candidates.size()] if not candidates.is_empty() else null
	var chest_item = ItemDatabase.create_item(random_item_id) if random_item_id else null
	
	# CRITICAL: Instantly give item to player instead of spawning loot object
	if by_player and is_instance_valid(by_player) and by_player.is_in_group("player") and chest_item:
		# Add item to player inventory
		if by_player.character_stats:
			var was_encumbered = by_player.character_stats.is_over_encumbered()
			by_player.character_stats.add_item(chest_item)
			if not was_encumbered and by_player.character_stats.is_over_encumbered():
				if by_player.has_method("show_floating_status"):
					by_player.show_floating_status("Encumbered!", Color(1.0, 0.5, 0.2))
			
			# Show pickup notification
			var items_texture = load(chest_item.spritePath) if chest_item.spritePath != "" else null
			var display_text = chest_item.item_name.to_upper()
			var item_color = Color.WHITE
			
			# Determine color based on item type/rarity
			if chest_item.item_type == Item.ItemType.Restoration:
				item_color = Color.GREEN
			elif chest_item.item_type == Item.ItemType.Equippable:
				item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
			else:
				item_color = Color.WHITE
			
			# Show notification with item sprite (pass chest_item for ItemSprite colorization)
			if items_texture:
				_show_item_pickup_notification(by_player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame, chest_item)
			else:
				_show_item_pickup_notification(by_player, display_text, item_color, null, 0, 0, 0, chest_item)
		
		# Play chest open sound
		if has_node("SfxChestOpen"):
			$SfxChestOpen.play()
		
		print(name, " opened by ", by_player.name, "! Item given: ", chest_item.item_name)
		
		# Sync chest opening visual to all clients (item already given on server)
		if multiplayer.has_multiplayer_peer():
			var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0
			# Reuse game_world from earlier in the function
			if game_world and game_world.has_method("_rpc_to_ready_peers"):
				var chest_name = name
				if has_meta("object_index"):
					chest_name = "InteractableObject_%d" % get_meta("object_index")
				# Sync chest open visual with item_data so clients can show the floating text
				var item_data = chest_item.save() if chest_item else {}
				game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data])
			# Sync inventory+equipment to joiner (server added item; joiner's client must apply)
			if multiplayer.is_server():
				var owner_id = by_player.get_multiplayer_authority()
				if owner_id != 1 and owner_id != multiplayer.get_unique_id():
					var inv_data: Array = []
					for inv_item in by_player.character_stats.inventory:
						inv_data.append(inv_item.save() if inv_item else null)
					var equip_data: Dictionary = {}
					for slot_name in by_player.character_stats.equipment.keys():
						var eq = by_player.character_stats.equipment[slot_name]
						equip_data[slot_name] = eq.save() if eq else null
					if by_player.has_method("_apply_inventory_and_equipment_from_server"):
						by_player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
	else:
		push_error("Chest: ERROR - No valid player to give item to!")

@rpc("any_peer", "reliable")
func _request_chest_open(player_peer_id: int):
	# Server receives chest open request from client
	if not multiplayer.is_server():
		return
	
	print("Chest: Server received RPC to open chest, player_peer_id: ", player_peer_id)
	
	if is_chest_opened:
		print("Chest: Chest already opened, ignoring request")
		return
	
	# Find the 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:
		push_error("Chest: ERROR - Could not find player with peer_id ", player_peer_id, " for chest opening!")
		return
	
	print("Chest: Found player ", player.name, " for peer_id ", player_peer_id, ", opening chest")
	# Open chest on server (this will give item to player)
	_open_chest(player)

@rpc("any_peer", "reliable")
func _sync_chest_open(loot_type_str: String = "coin", player_peer_id: int = 0, item_data: Dictionary = {}):
	# Sync chest opening to all clients (only visual - item already given on server)
	if not is_chest_opened and sprite and chest_opened_frame >= 0:
		is_chest_opened = true
		sprite.frame = chest_opened_frame
		
		# Play chest open sound on clients
		if has_node("SfxChestOpen"):
			$SfxChestOpen.play()
	
	# Show pickup notification on client side
	if player_peer_id > 0:
		var players = get_tree().get_nodes_in_group("player")
		var player = null
		for p in players:
			if p.get_multiplayer_authority() == player_peer_id:
				player = p
				break
		
		if player and is_instance_valid(player):
			# If item_data is provided, use it to show item notification
			if not item_data.is_empty():
				var chest_item = Item.new(item_data)
				var items_texture = load(chest_item.spritePath) if chest_item.spritePath != "" else null
				var display_text = chest_item.item_name.to_upper()
				var item_color = Color.WHITE
				
				if chest_item.item_type == Item.ItemType.Restoration:
					item_color = Color.GREEN
				elif chest_item.item_type == Item.ItemType.Equippable:
					item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
				
				if items_texture:
					_show_item_pickup_notification(player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame, chest_item)
				else:
					_show_item_pickup_notification(player, display_text, item_color, null, 0, 0, 0, chest_item)
			else:
				# Fallback to old loot type system (for backwards compatibility)
				match loot_type_str:
					"coin":
						var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
						_show_item_pickup_notification(player, "+1 COIN", Color(1.0, 0.84, 0.0), coin_texture, 6, 1, 0)
					"apple":
						var heal_amount = 20.0
						var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
						_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.GREEN, items_texture, 20, 14, (8 * 20) + 10)
					"banana":
						var heal_amount = 20.0
						var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
						_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.YELLOW, items_texture, 20, 14, (8 * 20) + 11)
					"cherry":
						var heal_amount = 20.0
						var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
						_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.RED, items_texture, 20, 14, (8 * 20) + 12)
					"key":
						var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
						_show_item_pickup_notification(player, "+1 KEY", Color.YELLOW, items_texture, 20, 14, (13 * 20) + 10)

func _show_item_pickup_notification(player: Node, text: String, text_color: Color, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0, item = null):
	# Show item graphic and text above player's head for 0.5s, then fade out over 0.5s
	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)
			# Position at player.position.y - 20 (just above head)
			floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20)
			floating_text.setup(text, text_color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame, item)

func play_destroy_sound():
	if object_type == "Pot":
		$SfxShatter.play()
	else:
		$SfxBreakCrate.play()
	$DragParticles.emitting = true
	$DragParticles/TimerSmokeParticles.start()
	pass

func play_drag_sound():
	if object_type == "Pot":
		if !$SfxDrag.playing:
			$SfxDrag.play()
	else:
		if !$SfxDragRock.playing:
			$SfxDragRock.play()
	$DragParticles.emitting = true
	pass

func stop_drag_sound():
	$SfxDrag.stop()
	$SfxDragRock.stop()
	$DragParticles.emitting = false
	pass


func _on_timer_smoke_particles_timeout() -> void:
	$DragParticles.emitting = false
	pass # Replace with function body.
