diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3a781a3..783a2e6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,19 +1,19 @@
# Contribution
-Existing issues marked with a [help wanted tag](https://github.com/ninetailsrabbit/[PLUGIN]/labels/help%20wanted) are the best candidates for contributions. Issues with that tag are either not prioritised, or are in need of help with someone with more expertise within a certain area
+Existing issues marked with a [help wanted tag](https://github.com/ninetailsrabbit/match3-board/labels/help%20wanted) are the best candidates for contributions. Issues with that tag are either not prioritised, or are in need of help with someone with more expertise within a certain area
## Reporting bugs
-1. **[Follow the template for bug reports.](https://github.com/ninetailsrabbit/[PLUGIN]/issues/new?assignees=ninetailsrabbit&labels=%F0%9F%90%9B+bug&projects=&template=bug_report.md&title)**
+1. **[Follow the template for bug reports.](https://github.com/ninetailsrabbit/match3-board/issues/new?assignees=ninetailsrabbit&labels=%F0%9F%90%9B+bug&projects=&template=bug_report.md&title)**
2. **Golden rule** Open _one_ ussie for _one_ bug .
-3. [Search for existing reports first.](https://github.com/ninetailsrabbit/[PLUGIN]/issues) If you found a similar issue in the tracker - better share your problem in the existing thread.
+3. [Search for existing reports first.](https://github.com/ninetailsrabbit/match3-board/issues) If you found a similar issue in the tracker - better share your problem in the existing thread.
4. Besides the platform, specify as many specifics as you can _(if relevant)_. CPU/GPU, input methods _(controller, mouse)_ and so on.
5. A simple reproduction project helps more than any reproduction steps. Include it whenever you can. Examining the problem first-hand is the easiest way to solve it.
## Proposing features
-1. **[Follow the template for feature requests.](https://github.com/ninetailsrabbit/[PLUGIN]/issues/new?assignees=ninetailsrabbit&labels=%E2%AD%90+feature&projects=&template=feature_request.md&title)**
-2. [Search for existing proposals first.](https://github.com/ninetailsrabbit/[PLUGIN]/issues)
+1. **[Follow the template for feature requests.](https://github.com/ninetailsrabbit/match3-board/issues/new?assignees=ninetailsrabbit&labels=%E2%AD%90+feature&projects=&template=feature_request.md&title)**
+2. [Search for existing proposals first.](https://github.com/ninetailsrabbit/match3-board/issues)
3. Request something with a real-world use-case. Abstract features may not be considered.
4. If you are capable of implementing said feature, include some code that demonstrates the finer details/nuances of said feature.
diff --git a/README.md b/README.md
index 07bca4f..882c22b 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,15 @@
@@ -20,11 +20,11 @@
# 📦 Installation
-1. [Download Latest Release](https://github.com/ninetailsrabbit/[PLUGIN]/releases/latest)
-2. Unpack the `addons/[PLUGIN]` folder into your `/addons` folder within the Godot project
+1. [Download Latest Release](https://github.com/ninetailsrabbit/match3-board/releases/latest)
+2. Unpack the `addons/ninetailsrabbit.match3_board` folder into your `/addons` folder within the Godot project
3. Enable this addon within the Godot settings: `Project > Project Settings > Plugins`
To better understand what branch to choose from for which Godot version, please refer to this table:
-|Godot Version|[PLUGIN] Branch|[PLUGIN] Version|
+|Godot Version|match3-board Branch|match3-board Version|
|---|---|--|
|[![GodotEngine](https://img.shields.io/badge/Godot_4.3.x_stable-blue?logo=godotengine&logoColor=white)](https://godotengine.org/)|`main`|`1.x`|
diff --git a/SECURITY.md b/SECURITY.md
index 7eaca52..a83c99f 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -8,4 +8,4 @@
## Reporting a Vulnerability
-Please [raise an issue](https://github.com/ninetailsrabbit/[PLUGIN]/issues) in case you find a security issue.
+Please [raise an issue](https://github.com/ninetailsrabbit/match3-board/issues) in case you find a security issue.
diff --git a/addons/my_plugin/plugin.cfg b/addons/my_plugin/plugin.cfg
deleted file mode 100644
index b1e49a4..0000000
--- a/addons/my_plugin/plugin.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[plugin]
-
-name="my_plugin"
-description=""
-author="Ninetailsrabbit"
-version="1.0.0"
-script="plugin.gd"
diff --git a/addons/ninetailsrabbit.match3_board/assets/board.svg b/addons/ninetailsrabbit.match3_board/assets/board.svg
new file mode 100644
index 0000000..d9fd6c2
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/assets/board.svg
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/ninetailsrabbit.match3_board/assets/board.svg.import b/addons/ninetailsrabbit.match3_board/assets/board.svg.import
new file mode 100644
index 0000000..c1de57e
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/assets/board.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dp0rg1kjqppec"
+path="res://.godot/imported/board.svg-1538b9ec53988ab8c30ff9f4bfcc2ea5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/assets/board.svg"
+dest_files=["res://.godot/imported/board.svg-1538b9ec53988ab8c30ff9f4bfcc2ea5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/ninetailsrabbit.match3_board/assets/piece.svg b/addons/ninetailsrabbit.match3_board/assets/piece.svg
new file mode 100644
index 0000000..30255e3
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/assets/piece.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/addons/ninetailsrabbit.match3_board/assets/piece.svg.import b/addons/ninetailsrabbit.match3_board/assets/piece.svg.import
new file mode 100644
index 0000000..4623a9d
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/assets/piece.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ckafybol0kwqg"
+path="res://.godot/imported/piece.svg-1960013a83aa536b201c3cf2a3335f60.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/assets/piece.svg"
+dest_files=["res://.godot/imported/piece.svg-1960013a83aa536b201c3cf2a3335f60.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/addons/my_plugin/assets/progress-background.png b/addons/ninetailsrabbit.match3_board/assets/progress-background.png
similarity index 100%
rename from addons/my_plugin/assets/progress-background.png
rename to addons/ninetailsrabbit.match3_board/assets/progress-background.png
diff --git a/addons/my_plugin/assets/progress-background.png.import b/addons/ninetailsrabbit.match3_board/assets/progress-background.png.import
similarity index 66%
rename from addons/my_plugin/assets/progress-background.png.import
rename to addons/ninetailsrabbit.match3_board/assets/progress-background.png.import
index 165e132..e238646 100644
--- a/addons/my_plugin/assets/progress-background.png.import
+++ b/addons/ninetailsrabbit.match3_board/assets/progress-background.png.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://qjmv3cxry8wu"
-path="res://.godot/imported/progress-background.png-998a5bde492cb044f40021717cd2a939.ctex"
+path="res://.godot/imported/progress-background.png-f9deb8d5d9b136dfe8b90954b7037c64.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://addons/my_plugin/assets/progress-background.png"
-dest_files=["res://.godot/imported/progress-background.png-998a5bde492cb044f40021717cd2a939.ctex"]
+source_file="res://addons/ninetailsrabbit.match3_board/assets/progress-background.png"
+dest_files=["res://.godot/imported/progress-background.png-f9deb8d5d9b136dfe8b90954b7037c64.ctex"]
[params]
diff --git a/addons/my_plugin/assets/progress_background_green.png b/addons/ninetailsrabbit.match3_board/assets/progress_background_green.png
similarity index 100%
rename from addons/my_plugin/assets/progress_background_green.png
rename to addons/ninetailsrabbit.match3_board/assets/progress_background_green.png
diff --git a/addons/my_plugin/assets/progress_background_green.png.import b/addons/ninetailsrabbit.match3_board/assets/progress_background_green.png.import
similarity index 72%
rename from addons/my_plugin/assets/progress_background_green.png.import
rename to addons/ninetailsrabbit.match3_board/assets/progress_background_green.png.import
index 2551338..f933e25 100644
--- a/addons/my_plugin/assets/progress_background_green.png.import
+++ b/addons/ninetailsrabbit.match3_board/assets/progress_background_green.png.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bdf47suvxkr8l"
-path="res://.godot/imported/progress_background_green.png-6d408d66d9d96656254b6aea57ff2db6.ctex"
+path="res://.godot/imported/progress_background_green.png-49a36c7309ca9c7c5168a30ca91409e1.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://addons/my_plugin/assets/progress_background_green.png"
-dest_files=["res://.godot/imported/progress_background_green.png-6d408d66d9d96656254b6aea57ff2db6.ctex"]
+source_file="res://addons/ninetailsrabbit.match3_board/assets/progress_background_green.png"
+dest_files=["res://.godot/imported/progress_background_green.png-49a36c7309ca9c7c5168a30ca91409e1.ctex"]
[params]
diff --git a/addons/my_plugin/plugin.gd b/addons/ninetailsrabbit.match3_board/ninetailsrabbit.match3_board.gd
similarity index 73%
rename from addons/my_plugin/plugin.gd
rename to addons/ninetailsrabbit.match3_board/ninetailsrabbit.match3_board.gd
index 65b61f5..c137ad1 100644
--- a/addons/my_plugin/plugin.gd
+++ b/addons/ninetailsrabbit.match3_board/ninetailsrabbit.match3_board.gd
@@ -12,6 +12,9 @@ func _enter_tree() -> void:
if not DirAccess.dir_exists_absolute(MyPluginSettings.PluginTemporaryReleaseUpdateDirectoryPath):
DirAccess.make_dir_recursive_absolute(MyPluginSettings.PluginTemporaryReleaseUpdateDirectoryPath)
+ add_custom_type("Match3Board", "Node2D", preload("src/match3_board.gd"), preload("assets/board.svg"))
+ add_custom_type("PieceDefinitionResource", "Resource", preload("src/components/pieces/piece_definition_resource.gd"), preload("assets/piece.svg"))
+
func _exit_tree() -> void:
MyPluginSettings.remove_settings()
@@ -20,6 +23,9 @@ func _exit_tree() -> void:
update_notify_tool_instance.free()
update_notify_tool_instance = null
+ remove_custom_type("PieceDefinitionResource")
+ remove_custom_type("Match3Board")
+
## Update tool referenced from https://github.com/MikeSchulze/gdUnit4/blob/master/addons/gdUnit4
func _setup_updater() -> void:
if MyPluginSettings.is_update_notification_enabled():
diff --git a/addons/ninetailsrabbit.match3_board/plugin.cfg b/addons/ninetailsrabbit.match3_board/plugin.cfg
new file mode 100644
index 0000000..f88d693
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="Match3-Board"
+description="This lightweight library provides the core logic and functionality you need to build engaging match-3 games. Focus on game design and mechanics while leaving the complex logic to this library"
+author="Ninetailsrabbit"
+version="1.0.0"
+script="ninetailsrabbit.match3_board.gd"
diff --git a/addons/my_plugin/settings/plugin_settings.gd b/addons/ninetailsrabbit.match3_board/settings/plugin_settings.gd
similarity index 93%
rename from addons/my_plugin/settings/plugin_settings.gd
rename to addons/ninetailsrabbit.match3_board/settings/plugin_settings.gd
index 4eba8f6..543a483 100644
--- a/addons/my_plugin/settings/plugin_settings.gd
+++ b/addons/ninetailsrabbit.match3_board/settings/plugin_settings.gd
@@ -1,8 +1,8 @@
@tool
class_name MyPluginSettings extends RefCounted
-const PluginPrefixName: String = "my_plugin" ## The folder name
-const GitRepositoryName: String = "my-plugin"
+const PluginPrefixName: String = "ninetailsrabbit.match3_board" ## The folder name
+const GitRepositoryName: String = "match3-board"
static var PluginName: String = "MyPlugin"
static var PluginProjectName: String = ProjectSettings.get_setting("application/config/name")
diff --git a/addons/ninetailsrabbit.match3_board/src/components/animators/piece_animator.gd b/addons/ninetailsrabbit.match3_board/src/components/animators/piece_animator.gd
new file mode 100644
index 0000000..8adbc46
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/animators/piece_animator.gd
@@ -0,0 +1,82 @@
+class_name PieceAnimator extends Node
+
+@onready var board = get_tree().get_first_node_in_group(Match3Preloader.BoardGroupName)
+
+func _enter_tree() -> void:
+ name = "PieceAnimator"
+
+
+func swap_pieces(from: PieceUI, to: PieceUI):
+ var from_global_position: Vector2 = from.global_position
+ var to_global_position: Vector2 = to.global_position
+ var tween: Tween = create_tween().set_parallel(true)
+
+ tween.tween_property(from, "global_position", to_global_position, 0.2).set_ease(Tween.EASE_IN)
+ tween.tween_property(from, "modulate:a", 0.1, 0.2).set_ease(Tween.EASE_IN)
+ tween.tween_property(to, "global_position", from_global_position, 0.2).set_ease(Tween.EASE_IN)
+ tween.tween_property(to, "modulate:a", 0.1, 0.2).set_ease(Tween.EASE_IN)
+ tween.chain()
+
+ tween.tween_property(from, "modulate:a", 1.0, 0.2).set_ease(Tween.EASE_OUT)
+ tween.tween_property(to, "modulate:a", 1.0, 0.2).set_ease(Tween.EASE_OUT)
+
+ await tween.finished
+
+
+func fall_down(piece: PieceUI, empty_cell: GridCellUI, _is_diagonal: bool = false):
+ var tween: Tween = create_tween()
+
+ tween.tween_property(piece, "global_position", empty_cell.global_position, 0.2)\
+ .set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_LINEAR)
+
+ await tween.finished
+
+
+func fall_down_pieces(movements) -> void:
+ if movements.size() > 0:
+ var tween: Tween = create_tween().set_parallel(true)
+
+ for movement in movements:
+ tween.tween_property(movement.to_cell.current_piece, "global_position", movement.to_cell.global_position, 0.2)\
+ .set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_LINEAR)
+
+ await tween.finished
+
+
+func spawn_pieces(new_pieces: Array[PieceUI]):
+ if new_pieces.size() > 0:
+ var tween: Tween = create_tween().set_parallel(true)
+
+ for piece: PieceUI in new_pieces:
+ var fall_distance = piece.cell_size.y * board.grid_height
+ piece.hide()
+ tween.tween_property(piece, "visible", true, 0.1)
+ tween.tween_property(piece, "global_position", piece.global_position, 0.25)\
+ .set_trans(Tween.TRANS_QUAD).from(Vector2(piece.global_position.x, piece.global_position.y - fall_distance))
+
+ await tween.finished
+
+
+func consume_sequence(sequence: Sequence):
+ if sequence.pieces().size() > 0:
+ var tween: Tween = create_tween().set_parallel(true)
+
+ for piece: PieceUI in sequence.pieces():
+ tween.tween_property(piece, "scale", Vector2.ZERO, 0.15).set_ease(Tween.EASE_OUT)
+
+ await tween.finished
+
+
+func spawn_special_piece(sequence: Sequence, new_piece: PieceUI):
+ if sequence.pieces().size() > 0:
+ var middle_cell: GridCellUI = sequence.middle_cell()
+ new_piece.hide()
+ var tween: Tween = create_tween().set_parallel(true)
+
+ for cell in sequence.cells.filter(func(grid_cell: GridCellUI): return grid_cell.has_piece() and grid_cell != middle_cell):
+ tween.tween_property(cell.current_piece, "global_position", middle_cell.global_position, 0.15)\
+ .set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUAD)
+ tween.chain()
+ tween.tween_property(new_piece, "visible", true, 0.1)
+
+ await tween.finished
diff --git a/addons/ninetailsrabbit.match3_board/src/components/consumers/sequence_consumer.gd b/addons/ninetailsrabbit.match3_board/src/components/consumers/sequence_consumer.gd
new file mode 100644
index 0000000..d24ff38
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/consumers/sequence_consumer.gd
@@ -0,0 +1,52 @@
+class_name SequenceConsumer extends Node
+
+@onready var board = get_tree().get_first_node_in_group(Match3Preloader.BoardGroupName)
+
+func _enter_tree() -> void:
+ name = "SequenceConsumer"
+
+#region Overridables
+
+func consume_sequence(sequence: Sequence) -> void:
+ var new_piece = detect_new_combined_piece(sequence)
+
+ if new_piece is PieceUI:
+ sequence.consume_cell(sequence.middle_cell())
+ board.draw_piece_on_cell(sequence.middle_cell(), new_piece)
+ await board.piece_animator.spawn_special_piece(sequence, new_piece)
+ sequence.consume([sequence.middle_cell()])
+ else:
+ await board.piece_animator.consume_sequence(sequence)
+ sequence.consume()
+
+
+func consume_sequences(sequences: Array[Sequence]) -> void:
+ for sequence: Sequence in sequences:
+ await consume_sequence(sequence)
+
+
+func detect_new_combined_piece(sequence: Sequence):
+ if sequence.all_pieces_are_of_type(PieceDefinitionResource.PieceType.Normal):
+ var piece: PieceUI = sequence.pieces().front()
+
+ if sequence.is_horizontal_or_vertical_shape():
+ match sequence.size():
+ 4:
+ var new_piece_definition = piece.piece_definition.match_4_piece
+
+ if new_piece_definition:
+ var special_piece: PieceUI = board.generate_new_piece()
+ special_piece.piece_definition = new_piece_definition
+
+ return special_piece
+ 5:
+ var new_piece_definition = piece.piece_definition.match_5_piece
+
+ if new_piece_definition:
+ var special_piece: PieceUI = board.generate_new_piece()
+ special_piece.piece_definition = new_piece_definition
+
+ return special_piece
+
+
+ return null
diff --git a/addons/ninetailsrabbit.match3_board/src/components/draggable_sprite_2d.gd b/addons/ninetailsrabbit.match3_board/src/components/draggable_sprite_2d.gd
new file mode 100644
index 0000000..cea9328
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/draggable_sprite_2d.gd
@@ -0,0 +1,104 @@
+class_name DraggableSprite2D extends Sprite2D
+
+signal drag_started
+signal drag_ended
+signal drag_enabled
+signal drag_disabled
+signal mouse_released
+signal picked_up_changed(picked: bool)
+
+
+@export var reset_position_on_release: bool = true
+@export var one_click_drag: bool = false
+@export var smooth_factor: float = 20.0
+@export var drag_input_action: String = "drag":
+ set(value):
+ drag_input_action = value
+
+ set_process(InputMap.has_action(drag_input_action))
+
+var mouse_region: Button
+var current_position: Vector2 = Vector2.ZERO
+var m_offset: Vector2 = Vector2.ZERO
+
+var original_global_position: Vector2 = Vector2.ZERO
+var original_position: Vector2 = Vector2.ZERO
+
+var drag_active: bool = false:
+ set(value):
+ if drag_active != value:
+ drag_active = value
+
+ if drag_active:
+ drag_enabled.emit()
+ else:
+ drag_disabled.emit()
+
+
+var picked_up: bool = false:
+ set(value):
+ if picked_up != value:
+ picked_up = value
+
+ picked_up_changed.emit(picked_up)
+
+ if picked_up:
+ drag_started.emit()
+ else:
+ drag_ended.emit()
+ reset_position()
+
+
+func _ready() -> void:
+ original_global_position = global_position
+ original_position = position
+
+ if mouse_region == null:
+ mouse_region = Button.new()
+ mouse_region.self_modulate.a8 = 0
+ add_child(mouse_region)
+
+
+ resize_mouse_region()
+ mouse_region.button_down.connect(on_mouse_region_pressed)
+ mouse_released.connect(on_mouse_released)
+ texture_changed.connect(on_texture_changed)
+
+ set_process(InputMap.has_action(drag_input_action))
+
+
+func _process(delta: float) -> void:
+ if InputMap.has_action(drag_input_action) and Input.is_action_just_released(drag_input_action) and picked_up:
+ mouse_released.emit()
+
+ elif mouse_region.button_pressed:
+ global_position = global_position.lerp(get_global_mouse_position(), smooth_factor * delta) if smooth_factor > 0 else get_global_mouse_position()
+ current_position = global_position + m_offset
+
+
+func reset_position() -> void:
+ if is_inside_tree() and reset_position_on_release:
+ global_position = original_global_position
+ position = original_position
+
+
+func resize_mouse_region() -> void:
+ if mouse_region:
+ mouse_region.position = Vector2.ZERO
+ mouse_region.anchors_preset = Control.PRESET_FULL_RECT
+
+
+func on_mouse_region_pressed() -> void:
+ picked_up = true
+
+ if is_inside_tree():
+ m_offset = transform.origin - get_global_mouse_position()
+
+
+func on_mouse_released() -> void:
+ picked_up = false
+
+
+func on_texture_changed() -> void:
+ if texture:
+ resize_mouse_region()
diff --git a/addons/ninetailsrabbit.match3_board/src/components/features/piece_weight_generator.gd b/addons/ninetailsrabbit.match3_board/src/components/features/piece_weight_generator.gd
new file mode 100644
index 0000000..626ade3
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/features/piece_weight_generator.gd
@@ -0,0 +1,66 @@
+class_name PieceWeightGenerator
+
+
+var random: RandomNumberGenerator = RandomNumberGenerator.new()
+var available_pieces: Array[PieceDefinitionResource] = []
+
+
+func _init() -> void:
+ random.randomize()
+
+
+func add_available_pieces(new_pieces: Array[PieceDefinitionResource]) -> void:
+ for piece: PieceDefinitionResource in new_pieces:
+ add_available_piece(piece)
+
+
+func add_available_piece(new_piece: PieceDefinitionResource) -> void:
+ if not available_pieces.has(new_piece):
+ available_pieces.append(new_piece)
+
+
+func update_piece(piece: PieceDefinitionResource) -> void:
+ var index = available_pieces.find(piece)
+
+ if index != -1:
+ available_pieces[index] = piece
+
+
+func roll(except: Array[PieceDefinitionResource] = []) -> PieceDefinitionResource:
+ var available_pieces_to_roll = available_pieces.filter(func(piece): return not except.has(piece))
+ var selected_piece: PieceDefinitionResource
+
+ assert(available_pieces_to_roll.size() > 0, "PieceWeightGenerator: No pieces available to roll")
+
+ available_pieces_to_roll.shuffle()
+ selected_piece = _roll_piece(available_pieces_to_roll, _prepare_weight(available_pieces_to_roll))
+
+ while selected_piece == null:
+ selected_piece = _roll_piece(available_pieces_to_roll, _prepare_weight(available_pieces_to_roll))
+
+ return selected_piece
+
+
+func _prepare_weight(pieces: Array[PieceDefinitionResource]) -> float:
+ var total_weight: float = 0.0
+
+ for piece: PieceDefinitionResource in pieces:
+ piece.reset_accum_weight()
+
+ total_weight += piece.weight
+ piece.total_accum_weight = total_weight
+
+ return total_weight
+
+
+func _roll_piece(pieces: Array[PieceDefinitionResource], total_weight: float):
+ var roll_result: float = randf_range(0.0, total_weight)
+ var selected_piece: PieceDefinitionResource
+
+ for piece: PieceDefinitionResource in pieces:
+ if roll_result <= piece.total_accum_weight:
+ selected_piece = piece.duplicate()
+ break;
+
+
+ return selected_piece
diff --git a/addons/ninetailsrabbit.match3_board/src/components/grid_cell_ui.gd b/addons/ninetailsrabbit.match3_board/src/components/grid_cell_ui.gd
new file mode 100644
index 0000000..4fa83d6
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/grid_cell_ui.gd
@@ -0,0 +1,265 @@
+class_name GridCellUI extends Node2D
+
+signal swapped_piece(from: GridCellUI, to: GridCellUI)
+signal swap_rejected(from: GridCellUI, to: GridCellUI)
+signal removed_piece(piece: PieceUI)
+
+const group_name: String = "grid_cell"
+
+@export var column: int
+@export var row: int
+@export var can_contain_piece: bool = true
+@export var cell_size: Vector2 = Vector2(48, 48)
+## Texture default path is temporary
+@export var odd_cell_texture: Texture2D = Match3Preloader.OddCellTexture
+@export var even_cell_texture: Texture2D = Match3Preloader.EvenCellTexture
+
+#region Neighbours
+var neighbour_up: GridCellUI
+var neighbour_bottom: GridCellUI
+var neighbour_right: GridCellUI
+var neighbour_left: GridCellUI
+var diagonal_neighbour_top_right: GridCellUI
+var diagonal_neighbour_top_left: GridCellUI
+var diagonal_neighbour_bottom_right: GridCellUI
+var diagonal_neighbour_bottom_left: GridCellUI
+#endregion
+
+var selected_background_image: Texture2D
+var background_sprite: Sprite2D
+var current_piece: PieceUI:
+ set(value):
+ if value != current_piece:
+ if value == null and is_inside_tree():
+ removed_piece.emit(current_piece)
+
+ current_piece = value
+
+
+func _init(_row: int, _column: int, piece: PieceUI = null, _can_contain_piece: bool = true) -> void:
+ assert(row >=0 and column >=0, "GridCellUI: A grid cell cannot have a negative column %d or row %d" % [column, row])
+
+ row = _row
+ column = _column
+ current_piece = piece
+ can_contain_piece = _can_contain_piece
+
+
+func _enter_tree() -> void:
+ add_to_group(group_name)
+
+ name = "Cell_Column%d_Row%d" % [column, row]
+ z_index = 20
+
+ prepare_background_sprite()
+
+
+func change_to_original_background_image() -> void:
+ background_sprite.texture = selected_background_image
+
+
+func change_background_image(new_image: Texture2D) -> void:
+ background_sprite.texture = new_image
+
+
+func prepare_background_sprite() -> void:
+ selected_background_image = even_cell_texture if (column + row) % 2 == 0 else odd_cell_texture
+
+ if background_sprite == null:
+ background_sprite = Sprite2D.new()
+ background_sprite.name = "BackgroundSprite"
+ background_sprite.texture = selected_background_image
+ background_sprite.z_index = -10
+ add_child(background_sprite)
+
+ if background_sprite.texture:
+ var texture_size = background_sprite.texture.get_size()
+ background_sprite.scale = Vector2(cell_size.x / texture_size.x, cell_size.y / texture_size.y)
+
+
+#region Position
+func board_position() -> Vector2:
+ return Vector2(row, column)
+
+
+func in_same_row_as(other_cell: GridCellUI) -> bool:
+ return row == other_cell.row
+
+
+func in_same_column_as(other_cell: GridCellUI) -> bool:
+ return column == other_cell.column
+
+
+func in_same_position_as(other_cell: GridCellUI) -> bool:
+ return in_same_column_as(other_cell) and in_same_row_as(other_cell)
+
+
+func in_same_grid_position_as(grid_position: Vector2) -> bool:
+ return grid_position.x == row and grid_position.y == column
+
+
+func is_row_neighbour_of(other_cell: GridCellUI) -> bool:
+ var left_column: int = column - 1
+ var right_column: int = column + 1
+
+ return in_same_row_as(other_cell) \
+ and [left_column, right_column].any(func(near_column: int): return other_cell.column == near_column)
+
+
+func is_column_neighbour_of(other_cell: GridCellUI) -> bool:
+ var upper_row: int = row - 1
+ var bottom_row: int = row + 1
+
+ return in_same_column_as(other_cell) \
+ and [upper_row, bottom_row].any(func(near_row: int): return other_cell.row == near_row)
+
+
+func is_adjacent_to(other_cell: GridCellUI) -> bool:
+ return is_row_neighbour_of(other_cell) or is_column_neighbour_of(other_cell)
+
+
+func in_diagonal_with(other_cell: GridCellUI) -> bool:
+ var diagonal_top_right: Vector2 = Vector2(row - 1, column + 1)
+ var diagonal_top_left: Vector2 = Vector2(row - 1, column - 1)
+ var diagonal_bottom_right: Vector2 = Vector2(row + 1, column + 1)
+ var diagonal_bottom_left: Vector2 = Vector2(row + 1, column - 1)
+
+ return other_cell.in_same_grid_position_as(diagonal_top_right) \
+ or other_cell.in_same_grid_position_as(diagonal_top_left) \
+ or other_cell.in_same_grid_position_as(diagonal_bottom_right) \
+ or other_cell.in_same_grid_position_as(diagonal_bottom_left)
+
+
+func is_top_left_corner() -> bool:
+ return neighbour_up == null and neighbour_left == null \
+ and neighbour_bottom is GridCellUI and neighbour_right is GridCellUI
+
+
+func is_top_right_corner() -> bool:
+ return neighbour_up == null and neighbour_right == null \
+ and neighbour_bottom is GridCellUI and neighbour_left is GridCellUI
+
+
+func is_bottom_left_corner() -> bool:
+ return neighbour_bottom == null and neighbour_left == null \
+ and neighbour_up is GridCellUI and neighbour_right is GridCellUI
+
+
+func is_bottom_right_corner() -> bool:
+ return neighbour_bottom == null and neighbour_right == null \
+ and neighbour_up is GridCellUI and neighbour_left is GridCellUI
+
+
+func is_top_border() -> bool:
+ return neighbour_up == null \
+ and neighbour_bottom is GridCellUI and neighbour_right is GridCellUI and neighbour_left is GridCellUI
+
+
+func is_bottom_border() -> bool:
+ return neighbour_bottom == null \
+ and neighbour_up is GridCellUI and neighbour_right is GridCellUI and neighbour_left is GridCellUI
+
+
+func is_right_border() -> bool:
+ return neighbour_right == null \
+ and neighbour_up is GridCellUI and neighbour_bottom is GridCellUI and neighbour_left is GridCellUI
+
+
+func is_left_border() -> bool:
+ return neighbour_left == null \
+ and neighbour_up is GridCellUI and neighbour_bottom is GridCellUI and neighbour_right is GridCellUI
+#endregion
+
+#region Piece related
+func is_empty() -> bool:
+ return current_piece == null
+
+
+func has_piece() -> bool:
+ return current_piece is PieceUI
+
+
+func assign_piece(new_piece: PieceUI, overwrite: bool = false) -> void:
+ if overwrite:
+ replace_piece(new_piece)
+
+ elif can_contain_piece and is_empty():
+ current_piece = new_piece
+
+
+func replace_piece(new_piece: PieceUI) -> PieceUI:
+ var previous_piece = current_piece
+ current_piece = null
+
+ assign_piece(new_piece)
+
+ return previous_piece
+
+
+func remove_piece():
+ if has_piece():
+ var previous_piece = current_piece
+ current_piece = null
+
+ return previous_piece
+
+ return null
+
+func swap_piece_with(other_cell: GridCellUI) -> bool:
+ if can_swap_piece_with(other_cell):
+ var previous_piece: PieceUI = current_piece
+ var new_piece = other_cell.current_piece
+
+ remove_piece()
+ assign_piece(new_piece)
+
+ other_cell.remove_piece()
+ other_cell.assign_piece(previous_piece)
+
+ swapped_piece.emit(self, other_cell)
+
+ return true
+
+ swap_rejected.emit(self, other_cell)
+
+ return false
+
+
+func available_neighbours(include_diagonals: bool = false) -> Array[GridCellUI]:
+ var neighbours: Array[GridCellUI] = []
+
+ if include_diagonals:
+ neighbours.assign(PluginUtilities.remove_falsy_values([
+ neighbour_up,
+ neighbour_bottom,
+ neighbour_right,
+ neighbour_left,
+ diagonal_neighbour_top_right,
+ diagonal_neighbour_top_left,
+ diagonal_neighbour_bottom_right,
+ diagonal_neighbour_bottom_left
+ ]))
+ else:
+
+ neighbours.assign(PluginUtilities.remove_falsy_values([
+ neighbour_up,
+ neighbour_bottom,
+ neighbour_right,
+ neighbour_left,
+ ]))
+
+ return neighbours
+
+
+func can_swap_piece_with(other_cell: GridCellUI) -> bool:
+ return other_cell != self \
+ and other_cell.has_piece() \
+ and has_piece() \
+ and can_contain_piece \
+ and other_cell.can_contain_piece \
+ and not current_piece.is_locked \
+ and not other_cell.current_piece.is_locked \
+ and other_cell.current_piece != current_piece \
+ and current_piece.can_be_swapped() and other_cell.current_piece.can_be_swapped()
+
+#endregion
diff --git a/addons/ninetailsrabbit.match3_board/src/components/highlighters/cell_highlighter.gd b/addons/ninetailsrabbit.match3_board/src/components/highlighters/cell_highlighter.gd
new file mode 100644
index 0000000..634c8fa
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/highlighters/cell_highlighter.gd
@@ -0,0 +1,32 @@
+class_name CellHighlighter extends Node
+
+@export var highlight_texture: Texture2D = Match3Preloader.HighlightedTexture
+
+@onready var board = get_tree().get_first_node_in_group(Match3Preloader.BoardGroupName)
+
+func _enter_tree() -> void:
+ name = "CellHighlighter"
+
+
+func highlight_cell(grid_cell: GridCellUI) -> void:
+ grid_cell.change_background_image(highlight_texture)
+
+
+func highlight_cells(origin_cell: GridCellUI, swap_mode = board.swap_mode) -> void:
+ match swap_mode:
+ Match3Preloader.BoardMovements.Adjacent:
+ for grid_cell: GridCellUI in origin_cell.available_neighbours(false):
+ highlight_cell(grid_cell)
+ Match3Preloader.BoardMovements.Cross:
+ for grid_cell: GridCellUI in board.cross_cells_from(origin_cell):
+ highlight_cell(grid_cell)
+ Match3Preloader.BoardMovements.Free:
+ highlight_cell(origin_cell)
+ Match3Preloader.BoardMovements.CrossDiagonal:
+ for grid_cell: GridCellUI in board.cross_diagonal_cells_from(origin_cell):
+ highlight_cell(grid_cell)
+
+
+func remove_current_highlighters() -> void:
+ for grid_cell: GridCellUI in board.grid_cells_flattened:
+ grid_cell.change_to_original_background_image()
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/piece_definition_resource.gd b/addons/ninetailsrabbit.match3_board/src/components/pieces/piece_definition_resource.gd
new file mode 100644
index 0000000..e3ae942
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/piece_definition_resource.gd
@@ -0,0 +1,63 @@
+class_name PieceDefinitionResource extends Resource
+
+enum PieceType {
+ Normal,
+ Special,
+ Obstacle
+}
+
+@export var id: StringName
+@export var name: String
+@export_multiline var description: String
+## The weight for probability generation
+@export var weight: float = 1.0
+## The type of this piece, refers to behaviour
+@export var type: PieceType = PieceType.Normal
+## A piece can share a behaviour (type) but with different shape so they are not strictly equals
+@export var shape: String = ""
+@export var image: Texture2D
+@export var can_be_swapped: bool = true
+@export var can_be_moved: bool = true
+@export var can_be_shuffled: bool = true
+@export var can_be_triggered: bool = false
+@export var can_be_replaced: bool = true
+@export var can_be_consumed: bool = true
+@export var match_4_piece: PieceDefinitionResource
+@export var match_5_piece: PieceDefinitionResource
+@export var tshape_piece: PieceDefinitionResource
+@export var lshape_piece: PieceDefinitionResource
+
+var total_accum_weight: float = 0.0
+
+
+#region Overridables
+func match_with(other_piece: PieceDefinitionResource) -> bool:
+ if not can_be_consumed:
+ return false
+
+ match type:
+ PieceType.Normal:
+ return (type == other_piece.type and shape == other_piece.shape) or (other_piece.is_special() and shape == other_piece.shape)
+ PieceType.Special:
+ return shape == other_piece.shape
+ PieceType.Obstacle:
+ return false
+ _:
+ return false
+#endregion
+
+
+func is_normal() -> bool:
+ return type == PieceType.Normal
+
+
+func is_special() -> bool:
+ return type == PieceType.Special
+
+
+func is_obstacle() -> bool:
+ return type == PieceType.Obstacle
+
+
+func reset_accum_weight() -> void:
+ total_accum_weight = 0.0
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/piece_ui.gd b/addons/ninetailsrabbit.match3_board/src/components/pieces/piece_ui.gd
new file mode 100644
index 0000000..c6f6623
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/piece_ui.gd
@@ -0,0 +1,73 @@
+class_name PieceUI extends Node2D
+
+signal selected
+signal unselected
+
+const GroupName: String = "piece"
+
+@export var piece_definition: PieceDefinitionResource
+@export var cell_size: Vector2i = Vector2(48, 48)
+@export var texture_scale: float = 0.85
+
+var board
+var is_locked: bool = false
+var is_selected: bool = false:
+ set(value):
+ if value != is_selected and not is_locked and is_inside_tree():
+ is_selected = value
+
+ if is_selected:
+ selected.emit()
+ board.piece_selected.emit(self as PieceUI)
+ else:
+ unselected.emit()
+ board.piece_unselected.emit(self as PieceUI)
+
+
+func _enter_tree() -> void:
+ add_to_group(GroupName)
+
+ name = "%s-%s" % [piece_definition.type, piece_definition.shape.to_pascal_case()]
+ is_selected = false
+ z_index = 20
+
+ if board == null:
+ board = get_tree().get_first_node_in_group(Match3Preloader.BoardGroupName)
+
+
+func prepare_sprite(sprite: Sprite2D) -> void:
+ sprite.texture = piece_definition.image
+ var texture_size = sprite.texture.get_size()
+ sprite.scale = Vector2(cell_size.x / texture_size.x, cell_size.y / texture_size.y) * texture_scale
+
+
+func match_with(other_piece:PieceUI) -> bool:
+ return piece_definition.match_with(other_piece.piece_definition)
+
+
+func can_be_swapped() -> bool:
+ return piece_definition.can_be_swapped
+
+
+func can_be_moved() -> bool:
+ return piece_definition.can_be_moved
+
+
+func can_be_replaced() -> bool:
+ return piece_definition.can_be_replaced
+
+
+func can_be_shuffled() -> bool:
+ return piece_definition.can_be_shuffled
+
+
+func can_be_triggered() -> bool:
+ return piece_definition.can_be_triggered
+
+
+func lock() -> void:
+ is_locked = true
+
+
+func unlock() -> void:
+ is_locked = false
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/cross_piece.gd b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/cross_piece.gd
new file mode 100644
index 0000000..b566306
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/cross_piece.gd
@@ -0,0 +1,73 @@
+class_name CrossPiece extends PieceUI
+
+@onready var draggable_sprite: DraggableSprite2D = $DraggableSprite2D
+@onready var detection_area: Area2D = $DraggableSprite2D/DetectionArea
+@onready var piece_area: Area2D = $PieceArea
+@onready var detection_area_collision: CollisionShape2D = $DraggableSprite2D/DetectionArea/CollisionShape2D
+@onready var piece_area_collision: CollisionShape2D = $PieceArea/CollisionShape2D
+
+var original_z_index: int = 0
+
+func _ready() -> void:
+ prepare_sprite(draggable_sprite)
+ prepare_area_detectors()
+
+ original_z_index = draggable_sprite.z_index
+
+ draggable_sprite.drag_started.connect(on_drag_started)
+ draggable_sprite.drag_ended.connect(on_drag_ended)
+ board.locked.connect(on_board_locked)
+ board.unlocked.connect(on_board_unlocked)
+
+
+func prepare_area_detectors() -> void:
+ piece_area.collision_layer = board.pieces_collision_layer
+ piece_area.collision_mask = 0
+ piece_area.monitoring = false
+ piece_area.monitorable = true
+
+ detection_area.collision_layer = 0
+ detection_area.collision_mask = board.pieces_collision_layer
+ detection_area.monitoring = true
+ detection_area.monitorable = false
+
+ piece_area_collision.shape.size = board.cell_size - Vector2i.ONE * (board.cell_size.x / 2)
+ detection_area_collision.shape.size = board.cell_size / 2
+
+ detection_area_collision.set_deferred("disabled", true)
+
+
+#region Signal callbacks
+func on_drag_started() -> void:
+ draggable_sprite.z_index = original_z_index + 100
+ z_as_relative = false
+
+ is_selected = true
+
+ piece_area_collision.set_deferred("disabled", true)
+ detection_area_collision.set_deferred("disabled", false)
+
+
+func on_drag_ended() -> void:
+ draggable_sprite.z_index = original_z_index
+ z_as_relative = true
+
+ is_selected = false
+
+ var piece_detected_areas: Array[Area2D] = detection_area.get_overlapping_areas()
+
+ if piece_detected_areas.size() > 0:
+ board.swap_requested.emit(self, piece_detected_areas.front().get_parent() as PieceUI)
+
+ piece_area_collision.set_deferred("disabled", false)
+ detection_area_collision.set_deferred("disabled", true)
+
+
+func on_board_locked() -> void:
+ process_mode = PROCESS_MODE_DISABLED
+
+
+func on_board_unlocked() -> void:
+ process_mode = PROCESS_MODE_INHERIT
+
+#endregion
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/cross_piece.tscn b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/cross_piece.tscn
new file mode 100644
index 0000000..ca1ad3a
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/cross_piece.tscn
@@ -0,0 +1,25 @@
+[gd_scene load_steps=5 format=3 uid="uid://buvlnpexha0yx"]
+
+[ext_resource type="Script" path="res://addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/cross_piece.gd" id="1_4cxhj"]
+[ext_resource type="Script" path="res://addons/ninetailsrabbit.match3_board/src/components/draggable_sprite_2d.gd" id="2_5oyqi"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_mvyoo"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_jvqmw"]
+
+[node name="CrossPiece" type="Node2D"]
+script = ExtResource("1_4cxhj")
+
+[node name="DraggableSprite2D" type="Sprite2D" parent="."]
+script = ExtResource("2_5oyqi")
+smooth_factor = 15.0
+
+[node name="DetectionArea" type="Area2D" parent="DraggableSprite2D"]
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="DraggableSprite2D/DetectionArea"]
+shape = SubResource("RectangleShape2D_mvyoo")
+
+[node name="PieceArea" type="Area2D" parent="."]
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="PieceArea"]
+shape = SubResource("RectangleShape2D_jvqmw")
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector.gd b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector.gd
new file mode 100644
index 0000000..66b7811
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector.gd
@@ -0,0 +1,136 @@
+class_name LineConnector extends Line2D
+
+signal added_piece(piece: LineConnectorPiece)
+signal match_selected(selected_pieces: Array[LineConnectorPiece])
+
+var pieces_connected: Array[LineConnectorPiece] = []
+var detection_area: Area2D
+var origin_piece: LineConnectorPiece
+var previous_matches: Array[LineConnectorPiece] = []
+var possible_next_matches: Array[LineConnectorPiece] = []
+
+
+func _exit_tree() -> void:
+ match_selected.emit(pieces_connected)
+
+ for piece: LineConnectorPiece in pieces_connected:
+ piece.piece_area.process_mode =Node.PROCESS_MODE_INHERIT
+
+ previous_matches.append_array(possible_next_matches)
+ ## TODO DESELECT HIGHLIGHTER ON THIS MATCHES
+
+ if detection_area and not detection_area.is_queued_for_deletion():
+ detection_area.queue_free()
+
+
+func _enter_tree() -> void:
+ width = 1.5
+ default_color = Color.YELLOW
+
+ added_piece.connect(on_added_piece)
+ match_selected.connect(on_line_match_selected)
+
+
+func _ready() -> void:
+ set_process(false)
+
+
+func _process(_delta: float) -> void:
+ if points.is_empty() or detection_area == null:
+ return
+
+ var mouse_position: Vector2 = get_global_mouse_position()
+
+ remove_point(points.size() - 1)
+ add_point(mouse_position)
+
+ detection_area.global_position = mouse_position
+
+
+func add_piece(new_piece: LineConnectorPiece) -> void:
+ new_piece.piece_area.process_mode = Node.PROCESS_MODE_DISABLED
+
+ pieces_connected.append(new_piece)
+ clear_points()
+
+ for piece_connected: LineConnectorPiece in pieces_connected:
+ add_point(piece_connected.global_position)
+
+ add_point(get_global_mouse_position())
+
+ added_piece.emit(new_piece)
+
+
+func detect_new_matches_from_last_piece(last_piece: LineConnectorPiece) -> void:
+ var origin_cell: GridCellUI = last_piece.board.grid_cell_from_piece(last_piece)
+
+ if origin_cell is GridCellUI:
+ var adjacent_cells: Array[GridCellUI] = origin_cell.available_neighbours(true)
+
+ previous_matches = possible_next_matches.duplicate()
+ possible_next_matches.clear()
+
+ for cell: GridCellUI in adjacent_cells:
+ var piece: LineConnectorPiece = cell.current_piece as LineConnectorPiece
+
+ if not pieces_connected.has(piece) and piece.match_with(last_piece):
+ possible_next_matches.append(piece)
+
+
+func prepare_detection_area(piece: PieceUI) -> void:
+ detection_area = Area2D.new()
+ detection_area.collision_layer = 0
+ detection_area.collision_mask = piece.board.pieces_collision_layer
+ detection_area.monitorable = false
+ detection_area.monitoring = true
+ detection_area.process_priority = 2
+ detection_area.disable_mode = CollisionObject2D.DISABLE_MODE_MAKE_STATIC
+
+ var collision_shape = CollisionShape2D.new()
+ collision_shape.shape = RectangleShape2D.new()
+ collision_shape.shape.size = origin_piece.cell_size / 2
+
+ detection_area.add_child(collision_shape)
+
+ get_tree().root.add_child(detection_area)
+
+ detection_area.global_position = get_global_mouse_position()
+ detection_area.area_entered.connect(on_piece_detected)
+
+ origin_piece.piece_area.process_mode = Node.PROCESS_MODE_DISABLED
+ set_process(true)
+
+
+#region Signal callbacks
+func on_piece_detected(other_area: Area2D) -> void:
+ var piece: LineConnectorPiece = other_area.get_parent() as LineConnectorPiece
+
+ if possible_next_matches.has(piece):
+ add_piece(piece)
+
+
+func on_added_piece(piece: LineConnectorPiece) -> void:
+ if pieces_connected.size() == 1:
+ origin_piece = piece
+ z_index = origin_piece.z_index + 25
+ z_as_relative = true
+ prepare_detection_area(origin_piece)
+
+ if pieces_connected.size() < piece.board.max_match:
+ detect_new_matches_from_last_piece(piece)
+
+ ## TODO - CELL HIGHLIGHTERS HERE
+
+ else:
+ set_process(false)
+ remove_point(points.size() - 1)
+ detection_area.process_mode = Node.PROCESS_MODE_DISABLED
+
+
+func on_line_match_selected(selected_pieces: Array[LineConnectorPiece]) -> void:
+ var cells: Array[GridCellUI] = []
+ cells.assign(selected_pieces.map(func(piece: PieceUI): return piece.board.grid_cell_from_piece(piece)))
+
+ origin_piece.board.consume_requested.emit(Sequence.new(cells, Sequence.Shapes.LineConnected))
+
+#endregion
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector_piece.gd b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector_piece.gd
new file mode 100644
index 0000000..672c4f4
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector_piece.gd
@@ -0,0 +1,46 @@
+class_name LineConnectorPiece extends PieceUI
+
+@onready var sprite: Sprite2D = $Sprite2D
+@onready var piece_area: Area2D = $PieceArea
+
+var mouse_region: Button
+var line_connector: LineConnector
+
+func _ready() -> void:
+ prepare_sprite(sprite)
+
+ piece_area.collision_layer = board.pieces_collision_layer
+ piece_area.collision_mask = 0
+ piece_area.monitorable = true
+ piece_area.monitoring = false
+
+ if mouse_region == null:
+ mouse_region = Button.new()
+ mouse_region.self_modulate.a8 = 100 ## TODO - CHANGE TO 0 WHEN FINISH DEBUG
+ sprite.add_child(mouse_region)
+
+
+ mouse_region.position = Vector2.ZERO
+ mouse_region.anchors_preset = Control.PRESET_FULL_RECT
+ mouse_region.button_down.connect(on_mouse_region_pressed)
+ mouse_region.button_up.connect(on_mouse_region_released)
+
+
+func on_mouse_region_pressed() -> void:
+ if board.is_locked:
+ return
+
+ is_selected = true
+
+ line_connector = LineConnector.new()
+ get_tree().root.add_child(line_connector)
+ line_connector.add_piece(self)
+
+
+func on_mouse_region_released() -> void:
+ is_selected = false
+
+ if line_connector and not line_connector.is_queued_for_deletion():
+ line_connector.queue_free()
+
+ line_connector = null
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector_piece.tscn b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector_piece.tscn
new file mode 100644
index 0000000..150aa50
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector_piece.tscn
@@ -0,0 +1,15 @@
+[gd_scene load_steps=3 format=3 uid="uid://b43jr3pipnh13"]
+
+[ext_resource type="Script" path="res://addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector_piece.gd" id="1_wfs0i"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_v5dgu"]
+
+[node name="LineConnectorPiece" type="Node2D"]
+script = ExtResource("1_wfs0i")
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+
+[node name="PieceArea" type="Area2D" parent="."]
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="PieceArea"]
+shape = SubResource("RectangleShape2D_v5dgu")
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/swap_piece.gd b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/swap_piece.gd
new file mode 100644
index 0000000..8437adb
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/swap_piece.gd
@@ -0,0 +1,22 @@
+class_name SwapPiece extends PieceUI
+
+@onready var sprite: Sprite2D = $Sprite2D
+
+var mouse_region: Button
+
+
+func _ready() -> void:
+ prepare_sprite(sprite)
+
+ if mouse_region == null:
+ mouse_region = Button.new()
+ mouse_region.self_modulate.a8 = 100 ## TODO - CHANGE TO 0 WHEN FINISH DEBUG
+ sprite.add_child(mouse_region)
+
+ mouse_region.position = Vector2.ZERO
+ mouse_region.anchors_preset = Control.PRESET_FULL_RECT
+ mouse_region.pressed.connect(on_mouse_region_pressed)
+
+
+func on_mouse_region_pressed() -> void:
+ is_selected = !is_selected
diff --git a/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/swap_piece.tscn b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/swap_piece.tscn
new file mode 100644
index 0000000..6d4df8e
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/swap_piece.tscn
@@ -0,0 +1,8 @@
+[gd_scene load_steps=2 format=3 uid="uid://86cb5uvabm7j"]
+
+[ext_resource type="Script" path="res://addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/swap_piece.gd" id="1_ptyn7"]
+
+[node name="SwapPiece" type="Node2D"]
+script = ExtResource("1_ptyn7")
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
diff --git a/addons/ninetailsrabbit.match3_board/src/components/sequence.gd b/addons/ninetailsrabbit.match3_board/src/components/sequence.gd
new file mode 100644
index 0000000..a5fb2fb
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/components/sequence.gd
@@ -0,0 +1,149 @@
+class_name Sequence
+
+signal consumed(pieces_consumed: Array[PieceDefinitionResource])
+
+enum Shapes {
+ Horizontal,
+ Vertical,
+ TShape,
+ LShape,
+ Diagonal,
+ LineConnected,
+ Irregular
+}
+
+
+var cells: Array[GridCellUI] = []
+var shape: Shapes = Shapes.Irregular
+
+
+func _init(_cells: Array[GridCellUI], _shape: Shapes = Shapes.Irregular) -> void:
+ cells = _cells.filter(func(grid_cell: GridCellUI): return grid_cell.has_piece())
+ shape = _detect_shape() if _shape == Shapes.Irregular else _shape
+
+
+func size() -> int:
+ return cells.size()
+
+
+func consume(except: Array[GridCellUI] = []) -> void:
+ var pieces_consumed: Array[PieceDefinitionResource] = []
+ pieces_consumed.assign(self.pieces().map(func(piece: PieceUI): return piece.piece_definition))
+
+ consumed.emit(pieces_consumed)
+
+ for cell: GridCellUI in cells.filter(func(grid_cell: GridCellUI): return not except.has(grid_cell)):
+ consume_cell(cell)
+
+
+func consume_cell(cell: GridCellUI) -> void:
+ if cells.has(cell):
+ var removed_piece = cell.remove_piece()
+
+ if removed_piece is PieceUI:
+ removed_piece.queue_free()
+
+
+func contain_special_piece() -> bool:
+ return pieces().filter(func(piece: PieceUI): return piece.piece_definition.is_special()).size() > 0
+
+
+func pieces() -> Array[PieceUI]:
+ var current_pieces: Array[PieceUI] = []
+
+ current_pieces.assign(PluginUtilities.remove_falsy_values(cells.map(func(grid_cell: GridCellUI): return grid_cell.current_piece)))
+
+ return current_pieces
+
+
+func all_pieces_are_of_type(type: PieceDefinitionResource.PieceType) -> bool:
+ if pieces().is_empty():
+ return false
+ else:
+ return pieces().all(func(piece: PieceUI): return piece.piece_definition.type == type)
+
+
+#region Cell position in sequence
+func middle_cell() -> GridCellUI:
+ return PluginUtilities.middle_element(cells)
+
+
+func top_edge_cell():
+ if shape == Shapes.Vertical:
+ return cells.front()
+
+ return null
+
+
+func bottom_edge_cell():
+ if shape == Shapes.Vertical:
+ return cells.back()
+
+ return null
+
+
+func right_edge_cell():
+ if shape == Shapes.Horizontal:
+ return cells.back()
+
+ return null
+
+
+func left_edge_cell():
+ if shape == Shapes.Horizontal:
+ return cells.front()
+
+ return null
+
+#endregion
+
+
+#region Shape detector
+func is_horizontal_shape() -> bool:
+ return shape == Shapes.Horizontal
+
+
+func is_vertical_shape() -> bool:
+ return shape == Shapes.Vertical
+
+
+func is_horizontal_or_vertical_shape() -> bool:
+ return is_horizontal_shape() or is_vertical_shape()
+
+
+func is_tshape() -> bool:
+ return shape == Shapes.TShape
+
+
+func is_lshape() -> bool:
+ return shape == Shapes.LShape
+
+
+func is_diagonal_shape() -> bool:
+ return shape == Shapes.Diagonal
+
+
+func is_line_connected_shape() -> bool:
+ return shape == Shapes.LineConnected
+
+
+func _detect_shape() -> Shapes:
+ var is_horizontal: bool = false
+ var is_vertical: bool = false
+ var is_diagonal: bool = false
+
+ for index: int in cells.size():
+ is_horizontal = index == 0 or cells[index].in_same_column_as(cells[index - 1])
+ is_vertical = index == 0 or cells[index].in_same_row_as(cells[index - 1])
+ is_diagonal = index == 0 or cells[index].in_diagonal_with(cells[index - 1])
+
+ ## We don't need to detect TShape or LShape as this ones are set always manually when the sequence it's created
+ if is_horizontal:
+ return Shapes.Horizontal
+ elif is_vertical:
+ return Shapes.Vertical
+ elif is_diagonal:
+ return Shapes.Diagonal
+ else:
+ return Shapes.Irregular
+#endregion
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/match3_preloader.gd b/addons/ninetailsrabbit.match3_board/src/debug_ui/match3_preloader.gd
new file mode 100644
index 0000000..967a1e6
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/debug_ui/match3_preloader.gd
@@ -0,0 +1,54 @@
+class_name Match3Preloader
+
+#region Shared
+const BoardGroupName: String = "match3-board"
+
+class FallMovement:
+ var from_cell: GridCellUI
+ var to_cell: GridCellUI
+ var is_diagonal: bool = false
+
+ func _init(_from_cell: GridCellUI, _to_cell: GridCellUI, _is_diagonal: bool = false) -> void:
+ from_cell = _from_cell
+ to_cell = _to_cell
+ is_diagonal = _is_diagonal
+
+
+enum BoardState {
+ WaitForInput,
+ Fill,
+ Consume
+}
+
+enum BoardMovements {
+ Adjacent,
+ Free,
+ Cross,
+ CrossDiagonal,
+ ConnectLine
+}
+
+enum BoardFillModes {
+ FallDown,
+ Side,
+ InPlace
+}
+#endregion
+
+#region Textures
+const BlueGem = preload("res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/blue_gem.png")
+const GreenGem = preload("res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/green_gem.png")
+const PurpleGem = preload("res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/purple_gem.png")
+const YellowGem = preload("res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/yellow_gem.png")
+#endregion
+
+#region Cell textures
+const EvenCellTexture = preload("res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/even.png")
+const HighlightedTexture = preload("res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/highlighted.png")
+const OddCellTexture = preload("res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/odd.png")
+#endregion
+#region Pieces
+const CrossPieceScene = preload("res://addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/cross_piece.tscn")
+const LineConnectorPieceScene = preload("res://addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/line_connector_piece.tscn")
+const SwapPieceScene = preload("res://addons/ninetailsrabbit.match3_board/src/components/pieces/swap_mode/swap_piece.tscn")
+#endregion
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/even.png b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/even.png
new file mode 100644
index 0000000..e9d76c1
Binary files /dev/null and b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/even.png differ
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/even.png.import b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/even.png.import
new file mode 100644
index 0000000..74019c3
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/even.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bitl5y7wpoxpl"
+path="res://.godot/imported/even.png-eafba3ef9dd9494d61f234629535822d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/even.png"
+dest_files=["res://.godot/imported/even.png-eafba3ef9dd9494d61f234629535822d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/highlighted.png b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/highlighted.png
new file mode 100644
index 0000000..d3d05ae
Binary files /dev/null and b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/highlighted.png differ
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/highlighted.png.import b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/highlighted.png.import
new file mode 100644
index 0000000..c421bd4
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/highlighted.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b74ndw12uja07"
+path="res://.godot/imported/highlighted.png-61827cdcef9f7f1ff0c8578f30318fef.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/highlighted.png"
+dest_files=["res://.godot/imported/highlighted.png-61827cdcef9f7f1ff0c8578f30318fef.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/odd.png b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/odd.png
new file mode 100644
index 0000000..f505743
Binary files /dev/null and b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/odd.png differ
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/odd.png.import b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/odd.png.import
new file mode 100644
index 0000000..bf92543
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/odd.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6m4agye6vlqx"
+path="res://.godot/imported/odd.png-77e80c98066333ff8280c6e5197e76e6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_cells/odd.png"
+dest_files=["res://.godot/imported/odd.png-77e80c98066333ff8280c6e5197e76e6.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/blue_gem.png b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/blue_gem.png
new file mode 100644
index 0000000..09850b0
Binary files /dev/null and b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/blue_gem.png differ
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/blue_gem.png.import b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/blue_gem.png.import
new file mode 100644
index 0000000..7694d7b
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/blue_gem.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ctiniq36c2dh7"
+path="res://.godot/imported/blue_gem.png-c1b98f6a57a3a9efd7acea071d3432e7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/blue_gem.png"
+dest_files=["res://.godot/imported/blue_gem.png-c1b98f6a57a3a9efd7acea071d3432e7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/green_gem.png b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/green_gem.png
new file mode 100644
index 0000000..a0224e3
Binary files /dev/null and b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/green_gem.png differ
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/green_gem.png.import b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/green_gem.png.import
new file mode 100644
index 0000000..dc7cd77
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/green_gem.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dchtf3oa72c7v"
+path="res://.godot/imported/green_gem.png-c1766fb9f47b6e1288930379186f7079.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/green_gem.png"
+dest_files=["res://.godot/imported/green_gem.png-c1766fb9f47b6e1288930379186f7079.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/purple_gem.png b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/purple_gem.png
new file mode 100644
index 0000000..bc95f91
Binary files /dev/null and b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/purple_gem.png differ
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/purple_gem.png.import b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/purple_gem.png.import
new file mode 100644
index 0000000..c8c88dd
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/purple_gem.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://frw370xlyi52"
+path="res://.godot/imported/purple_gem.png-0a2001604aefe50ffb695755dd2f1cec.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/purple_gem.png"
+dest_files=["res://.godot/imported/purple_gem.png-0a2001604aefe50ffb695755dd2f1cec.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/yellow_gem.png b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/yellow_gem.png
new file mode 100644
index 0000000..2b5f3cc
Binary files /dev/null and b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/yellow_gem.png differ
diff --git a/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/yellow_gem.png.import b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/yellow_gem.png.import
new file mode 100644
index 0000000..d396fa8
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/yellow_gem.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bxh84kxgxf2df"
+path="res://.godot/imported/yellow_gem.png-3f542e9d329541e5a5681f9fb02f33af.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/ninetailsrabbit.match3_board/src/debug_ui/preview_pieces/yellow_gem.png"
+dest_files=["res://.godot/imported/yellow_gem.png-3f542e9d329541e5a5681f9fb02f33af.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
diff --git a/addons/ninetailsrabbit.match3_board/src/match3_board.gd b/addons/ninetailsrabbit.match3_board/src/match3_board.gd
new file mode 100644
index 0000000..44dbf0c
--- /dev/null
+++ b/addons/ninetailsrabbit.match3_board/src/match3_board.gd
@@ -0,0 +1,924 @@
+@tool
+class_name Match3Board extends Node2D
+
+signal swapped_pieces(from: PieceUI, to: PieceUI, matches: Array[Sequence])
+signal swap_requested(from: PieceUI, to: PieceUI)
+signal swap_failed(from: GridCellUI, to: GridCellUI)
+signal swap_rejected(from: PieceUI, to: PieceUI)
+signal consume_requested(sequence: Sequence)
+signal piece_selected(piece: PieceUI)
+signal piece_unselected(piece: PieceUI)
+signal state_changed(from: Match3Preloader.BoardState, to: Match3Preloader.BoardState)
+signal prepared_board
+signal locked
+signal unlocked
+
+@export_group("Debug")
+@export var preview_grid_in_editor: bool = false:
+ set(value):
+ if value != preview_grid_in_editor:
+ preview_grid_in_editor = value
+
+ if preview_grid_in_editor:
+ draw_preview_grid()
+ else:
+ remove_preview_sprites()
+
+## Tool button to clean the current grid preview
+@export var clean_current_preview: bool = false:
+ get:
+ return false
+ set(value):
+ remove_preview_sprites()
+
+@export var preview_pieces: Array[Texture2D] = [
+ Match3Preloader.BlueGem,
+ Match3Preloader.GreenGem,
+ Match3Preloader.YellowGem,
+ Match3Preloader.PurpleGem
+]
+
+@export var odd_cell_texture: Texture2D = Match3Preloader.OddCellTexture
+@export var even_cell_texture: Texture2D = Match3Preloader.EvenCellTexture
+@export var empty_cells: Array[Vector2] = []:
+ set(value):
+ if empty_cells != value:
+ empty_cells = value
+ draw_preview_grid()
+
+@export_group("Size")
+@export var grid_width: int = 8:
+ set(value):
+ if grid_width != value:
+ grid_width = max(MinGridWidth, value)
+ draw_preview_grid()
+@export var grid_height: int = 7:
+ set(value):
+ if grid_height != value:
+ grid_height = max(MinGridHeight, value)
+ draw_preview_grid()
+
+@export var cell_size: Vector2i = Vector2i(48, 48):
+ set(value):
+ if value != cell_size:
+ cell_size = value
+ draw_preview_grid()
+@export var cell_offset: Vector2i = Vector2i(5, 10):
+ set(value):
+ if value != cell_offset:
+ cell_offset = value
+ draw_preview_grid()
+
+@export_group("Matches")
+@export var pieces_collision_layer: int = 1
+@export var swap_mode: Match3Preloader.BoardMovements = Match3Preloader.BoardMovements.Adjacent
+@export var fill_mode = Match3Preloader.BoardFillModes.FallDown
+@export var available_pieces: Array[PieceDefinitionResource] = []
+@export var available_moves_on_start: int = 25
+@export var allow_matches_on_start: bool = false
+@export var horizontal_shape: bool = true
+@export var vertical_shape: bool = true
+@export var tshape: bool = true
+@export var lshape: bool = true
+@export var min_match: int = 3:
+ set(value):
+ min_match = max(3, value)
+@export var max_match: int = 5:
+ set(value):
+ max_match = max(min_match, value)
+@export var min_special_match: int = 2:
+ set(value):
+ min_special_match = max(2, value)
+@export var max_special_match: int = 2:
+ set(value):
+ max_special_match = max(min_special_match, value)
+
+
+const MinGridWidth: int = 3
+const MinGridHeight: int = 3
+
+
+var pieces_by_swap_mode: Dictionary = {
+ Match3Preloader.BoardMovements.Adjacent: Match3Preloader.SwapPieceScene,
+ Match3Preloader.BoardMovements.Free: Match3Preloader.SwapPieceScene,
+ Match3Preloader.BoardMovements.Cross: Match3Preloader.CrossPieceScene,
+ Match3Preloader.BoardMovements.CrossDiagonal: Match3Preloader.CrossPieceScene,
+ Match3Preloader.BoardMovements.ConnectLine: Match3Preloader.LineConnectorPieceScene
+}
+
+#region Features
+var piece_weight_generator: PieceWeightGenerator
+var piece_animator: PieceAnimator
+var sequence_consumer: SequenceConsumer
+var cell_highlighter: CellHighlighter
+#endregion
+
+var debug_preview_node: Node2D
+var grid_cells: Array = [] # Multidimensional to access cells by column & row
+var grid_cells_flattened: Array[GridCellUI] = []
+var current_selected_piece: PieceUI
+var is_locked: bool = false:
+ set(value):
+ if value != is_locked:
+ is_locked = value
+
+ if is_locked:
+ locked.emit()
+ else:
+ unlocked.emit()
+
+var current_state: Match3Preloader.BoardState = Match3Preloader.BoardState.WaitForInput:
+ set(new_state):
+ if new_state != current_state:
+ state_changed.emit(current_state, new_state)
+ current_state = new_state
+
+var pending_sequences: Array[Sequence] = []
+
+
+func _enter_tree() -> void:
+ if not Engine.is_editor_hint():
+ add_to_group(Match3Preloader.BoardGroupName)
+ remove_preview_sprites()
+
+ prepared_board.connect(on_prepared_board)
+ piece_selected.connect(on_piece_selected)
+ piece_unselected.connect(on_piece_unselected)
+ swap_requested.connect(on_swap_requested)
+ swap_failed.connect(on_swap_failed)
+ swap_rejected.connect(on_swap_rejected)
+ swapped_pieces.connect(on_swapped_pieces)
+ consume_requested.connect(on_consume_requested)
+ state_changed.connect(on_state_changed)
+
+ if piece_weight_generator == null:
+ piece_weight_generator = PieceWeightGenerator.new()
+
+ if cell_highlighter == null:
+ cell_highlighter = CellHighlighter.new()
+
+ if piece_animator == null:
+ piece_animator = PieceAnimator.new()
+
+ if sequence_consumer == null:
+ sequence_consumer = SequenceConsumer.new()
+
+ add_child(cell_highlighter)
+ add_child(piece_animator)
+ add_child(sequence_consumer)
+
+
+func _ready() -> void:
+ if not Engine.is_editor_hint():
+ prepare_board()
+
+#region Board
+## Only prepares the grid cells based on width and height
+func prepare_board():
+ if not Engine.is_editor_hint() and grid_cells.is_empty():
+
+ for column in grid_width:
+ grid_cells.append([])
+
+ for row in grid_height:
+ var grid_cell: GridCellUI = GridCellUI.new(row, column)
+ grid_cell.cell_size = cell_size
+
+ grid_cells[column].append(grid_cell)
+
+ grid_cells_flattened.append_array(PluginUtilities.flatten(grid_cells))
+
+ add_pieces(available_pieces)
+
+ prepared_board.emit()
+
+ return self
+
+
+func add_pieces(new_pieces: Array[PieceDefinitionResource]) -> void:
+ piece_weight_generator.add_available_pieces(new_pieces)
+
+
+func generate_new_piece(selected_swap_mode: Match3Preloader.BoardMovements = swap_mode) -> PieceUI:
+ return pieces_by_swap_mode[selected_swap_mode].instantiate() as PieceUI
+
+
+func draw_board():
+ for grid_cell: GridCellUI in grid_cells_flattened:
+ draw_grid_cell(grid_cell)
+ draw_random_piece_on_cell(grid_cell)
+
+ if not allow_matches_on_start:
+ remove_matches_from_board()
+
+ return self
+
+
+func remove_matches_from_board() -> void:
+ var sequences: Array[Sequence] = find_board_sequences()
+
+ while sequences.size() > 0:
+ for sequence: Sequence in sequences:
+ var cells_to_change = sequence.cells.slice(0, (sequence.cells.size() / min_match) + 1)
+ var piece_exceptions: Array[PieceDefinitionResource] = []
+ piece_exceptions.assign(cells_to_change.map(func(cell: GridCellUI): return cell.current_piece.piece_definition))
+
+ for current_cell: GridCellUI in cells_to_change:
+ var removed_piece = current_cell.remove_piece()
+ removed_piece.free()
+ draw_random_piece_on_cell(current_cell, piece_exceptions)
+
+ sequences = find_board_sequences()
+
+
+func draw_grid_cell(grid_cell: GridCellUI) -> void:
+ if not grid_cell.is_inside_tree():
+ add_child(grid_cell)
+ grid_cell.position = Vector2(grid_cell.cell_size.x * grid_cell.column + cell_offset.x, grid_cell.cell_size.y * grid_cell.row + cell_offset.y)
+
+
+func draw_random_piece_on_cell(grid_cell: GridCellUI, except: Array[PieceDefinitionResource] = []) -> void:
+ var new_piece: PieceUI = generate_new_piece()
+ new_piece.piece_definition = piece_weight_generator.roll(except)
+ draw_piece_on_cell(grid_cell, new_piece)
+
+
+func draw_piece_on_cell(grid_cell: GridCellUI, new_piece: PieceUI) -> void:
+ if grid_cell.can_contain_piece:
+ new_piece.cell_size = cell_size
+ new_piece.board = self
+
+ add_child(new_piece)
+ new_piece.position = grid_cell.position
+
+ grid_cell.remove_piece()
+ grid_cell.assign_piece(new_piece)
+
+#endregion
+
+#region Cells
+func get_cell_or_null(column: int, row: int):
+ if not grid_cells.is_empty() and column >= 0 and row >= 0:
+ if column <= grid_cells.size() - 1 and row <= grid_cells[0].size() - 1:
+ return grid_cells[column][row]
+
+ return null
+
+
+func cross_cells_from(origin_cell: GridCellUI) -> Array[GridCellUI]:
+ var cross_cells: Array[GridCellUI] = []
+ cross_cells.assign(PluginUtilities.remove_duplicates(
+ grid_cells_from_row(origin_cell.row) + grid_cells_from_column(origin_cell.column))
+ )
+
+ return cross_cells
+
+
+func cross_diagonal_cells_from(origin_cell: GridCellUI) -> Array[GridCellUI]:
+ var distance: int = grid_width + grid_height
+ var cross_diagonal_cells: Array[GridCellUI] = []
+
+ cross_diagonal_cells.assign(PluginUtilities.remove_falsy_values(PluginUtilities.remove_duplicates(
+ diagonal_top_left_cells_from(origin_cell, distance)\
+ + diagonal_top_right_cells_from(origin_cell, distance)\
+ + diagonal_bottom_left_cells_from(origin_cell, distance)\
+ + diagonal_bottom_right_cells_from(origin_cell, distance)\
+ )))
+
+ return cross_diagonal_cells
+
+
+func diagonal_top_right_cells_from(cell: GridCellUI, distance: int) -> Array[GridCellUI]:
+ var diagonal_cells: Array[GridCellUI] = []
+
+ distance = clamp(distance, 0, grid_width)
+ var current_cell = cell.diagonal_neighbour_top_right
+
+ if distance > 0 and current_cell is GridCellUI:
+ diagonal_cells.append_array(([current_cell] as Array[GridCellUI]) + diagonal_top_right_cells_from(current_cell, distance - 1))
+
+ return diagonal_cells
+
+
+func diagonal_top_left_cells_from(cell: GridCellUI, distance: int) -> Array[GridCellUI]:
+ var diagonal_cells: Array[GridCellUI] = []
+
+ distance = clamp(distance, 0, grid_width)
+ var current_cell = cell.diagonal_neighbour_top_left
+
+ if distance > 0 and current_cell is GridCellUI:
+ diagonal_cells.append_array(([current_cell] as Array[GridCellUI]) + diagonal_top_left_cells_from(current_cell, distance - 1))
+
+ return diagonal_cells
+
+
+func diagonal_bottom_left_cells_from(cell: GridCellUI, distance: int) -> Array[GridCellUI]:
+ var diagonal_cells: Array[GridCellUI] = []
+
+ distance = clamp(distance, 0, grid_width)
+ var current_cell = cell.diagonal_neighbour_bottom_left
+
+ if distance > 0 and current_cell is GridCellUI:
+ diagonal_cells.append_array(([current_cell] as Array[GridCellUI]) + diagonal_bottom_left_cells_from(current_cell, distance - 1))
+
+ return diagonal_cells
+
+
+func diagonal_bottom_right_cells_from(cell: GridCellUI, distance: int) -> Array[GridCellUI]:
+ var diagonal_cells: Array[GridCellUI] = []
+
+ distance = clamp(distance, 0, grid_width)
+ var current_cell = cell.diagonal_neighbour_bottom_right
+
+ if distance > 0 and current_cell is GridCellUI:
+ diagonal_cells.append_array(([current_cell] as Array[GridCellUI]) + diagonal_bottom_right_cells_from(current_cell, distance - 1))
+
+ return diagonal_cells
+
+
+func update_grid_cells_neighbours() -> void:
+ if not grid_cells.is_empty():
+ for grid_cell: GridCellUI in grid_cells_flattened:
+ grid_cell.neighbour_up = get_cell_or_null(grid_cell.column, grid_cell.row - 1)
+ grid_cell.neighbour_bottom = get_cell_or_null(grid_cell.column, grid_cell.row + 1)
+ grid_cell.neighbour_right = get_cell_or_null(grid_cell.column + 1, grid_cell.row )
+ grid_cell.neighbour_left = get_cell_or_null(grid_cell.column - 1, grid_cell.row)
+ grid_cell.diagonal_neighbour_top_right = get_cell_or_null(grid_cell.column + 1, grid_cell.row - 1)
+ grid_cell.diagonal_neighbour_top_left = get_cell_or_null(grid_cell.column - 1, grid_cell.row - 1)
+ grid_cell.diagonal_neighbour_bottom_right = get_cell_or_null(grid_cell.column + 1, grid_cell.row + 1)
+ grid_cell.diagonal_neighbour_bottom_left = get_cell_or_null(grid_cell.column - 1, grid_cell.row + 1)
+
+func grid_cell_from_piece(piece: PieceUI):
+ var found_pieces = grid_cells_flattened.filter(
+ func(cell: GridCellUI): return cell.has_piece() and cell.current_piece == piece
+ )
+
+ if found_pieces.size() == 1:
+ return found_pieces.front()
+
+
+func grid_cells_from_row(row: int) -> Array[GridCellUI]:
+ var cells: Array[GridCellUI] = []
+
+ if grid_cells.size() > 0 and PluginUtilities.value_is_between(row, 0, grid_height - 1):
+ for column in grid_width:
+ cells.append(grid_cells[column][row])
+
+ return cells
+
+
+func grid_cells_from_column(column: int) -> Array[GridCellUI]:
+ var cells: Array[GridCellUI] = []
+
+ if grid_cells.size() > 0 and PluginUtilities.value_is_between(column, 0, grid_width - 1):
+ for row in grid_height:
+ cells.append(grid_cells[column][row])
+
+ return cells
+
+
+func adjacent_cells_from(origin_cell: GridCellUI) -> Array[GridCellUI]:
+ return origin_cell.available_neighbours(false)
+
+
+func first_movable_cell_on_column(column: int):
+ var cells: Array[GridCellUI] = grid_cells_from_column(column)
+ cells.reverse()
+
+ var movable_cells = cells.filter(
+ func(cell: GridCellUI):
+ return cell.has_piece() and cell.current_piece.can_be_moved() and (cell.neighbour_bottom and cell.neighbour_bottom.is_empty())
+ )
+
+ if movable_cells.size() > 0:
+ return movable_cells.front()
+
+ return null
+
+
+func last_empty_cell_on_column(column: int):
+ var cells: Array[GridCellUI] = grid_cells_from_column(column)
+ cells.reverse()
+
+ var current_empty_cells = cells.filter(func(cell: GridCellUI): return cell.can_contain_piece and cell.is_empty())
+
+ if current_empty_cells.size() > 0:
+ return current_empty_cells.front()
+
+ return null
+
+
+func pending_empty_cells_to_fill() -> Array[GridCellUI]:
+ return grid_cells_flattened.filter(func(cell: GridCellUI): return cell.is_empty() and cell.can_contain_piece)
+#endregion
+
+#region Sequence finder
+@warning_ignore("unassigned_variable")
+func find_horizontal_sequences(cells: Array[GridCellUI]) -> Array[Sequence]:
+ var sequences: Array[Sequence] = []
+ var current_matches: Array[GridCellUI] = []
+
+ if horizontal_shape:
+ var valid_cells = cells.filter(func(cell: GridCellUI): return cell.has_piece())
+ var previous_cell: GridCellUI
+
+ for current_cell: GridCellUI in valid_cells:
+
+ if current_matches.is_empty() \
+ or (previous_cell is GridCellUI and previous_cell.is_row_neighbour_of(current_cell) and current_cell.current_piece.match_with(previous_cell.current_piece)):
+ current_matches.append(current_cell)
+
+ if current_matches.size() == max_match:
+ sequences.append(Sequence.new(current_matches, Sequence.Shapes.Horizontal))
+ current_matches.clear()
+ else:
+ if PluginUtilities.value_is_between(current_matches.size(), min_match, max_match):
+ sequences.append(Sequence.new(current_matches, Sequence.Shapes.Horizontal))
+
+ current_matches.clear()
+ current_matches.append(current_cell)
+
+ if current_cell == valid_cells.back() and PluginUtilities.value_is_between(current_matches.size(), min_match, max_match):
+ sequences.append(Sequence.new(current_matches, Sequence.Shapes.Horizontal))
+
+ previous_cell = current_cell
+
+ sequences.sort_custom(_sort_by_size_descending)
+
+ return sequences
+
+
+@warning_ignore("unassigned_variable")
+func find_vertical_sequences(cells: Array[GridCellUI]) -> Array[Sequence]:
+ var sequences: Array[Sequence] = []
+ var current_matches: Array[GridCellUI] = []
+
+ if vertical_shape:
+ var valid_cells = cells.filter(func(cell: GridCellUI): return cell.has_piece())
+ var previous_cell: GridCellUI
+
+ for current_cell: GridCellUI in valid_cells:
+
+ if current_matches.is_empty() \
+ or (previous_cell is GridCellUI and previous_cell.is_column_neighbour_of(current_cell) and current_cell.current_piece.match_with(previous_cell.current_piece)):
+ current_matches.append(current_cell)
+
+ if current_matches.size() == max_match:
+ sequences.append(Sequence.new(current_matches, Sequence.Shapes.Vertical))
+ current_matches.clear()
+ else:
+ if PluginUtilities.value_is_between(current_matches.size(), min_match, max_match):
+ sequences.append(Sequence.new(current_matches, Sequence.Shapes.Vertical))
+
+ current_matches.clear()
+ current_matches.append(current_cell)
+
+ if current_cell.in_same_grid_position_as(valid_cells.back().board_position()) and PluginUtilities.value_is_between(current_matches.size(), min_match, max_match):
+ sequences.append(Sequence.new(current_matches, Sequence.Shapes.Vertical))
+
+ previous_cell = current_cell
+
+
+ sequences.sort_custom(_sort_by_size_descending)
+
+ return sequences
+
+
+func find_tshape_sequence(sequence_a: Sequence, sequence_b: Sequence):
+ if tshape and sequence_a != sequence_b and sequence_a.is_horizontal_or_vertical_shape() and sequence_b.is_horizontal_or_vertical_shape():
+ var horizontal_sequence: Sequence = sequence_a if sequence_a.is_horizontal_shape() else sequence_b
+ var vertical_sequence: Sequence = sequence_a if sequence_a.is_vertical_shape() else sequence_b
+
+ if horizontal_sequence.is_horizontal_shape() and vertical_sequence.is_vertical_shape():
+ var left_edge_cell: GridCellUI = horizontal_sequence.left_edge_cell()
+ var right_edge_cell: GridCellUI = horizontal_sequence.right_edge_cell()
+ var top_edge_cell: GridCellUI = vertical_sequence.top_edge_cell()
+ var bottom_edge_cell: GridCellUI = vertical_sequence.bottom_edge_cell()
+ var horizontal_middle_cell: GridCellUI = horizontal_sequence.middle_cell()
+ var vertical_middle_cell: GridCellUI = vertical_sequence.middle_cell()
+
+ if horizontal_middle_cell.in_same_position_as(top_edge_cell) \
+ or horizontal_middle_cell.in_same_position_as(bottom_edge_cell) \
+ or vertical_middle_cell.in_same_position_as(left_edge_cell) or vertical_middle_cell.in_same_position_as(right_edge_cell):
+
+ var cells: Array[GridCellUI] = []
+
+ ## We need to iterate manually to be able append the item type on the array
+ for cell: GridCellUI in PluginUtilities.remove_duplicates(horizontal_sequence.cells + vertical_sequence.cells):
+ cells.append(cell)
+
+ return Sequence.new(cells, Sequence.Shapes.TShape)
+
+ return null
+
+
+func find_lshape_sequence(sequence_a: Sequence, sequence_b: Sequence):
+ if tshape and sequence_a != sequence_b and sequence_a.is_horizontal_or_vertical_shape() and sequence_b.is_horizontal_or_vertical_shape():
+ var horizontal_sequence: Sequence = sequence_a if sequence_a.is_horizontal_shape() else sequence_b
+ var vertical_sequence: Sequence = sequence_a if sequence_a.is_vertical_shape() else sequence_b
+
+ if horizontal_sequence.is_horizontal_shape() and vertical_sequence.is_vertical_shape():
+ var left_edge_cell: GridCellUI = horizontal_sequence.left_edge_cell()
+ var right_edge_cell: GridCellUI = horizontal_sequence.right_edge_cell()
+ var top_edge_cell: GridCellUI = vertical_sequence.top_edge_cell()
+ var bottom_edge_cell: GridCellUI = vertical_sequence.bottom_edge_cell()
+ #
+ if left_edge_cell.in_same_position_as(top_edge_cell) \
+ or left_edge_cell.in_same_position_as(bottom_edge_cell) \
+ or right_edge_cell.in_same_position_as(top_edge_cell) or right_edge_cell.in_same_position_as(bottom_edge_cell):
+
+ var cells: Array[GridCellUI] = []
+
+ ## We need to iterate manually to be able append the item type on the array
+ for cell: GridCellUI in PluginUtilities.remove_duplicates(horizontal_sequence.cells + vertical_sequence.cells):
+ cells.append(cell)
+
+ return Sequence.new(cells, Sequence.Shapes.LShape)
+
+ return null
+
+
+func find_horizontal_board_sequences() -> Array[Sequence]:
+ var horizontal_sequences: Array[Sequence] = []
+
+ for row in grid_height:
+ horizontal_sequences.append_array(find_horizontal_sequences(grid_cells_from_row(row)))
+
+ return horizontal_sequences
+
+
+func find_vertical_board_sequences() -> Array[Sequence]:
+ var vertical_sequences: Array[Sequence] = []
+
+ for column in grid_width:
+ vertical_sequences.append_array(find_vertical_sequences(grid_cells_from_column(column)))
+
+ return vertical_sequences
+
+
+func find_board_sequences() -> Array[Sequence]:
+ var horizontal_sequences: Array[Sequence] = find_horizontal_board_sequences()
+ var vertical_sequences: Array[Sequence] = find_vertical_board_sequences()
+
+ var valid_horizontal_sequences: Array[Sequence] = []
+ var valid_vertical_sequences: Array[Sequence] = []
+ var tshape_sequences: Array[Sequence] = []
+ var lshape_sequences: Array[Sequence] = []
+
+ if vertical_sequences.is_empty() and not horizontal_sequences.is_empty():
+ valid_horizontal_sequences.append_array(horizontal_sequences)
+ elif horizontal_sequences.is_empty() and not vertical_sequences.is_empty():
+ valid_vertical_sequences.append_array(vertical_sequences)
+ else:
+ for horizontal_sequence: Sequence in horizontal_sequences:
+ var add_horizontal_sequence: bool = true
+
+ for vertical_sequence: Sequence in vertical_sequences:
+ var lshape_sequence = find_lshape_sequence(horizontal_sequence, vertical_sequence)
+
+ if lshape_sequence is Sequence:
+ lshape_sequences.append(lshape_sequence)
+ add_horizontal_sequence = false
+ else:
+ var tshape_sequence = find_tshape_sequence(horizontal_sequence, vertical_sequence)
+
+ if tshape_sequence is Sequence:
+ tshape_sequences.append(tshape_sequence)
+ add_horizontal_sequence = false
+
+ if add_horizontal_sequence:
+ valid_vertical_sequences.append(vertical_sequence)
+
+ if add_horizontal_sequence:
+ valid_horizontal_sequences.append(horizontal_sequence)
+
+ return valid_horizontal_sequences + valid_vertical_sequences + tshape_sequences + lshape_sequences
+
+
+func find_match_from_cell(cell: GridCellUI):
+ if cell.has_piece():
+ var horizontal_sequences: Array[Sequence] = find_horizontal_board_sequences()
+ var vertical_sequences: Array[Sequence] = find_vertical_board_sequences()
+
+ var horizontal = horizontal_sequences.filter(func(sequence: Sequence): return sequence.cells.has(cell))
+ var vertical = vertical_sequences.filter(func(sequence: Sequence): return sequence.cells.has(cell))
+
+ if not horizontal.is_empty() and not vertical.is_empty():
+ var tshape_sequence = find_tshape_sequence(horizontal.front(), vertical.front())
+
+ if tshape_sequence:
+ return tshape_sequence
+
+ var lshape_sequence = find_lshape_sequence(horizontal.front(), vertical.front())
+
+ if lshape_sequence:
+ return lshape_sequence
+ else:
+ if horizontal:
+ return horizontal.front()
+
+ if vertical:
+ return vertical.front()
+
+ return null
+
+
+func _sort_by_size_descending(a: Sequence, b: Sequence):
+ return a.size() > b.size()
+#endregion
+
+#region Movements
+## TODO - INTEGRATE THE DIAGONAL SIDE DOWN FEATURE ON THIS CALCULATION
+func calculate_fall_movements_on_column(column: int) -> Array[Match3Preloader.FallMovement]:
+ var cells: Array[GridCellUI] = grid_cells_from_column(column)
+ var movements: Array[Match3Preloader.FallMovement] = []
+
+ while cells.any(
+ func(cell: GridCellUI):
+ return cell.has_piece() and cell.current_piece.can_be_moved() and (cell.neighbour_bottom and cell.neighbour_bottom.can_contain_piece and cell.neighbour_bottom.is_empty())
+ ):
+
+ var from_cell = first_movable_cell_on_column(column)
+ var to_cell = last_empty_cell_on_column(column)
+
+ if from_cell is GridCellUI and to_cell is GridCellUI:
+ # The pieces needs to be assign here to detect the new empty cells in the while loop
+ to_cell.assign_piece(from_cell.current_piece, true)
+ from_cell.remove_piece()
+ movements.append(Match3Preloader.FallMovement.new(from_cell, to_cell))
+
+ return movements
+
+
+func calculate_all_fall_movements() -> Array[Match3Preloader.FallMovement]:
+ var movements: Array[Match3Preloader.FallMovement] = []
+
+ for column in grid_width:
+ movements.append_array(calculate_fall_movements_on_column(column))
+
+ return movements
+
+#endregion
+
+#region Swap
+func swap_pieces_request(from_grid_cell: GridCellUI, to_grid_cell: GridCellUI) -> void:
+ match swap_mode:
+ Match3Preloader.BoardMovements.Adjacent:
+ swap_adjacent(from_grid_cell, to_grid_cell)
+ Match3Preloader.BoardMovements.Free:
+ swap_free(from_grid_cell, to_grid_cell)
+ Match3Preloader.BoardMovements.Cross:
+ swap_cross(from_grid_cell, to_grid_cell)
+ Match3Preloader.BoardMovements.CrossDiagonal:
+ swap_cross_diagonal(from_grid_cell, to_grid_cell)
+ _:
+ unlock()
+
+
+func swap_adjacent(from_grid_cell: GridCellUI, to_grid_cell: GridCellUI) -> void:
+ if from_grid_cell.is_adjacent_to(to_grid_cell) && from_grid_cell.swap_piece_with(to_grid_cell):
+ swap_pieces(from_grid_cell, to_grid_cell)
+ else:
+ swap_rejected.emit(from_grid_cell.current_piece as PieceUI, to_grid_cell.current_piece as PieceUI)
+
+
+func swap_free(from_grid_cell: GridCellUI, to_grid_cell: GridCellUI) -> void:
+ if from_grid_cell.swap_piece_with(to_grid_cell):
+ swap_pieces(from_grid_cell, to_grid_cell)
+ else:
+ swap_rejected.emit(from_grid_cell.current_piece as PieceUI, to_grid_cell.current_piece as PieceUI)
+
+
+func swap_cross(from_grid_cell: GridCellUI, to_grid_cell: GridCellUI) -> void:
+ if (from_grid_cell.in_same_column_as(to_grid_cell) or from_grid_cell.in_same_row_as(to_grid_cell)) and from_grid_cell.swap_piece_with(to_grid_cell):
+ swap_pieces(from_grid_cell, to_grid_cell)
+ else:
+ swap_rejected.emit(from_grid_cell.current_piece as PieceUI, to_grid_cell.current_piece as PieceUI)
+
+
+func swap_cross_diagonal(from_grid_cell: GridCellUI, to_grid_cell: GridCellUI) -> void:
+ if cross_diagonal_cells_from(from_grid_cell).has(to_grid_cell):
+ swap_pieces(from_grid_cell, to_grid_cell)
+ else:
+ swap_rejected.emit(from_grid_cell.current_piece as PieceUI, to_grid_cell.current_piece as PieceUI)
+
+
+func swap_pieces(from_grid_cell: GridCellUI, to_grid_cell: GridCellUI) -> void:
+ if from_grid_cell.can_swap_piece_with(to_grid_cell):
+ var matches: Array[Sequence] = []
+
+ for sequence: Sequence in PluginUtilities.remove_falsy_values([
+ find_match_from_cell(from_grid_cell),
+ find_match_from_cell(to_grid_cell)
+ ]):
+ matches.append(sequence)
+
+ await piece_animator.swap_pieces(from_grid_cell.current_piece, to_grid_cell.current_piece)
+
+ if matches.size() > 0:
+ swapped_pieces.emit(from_grid_cell.current_piece, to_grid_cell.current_piece, matches)
+ else:
+ await piece_animator.swap_pieces(from_grid_cell.current_piece, to_grid_cell.current_piece)
+
+ from_grid_cell.swap_piece_with(to_grid_cell)
+ swap_rejected.emit(from_grid_cell.current_piece as PieceUI, to_grid_cell.current_piece as PieceUI)
+
+ return
+
+ swap_failed.emit(from_grid_cell, to_grid_cell)
+
+#endregion
+
+#region Lock related
+func lock() -> void:
+ is_locked = true
+
+ lock_all_pieces()
+ unselect_all_pieces()
+
+
+func unlock() -> void:
+ is_locked = false
+
+ unlock_all_pieces()
+
+
+func fall_pieces() -> void:
+ await piece_animator.fall_down_pieces(calculate_all_fall_movements())
+
+
+func fill_pieces() -> void:
+ var empty_cells = pending_empty_cells_to_fill()
+
+ if empty_cells.size() > 0:
+ for empty_cell: GridCellUI in empty_cells:
+ draw_random_piece_on_cell(empty_cell)
+
+ var new_pieces: Array[PieceUI] = []
+ new_pieces.assign(empty_cells.map(func(cell: GridCellUI): return cell.current_piece))
+
+ await piece_animator.spawn_pieces(new_pieces)
+
+
+func lock_all_pieces() -> void:
+ for piece: PieceUI in PluginUtilities.find_nodes_of_custom_class(self, PieceUI):
+ piece.lock()
+
+
+func unlock_all_pieces() -> void:
+ for piece: PieceUI in PluginUtilities.find_nodes_of_custom_class(self, PieceUI):
+ piece.unlock()
+
+
+func unselect_all_pieces() -> void:
+ for piece: PieceUI in get_tree().get_nodes_in_group(PieceUI.GroupName):
+ piece.is_selected = false
+
+#endregion
+
+#region Debug
+func draw_preview_grid() -> void:
+ if Engine.is_editor_hint() and preview_grid_in_editor:
+ remove_preview_sprites()
+
+ if debug_preview_node == null:
+ debug_preview_node = Node2D.new()
+ debug_preview_node.name = "BoardEditorPreview"
+ add_child(debug_preview_node)
+ PluginUtilities.set_owner_to_edited_scene_root(debug_preview_node)
+
+ for column in grid_width:
+ for row in grid_height:
+
+ if empty_cells.has(Vector2(row, column)):
+ continue
+
+ var current_cell_sprite: Sprite2D = Sprite2D.new()
+ current_cell_sprite.name = "Cell_Column%d_Row%d" % [column, row]
+ current_cell_sprite.texture = even_cell_texture if (column + row) % 2 == 0 else odd_cell_texture
+ current_cell_sprite.position = Vector2(cell_size.x * column + cell_offset.x, cell_size.y * row + cell_offset.y)
+
+ debug_preview_node.add_child(current_cell_sprite)
+ PluginUtilities.set_owner_to_edited_scene_root(current_cell_sprite)
+
+ if current_cell_sprite.texture:
+ var cell_texture_size = current_cell_sprite.texture.get_size()
+ current_cell_sprite.scale = Vector2(cell_size.x / cell_texture_size.x, cell_size.y / cell_texture_size.y)
+
+ if preview_pieces.size():
+ var current_piece_sprite: Sprite2D = Sprite2D.new()
+ current_piece_sprite.name = "Piece_Column%d_Row%d" % [column, row]
+ current_piece_sprite.texture = preview_pieces.pick_random()
+ current_piece_sprite.position = current_cell_sprite.position
+
+ debug_preview_node.add_child(current_piece_sprite)
+ PluginUtilities.set_owner_to_edited_scene_root(current_piece_sprite)
+
+ if current_piece_sprite.texture:
+ var piece_texture_size = current_piece_sprite.texture.get_size()
+ ## The 0.85 value it's to adjust the piece inside the cell reducing the scale size
+ current_piece_sprite.scale = Vector2(cell_size.x / piece_texture_size.x, cell_size.y / piece_texture_size.y) * 0.85
+
+
+
+func remove_preview_sprites() -> void:
+ if Engine.is_editor_hint():
+ if debug_preview_node:
+ debug_preview_node.free()
+ debug_preview_node = null
+
+ for child: Node2D in get_children(true).filter(func(node: Node): return node is Node2D):
+ child.free()
+#endregion
+
+#region Signal callbacks
+func on_prepared_board() -> void:
+ draw_board()
+ update_grid_cells_neighbours()
+
+
+func on_state_changed(from: Match3Preloader.BoardState, to: Match3Preloader.BoardState) -> void:
+ match to:
+ Match3Preloader.BoardState.WaitForInput:
+ unlock()
+ Match3Preloader.BoardState.Consume:
+ lock()
+ if pending_sequences.is_empty():
+ pending_sequences = find_board_sequences()
+
+ await sequence_consumer.consume_sequences(pending_sequences)
+ await get_tree().process_frame
+
+ current_state = Match3Preloader.BoardState.Fill
+ Match3Preloader.BoardState.Fill:
+ lock()
+ pending_sequences.clear()
+ await fall_pieces()
+ await get_tree().process_frame
+ await fill_pieces()
+
+ pending_sequences = find_board_sequences()
+
+ current_state = Match3Preloader.BoardState.WaitForInput if pending_sequences.is_empty() else Match3Preloader.BoardState.Consume
+
+
+func on_swap_requested(from_piece: PieceUI, to_piece: PieceUI) -> void:
+ current_selected_piece = null
+
+ unselect_all_pieces()
+
+ if not is_locked:
+ var from_grid_cell: GridCellUI = grid_cell_from_piece(from_piece)
+ var to_grid_cell: GridCellUI = grid_cell_from_piece(to_piece)
+
+ if from_grid_cell and to_grid_cell and from_grid_cell.can_swap_piece_with(to_grid_cell):
+ swap_pieces_request(from_grid_cell, to_grid_cell)
+
+
+func on_swapped_pieces(_from: PieceUI, _to: PieceUI, matches: Array[Sequence]) -> void:
+ pending_sequences = matches
+ current_state = Match3Preloader.BoardState.Consume
+
+
+func on_swap_failed(_from: PieceUI, _to: PieceUI) -> void:
+ unlock()
+
+
+func on_swap_rejected(_from: PieceUI, _to: PieceUI) -> void:
+ unlock()
+
+
+func on_consume_requested(sequence: Sequence) -> void:
+ if is_locked:
+ return
+
+ if swap_mode == Match3Preloader.BoardMovements.ConnectLine:
+
+ if sequence.size() >= min_match:
+ pending_sequences = [sequence] as Array[Sequence]
+ current_state = Match3Preloader.BoardState.Consume
+
+
+func on_piece_selected(piece: PieceUI) -> void:
+ if is_locked:
+ return
+
+ if current_selected_piece and current_selected_piece != piece:
+ swap_requested.emit(current_selected_piece as PieceUI, piece as PieceUI)
+ current_selected_piece = null
+ cell_highlighter.remove_current_highlighters()
+ return
+
+
+ current_selected_piece = piece
+ cell_highlighter.highlight_cells(grid_cell_from_piece(current_selected_piece), swap_mode)
+
+
+func on_piece_unselected(_piece: PieceUI) -> void:
+ if is_locked:
+ return
+
+ current_selected_piece = null
+ cell_highlighter.remove_current_highlighters()
+
+#endregion
diff --git a/addons/my_plugin/updater/plugin_version.gd b/addons/ninetailsrabbit.match3_board/updater/plugin_version.gd
similarity index 100%
rename from addons/my_plugin/updater/plugin_version.gd
rename to addons/ninetailsrabbit.match3_board/updater/plugin_version.gd
diff --git a/addons/my_plugin/updater/update_notify_tool.gd b/addons/ninetailsrabbit.match3_board/updater/update_notify_tool.gd
similarity index 100%
rename from addons/my_plugin/updater/update_notify_tool.gd
rename to addons/ninetailsrabbit.match3_board/updater/update_notify_tool.gd
diff --git a/addons/my_plugin/updater/update_notify_tool.tscn b/addons/ninetailsrabbit.match3_board/updater/update_notify_tool.tscn
similarity index 96%
rename from addons/my_plugin/updater/update_notify_tool.tscn
rename to addons/ninetailsrabbit.match3_board/updater/update_notify_tool.tscn
index b0d0952..10f5cf2 100644
--- a/addons/my_plugin/updater/update_notify_tool.tscn
+++ b/addons/ninetailsrabbit.match3_board/updater/update_notify_tool.tscn
@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://0xyeci1tqebj"]
-[ext_resource type="Script" path="res://addons/my_plugin/updater/update_notify_tool.gd" id="1_ul4xs"]
+[ext_resource type="Script" path="res://addons/ninetailsrabbit.match3_board/updater/update_notify_tool.gd" id="1_ul4xs"]
[node name="UpdateNotifyTool" type="Window"]
disable_3d = true
diff --git a/addons/my_plugin/updater/update_progress_bar.gd b/addons/ninetailsrabbit.match3_board/updater/update_progress_bar.gd
similarity index 100%
rename from addons/my_plugin/updater/update_progress_bar.gd
rename to addons/ninetailsrabbit.match3_board/updater/update_progress_bar.gd
diff --git a/addons/my_plugin/updater/update_progress_bar.tscn b/addons/ninetailsrabbit.match3_board/updater/update_progress_bar.tscn
similarity index 91%
rename from addons/my_plugin/updater/update_progress_bar.tscn
rename to addons/ninetailsrabbit.match3_board/updater/update_progress_bar.tscn
index ac85fc3..a61fabc 100644
--- a/addons/my_plugin/updater/update_progress_bar.tscn
+++ b/addons/ninetailsrabbit.match3_board/updater/update_progress_bar.tscn
@@ -1,7 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://2eahgaw88y6q"]
-[ext_resource type="Script" path="res://addons/my_plugin/updater/update_progress_bar.gd" id="1_7qcxe"]
-[ext_resource type="Texture2D" uid="uid://bdf47suvxkr8l" path="res://addons/my_plugin/assets/progress_background_green.png" id="2_sw0xs"]
+[ext_resource type="Script" path="res://addons/ninetailsrabbit.match3_board/updater/update_progress_bar.gd" id="1_7qcxe"]
+[ext_resource type="Texture2D" uid="uid://bdf47suvxkr8l" path="res://addons/ninetailsrabbit.match3_board/assets/progress_background_green.png" id="2_sw0xs"]
[node name="UpdateProgressBar" type="ConfirmationDialog"]
disable_3d = true
diff --git a/addons/my_plugin/utils/plugin_utilities.gd b/addons/ninetailsrabbit.match3_board/utils/plugin_utilities.gd
similarity index 63%
rename from addons/my_plugin/utils/plugin_utilities.gd
rename to addons/ninetailsrabbit.match3_board/utils/plugin_utilities.gd
index 87fe24b..8ad9d28 100644
--- a/addons/my_plugin/utils/plugin_utilities.gd
+++ b/addons/ninetailsrabbit.match3_board/utils/plugin_utilities.gd
@@ -1,6 +1,101 @@
class_name PluginUtilities
+
+## Flatten any array with n dimensions recursively
+static func flatten(array: Array):
+ var result := []
+
+ for i in array.size():
+ if typeof(array[i]) >= TYPE_ARRAY:
+ result.append_array(flatten(array[i]))
+ else:
+ result.append(array[i])
+
+ return result
+
+
+static func pick_random_values(array: Array, items_to_pick: int = 1, duplicates: bool = true) -> Array:
+ var result := []
+ var target = flatten(array.duplicate())
+ target.shuffle()
+
+ items_to_pick = min(target.size(), items_to_pick)
+
+ for i in range(items_to_pick):
+ var item = target.pick_random()
+ result.append(item)
+
+ if not duplicates:
+ target.erase(item)
+
+ return result
+
+
+static func remove_duplicates(array: Array) -> Array:
+ var cleaned_array := []
+
+ for element in array:
+ if not cleaned_array.has(element):
+ cleaned_array.append(element)
+
+ return cleaned_array
+
+
+static func remove_falsy_values(array: Array) -> Array:
+ var cleaned_array := []
+
+ for element in array:
+ if element:
+ cleaned_array.append(element)
+
+ return cleaned_array
+
+
+static func middle_element(array: Array):
+ if array.size() > 2:
+ return array[floor(array.size() / 2.0)]
+
+ return null
+
+
+## To detect if a contains elements of b
+static func intersects(a: Array, b: Array) -> bool:
+ for e: Variant in a:
+ if b.has(e):
+ return true
+
+ return false
+
+
+static func value_is_between(number: int, min_value: int, max_value: int, inclusive: = true) -> bool:
+ if inclusive:
+ return number >= min(min_value, max_value) and number <= max(min_value, max_value)
+ else :
+ return number > min(min_value, max_value) and number < max(min_value, max_value)
+
+
+## Only works for native custom class not for GDScriptNativeClass
+## Example NodePositioner.find_nodes_of_custom_class(self, MachineState)
+static func find_nodes_of_custom_class(node: Node, class_to_find: Variant) -> Array:
+ var result := []
+
+ var childrens = node.get_children(true)
+
+ for child in childrens:
+ if child.get_script() == class_to_find:
+ result.append(child)
+ else:
+ result.append_array(find_nodes_of_custom_class(child, class_to_find))
+
+ return result
+
+
+static func set_owner_to_edited_scene_root(node: Node) -> void:
+ if Engine.is_editor_hint() and node.get_tree():
+ node.owner = node.get_tree().edited_scene_root
+
+
static func is_valid_url(url: String) -> bool:
var regex = RegEx.new()
var url_pattern = "/(https:\\/\\/www\\.|http:\\/\\/www\\.|https:\\/\\/|http:\\/\\/)?[a-zA-Z]{2,}(\\.[a-zA-Z]{2,})(\\.[a-zA-Z]{2,})?\\/[a-zA-Z0-9]{2,}|((https:\\/\\/www\\.|http:\\/\\/www\\.|https:\\/\\/|http:\\/\\/)?[a-zA-Z]{2,}(\\.[a-zA-Z]{2,})(\\.[a-zA-Z]{2,})?)|(https:\\/\\/www\\.|http:\\/\\/www\\.|https:\\/\\/|http:\\/\\/)?[a-zA-Z0-9]{2,}\\.[a-zA-Z0-9]{2,}\\.[a-zA-Z0-9]{2,}(\\.[a-zA-Z0-9]{2,})?/g"
diff --git a/addons/my_plugin/utils/script_editor_controls.gd b/addons/ninetailsrabbit.match3_board/utils/script_editor_controls.gd
similarity index 100%
rename from addons/my_plugin/utils/script_editor_controls.gd
rename to addons/ninetailsrabbit.match3_board/utils/script_editor_controls.gd
diff --git a/icon.svg b/icon.svg
index 9d8b7fa..5a26e27 100644
--- a/icon.svg
+++ b/icon.svg
@@ -1 +1,23 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/project.godot b/project.godot
index de91949..4fbaa77 100644
--- a/project.godot
+++ b/project.godot
@@ -10,7 +10,9 @@ config_version=5
[application]
-config/name="godot-plugin-template"
+config/name="Match3 Board"
+config/description="This lightweight library provides the core logic and functionality you need to build engaging match-3 games. Focus on game design and mechanics while leaving the complex logic to this library"
+config/version="1.0.0"
config/features=PackedStringArray("4.3", "GL Compatibility")
config/icon="res://icon.svg"