182 lines
5.4 KiB
GDScript
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
|