extends Actor

enum STATES{DEFAULT, BUSY, OBTAIN, EMOTING, FREECAM, FISHING_CHARGE, FISHING_CAST, FISHING, FISHING_CANCEL, FISHING_STRUGGLE, SHOVEL_CAST, SHOVEL_STRUGGLE, SHOVEL_CANCEL, \
NET_CAST, NET_STRUGGLE, NET_CANCEL, SHOWCASE, CONSUME_ITEM, METAL_DETECTOR, GUITAR, GAMBLING}

const GRAVITY = 32.0
const BAIT_DATA = {
	"": {"catch": 0.0, "max_tier": 0, "quality": []}, 
	
	"worms": {"catch": 0.06, "max_tier": 1, "quality": [1.0]}, 
	"cricket": {"catch": 0.06, "max_tier": 2, "quality": [1.0, 0.05]}, 
	"leech": {"catch": 0.06, "max_tier": 2, "quality": [1.0, 0.15, 0.05]}, 
	"minnow": {"catch": 0.06, "max_tier": 2, "quality": [1.0, 0.5, 0.25, 0.05]}, 
	"squid": {"catch": 0.06, "max_tier": 2, "quality": [1.0, 0.8, 0.45, 0.15, 0.05]}, 
	"nautilus": {"catch": 0.06, "max_tier": 2, "quality": [1.0, 0.98, 0.75, 0.55, 0.25, 0.05]}, 
}
const PARTICLE_DATA = {
	"dust_run": preload("res://Scenes/Particles/dust_running.tscn"), 
	"dust_land": preload("res://Scenes/Particles/dust_land.tscn"),
	"splash": preload("res://Scenes/Particles/water_splash.tscn"), 
	"small_splash": preload("res://Scenes/Particles/small_splash.tscn"), 
	"music": preload("res://Scenes/Particles/music_particle.tscn"), 
	"kiss": preload("res://Scenes/Particles/kiss.tscn"), 
}

export (NodePath) var hand_sprite_node
export (NodePath) var hand_bone_node
export  var NPC_body = false
export  var NPC_cosmetics = {"species": "species_cat", "pattern": "pattern_none", "primary_color": "pcolor_white", "secondary_color": "scolor_tan", "hat": "hat_none", "undershirt": "shirt_none", "overshirt": "overshirt_none", "title": "title_rank_1", "bobber": "bobber_default", "eye": "eye_halfclosed", "nose": "nose_cat", "mouth": "mouth_default", "accessory": [], "tail": "tail_cat"}
export  var NPC_name = "NPC Test"
export  var NPC_title = "npc title here"

var camera_zoom = 5.0

var direction = Vector3()
var gravity_vec = Vector3()
var dive_vec = Vector3()
var velocity = Vector3()
var player_scale = 1.0
var player_scale_y = 1.0

var cam_orbit_node = null
var cam_follow_node = null
var cam_push = 0.0
var cam_push_cur = 0.0
var cam_return = 1.0
var force_cam_look = false
var mouse_world_pos = Vector3()
var mouse_look = false

var state = STATES.DEFAULT
var walk_speed = 3.2
var slow_walk_speed = 1.0
var sprint_speed = 6.88
var sneak_speed = 1.3
var jump_height = 7.5
var jump_leap = 0.0
var dive_distance = 15.0
var accel = 64.0
var sprinting = false
var slow_walking = false
var sneaking = false
var diving = false
var request_jump = false
var ignore_snap = 0
var snapped = false
var sitting = false
var locked = false
var busy = false
var in_air = false
var gravity_disable = false
var hud
var int_text = ""
var interact_cooldown = 60
var xp_buildup = 0
var showcase_ref

var held_item = PlayerData.FALLBACK_ITEM
var caught_item = PlayerData.FALLBACK_ITEM
var held_item_weight = 1.0
var previous_item = 0
var primary_hold_timer = 0
var fish_zone_data = {}
var rod_cast_data = ""
var casted_bait = ""
var rod_cast_dist = 0.0
var rod_depth = 0
var failed_casts = 0
var recent_reel = 0.0
var item_scene = null
var bobber_hpos = Vector3()
var bobber_vpos = 0
var bobber_control = true
var last_valid_pos = Vector3()
var retract_splash = false
var show_local = false
var bait_warn = 2
var in_rain = false
var consume_on_state_change = - 1
var item_cooldown = 0

var rod_damage = 1.0
var rod_spd = 1.0
var rod_chance = 0.0



var boost_timer = 0
var boost_amt = 1.3
var boost_mult = 1.0
var catch_drink_timer = 0
var catch_drink_boost = 1.0
var catch_drink_reel = 1.0
var catch_drink_xp = 1.0
var catch_drink_gold_add = Vector2(0, 0)
var catch_drink_gold_percent = 0.0
var catch_drink_tier = 0
var drunk_timer = 0
var drunk_tier = 0
var drunk_wander_length = 0
var drunk_wander_dir = 0

var death_counter = 0
var metal_detect_flop = false
var metal_detect_alert_level = 0
var metal_detect_alert_cd = 0

var emoting = false
var emote_full = false
var emote_locked = false
var emote_looping = false
var animation_timer = 0
var animation_goal = 99
var buffer_state = - 1
var animation_data = {
	"moving": false, 
	"sprinting": false, 
	"sneaking": false, 
	"emoting": false, 
	"emote_full": false, 
	"diving": false, 
	"sitting": false, 
	"alert": false, 
	"emote": "", 
	"arm_state": "", 
	"caught_item": {}, 
	"arm_value": 0.0, 
	"item_bend": 0.0, 
	"busy": false, 
	"land": 0.0, 
	"talking": 0.0, 
	"recent_reel": 0.0, 
	"bobber_position": Vector3(), 
	"bobber_visible": false, 
	"caught_fish": false, 
	"player_scale": 1.0, 
	"player_scale_y": 1.0, 
	"mushroom": false, 
	"run_mult": 1.0, 
	"walk_mult": 1.25, 
	"drunk_tier": 0, 
	"wagging": false, 
	"emote_timescale": 1.0, 
	"back_bend": 0.0, 
	"dive_scrape": false, 
	"reel_slow": false, 
	"reel_fast": false, 
	"state": state, 
}



var custom_held_item = ""

var cosmetic_data = {}
var selected_build_object
var old_rot = Vector3()
var rot_diff = 0.0

var prop_ids = []
var cam_move = false
var freecamming = false

onready var cam_base = $cam_base
onready var cam_pivot = $cam_base / cam_pivot
onready var camera = $Camera
onready var camera_point = $cam_base / cam_pivot / SpringArm / camera_point
onready var cam_arm = $cam_base / cam_pivot / SpringArm
onready var body = $body
onready var body_mesh = $body / player_body / Armature / Skeleton / body_main
onready var rot_help = $rot_help
onready var interact_range = $interact_range
onready var anim_tree = $body / AnimationTree
onready var skeleton = $body / player_body / Armature / Skeleton
onready var title = $Viewport / player_label
onready var item_sprite = get_node(hand_sprite_node)
onready var hand_bone = get_node(hand_bone_node)
onready var sound_emit = $sound_emit
onready var face = $body / player_body / Armature / Skeleton / face / player_face
onready var tail = $body / player_body / Armature / Skeleton / tail / holder / tail
onready var freecam_anchor = $camera_freecam_anchor
onready var sound_manager = $sound_manager

onready var fish_detect = $detection_zones / fishing_detect
onready var fishing_update = $detection_zones / fishing_update
onready var fishing_area = $detection_zones / fishing_detect / fishing_area
onready var fish_timer = $fish_catch_timer
onready var bobber_preview = $bobber_preview
onready var bobber = $bobber
onready var bobber_mesh = $bobber / bobber_mesh
onready var bobber_line = $bobber / line
onready var ripples = $bobber / ripples
onready var caught_fish = $bobber / caught_item

onready var shovel_area = $detection_zones / shovel_detect
onready var net_area = $detection_zones / net_detect

onready var safe_check = $safe_check

onready var lvlparticle = $emotion_particles / lvl_particles
onready var lvlparticleb = $emotion_particles / lvl_particles2

signal _animation_finished
signal _primary_release
signal _state_change
signal _menu_closed





func _ready():
	add_to_group("player")
	rot_help.set_as_toplevel(true)
	cam_base.set_as_toplevel(true)
	camera.set_as_toplevel(true)
	freecam_anchor.set_as_toplevel(true)
	anim_tree.tree_root = anim_tree.tree_root.duplicate(true)
	_update_cosmetics(cosmetic_data)
	title.visible = not dead_actor
	
	if NPC_body:
		camera.queue_free()
		remove_from_group("player")
		
		yield (get_tree(), "idle_frame")
		var new = NPC_cosmetics
		Network._send_actor_action(actor_id, "_update_cosmetics", [new])
		_update_cosmetics(new)
		
		yield (get_tree(), "idle_frame")
		title.visible = true
		title.label = NPC_name
		title.title = NPC_title
	

func _setup_controlled():
	if NPC_body: return 
	
	add_to_group("controlled_player")
	camera = $Camera
	camera.current = true
	hud = load("res://Scenes/HUD/playerhud.tscn").instance()
	hud.player = self
	hud.connect("_player_sit", self, "_toggle_sit")
	hud.connect("_play_emote", self, "_play_emote")
	hud.connect("_menu_entered", self, "_hud_menu_entered")
	hud.connect("_message_sent", self, "_message_sent")
	hud.connect("_freecam_toggle", self, "_toggle_freecam")
	get_tree().get_root().add_child(hud)
	
	PlayerData.connect("_clear_all_props", self, "_clear_all_props")
	PlayerData.connect("_place_prop", self, "_create_prop")
	PlayerData.connect("_play_sfx", self, "_play_sfx")
	PlayerData.connect("_play_guitar", self, "_strum_guitar")
	PlayerData.connect("_hammer_guitar", self, "_hammer_string")
	PlayerData.connect("_return_to_spawn", self, "_return_to_spawn")
	PlayerData.connect("_punched", self, "_punched")
	PlayerData.connect("_item_removal", self, "_item_removal")
	var delayed_update = get_tree().create_timer(1.0).connect("timeout", self, "_change_cosmetics")
	
	PlayerData.connect("_wag_toggle", self, "_wag")
	PlayerData.connect("_kiss", self, "_kiss")
	Network.connect("_user_connected", self, "_refresh_cosmetics")
	

func _setup_not_controlled():
	camera = $Camera
	camera.queue_free()
	
	$bobber_preview.visible = false
	$local_range.visible = false
	$detection_zones / metal_detect_consume.monitoring = false
	$detection_zones / metal_detect_consume.monitorable = false
	
	
	$detection_zones / prop_detect.monitoring = false
	$detection_zones / metal_detect_far.monitoring = false
	$detection_zones / metal_detect.monitoring = false
	$detection_zones / metal_detect_close.monitoring = false
	$detection_zones / metal_detect_veryclose.monitoring = false
	$detection_zones / punch.monitoring = false
	$interact_range.monitoring = false
	$water_detect.monitoring = false
	$raincloud_check.monitoring = false





func _physics_process(delta):
	_process_animation()
	_process_sounds()
	
	if ( not in_zone != $CollisionShape.disabled):
		$CollisionShape.disabled = not in_zone

func _process(delta):
	if controlled: $paint_node._paint_process(delta)

func _controlled_process(delta):
	_get_input()
	_process_movement(delta)
	_process_timers()
	_interact_check()
	_update_animation_data()
	_camera_update()
	
	_freecam_input(delta)
	
	body.visible = camera_zoom > 0.5 or cam_orbit_node != null
	current_zone = world.active_zone
	current_zone_owner = world.active_zone_owner
	
	fish_detect.translation.z = - rod_cast_dist
	bobber_preview.translation.z = - clamp(primary_hold_timer * 0.06, 1.5, 9.0)
	if bobber_preview.is_colliding(): $"%bobber_prev_mesh".global_transform.origin = bobber_preview.get_collision_point() + Vector3(0, 0.05, 0)
	bobber_preview.visible = state == STATES.FISHING_CHARGE
	
	if recent_reel > 0: recent_reel -= 1
	if interact_cooldown > 0: interact_cooldown -= 1
	
	animation_data["reel_slow"] = recent_reel > 8
	animation_data["reel_fast"] = state == STATES.FISHING_STRUGGLE
	
	
	if (packet_send_interval == - 1 or Engine.get_physics_frames() % packet_send_interval == 0):
		Network._send_actor_animation_update(actor_id, animation_data)
	
	Network.MESSAGE_ORIGIN = global_transform.origin
	$local_range.visible = show_local
	
	$paint_node.global_transform.origin = mouse_world_pos
	$metaldetect_dot.modulate.a = lerp($metaldetect_dot.modulate.a, 0.0, 0.1)


func _camera_update():
	cam_push_cur = lerp(cam_push_cur, cam_push * rod_cast_dist, 0.2)
	var push = global_transform.basis.z * cam_push_cur
	camera_zoom = clamp(camera_zoom, 0.0, 20.0)
	
	var cam_zoom = camera_zoom
	var cam_zoom_lerp = 0.4
	var cam_follow_point = true
	var cam_follow_pos = Vector3()
	var cam_follow_rot = Vector3()
	var sit_add = Vector3(0, - 0.2, 0.0) if sitting else Vector3.ZERO
	var cam_base_pos = global_transform.origin + push + sit_add
	
	var desired_fov = 50
	if sprinting and direction != Vector3.ZERO:
		desired_fov += 2
		if boost_timer > 0: desired_fov += 2 * boost_amt
	if animation_data["mushroom"]: desired_fov += 10
	
	camera.fov = lerp(camera.fov, desired_fov, 0.2)
	
	if is_instance_valid(cam_orbit_node) and cam_orbit_node.is_visible_in_tree():
		cam_base_pos = cam_orbit_node.global_transform.origin
		
	if is_instance_valid(cam_follow_node) and cam_follow_node.is_visible_in_tree():
		cam_follow_point = false
		cam_follow_pos = cam_follow_node.global_transform.origin
		cam_follow_rot = cam_follow_node.global_rotation
	
	var cam_speed = 0.08
	if cam_follow_point:
		cam_return = lerp(cam_return, 1.0, cam_speed * 0.5)
		camera.global_transform.origin = lerp(camera.global_transform.origin, camera_point.global_transform.origin, cam_return)
		camera.rotation.x = lerp_angle(camera.rotation.x, camera_point.global_rotation.x, cam_return)
		camera.rotation.y = lerp_angle(camera.rotation.y, camera_point.global_rotation.y, cam_return)
		camera.rotation.z = lerp_angle(camera.rotation.z, camera_point.global_rotation.z, cam_return)
	else :
		cam_return = 0.0
		camera.global_transform.origin = lerp(camera.global_transform.origin, cam_follow_pos, cam_speed)
		camera.rotation.x = lerp_angle(camera.rotation.x, cam_follow_rot.x, cam_speed)
		camera.rotation.y = lerp_angle(camera.rotation.y, cam_follow_rot.y, cam_speed)
		camera.rotation.z = lerp_angle(camera.rotation.z, cam_follow_rot.z, cam_speed)
	
	cam_base.global_transform.origin = cam_base_pos
	cam_arm.spring_length = lerp(cam_arm.spring_length, cam_zoom, cam_zoom_lerp)

func _interact_check():
	if not controlled: return 
	var in_range = false
	for area in interact_range.get_overlapping_areas():
		if area.is_in_group("interactable") and area.is_visible_in_tree():
			int_text = area.text
			in_range = true
			break
	if hud:
		hud.interact = in_range
		hud.int_text = int_text

func _hud_menu_entered(menu):
	if menu == 0:
		if not freecamming:
			cam_orbit_node = null
		cam_follow_node = null
		force_cam_look = false
		_exit_showcase()
		emit_signal("_menu_closed")

func _process_timers():
	if catch_drink_timer > 0:
		catch_drink_timer -= 1
		if catch_drink_timer <= 0:
			catch_drink_boost = 1.0
			catch_drink_reel = 1.0
			catch_drink_xp = 1.0
			catch_drink_gold_add = Vector2(0, 0)
			catch_drink_gold_percent = 0.0
	
	drunk_tier = 0
	if drunk_timer > 0:
		drunk_timer -= 1
		if drunk_timer >= 19000: drunk_tier = 3
		elif drunk_timer >= 10000: drunk_tier = 2
		elif drunk_timer > 0: drunk_tier = 1
		else : drunk_tier = 0
	
	if item_cooldown > 0: item_cooldown -= 1





func _get_input():
	direction = Vector3.ZERO
	
	if Input.is_action_just_released("primary_action"): _primary_action_release()
	if Input.is_action_pressed("primary_action"): _primary_action_hold()
	else : primary_hold_timer = 0
	
	if busy:
		Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
		return 
	
	if Input.is_action_pressed("secondary_action") or camera_zoom <= 0.0:
		if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
			PlayerData.original_mouse_position = get_tree().get_nodes_in_group("world_viewport")[0].get_mouse_position()
			Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	else :
		if Input.mouse_mode != Input.MOUSE_MODE_VISIBLE:
			Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
			Input.warp_mouse_position(PlayerData.original_mouse_position)
	
	if Input.is_action_just_released("zoom_in"): camera_zoom -= 0.5
	if Input.is_action_just_released("zoom_out"): camera_zoom += 0.5
	
	if Input.is_action_just_pressed("interact"): _interact()
	
	if Input.is_action_just_pressed("bind_1"): _equip_hotbar(0)
	if Input.is_action_just_pressed("bind_2"): _equip_hotbar(1)
	if Input.is_action_just_pressed("bind_3"): _equip_hotbar(2)
	if Input.is_action_just_pressed("bind_4"): _equip_hotbar(3)
	if Input.is_action_just_pressed("bind_5"): _equip_hotbar(4)
	
	if Input.is_action_just_pressed("bark"):
		_bark()
	if Input.is_action_just_pressed("kiss"):
		_kiss()
	
	if locked: return 
	
	
	if is_instance_valid(camera):
		var camera_cam = camera
		var ray_length = 1000
		var mouse_pos = get_tree().get_nodes_in_group("world_viewport")[0].get_mouse_position() / Globals.pixelize_amount
		var from = camera_cam.project_ray_origin(mouse_pos)
		var to = from + camera_cam.project_ray_normal(mouse_pos) * ray_length
		var space_state = get_world().get_direct_space_state()
		var result = space_state.intersect_ray(from, to, [])
		if result.has("position"):
			mouse_world_pos = result["position"]
			mouse_world_pos.y = global_transform.origin.y
	
	if freecamming: return 
	if Input.is_action_just_pressed("move_jump"): request_jump = true
	
	mouse_look = false
	
	if sitting: return 
	
	if Input.is_action_pressed("move_forward"): direction -= cam_base.transform.basis.z
	if Input.is_action_pressed("move_back"): direction += cam_base.transform.basis.z
	if Input.is_action_pressed("move_right"): direction += cam_base.transform.basis.x
	if Input.is_action_pressed("move_left"): direction -= cam_base.transform.basis.x
	
	if drunk_wander_length > 0:
		direction += drunk_wander_dir
		drunk_wander_length -= 1
	
	mouse_look = Input.is_action_pressed("mouse_look")
	
	sprinting = not Input.is_action_pressed("move_sneak") and Input.is_action_pressed("move_sprint")
	sneaking = Input.is_action_pressed("move_sneak") and not Input.is_action_pressed("move_sprint")
	slow_walking = Input.is_action_pressed("move_walk")
	
	if emote_locked:
		request_jump = false
		direction = Vector3.ZERO

func _input(event):
	if not controlled: return 
	
	var mouse_sens = OptionsMenu.mouse_sens
	var invert = Vector2(1, 1)
	if OptionsMenu.mouse_invert == 1: invert.x = - 1
	elif OptionsMenu.mouse_invert == 2: invert.y = - 1
	elif OptionsMenu.mouse_invert == 3: invert = Vector2( - 1, - 1)
	
	if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
		cam_base.rotation_degrees.y -= event.relative.x * mouse_sens * invert.x
		cam_pivot.rotation_degrees.x -= event.relative.y * mouse_sens * invert.y
		cam_pivot.rotation_degrees.x = clamp(cam_pivot.rotation_degrees.x, - 80, 80)

func _unhandled_input(event):
	if not controlled: return 
	
	if event.is_action_pressed("primary_action"): _primary_action()




func _process_movement(delta):
	var snap_vec = Vector3(0, - 0.4, 0)
	var y_slow = 0.02
	
	if is_on_floor() and in_air:
		if gravity_vec.y <= - 12.0 and not diving:
			diving = true
			dive_vec = gravity_vec.length() * - transform.basis.z * 0.2
		player_scale_y = 1.0 - clamp(((abs(gravity_vec.y) / 24.0) * 6.0), 0.0, 0.6)
		in_air = false
		_sync_particle("dust_land", Vector3(0, - 1, 0))
		_sync_sfx("land")
		animation_data["land"] = 0.3
	elif gravity_vec.y < - 6.0:
		in_air = true
	
	player_scale_y = lerp(player_scale_y, 1.0, 0.35)
	
	if is_on_floor() and ignore_snap <= 0:
		y_slow = 0.2
		gravity_vec = get_floor_normal() * - 1.0
		snapped = true
	elif snapped and ignore_snap <= 0:
		gravity_vec = Vector3.ZERO
		snapped = false
	else :
		snap_vec = Vector3.ZERO
		var grav = GRAVITY * Vector3.DOWN * delta
		gravity_vec += grav
	
	if request_jump:
		request_jump = false
		snap_vec = Vector3.ZERO
		if is_on_floor():
			_sync_sfx("jump")
			snapped = false
			diving = false
			gravity_vec = Vector3(0, jump_height, 0)
			if sprinting and jump_leap > 0.0:
				gravity_vec += - transform.basis.z.normalized() * jump_leap
		elif not diving:
			_sync_sfx("dive_woosh")
			snapped = false
			diving = true
			dive_vec = - transform.basis.z.normalized() * dive_distance
			dive_vec.y = 0
			gravity_vec += Vector3(0, jump_height * 1, 0)
		_toggle_sit(true)
	
	if ignore_snap > 0:
		snap_vec = Vector3.ZERO
		ignore_snap -= 1
		snapped = false
	
	boost_mult = 1.0
	if boost_timer > 0:
		boost_timer -= 1
		boost_mult = boost_amt
	
	var _speed = walk_speed
	if sprinting: _speed = sprint_speed * boost_mult
	elif sneaking: _speed = sneak_speed
	elif slow_walking: _speed = slow_walk_speed
	var _accel = accel if is_on_floor() else accel * 0.35
	
	var speed_mult = 1.35
	
	
	speed_mult = clamp(speed_mult - ((held_item_weight / 25.0) * 0.05), 0.15, 1.0)
	
	if diving: speed_mult = 1.5
	if gravity_disable: gravity_vec = Vector3.ZERO
	
	velocity = velocity.move_toward(direction.normalized() * _speed * speed_mult, delta * _accel)
	move_and_slide_with_snap(velocity + gravity_vec + dive_vec, snap_vec, Vector3.UP)
	
	dive_vec = dive_vec.move_toward(Vector3.ZERO, delta * _accel * y_slow)
	if not diving: dive_vec = Vector3.ZERO
	
	rot_help.global_transform.origin = global_transform.origin
	
	if direction != Vector3.ZERO:
		var dir = direction
		if diving and dive_vec != Vector3.ZERO: dir = dive_vec.normalized()
		
		rot_help.look_at(cam_base.global_transform.origin + dir, Vector3.UP)
		rotation.y = lerp_angle(rotation.y, rot_help.rotation.y, 0.2)
	elif force_cam_look:
		rot_help.look_at(camera.global_transform.origin, Vector3.UP)
		rotation.y = lerp_angle(rotation.y, rot_help.rotation.y, 0.2)
	elif mouse_look and not busy:
		rot_help.look_at(mouse_world_pos, Vector3.UP)
		rot_diff = abs(rot_help.rotation.y - rotation.y)
		rotation.y = lerp_angle(rotation.y, rot_help.rotation.y, 0.2)
	
	rot_diff = lerp(rot_diff, 0.0, 0.5)
	
	if diving and dive_vec.length() > 0.4 and is_on_floor():
		
		animation_data["dive_scrape"] = true
		
		if Engine.get_idle_frames() % 4 == 0:
			_sync_particle("dust_run", Vector3(0, - 0.8, 0))
	else :
		
		animation_data["dive_scrape"] = false
	
	if gravity_vec.y < - 250:
		_kill()




func _enter_state(new_state):
	if new_state == - 1: return 
	
	if consume_on_state_change != - 1:
		PlayerData._remove_item(consume_on_state_change, false)
		consume_on_state_change = - 1
	
	state = new_state
	emit_signal("_state_change")
	cam_push = 0.0
	animation_data["bobber_visible"] = false
	
	match state:
		STATES.DEFAULT:
			animation_data["item_bend"] = 0.0
			locked = false
		STATES.BUSY: locked = false
		STATES.OBTAIN: locked = true
		STATES.SHOWCASE:
			locked = true
			_equip_item(PlayerData._find_item_code(showcase_ref), true, true)
		
		STATES.FISHING:
			cam_push = - 0.3
			locked = true
			animation_data["bobber_visible"] = true
			PlayerData.emit_signal("_help_update", "hold to reel (SPRINT reels quicker)")
			_enter_animation("rod_idle", true)
		STATES.FISHING_CANCEL:
			animation_data["item_bend"] = 0.1
			cam_push = - 0.3
			locked = true
			rod_cast_dist = 0.0
			animation_data["bobber_visible"] = true
			PlayerData.emit_signal("_item_equip", held_item.ref)
			_bobber_retract()
			_enter_animation("rod_retract", false, true, STATES.DEFAULT)
		STATES.FISHING_CAST:
			cam_push = - 0.3
			animation_data["bobber_visible"] = true
			locked = true
		STATES.FISHING_CHARGE:
			cam_push = 0.0
			locked = false
		STATES.FISHING_STRUGGLE:
			animation_data["item_bend"] = - 0.7
			cam_push = - 0.3
			animation_data["bobber_visible"] = true
			locked = true
			_enter_animation("rod_struggle", true)
		
		STATES.GUITAR: locked = true
		
		STATES.SHOVEL_CAST: locked = true
		STATES.SHOVEL_STRUGGLE:
			locked = true
			_shovel_check()
		STATES.SHOVEL_CANCEL:
			locked = true
			_enter_animation("shovel_retract", false, true, STATES.DEFAULT)
		
		STATES.NET_CAST: locked = true
		STATES.NET_STRUGGLE:
			locked = true
			_net_check()
		STATES.NET_CANCEL:
			locked = true
			_enter_animation("net_retract", false, true, STATES.DEFAULT)
		
		STATES.CONSUME_ITEM:
			locked = false
			_equip_item({"ref": 0}, true, true)
			_enter_state(STATES.DEFAULT)

func _primary_action():
	match state:
		STATES.DEFAULT:
			_use_item()
		STATES.OBTAIN:
			_enter_animation("equip", false, true, 0, false, 1.5)

func _primary_action_hold():
	primary_hold_timer += 1
	
	match state:
		STATES.DEFAULT:
			return 
		STATES.FISHING:
			_enter_animation("rod_reel", true)
			rod_cast_dist -= 0.04 if Input.is_action_pressed("move_sprint") else 0.01
			bobber_control = true
			recent_reel = 15
			if rod_cast_dist < 1.5: _enter_state(STATES.FISHING_CANCEL)

func _primary_action_release():
	emit_signal("_primary_release")
	
	match state:
		STATES.DEFAULT:
			_release_item()
		STATES.FISHING:
			_enter_animation("rod_idle", true)
		STATES.METAL_DETECTOR:
			_enter_state(STATES.DEFAULT)





func _interact():
	if not controlled or interact_cooldown > 0: return 
	for area in interact_range.get_overlapping_areas():
		if area.is_in_group("interactable") and area.is_visible_in_tree():
			area._activate(self)
			interact_cooldown = 60
			return 

func _enter_zone(zone, entrance_id, zone_owner = - 1):
	if not controlled: return 
	world._enter_zone(zone, zone_owner)
	PlayerData.player_saved_zone = zone
	PlayerData.player_saved_zone_owner = zone_owner
	
	print("Finding entrance w id: ", entrance_id, " and owner: ", zone_owner)
	for entrance in get_tree().get_nodes_in_group("area_entrance"):
		print(entrance.entrance_id, ": ", entrance.entrance_owner, " ?")
		if entrance.entrance_id == entrance_id and entrance.entrance_owner == zone_owner:
			global_transform.origin = entrance.global_transform.origin
			last_valid_pos = global_transform.origin
			return 
	
	print("Fallback!")
	global_transform.origin = world.map.spawn_position.global_transform.origin
	world._enter_zone("main_zone", - 1)
	PlayerData.player_saved_zone = "main_zone"
	PlayerData.player_saved_zone_owner = - 1
	last_valid_pos = global_transform.origin

func _obtain_item(ref, bonus_text = [], journal_check = true):
	old_rot = rotation
	showcase_ref = ref
	_equip_item({"ref": - 1}, true, true)
	_enter_animation("equip", false, false, STATES.SHOWCASE, false, 1.5)
	_play_sfx("strum")
	
	
	var data = PlayerData._find_item_code(ref)
	var text = "You caught a " + PlayerData._get_item_name(ref) + "! [color=#d5aa73](It's " + str(data["size"]) + "cm!)[/color]\n\n" + str(Globals.item_data[data["id"]]["file"].catch_blurb)
	
	if journal_check:
		var new = true
		for type in PlayerData.journal_logs.keys():
			for entry in PlayerData.journal_logs[type].keys():
				if entry == data.id and PlayerData.journal_logs[type][entry].count > 1:
					new = false
					break
		if new:
			text = "Woah, a new creature! " + text
	
	hud.dialogue_text = [text]
	if bonus_text != []: hud.dialogue_text.append_array(bonus_text)
	hud._change_menu(6)
	
	force_cam_look = true
	yield (get_tree().create_timer(0.1), "timeout")
	cam_follow_node = $catch_cam_position

func _level_up():
	yield (get_tree().create_timer(0.4), "timeout")
	var bubble = title._create_level_bubble()
	Network._send_actor_action(actor_id, "_sync_level_bubble", [], false)
	GlobalAudio._play_sound("jingle_win")
	
	_play_emote("emote_cheer", "happy")
	
	$emotion_particles / lvl_particles.restart()
	$emotion_particles / lvl_particles2.restart()

func _exit_showcase():
	if state != STATES.SHOWCASE: return 
	_exit_animation()
	_enter_state(STATES.DEFAULT)
	_equip_item(PlayerData._find_item_code(previous_item), false)
	get_tree().create_tween().tween_property(self, "rotation", old_rot, 0.3)
	
	if xp_buildup > 0:
		PlayerData._add_xp(ceil(xp_buildup))
		xp_buildup = 0

func _equip_hotbar(slot):
	if locked or not PlayerData.hotbar.keys().has(slot): return 
	var ref = PlayerData.hotbar[slot]
	_equip_item(PlayerData._find_item_code(ref))

func _equip_item(item_data, skip_anim = false, forced = false, set_prev = true):
	if set_prev and held_item["ref"] != 0: previous_item = held_item["ref"]
	if (state != STATES.DEFAULT and not forced) or held_item["ref"] == item_data["ref"]: return 
	
	if not item_data.keys().has("id") or not Globals.item_data.keys().has(item_data["id"]):
		item_data = PlayerData.FALLBACK_ITEM
	
	PlayerData.emit_signal("_item_equip", item_data["ref"])
	
	if not skip_anim:
		_sync_sfx("equip")
		_enter_state(STATES.BUSY)
		_enter_animation("equip", false, false, STATES.DEFAULT, false, 1.5)
		yield (self, "_animation_finished")

	
	var held_data = item_data.duplicate()
	
	hud.show_bait = Globals.item_data[item_data["id"]]["file"].show_bait
	
	var data = Globals.item_data[item_data["id"]]["file"]
	held_item_weight = item_data["size"]
	if not data.uses_size: held_item_weight = 0.0
	
	_update_held_item(held_data)
	Network._send_actor_action(actor_id, "_update_held_item", [held_item], false)

func _use_item():
	if held_item.empty(): return 
	var item_data = Globals.item_data[held_item["id"]]["file"]
	if has_method(item_data.action) and item_data.action != "":
		callv(item_data.action, item_data.action_params)

func _release_item():
	if held_item.empty(): return 
	var item_data = Globals.item_data[held_item["id"]]["file"]
	if has_method(item_data.release_action) and item_data.release_action != "":
		call(item_data.release_action)







func _cast_fishing_rod():
	rod_cast_data = PlayerData.LURE_DATA[PlayerData.lure_selected].effect_id
	animation_data["caught_item"] = {}
	bait_warn = 1
	
	rod_damage = [1, 3, 10, 20, 35, 50][PlayerData.rod_power_level]
	rod_spd = [0.0, 0.1, 0.24, 0.4, 0.7, 1.0][PlayerData.rod_speed_level]
	rod_chance = [0.0, 0.02, 0.04, 0.06, 0.08, 0.1][PlayerData.rod_chance_level]
	
	_enter_state(STATES.FISHING_CHARGE)
	_enter_animation("rod_wind", true, false, - 1, false)
	yield (self, "_primary_release")
	if state != STATES.FISHING_CHARGE: return 
	_enter_state(STATES.FISHING_CAST)
	_sync_sfx("woosh", null, 1.0, 0.4)
	var strength = clamp(primary_hold_timer * 0.06, 1.5, 9.0)
	rod_cast_dist = strength
	fish_detect.translation.z = - strength
	
	var is_valid_fishing_spot = false
	if bobber_preview.is_colliding() and bobber_preview.get_collider().is_in_group("valid_water"):
		is_valid_fishing_spot = true
	
	_bobber_cast(rod_cast_dist, bobber_preview.get_collision_point() + Vector3(0, 0.75, 0), is_valid_fishing_spot)
	
	rod_depth = 0
	
	casted_bait = PlayerData.bait_selected
	if casted_bait != "" and PlayerData.bait_inv[casted_bait] <= 0: casted_bait = ""
	
	_exit_animation()
	if is_valid_fishing_spot:
		animation_data["item_bend"] = 0.3
		retract_splash = true
		_enter_animation("rod_cast", false, true, STATES.FISHING)
	else :
		animation_data["item_bend"] = 0.3
		retract_splash = false
		_enter_animation("rod_cast", false, true, STATES.FISHING_CANCEL)
	
	yield (get_tree().create_timer(0.4), "timeout")
	animation_data["item_bend"] = - 0.2

func _on_fish_catch_timer_timeout():
	if not controlled: return 
	
	fish_timer.wait_time = rand_range(2.0, 3.0)
	fish_timer.start()
	
	if state != STATES.FISHING: return 
	var fish_type = "ocean"
	var junk_mult = 1.0
	
	
	fish_zone_data = {"id": - 1, "boost": 0.0}
	for zone in fishing_area.get_overlapping_areas():
		if zone.is_in_group("fish_zone"):
			fish_zone_data["id"] = zone.id
			fish_zone_data["boost"] = zone.chance_boost
			junk_mult = zone.junk_mult
			if zone.fish_type != "": fish_type = zone.fish_type
	
	var fish_chance = 0.0
	var base_chance = BAIT_DATA[casted_bait]["catch"]
	fish_chance = base_chance
	fish_chance += (base_chance * failed_casts)
	fish_chance += (base_chance * rod_chance)
	fish_chance += fish_zone_data["boost"] * fish_chance
	if recent_reel > 0: fish_chance *= 1.1
	if rod_cast_data == "attractive": fish_chance *= 1.3
	if in_rain: fish_chance *= 1.1
	
	fish_chance *= catch_drink_boost
	print("Fish chance w ", fish_chance, "w type ", fish_type)
	
	bait_warn -= 1
	if bait_warn <= 0 and fish_chance <= 0.0:
		bait_warn = 8
		var text = ""
		if casted_bait == "":
			text = "[color=#ac0029]You've got no bait attached! You won't catch any fish like that...[/color]"
		else :
			text = "[color=#ac0029]Seems nothing is going to bite... perhaps your bait isn't for this water...[/color]"
		
		Network._update_chat(text)
	
	if randf() > fish_chance:
		failed_casts += 0.05
		return 
	failed_casts = 0.0
	
	
	var bait_use_chance = 1.0
	if rod_cast_data == "efficient": bait_use_chance = 0.8
	
	if randf() < bait_use_chance: PlayerData._use_bait(casted_bait)
	else : PlayerData._send_notification("The Efficient Lure saved your bait!")
	var max_tier = BAIT_DATA[casted_bait]["max_tier"]
	
	var double_bait = 0.0
	if ["large", "sparkling", "double"].has(rod_cast_data): double_bait = 0.25
	if randf() < double_bait:
		PlayerData._use_bait(casted_bait)
		PlayerData._send_notification("Your lure used extra bait...", 1)
	
	if rod_cast_data == "gold":
		for i in 2: PlayerData._use_bait(casted_bait)
		PlayerData._send_notification("Your golden lure used extra bait...", 1)
	
	var treasure_mult = 1.0
	if rod_cast_data == "magnet": treasure_mult = 2.0
	if rod_cast_data == "salty": fish_type = "ocean"
	if rod_cast_data == "fresh": fish_type = "lake"
	
	var force_av_size = false
	
	if randf() < 0.05 * treasure_mult * junk_mult:
		fish_type = "water_trash"
		max_tier = 0
		force_av_size = true
	
	
	
	
	
	
	
	if in_rain and randf() < 0.08:
		fish_type = "rain"
	
	
	var rolls = []
	for i in 3:
		var roll = Globals._roll_loot_table(fish_type, max_tier)
		var s = Globals._roll_item_size(roll)
		rolls.append([roll, s])
	
	
	var reroll_type = "none"
	if rod_cast_data == "small": reroll_type = "small"
	if rod_cast_data == "sparkling": reroll_type = "tier"
	if rod_cast_data == "large": reroll_type = "large"
	if rod_cast_data == "gold": reroll_type = "rare"
	
	var chosen = rolls[0]
	for roll in rolls:
		match reroll_type:
			"none":
				chosen = roll
			
			"small":
				if roll[1] < chosen[1]:
					chosen = roll
			
			"large":
				if roll[1] > chosen[1]:
					chosen = roll
			
			"tier":
				var old_tier = Globals.item_data[chosen[0]]["file"].tier
				var new_tier = Globals.item_data[roll[0]]["file"].tier
				if new_tier > old_tier:
					chosen = roll
			
			"rare":
				var new_rare = Globals.item_data[roll[0]]["file"].rare
				if new_rare:
					chosen = roll
	
	var fish_roll = chosen[0]
	var size = chosen[1]
	
	
	var quality = PlayerData.ITEM_QUALITIES.NORMAL
	var r = randf()
	for q in PlayerData.ITEM_QUALITIES.size():
		if BAIT_DATA[casted_bait]["quality"].size() - 1 < q:
			print("bait does not support rarity ", q)
			break
		if randf() < BAIT_DATA[casted_bait]["quality"][q]:
			quality = q
	print("-------------------------Rolled Quality: ", quality)
	
	if randf() < 0.02 * treasure_mult:
		fish_roll = "treasure_chest"
		size = 60.0
		quality = 0
	
	var data = Globals.item_data[fish_roll]["file"]
	var quality_data = PlayerData.QUALITY_DATA[quality]
	
	if force_av_size: size = data.average_size
	
	var diff_mult = clamp(size / data.average_size, 0.7, 1.8)
	var difficulty = clamp((data.catch_difficulty * diff_mult * quality_data.diff) + quality_data.bdiff, 1.0, 250.0)
	
	var xp_mult = size / data.average_size
	if xp_mult < 0.15: xp_mult = 1.25 + xp_mult
	xp_mult = max(0.5, xp_mult)
	var xp_add = ceil(data.obtain_xp * xp_mult * catch_drink_xp * quality_data.worth)
	
	print("Total Rolls: ", rolls)
	print("Roll fish ", fish_roll, " with size ", size, " and diff Mult: ", diff_mult)
	
	if fish_zone_data["id"] != - 1:
		_wipe_actor(fish_zone_data["id"])
		Network._send_actor_action(actor_id, "_wipe_actor", [fish_zone_data["id"]])
	
	_enter_state(STATES.FISHING_STRUGGLE)
	animation_data["alert"] = true
	yield (get_tree().create_timer(1.0), "timeout")
	
	animation_data["alert"] = false
	
	var delay_time = 0
	while hud.using_chat:
		yield (get_tree().create_timer(0.15), "timeout")
		delay_time += 1
		
		if delay_time > 200:
			_enter_state(STATES.FISHING_CANCEL)
			return 
	
	hud._open_minigame("fishing3", {"fish": fish_roll, "rod_type": rod_cast_data, "reel_mult": catch_drink_reel, "quality": quality, "damage": rod_damage, "speed": rod_spd}, difficulty)
	var success = yield (hud, "_minigame_finished")
	print("SUCCESS: ", success)
	if success:
		animation_data["caught_item"] = {"id": fish_roll, "size": size}
		_enter_state(STATES.FISHING_CANCEL)
		yield (self, "_state_change")
		
		var ref
		var catches = 1
		var bonus_text = []
		
		if rod_cast_data == "double" and randf() < 0.15:
			catches = 2
			bonus_text.append("Your Double Hook doubled the fish!")
		
		if PlayerData.rod_luck_level > 0 and randf() < 0.15:
			bonus_text.append("How lucky! You found a bonus [color=#d57900]Coin Bag[/color] aswell!")
			PlayerData._add_item("luck_moneybag", - 1, randi() % 15 + 15, PlayerData.rod_luck_level)
		
		var tags = []
		
		for i in catches:
			ref = PlayerData._add_item(fish_roll, - 1, size, quality, tags)
			PlayerData._log_item(fish_roll, size, quality)
			PlayerData._quest_progress("catch", fish_roll)
			PlayerData._quest_progress("catch_type", data.loot_table)
			PlayerData._quest_progress("catch_small", PlayerData._get_size_type(fish_roll, size))
			PlayerData._quest_progress("catch_big", PlayerData._get_size_type(fish_roll, size))
			if fish_roll == "treasure_chest": PlayerData._quest_progress("catch_treasure")
			if in_rain: PlayerData._quest_progress("catch_rain")
			if data.tier == 2: PlayerData._quest_progress("catch_hightier")
			xp_buildup += xp_add
			
			PlayerData._catch_fish()
		
		_obtain_item(ref, bonus_text)
		
		if rod_cast_data == "lucky":
			var worth = PlayerData._get_item_worth(ref)
			var gold = max(1, ceil(worth * rand_range(0.01, 0.1)))
			PlayerData.money += gold
			PlayerData._send_notification("Your Lucky Lure got you $" + str(gold) + "!")
		
		if catch_drink_gold_add != Vector2(0, 0):
			var gold = max(1, ceil(rand_range(catch_drink_gold_add.x, catch_drink_gold_add.y)))
			PlayerData.money += gold
			PlayerData._send_notification("Your Catcher's Luck got you $" + str(gold) + "!")
		
		if catch_drink_gold_percent > 0.0:
			var worth = PlayerData._get_item_worth(ref)
			var gold = max(1, ceil(rand_range(worth * 0.01, worth * catch_drink_gold_percent)))
			PlayerData.money += gold
			PlayerData._send_notification("Your Catcher's Luck Ultra got you $" + str(gold) + "!")
		
		
	else :
		_enter_state(STATES.FISHING_CANCEL)

func _wipe_actor(id):
	var actor = world._get_actor_by_id(id)
	print("Wiping actor ", id, " : ", actor)
	if is_instance_valid(actor):
		actor._deinstantiate(true)


func _shovel():
	_enter_state(STATES.SHOVEL_CAST)
	_enter_animation("shovel_dig", false, true, STATES.SHOVEL_STRUGGLE)

func _shovel_check():
	var mound = false
	
	var area
	for a in shovel_area.get_overlapping_areas():
		if a.is_in_group("mound"):
			mound = true
			area = a
	
	if is_instance_valid(area):
		var id = area.owner.actor_id
		_wipe_actor(id)
		Network._send_actor_action(actor_id, "_wipe_actor", [id])
	
	if mound:
		_enter_animation("shovel_struggle", true, true)
		hud._open_minigame("shoveling", {"damage": 5.0})
		var success = yield (hud, "_minigame_finished")
		print("SUCCESS: ", success)
		if success:
			_enter_state(STATES.SHOVEL_CANCEL)
			yield (self, "_state_change")
			var ref = PlayerData._add_item("spider")
			
			
			_obtain_item(ref)
		else :
			_enter_state(STATES.SHOVEL_CANCEL)
	else :
		_enter_state(STATES.SHOVEL_CANCEL)


func _cast_net():
	_enter_state(STATES.NET_CAST)
	_enter_animation("net_use", false, true, STATES.NET_STRUGGLE)

func _net_check():
	var bug = false
	
	var area
	for a in net_area.get_overlapping_areas():
		if a.is_in_group("bug"):
			bug = true
			area = a
	
	var bug_id = ""
	if is_instance_valid(area):
		var id = area.owner.actor_id
		bug_id = area.owner.bug_id
		_wipe_actor(id)
		Network._send_actor_action(actor_id, "_wipe_actor", [id])
	
	if bug:
		_enter_animation("net_struggle", true, true)
		hud._open_minigame("shoveling", {"damage": 5.0})
		var success = yield (hud, "_minigame_finished")
		print("SUCCESS: ", success)
		if success:
			_enter_state(STATES.NET_CANCEL)
			yield (self, "_state_change")
			var size = Globals._roll_item_size(bug_id)
			var ref = PlayerData._add_item(bug_id, - 1, size)
			
			
			_obtain_item(ref)
		else :
			_enter_state(STATES.NET_CANCEL)
	else :
		_enter_state(STATES.NET_CANCEL)














func _toggle_freecam():
	if busy: return 
	
	if not freecamming:
		PlayerData._send_notification("entering freecam mode")
		freecamming = true
		hud.freecamming = true
		cam_orbit_node = freecam_anchor
		freecam_anchor.global_transform.origin = global_transform.origin
	else :
		PlayerData._send_notification("exiting freecam mode")
		freecamming = false
		hud.freecamming = false
		cam_orbit_node = null

func _freecam_input(delta):
	if not freecamming: return 
	
	var speed = 4.0
	if Input.is_action_pressed("move_sprint"):
		speed = 7.0
	if Input.is_action_pressed("move_sneak"):
		speed = 1.0
	var max_dist = 15.0
	var build_dir = Vector3.ZERO
	
	if Input.is_action_pressed("move_forward"): build_dir -= cam_base.transform.basis.z
	if Input.is_action_pressed("move_back"): build_dir += cam_base.transform.basis.z
	if Input.is_action_pressed("move_right"): build_dir += cam_base.transform.basis.x
	if Input.is_action_pressed("move_left"): build_dir -= cam_base.transform.basis.x
	if Input.is_action_pressed("move_up"): build_dir += cam_base.transform.basis.y
	if Input.is_action_pressed("move_down"): build_dir -= cam_base.transform.basis.y
	
	freecam_anchor.global_transform.origin += build_dir * speed * delta
	
	freecam_anchor.global_transform.origin.x = clamp(freecam_anchor.global_transform.origin.x, global_transform.origin.x - max_dist, global_transform.origin.x + max_dist)
	freecam_anchor.global_transform.origin.y = clamp(freecam_anchor.global_transform.origin.y, global_transform.origin.y - max_dist, global_transform.origin.y + max_dist)
	freecam_anchor.global_transform.origin.z = clamp(freecam_anchor.global_transform.origin.z, global_transform.origin.z - max_dist, global_transform.origin.z + max_dist)





func _create_prop(ref, offset = Vector3(0, 1, 0)):
	if not controlled: return 
	
	
	for prop in prop_ids:
		if prop.ref == ref:
			_wipe_actor(prop.id)
			prop_ids.erase(prop)
			PlayerData._send_notification("clearing prop", 1)
			PlayerData.props_placed = prop_ids
			PlayerData.emit_signal("_prop_update")
			return 
	
	
	if $detection_zones / prop_detect.get_overlapping_bodies().size() > 0 or not is_on_floor() or not $detection_zones / prop_ray.is_colliding():
		PlayerData._send_notification("invalid prop placement", 1)
		return 
	
	
	if prop_ids.size() > 4:
		PlayerData._send_notification("prop limit reached", 1)
		return 
	
	
	var item = PlayerData._find_item_code(ref)
	var data = Globals.item_data[item["id"]]["file"]
	var ver_offset = Vector3(0, 1.0, 0) * (1.0 - player_scale)
	print(ver_offset)
	var pos = global_transform.origin + (global_transform.basis.z * - 2.0) - offset + ver_offset
	var rot = rotation + Vector3(0, deg2rad(180), 0)
	var prop_code = data.prop_code
	
	
	var blacklist = ["island_tiny", "island_med", "island_big"]
	if current_zone_owner != - 1:
		if blacklist.has(prop_code):
			PlayerData._send_notification("this prop cannot be spawned here...", 1)
			return 
	
	
	var new_id = Network._sync_create_actor(prop_code, pos, current_zone, - 1, Network.STEAM_ID, {"rotation": rot, "current_zone_owner": current_zone_owner})
	prop_ids.append({"id": new_id, "ref": ref, "prop_id": data.action})
	PlayerData.props_placed = prop_ids
	PlayerData.emit_signal("_prop_update")
	PlayerData._send_notification("prop placed", 0)
	
	_sync_particle("dust_land", pos, true)
	_sync_sfx("poof", pos)

func _clear_props():
	if prop_ids.size() <= 0:
		PlayerData._send_notification("no props to undo", 1)
		return 
	PlayerData._send_notification("undo prop", 0)
	_wipe_actor(prop_ids[prop_ids.size() - 1][0])
	prop_ids.remove(prop_ids.size() - 1)

func _clear_all_props():
	if prop_ids.size() <= 0:
		PlayerData._send_notification("no props to clear", 1)
		return 
	
	PlayerData._send_notification("clearing all props", 0)
	for prop in prop_ids:
		_wipe_actor(prop.id)
	prop_ids.clear()
	PlayerData.props_placed = prop_ids
	PlayerData.emit_signal("_prop_update")

func _item_place_prop():
	if not controlled: return 
	_create_prop(held_item.ref)





func _update_animation_data():
	if animation_data["emote"] != "":
		var anim = $body / player_body / AnimationPlayer.get_animation(animation_data["emote"])
		animation_timer += 1
		animation_goal = floor((anim.length * 60) / animation_data["emote_timescale"])
		if animation_timer >= animation_goal and emoting and animation_goal > 0 and not emote_looping:
			print("Finished Emoting ", animation_data["emote"], " bufferstate: ", buffer_state)
			emit_signal("_animation_finished")
			emoting = false
			emote_full = false
			emote_locked = false
			animation_data["emote"] = ""
			if buffer_state != - 1: _enter_state(buffer_state)
	
	animation_data["emoting"] = emoting
	animation_data["emote_full"] = emoting and emote_full
	animation_data["moving"] = direction != Vector3.ZERO or rot_diff > 0.05
	animation_data["sprinting"] = sprinting and direction != Vector3.ZERO
	animation_data["sneaking"] = sneaking and direction != Vector3.ZERO
	animation_data["diving"] = diving
	animation_data["sitting"] = sitting
	animation_data["busy"] = busy and state == STATES.DEFAULT and not emoting
	animation_data["land"] = lerp(animation_data["land"], 0.0, 0.04)
	animation_data["talking"] = lerp(animation_data["talking"], 0.0, 0.1)
	animation_data["recent_reel"] = lerp(animation_data["recent_reel"], recent_reel, 0.2)
	animation_data["caught_fish"] = state == STATES.FISHING_STRUGGLE
	animation_data["player_scale"] = lerp(animation_data["player_scale"], player_scale, 0.06)
	animation_data["player_scale_y"] = lerp(animation_data["player_scale_y"], player_scale_y, 0.06)
	animation_data["run_mult"] = 1.15 * boost_mult
	animation_data["walk_mult"] = 1.25 if not slow_walking else 0.7
	animation_data["drunk_tier"] = drunk_tier
	animation_data["state"] = state
	
	$emotion_particles / mushroom_trail.emitting = animation_data["mushroom"]
	$emotion_particles / drunk_particles.emitting = animation_data["drunk_tier"] > 1
	if is_on_floor(): animation_data["mushroom"] = false
	
	if not bobber_control:
		animation_data["bobber_position"] = Vector3(bobber_hpos.x, bobber_vpos, bobber_hpos.z)
	else :
		var pos = global_transform.origin + ( - rod_cast_dist * global_transform.basis.z)
		pos.y = bobber_vpos + sin(OS.get_ticks_msec() * 0.002) * 0.05
		animation_data["bobber_position"] = lerp(animation_data["bobber_position"], pos, 0.2)

func _process_animation():
	var anim_lerp = 0.4
	
	var emote_type = 0
	if animation_data["emoting"]: emote_type = 1
	if animation_data["emote_full"]: emote_type = 2
	
	anim_tree.set("parameters/emote_full/blend_amount", lerp(anim_tree.get("parameters/emote_full/blend_amount"), float(emote_type == 2), anim_lerp))
	if anim_tree.get("parameters/emote_full/blend_amount") < 0.1 and emote_type != 2: anim_tree.set("parameters/emote_full/blend_amount", 0.0)
	anim_tree.set("parameters/emote_half/blend_amount", lerp(anim_tree.get("parameters/emote_half/blend_amount"), float(emote_type == 1), anim_lerp))
	if anim_tree.get("parameters/emote_half/blend_amount") < 0.1 and emote_type != 1: anim_tree.set("parameters/emote_half/blend_amount", 0.0)
	
	anim_tree.set("parameters/arms/blend_amount", lerp(anim_tree.get("parameters/arms/blend_amount"), float( not animation_data["emoting"]), anim_lerp))
	anim_tree.set("parameters/movement/blend_amount", lerp(anim_tree.get("parameters/movement/blend_amount"), float(animation_data["moving"]), anim_lerp))
	anim_tree.set("parameters/run_blend/blend_amount", lerp(anim_tree.get("parameters/run_blend/blend_amount"), float(animation_data["sprinting"]) - float(animation_data["sneaking"]), anim_lerp))
	anim_tree.set("parameters/dive/blend_amount", lerp(anim_tree.get("parameters/dive/blend_amount"), float(animation_data["diving"]), anim_lerp))
	anim_tree.set("parameters/arm_blend/blend_position", float(animation_data["arm_value"]))
	anim_tree.set("parameters/sitting/blend_amount", lerp(anim_tree.get("parameters/sitting/blend_amount"), float(animation_data["sitting"]), anim_lerp))
	anim_tree.set("parameters/thinking/blend_amount", lerp(anim_tree.get("parameters/thinking/blend_amount"), float(animation_data["busy"]), anim_lerp))
	anim_tree.set("parameters/land/blend_amount", lerp(anim_tree.get("parameters/land/blend_amount"), float(animation_data["land"]), anim_lerp))
	anim_tree.set("parameters/talking/blend_amount", lerp(anim_tree.get("parameters/talking/blend_amount"), float(animation_data["talking"]), anim_lerp))
	anim_tree.set("parameters/runmodif/scale", animation_data["run_mult"])
	anim_tree.set("parameters/walkmodif/scale", animation_data["walk_mult"])
	anim_tree.set("parameters/emote_time/scale", animation_data["emote_timescale"])
	anim_tree.set("parameters/emote_timeb/scale", animation_data["emote_timescale"])
	
	face._show_blush(animation_data["drunk_tier"] > 1)
	if animation_data["caught_item"] != caught_item:
		caught_item = animation_data["caught_item"]
		_update_caught_item(animation_data["caught_item"])
	
	if item_scene:
		item_scene.item_bend = lerp(item_scene.item_bend, animation_data["item_bend"], 0.4)
		bobber_line.end_anchor = item_scene.get_node(item_scene.hotspot_node).global_transform.origin
	bobber_line.y_drop = - 1.5 + ((animation_data["recent_reel"] / 15.0) * 0.5)
	if animation_data["caught_fish"]: bobber_line.y_drop = 0
	if bobber.visible: bobber.global_transform.origin = animation_data["bobber_position"] + Vector3(0, (0.0 if not animation_data["caught_fish"] else - 0.3), 0)
	bobber.visible = animation_data["bobber_visible"]
	bobber_line.active = bobber.visible
	
	
	if animation_data["player_scale"] != scale.y:
		scale = animation_data["player_scale"] * Vector3.ONE
		scale.y *= animation_data["player_scale_y"]
	
	bobber_line.line_scale = animation_data["player_scale"]
	
	ripples.visible = animation_data["state"] == STATES.FISHING
	var rip_anim = "default" if animation_data["recent_reel"] <= 1.0 else "wake"
	if ripples.animation != rip_anim: ripples.animation = rip_anim
	
	var root = anim_tree.tree_root
	var node = root.get_node("emote_anim")
	var node_b = root.get_node("emote_anim_b")
	if node.animation != animation_data["emote"]:
		anim_tree.set("parameters/emote_sync/seek_position", 0.0)
		anim_tree.set("parameters/emote_half_sync/seek_position", 0.0)
		node.set_animation(animation_data["emote"])
		node_b.set_animation(animation_data["emote"])
	
	$"%head_alert".visible = animation_data["alert"]
	
	tail.sitting = animation_data["sitting"]
	tail.diving = animation_data["diving"]
	tail.motion = float(animation_data["moving"])
	tail.wagging = animation_data["wagging"]
	
	
	if animation_data["back_bend"] != 0.0:
		var pose = skeleton.get_bone_pose(1)
		pose = pose.rotated(Vector3(1, 0, 0), animation_data["back_bend"])
		skeleton.set_bone_custom_pose(1, pose)
	else :
		skeleton.set_bone_custom_pose(1, Transform())

func _enter_animation(anim_name, loop = false, _locked = true, state_enter = - 1, full_anim = true, timescale = 1.0):
	if anim_name == animation_data["emote"]: return 
	diving = false
	
	animation_data["emote_timescale"] = timescale
	
	
	emote_full = full_anim
	emote_locked = _locked
	emote_looping = loop
	buffer_state = state_enter if not emote_looping else - 1
	if emote_looping: _enter_state(state_enter)
	animation_data["emote"] = anim_name
	animation_timer = 0
	emoting = true

func _exit_animation():
	emit_signal("_animation_finished")
	emoting = false
	emote_full = false
	emote_locked = false
	emote_looping = false
	animation_data["emote"] = ""

func _update_held_item(new):
	if new.empty() or held_item == new: return 
	
	held_item = new
	var item_data = Globals.item_data[new["id"]]["file"]
	var alt_scale_mult = 1.0
	
	
	item_sprite.texture = item_data.icon.duplicate() if (item_data.show_item and not item_data.show_scene) else null
	
	
	
	if is_instance_valid(item_scene): item_scene.queue_free()
	item_scene = null
	if item_data.show_scene and item_data.item_scene:
		item_scene = item_data.item_scene.instance()
		hand_bone.add_child(item_scene)
	
	var scale_mult = 0.07 - clamp(new["size"] * 0.01, 0.01, 0.061)
	var item_scale = 1.0 if not item_data.uses_size else new["size"] * scale_mult
	item_scale *= alt_scale_mult
	item_sprite.scale = Vector3(item_scale, item_scale, item_scale)
	
	
	var y = 0.0
	var offs = 0.0
	var back_bend = 0.0
	if item_data.uses_size:
		y = clamp(item_scale * item_scale * 0.08, 0.0, 0.36)
		offs = clamp(item_scale * item_scale, 0.0, 16.0)
		back_bend = clamp(item_scale * item_scale, 0.0, 55.0)
	
	item_sprite.translation.y = y
	item_sprite.offset.y = item_data.hold_offset + offs
	
	
	$body / player_body / Armature / Skeleton / BoneAttachment / Spatial.rotation_degrees = Vector3(0.0, 0.0, back_bend * 0.7)
	
	item_sprite.translation.z = 0.0
	item_sprite.vel = 0.0
	item_sprite.mult = clamp(item_scale * 3.5, 0.0, 1.2) if item_data.category == "fish" and item_data.alive else 0.0
	
	item_sprite.modulate = Color("#ffffff")
	item_sprite.opacity = 1.0
	for child in item_sprite.get_children(): child.emitting = false
	
	if PlayerData.QUALITY_DATA.has(new["quality"]):
		item_sprite.modulate = Color(PlayerData.QUALITY_DATA[new["quality"]]["mod"])
		item_sprite.opacity = PlayerData.QUALITY_DATA[new["quality"]]["op"]
		if PlayerData.QUALITY_DATA[new["quality"]]["particle"] > - 1: item_sprite.get_child(PlayerData.QUALITY_DATA[new["quality"]]["particle"]).emitting = true
	
	animation_data["arm_value"] = item_data.arm_value
	animation_data["back_bend"] = deg2rad( - back_bend)

func _update_caught_item(new):
	if new.empty():
		caught_fish.texture = null
		return 
	caught_item = new
	
	var item_data = Globals.item_data[new["id"]]["file"]
	caught_fish.texture = item_data.icon.duplicate() if item_data.show_item else null
	
	var item_scale = 1.0 if not item_data.uses_size else new["size"] * 0.01
	caught_fish.scale = Vector3(item_scale, item_scale, item_scale)

func _bobber_cast(dist, end, splash):
	bobber_control = false
	var height = dist * 0.4
	height += max(0, global_transform.origin.y - end.y)
	
	end.y -= 1
	
	bobber_hpos = global_transform.origin
	bobber_vpos = global_transform.origin.y
	yield (get_tree().create_timer(0.4), "timeout")
	
	var tween = get_tree().create_tween()
	
	tween.set_trans(1)
	tween.set_ease(1)
	tween.tween_property(self, "bobber_vpos", end.y + height, dist * 0.05)
	tween.set_ease(0)
	tween.tween_property(self, "bobber_vpos", end.y - (height * 0.2), dist * 0.05)
	if splash:
		tween.tween_callback(self, "_bobber_splash")
	
	
	tween.set_trans(1)
	tween.set_ease(1)
	tween.tween_property(self, "bobber_vpos", end.y + (height * 0.1), (dist * 0.05))
	tween.set_ease(0)
	tween.tween_property(self, "bobber_vpos", end.y - (height * 0.05), (dist * 0.1))
	

	tween.set_trans(1)
	tween.set_ease(1)
	tween.tween_property(self, "bobber_vpos", end.y, (dist * 0.1))
	
	var htween = get_tree().create_tween()
	htween.set_trans(4)
	htween.set_ease(1)
	htween.tween_property(self, "bobber_hpos", end + (global_transform.basis.z * 0.5), dist * 0.1)
	htween.tween_property(self, "bobber_hpos", end + (global_transform.basis.z * 0.2), dist * 0.15)
	htween.tween_property(self, "bobber_hpos", end, dist * 0.1)
	
	yield (htween, "finished")
	bobber_control = true

func _bobber_retract():
	bobber_control = false
	bobber_hpos = bobber.global_transform.origin
	bobber_vpos = bobber.global_transform.origin.y
	
	yield (get_tree().create_timer(0.4), "timeout")
	
	if retract_splash: _bobber_splash("splashb")
	var tween = get_tree().create_tween()
	tween.set_trans(1)
	tween.set_ease(1)
	tween.tween_property(self, "bobber_vpos", global_transform.origin.y + 2.0, 0.3)
	tween.set_ease(0)
	tween.tween_property(self, "bobber_vpos", global_transform.origin.y, 0.3)
	
	var htween = get_tree().create_tween()
	htween.tween_property(self, "bobber_hpos", global_transform.origin, 0.6)

func _bobber_splash(sfx = "splash"):
	_sync_particle("splash", bobber.global_transform.origin, true)
	_sync_sfx(sfx, Vector3(bobber_hpos.x, bobber_vpos, bobber_hpos.z))





func _toggle_sit(exit = false):
	if not exit: sitting = not sitting
	else : sitting = false

func _play_emote(emote_id, emotion = ""):
	if emote_id == "": return 
	
	if emote_id == "sit":
		_toggle_sit()
	elif not emote_locked:
		_enter_animation(emote_id, false, true)
		if emotion != "":
			_sync_face_emote(emotion)





func _change_cosmetics():
	if cosmetic_data == PlayerData.cosmetics_equipped: return 
	var new = PlayerData.cosmetics_equipped.duplicate()
	if not dead_actor: Network._send_actor_action(actor_id, "_update_cosmetics", [new])
	_update_cosmetics(new)

func _update_cosmetics(data):
	var valid = true
	for key in PlayerData.FALLBACK_COSM.keys():
		if not data.keys().has(key):
			print("missing key ", key)
			valid = false
	for key in data.keys():
		if not (data[key] is Array):
			if not Globals._cosmetic_exists(data[key]):
				print("cosm ", data[key], " does not exist")
				valid = false
		else :
			for c in data[key]:
				if not Globals._cosmetic_exists(c):
					print("cosm ", data[key], " does not exist")
					valid = false
	
	if not valid: data = PlayerData.FALLBACK_COSM.duplicate()
	
	cosmetic_data = data
	
	for child in skeleton.get_children():
		if child.is_in_group("cosmetic"): child.queue_free()
	
	if data.empty(): return 
	
	title.label = str(Network._get_username_from_id(owner_id))
	title.title = Globals.cosmetic_data[data["title"]]["file"].title
	
	face._setup_face(data)
	var species_id = Globals.cosmetic_data[data.species]["file"].cos_internal_id
	var species = _create_cosmetic(data["species"], species_id)
	_create_cosmetic(data["undershirt"], species_id)
	_create_cosmetic(data["overshirt"], species_id)
	_create_cosmetic(data["legs"], species_id)
	_create_cosmetic(data["hat"], species_id)
	for misc in data["accessory"]: _create_cosmetic(misc, species_id)
	
	
	var pattern = Globals.cosmetic_data[data["pattern"]]["file"]
	body_mesh.material_override.set_shader_param("texture_albedo", pattern.body_pattern[0])
	
	var primary_color = Globals.cosmetic_data[data["primary_color"]]["file"]
	var secondary_color = Globals.cosmetic_data[data["secondary_color"]]["file"] if pattern.body_pattern[0] else Globals.cosmetic_data[data["primary_color"]]["file"]
	body_mesh.material_override.set_shader_param("albedo", primary_color.main_color)
	body_mesh.material_override.set_shader_param("albedo_secondary", secondary_color.main_color)
	
	
	var tail_data = Globals.cosmetic_data[data["tail"]]["file"]
	tail._load_tail(tail_data.mesh, primary_color.main_color)
	
	if species:
		species.material_override.set_shader_param("albedo", primary_color.main_color)
		species.material_override.set_shader_param("albedo_secondary", secondary_color.main_color)
		
		match data["species"]:
			"species_cat": species.material_override.set_shader_param("texture_albedo", pattern.body_pattern[1])
			"species_dog": species.material_override.set_shader_param("texture_albedo", pattern.body_pattern[2])
	
	if not data.keys().has("bobber") or data["bobber"] == "": data["bobber"] = "bobber_default"
	var bobber_file = Globals.cosmetic_data[data["bobber"]]["file"]
	bobber_mesh.mesh = bobber_file.mesh
	bobber_mesh.set_surface_material(0, bobber_file.material)
	if bobber_file.secondary_material: bobber_mesh.set_surface_material(1, bobber_file.secondary_material)
	if bobber_file.third_material: bobber_mesh.set_surface_material(2, bobber_file.third_material)
	
func _create_cosmetic(id, species_id = 0):
	if id == "": return 
	if not Globals.cosmetic_data.keys().has(id):
		print("Failed finding cosmetic: ", id)
		return 
	print("Creating Cosmetic: ", id)
	var data = Globals.cosmetic_data[id]["file"]
	
	if data.scene_replace:
		var bone_attach = BoneAttachment.new()
		skeleton.add_child(bone_attach)
		bone_attach.bone_name = "pelvis"
		bone_attach.add_to_group("cosmetic")
		
		var cosm = data.scene_replace.instance()
		bone_attach.add_child(cosm)
		return cosm
	
	var cosm = preload("res://Scenes/Entities/Player/cosmetic_node.tscn").instance()
	cosm.mesh = data.mesh
	if data.species_alt_mesh.size() > 0:
		cosm.mesh = data.species_alt_mesh[species_id]
	
	cosm.skin = data.mesh_skin
	
	if not data.material:
		cosm.material_override = preload("res://Assets/Shaders/player_skins.tres").duplicate()
		cosm.material_override.set_shader_param("albedo", data.main_color)
		cosm.material_override.set_shader_param("albedo_secondary", data.main_color)
		cosm.material_override.set_shader_param("texture_albedo", null)
	else :
		
		cosm.material_override = null
		cosm.set_surface_material(0, data.material)
		if data.secondary_material: cosm.set_surface_material(1, data.secondary_material)
	
	cosm.skeleton = skeleton.get_path()
	skeleton.add_child(cosm)
	return cosm


















func _on_step_timer_timeout():
	if not controlled: return 
	
	
	if randf() < 0.03 * drunk_tier and drunk_wander_length <= 0:
		drunk_wander_length = randi() % 25 + 5
		drunk_wander_dir = Vector3(
			rand_range( - 1, 1), 
			0.0, 
			rand_range( - 1, 1))
	
	if state == STATES.FISHING_STRUGGLE:
		_sync_particle("small_splash", bobber.global_transform.origin, true)
	
	if recent_reel > 0 and state == STATES.FISHING:
		fishing_update.translation.z = - rod_cast_dist
		fishing_update.force_raycast_update()
		if fishing_update.is_colliding() and not fishing_update.get_collider().is_in_group("valid_water"):
				_enter_state(STATES.FISHING_CANCEL)
	
	if velocity != Vector3.ZERO:
		var vol = 1.6
		if sprinting: vol = 2.8
		elif sneaking: vol = 0.3
		
		if is_on_floor() and sprinting: _sync_particle("dust_run", Vector3(0, - 0.95, 0))
	
	if is_on_floor() and safe_check.is_colliding() and safe_check.get_collision_normal() == Vector3(0, 1, 0) and not gravity_disable:
		if death_counter > 0: death_counter -= 1
		last_valid_pos = global_transform.origin
	
	PlayerData.player_saved_position = global_transform.origin





func _message_sent(text):
	
	var split = text.split(" ")
	for s in split:
		match s:
			":(", ":[", "D:", ";_;", ";~;", ":C", ":c": _sync_face_emote("sad")
			":)", ":D", ":]": _sync_face_emote("love")
			"xD", "!!!", "<3", "xd", "LMAO", "LOL": _sync_face_emote("happy")
			">:(", "D:<", ">:[": _sync_face_emote("angry")
			":/", ":|": _sync_face_emote("flat")
			":3", ":3c", ">:3c", ">:3": _sync_face_emote("cat")
			"O.o", "!?!?", "?!?!", ":o", ":O": _sync_face_emote("surprised")
	
	var bubble = title._create_speech_bubble(text, PlayerData.voice_speed)
	bubble.connect("_letter_said", self, "_sync_talk")
	Network._send_actor_action(actor_id, "_sync_create_bubble", [text], false)

func _sync_talk(letter):
	var blacklist = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
	if not blacklist.has(letter.to_lower()): return 
	
	animation_data["talking"] = 0.4
	_talk(letter, PlayerData.voice_pitch)
	Network._send_actor_action(actor_id, "_talk", [letter, PlayerData.voice_pitch], false)

func _talk(letter, pitch = 1.5):
	if not in_zone or not visible: return 
	
	face._talk()
	sound_manager._construct_voice(letter, "NewVoice", pitch)

func _sync_face_emote(emotion):
	_face_emote(emotion)
	Network._send_actor_action(actor_id, "_face_emote", [emotion], false)

func _sync_create_bubble(text):
	title._create_speech_bubble(text)

func _sync_level_bubble(text):
	title._create_level_bubble(text)
	$emotion_particles / lvl_particles.restart()
	$emotion_particles / lvl_particles2.restart()

func _face_emote(emotion):
	face._emote(emotion, 2.4)





func _sync_particle(id, offset = Vector3.ZERO, global = false):
	_play_particle(id, offset, global)
	Network._send_actor_action(actor_id, "_play_particle", [id, offset, global], false)

func _play_particle(id, offset, global):
	if not PARTICLE_DATA.keys().has(id): return 
	var p = PARTICLE_DATA[id].instance()
	add_child(p)
	
	if not global:
		p.translation = offset
	else :
		p.set_as_toplevel(true)
		p.global_transform.origin = offset
	
	p.emitting = true
	yield (get_tree().create_timer(p.lifetime + 0.1), "timeout")
	p.queue_free()





func _on_water_detect_area_entered(area):
	if gravity_disable: return 
	if area.is_in_group("water"):
		_kill()

func _kill(skip_anim = false):
	if not controlled: return 
	
	death_counter += 1
	if death_counter >= 10:
		if not skip_anim: PlayerData._send_notification("too many deaths in a row! sending to spawn...", 1)
		else : PlayerData._send_notification("returning to spawn", 1)
		
		world._enter_zone("main_zone", - 1)
		PlayerData.player_saved_zone = "main_zone"
		PlayerData.player_saved_zone_owner = - 1
		last_valid_pos = world.map.spawn_position.global_transform.origin
		death_counter = 0
	
	if state == STATES.FISHING or state == STATES.FISHING_CAST or state == STATES.FISHING_CAST:
		_enter_state(STATES.FISHING_CANCEL)
	
	cam_push = 0.0
	gravity_disable = true
	
	if not skip_anim:
		_enter_animation("drown", true, true)
		_sync_sfx("drown")
		_sync_particle("splash", global_transform.origin, true)
	
	SceneTransition._fake_scene_change()
	yield (SceneTransition, "_finished")
	global_transform.origin = last_valid_pos + Vector3(0, 0.5, 0)
	yield (get_tree().create_timer(0.3), "timeout")
	gravity_disable = false
	_enter_state(0)
	_exit_animation()





func _consume_item(id):
	if state != STATES.DEFAULT: return 
	_enter_state(STATES.EMOTING)
	
	consume_on_state_change = held_item["ref"]
	_enter_animation("drink", false, true, STATES.CONSUME_ITEM, true)
	
	var sfx = "drink"
	
	match id:
		"growth": player_scale = clamp(player_scale + 0.1, 0.7, 1.3)
		"shrink": player_scale = clamp(player_scale - 0.1, 0.7, 1.3)
		"revert":
			player_scale = 1.0
			drunk_timer = max(drunk_timer - 9000, 0)
			sfx = "drink_nocap"
		"speed":
			boost_timer = 18000
			boost_amt = 1.3
			sfx = "drink_nocap"
		"speed_burst":
			boost_timer = 900
			boost_amt = 4.0
			sfx = "drink_nocap"
		"catch":
			catch_drink_timer = 18000
			catch_drink_boost = 1.15
			catch_drink_reel = 1.25
			catch_drink_xp = 1.0
			catch_drink_tier = 1
			catch_drink_gold_add = Vector2(1, 10)
			catch_drink_gold_percent = 0.0
		"catch_big":
			catch_drink_timer = 18000
			catch_drink_boost = 1.3
			catch_drink_reel = 1.45
			catch_drink_xp = 1.0
			catch_drink_tier = 2
			catch_drink_gold_add = Vector2(10, 50)
			catch_drink_gold_percent = 0.25
		"catch_deluxe":
			catch_drink_timer = 18000
			catch_drink_boost = 1.3
			catch_drink_reel = 1.45
			catch_drink_xp = 1.25
			catch_drink_tier = 3
			catch_drink_gold_add = Vector2(0, 0)
			catch_drink_gold_percent = 0.0
		"beer":
			drunk_timer += 9000
			drunk_timer = clamp(drunk_timer, 0, 50000)
		"beer_big":
			drunk_timer += 42000
			drunk_timer = clamp(drunk_timer, 0, 50000)
			sfx = "drink_nocap"
	
	_sync_sfx(sfx)
	


func _open_chest(rare = false):
	if state != STATES.DEFAULT: return 
	
	consume_on_state_change = held_item["ref"]
	_enter_state(STATES.CONSUME_ITEM)
	
	var cosm = PlayerData._get_unowned_cosmetic()
	if cosm != null: PlayerData._unlock_cosmetic(cosm)
	else :
		PlayerData._send_notification("You found 100 dollars inside the chest!")
		PlayerData.money += 100

func _open_ringbox():
	if state != STATES.DEFAULT: return 
	
	consume_on_state_change = held_item["ref"]
	_enter_state(STATES.CONSUME_ITEM)
	
	if not PlayerData.cosmetics_unlocked.has("accessory_ring"): PlayerData._unlock_cosmetic("accessory_ring")
	else :
		PlayerData._send_notification("You already have a ring unlocked... Obtained $1000 instead.")
		PlayerData.money += 1000



func _scratch_off(type):
	_enter_state(STATES.GUITAR)
	hud._open_minigame("scratch_off", {"type": type})
	var _win = yield (hud, "_minigame_finished")
	yield (get_tree().create_timer(0.15), "timeout")
	_enter_state(STATES.DEFAULT)
	
	consume_on_state_change = held_item["ref"]
	_enter_state(STATES.CONSUME_ITEM)
	
	if not _win:
		_sync_face_emote("angry")
		_sync_sfx("rip")
	else :
		_sync_face_emote("happy")





func _mushroom_bounce():
	if not controlled: return 
	ignore_snap = 10
	var bounce_horz = 16.0 if direction != Vector3.ZERO else 0.0
	var bounce_vert = 32.0
	gravity_vec = Vector3.ZERO
	gravity_vec.x += (direction.normalized() * bounce_horz).x
	gravity_vec.z += (direction.normalized() * bounce_horz).z
	gravity_vec.y += bounce_vert
	animation_data["mushroom"] = true





func _sync_sfx(id, position = null, pitch = 1.0, delay = 0.0):
	if not controlled: return 
	if delay > 0.0: yield (get_tree().create_timer(delay), "timeout")
	
	_play_sfx(id, position, pitch)
	Network._send_actor_action(actor_id, "_play_sfx", [id, position, pitch], false)

func _play_sfx(id, position = null, pitch = 1.0):
	if not in_zone or not visible: return 
	sound_manager._play_sound(id, position, pitch)
	print("playing sfx ", id)

func _process_sounds():



	
	if $sound_manager / dive_scrape.playing != animation_data["dive_scrape"]: $sound_manager / dive_scrape.playing = animation_data["dive_scrape"] and in_zone
	if $sound_manager / reel_slow.stream_paused == animation_data["reel_slow"]: $sound_manager / reel_slow.stream_paused = not animation_data["reel_slow"] and in_zone
	if $sound_manager / reel_fast.playing != animation_data["reel_fast"]: $sound_manager / reel_fast.playing = animation_data["reel_fast"] and in_zone





func _on_rain_timer_timeout():
	if not controlled: return 
	
	var rain = false
	for child in $raincloud_check.get_overlapping_areas():
		if child.is_in_group("rain_cloud"):
			rain = true
	
	if rain != in_rain:
		in_rain = rain
		PlayerData.emit_signal("_rain_toggle", in_rain)
	
	if not in_rain: Network.set_rich_presence("#default")
	else : Network.set_rich_presence("#rain")





func _wag():
	animation_data["wagging"] = not animation_data["wagging"]

func _paint(size, color):
	if not controlled: return 
	
	var in_zone = false
	for area in $paint_node / Area.get_overlapping_areas():
		if area.is_in_group("canvas"): in_zone = true
	if not in_zone: PlayerData._send_notification("mouse must be on a drawing zone!", 1)
	
	$paint_node.size = size
	$paint_node.color = color
	$paint_node.drawing = true

func _paint_stop():
	if not controlled: return 
	$paint_node.drawing = false
	PlayerData.emit_signal("_chalk_send")

func _metal_detect_begin():
	pass

func _metaldetect_update():
	if not controlled or held_item.id == "":
		return 
	
	var idata = Globals.item_data[held_item["id"]]["file"]
	if not idata.detect_item:
		return 
	
	var alert_level = 0
	for b in $detection_zones / metal_detect_far.get_overlapping_bodies():
		if b.is_in_group("metal_spawn"):
			if abs(b.global_transform.origin.y - global_transform.origin.y) > 3.5: continue
			alert_level = b.global_transform.origin.distance_to(global_transform.origin)
	
	metal_detect_alert_level = alert_level
	
	if alert_level > 0: _metal_detect_beep()

func _metal_detect_beep():
	var new_cd = ceil(max(metal_detect_alert_level * 0.5, 1))
	
	if abs(new_cd - metal_detect_alert_cd) > 4: metal_detect_alert_cd = 0
	
	metal_detect_alert_cd -= 1
	if metal_detect_alert_cd > 0: return 
	
	metal_detect_alert_cd = new_cd
	
	var pitch = max(6.0 - metal_detect_alert_level * 0.3, 0.5)
	
	$metaldetect_dot.modulate.a = 1.0
	if metal_detect_flop: _sync_sfx("md_beep_slowb", null, pitch)
	else : _sync_sfx("md_beep_slow", null, pitch)
	metal_detect_flop = not metal_detect_flop

func _on_metal_detect_consume_body_entered(b):
	if not controlled or held_item.id == "": return 
	
	var idata = Globals.item_data[held_item["id"]]["file"]
	if not idata.detect_item: return 
	
	if b.is_in_group("metal_spawn"):
		b._reveal()

func _on_image_update_timeout():
	if not controlled or dead_actor: return 
	_update_held_item(held_item)
	Network._send_actor_action(actor_id, "_update_held_item", [held_item], false)

func _real_step(run = false):
	if anim_tree.get("parameters/dive/blend_amount") > 0.2 or anim_tree.get("parameters/movement/blend_amount") < 0.8: return 
	if not is_on_floor(): return 
	var sfx = "step" if not run else "step_run"
	if boost_amt > 1.0 and boost_timer > 1: sfx = "step_fastrun"
	_sync_sfx(sfx)



func _play_guitar():
	_enter_state(STATES.GUITAR)
	hud._open_minigame("guitar")
	var _win = yield (hud, "_minigame_finished")
	yield (get_tree().create_timer(0.15), "timeout")
	_enter_state(STATES.DEFAULT)

func _strum_guitar(string, fret, volume):
	_sync_strum(string, fret, volume)
	Network._send_actor_action(actor_id, "_sync_strum", [string, fret, volume])
	
	animation_data["land"] = clamp(animation_data["land"] + 0.06, 0.0, 0.3)
	_sync_face_emote("strum")
	if randf() < 0.05:
		_sync_particle("music")

func _sync_strum(string, fret, volume):
	if not in_zone or not visible: return 
	$guitar_sounds.get_child(string)._play_fret(fret, false, volume)

func _hammer_string(string, fret):
	_sync_hammer(string, fret)
	Network._send_actor_action(actor_id, "_sync_hammer", [string, fret])

func _sync_hammer(string, fret):
	if not in_zone or not visible: return 
	$guitar_sounds.get_child(string)._hammer_fret(fret)

func _bark():
	if not controlled: return 
	var bark_id = "bark_dog"
	bark_id = {
		"species_cat": ["bark_cat", "growl_cat", "whine_cat"], 
		"species_dog": ["bark_dog", "growl_dog", "whine_dog"], 
	}[PlayerData.cosmetics_equipped.species]
	
	var type = 0
	if Input.is_action_pressed("move_sneak"):
		_sync_face_emote("growl")
		type = 1
	elif Input.is_action_pressed("move_walk"):
		_sync_face_emote("whine")
		type = 2
	else :
		_sync_face_emote("bark")
	
	bark_id = bark_id[type]
	_sync_sfx(bark_id)

func _kiss():
	if not controlled: return 
	
	animation_data["land"] = animation_data["land"] + 0.2
	_sync_face_emote("kiss")
	_sync_sfx("kiss")
	_sync_particle("kiss")

func _punch(type = 0):
	if not controlled or item_cooldown > 0: return 
	item_cooldown = 30
	
	animation_data["land"] = animation_data["land"] + 1.0
	for b in $detection_zones / punch.get_overlapping_bodies():
		if b.is_in_group("player") and b != self and not b.controlled:
			Network._send_P2P_Packet({"type": "player_punch", "from": global_transform.origin, "player": owner_id, "punch_type": type}, str(b.owner_id), 2)
	
	_sync_face_emote("punch")
	_sync_sfx("punch")
	_sync_punch()
	Network._send_actor_action(actor_id, "_sync_punch")

func _sync_punch():
	$emotion_particles / punch_particles.restart()
	$emotion_particles / punchb_particles.restart()

func _punched(from, type):
	if not controlled: return 
	
	if OptionsMenu.punchable: return 
	
	var dir = (global_transform.origin - from).normalized()
	
	ignore_snap = 10
	var bounce_horz = 4.0
	var bounce_vert = 8.0
	
	match type:
		0:
			bounce_horz = 4.0
			bounce_vert = 8.0
		1:
			bounce_horz = 12.0
			bounce_vert = 24.0
	
	gravity_vec = Vector3.ZERO
	gravity_vec.x += (dir.normalized() * bounce_horz).x
	gravity_vec.z += (dir.normalized() * bounce_horz).z
	gravity_vec.y += bounce_vert
	
	_sync_face_emote("angry")

func _tambourine():
	_sync_sfx("tambourine")
	
	animation_data["land"] = clamp(animation_data["land"] + 0.14, 0.0, 0.3)
	_sync_face_emote("strum")
	if randf() < 0.25:
		_sync_particle("music")

func _on_cosmetic_refresh_timeout():
	if not controlled: return 
	_refresh_cosmetics()

func _refresh_cosmetics():
	if not controlled: return 
	yield (get_tree().create_timer(1.0), "timeout")
	var new = PlayerData.cosmetics_equipped.duplicate()
	if not dead_actor: Network._send_actor_action(actor_id, "_update_cosmetics", [new])

func _return_to_spawn():
	if not controlled: return 
	death_counter = 11
	_kill(true)

func _item_removal(ref):
	print("Sold Item")
	if held_item.keys().has("ref") and held_item["ref"] == ref: _equip_item(PlayerData.FALLBACK_ITEM.duplicate())
