Extensive work done on basically everything. Attacks prototyped, animations prototyped, pawnbody split out, all traps but pitfall implemented, UI hooked up more.

This commit is contained in:
2026-01-01 07:36:54 -05:00
parent 84bf495d11
commit b4910013c0
89 changed files with 1994 additions and 64 deletions

View File

@@ -0,0 +1,10 @@
extends Control
var pawns = [
preload("res://data/pawns/van_reily.tres"),
preload("res://data/pawns/lou_riche.tres"),
preload("res://data/pawns/tico.tres"),
preload("res://data/pawns/john_bishous.tres"),
preload("res://data/pawns/abdoll_relin.tres"),
preload("res://data/pawns/tenrou_ugetsu.tres")
]

View File

@@ -0,0 +1 @@
uid://grs3nyom325o

1
scripts/combat_target.gd Normal file
View File

@@ -0,0 +1 @@
class_name CombatTarget extends StaticBody3D

View File

@@ -0,0 +1 @@
uid://clqnjqolkujea

View File

@@ -0,0 +1,18 @@
class_name ControlDisplay extends Control
@onready var range_attack_button : TextureButton = $RangedAttack
@onready var melee_attack_button : TextureButton = $MeleeAttack
@onready var trap_button : TextureButton = $Trap
@onready var detect_button : TextureButton = $Detect
@onready var switch_button : TextureButton = $Switch
func _on_melee_range_changed(melee : bool) -> void:
range_attack_button.visible = !melee
melee_attack_button.visible = melee
func _process(delta: float) -> void:
trap_button.set_pressed_no_signal(Input.is_action_pressed("lay trap"))
detect_button.set_pressed_no_signal(Input.is_action_pressed("detect"))
switch_button.set_pressed_no_signal(Input.is_action_pressed("detonate"))
melee_attack_button.set_pressed_no_signal(Input.is_action_pressed("attack"))
range_attack_button.set_pressed_no_signal(Input.is_action_pressed("attack"))

View File

@@ -0,0 +1 @@
uid://ci7vc41m5alq5

View File

@@ -65,7 +65,7 @@ func start_disarming() -> void:
disarm_button_container.add_child(icon)
confirmation_dialog.visible = false
disarm_window.visible = true
timer.start(3 + (2 * len(disarm_buttons) * randf_range(.75, 1.25) ))
timer.start(2 + (len(disarm_buttons) * randf_range(.75, 1.25) ))
func try_advance(btn : int) -> void:
@@ -91,7 +91,9 @@ func button_pressed(event : InputEventAction) -> void:
match(event.action):
"detonate":
start_disarming()
"attack":
"attack":
var trap = Game.level.get_square_trap(square)
trap.disarming = false
Game.player.close_modal()
_: return

46
scripts/gas_emitter.gd Normal file
View File

@@ -0,0 +1,46 @@
extends Node3D
var gas_square_template = preload("res://templates/gas_square.tscn")
var cycles : int = 3
var square : Vector3i = Vector3i(2,0,0)
var squares : Dictionary[Vector3i, bool] = {}
var last_spread : Array[Vector3i]
var trap_owner : int
var damage : int = 4
func _ready() -> void:
emit_gas.call_deferred(square)
last_spread = [square]
squares[square] = true
func _on_timeout() -> void:
cycles -= 1
if cycles <= 0:
queue_free()
return
spread()
func spread() -> void:
var new_squares : Dictionary[Vector3i, bool] = {}
for square in last_spread:
for i in range(-1, 2):
for j in range(-1, 2):
for k in range(-1, 2):
var ts = square + Vector3i(i, j, k)
new_squares[square + Vector3i(i, j, k)] = true
last_spread = []
for square in new_squares.keys():
if squares.has(square):
continue
var floor_square = square + Vector3i(0,-1,0)
if(Game.level.floor_layer.get_cell_item(floor_square) == GridMap.INVALID_CELL_ITEM
and Game.level.ramp_layer.get_cell_item(floor_square) == GridMap.INVALID_CELL_ITEM):
continue
last_spread.append(square)
squares[square] = true
emit_gas(square)
func emit_gas(square) -> void:
var gas = gas_square_template.instantiate()
gas.setup(trap_owner, damage)
Game.level.add_vfx(gas, square)

View File

@@ -0,0 +1 @@
uid://b06ywr7w0wigy

27
scripts/gas_square.gd Normal file
View File

@@ -0,0 +1,27 @@
extends Area3D
@onready var particles : GPUParticles3D = %ParticleFX
var trap_owner : int
var damage : int
var time_remaining : float
func _ready() -> void:
particles.emitting = true
func _physics_process(delta: float) -> void:
for body in get_overlapping_bodies():
body.poison(damage, 2.0)
time_remaining -= delta
if time_remaining <= 0:
queue_free()
func setup(owner, damage) -> void:
self.trap_owner = trap_owner
self.damage = damage
time_remaining = 2.0
func _on_body_entered(body: Node3D) -> void:
if body.can_hurt():
body.hurt(damage)
body.poison(damage, 2.0)

View File

@@ -0,0 +1 @@
uid://6xmp5cbmjrbw

View File

@@ -1,6 +1,7 @@
class_name HealthBar extends TextureProgressBar
@export var healthy_gradient : GradientTexture1D
@export var poisoned_gradient : GradientTexture1D
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
@@ -13,3 +14,6 @@ func _process(delta: float) -> void:
func _on_health_changed(current : int, max : int) -> void:
max_value = max
value = current
func _on_poison_status_changed(poisoned : bool) -> void:
texture_progress = poisoned_gradient if poisoned else healthy_gradient

View File

@@ -2,12 +2,26 @@ class_name HUD extends Control
@onready var trap_display : TrapDisplay = %TrapDisplay
@onready var p1_healthbar : HealthBar = %P1HealthBar
@onready var control_display : ControlDisplay = %ControlDisplay
@onready var ammo_label : Label = %AmmoLabel
func _ready() -> void:
Game.hud = self
func register_player(player : Player) -> void:
player.health_changed.connect(p1_healthbar._on_health_changed)
player.poison_status_changed.connect(p1_healthbar._on_poison_status_changed)
trap_display._on_trap_list_changed(player.data.traps, player.data.active_trap)
player.trap_cycled.connect(trap_display._on_trap_cycled)
player.trap_quantity_changed.connect(trap_display._on_trap_quantity_changed)
player.trap_list_changed.connect(trap_display._on_trap_list_changed)
player.combat_target_changed.connect(control_display._on_melee_range_changed)
player.ammo_changed.connect(_on_ammo_changed)
set_ammo(player.ammo)
func set_ammo(ammo : int) -> void:
ammo_label.text = str(ammo)
ammo_label.modulate = Color.DIM_GRAY if ammo == 0 else Color.WHITE
func _on_ammo_changed(current : int, max : int) -> void:
set_ammo(current)

View File

@@ -1,19 +1,48 @@
extends Camera3D
class_name PlayerCamera extends Camera3D
var target
var offset : Vector3
var player_offset : Vector3
@export var decay = 0.9 # How quickly the shaking stops [0, 1].
@export var max_offset = Vector2(2, 1.5) # Maximum hor/ver shake in pixels.
@export var noise : FastNoiseLite
var noise_y = 0
var trauma = 0.0 # Current shake strength.
var trauma_power = 2 # Trauma exponent. Use [2, 3].
func _enter_tree() -> void:
call_deferred("register_player")
func _ready() -> void:
randomize()
noise.seed = randi()
func register_player() -> void:
target = Game.player
target.camera = self
if target:
offset = global_position - target.global_position
player_offset = global_position - target.global_position
func add_trauma(amount):
trauma = min(trauma + amount, 1.0)
func _process(delta: float) -> void:
if target:
global_position = target.global_position + offset
global_position = target.global_position + player_offset
if trauma:
trauma = max(trauma - decay * delta, 0)
shake()
else:
h_offset = 0
v_offset = 0
func shake():
noise_y += .1
var amount = pow(trauma, trauma_power)
var n_val = noise.get_noise_2d(noise.seed*2, noise_y)
h_offset = max_offset.x * amount * randf_range(-1, 1)
v_offset = max_offset.y * amount * randf_range(-1, 1)
print("%f %f" % [h_offset, v_offset])

10
scripts/pawn_body.gd Normal file
View File

@@ -0,0 +1,10 @@
class_name PawnBody extends Node3D
@export var projectile_template : PackedScene
@onready var anim_player : AnimationPlayer = %AnimationPlayer
@onready var ranged_point : Node3D = %RangedPoint
@onready var material : StandardMaterial3D = $Mesh.material
func play_animation(anim_name : String) -> void:
anim_player.play(anim_name)

1
scripts/pawn_body.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://wye3mat5y5yg

View File

@@ -1,46 +1,131 @@
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 = $Body
@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)
signal trap_quantity_changed(trap_index, quantity)
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 dir.length_squared() > 0:
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 /= 3
if !is_on_floor():
velocity += get_gravity()
velocity *= .33
velocity += Vector3(0, down, 0)
move_and_slide()
elif !is_on_floor():
velocity = get_gravity()
velocity += get_gravity() * delta
move_and_slide()
if detecting:
@@ -60,18 +145,101 @@ func _physics_process(delta: float) -> void:
evt.action = button
evt.pressed = true
modal.button_pressed(evt)
else:
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("detect"):
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()
if !detecting and Input.is_action_just_pressed("lay trap"):
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
@@ -93,8 +261,9 @@ func try_lay_trap() -> void:
func lay_trap(square : Vector3i, idx : int) -> void:
var type : Trap.Type = data.traps[idx].type
var trap = trap_template.instantiate()
trap.setup(type, Multiplayer.id)
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)
@@ -125,12 +294,19 @@ func update_detecting() -> void:
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)
@@ -140,9 +316,20 @@ func update_detecting() -> void:
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
@@ -175,11 +362,16 @@ func start_detecting() -> void:
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)
@@ -221,6 +413,9 @@ func cycle_active_trap(dir) -> void:
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)
@@ -232,3 +427,58 @@ func _on_trap_disarmed(type : Trap.Type) -> void:
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

23
scripts/projectile.gd Normal file
View File

@@ -0,0 +1,23 @@
class_name Projectile extends RigidBody3D
var direction : Vector3
var speed : float
var damage
var time_remaining : float
func _ready() -> void:
linear_velocity = speed * direction
time_remaining = 2
func _physics_process(delta: float) -> void:
time_remaining -= delta
if time_remaining <= 0:
queue_free()
func _on_body_entered(body: Node) -> void:
queue_free()
func _on_hit_area_entered(body: Node3D) -> void:
if body is Player:
body.hurt(damage)

View File

@@ -0,0 +1 @@
uid://dqmdowebyfwbq

View File

@@ -7,6 +7,6 @@ func button_pressed(event : InputEventAction) -> void:
"detonate":
Game.player.remove_trap_at(square)
Game.player.close_modal()
"attack":
"attack":
Game.player.close_modal()
_: return

View File

@@ -4,6 +4,7 @@ const player_controller = preload("res://templates/singleplayer_pc.tscn")
const trap_template = preload("res://templates/trap.tscn")
@onready var floor_layer : GridMap = %Floor
@onready var ramp_layer : GridMap = %Ramps
@onready var marker_layer : GridMap = %Markers
@export var difficulty : int = 1
@@ -14,12 +15,14 @@ func _ready() -> void:
Game.level = self
var player = player_controller.instantiate()
var traps : Array[PlayerData.TrapData] = [
PlayerData.TrapData.new(Trap.Type.FORCE_PANEL, 3, 3),
PlayerData.TrapData.new(Trap.Type.BOMB, 3, 3),
PlayerData.TrapData.new(Trap.Type.SWITCH, 3, 3),
PlayerData.TrapData.new(Trap.Type.MINE, 3, 3)
PlayerData.TrapData.new(Trap.Type.MINE, 3, 3),
PlayerData.TrapData.new(Trap.Type.GAS, 3, 3)
]
add_child(player)
generate_trap(Trap.Type.MINE, Vector3i(5,0,2))
generate_trap(Trap.Type.FORCE_PANEL, Vector3(-1, 0, 0), Vector3i(5,0,2))
player.setup(traps)
func is_square_detected(crd) -> bool:
@@ -44,7 +47,12 @@ func add_trap(trap : Trap, crd : Vector3i) -> void:
traps[crd] = trap
trap.global_position = Vector3(crd) + Vector3(.5, 0, .5)
add_child(trap)
pass
func add_projectile(shot : Projectile) -> void:
add_child(shot)
func remove_trap_square(crd : Vector3i) -> void:
traps.erase(crd)
func get_square_trap(crd : Vector3i) -> Trap:
if traps.has(crd):
@@ -52,6 +60,8 @@ func get_square_trap(crd : Vector3i) -> Trap:
else:
return null
func is_valid_trap_square(crd : Vector3i) -> bool:
if floor_layer.get_cell_item(crd + Vector3i(0,-1,0)) == GridMap.INVALID_CELL_ITEM:
return false
@@ -68,9 +78,9 @@ func activate_trap(crd : Vector3i) -> void:
#if trap:
func generate_trap(type : Trap.Type, square : Vector3i):
func generate_trap(type : Trap.Type, dir : Vector3, square : Vector3i):
var trap = trap_template.instantiate()
trap.setup(type, -1)
trap.setup(type, dir, -1)
add_trap(trap, square)
func disarm_trap(crd : Vector3i) -> void:

View File

@@ -26,40 +26,64 @@ const trap_icons : Dictionary = {
Trap.Type.MINE : preload("res://visuals/images/icons/t-mine.png"),
}
const explosion_template = preload("res://templates/explosion.tscn")
const bomb_explosion_template = preload("res://templates/explosion.tscn")
const mine_explosion_template = preload("res://templates/explosion.tscn")
const switch_explosion_template = preload("res://templates/explosion.tscn")
const gas_emitter_template = preload("res://templates/gas_emitter.tscn")
@onready var range_area : Area3D = %RangeArea
@onready var range_shape : BoxShape3D = %RangeShape.shape
@onready var model : MeshInstance3D = %Model
@onready var icon : Sprite3D = %Icon
@onready var force_strip : Sprite3D = %ForceStrip
@onready var material : StandardMaterial3D = model.get_surface_override_material(0)
@onready var reveal_timer : Timer = %RevealTimer
@onready var activation_timer : Timer = %ActivationTimer
var type : Type
var square : Vector3i
var trap_owner : int
var direction : Vector3
var disarming : bool
var disarm_id : int
var delayed_trigger_tween : Tween = null
var damage : int = 10
var just_revealed : bool = false
signal disarmed(type : Trap.Type)
signal activated(type : Trap.Type)
func setup(type : Type, trap_owner : int) -> void:
func setup(type : Type, direction : Vector3, trap_owner : int) -> void:
self.type = type
self.trap_owner = trap_owner
if type == Type.FORCE_PANEL:
var r : float = atan2(direction.z, direction.x)
var cardinal : float = roundi(r * 2 / PI) * PI / 2
self.direction = Vector3(cos(cardinal), 0, sin(cardinal))
func disarm() -> void:
disarmed.emit(type)
queue_free()
func reveal() -> void:
if model.visible:
return
model.visible = true
reveal_timer.start(5)
just_revealed = true
func is_just_revealed() -> bool:
return just_revealed
func is_revealed() -> bool:
return model.visible
func _on_reveal_timeout() -> void:
if Game.level.is_square_detected(square):
if Game.level.is_square_detected(square) or disarming:
reveal_timer.start(5)
else:
model.visible = false
@@ -71,15 +95,87 @@ func _ready() -> void:
icon.visible = owns_trap
range_shape.size = range_shapes[type]
material.albedo_color = Color.YELLOW if owns_trap else Color.RED
match(type):
Type.BOMB:
damage = 15
Type.MINE:
damage = 10
Type.SWITCH:
damage = 5
Type.GAS:
activation_timer.start()
Type.FORCE_PANEL:
var r : float = atan2(direction.z, -direction.x) + PI/2
force_strip.rotate_y(r)
force_strip.visible = true
func _process(delta: float) -> void:
just_revealed = false
func blast(body : Player) -> void:
body.hurt(damage)
func activate() -> void:
var exp = explosion_template.instantiate()
Game.level.add_vfx(exp, square)
var explode : bool = false
match(type):
Type.FORCE_PANEL:
for body in get_overlapping_bodies():
body.fling(direction, 5.0)
Type.MINE:
var exp = mine_explosion_template.instantiate()
Game.level.add_vfx(exp, square)
explode = true
Type.SWITCH:
var exp = switch_explosion_template.instantiate()
Game.level.add_vfx(exp, square)
explode = true
Type.BOMB:
var exp = bomb_explosion_template.instantiate()
Game.level.add_vfx(exp, square)
explode = true
Type.GAS:
var emitter = gas_emitter_template.instantiate()
emitter.trap_owner = trap_owner
emitter.square = square
emitter.damage = 4
Game.level.add_vfx(emitter, square)
if explode:
trigger_adjacent_bombs()
blast_players()
Game.level.remove_trap_square(square)
activated.emit(type)
queue_free()
func trigger_adjacent_bombs() -> void:
for trap : Trap in range_area.get_overlapping_areas():
if trap.type == Type.BOMB:
trap.delay_trigger()
func blast_players() -> void:
for body in range_area.get_overlapping_bodies():
body.hurt(damage)
#match(type):
blast(body)
func delay_trigger() -> void:
if delayed_trigger_tween != null:
return
delayed_trigger_tween = create_tween()
delayed_trigger_tween.tween_interval(.25)
delayed_trigger_tween.tween_callback(activate)
func _on_body_entered(body: Node3D) -> void:
if type == Type.GAS:
return
if body.id == trap_owner:
return
if !disarming or body.id != disarm_id:
if !body.detecting:
activate()
func _on_activation_timer_timeout() -> void:
activate()

View File

@@ -15,6 +15,10 @@ func proximal_shake(distance : float) -> void:
if !Game.player:
return
var d = global_position.distance_squared_to(Game.player.global_position)
if d <= distance * distance:
Input.start_joy_vibration(0, 1,1,1000)
var d_sq = distance * distance
if d <= d_sq:
var str : float = (d_sq - d) / d_sq
var l_val : float = lerp(0.0, 1.0, str)
Input.start_joy_vibration(0, l_val,l_val,lerp(0.5, 1.0, str))
Game.player.camera.add_trauma(l_val)