239 lines
7 KiB
GDScript
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
|