godot-puzzle/piece.gd
2025-05-05 00:30:41 -04:00

154 lines
3.8 KiB
GDScript

class_name Piece
extends MeshInstance3D
const BALL = preload("res://piece/ball.tscn")
const FLOOR_ICE = preload("res://piece/floor_ice.tscn")
const GOAL = preload("res://piece/goal.tscn")
const PLAYER = preload("res://piece/player.tscn")
const WALL = preload("res://piece/wall.tscn")
enum Type {
Wall,
Goal,
Ball,
Player,
FloorIce,
}
## squares per second
@export_range(0.1, 50) var anim_speed := 10.0
@export_range(0.1, 50) var anim_speed_slow := 1.0
#TODO: {un,}do_bump animation method (good visual clarity)
## logical position
var lpos: Vector2i:
get:
return Vector2i(int(target_pos.x), int(target_pos.z))
set(val):
position = target_pos
start_pos = position
# we offset by 0.5 to be in the middle of the square (for easier editing)
target_pos = Vector3(val.x + 0.5, position.y, val.y + 0.5)
anim_progress = 0
## logical velocity
@export var lvel := Vector2i.ZERO
@export var type: Piece.Type
@onready var start_pos := position
@onready var target_pos := position
@onready var anim_progress := 1.0
var speedometer: Label3D
# TODO: much, *much* better animation is needed. ESPECIALLY if we're doing slowmo
# (we're doing slowmo because it is SICK)
func do_step(board: Board):
if lvel == Vector2i.ZERO:
return
var move := lvel.clampi(-1, 1)
var new_pos := lpos + move
# ball being collided *with* gets the remainder of the momentum
# EMERGENT COMPLEXITY!??!?
if board.solid_at(new_pos):
do_bump(move).call()
var ball_here := board.type_at(new_pos, Piece.Type.Ball)
if ball_here:
var rem := lvel % 2
ball_here.do_push(lvel/2 + rem).call()
lvel = -lvel/2
else:
lvel *= -1
return
lpos = new_pos
var on_ice := !!board.type_at(lpos, Piece.Type.FloorIce)
if not on_ice:
lvel -= move
func undo_step() -> Callable:
var old_pos := lpos
var old_vel := lvel
return func():
lpos = old_pos
lvel = old_vel
func do_move(move: Vector2i) -> Callable:
return func():
lpos += move
func undo_move() -> Callable:
var old_pos := lpos
return func():
lpos = old_pos
func do_push(move: Vector2i) -> Callable:
return func():
lvel += move
anim_progress = 1
func undo_push() -> Callable:
var old_vel := lvel
return func():
lvel = old_vel
anim_progress = 1
# no logical effect, purely for aesthetics
# (and communicating !!!! player yes)
func do_bump(move: Vector2i) -> Callable:
var real_lpos := lpos
return func():
lpos += move
lpos = real_lpos
anim_progress = 0.5
# TODO?: maybe fix? maybe the bump should be more complicated?
func undo_bump(move: Vector2i) -> Callable:
return do_bump(move)
static func ball(pos: Vector2i) -> Piece:
return BALL.instantiate().with_lpos(pos)
static func floor_ice(pos: Vector2i) -> Piece:
return FLOOR_ICE.instantiate().with_lpos(pos)
static func goal(pos: Vector2i) -> Piece:
return GOAL.instantiate().with_lpos(pos)
static func player(pos: Vector2i) -> Piece:
return PLAYER.instantiate().with_lpos(pos)
static func wall(pos: Vector2i) -> Piece:
return WALL.instantiate().with_lpos(pos)
func _ready() -> void:
anim_progress = 1
speedometer = Label3D.new()
speedometer.billboard = BaseMaterial3D.BILLBOARD_ENABLED
speedometer.no_depth_test = true
speedometer.font_size = 64
var font := SystemFont.new()
font.font_names = PackedStringArray(["monospace"])
speedometer.font = font
add_child(speedometer)
#TODO: arrow!
func format_vel(vel: Vector2) -> String:
if vel.is_zero_approx():
return ""
else:
return "%d,%d" % [vel.x,vel.y]
func _process(delta: float) -> void:
var speed := anim_speed_slow if Input.is_action_pressed("slowmo") else anim_speed
anim_progress = min(1, anim_progress + speed*delta)
position = start_pos.lerp(target_pos, anim_progress)
speedometer.text = format_vel(lvel)
# weird hack only necessary for the soon to be gone text parser
func with_lpos(pos: Vector2i) -> Piece:
lpos = pos
lpos = pos
anim_progress = 1
return self