godot-puzzle/board.gd
2025-05-31 11:44:17 -04:00

124 lines
3.4 KiB
GDScript

class_name Board
extends Node3D
## squares per second
@export_range(0.0, 1, 0.01) var anim_time := 0.15
var dims: Vector2i
func _ready():
for piece in pieces():
dims.x = max(dims.x, piece.lpos.x)
dims.y = max(dims.y, piece.lpos.y)
# for zooming out to see the whole puzzle
func top_left_aabb() -> AABB:
return AABB(position-Vector3(0,0,1), Vector3.ONE*0.1)
func bottom_right_aabb() -> AABB:
return AABB(position + Vector3(dims.x, 0, dims.y) + Vector3(1,0,2), Vector3.ONE*0.1)
func pieces() -> Array[Piece]:
var sorted: Array[Piece] = []
for child in get_children():
if child is Piece:
sorted.push_back(child)
# important for consistency.
# everything involving pieces is done top to bottom, then left to right
# expensive? yes. wasteful? definitely. worth it? i think so.
sorted.sort_custom(func(a, b): return a.lpos.y < b.lpos.y or (a.lpos.y == b.lpos.y and a.lpos.x < b.lpos.x))
return sorted
func pieces_at(pos: Vector2i) -> Array[Piece]:
var out: Array[Piece] = []
for piece in pieces():
if piece.lpos == pos:
out.push_back(piece)
return out
func find_piece_at(pos: Vector2i, filter: Callable) -> Piece:
var at := pieces_at(pos)
var idx := at.find_custom(filter)
if idx < 0: return
return at[idx]
func any_at(pos: Vector2i, filter: Callable) -> bool:
return pieces_at(pos).any(filter)
func type_at(pos: Vector2i, type: Piece.Type) -> Piece:
return find_piece_at(pos, func(p): return p.type == type)
func solid_at(pos: Vector2i) -> bool:
return any_at(pos, func(p): return p.type == Piece.Type.Wall or p.type == Piece.Type.Ball or p.type == Piece.Type.Player)
func passable_at(pos: Vector2i, for_player := false) -> bool:
return not solid_at(pos) and not (for_player and type_at(pos, Piece.Type.PlayerBarrier))
func remove_piece(piece: Piece):
remove_child(piece)
func add_piece(piece: Piece):
add_child(piece)
func add_pieces(piece: Array[Piece]):
for p in piece:
add_piece(p)
func player() -> Piece:
for piece in pieces():
if piece.type == Piece.Type.Player:
return piece
return null
var last_tween: Tween = null
func finish_tween():
if last_tween:
last_tween.custom_step(413)
last_tween.kill()
last_tween = null
# here are the phases:
# cardinal velocity
# diagonal velocity
# uhh that's it lol?
# well, subphases: higher lvel magnitude moves first within a phase
## returns the total time animation *will* take
func step_anim_time() -> float:
var pieces_moving := pieces().filter(func(piece): return piece.lvel != Vector2i.ZERO).size()
return pieces_moving * anim_time
func do_step():
var pieces_moving := pieces().filter(func(piece): return piece.lvel != Vector2i.ZERO)
var pieces_cardinal := pieces_moving.filter(func(piece): return piece.lvel.x == 0 or piece.lvel.y == 0)
var pieces_diagonal := pieces_moving.filter(func(piece): return piece.lvel.x != 0 and piece.lvel.y != 0)
var magnitude_sort := func(a: Piece, b: Piece):
return a.lvel.length() > b.lvel.length()
pieces_cardinal.sort_custom(magnitude_sort)
pieces_diagonal.sort_custom(magnitude_sort)
finish_tween()
var tween := get_tree().create_tween()
last_tween = tween
for piece in pieces_cardinal:
piece.do_step(self, tween)
for piece in pieces_diagonal:
piece.do_step(self, tween)
if pieces_moving.is_empty():
# no tweens -> invalid -> annoying error message
tween.kill()
func undo_step() -> Callable:
var undos: Array[Callable] = []
for piece in pieces():
undos.push_back(piece.undo_step())
return func():
for undo in undos:
undo.call()