Files
net-gunner/scripts/player.gd

490 lines
14 KiB
GDScript

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 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
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
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:
state = State.KNOCKUP
input_locked = true