Renamed a bunch of player stuff to pawn stuff and implemented extensive work on getting single-address 'netplay' code working. Not sure if I've created issues with single player but in theory it should all transfer across as if the player is simply always the host.
This commit is contained in:
568
scripts/pawn_controller.gd
Normal file
568
scripts/pawn_controller.gd
Normal file
@@ -0,0 +1,568 @@
|
||||
class_name PawnController extends CharacterBody3D
|
||||
|
||||
enum State {
|
||||
NORMAL,
|
||||
KNOCKDOWN,
|
||||
KNOCKUP,
|
||||
FLUNG,
|
||||
BOUND,
|
||||
DEAD
|
||||
}
|
||||
|
||||
const camera_template = preload("res://templates/camera.tscn")
|
||||
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 : 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
|
||||
|
||||
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 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()
|
||||
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 struggle_timer > 0:
|
||||
struggle_timer -= delta
|
||||
var snapped_angle = round(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
|
||||
|
||||
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_detecting()
|
||||
|
||||
#Deal with the rest of the buttons
|
||||
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 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 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_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
|
||||
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 = {}
|
||||
|
||||
@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
|
||||
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))
|
||||
Reference in New Issue
Block a user