class_name Player extends CharacterBody3D enum State { NORMAL, KNOCKDOWN, KNOCKUP, FLUNG, BOUND, DEAD } const trap_template = preload("res://templates/trap.tscn") const remove_trap_modal = preload("res://templates/remove_trap_modal.tscn") const disarm_trap_modal = preload("res://templates/disarm_trap_modal.tscn") @export var speed : float = 10 @onready var body : PawnBody = $PawnBody @onready var data : PlayerData = $Data @onready var trap_sound : AudioStreamPlayer3D = $TrapSound @onready var detonate_sound : AudioStreamPlayer3D = $DetonateSound @onready var detect_sound : AudioStreamPlayer3D = $DetectSound @onready var reload_sound : AudioStreamPlayer3D = $PawnBody/ReloadSound @onready var detect_icon : Sprite3D = $DetectIcon var id : int = 1 var state : State var button_actions : Dictionary[int, String] var current_square : Vector3i var facing : Vector3 var detecting : bool = false var detect_squares : Dictionary[Vector3i, bool] = {} var detect_tween : Tween = null var fling_direction : Vector3 var fling_speed : float var poison_strength : int = 0 var poison_time_remaining : float = 0 var poison_pulse_timer : float var melee_range : float = 3.0 var ranged_range : float = 6 var attack_timer : float = 0 var melee_recovery_time : float = .75 var ranged_recovery_time : float = .2 var ranged_reload_time : float = 1 var projectile_speed : float = 10.0 var projectile_damage : int = 4 var ammo = 5 var max_ammo = 5 var combat_target var meleeing : bool = false var input_locked : bool = false var action_tween : Tween = null var camera : PlayerCamera = null var modal = null signal trap_cycled(trap_index : int) signal trap_quantity_changed(trap_index : int, quantity : int) signal trap_list_changed(traps) signal health_changed(current : int, max : int) signal poison_status_changed(poisoned : bool) signal combat_target_changed(melee : bool) signal ammo_changed(current : int, max : int) func _physics_process(delta: float) -> void: if attack_timer > 0: attack_timer -= delta check_attack_target() if is_poisoned(): poison_time_remaining -= delta poison_pulse_timer += delta if poison_pulse_timer >= 1.0: poison_pulse_timer -= 1.0 hurt(poison_strength) if poison_time_remaining <= 0: var pshader : ShaderMaterial = body.material.next_pass pshader.set_shader_parameter("strength", 0) poison_status_changed.emit(false) var dir = Input.get_vector("west", "east", "north", "south") dir = Vector3(dir.x, 0, dir.y) if input_locked or modal != null: dir = Vector3.ZERO if state == State.FLUNG: dir = fling_direction body.look_at(body.global_position - dir) var down = 0 if !is_on_floor(): down = velocity.y + get_gravity().y * delta velocity = fling_speed * dir #Raycast for a wall var space_state = get_world_3d().direct_space_state # use global coordinates, not local to node var start = global_position + Vector3(0,0.25,0) var end = start + velocity.normalized() * 0.25 + velocity * 2 * delta var query = PhysicsRayQueryParameters3D.create(start, end, 1|2, [self]) var result = space_state.intersect_ray(query) if result: var opp : Player = result.collider as Player if opp: opp.knockdown(fling_direction) opp.hurt(10) knockdown(-fling_direction) hurt(10) else: velocity += Vector3(0, down, 0) move_and_slide() elif dir.length_squared() > 0: facing = dir.normalized() var down = 0 if !is_on_floor(): down = velocity.y + get_gravity().y * delta body.look_at(body.global_position - dir) velocity = speed * dir if is_poisoned(): velocity *= 0.5 if detecting: velocity *= .33 velocity += Vector3(0, down, 0) move_and_slide() elif !is_on_floor(): velocity += get_gravity() * delta move_and_slide() if detecting: update_detecting() if !input_locked: if modal: var evt = InputEventAction.new() var buttons = [ "lay trap", "detect", "attack", "detonate" ] for button in buttons: if Input.is_action_just_pressed(button): evt.action = button evt.pressed = true modal.button_pressed(evt) elif state == State.NORMAL: if Input.is_action_just_pressed("left cycle trap"): cycle_active_trap(-1) if Input.is_action_just_pressed("right cycle trap"): cycle_active_trap(1) if Input.is_action_just_pressed("detonate"): detonate() elif Input.is_action_just_pressed("detect"): start_detecting() elif Input.is_action_just_released("detect"): stop_detecting() elif !detecting and Input.is_action_just_pressed("lay trap"): try_lay_trap() elif Input.is_action_pressed("attack"): attack() func attack() -> void: if attack_timer > 0: return if meleeing: body.play_animation("melee") attack_timer = melee_recovery_time else: if ammo <= 0: reload_sound.play() ammo = max_ammo attack_timer = ranged_reload_time ammo_changed.emit(ammo, max_ammo) return ammo-=1 ammo_changed.emit(ammo, max_ammo) attack_timer = ranged_recovery_time body.play_animation("shoot") if combat_target != null: var v = body.global_position.direction_to(combat_target.global_position) v.y = 0 body.look_at(body.global_position - v) func fire_ranged() -> void: var shot = body.projectile_template.instantiate() var tdir : Vector3 = Vector3.ZERO shot.speed = projectile_speed tdir = body.ranged_point.global_position.direction_to(combat_target.global_position) if combat_target else facing shot.direction = tdir shot.damage = projectile_damage shot.position = body.ranged_point.global_position Game.level.add_projectile(shot) func check_attack_target() -> void: var ranged_closest = null var ranged_d_sq = 99999999 var melee_closest = null var melee_d_sq = 99999999 var ranged_sq = ranged_range * ranged_range var melee_sq = melee_range * melee_range var space_state = get_world_3d().direct_space_state combat_target = null # use global coordinates, not local to node for target : Node3D in get_tree().get_nodes_in_group("combat"): if target == self: continue #Check to see if they're within the correct direction var angle = abs(facing.angle_to(target.global_position - global_position)) if angle > PI / 4.0: continue #Determine if they're within shot range var d_sq = global_position.distance_squared_to(target.global_position) if(d_sq > ranged_sq or(ranged_closest != null and ranged_d_sq <= d_sq)): continue #Raycast to see if they're a valid target var start = global_position + Vector3(0,1,0) var end = target.global_position + Vector3(0,1,0) var query = PhysicsRayQueryParameters3D.create(start, end, 1|2, [self]) var result = space_state.intersect_ray(query) if !result or (result.collider is not Player and result.collider is not CombatTarget): return if d_sq < melee_sq: melee_closest = target melee_d_sq = d_sq else: ranged_closest = target ranged_d_sq = d_sq if melee_closest != null: meleeing = true combat_target = melee_closest else: meleeing = false if ranged_closest != null: combat_target = ranged_closest combat_target_changed.emit(melee_closest != null) func try_lay_trap() -> void: if !is_on_floor(): return if data.traps[data.active_trap].quantity < 1: return var square : Vector3i = (global_position - Vector3.ONE * .5).round() if !Game.level.is_valid_trap_square(square): return action_tween = create_tween() input_locked = true action_tween.tween_interval(.2) action_tween.tween_callback(Callable(lay_trap).bind(square, data.active_trap)) action_tween.tween_interval(.25) action_tween.tween_callback(clear_action) func lay_trap(square : Vector3i, idx : int) -> void: var type : Trap.Type = data.traps[idx].type var trap = trap_template.instantiate() trap.setup(type, facing, id) trap.disarmed.connect(_on_trap_disarmed) trap.activated.connect(_on_trap_activated) data.traps[idx].quantity -= 1 trap_quantity_changed.emit(idx, data.traps[idx].quantity) Game.level.add_trap(trap, square) trap_sound.play() func clear_action() -> void: input_locked = false action_tween = null func update_detecting() -> void: var new_square : Vector3i = (global_position - Vector3.ONE * .5).round() if new_square == current_square: return current_square = new_square var new_squares : Dictionary[Vector3i, bool] = {} for i in range(-2, 3): for j in range(-2, 3): for k in range(-2, 2): if abs(i) + abs(j) < 3: var sq = current_square + Vector3i(i, k, j) new_squares[sq] = true for sq in detect_squares.keys(): if !new_squares.has(sq): Game.level.detect_square(sq, false) var remove_list = [] var trap_detected : bool = false for sq in new_squares.keys(): if detect_squares.has(sq): continue if !Game.level.detect_square(sq, true): remove_list.append(sq) else: var trap = Game.level.get_square_trap(sq + Vector3i(0,1,0)) if trap and trap.is_just_revealed(): trap_detected = true if trap_detected: detect_alert() detect_squares = new_squares for key in remove_list: detect_squares.erase(key) var trap : Trap = Game.level.get_square_trap(current_square) if trap != null: if trap.trap_owner == Multiplayer.id: show_remove_trap_modal() else: trap.disarming = true show_disarm_trap_modal() func detect_alert() -> void: detect_sound.play() detect_icon.visible = true if detect_tween != null: detect_tween.stop() detect_tween = create_tween() detect_tween.tween_interval(.75) detect_tween.tween_property(detect_icon, "visible", false, 0) detect_tween.tween_property(self, "detect_tween", null, 0) func close_modal() -> void: modal.queue_free() modal = null func show_remove_trap_modal() -> void: stop_detecting() modal = remove_trap_modal.instantiate() modal.square = current_square Game.level.add_child(modal) func show_disarm_trap_modal() -> void: stop_detecting() modal = disarm_trap_modal.instantiate() modal.difficulty = Game.level.difficulty modal.square = current_square var trap = Game.level.traps[current_square] trap.disarming = true trap.disarm_id = Multiplayer.id Game.level.add_child(modal) func start_detecting() -> void: detecting = true current_square = (global_position - Vector3.ONE * .5).round() detect_squares = {} for i in range(-2, 3): for j in range(-2, 3): for k in range(-2, 2): if abs(i) + abs(j) < 3: var sq = current_square + Vector3i(i, k, j) detect_squares[sq] = true var remove_list = [] var trap_detected : bool = false for sq in detect_squares.keys(): if !Game.level.detect_square(sq, true): remove_list.append(sq) else: var trap = Game.level.get_square_trap(sq + Vector3i(0,1,0)) if trap and trap.is_just_revealed(): trap_detected = true if trap_detected: detect_alert() for key in remove_list: detect_squares.erase(key) var trap : Trap = Game.level.get_square_trap(current_square) if trap != null: if trap.trap_owner == Multiplayer.id: show_remove_trap_modal() func stop_detecting() -> void: detecting = false for sq in detect_squares.keys(): Game.level.detect_square(sq, false) detect_squares = {} func setup(traps) -> void: $Data.traps = traps Game.setup_player(self) func remove_trap_at(square) -> void: var trap : Trap = Game.level.traps[square] for i in range(len(data.traps)): var d = data.traps[i] if d.type == trap.type: d.quantity += 1 trap_quantity_changed.emit(i, d.quantity) break trap.queue_free() Game.level.traps.erase(square) func cycle_active_trap(dir) -> void: var prev = data.active_trap data.active_trap += dir if data.active_trap < 0: data.active_trap = 0 if data.active_trap >= len(data.traps): data.active_trap = len(data.traps) - 1 if prev != data.active_trap: trap_cycled.emit(data.active_trap) func can_hurt() -> bool: return true func hurt(damage : int) -> void: data.life = max(0, data.life - damage) health_changed.emit(data.life, data.max_life) func _on_trap_disarmed(type : Trap.Type) -> void: for i in range(len(data.traps)): var d = data.traps[i] if d.type == type: d.max -= 1 trap_quantity_changed.emit(i, d.quantity) break func _on_trap_activated(type : Trap.Type) -> void: for i in range(len(data.traps)): var d = data.traps[i] if d.type == type: d.quantity = min(d.max, d.quantity+ 1) trap_quantity_changed.emit(i, d.quantity) break func detonate() -> void: var switch_list = [] for trap : Trap in Game.level.traps.values(): if trap.type == Trap.Type.SWITCH and trap.trap_owner == id: switch_list.append(trap) detonate_sound.play() for trap : Trap in switch_list: trap.activate() func is_poisoned() -> bool: return poison_time_remaining > 0 func poison(damage : int, length : float) -> void: if is_poisoned(): if damage > poison_strength: poison_strength = damage if length > poison_time_remaining: poison_time_remaining = length else: poison_strength = damage poison_time_remaining = length poison_pulse_timer = 0 var pshader : ShaderMaterial = body.material.next_pass pshader.set_shader_parameter("strength", 0.5) poison_status_changed.emit(true) func fling(direction : Vector3, speed : float) -> void: state = State.FLUNG fling_direction = direction fling_speed = speed func knockdown(direction : Vector3) -> void: state = State.KNOCKDOWN input_locked = true body.look_at(Vector3(0,1,0), direction) var knockdown_tween = create_tween() knockdown_tween.tween_interval(1.5) knockdown_tween.tween_property(self, "state", State.NORMAL, 0) knockdown_tween.tween_property(self, "input_locked", false, 0) knockdown_tween.tween_callback(Callable(body.look_at).bind(facing, Vector3(0,1,0))) func knockup(velocity : Vector3) -> void: state = State.KNOCKUP input_locked = true