521 lines
16 KiB
GDScript
521 lines
16 KiB
GDScript
tool
|
|
class_name Taquin
|
|
extends Control
|
|
|
|
signal state_changed(previous, new)
|
|
|
|
enum Direction { UP, DOWN, LEFT, RIGHT }
|
|
enum State {
|
|
MAIN,
|
|
WINNING,
|
|
GAME_OVER,
|
|
}
|
|
|
|
const _state_transitions = {
|
|
State.MAIN : [ State.WINNING ],
|
|
State.WINNING : [ State.GAME_OVER ],
|
|
State.GAME_OVER : [ State.MAIN ]
|
|
}
|
|
const Piece = preload("res://src/Piece.tscn")
|
|
const Utils = preload("res://src/Utils.gd")
|
|
|
|
export var rows: int = NewGamePanel.normal_rows
|
|
export var columns: int = NewGamePanel.normal_columns
|
|
export var shuffle_iterations: int = NewGamePanel.normal_iterations
|
|
export var artwork_texture: Texture
|
|
export(State) var current_state = State.MAIN
|
|
export var autoload_fresh_game := true
|
|
|
|
var board_size := Vector2.ZERO
|
|
var interpiece := 4
|
|
var min_padding := 15
|
|
var padding := Vector2(min_padding, min_padding)
|
|
|
|
var pieces: Array = []
|
|
var rng := RandomNumberGenerator.new()
|
|
|
|
var current_animation_path := "AnimationPlayer/MockPiece:position"
|
|
|
|
var swipe := Vector2(0, 0)
|
|
var is_sliding := false
|
|
var minimum_slide_length := 5.0
|
|
|
|
var current_sliding_piece: Piece = null
|
|
var missing_piece: Piece = null
|
|
|
|
var current_origin := Vector2.ZERO
|
|
var current_goal := Vector2.ZERO
|
|
var current_axis := Vector2.ZERO
|
|
var current_touch_slide := Vector2.ZERO
|
|
var local_min_position := Vector2.ZERO
|
|
var local_max_position := Vector2.ZERO
|
|
|
|
var hint_active := false setget set_hint_active, get_hint_active
|
|
|
|
var _artwork_path := NewGamePanel.default_artwork_path
|
|
|
|
onready var hint_tween = $HintTween
|
|
|
|
func position_for_index(index: Vector2, size: int) -> Vector2:
|
|
return padding + Vector2(index.x * (size + interpiece), index.y * (size + interpiece))
|
|
|
|
func compute_piece_size() -> int:
|
|
var w_size: int = (board_size.x - (2 * min_padding) - ((columns - 1) * interpiece)) / columns
|
|
var h_size: int = (board_size.y - (2 * min_padding) - ((rows - 1) * interpiece)) / rows
|
|
return int(min(w_size, h_size))
|
|
|
|
func compute_padding(piece_size: int) -> Vector2:
|
|
var p = Vector2(0, 0)
|
|
p.x = board_size.x - columns * piece_size - (columns - 1) * interpiece
|
|
p.y = board_size.y - rows * piece_size - (rows - 1) * interpiece
|
|
p = p / Vector2(2, 2)
|
|
return p
|
|
|
|
func _ready() -> void:
|
|
$AnimationPlayer/MockPiece.visible = false
|
|
$Particles2D.emitting = false
|
|
|
|
board_size.x = min(rect_size.x, rect_size.y)
|
|
board_size.y = board_size.x
|
|
|
|
$Background.rect_size = board_size
|
|
$Background.rect_position = (rect_size - $Background.rect_size) / 2
|
|
$Background.set_anchors_preset(Control.PRESET_CENTER)
|
|
|
|
rng.randomize()
|
|
|
|
if autoload_fresh_game:
|
|
start_fresh()
|
|
if artwork_texture == null:
|
|
print_debug("Load texture from: ", NewGamePanel.default_artwork_path)
|
|
artwork_texture = Utils.load_texture_from_path(NewGamePanel.default_artwork_path)
|
|
new_game(NewGamePanel.normal_columns, NewGamePanel.normal_rows, NewGamePanel.normal_iterations, artwork_texture)
|
|
|
|
func _unhandled_input(event):
|
|
# Forward keyboard event
|
|
_gui_input(event)
|
|
|
|
func _gui_input(event):
|
|
if $AnimationPlayer.is_playing():
|
|
# Disable input during animation
|
|
return
|
|
if hint_active or hint_tween.is_active():
|
|
return
|
|
|
|
match current_state:
|
|
# If we are in the winning animation, fast-forward to game over screen
|
|
State.WINNING:
|
|
transition_to(State.GAME_OVER)
|
|
return
|
|
State.GAME_OVER:
|
|
return
|
|
|
|
#
|
|
# Handle keyboard input
|
|
#
|
|
if event.is_action_pressed("ui_up"):
|
|
move_piece(Direction.UP, 1.0)
|
|
return
|
|
if event.is_action_pressed("ui_down"):
|
|
move_piece(Direction.DOWN, 1.0)
|
|
return
|
|
if event.is_action_pressed("ui_left"):
|
|
move_piece(Direction.LEFT, 1.0)
|
|
return
|
|
if event.is_action_pressed("ui_right"):
|
|
move_piece(Direction.RIGHT, 1.0)
|
|
return
|
|
|
|
#
|
|
# Handle touch input
|
|
#
|
|
if event is InputEventScreenDrag:
|
|
swipe = event.relative
|
|
# We check that the slide to have a minimum length
|
|
# before detecting the direction to avoid jittering.
|
|
if swipe.length() > minimum_slide_length and not is_sliding:
|
|
print("sliding started")
|
|
is_sliding = true
|
|
current_touch_slide = Vector2.ZERO
|
|
var angle = swipe.angle()
|
|
var direction = direction_for_angle(angle)
|
|
debug_print_direction(direction)
|
|
current_sliding_piece = sliding_piece_for_direction(direction)
|
|
if current_sliding_piece != null:
|
|
current_axis = axis_for_direction(direction)
|
|
var local_end_position = current_axis * (current_sliding_piece.size + interpiece)
|
|
local_min_position = Vector2(min(0, local_end_position.x), min(0, local_end_position.y))
|
|
local_max_position = Vector2(max(0, local_end_position.x), max(0, local_end_position.y))
|
|
current_goal = current_sliding_piece.position + local_end_position
|
|
current_touch_slide += swipe
|
|
if current_sliding_piece != null:
|
|
var delta = current_touch_slide.project(current_axis)
|
|
delta.x = clamp(delta.x, local_min_position.x, local_max_position.x)
|
|
delta.y = clamp(delta.y, local_min_position.y, local_max_position.y)
|
|
current_sliding_piece.position = current_origin + delta
|
|
|
|
if event is InputEventScreenTouch:
|
|
if not event.pressed: # Touch released
|
|
is_sliding = false
|
|
if current_sliding_piece != null:
|
|
var current_position = current_sliding_piece.position
|
|
if current_position.distance_to(current_origin) > current_position.distance_to(current_goal):
|
|
current_sliding_piece.position = current_goal
|
|
commit_slide(true, true)
|
|
else:
|
|
reset_position(current_sliding_piece)
|
|
reset_slide()
|
|
|
|
func debug_print_direction(direction: int):
|
|
match direction:
|
|
Direction.UP:
|
|
print("Direction ⬆️ UP")
|
|
Direction.DOWN:
|
|
print("Direction ⬇️ DOWN")
|
|
Direction.LEFT:
|
|
print("Direction ⬅️ LEFT")
|
|
Direction.RIGHT:
|
|
print("Direction ➡️ RIGHT")
|
|
_:
|
|
assert(false)
|
|
|
|
func axis_for_direction(direction: int) -> Vector2:
|
|
match direction:
|
|
Direction.UP:
|
|
return Vector2.UP
|
|
Direction.DOWN:
|
|
return Vector2.DOWN
|
|
Direction.LEFT:
|
|
return Vector2.LEFT
|
|
Direction.RIGHT:
|
|
return Vector2.RIGHT
|
|
_:
|
|
assert(false)
|
|
return Vector2.ZERO
|
|
|
|
func direction_for_angle(angle: float) -> int:
|
|
if angle < PI / 4 and angle >= - PI / 4:
|
|
return Direction.RIGHT
|
|
if angle >= PI / 4 and angle < PI - PI / 4:
|
|
return Direction.DOWN
|
|
if angle >= - PI + PI / 4 and angle < - PI / 4:
|
|
return Direction.UP
|
|
if angle >= PI - PI / 4 or angle < -PI + PI / 4:
|
|
return Direction.LEFT
|
|
assert(false)
|
|
return Direction.DOWN
|
|
|
|
func sliding_piece_for_direction(direction) -> Piece:
|
|
var destination: Vector2 = missing_piece.taquin_current_index
|
|
match direction:
|
|
Direction.UP:
|
|
destination.y += 1
|
|
Direction.DOWN:
|
|
destination.y -= 1
|
|
Direction.LEFT:
|
|
destination.x += 1
|
|
Direction.RIGHT:
|
|
destination.x -= 1
|
|
|
|
if (destination.x < 0 || destination.x >= columns
|
|
|| destination.y < 0 || destination.y >= rows):
|
|
print("\/!\\ Impossible move")
|
|
return null
|
|
|
|
var piece = pieces[destination.x][destination.y]
|
|
current_origin = piece.position
|
|
|
|
return piece
|
|
|
|
func move_piece(direction, speed: float) -> bool:
|
|
current_sliding_piece = sliding_piece_for_direction(direction)
|
|
if current_sliding_piece == null:
|
|
reset_slide()
|
|
return false
|
|
|
|
if speed > 0.0:
|
|
var moving_piece_animation: Animation = $AnimationPlayer.get_animation("MovingPiece")
|
|
|
|
assert(moving_piece_animation != null)
|
|
assert(moving_piece_animation.get_track_count() > 0)
|
|
|
|
var moving_piece_track_index: int = moving_piece_animation.find_track(current_animation_path)
|
|
|
|
assert(moving_piece_track_index != -1)
|
|
|
|
var new_animation_path: String = str($AnimationPlayer.get_parent().get_path_to(current_sliding_piece), ":position")
|
|
moving_piece_animation.track_set_path(moving_piece_track_index, new_animation_path)
|
|
current_animation_path = new_animation_path
|
|
|
|
moving_piece_animation.track_set_key_value(moving_piece_track_index, 0, current_sliding_piece.position)
|
|
moving_piece_animation.track_set_key_value(moving_piece_track_index, 1, missing_piece.position)
|
|
$AnimationPlayer.play("MovingPiece")
|
|
else:
|
|
var previous_position = current_sliding_piece.position
|
|
current_sliding_piece.position = missing_piece.position
|
|
missing_piece.position = previous_position
|
|
commit_slide(false, false)
|
|
|
|
return true
|
|
|
|
func commit_slide(audio: bool, check_solved: bool):
|
|
assert(current_sliding_piece != null)
|
|
assert(current_origin != Vector2.ZERO)
|
|
|
|
var x_delta = missing_piece.taquin_current_index.x - current_sliding_piece.taquin_current_index.x
|
|
var y_delta = missing_piece.taquin_current_index.y - current_sliding_piece.taquin_current_index.y
|
|
assert(abs(x_delta) + abs(y_delta) == 1)
|
|
|
|
swap_pieces(missing_piece, current_sliding_piece)
|
|
reset_position(missing_piece)
|
|
|
|
if audio:
|
|
$AudioStreamPlayer.play()
|
|
|
|
ensure_validity()
|
|
reset_slide()
|
|
update()
|
|
if check_solved:
|
|
check_solved()
|
|
|
|
func reset_slide():
|
|
current_sliding_piece = null
|
|
current_origin = Vector2.ZERO
|
|
|
|
func reset_position(p: Piece):
|
|
p.position = position_for_index(p.taquin_current_index, p.size)
|
|
|
|
func swap_pieces(a: Piece, b: Piece) -> void:
|
|
var a_index := a.taquin_current_index
|
|
a.taquin_current_index = b.taquin_current_index
|
|
pieces[b.taquin_current_index.x][b.taquin_current_index.y] = a
|
|
b.taquin_current_index = a_index
|
|
pieces[a_index.x][a_index.y] = b
|
|
|
|
func shuffle(count: int, speed: float) -> void:
|
|
print_debug("")
|
|
var previous_direction: int = Direction.DOWN
|
|
|
|
while count > 0:
|
|
var direction = rng.randi_range(Direction.UP, Direction.RIGHT)
|
|
|
|
# Avoid reversing the previous move
|
|
if direction == Direction.UP and previous_direction == Direction.DOWN:
|
|
continue
|
|
if direction == Direction.DOWN and previous_direction == Direction.UP:
|
|
continue
|
|
if direction == Direction.RIGHT and previous_direction == Direction.LEFT:
|
|
continue
|
|
if direction == Direction.LEFT and previous_direction == Direction.RIGHT:
|
|
continue
|
|
|
|
# Retry until the move is valid
|
|
if move_piece(direction, speed):
|
|
previous_direction = direction
|
|
count -= 1
|
|
debug_print_direction(direction)
|
|
|
|
func check_solved() -> bool:
|
|
for c in range(columns):
|
|
for r in range(rows):
|
|
if pieces[c][r].order != 1 + c + r * columns:
|
|
return false
|
|
|
|
transition_to(State.WINNING)
|
|
return true
|
|
|
|
func ensure_validity() -> void:
|
|
for c in range(columns):
|
|
for r in range(rows):
|
|
var piece = pieces[c][r]
|
|
assert(piece.taquin_current_index.x == c)
|
|
assert(piece.taquin_current_index.y == r)
|
|
|
|
func set_pieces_reflection(value: bool) -> void:
|
|
for c in range(columns):
|
|
for r in range(rows):
|
|
var piece = pieces[c][r]
|
|
piece.set_reflection(value)
|
|
|
|
func current_state_name() -> String:
|
|
return State.keys()[current_state]
|
|
|
|
func transition_to(state):
|
|
if current_state == state:
|
|
return
|
|
assert(state in _state_transitions[current_state])
|
|
var previous_state = current_state
|
|
current_state = state
|
|
match current_state:
|
|
State.WINNING:
|
|
$Particles2D.emitting = true
|
|
set_pieces_reflection(true)
|
|
$Timer.start(-1)
|
|
State.GAME_OVER:
|
|
$Particles2D.emitting = false
|
|
$Timer.stop()
|
|
emit_signal("state_changed", previous_state, current_state)
|
|
|
|
func save() -> Dictionary:
|
|
# order start from top-left (1) and iterate over every row
|
|
# eg. 1 2 3
|
|
# 4 5 6
|
|
# 7 8 9
|
|
var serialized_pieces = []
|
|
for r in range(rows):
|
|
for c in range(columns):
|
|
var piece: Piece = pieces[c][r]
|
|
serialized_pieces.append(piece.order)
|
|
|
|
return {
|
|
"rows": rows,
|
|
"columns": columns,
|
|
"pieces": serialized_pieces,
|
|
"hidden_piece": serialized_pieces.size(),
|
|
"artwork_path": _artwork_path,
|
|
}
|
|
|
|
func load(saved_state) -> bool:
|
|
print("load save state: ", saved_state)
|
|
if not saved_state.has_all(["rows", "columns", "pieces", "hidden_piece", "artwork_path"]):
|
|
assert(false, "Invalid save state")
|
|
return false
|
|
rows = saved_state["rows"]
|
|
columns = saved_state["columns"]
|
|
|
|
var texture := Utils.load_texture_from_path(saved_state["artwork_path"])
|
|
if texture == null:
|
|
return false
|
|
artwork_texture = texture
|
|
_artwork_path = saved_state["artwork_path"]
|
|
print_debug("Load artwork from: ", _artwork_path)
|
|
|
|
init(saved_state["pieces"], saved_state["hidden_piece"], artwork_texture)
|
|
return true
|
|
|
|
func init(pieces_order: Array, hidden_piece: int, artwork: Texture) -> void:
|
|
var piece_size: int = compute_piece_size()
|
|
padding = compute_padding(piece_size)
|
|
print("piece size: ", piece_size)
|
|
print("padding: ", padding)
|
|
|
|
if pieces.size() > 0:
|
|
for c in range(pieces.size()):
|
|
for r in range(pieces[c].size()):
|
|
pieces[c][r].queue_free()
|
|
pieces.clear()
|
|
|
|
for c in range(columns):
|
|
var pieces_row: Array = []
|
|
for r in range(rows):
|
|
var piece = Piece.instance()
|
|
|
|
# Uniforms
|
|
piece.size = piece_size
|
|
piece.piece_scale = Vector2(piece_size, piece_size) / board_size
|
|
piece.texture = artwork_texture
|
|
|
|
# order start from top-left (1) and iterate over every row
|
|
# eg. 1 2 3
|
|
# 4 5 6
|
|
# 7 8 9
|
|
piece.order = pieces_order[c + r * columns]
|
|
piece.taquin_original_index = Vector2((piece.order - 1) % columns, (piece.order - 1) / columns)
|
|
piece.taquin_original_position = position_for_index(piece.taquin_original_index, piece.size)
|
|
piece.taquin_current_index = Vector2(c, r)
|
|
piece.taquin_current_position = position_for_index(piece.taquin_current_index, piece.size)
|
|
piece.taquin_original_normalized_position = piece.taquin_original_position / board_size
|
|
|
|
piece.position = piece.taquin_current_position
|
|
|
|
if piece.order == hidden_piece:
|
|
piece.visible = false
|
|
missing_piece = piece
|
|
|
|
$Background.add_child(piece)
|
|
pieces_row.append(piece)
|
|
|
|
pieces.append(pieces_row)
|
|
assert(missing_piece != null)
|
|
|
|
func new_game(columns: int, rows: int, shuffle_iterations: int, artwork_texture: Texture) -> void:
|
|
self.columns = columns
|
|
self.rows = rows
|
|
self.shuffle_iterations = shuffle_iterations
|
|
print_debug("%d x %d with %d shuffles" % [columns, rows, shuffle_iterations])
|
|
var pieces_order: Array = []
|
|
for order in range(1, rows * columns + 1):
|
|
pieces_order.append(order)
|
|
var hidden_piece = rows * columns # Last piece is hidden
|
|
init(pieces_order, hidden_piece, artwork_texture)
|
|
shuffle(shuffle_iterations, 0.0)
|
|
|
|
func start_fresh():
|
|
if artwork_texture == null:
|
|
print_debug("Load texture from: ", NewGamePanel.default_artwork_path)
|
|
artwork_texture = Utils.load_texture_from_path(NewGamePanel.default_artwork_path)
|
|
|
|
new_game(NewGamePanel.normal_columns, NewGamePanel.normal_rows, NewGamePanel.normal_iterations, artwork_texture)
|
|
|
|
#
|
|
# Hints
|
|
#
|
|
func show_hints() -> void:
|
|
set_hint_active(true)
|
|
hint_tween.remove_all()
|
|
for c in range(columns):
|
|
for r in range(rows):
|
|
var piece: Piece = pieces[c][r]
|
|
if piece.taquin_current_index != piece.taquin_original_index:
|
|
piece.taquin_current_position = piece.position
|
|
hint_tween.interpolate_property(piece, "position", piece.position, piece.taquin_original_position, 1.0)
|
|
hint_tween.start()
|
|
|
|
func discard_hints() -> void:
|
|
hint_tween.remove_all()
|
|
for c in range(columns):
|
|
for r in range(rows):
|
|
var piece: Piece = pieces[c][r]
|
|
if piece.taquin_current_index != piece.taquin_original_index:
|
|
hint_tween.interpolate_property(piece, "position", piece.position, piece.taquin_current_position, 0.2)
|
|
hint_tween.interpolate_callback(self, 0.2, "set_hint_active", false)
|
|
hint_tween.start()
|
|
|
|
func get_hint_active() -> bool:
|
|
return hint_active
|
|
|
|
func set_hint_active(value: bool):
|
|
hint_active = value
|
|
|
|
#
|
|
# Signals
|
|
#
|
|
func _on_Timer_timeout():
|
|
transition_to(State.GAME_OVER)
|
|
|
|
func _on_AnimationPlayer_animation_finished(anim_name):
|
|
match anim_name:
|
|
"MovingPiece":
|
|
commit_slide(true, true)
|
|
|
|
func _on_NewGamePanel_start_triggered(preferences):
|
|
var difficulty_mode = preferences.get_value("game", "difficulty", "normal")
|
|
print_debug("difficulty mode: ", difficulty_mode)
|
|
var columns = preferences.get_value("game", "columns", NewGamePanel.normal_columns)
|
|
var rows = preferences.get_value("game", "rows", NewGamePanel.normal_rows)
|
|
var shuffle_iterations = preferences.get_value("game", "shuffle_iterations", NewGamePanel.normal_iterations)
|
|
var artwork_path = preferences.get_value("game", "artwork_path", NewGamePanel.default_artwork_path)
|
|
print_debug("new game triggered with artwork path: ", artwork_path)
|
|
artwork_texture = Utils.load_texture_from_path(artwork_path)
|
|
if artwork_path != null:
|
|
_artwork_path = artwork_path
|
|
new_game(columns, rows, shuffle_iterations, artwork_texture)
|
|
|
|
|
|
func _on_Hints_button_down():
|
|
if not hint_active:
|
|
show_hints()
|
|
|
|
func _on_Hints_button_up():
|
|
discard_hints()
|