managems/entities/gem_grid.gd

317 lines
10 KiB
GDScript

extends Control
signal is_ready()
signal gem_matched(matched_results)
signal gems_swapped()
const GemScene = preload("res://entities/gem.tscn")
# Random generator
var rnd = Random.new()
# Grid dimensions
const grid_width = 8
const grid_height = 8
# The grid
var grid = LogicalGrid.new(grid_width, grid_height)
# Cursor position
var cursor_i: int = 0
var cursor_j: int = 0
# Status of the cursor
var gem_selected: bool = false
# Counter for events sent by gems
var event_count: int = 0
# State machine for animations and event handling
# NONE : Do not change state
# INIT : Initial state, only here before the end of the _ready function
# READY : Accept input
# SWAPPING : Reject input, change the position of gems
# MATCHING : Reject input, match gems
#
# --------- ------------ ------------
# | READY |--(gem swapped)-->| SWAPPING |--(swap ended, gems are matching)-->| MATCHING |
# --------- ------------ ------------
# ^ ^ | |
# | ------------(match occured)----------------- |
# | |
# -------------------------------(no match)-----------------------------------
#
enum State { NONE, INIT, READY, SWAPPING, MATCHING }
var current_state = State.INIT
var next_state = State.NONE
# Called when the node enters the scene tree for the first time.
func _ready():
# Init the grid
for i in range(grid_width):
for j in range(grid_height):
add_gem(i, j, i, j)
# match gems (without emitting events)
match_gems()
# change the direction
grid.set_flow_direction(LogicalGrid.FlowDirection.DOWN)
# State is ready
current_state = State.READY
# Tell all gems the grid is ready
emit_signal("is_ready")
# State machine
func _process(delta):
if (next_state != State.NONE) and (current_state != next_state):
match next_state:
State.READY:
show_cursor()
current_state = next_state
next_state = State.NONE
State.SWAPPING:
hide_cursor()
event_count = 0
emit_signal("gems_swapped")
current_state = next_state
next_state = State.NONE
State.MATCHING:
current_state = next_state
match_gems() # Computes next state
else:
next_state = State.NONE
# Add a new gem on the board to replace a gem at i,j
# On screen initialy displayed at position screen_i,screen_j
func add_gem(i, j, screen_i, screen_j):
var g = GemScene.instance()
g.randomize(rnd)
g.set_grid_position(screen_i, screen_j)
if current_state != State.INIT:
g.enable_animations()
else:
connect("is_ready", g, "enable_animations")
connect("gems_swapped", g, "update_position")
g.connect("position_reached", self, "on_gem_moved")
$Gems.add_child(g)
grid.push_value(i, j, g)
# Swap gems between cursor position and given position
func gem_swap(i_to, j_to):
if (i_to >= 0) and (j_to >= 0) and (i_to < grid_width) and (j_to < grid_width):
# Swap gem
grid.swap_values(cursor_i, cursor_j, i_to, j_to)
grid.at(cursor_i, cursor_j).move_to_position(cursor_i, cursor_j)
grid.at(i_to, j_to).move_to_position(i_to, j_to)
# Move cursor
gem_selected = false
cursor_i = i_to
cursor_j = j_to
update_cursor()
# Change state
next_state = State.SWAPPING
# Called each time a gems have finished moving
func on_gem_moved():
event_count += 1
if event_count >= (grid_width * grid_height):
# All gems have responded : matching
next_state = State.MATCHING
func match_gems():
# Init signaled result
var results = Dictionary()
for t in Gem.Type.values():
for e in Gem.Effect.values():
results[[t, e]] = 0
# Check loop
var check_loop: bool = true
var no_match = true
while check_loop:
no_match = true
check_loop = false
# Mark the gems to match.
check_matched()
# Collect the gems to match in results
var to_remove = []
for i in range(grid_width):
for j in range(grid_height):
var g = grid.at(i, j)
if g.matched:
# Loop if we are in the init state
check_loop = (current_state == State.INIT)
no_match = false
results[[g.type, g.effect]] = results[[g.type, g.effect]] + 1 + g.bonus
# The indexes to remove are placed by pushing front,
# so that the last indexes are removed first
to_remove.push_front([i, j])
# Remove the gems
grid.remove_values(to_remove)
# Add new gems to fill the empty spaces.
# The initial positions depends on the flow direction.
var counts = [0]
var dir = grid.get_flow_direction()
if LogicalGrid.isFlowDirectionVertical(dir):
counts.resize(grid_width)
else:
counts.resize(grid_height)
for i in range(counts.size()):
counts[i] = 0
for idx in to_remove:
var screen_idx = []
var i = 0
# Gems are not added on the position they are on the grid,
# but outside so that the animation looks correct
match dir:
LogicalGrid.FlowDirection.UP:
i = idx[0]
screen_idx = [i, grid_height + counts[i]]
LogicalGrid.FlowDirection.DOWN:
i = idx[0]
screen_idx = [i, -1 - counts[i]]
LogicalGrid.FlowDirection.LEFT:
i = idx[1]
screen_idx = [grid_width + counts[i], i]
LogicalGrid.FlowDirection.RIGHT:
i = idx[1]
screen_idx = [-1 - counts[i], i]
counts[i] = counts[i] + 1
add_gem(idx[0], idx[1], screen_idx[0], screen_idx[1])
# Update gems next position
for i in range(grid_width):
for j in range(grid_height):
grid.at(i, j).move_to_position(i, j)
if current_state == State.INIT:
grid.at(i, j).update_position()
# Emit events and compute next state
if current_state != State.INIT:
emit_signal("gem_matched", results)
if no_match:
next_state = State.READY
else:
next_state = State.SWAPPING
# If a gem is surrounded by two gem of the same type vertically or horizontally,
# mark the gem and its surronding gems as matched
func check_matched():
for i in range(grid_width):
for j in range(grid_height):
check_matched_at(i, j)
func check_matched_at(i, j):
# check vertically
var up_i = i
var up_j = j - 1
var down_i = i
var down_j = j + 1
if (up_j >= 0) and (down_j < grid_height):
if same_type(i, j, up_i, up_j) and same_type(i, j, down_i, down_j):
# match
mark_matched(i, j)
mark_matched(up_i, up_j)
mark_matched(down_i, down_j)
# Add a bonus if the effect is the same
if same_effect(i, j, up_i, up_j) and same_effect(i, j, down_i, down_j):
mark_bonus(i, j)
# Check horizontally
var left_i = i - 1
var left_j = j
var right_i = i + 1
var right_j = j
if (left_i >= 0) and (right_i < grid_width):
if same_type(i, j, left_i, left_j) and same_type(i, j, right_i, right_j):
# match
mark_matched(i, j)
mark_matched(left_i, left_j)
mark_matched(right_i, right_j)
# Add a bonus if the effect is the same
if same_effect(i, j, left_i, left_j) and same_effect(i, j, right_i, right_j):
mark_bonus(i, j)
func mark_matched(i, j):
var g = grid.at(i, j)
g.set_matched()
# disconnect signals related to gem displacement
if is_connected("gems_swapped", g, "update_position"):
disconnect("gems_swapped", g, "update_position")
if g.is_connected("position_reached", self, "on_gem_moved"):
g.disconnect("position_reached", self, "on_gem_moved")
func mark_bonus(i, j):
var g = grid.at(i, j)
g.set_bonus()
# True if gems at (i,j) and (x,y) have the same type
func same_type(i, j, x, y):
return grid.at(i, j).type == grid.at(x, y).type
# True is gems at (i,j) and (x,y) have the same effect
func same_effect(i, j, x, y):
return grid.at(i, j).effect == grid.at(x, y).effect
# Cursor management
func update_cursor():
# Position
$Cursor.set_position(64 * Vector2(cursor_i,cursor_j))
# Selected ?
var offset = 0
if gem_selected:
offset = 128
$Cursor.texture.region.position.x = offset
func show_cursor():
$Cursor.show()
func hide_cursor():
$Cursor.hide()
# Input handling
# Only handle events if state is READY and not in transition
func accept_input():
return (current_state == State.READY) and (next_state == State.NONE)
func up():
if accept_input():
if gem_selected:
gem_swap(cursor_i, cursor_j - 1)
else:
cursor_j = cursor_j - 1
if cursor_j < 0:
cursor_j = grid_height - 1
update_cursor()
func down():
if accept_input():
if gem_selected:
gem_swap(cursor_i, cursor_j + 1)
else:
cursor_j = (cursor_j + 1) % grid_height
update_cursor()
func left():
if accept_input():
if gem_selected:
gem_swap(cursor_i - 1, cursor_j)
else:
cursor_i = cursor_i - 1
if cursor_i < 0:
cursor_i = grid_width - 1
update_cursor()
func right():
if accept_input():
if gem_selected:
gem_swap(cursor_i + 1, cursor_j)
else:
cursor_i = (cursor_i + 1) % grid_width
update_cursor()
func select():
if accept_input():
gem_selected = !gem_selected
update_cursor()
func cancel():
if accept_input():
gem_selected = false
update_cursor()