godot-puzzle/piece.gd

145 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,
PlayerBarrier,
}
func pos_of_lpos(pos: Vector2i, y := 0.0) -> Vector3:
return Vector3(pos.x, y, pos.y)
func target_pos() -> Vector3:
return pos_of_lpos(lpos, position.y) + Vector3(0.5, 0, 0.5)
var last_move_tween: Tween = null
func tween_to_target(tween := get_tree().create_tween(), anim_time: float = get_parent().anim_time):
if last_move_tween:
last_move_tween.custom_step(413)
last_move_tween.kill()
tween.tween_property(self, "position", target_pos(), anim_time)
last_move_tween = tween
func lpos_of_pos(pos: Vector3) -> Vector2i:
return Vector2i(int(pos.x), int(pos.z))
## logical position
@onready var lpos := lpos_of_pos(position)
## logical velocity
@export var lvel := Vector2i.ZERO
@onready var lvel_displayed := lvel
@export var type: Piece.Type
var speedometer: Label3D
@onready var arrow := get_node_or_null("Arrow") as MeshInstance3D
func do_step(board: Board, tween: Tween):
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):
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
do_bump(move, tween, ball_here).call()
return
lpos = new_pos
var on_ice := !!board.type_at(lpos, Piece.Type.FloorIce)
if not on_ice:
lvel -= move
lvel_displayed = lvel
tween_to_target(tween)
func undo_step() -> Callable:
var old_pos := lpos
var old_vel := lvel
return func():
lpos = old_pos
lvel = old_vel
lvel_displayed = lvel
tween_to_target()
func do_move(move: Vector2i) -> Callable:
return func():
position = target_pos()
lpos += move
tween_to_target()
func undo_move() -> Callable:
var old_pos := lpos
return func():
position = target_pos()
lpos = old_pos
tween_to_target()
func do_push(move: Vector2i, show_now := false) -> Callable:
return func():
lvel += move
if show_now:
lvel_displayed = lvel
func undo_push() -> Callable:
var old_vel := lvel
return func():
lvel = old_vel
lvel_displayed = lvel
# no logical effect, purely for aesthetics
# (and communicating !!!! player yes)
func do_bump(move: Vector2i, tween := get_tree().create_tween(), bumpee: Piece = null, delay_bumpee_update := false) -> Callable:
var old_lvel_bumpee := Vector2i.ZERO
if bumpee:
old_lvel_bumpee = bumpee.lvel
return func():
tween.tween_property(self, "position", target_pos() + pos_of_lpos(move)/2, get_parent().anim_time/2)
if bumpee:
tween.tween_callback(func():
if delay_bumpee_update:
bumpee.lvel_displayed = bumpee.lvel
else:
bumpee.lvel_displayed = old_lvel_bumpee
)
tween.tween_callback(func(): lvel_displayed = lvel)
tween_to_target(tween, get_parent().anim_time/2)
func undo_bump(_move: Vector2i) -> Callable:
return func(): pass
func _ready() -> void:
speedometer = Label3D.new()
speedometer.billboard = BaseMaterial3D.BILLBOARD_ENABLED
speedometer.no_depth_test = true
speedometer.font_size = 64
add_child(speedometer)
func format_vel(vel: Vector2) -> String:
if vel.is_zero_approx():
return ""
else:
# we use abs because the arrow already indicates direction
return "%d,%d" % [abs(vel.x),abs(vel.y)]
func _process(_delta: float) -> void:
speedometer.text = format_vel(lvel_displayed)
if arrow:
arrow.visible = lvel_displayed != Vector2i.ZERO
if lvel != Vector2i.ZERO:
rotation.y = (1.0*lvel_displayed.clampi(-1,1)).angle_to(Vector2.UP)