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 := true 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"), ] 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