godot-puzzle/main.gd
2025-05-31 13:35:58 -04:00

239 lines
7 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
@onready var camera_start_y := camera.position.y
@onready var rolltime_indicator: Sprite2D = $RolltimeIndicator
@onready var victory_indicator: Sprite2D = $VictoryIndicator
# TODO: tutorialize slowmo when things get complicated
var hist := UndoRedo.new()
var board: Board
var player: Piece
var level_num := -1
# TODO: save progress
@export var test := false
# TODO: yet more levels
var levels: Array[PackedScene] = [
preload("res://level/player_barrier_01.tscn"),
preload("res://level/level_00.tscn"),
preload("res://level/level_01.tscn"),
preload("res://level/diagonal_bounce_00.tscn"),
preload("res://level/ball_bounce_00.tscn"),
preload("res://level/timing_00.tscn"),
preload("res://level/level_04.tscn"),
preload("res://level/squeeze_00.tscn"),
preload("res://level/squeeze_01.tscn"),
preload("res://level/level_03.tscn"),
preload("res://level/lvel_cancelling_00.tscn"),
]
var time := 0
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.2
func _ready() -> void:
if test:
levels.reverse()
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
handle_move(delta)
var move_last := Vector2i.ZERO
var move_hold_time := 0.0
var move_last_integer := -1
func arrf(x: float) -> float:
return 5*x**(4.0/3)
# TODO: waiting ARR
func handle_move(delta: float):
var dir := Vector2i(Input.get_vector(&"l", &"r", &"u", &"d").snapped(Vector2.ONE))
if dir == Vector2i.ZERO or (abs(dir.x) > 0 and abs(dir.y) > 0):
move_last = Vector2i.ZERO
move_hold_time = 0
move_last_integer = -1
return
var fx := floori(arrf(move_hold_time))
if fmod(arrf(move_hold_time), 1) < 0.1 and fx != move_last_integer:
var steps := maxi(1, fx - move_last_integer)
for i in range(steps):
step(dir)
move_last_integer = fx
move_last = dir
move_hold_time += delta
func _unhandled_input(event: InputEvent) -> void:
if 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()
for piece in board.pieces():
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
func finish_tween():
if last_sun_tween:
last_sun_tween.custom_step(413)
last_sun_tween.kill()
var last_sun_tween: Tween = null
# time advancing, basically
func board_step():
var anim_time := maxf(board.step_anim_time(), board.anim_time)
hist.add_do_method(board.do_step)
hist.add_do_property(self, "time", time+1)
hist.add_do_method(func():
finish_tween()
var tween := get_tree().create_tween()
tween.tween_property($Sun, "rotation_degrees:y", -15*time, anim_time)
tween.parallel().tween_property(rolltime_indicator, "modulate:a", 0.15, 0.07)
tween.tween_property(rolltime_indicator, "modulate:a", 0, 0.1)
last_sun_tween = tween
)
hist.add_undo_property(self, "time", time)
hist.add_undo_method(func():
finish_tween()
var tween := get_tree().create_tween()
tween.tween_property($Sun, "rotation_degrees:y", -15*time, 0.1)
last_sun_tween = tween
)
hist.add_undo_method(board.undo_step())
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
finish_tween()
board.finish_tween()
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:
var tween := get_tree().create_tween()
hist.create_action("push")
hist.add_do_method(ball.do_push(move))
hist.add_do_method(player.do_bump(move, tween, ball, true))
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")
if last_sun_tween and last_sun_tween.is_running():
await last_sun_tween.finished
var tween := get_tree().create_tween()
tween.tween_property(victory_indicator, "modulate:a", 1, 0.1).set_delay(0.3)
tween.tween_callback(func(): victory_indicator.modulate.a = 0).set_delay(0.9)
await tween.finished
camera.position.y = camera_start_y
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)
player = board.player()
$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