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 @@
Logo -

PLUGIN NAME

+

Match3 Board

- PLUGIN DESCRIPTION + The core logic and functionality you need to build engaging match-3 games
· - Report Bug + Report Bug · - Request Features + Request Features

@@ -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"