godot-puzzle/main.gd
mehbark f1d959d8d9 controller input mapping, disable echo
input handling is terrible right now
2025-05-24 19:00:39 -04:00

182 lines
5.4 KiB
GDScript

extends Node3D
# moving and pushing take no time.
# MOVING. AND PUSHING. TAKE NO TIME.
# YES YES YES
# maybe only one move phase to keep things interesting?
# we lose some sokobanness, but is that a bad thing?
@onready var camera: Camera3D = $Camera
# TODO: wall that player cannot go through but balls can
# (important)
var hist := UndoRedo.new()
var board: Board
var player: Piece
var level_num := -1
# TODO: instead of dumb numbered levels, GIVE THEM GOOD NAMES AND MANUALLY ORDER THEM!
var levels: Array[PackedScene] = [
preload("res://level/level_00.tscn"),
preload("res://level/level_01.tscn"),
preload("res://level/level_02.tscn"),
preload("res://level/level_03.tscn"),
preload("res://level/level_04.tscn"),
]
# TODO: screen transition to hide awkward animations lol
var time := 0:
set(new_time):
var tween := get_tree().create_tween()
tween.tween_property($Sun, "rotation_degrees:y", -15*new_time, 0.1)
time = new_time
var advancing := false
@onready var sound: AudioStreamPlayer = $Sound
@onready var sounds_hit := audio_stream_randomizer_from_dir("res://sfx/hit")
@onready var sounds_undo := audio_stream_randomizer_from_dir("res://sfx/undo")
@onready var sounds_redo := audio_stream_randomizer_from_dir("res://sfx/redo")
@export_range(0, 1) var slowmo_speed := 0.1
func _ready() -> void:
advance_level()
func _process(delta: float) -> void:
$UndoButton.disabled = not hist.has_undo()
$RedoButton.disabled = not hist.has_redo()
$RestartButton.disabled = not hist.has_undo()
$Clock.text = "T = %d" % time
var slowmo := Input.is_action_pressed("slowmo");
$SlowmoIndicator.text = "slowmo" if slowmo else ""
Engine.time_scale = slowmo_speed if slowmo else 1.0
# TODO: this is kinda comically slow BUT STOP WORKING ON THE GAME
AudioServer.playback_speed_scale = slowmo_speed if slowmo else 1.0
if not $TopLeft.is_on_screen() or not $BottomRight.is_on_screen():
camera.position.y += 10*delta
func _input(event: InputEvent) -> void:
if event.is_action_pressed("u", false, true):
step(Vector2i.UP)
elif event.is_action_pressed("d", false, true):
step(Vector2i.DOWN)
elif event.is_action_pressed("l", false, true):
step(Vector2i.LEFT)
elif event.is_action_pressed("r", false, true):
step(Vector2i.RIGHT)
elif event.is_action_pressed("undo", false, true):
undo()
elif event.is_action_pressed("redo", false, true):
redo()
elif event.is_action_pressed("restart", false, true):
restart()
elif event.is_action_pressed("wait", false, true):
step(Vector2i.ZERO)
func undo():
if hist.undo():
sound.stream = sounds_undo
sound.play()
func redo():
if hist.redo():
sound.stream = sounds_redo
sound.play()
# TODO: make restart undoable?
func restart():
while hist.has_undo():
hist.undo()
# HACK: avoid an inconsistent visual board state
for piece in board.pieces():
for tween in piece.tweens:
tween.kill()
piece.tween_to_target()
# i think it's unintuitive to be able to redo from restart
# scratch that i like that, don't clear the history
# you can like replay your steps up to a point
# time advancing, basically
func board_step():
hist.add_do_property(self, "time", time+1)
hist.add_do_method(board.do_step)
hist.add_undo_method(board.undo_step())
hist.add_undo_property(self, "time", time)
func step(move: Vector2i):
if advancing:
return
if won():
advance_level()
return
if move == Vector2i.ZERO:
hist.create_action("wait")
board_step()
hist.commit_action()
if won():
advance_level()
return
var pos := player.lpos
var ball := board.find_piece_at(pos + move, func(p): return p.type == Piece.Type.Ball)
if board.passable_at(pos + move, true):
hist.create_action("move")
hist.add_do_method(player.do_move(move))
hist.add_undo_method(player.undo_move())
hist.commit_action()
elif not board.type_at(pos+move, Piece.Type.PlayerBarrier) and ball != null:
hist.create_action("push")
hist.add_do_method(ball.do_push(move))
hist.add_do_method(player.do_bump(move))
hist.add_undo_method(player.undo_bump(move))
hist.add_undo_method(ball.undo_push())
hist.commit_action()
else:
sound.stream = sounds_hit
sound.play()
if won():
advance_level()
func won() -> bool:
for piece in board.pieces():
if piece.type == Piece.Type.Goal:
var ball := board.type_at(piece.lpos, Piece.Type.Ball)
if !ball or ball.lvel != Vector2i.ZERO:
return false
return true
func advance_level():
hist.clear_history()
advancing = true
if level_num >= 0:
print("level won")
await get_tree().create_timer(1).timeout
advancing = false
$Sun.rotation.y = 0
time = 0
level_num += 1
if level_num >= levels.size():
print("you win")
return
if board: board.queue_free()
board = levels[level_num].instantiate()
add_child(board)
for piece in board.pieces():
if piece.type == Piece.Type.Player:
player = piece
$TopLeft.aabb = board.top_left_aabb()
$BottomRight.aabb = board.bottom_right_aabb()
var center := (board.top_left_aabb().position + board.bottom_right_aabb().position)/2
var old_y := camera.position.y
camera.position = Vector3(center.x, old_y, center.z)
func audio_stream_randomizer_from_dir(dir: String) -> AudioStreamRandomizer:
var stream := AudioStreamRandomizer.new()
stream.random_pitch = 1.1
var hit_dir := DirAccess.open(dir)
hit_dir.list_dir_begin()
var file_name := hit_dir.get_next()
while file_name != "":
if file_name.ends_with("ogg"):
stream.add_stream(-1, load(dir+"/"+file_name))
file_name = hit_dir.get_next()
return stream