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 # 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, 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) # TODO?: maybe fix? maybe the bump should be more complicated? 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)