UI work, placing traps, removing traps, disarming traps, new sound fx.

This commit is contained in:
2025-12-25 17:52:30 -05:00
parent 3b6407d6e5
commit 55eb37ca74
34 changed files with 867 additions and 72 deletions

View File

@@ -1,14 +1,8 @@
extends Node
enum TrapType{
BOMB,
MINE,
GAS,
FORCE_PANEL,
SWITCH,
PITFALL
}
const blinder_template = preload("res://templates/blinder.tscn")
const one_shot_template = preload("res://templates/one_shot.tscn")
const vs_man_level = preload("res://scenes/multiplayer_test.tscn")
@@ -41,3 +35,11 @@ func start_vs_man() -> void:
var spawner = get_tree().get_first_node_in_group("level_spawner") as MultiplayerSpawner
spawner.add_child(level)
func oneshot(stream : AudioStream) -> void:
var shot = one_shot_template.instantiate()
shot.stream = stream
if level:
level.add_child(shot)
else:
add_child(shot)

View File

@@ -4,7 +4,7 @@ extends Node
const SERVER_PORT = 8080
const SERVER_IP = "127.0.0.1"
var handle : String
var id : int
var id : int = 1
var players : Dictionary = {}
signal client_added(handle : String, id : int)

View File

@@ -0,0 +1,103 @@
class_name DisarmTrapModal extends Control
const action_value : Dictionary[String, int] = {
"detonate" : 0,
"lay trap" : 1,
"detect" : 2,
"attack" : 3
}
const icon_textures : Array = [
{
"normal" : preload("res://visuals/images/icons/switch button.png"),
"pressed" : preload("res://visuals/images/icons/lit switch button.png")
},
{
"normal" : preload("res://visuals/images/icons/trap button.png"),
"pressed" : preload("res://visuals/images/icons/lit trap button.png")
},
{
"normal" : preload("res://visuals/images/icons/detect button.png"),
"pressed" : preload("res://visuals/images/icons/lit detect button.png")
},
{
"normal" : preload("res://visuals/images/icons/ranged attack button.png"),
"pressed" : preload("res://visuals/images/icons/lit ranged attack button.png")
},
]
const disarm_icon_template = preload("res://templates/disarm_icon.tscn")
const success_sound = preload("res://audio/sounds/menu select.wav")
const fail_sound = preload("res://audio/sounds/134688__otbtechno__bike-bell.wav")
const progress_sound = preload("res://audio/sounds/BUTTON Generic Metal Medium High Mechanics 02.wav")
@onready var confirmation_dialog : ConfirmationDialog = %ConfirmationDialog
@onready var disarm_window : PanelContainer = %DisarmWindow
@onready var disarm_button_container : HBoxContainer = %DisarmContainer
@onready var timer : Timer = %Timer
@onready var timer_label : Label = %TimerLabel
var square : Vector3i
var disarm_icons : Array[TextureButton]
var disarm_buttons : Array[int]
var progress : int
var disarming : bool
var difficulty : int = 1
func _process(delta: float) -> void:
timer_label.text = "%02d.3" % timer.time_left
func start_disarming() -> void:
disarming = true
progress = 0
disarm_icons = []
disarm_buttons = []
for i in range(difficulty):
var btn = randi_range(0,3)
var icon : TextureButton = disarm_icon_template.instantiate()
icon.texture_normal = icon_textures[btn].normal
icon.texture_pressed = icon_textures[btn].pressed
disarm_buttons.append(btn)
disarm_icons.append(icon)
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) ))
func try_advance(btn : int) -> void:
if btn == disarm_buttons[progress]:
disarm_icons[progress].set_pressed_no_signal(true)
progress += 1
if progress >= len(disarm_buttons):
Game.player.close_modal()
Game.oneshot(success_sound)
Game.level.disarm_trap(square)
else:
Game.oneshot(progress_sound)
else:
Game.player.close_modal()
Game.oneshot(fail_sound)
Game.level.activate_trap(square)
func button_pressed(event : InputEventAction) -> void:
if disarming:
try_advance(action_value[event.action])
else:
match(event.action):
"detonate":
start_disarming()
"attack":
Game.player.close_modal()
_: return
func _on_timer_timeout() -> void:
Game.player.close_modal()
Game.oneshot(fail_sound)
Game.level.activate_trap(square)

View File

@@ -0,0 +1 @@
uid://5nrsscwod1xg

8
scripts/one_shot.gd Normal file
View File

@@ -0,0 +1,8 @@
extends AudioStreamPlayer
func _ready() -> void:
$Timer.start(stream.get_length())
func _on_timeout() -> void:
queue_free()

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

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

View File

@@ -1,21 +1,34 @@
class_name Player extends CharacterBody3D
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 data : PlayerData = $Data
@onready var data : PlayerData = $Data
@onready var trap_sound : AudioStreamPlayer3D = $TrapSound
var button_actions : Dictionary[int, String]
var current_square : Vector3i
var detecting : bool
var detecting : bool = false
var detect_squares : Dictionary[Vector3i, bool] = {}
var input_locked : bool = false
var action_tween : Tween = null
var modal = null
signal trap_cycled(trap_index)
signal trap_quantity_changed(trap_index, quantity)
signal trap_list_changed(traps)
func _physics_process(delta: float) -> void:
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:
body.look_at(body.global_position - dir)
velocity = speed * dir
@@ -24,18 +37,70 @@ func _physics_process(delta: float) -> void:
if !is_on_floor():
velocity += get_gravity()
move_and_slide()
elif !is_on_floor():
velocity = get_gravity()
move_and_slide()
if detecting:
update_detecting()
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"):
start_detecting()
elif Input.is_action_just_released("detect"):
stop_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)
else:
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"):
start_detecting()
elif Input.is_action_just_released("detect"):
stop_detecting()
if !detecting and Input.is_action_just_pressed("lay trap"):
try_lay_trap()
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, Multiplayer.id)
trap.disarmed.connect(_on_trap_disarmed)
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()
@@ -68,9 +133,30 @@ func update_detecting() -> void:
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:
show_disarm_trap_modal()
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
Game.level.add_child(modal)
func start_detecting() -> void:
detecting = true
@@ -92,6 +178,10 @@ func start_detecting() -> void:
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
@@ -103,6 +193,17 @@ 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
@@ -114,3 +215,11 @@ func cycle_active_trap(dir) -> void:
if prev != data.active_trap:
trap_cycled.emit(data.active_trap)
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

View File

@@ -1,11 +1,11 @@
class_name PlayerData extends Node
class TrapData:
var type : Game.TrapType
var type : Trap.Type
var quantity : int
var max : int
func _init(type : Game.TrapType, quantity : int, max : int) -> void:
func _init(type : Trap.Type, quantity : int, max : int) -> void:
self.type = type
self.quantity = quantity
self.max = max

View File

@@ -0,0 +1,12 @@
class_name RemoveTrapModal extends ConfirmationDialog
var square : Vector3i
func button_pressed(event : InputEventAction) -> void:
match(event.action):
"detonate":
Game.player.remove_trap_at(square)
Game.player.close_modal()
"attack":
Game.player.close_modal()
_: return

View File

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

View File

@@ -1,26 +1,71 @@
class_name Level extends Node3D
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 marker_layer : GridMap = %Markers
@export var difficulty : int = 1
var traps : Dictionary[Vector3i, Trap] = {}
func _ready() -> void:
Game.level = self
var player = player_controller.instantiate()
var traps : Array[PlayerData.TrapData] = [
PlayerData.TrapData.new(Trap.Type.BOMB, 3, 3),
PlayerData.TrapData.new(Trap.Type.SWITCH, 3, 3),
PlayerData.TrapData.new(Trap.Type.MINE, 3, 3)
]
add_child(player)
generate_trap(Trap.Type.MINE, Vector3i(5,0,2))
player.setup(traps)
func is_square_detected(crd) -> bool:
return marker_layer.get_cell_item(crd + Vector3i(0,-1,0)) != GridMap.INVALID_CELL_ITEM
func detect_square(crd : Vector3i, mark : bool) -> bool:
var cell = floor_layer.get_cell_item(crd)
if cell == GridMap.INVALID_CELL_ITEM:
return false
marker_layer.set_cell_item(crd, 0 if mark else GridMap.INVALID_CELL_ITEM)
if mark:
var trap_crd = crd + Vector3i(0,1,0)
if traps.has(trap_crd):
var trap : Trap = traps[trap_crd]
if trap.trap_owner != Multiplayer.id:
trap.reveal()
return true
func add_trap(trap : Trap, crd : Vector3i) -> void:
trap.square = crd
traps[crd] = trap
trap.global_position = Vector3(crd) + Vector3(.5, 0, .5)
add_child(trap)
pass
func get_square_trap(crd : Vector3i) -> Trap:
if traps.has(crd):
return traps[crd]
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
if traps.has(crd):
return false
return true
func _ready() -> void:
Game.level = self
var player = player_controller.instantiate()
var traps : Array[PlayerData.TrapData] = [
PlayerData.TrapData.new(Game.TrapType.BOMB, 3, 3),
PlayerData.TrapData.new(Game.TrapType.SWITCH, 3, 3),
PlayerData.TrapData.new(Game.TrapType.MINE, 3, 3)
]
add_child(player)
player.setup(traps)
func generate_trap(type : Trap.Type, square : Vector3i):
var trap = trap_template.instantiate()
trap.setup(type, -1)
add_trap(trap, square)
func disarm_trap(crd : Vector3i) -> void:
var trap = traps[crd]
trap.disarm()
traps.erase(crd)

54
scripts/trap.gd Normal file
View File

@@ -0,0 +1,54 @@
class_name Trap extends Area3D
enum Type{
BOMB,
MINE,
GAS,
FORCE_PANEL,
SWITCH,
PITFALL
}
const trap_icons : Dictionary = {
Trap.Type.BOMB : preload("res://visuals/images/icons/t-bomb.png"),
Trap.Type.GAS : preload("res://visuals/images/icons/t-gas.png"),
Trap.Type.PITFALL : preload("res://visuals/images/icons/t-pitfall.png"),
Trap.Type.FORCE_PANEL : preload("res://visuals/images/icons/t-force_panel.png"),
Trap.Type.SWITCH : preload("res://visuals/images/icons/t-switch.png"),
Trap.Type.MINE : preload("res://visuals/images/icons/t-mine.png"),
}
@onready var model : MeshInstance3D = %Model
@onready var icon : Sprite3D = %Icon
@onready var material : StandardMaterial3D = model.get_surface_override_material(0)
@onready var reveal_timer : Timer = %RevealTimer
var type : Type
var square : Vector3i
var trap_owner : int
signal disarmed(type : Trap.Type)
func setup(type : Type, trap_owner : int) -> void:
self.type = type
self.trap_owner = trap_owner
func disarm() -> void:
disarmed.emit(type)
queue_free()
func reveal() -> void:
model.visible = true
reveal_timer.start(5)
func _on_reveal_timeout() -> void:
if Game.level.is_square_detected(square):
reveal_timer.start(5)
else:
model.visible = false
func _ready() -> void:
var owns_trap = trap_owner == Multiplayer.id
icon.texture = trap_icons[type]
model.visible = owns_trap
icon.visible = owns_trap
material.albedo_color = Color.YELLOW if owns_trap else Color.RED

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

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

View File

@@ -1,18 +1,18 @@
class_name TrapIcon extends Control
const trap_icons : Dictionary = {
Game.TrapType.BOMB : preload("res://visuals/images/icons/bomb.png"),
Game.TrapType.GAS : preload("res://visuals/images/icons/gas.png"),
Game.TrapType.PITFALL : preload("res://visuals/images/icons/pitfall.png"),
Game.TrapType.FORCE_PANEL : preload("res://visuals/images/icons/force_panel.png"),
Game.TrapType.SWITCH : preload("res://visuals/images/icons/switch.png"),
Game.TrapType.MINE : preload("res://visuals/images/icons/mine.png"),
Trap.Type.BOMB : preload("res://visuals/images/icons/bomb.png"),
Trap.Type.GAS : preload("res://visuals/images/icons/gas.png"),
Trap.Type.PITFALL : preload("res://visuals/images/icons/pitfall.png"),
Trap.Type.FORCE_PANEL : preload("res://visuals/images/icons/force_panel.png"),
Trap.Type.SWITCH : preload("res://visuals/images/icons/switch.png"),
Trap.Type.MINE : preload("res://visuals/images/icons/mine.png"),
}
@onready var icon_image : TextureRect = %Icon
@onready var qty_label : Label = %Label
func setup(type : Game.TrapType, qty : int) -> void:
func setup(type : Trap.Type, qty : int) -> void:
icon_image.texture = trap_icons[type]
set_quantity(qty)