558 lines
15 KiB
GDScript
558 lines
15 KiB
GDScript
class_name PawnController 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 input : PawnInput = $PawnInput
|
|
@onready var data : PawnLevelData = $Data
|
|
@onready var trap_sound : AudioStreamPlayer3D = $TrapSound
|
|
@onready var detonate_sound : AudioStreamPlayer3D = $DetonateSound
|
|
@onready var detect_sound : AudioStreamPlayer3D = $DetectSound
|
|
@onready var fling_sound : AudioStreamPlayer3D = $FlingSound
|
|
@onready var crash_sound : AudioStreamPlayer3D = $CrashSound
|
|
@onready var reload_sound : AudioStreamPlayer3D = $PawnBody/ReloadSound
|
|
@onready var detect_icon : Sprite3D = $DetectIcon
|
|
|
|
var id : int = 1
|
|
|
|
@export var state : State
|
|
var button_actions : Dictionary[int, String]
|
|
var current_square : Vector3i
|
|
var facing : Vector3
|
|
@export var detecting : bool = false
|
|
var detect_squares : Dictionary[Vector3i, bool] = {}
|
|
var detect_tween : Tween = null
|
|
var fling_direction : Vector3
|
|
var fling_speed : float
|
|
var struggle_timer : float
|
|
var struggle_angle : 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 : PawnCamera = 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 harmed()
|
|
signal poison_status_changed(poisoned : bool)
|
|
signal combat_target_changed(melee : bool)
|
|
signal ammo_changed(current : int, max : int)
|
|
signal struggling(value : float)
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if attack_timer > 0:
|
|
attack_timer -= delta
|
|
check_attack_target()
|
|
|
|
update_poison(delta)
|
|
|
|
if !update_struggle(delta):
|
|
return
|
|
|
|
var dir = input.dir
|
|
|
|
if input_locked or modal != null:
|
|
dir = Vector3.ZERO
|
|
|
|
var can_fall : bool = false
|
|
var moving : bool = false
|
|
match(state):
|
|
State.FLUNG:
|
|
can_fall = true
|
|
moving = true
|
|
dir = fling_direction
|
|
body.look_at(body.global_position - dir)
|
|
|
|
can_fall = true
|
|
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 : PawnController = result.collider as PawnController
|
|
if opp:
|
|
opp.knockdown(fling_direction)
|
|
opp.hurt(10)
|
|
knockdown(-fling_direction)
|
|
hurt(10)
|
|
moving = false
|
|
State.NORMAL:
|
|
can_fall = true
|
|
if dir.length_squared() > 0:
|
|
moving = true
|
|
facing = dir.normalized()
|
|
body.look_at(body.global_position - dir)
|
|
velocity = speed * dir
|
|
if is_poisoned():
|
|
velocity *= 0.5
|
|
if detecting:
|
|
velocity *= .33
|
|
State.KNOCKUP:
|
|
if is_on_floor():
|
|
knockdown(facing)
|
|
else:
|
|
moving = true
|
|
can_fall = true
|
|
State.KNOCKDOWN,State.BOUND,State.DEAD:
|
|
return
|
|
|
|
|
|
if can_fall:
|
|
var down = 0
|
|
if !is_on_floor():
|
|
velocity.y = velocity.y + get_gravity().y * delta
|
|
moving = true
|
|
|
|
if moving:
|
|
move_and_slide()
|
|
|
|
if detecting:
|
|
update_detect_region.rpc(true)
|
|
|
|
#Deal with the rest of the buttons
|
|
if !input_locked:
|
|
if modal:
|
|
var buttons = [
|
|
"lay trap",
|
|
"detect",
|
|
"attack",
|
|
"detonate"
|
|
]
|
|
for button in buttons:
|
|
if input.is_action_just_pressed(button):
|
|
modal.button_pressed(button)
|
|
elif state == State.NORMAL:
|
|
update_actions()
|
|
|
|
func attack() -> void:
|
|
if attack_timer > 0:
|
|
return
|
|
if meleeing:
|
|
body.play_animation.rpc("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 update_actions() -> void:
|
|
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 update_struggle(delta : float) -> bool:
|
|
if struggle_timer > 0:
|
|
struggle_timer -= delta
|
|
var snapped_angle = round(input.dir.angle_to(Vector3(1,0,0)) * 4 / PI) * PI * .25
|
|
if abs(snapped_angle - struggle_angle) >= PI * .25:
|
|
struggle_angle = snapped_angle
|
|
struggle_timer -= 2 * delta
|
|
struggling.emit(struggle_timer)
|
|
if struggle_timer <= 0:
|
|
hurt(1)
|
|
knockdown(facing)
|
|
else:
|
|
return false
|
|
return true
|
|
|
|
func update_poison(delta : float) -> void:
|
|
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)
|
|
|
|
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 PawnController 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 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_callback(func(): detect_tween = null)
|
|
|
|
func close_modal() -> void:
|
|
if modal != null:
|
|
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
|
|
trap.removed.connect(modal._on_trap_removed)
|
|
trap.activated.connect(modal._on_trap_failed)
|
|
harmed.connect(modal._on_trap_failed)
|
|
Game.level.add_child(modal)
|
|
|
|
func start_detecting() -> void:
|
|
detecting = true
|
|
update_detect_region.rpc(false)
|
|
|
|
@rpc("authority")
|
|
func update_detect_region(update : bool) -> void:
|
|
var new_square : Vector3i = (global_position - Vector3.ONE * .5).round()
|
|
if update and 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
|
|
|
|
if update:
|
|
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 update and 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 stop_detecting() -> void:
|
|
detecting = false
|
|
clear_detect_region()
|
|
|
|
@rpc("authority")
|
|
func clear_detect_region() -> void:
|
|
for sq in detect_squares.keys():
|
|
Game.level.detect_square(sq, false)
|
|
detect_squares = {}
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func setup(id : int, traps : Array, pos : Vector3) -> void:
|
|
self.id = id
|
|
self.global_position = pos
|
|
var traplist : Array[PawnLevelData.TrapData] = []
|
|
for trap in traps:
|
|
traplist.append(PawnLevelData.TrapData.new(trap.type, trap.qty, trap.qty))
|
|
$Data.traps = traplist
|
|
input.set_multiplayer_authority(id)
|
|
struggling.connect(body._on_struggle_changed)
|
|
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.remove()
|
|
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)
|
|
if damage > 0:
|
|
harmed.emit()
|
|
if struggle_timer > 0:
|
|
struggle_timer = 0
|
|
struggling.emit(struggle_timer)
|
|
knockdown(Vector3(0,0,-1))
|
|
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
|
|
#hurt
|
|
#blast
|
|
#blast_players
|
|
#activate
|
|
#activate_trap
|
|
#fail
|
|
#on_trap_failed
|
|
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
|
|
fling_sound.play()
|
|
|
|
func knockdown(direction : Vector3) -> void:
|
|
state = State.KNOCKDOWN
|
|
input_locked = true
|
|
body.look_at(Vector3(0,1,0), direction)
|
|
fling_sound.stop()
|
|
crash_sound.play()
|
|
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:
|
|
if state != State.KNOCKUP:
|
|
state = State.KNOCKUP
|
|
input_locked = true
|
|
if is_on_floor():
|
|
self.velocity = Vector3.UP * .1
|
|
move_and_slide()
|
|
self.velocity = velocity
|
|
|
|
|
|
|
|
func pitfall(duration : float) -> void:
|
|
struggle_timer = duration
|
|
body.struggle_bar.max_value = struggle_timer
|
|
body.struggle_bar.value = struggle_timer
|
|
body.show_struggle()
|
|
|
|
func start_pitfall(square : Vector3, duration : float) -> void:
|
|
input_locked = true
|
|
velocity = Vector3.ZERO
|
|
var tween = create_tween()
|
|
tween.tween_property(self, "position", square + Vector3(.5,0,.5), 0.25)
|
|
tween.tween_property(self, "input_locked", false, 0)
|
|
tween.tween_callback(pitfall.bind(duration))
|