From e66ba7542cb622de9f9cd300183f0a76c0b9a76f Mon Sep 17 00:00:00 2001 From: adtpdn Date: Thu, 29 Jan 2026 03:04:24 +0800 Subject: [PATCH] update --- project.godot | 82 +--- scenes/main.tscn | 97 ++++- scenes/player.gd | 102 ++++- scripts/managers/goal_manager.gd | 65 ++- scripts/managers/player_input_manager.gd | 10 + scripts/managers/player_movement_manager.gd | 120 ++---- scripts/managers/playerboard_manager.gd | 36 +- scripts/managers/powerup_manager.gd | 176 +++------ scripts/managers/special_tiles_manager.gd | 417 +++++++++----------- scripts/managers/ui_manager.gd | 37 +- scripts/ui/powerup_inventory_ui.gd | 94 +++++ tiles_armagedon_a1.res | Bin 0 -> 11552 bytes 12 files changed, 687 insertions(+), 549 deletions(-) create mode 100644 scripts/ui/powerup_inventory_ui.gd create mode 100644 tiles_armagedon_a1.res diff --git a/project.godot b/project.godot index 8e5e642..074c239 100644 --- a/project.godot +++ b/project.godot @@ -11,40 +11,26 @@ config_version=5 [application] config/name="tekton-local" -run/main_scene="res://scenes/lobby.tscn" +run/main_scene="res://scenes/main_scene.tscn" config/features=PackedStringArray("4.5", "Forward Plus") config/icon="res://icon.svg" [autoload] -NakamaManager="*res://scripts/nakama_manager.gd" -NetworkManager="*res://scripts/network_manager.gd" -Nakama="*res://addons/com.heroiclabs.nakama/Nakama.gd" -PlayerManager="*res://scripts/managers/player_manager.gd" -TurnManager="*res://scripts/managers/turn_manager.gd" -GoalManager="*res://scripts/managers/goal_manager.gd" -GameStateManager="*res://scripts/managers/game_state_manager.gd" -LobbyManager="*res://scripts/managers/lobby_manager.gd" -GameUpdateManager="*res://scripts/managers/game_update_manager.gd" -AuthManager="*res://scripts/managers/auth_manager.gd" -UserProfileManager="*res://scripts/managers/user_profile_manager.gd" -Satori="*res://addons/com.heroiclabs.nakama/Satori.gd" +BeehaveGlobalMetrics="*res://addons/beehave/metrics/beehave_global_metrics.gd" +BeehaveGlobalDebugger="*res://addons/beehave/debug/global_debugger.gd" [display] window/size/viewport_width=1366 window/size/viewport_height=720 -window/size/window_width_override=1280 -window/size/window_height_override=720 +window/size/window_width_override=1024 +window/size/window_height_override=576 window/stretch/mode="viewport" -[editor] - -run/main_run_args="--touch" - [editor_plugins] -enabled=PackedStringArray("res://addons/enhanced_gridmap/plugin.cfg", "res://addons/com.heroiclabs.nakama/plugin.cfg") +enabled=PackedStringArray("res://addons/enhanced_gridmap/plugin.cfg") [input] @@ -58,59 +44,3 @@ put_item={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } -move_northwest={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":81,"location":0,"echo":false,"script":null) -] -} -move_north={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":87,"location":0,"echo":false,"script":null) -] -} -move_northeast={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) -] -} -move_west={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) -] -} -move_east={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) -] -} -move_southwest={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":122,"location":0,"echo":false,"script":null) -] -} -move_south={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":88,"key_label":0,"unicode":120,"location":0,"echo":false,"script":null) -] -} -move_southeast={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null) -] -} -action_grab={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) -] -} -action_put={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) -, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":83,"location":0,"echo":false,"script":null) -] -} -use_powerup={ -"deadzone": 0.2, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":70,"location":0,"echo":false,"script":null) -] -} diff --git a/scenes/main.tscn b/scenes/main.tscn index 1b206de..44c3762 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -24,6 +24,7 @@ [ext_resource type="StyleBox" uid="uid://3yog1weaqhxb" path="res://assets/styles/ribbon_unselected_gui.tres" id="20_q6bc1"] [ext_resource type="Script" uid="uid://b54tfa0n6kogi" path="res://scripts/managers/touch_controls.gd" id="touch_manager"] [ext_resource type="Script" uid="uid://djiml4sh61dc1" path="res://scripts/ui/virtual_joystick.gd" id="virtual_joystick"] +[ext_resource type="Script" uid="uid://powerup_ui_script_id" path="res://scripts/ui/powerup_inventory_ui.gd" id="powerup_ui_script"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerboard"] content_margin_left = 8.0 @@ -55,7 +56,7 @@ script = ExtResource("1_xcpe3") mesh_library = ExtResource("1_110wo") cell_size = Vector3(1, 0.2, 1) data = { -"cells": PackedInt32Array(0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 9, 0, 0, 10, 0, 0, 11, 0, 0, 12, 0, 0, 13, 0, 1, 0, 0, 1, 1, 0, 1, 2, 0, 1, 3, 0, 1, 4, 0, 1, 5, 0, 1, 6, 0, 1, 7, 0, 1, 8, 0, 1, 9, 0, 1, 10, 0, 1, 11, 0, 1, 12, 0, 1, 13, 0, 2, 0, 0, 2, 1, 0, 2, 2, 0, 2, 3, 0, 2, 4, 0, 2, 5, 0, 2, 6, 0, 2, 7, 0, 2, 8, 0, 2, 9, 0, 2, 10, 0, 2, 11, 0, 2, 12, 0, 2, 13, 0, 3, 0, 0, 3, 1, 0, 3, 2, 0, 3, 3, 0, 3, 4, 0, 3, 5, 0, 3, 6, 0, 3, 7, 0, 3, 8, 0, 3, 9, 0, 3, 10, 0, 3, 11, 0, 3, 12, 0, 3, 13, 0, 4, 0, 0, 4, 1, 0, 4, 2, 0, 4, 3, 0, 4, 4, 0, 4, 5, 0, 4, 6, 0, 4, 7, 0, 4, 8, 0, 4, 9, 0, 4, 10, 0, 4, 11, 0, 4, 12, 0, 4, 13, 0, 5, 0, 0, 5, 1, 0, 5, 2, 0, 5, 3, 0, 5, 4, 0, 5, 5, 0, 5, 6, 0, 5, 7, 0, 5, 8, 0, 5, 9, 0, 5, 10, 0, 5, 11, 0, 5, 12, 0, 5, 13, 0, 6, 0, 0, 6, 1, 0, 6, 2, 0, 6, 3, 0, 6, 4, 0, 6, 5, 0, 6, 6, 0, 6, 7, 0, 6, 8, 0, 6, 9, 0, 6, 10, 0, 6, 11, 0, 6, 12, 0, 6, 13, 0, 7, 0, 0, 7, 1, 0, 7, 2, 0, 7, 3, 0, 7, 4, 0, 7, 5, 0, 7, 6, 0, 7, 7, 0, 7, 8, 0, 7, 9, 0, 7, 10, 0, 7, 11, 0, 7, 12, 0, 7, 13, 0, 8, 0, 0, 8, 1, 0, 8, 2, 0, 8, 3, 0, 8, 4, 0, 8, 5, 0, 8, 6, 0, 8, 7, 0, 8, 8, 0, 8, 9, 0, 8, 10, 0, 8, 11, 0, 8, 12, 0, 8, 13, 0, 9, 0, 0, 9, 1, 0, 9, 2, 0, 9, 3, 0, 9, 4, 0, 9, 5, 0, 9, 6, 0, 9, 7, 0, 9, 8, 0, 9, 9, 0, 9, 10, 0, 9, 11, 0, 9, 12, 0, 9, 13, 0, 10, 0, 0, 10, 1, 0, 10, 2, 0, 10, 3, 0, 10, 4, 0, 10, 5, 0, 10, 6, 0, 10, 7, 0, 10, 8, 0, 10, 9, 0, 10, 10, 0, 10, 11, 0, 10, 12, 0, 10, 13, 0, 11, 0, 0, 11, 1, 0, 11, 2, 0, 11, 3, 0, 11, 4, 0, 11, 5, 0, 11, 6, 0, 11, 7, 0, 11, 8, 0, 11, 9, 0, 11, 10, 0, 11, 11, 0, 11, 12, 0, 11, 13, 0, 12, 0, 0, 12, 1, 0, 12, 2, 0, 12, 3, 0, 12, 4, 0, 12, 5, 0, 12, 6, 0, 12, 7, 0, 12, 8, 0, 12, 9, 0, 12, 10, 0, 12, 11, 0, 12, 12, 0, 12, 13, 0, 13, 0, 0, 13, 1, 0, 13, 2, 0, 13, 3, 0, 13, 4, 0, 13, 5, 0, 13, 6, 0, 13, 7, 0, 13, 8, 0, 13, 9, 0, 13, 10, 0, 13, 11, 0, 13, 12, 0, 13, 13, 0, 65537, 0, 10, 65537, 1, 8, 65537, 2, 11, 65537, 3, 8, 65537, 4, 10, 65537, 5, 11, 65537, 6, 9, 65537, 7, 9, 65537, 8, 10, 65537, 9, 8, 65537, 10, 12, 65537, 11, 10, 65537, 12, 10, 65537, 13, 10, 65538, 0, 12, 65538, 1, 11, 65538, 2, 10, 65538, 3, 10, 65538, 4, 11, 65538, 5, 9, 65538, 6, 10, 65538, 7, 12, 65538, 8, 10, 65538, 9, 8, 65538, 10, 12, 65538, 11, 11, 65538, 12, 10, 65538, 13, 10, 65539, 0, 9, 65539, 1, 10, 65539, 2, 10, 65539, 3, 11, 65539, 4, 11, 65539, 5, 8, 65539, 6, 9, 65539, 7, 11, 65539, 8, 12, 65539, 9, 11, 65539, 10, 8, 65539, 11, 10, 65539, 12, 8, 65539, 13, 8, 65540, 0, 12, 65540, 1, 8, 65540, 2, 9, 65540, 3, 8, 65540, 4, 9, 65540, 5, 11, 65540, 6, 9, 65540, 7, 9, 65540, 8, 8, 65540, 9, 9, 65540, 10, 10, 65540, 11, 8, 65540, 12, 12, 65540, 13, 12, 65541, 0, 8, 65541, 1, 8, 65541, 2, 10, 65541, 3, 12, 65541, 4, 8, 65541, 5, 8, 65541, 6, 12, 65541, 7, 8, 65541, 8, 8, 65541, 9, 11, 65541, 10, 12, 65541, 11, 9, 65541, 12, 11, 65541, 13, 11, 65542, 0, 12, 65542, 1, 8, 65542, 2, 10, 65542, 3, 12, 65542, 4, 12, 65542, 5, 11, 65542, 6, 10, 65542, 7, 11, 65542, 8, 9, 65542, 9, 9, 65542, 10, 8, 65542, 11, 11, 65542, 12, 11, 65542, 13, 9, 65543, 0, 8, 65543, 1, 9, 65543, 2, 8, 65543, 3, 8, 65543, 4, 10, 65543, 5, 9, 65543, 6, 12, 65543, 7, 8, 65543, 8, 10, 65543, 9, 8, 65543, 10, 9, 65543, 11, 12, 65543, 12, 9, 65543, 13, 9, 65544, 0, 12, 65544, 1, 8, 65544, 2, 8, 65544, 3, 10, 65544, 4, 10, 65544, 5, 12, 65544, 6, 12, 65544, 7, 10, 65544, 8, 9, 65544, 9, 9, 65544, 10, 10, 65544, 11, 10, 65544, 12, 9, 65544, 13, 11, 65545, 0, 10, 65545, 1, 8, 65545, 2, 10, 65545, 3, 10, 65545, 4, 11, 65545, 5, 11, 65545, 6, 9, 65545, 7, 9, 65545, 8, 10, 65545, 9, 12, 65545, 10, 9, 65545, 11, 8, 65545, 12, 8, 65545, 13, 12, 65546, 0, 10, 65546, 1, 10, 65546, 2, 12, 65546, 3, 12, 65546, 4, 9, 65546, 5, 9, 65546, 6, 10, 65546, 7, 8, 65546, 8, 8, 65546, 9, 10, 65546, 10, 12, 65546, 11, 11, 65546, 12, 12, 65546, 13, 10, 65547, 0, 10, 65547, 1, 9, 65547, 2, 8, 65547, 3, 10, 65547, 4, 8, 65547, 5, 11, 65547, 6, 9, 65547, 7, 8, 65547, 8, 11, 65547, 9, 9, 65547, 10, 8, 65547, 11, 12, 65547, 12, 8, 65547, 13, 10, 65548, 0, 11, 65548, 1, 12, 65548, 2, 8, 65548, 3, 9, 65548, 4, 9, 65548, 5, 9, 65548, 6, 10, 65548, 7, 10, 65548, 8, 12, 65548, 9, 9, 65548, 10, 11, 65548, 11, 11, 65548, 12, 11, 65548, 13, 10, 65536, 0, 2031628, 65536, 1, 2031627, 65536, 2, 2031626, 65536, 3, 2031625, 65536, 4, 2031628, 65536, 5, 2031627, 65536, 6, 2031626, 65536, 7, 2031627, 65536, 8, 2031628, 65536, 9, 2031628, 65536, 10, 2031628, 65536, 11, 2031627, 65536, 12, 2031626, 65536, 13, 2031624, 65549, 0, 2031627, 65549, 1, 2031624, 65549, 2, 2031624, 65549, 3, 2031628, 65549, 4, 2031626, 65549, 5, 2031625, 65549, 6, 2031627, 65549, 7, 2031628, 65549, 8, 2031624, 65549, 9, 2031628, 65549, 10, 2031627, 65549, 11, 2031628, 65549, 12, 2031625, 65549, 13, 2031626) +"cells": PackedInt32Array(0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 3, 0, 0, 4, 0, 0, 5, 0, 0, 6, 0, 0, 7, 0, 0, 8, 0, 0, 9, 0, 0, 10, 0, 0, 11, 0, 0, 12, 0, 0, 13, 0, 1, 0, 0, 1, 1, 0, 1, 2, 0, 1, 3, 0, 1, 4, 0, 1, 5, 0, 1, 6, 0, 1, 7, 0, 1, 8, 0, 1, 9, 0, 1, 10, 0, 1, 11, 0, 1, 12, 0, 1, 13, 0, 2, 0, 0, 2, 1, 0, 2, 2, 0, 2, 3, 0, 2, 4, 0, 2, 5, 0, 2, 6, 0, 2, 7, 0, 2, 8, 0, 2, 9, 0, 2, 10, 0, 2, 11, 0, 2, 12, 0, 2, 13, 0, 3, 0, 0, 3, 1, 0, 3, 2, 0, 3, 3, 0, 3, 4, 0, 3, 5, 0, 3, 6, 0, 3, 7, 0, 3, 8, 0, 3, 9, 0, 3, 10, 0, 3, 11, 0, 3, 12, 0, 3, 13, 0, 4, 0, 0, 4, 1, 0, 4, 2, 0, 4, 3, 0, 4, 4, 0, 4, 5, 0, 4, 6, 0, 4, 7, 0, 4, 8, 0, 4, 9, 0, 4, 10, 0, 4, 11, 0, 4, 12, 0, 4, 13, 0, 5, 0, 0, 5, 1, 0, 5, 2, 0, 5, 3, 0, 5, 4, 0, 5, 5, 0, 5, 6, 0, 5, 7, 0, 5, 8, 0, 5, 9, 0, 5, 10, 0, 5, 11, 0, 5, 12, 0, 5, 13, 0, 6, 0, 0, 6, 1, 0, 6, 2, 0, 6, 3, 0, 6, 4, 0, 6, 5, 0, 6, 6, 0, 6, 7, 0, 6, 8, 0, 6, 9, 0, 6, 10, 0, 6, 11, 0, 6, 12, 0, 6, 13, 0, 7, 0, 0, 7, 1, 0, 7, 2, 0, 7, 3, 0, 7, 4, 0, 7, 5, 0, 7, 6, 0, 7, 7, 0, 7, 8, 0, 7, 9, 0, 7, 10, 0, 7, 11, 0, 7, 12, 0, 7, 13, 0, 8, 0, 0, 8, 1, 0, 8, 2, 0, 8, 3, 0, 8, 4, 0, 8, 5, 0, 8, 6, 0, 8, 7, 0, 8, 8, 0, 8, 9, 0, 8, 10, 0, 8, 11, 0, 8, 12, 0, 8, 13, 0, 9, 0, 0, 9, 1, 0, 9, 2, 0, 9, 3, 0, 9, 4, 0, 9, 5, 0, 9, 6, 0, 9, 7, 0, 9, 8, 0, 9, 9, 0, 9, 10, 0, 9, 11, 0, 9, 12, 0, 9, 13, 0, 10, 0, 0, 10, 1, 0, 10, 2, 0, 10, 3, 0, 10, 4, 0, 10, 5, 0, 10, 6, 0, 10, 7, 0, 10, 8, 0, 10, 9, 0, 10, 10, 0, 10, 11, 0, 10, 12, 0, 10, 13, 0, 11, 0, 0, 11, 1, 0, 11, 2, 0, 11, 3, 0, 11, 4, 0, 11, 5, 0, 11, 6, 0, 11, 7, 0, 11, 8, 0, 11, 9, 0, 11, 10, 0, 11, 11, 0, 11, 12, 0, 11, 13, 0, 12, 0, 0, 12, 1, 0, 12, 2, 0, 12, 3, 0, 12, 4, 0, 12, 5, 0, 12, 6, 0, 12, 7, 0, 12, 8, 0, 12, 9, 0, 12, 10, 0, 12, 11, 0, 12, 12, 0, 12, 13, 0, 13, 0, 0, 13, 1, 0, 13, 2, 0, 13, 3, 0, 13, 4, 0, 13, 5, 0, 13, 6, 0, 13, 7, 0, 13, 8, 0, 13, 9, 0, 13, 10, 0, 13, 11, 0, 13, 12, 0, 13, 13, 0, 65537, 0, 10, 65537, 1, 8, 65537, 2, 11, 65537, 3, 8, 65537, 4, 10, 65537, 5, 11, 65537, 6, 9, 65537, 7, 9, 65537, 8, 10, 65537, 9, 8, 65537, 10, 12, 65537, 11, 10, 65537, 12, 10, 65537, 13, 10, 65538, 0, 12, 65538, 1, 11, 65538, 2, 10, 65538, 3, 10, 65538, 4, 11, 65538, 5, 9, 65538, 6, 10, 65538, 7, 12, 65538, 8, 10, 65538, 9, 8, 65538, 10, 12, 65538, 11, 11, 65538, 12, 10, 65538, 13, 10, 65539, 0, 9, 65539, 1, 10, 65539, 2, 10, 65539, 3, 11, 65539, 4, 11, 65539, 5, 8, 65539, 6, 9, 65539, 7, 11, 65539, 8, 12, 65539, 9, 11, 65539, 10, 8, 65539, 11, 10, 65539, 12, 8, 65539, 13, 8, 65540, 0, 12, 65540, 1, 8, 65540, 2, 9, 65540, 3, 8, 65540, 4, 9, 65540, 5, 11, 65540, 6, 9, 65540, 7, 9, 65540, 8, 8, 65540, 9, 9, 65540, 10, 10, 65540, 11, 8, 65540, 12, 12, 65540, 13, 12, 65541, 0, 8, 65541, 1, 8, 65541, 2, 10, 65541, 3, 12, 65541, 4, 8, 65541, 5, 8, 65541, 6, 12, 65541, 7, 8, 65541, 8, 8, 65541, 9, 11, 65541, 10, 12, 65541, 11, 9, 65541, 12, 11, 65541, 13, 11, 65542, 0, 12, 65542, 1, 8, 65542, 2, 10, 65542, 3, 12, 65542, 4, 12, 65542, 5, 11, 65542, 6, 10, 65542, 7, 11, 65542, 8, 9, 65542, 9, 9, 65542, 10, 8, 65542, 11, 11, 65542, 12, 11, 65542, 13, 9, 65543, 0, 8, 65543, 1, 9, 65543, 2, 8, 65543, 3, 8, 65543, 4, 10, 65543, 5, 9, 65543, 6, 12, 65543, 7, 8, 65543, 8, 10, 65543, 9, 8, 65543, 10, 9, 65543, 11, 12, 65543, 12, 9, 65543, 13, 9, 65544, 0, 12, 65544, 1, 8, 65544, 2, 8, 65544, 3, 10, 65544, 4, 10, 65544, 5, 12, 65544, 6, 12, 65544, 7, 10, 65544, 8, 9, 65544, 9, 9, 65544, 10, 10, 65544, 11, 10, 65544, 12, 9, 65544, 13, 11, 65545, 0, 10, 65545, 1, 8, 65545, 2, 10, 65545, 3, 10, 65545, 4, 11, 65545, 5, 11, 65545, 6, 9, 65545, 7, 9, 65545, 8, 10, 65545, 9, 12, 65545, 10, 9, 65545, 11, 8, 65545, 12, 8, 65545, 13, 12, 65546, 0, 10, 65546, 1, 10, 65546, 2, 12, 65546, 3, 12, 65546, 4, 9, 65546, 5, 9, 65546, 6, 10, 65546, 7, 8, 65546, 8, 8, 65546, 9, 10, 65546, 10, 12, 65546, 11, 11, 65546, 12, 12, 65546, 13, 10, 65547, 0, 10, 65547, 1, 9, 65547, 2, 8, 65547, 3, 10, 65547, 4, 8, 65547, 5, 11, 65547, 6, 9, 65547, 7, 8, 65547, 8, 11, 65547, 9, 9, 65547, 10, 8, 65547, 11, 12, 65547, 12, 8, 65547, 13, 10, 65548, 0, 11, 65548, 1, 12, 65548, 2, 8, 65548, 3, 9, 65548, 4, 9, 65548, 5, 9, 65548, 6, 10, 65548, 7, 10, 65548, 8, 12, 65548, 9, 9, 65548, 10, 11, 65548, 11, 11, 65548, 12, 11, 65548, 13, 10, 65536, 0, 2031628, 65536, 1, 2031627, 65536, 2, 2031626, 65536, 3, 2031625, 65536, 4, 2031628, 65536, 5, 2031627, 65536, 6, 2031626, 65536, 7, 2031627, 65536, 8, 2031628, 65536, 9, 2031628, 65536, 10, 2031628, 65537, 0, 10, 65537, 1, 8, 65537, 2, 11, 65537, 3, 8, 65537, 4, 10, 65537, 5, 11, 65537, 6, 9, 65537, 7, 9, 65537, 8, 10, 65537, 9, 8, 65537, 10, 12, 65537, 11, 10, 65537, 12, 10, 65537, 13, 10, 65538, 0, 12, 65538, 1, 11, 65538, 2, 10, 65538, 3, 10, 65538, 4, 11, 65538, 5, 9, 65538, 6, 10, 65538, 7, 12, 65538, 8, 10, 65538, 9, 8, 65538, 10, 12, 65538, 11, 11, 65538, 12, 10, 65538, 13, 10, 65539, 0, 9, 65539, 1, 10, 65539, 2, 10, 65539, 3, 11, 65539, 4, 11, 65539, 5, 8, 65539, 6, 9, 65539, 7, 11, 65539, 8, 12, 65539, 9, 11, 65539, 10, 8, 65539, 11, 10, 65539, 12, 8, 65539, 13, 8, 65540, 0, 12, 65540, 1, 8, 65540, 2, 9, 65540, 3, 8, 65540, 4, 9, 65540, 5, 11, 65540, 6, 9, 65540, 7, 9, 65540, 8, 8, 65540, 9, 9, 65540, 10, 10, 65540, 11, 8, 65540, 12, 12, 65540, 13, 12, 65541, 0, 8, 65541, 1, 8, 65541, 2, 10, 65541, 3, 12, 65541, 4, 8, 65541, 5, 8, 65541, 6, 12, 65541, 7, 8, 65541, 8, 8, 65541, 9, 11, 65541, 10, 12, 65541, 11, 9, 65541, 12, 11, 65541, 13, 11, 65542, 0, 12, 65542, 1, 8, 65542, 2, 10, 65542, 3, 12, 65542, 4, 12, 65542, 5, 11, 65542, 6, 10, 65542, 7, 11, 65542, 8, 9, 65542, 9, 9, 65542, 10, 8, 65542, 11, 11, 65542, 12, 11, 65542, 13, 9, 65543, 0, 8, 65543, 1, 9, 65543, 2, 8, 65543, 3, 8, 65543, 4, 10, 65543, 5, 9, 65543, 6, 12, 65543, 7, 8, 65543, 8, 10, 65543, 9, 8, 65543, 10, 9, 65543, 11, 12, 65543, 12, 9, 65543, 13, 9, 65544, 0, 12, 65544, 1, 8, 65544, 2, 8, 65544, 3, 10, 65544, 4, 10, 65544, 5, 12, 65544, 6, 12, 65544, 7, 10, 65544, 8, 9, 65544, 9, 9, 65544, 10, 10, 65544, 11, 10, 65544, 12, 9, 65544, 13, 11, 65545, 0, 10, 65545, 1, 8, 65545, 2, 10, 65545, 3, 10, 65545, 4, 11, 65545, 5, 11, 65545, 6, 9, 65545, 7, 9, 65545, 8, 10, 65545, 9, 12, 65545, 10, 9, 65545, 11, 8, 65545, 12, 8, 65545, 13, 12, 65546, 0, 10, 65546, 1, 10, 65546, 2, 12, 65546, 3, 12, 65546, 4, 9, 65546, 5, 9, 65546, 6, 10, 65546, 7, 8, 65546, 8, 8, 65546, 9, 10, 65546, 10, 12, 65546, 11, 11, 65546, 12, 12, 65546, 13, 10, 65547, 0, 10, 65547, 1, 9, 65547, 2, 8, 65547, 3, 10, 65547, 4, 8, 65547, 5, 11, 65547, 6, 9, 65547, 7, 8, 65547, 8, 11, 65547, 9, 9, 65547, 10, 8, 65547, 11, 12, 65547, 12, 8, 65547, 13, 10, 65548, 0, 11, 65548, 1, 12, 65548, 2, 8, 65548, 3, 9, 65548, 4, 9, 65548, 5, 9, 65548, 6, 10, 65548, 7, 10, 65548, 8, 12, 65548, 9, 9, 65548, 10, 11, 65548, 11, 11, 65548, 12, 11, 65548, 13, 10, 65536, 0, 2031628, 65536, 1, 2031627, 65536, 2, 2031626, 65536, 3, 2031625, 65536, 4, 2031628, 65536, 5, 2031627, 65536, 6, 2031626, 65536, 7, 2031627, 65536, 8, 2031628, 65536, 9, 2031628, 65536, 10, 2031628, 65536, 11, 2031627, 65536, 12, 2031626, 65536, 13, 2031624, 65549, 0, 2031627, 65549, 1, 2031624, 65549, 2, 2031624, 65549, 3, 2031628, 65549, 4, 2031626, 65549, 5, 2031625, 65549, 6, 2031627, 65549, 7, 2031628, 65549, 8, 2031624, 65549, 9, 2031628, 65549, 10, 2031627, 65549, 11, 2031628, 65549, 12, 2031625, 65549, 13, 2031626) } script = ExtResource("2_hbe1v") columns = 14 @@ -64,6 +65,8 @@ floors = 2 auto_randomize = true metadata/_editor_floor_ = Vector3(0, 1, 0) + + [node name="Camera3D" type="Camera3D" parent="."] transform = Transform3D(1, 0, 0, 0, 0.422618, 0.906308, 0, -0.906308, 0.422618, 7, 22.925, 18.4489) environment = ExtResource("4_ky38j") @@ -9788,6 +9791,98 @@ grow_horizontal = 0 grow_vertical = 0 text = "⚙" +[node name="PowerUpInventoryUI" type="Control" parent="."] +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -228.0 +offset_top = -100.0 +offset_right = -28.0 +offset_bottom = -20.0 +grow_horizontal = 0 +grow_vertical = 0 +script = ExtResource("powerup_ui_script") + +[node name="Panel" type="Panel" parent="PowerUpInventoryUI"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_playerboard") + +[node name="HBoxContainer" type="HBoxContainer" parent="PowerUpInventoryUI"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +alignment = 1 + +[node name="CoinIcon" type="TextureRect" parent="PowerUpInventoryUI/HBoxContainer"] +layout_mode = 2 +texture = ExtResource("10_my1qp") +expand_mode = 2 + +[node name="SelectRect" type="TextureRect" parent="PowerUpInventoryUI/HBoxContainer/CoinIcon"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("8_8meci") + +[node name="HeartIcon" type="TextureRect" parent="PowerUpInventoryUI/HBoxContainer"] +layout_mode = 2 +texture = ExtResource("7_xwcc1") +expand_mode = 2 + +[node name="SelectRect" type="TextureRect" parent="PowerUpInventoryUI/HBoxContainer/HeartIcon"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("8_8meci") + +[node name="DiamondIcon" type="TextureRect" parent="PowerUpInventoryUI/HBoxContainer"] +layout_mode = 2 +texture = ExtResource("8_quhbu") +expand_mode = 2 + +[node name="SelectRect" type="TextureRect" parent="PowerUpInventoryUI/HBoxContainer/DiamondIcon"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("8_8meci") + +[node name="StarIcon" type="TextureRect" parent="PowerUpInventoryUI/HBoxContainer"] +layout_mode = 2 +texture = ExtResource("9_i0gbs") +expand_mode = 2 + +[node name="SelectRect" type="TextureRect" parent="PowerUpInventoryUI/HBoxContainer/StarIcon"] +visible = false +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("8_8meci") + [connection signal="pressed" from="Menu/Host" to="." method="_on_host_pressed"] [connection signal="pressed" from="Menu/Join" to="." method="_on_join_pressed"] [connection signal="text_submitted" from="MessageInput" to="." method="_on_message_input_text_submitted"] diff --git a/scenes/player.gd b/scenes/player.gd index 8d19ab0..2833bc4 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -20,6 +20,15 @@ var is_frozen: bool = false var is_invisible: bool = false var original_movement_range: int = 1 +var is_attack_mode: bool = false: + set(value): + is_attack_mode = value + # Visual feedback for attack mode (Red Tint) + if is_attack_mode: + _apply_tint_recursive(self, Color(1.0, 0.5, 0.5)) + else: + _apply_tint_recursive(self, Color.WHITE) + @export var is_bot: bool = false @export var enhanced_gridmap_path: NodePath = "/root/Main/EnhancedGridMap" @@ -668,11 +677,31 @@ func drop_random_item(): rpc("display_message", "Dropped item!", 4) print("Player %s dropped item %d at %s" % [name, item_id, drop_pos]) -func playerboard_is_empty() -> bool: - for item in playerboard: - if item != -1: - return false - return true + +@rpc("any_peer", "call_local") +func drop_all_tiles(): + """Drops all tiles from playerboard. Used by Super Push.""" + var dropped_count = 0 + + for i in range(playerboard.size()): + if playerboard[i] != -1: + var item_id = playerboard[i] + playerboard[i] = -1 + + # Find a spot to drop? Or just destroy? + # User: "drop all tiles on playerboard" -> usually implies they scatter on floor + # But scattering 25 tiles is chaotic. + # Let's drop a few random ones and destroy rest? + # Or per user "spawn nearby tiles into power up" -> Maybe the dropped tiles BECOME powerups? + # User request: "victim drops all tiles... and spawn / replace your nearby tiles into power up" + # I will just clear the board here. Spawning is handled by spawn_powerups_around. + dropped_count += 1 + + if dropped_count > 0: + rpc("sync_playerboard", playerboard) + rpc("trigger_screen_shake", "targeted") + rpc("display_message", "CRITICALLY HIT!", 4) + print("Player %s dropped %d tiles due to Super Push" % [name, dropped_count]) func _find_valid_drop_position() -> Vector2i: # Try random adjacent cells @@ -681,16 +710,65 @@ func _find_valid_drop_position() -> Vector2i: for neighbor in neighbors: var pos = neighbor.position - if enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) == -1: # Empty floor? No, 0 is floor. -1 is void? - # Wait, items are on layer 1 usually? - # Check logic: grab_item uses y=1? - var item_cell = Vector3i(pos.x, 1, pos.y) - if enhanced_gridmap.get_cell_item(item_cell) == -1: - if not is_position_occupied(pos): - return pos + # Check item layer + var item_cell = Vector3i(pos.x, 1, pos.y) + if enhanced_gridmap.get_cell_item(item_cell) == -1: + if not is_position_occupied(pos): + return pos return Vector2i(-1, -1) +# ============================================================================= +# Targeting Action +# ============================================================================= + +func attempt_target_action(target_index: int): + # 1. Get Selected Effect from UI + var main = get_tree().get_root().get_node_or_null("Main") + if not main or not main.ui_manager: + return + + # Quick check if UI exists and has a selection + # We need to access the script variable 'selected_effect' from the dynamically created/loaded UI. + # Let's assume ui_manager has a reference or we find it. + # Based on previous step, we haven't integrated PowerUpInventoryUI into UIManager yet. + # So we might fail here if not wired up. + # For now, let's look for "PowerUpInventoryUI" in CanvasLayer. + + var inventory_ui = main.ui_manager.get_node_or_null("PowerUpInventoryUI") + # Or check if ui_manager tracks it. + # Note: We haven't instantiated it yet in UIManager. We will need to do that. + + if not inventory_ui or inventory_ui.selected_effect == -1: + return # No effect selected using mouse/touch first + + var effect = inventory_ui.selected_effect + + # 2. Find Target Player + var player_manager = main.get_node_or_null("PlayerManager") + if not player_manager: + return + + if target_index < 0 or target_index >= player_manager.connected_peer_ids.size(): + return + + var target_id = player_manager.connected_peer_ids[target_index] + var target_player = main.get_node_or_null(str(target_id)) + + if not target_player: + return + + if target_player == self and effect != 4: # 4 = INVISIBLE (Self) + # Trying to target self with harmful effect? + rpc("display_message", "Can't target self!", 3) + return + + # 3. Activate Effect + var st_manager = get_node_or_null("SpecialTilesManager") + if st_manager: + st_manager.activate_effect(effect, target_player) + inventory_ui.deselect() + func _process(delta): if is_multiplayer_authority(): diff --git a/scripts/managers/goal_manager.gd b/scripts/managers/goal_manager.gd index 0f88fcd..3d7e310 100644 --- a/scripts/managers/goal_manager.gd +++ b/scripts/managers/goal_manager.gd @@ -1,9 +1,13 @@ extends Node -# GoalManager - Manages goal generation and synchronization +# GoalManager - Manages goal generation, synchronization, and speed tracking var preset_goals: Array = [] +# Speed Tracking +var player_completion_times: Dictionary = {} # { player_id: [time_taken, time_taken, ...] } +var player_start_times: Dictionary = {} # { player_id: timestamp_msec } + func initialize_random_goals(size: int, min_value: int, max_value: int, null_count: float) -> Array: var goals = [] var rng = RandomNumberGenerator.new() @@ -38,3 +42,62 @@ func get_goals_for_player(player_index: int) -> Array: if player_index >= 0 and player_index < preset_goals.size(): return preset_goals[player_index].duplicate() return [] + +# ============================================================================= +# Speed Tracking Logic +# ============================================================================= + +func mark_goal_start(player_id: int): + player_start_times[player_id] = Time.get_ticks_msec() + +func mark_goal_complete(player_id: int): + if not player_start_times.has(player_id): + return + + var duration_sec = (Time.get_ticks_msec() - player_start_times[player_id]) / 1000.0 + + if not player_completion_times.has(player_id): + player_completion_times[player_id] = [] + + player_completion_times[player_id].append(duration_sec) + # Reset start time for next goal + player_start_times[player_id] = Time.get_ticks_msec() + + # print("Player %s completed goal in %.2fs" % [player_id, duration_sec]) + +func get_player_average_time(player_id: int) -> float: + if not player_completion_times.has(player_id) or player_completion_times[player_id].is_empty(): + return 10.0 # Default baseline (10 seconds) + + var total = 0.0 + for t in player_completion_times[player_id]: + total += t + return total / player_completion_times[player_id].size() + +func get_global_average_time() -> float: + var total_avg = 0.0 + var count = 0 + + for pid in player_completion_times: + var p_avg = get_player_average_time(pid) + total_avg += p_avg + count += 1 + + if count == 0: + return 10.0 + + return total_avg / count + +func get_boost_multiplier(player_id: int) -> float: + var p_avg = get_player_average_time(player_id) + var g_avg = get_global_average_time() + + if p_avg > g_avg: + # Player is slower than average -> Boost fills faster + # Scale up to 1.5x based on how much slower (capped) + var ratio = p_avg / max(g_avg, 0.1) + return min(ratio, 1.5) + else: + # Player is faster than average -> Boost fills slower + # Scale down to 0.8x + return 0.8 diff --git a/scripts/managers/player_input_manager.gd b/scripts/managers/player_input_manager.gd index 6e4cfd9..ffcdf5c 100644 --- a/scripts/managers/player_input_manager.gd +++ b/scripts/managers/player_input_manager.gd @@ -46,6 +46,16 @@ func _process(delta): if target_position != player.current_position: movement_manager.simple_move_to(target_position) + # Targeting Inputs (1, 2, 3, 4) + if Input.is_key_pressed(KEY_1): + player.attempt_target_action(0) + elif Input.is_key_pressed(KEY_2): + player.attempt_target_action(1) + elif Input.is_key_pressed(KEY_3): + player.attempt_target_action(2) + elif Input.is_key_pressed(KEY_4): + player.attempt_target_action(3) + func handle_unhandled_input(event): # Early return if not authorized human player if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"): diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 7093634..39fed10 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -97,94 +97,48 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool: var other_player = player.get_player_at_position(target_pos) if not other_player: - return false # Should be occupied if we called this, but safety check + return false - # Calculate where they will be pushed to - var pushed_to_pos = target_pos + direction - - # Check if pushed destination is valid - if not enhanced_gridmap.is_position_valid(pushed_to_pos): - # Blocked by world bounds -> Double Push! - other_player.rpc("apply_stagger", 1.5) - return false - - # Check walkability of pushed destination - var cell_item = enhanced_gridmap.get_cell_item(Vector3i(pushed_to_pos.x, 0, pushed_to_pos.y)) - if cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items: - # Blocked by obstacle -> Double Push! - other_player.rpc("apply_stagger", 1.5) - return false - - # Check if pushed destination is ALREADY occupied (Double Push / Crush) - if player.is_position_occupied(pushed_to_pos): - # Blocked by another player -> Double Push! - other_player.rpc("apply_stagger", 1.5) - return false - - # Check if other player is currently moving (don't push moving players to avoid sync issues) - if other_player.is_player_moving: + # === NEW LOGIC: Only allow push if in ATTACK MODE === + if not player.get("is_attack_mode"): + # Standard bumping effect or nothing? + # User said "Remove standard push", so we just do nothing or small shake return false - # EXECUTE PUSH - print("Player %s PUSHING %s to %s" % [player.name, other_player.name, pushed_to_pos]) + # === SUPER PUSH (Attack Mode) === + print("Player %s SUPER PUSHING %s!" % [player.name, other_player.name]) - # Force move the other player - # We use rpc to sync this change. Since we are authority, we can dictate pos. + # 1. Drop Victim's Tiles + if other_player.has_method("drop_all_tiles"): + other_player.rpc("drop_all_tiles") # Sync drop - # 1. Update their position variable immediately so our move check passes next frame? - # actually simple_move_to continues immediately. - - # We need to forcefully animate/move them. - # Let's use their own movement RPC if possible, or a special push RPC. - # For simplicity, we'll assume they snap-move or use the standard path movement but faster? - # Let's use the standard movement for now to ensure consistency. - - var path = [Vector2(target_pos.x, target_pos.y), Vector2(pushed_to_pos.x, pushed_to_pos.y)] - # path.pop_front() # start_movement_along_path expects full path? - # Actually start_movement_along_path usually takes [start, end] or [end]? - # Looking at simple_move_to above: - # path = [current, target] - # path.pop_front() -> path is [target] - # So it expects just the waypoints excluding start. - - var push_path = [Vector2(pushed_to_pos.x, pushed_to_pos.y)] - - # Call RPC on the OTHER player - other_player.rpc("start_movement_along_path", push_path, false) # false = no cam checks? - # Wait, the boolean arg is "is_local_human"? - # In player.gd call: player.rpc("start_movement_along_path", path, not (player.is_bot...)) - - # Update their current_position immediately so `is_position_occupied` returns false for us - # (The RPC handles the visual tween, but we need logical update) - # However, start_movement_along_path usually updates current_position at start or end? - # If we don't update it now, our `player.is_position_occupied(grid_position)` check in subsequent frames might be ok, - # but `simple_move_to` is synchronous-ish. - - # Crucial: We need to make sure `player.is_position_occupied` returns false for `grid_position` - # RIGHT NOW so we can return true and move into it. - # But `is_position_occupied` checks `current_position` and `target_position`. - # So we need to update `other_player`'s state. - - other_player.target_position = pushed_to_pos - other_player.is_player_moving = true # Mark them as moving so they occupy the NEW spot vs OLD spot? - # Actually is_position_occupied checks BOTH current and target. - # So if they are moving, they occupy BOTH until finished? - # See player.gd: - # if player.is_player_moving and player.target_position == pos: return true - # if player.current_position == pos: return true - - # This implies a moving player blocks 2 tiles. - # If we want to move into `target_pos`, `other_player` must NOT count as occupying it. - # But they ARE at `target_pos`. - # We physically can't be at the same spot. - # So we essentially need them to VACATE `target_pos` logically. - - # Hack/Fix: We manually update their `current_position` to `pushed_to_pos` immediately? - # No, that breaks visual interpolation. - - # Solution: The push logic implies simultaneity or high speed. - # If we assume the push is instant-ish logic: - # We update valid logical positions. Visuals catch up. + # 2. Spawn PowerUps around Victim + # We delegate this to the attacker's SpecialTilesManager to handle the spawning authority + if player.special_tiles_manager and player.special_tiles_manager.has_method("spawn_powerups_around"): + player.special_tiles_manager.spawn_powerups_around(other_player.current_position) + + # 3. Knockback / Stagger + # Push them away + var pushed_to_pos = target_pos + direction + if enhanced_gridmap.is_position_valid(pushed_to_pos) and \ + enhanced_gridmap.get_cell_item(Vector3i(pushed_to_pos.x, 0, pushed_to_pos.y)) != -1 and \ + not player.is_position_occupied(pushed_to_pos): + + # Valid push + var push_path = [Vector2(pushed_to_pos.x, pushed_to_pos.y)] + other_player.rpc("start_movement_along_path", push_path, false) + other_player.target_position = pushed_to_pos # Logical update + + else: + # Wall/Blocked -> Stagger in place + other_player.rpc("apply_stagger", 1.5) + + # 4. Consume Boost + if player.powerup_manager: + player.powerup_manager.reset_boost() + + # 5. Reset Attack Mode + player.is_attack_mode = false return true diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index 55b1c3c..a7e0b53 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -64,15 +64,17 @@ func grab_item(grid_position: Vector2i) -> bool: # Apply changes locally first, server will validate/sync enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately - # Check if grabbed item is a holo tile (11-14) - add to powerup instead of triggering effect - var is_holo = item >= 11 and item <= 14 - if is_holo: - # Add holo pickup to power-up manager (4 pickups = 1 bar) - var powerup_manager = player.get_node_or_null("PowerUpManager") - if powerup_manager: - powerup_manager.add_holo_pickup() - # Convert holo tile to normal tile (11->7, 12->8, 13->9, 14->10) - item = item - 4 + # Handle Power-Up / Holo Tiles + # Holo Matrix: 11->7 (Heart), 12->8 (Diamond), 13->9 (Star), 14->10 (Coin) + if item >= 11 and item <= 14: + item = item - 4 # Convert to normal tile ID + + # Check if it's a power up tile (7-10) + if item >= 7 and item <= 10: + var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") + if special_tiles_manager: + # Add to inventory + special_tiles_manager.add_powerup_from_item(item) player.playerboard[target_slot] = item # Add to playerboard immediately @@ -83,7 +85,7 @@ func grab_item(grid_position: Vector2i) -> bool: # Check if goal is completed after grabbing _check_goal_completion() - + # === Server Sync === if multiplayer.is_server(): # HOST/SERVER: Broadcast to all clients @@ -207,12 +209,16 @@ func bot_try_grab_item() -> bool: var empty_slot = player.playerboard.find(-1) if empty_slot != -1: if player.is_multiplayer_authority(): - # Check if grabbed item is a holo tile (11-14) - add to powerup + # Convert Holo (11-14) if item >= 11 and item <= 14: - var powerup_manager = player.get_node_or_null("PowerUpManager") - if powerup_manager: - powerup_manager.add_holo_pickup() - item = item - 4 # Convert to normal tile + item = item - 4 + + # Inventory Add + if item >= 7 and item <= 10: + var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") + if special_tiles_manager: + special_tiles_manager.add_powerup_from_item(item) + player.playerboard[empty_slot] = item player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1) player.rpc("sync_playerboard", player.playerboard) diff --git a/scripts/managers/powerup_manager.gd b/scripts/managers/powerup_manager.gd index dfb28e1..f4543b2 100644 --- a/scripts/managers/powerup_manager.gd +++ b/scripts/managers/powerup_manager.gd @@ -1,161 +1,95 @@ extends Node -# PowerUpManager - Handles power-up points, holo tile tracking, and special effect usage +# PowerUpManager - Handles Boost Meter (Time-based logic) +# Note: Inventory logic is now in SpecialTilesManager + PlayerboardManager -const MAX_POINTS: int = 12 -const POINTS_PER_BAR: int = 4 -const MAX_BARS: int = 4 -const HOLO_PICKUPS_PER_BAR: int = 4 -const SPECIAL_COOLDOWN: float = 4.0 # 4 second cooldown +const MAX_BOOST: float = 100.0 +const BASE_FILL_RATE: float = 4.0 # 4 points per second baseline (25s to full) var player: Node3D var enhanced_gridmap: Node +var goal_manager: Node -# Power-up state -var current_points: int = 0 -var holo_pickup_count: int = 0 -var special_cooldown_timer: float = 0.0 # Current cooldown remaining +# Boost State +var current_boost: float = 0.0 -signal points_changed(current: int, max_points: int) +signal points_changed(current: int, max_points: int) # Reused for UI (int casting) signal bar_filled() -signal effect_used() +signal boost_reset() func initialize(p_player: Node3D, p_gridmap: Node): player = p_player enhanced_gridmap = p_gridmap + + # Find GoalManager + var main = player.get_tree().get_root().get_node_or_null("Main") + if main: + goal_manager = main.get_node_or_null("GoalManager") + set_process(true) func _process(delta): - # Update cooldown timer - if special_cooldown_timer > 0: - special_cooldown_timer -= delta + if not is_instance_valid(player) or not player.is_multiplayer_authority(): + return + + # Only fill if not full + if current_boost < MAX_BOOST: + var multiplier = 1.0 + if goal_manager: + # Use authority ID for lookup + multiplier = goal_manager.get_boost_multiplier(player.get_multiplayer_authority()) + + current_boost += BASE_FILL_RATE * multiplier * delta + current_boost = min(current_boost, MAX_BOOST) + + # Update UI (Cast to int for compatibility with existing UI slider/bar) + emit_signal("points_changed", int(current_boost), int(MAX_BOOST)) + + if current_boost >= MAX_BOOST: + _on_boost_full() -# ============================================================================= -# Holo Tile Pickup -# ============================================================================= - -func add_holo_pickup(): - """Called when player picks up a holo tile (11-14).""" - holo_pickup_count += 1 - - if holo_pickup_count >= HOLO_PICKUPS_PER_BAR: - holo_pickup_count = 0 - _add_bar() - - print("[PowerUp] Player %s picked up holo tile. Count: %d/4" % [player.name, holo_pickup_count]) - - if player.is_multiplayer_authority(): - rpc("sync_holo_count", holo_pickup_count, current_points) - -func _add_bar(): - """Add one full bar (4 points) of power-up.""" - var points_to_add = POINTS_PER_BAR - current_points = min(current_points + points_to_add, MAX_POINTS) - +func _on_boost_full(): + player.is_attack_mode = true emit_signal("bar_filled") - emit_signal("points_changed", current_points, MAX_POINTS) - - # Type 1 = POWERUP message for special styling - player.rpc("display_message", "Power-up bar filled!", 1) - print("[PowerUp] Player %s gained 1 bar! Total: %d/%d points" % [player.name, current_points, MAX_POINTS]) + player.rpc("display_message", "ATTACK MODE READY!", 1) + print("[PowerUp] Player %s Boost Full! Entering Attack Mode." % player.name) if player.is_multiplayer_authority(): - player.get_node("PowerUpManager").rpc("sync_points", current_points) + rpc("sync_boost", current_boost) -# ============================================================================= -# Goal Completion Reward -# ============================================================================= - -func acquire_smash_bonus(): - """Called when player is smashed. Grants 1 bar up to a max of 2 bars.""" - if get_bars() < 2: - _add_bar() - print("[PowerUp] Player %s gained smash bonus bar! Total: %d/%d" % [player.name, current_points, MAX_POINTS]) - else: - print("[PowerUp] Player %s smash bonus capped (already has >= 2 bars)" % player.name) - -func add_goal_completion_reward(): - """Called when player completes a goal pattern. Awards 1 bar.""" - _add_bar() - print("[PowerUp] Player %s completed goal - awarded 1 bar" % [player.name]) - -# ============================================================================= -# Using Special Effects -# ============================================================================= - -func can_use_special() -> bool: - """Returns true if player has at least 1 bar (4 points) AND IS NOT ON COOLDOWN.""" - return current_points >= POINTS_PER_BAR and special_cooldown_timer <= 0 - -func get_bars() -> int: - """Returns current number of full bars.""" - return current_points / POINTS_PER_BAR - -func use_special_effect(): - """Consume 1 bar and trigger a random special effect.""" - if not can_use_special(): - # Type 3 = WARNING message - player.rpc("display_message", "Not enough power-up!", 3) - return false - - # Check cooldown - if special_cooldown_timer > 0: - player.rpc("display_message", "Special on cooldown! (%.1fs)" % special_cooldown_timer, 3) - return false - - # Consume 1 bar - current_points -= POINTS_PER_BAR - emit_signal("effect_used") - emit_signal("points_changed", current_points, MAX_POINTS) - - # Start cooldown - special_cooldown_timer = SPECIAL_COOLDOWN - - # Play special animation (backflip) - synced across network - if player.is_multiplayer_authority() and player.has_method("sync_special_animation"): - player.rpc("sync_special_animation") - - # Trigger random special effect via SpecialTilesManager - var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") - if special_tiles_manager: - special_tiles_manager.trigger_random_effect() - - print("[PowerUp] Player %s used special effect! Remaining: %d/%d points, Cooldown: %.1fs" % [player.name, current_points, MAX_POINTS, SPECIAL_COOLDOWN]) +func reset_boost(): + current_boost = 0.0 + player.is_attack_mode = false + emit_signal("points_changed", 0, int(MAX_BOOST)) + emit_signal("boost_reset") if player.is_multiplayer_authority(): - rpc("sync_points", current_points) - - return true + rpc("sync_boost", 0.0) # ============================================================================= # Sync # ============================================================================= @rpc("any_peer", "call_local", "reliable") -func sync_holo_count(count: int, points: int): - holo_pickup_count = count - current_points = points - emit_signal("points_changed", current_points, MAX_POINTS) - -@rpc("any_peer", "call_local", "reliable") -func sync_points(points: int): - current_points = points - emit_signal("points_changed", current_points, MAX_POINTS) +func sync_boost(value: float): + current_boost = value + emit_signal("points_changed", int(current_boost), int(MAX_BOOST)) + + # Client-side Attack Mode visual check (?) + if current_boost >= MAX_BOOST: + # Could trigger visual effect here + pass # ============================================================================= # Getters # ============================================================================= func get_points() -> int: - return current_points + return int(current_boost) func get_max_points() -> int: - return MAX_POINTS + return int(MAX_BOOST) func get_fill_percentage() -> float: - return float(current_points) / float(MAX_POINTS) + return current_boost / MAX_BOOST -func reset(): - current_points = 0 - holo_pickup_count = 0 - emit_signal("points_changed", current_points, MAX_POINTS) diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index 7ff771a..126b301 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -39,6 +39,21 @@ const INVISIBLE_DURATION = 6.0 var blocked_tiles: Array[Dictionary] = [] # {position: Vector3i, original_item: int, timer: float} var invisible_timer: float = 0.0 +# INVENTORY SYSTEM +# Stores count of each power-up type. Max 1 per type as per user request? +# "player can store 1 of each different power up" +var inventory = { + SpecialEffect.BURN_TILES: false, # Coin + SpecialEffect.SPAWN_TILES: false, # (Merged with Coin or deprecated? User said "coin : random between two") + # Let's map Items 7-10 to Effects + # 7=Heart=Block, 8=Diamond=Freeze, 9=Star=Invisible, 10=Coin=Burn/Spawn + SpecialEffect.BLOCK_FLOOR: false, + SpecialEffect.FREEZE_PLAYER: false, + SpecialEffect.INVISIBLE_MODE: false +} + +# Signal for UI +signal inventory_updated(inventory_data: Dictionary) func initialize(p_player: Node3D, p_gridmap: Node): player = p_player @@ -46,6 +61,83 @@ func initialize(p_player: Node3D, p_gridmap: Node): rng = RandomNumberGenerator.new() rng.randomize() +# ============================================================================= +# Helper: Item ID to Effect Enum +# ============================================================================= +func get_effect_from_item(item_id: int) -> int: + match item_id: + 7: return SpecialEffect.BLOCK_FLOOR # Heart + 8: return SpecialEffect.FREEZE_PLAYER # Diamond + 9: return SpecialEffect.INVISIBLE_MODE # Star + 10: return SpecialEffect.BURN_TILES # Coin (Handles Burn or Spawn) + _: return -1 + +func add_powerup_from_item(item_id: int): + var effect = get_effect_from_item(item_id) + if effect != -1: + inventory[effect] = true + emit_signal("inventory_updated", inventory) + print("Player %s picked up powerup for effect %s" % [player.name, SpecialEffect.keys()[effect]]) + + if player.is_multiplayer_authority(): + rpc("sync_inventory_add", effect) + +@rpc("any_peer", "call_local", "reliable") +func sync_inventory_add(effect: int): + inventory[effect] = true + emit_signal("inventory_updated", inventory) + +func remove_powerup(effect: int): + inventory[effect] = false + emit_signal("inventory_updated", inventory) + if player.is_multiplayer_authority(): + rpc("sync_inventory_remove", effect) + +@rpc("any_peer", "call_local", "reliable") +func sync_inventory_remove(effect: int): + inventory[effect] = false + emit_signal("inventory_updated", inventory) + +# ============================================================================= +# Activate Effect (Explicit Target) +# ============================================================================= + +func activate_effect(effect: int, target_player: Node3D = null): + # Validation + if not inventory.get(effect, false): + return # Start/Client mismatch + + # Consume + remove_powerup(effect) + + print("[SpecialTiles] Player %s activated %s on %s" % [player.name, SpecialEffect.keys()[effect], target_player.name if target_player else "Self"]) + + match effect: + SpecialEffect.BURN_TILES: + # Coin: Random between Burn or Spawn + # "coin : random between two... make it not use directly" -> When activated, it does one of them. + if rng.randf() < 0.5: + _execute_burn_tiles(target_player) + else: + # Spawn tiles around SELF (as per user request "around activating player") + _execute_spawn_tiles(player) + + SpecialEffect.BLOCK_FLOOR: + if target_player: + _execute_block_floor(target_player) + + SpecialEffect.FREEZE_PLAYER: + if target_player: + _execute_freeze_player(target_player) + + SpecialEffect.INVISIBLE_MODE: + # Always self + _execute_invisible_mode(player) + + # Play generic cast animation or sound? + if player.is_multiplayer_authority(): + player.rpc("trigger_screen_shake", "light") + # ============================================================================= # Check if item is a holo tile @@ -65,15 +157,15 @@ func trigger_random_effect(): match effect: SpecialEffect.BURN_TILES: - _execute_burn_tiles() + _execute_burn_tiles(_get_random_opponent()) SpecialEffect.SPAWN_TILES: - _execute_spawn_tiles() + _execute_spawn_tiles(player) SpecialEffect.FREEZE_PLAYER: - _execute_freeze_player() + _execute_freeze_player(_get_random_opponent()) SpecialEffect.BLOCK_FLOOR: - _execute_block_floor() + _execute_block_floor(player) SpecialEffect.INVISIBLE_MODE: - _execute_invisible_mode() + _execute_invisible_mode(player) # Sync effect to all clients if player.is_multiplayer_authority(): @@ -87,262 +179,122 @@ func sync_effect_triggered(effect: int): # Effect Implementations # ============================================================================= -func _execute_burn_tiles(): - # NEW LOGIC: Put back random target tiles from their playerboard to their position nearest - # Find random opponent - var opponent = _get_random_opponent() - if not opponent: - print("[SpecialTiles] No opponent found for BURN_TILES") - return +func _execute_burn_tiles(target: Node3D): + if not target: return - # Get opponent's playerboard items + # Knock tiles from target's board var board_indices = [] - for i in range(opponent.playerboard.size()): - if opponent.playerboard[i] != -1: + for i in range(target.playerboard.size()): + if target.playerboard[i] != -1: board_indices.append(i) if board_indices.is_empty(): - return # Nothing to burn + return - # Pick random 1x (3x3 equivalent = ~3-4 tiles) or 2x amount - # Let's say we burn 3 to 6 tiles var burn_count = rng.randi_range(3, 6) board_indices.shuffle() - var tiles_burned = 0 - - # Get valid empty spots near opponent to dump tiles - var empty_spots = _get_empty_neighbors_recursive(opponent.current_position, 2) - empty_spots.shuffle() + # Drop logic similar to burn but we just destroy them or scatter? + # "BURN_TILES, # Remove 3x3 pattern tiles" -> User request says Remove pattern. + # Original code did burn. + # Let's just remove them. for i in range(min(burn_count, board_indices.size())): - var slot_idx = board_indices[i] - var item = opponent.playerboard[slot_idx] + target.playerboard[board_indices[i]] = -1 - # Remove from opponent board - opponent.playerboard[slot_idx] = -1 - - # Determine where to put it - var target_pos = Vector3i.ZERO - var target_item = item - - if not empty_spots.is_empty(): - # Place on empty spot - var pos_2d = empty_spots.pop_back() - target_pos = Vector3i(pos_2d.x, 1, pos_2d.y) - else: - # No empty spots? "Replace it with new one" at a random nearby non-empty spot? - # Or just find ANY nearby spot and overwrite - var neighbors = enhanced_gridmap.get_neighbors(opponent.current_position, 1) - if not neighbors.is_empty(): - var rand_n = neighbors[rng.randi() % neighbors.size()] - target_pos = Vector3i(rand_n.position.x, 1, rand_n.position.y) - # If we are overwriting or essentially "spawning" a new one to replace it - target_item = rng.randi_range(7, 10) # As per request "replace it with new one" if floor not empty - - if target_pos != Vector3i.ZERO: - if player.is_multiplayer_authority(): - var main = player.get_tree().get_root().get_node_or_null("Main") - if main: - main.rpc("sync_grid_item", target_pos.x, target_pos.y, target_pos.z, target_item) - # Sync opponent board change - main.rpc("sync_playerboard", opponent.name.to_int(), opponent.playerboard) - tiles_burned += 1 - - if tiles_burned > 0: - # Trigger screen shake - if opponent.is_multiplayer_authority(): - opponent.rpc("trigger_screen_shake", "targeted") - else: - opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted") + if player.is_multiplayer_authority(): + # Sync the change on the target + var main = player.get_tree().get_root().get_node_or_null("Main") + if main: + main.rpc("sync_playerboard", target.name.to_int(), target.playerboard) - print("[SpecialTiles] BURN_TILES: Knocked %d tiles from %s" % [tiles_burned, opponent.name]) - player.rpc("display_message", "Knocked tiles from %s!" % opponent.display_name) - opponent.rpc("display_message", "%s knocked tiles out of your bag!" % player.display_name) + target.rpc("display_message", "Burned by %s!" % player.display_name, 3) -func _execute_spawn_tiles(): - # NEW LOGIC: Spawn more in neighbor space (radius 2) - var radius = 2 - var candidates = [] +func _execute_spawn_tiles(target: Node3D): + # Spawn tiles around TARGET (usually Self for Coin) + spawn_powerups_around(target.current_position, false) # False = normal tiles? User says "Spawn 3x3 pattern tiles" + # Okay "SPAWN_TILES" usually means useful numbered tiles. + # But "spawn / replace your nearby tiles into power up" is for Headbutt. + # For Coin->Spawn_Tiles: "Spawn 3x3 pattern tiles around activating player ( self )". + # So random number tiles (7-10 are powerups, 1-6 are normal? No, 7-10 are patterns in this game). + # "Spawn 3x3 pattern tiles" -> Tiles with ID 7,8,9,10 are the goal tiles. - for x in range(-radius, radius + 1): - for y in range(-radius, radius + 1): - if x == 0 and y == 0: continue - var pos = player.current_position + Vector2i(x, y) - if enhanced_gridmap.is_position_valid(pos): - var cell = Vector3i(pos.x, 1, pos.y) - if enhanced_gridmap.get_cell_item(cell) == -1: - candidates.append(cell) - - var spawn_count = rng.randi_range(3, 8) # Spawn a bunch - candidates.shuffle() - - var actual_spawned = 0 - for i in range(min(spawn_count, candidates.size())): - var cell = candidates[i] - var new_tile = rng.randi_range(7, 10) - if player.is_multiplayer_authority(): - var main = player.get_tree().get_root().get_node_or_null("Main") - if main: - main.rpc("sync_grid_item", cell.x, cell.y, cell.z, new_tile) - actual_spawned += 1 - - print("[SpecialTiles] SPAWN_TILES: Spawned %d tiles around %s" % [actual_spawned, player.name]) - player.rpc("display_message", "Spawned tiles nearby!") + target.rpc("display_message", "Tiles Spawned!", 2) -func _execute_freeze_player(): - # Find random opponent - var opponent = _get_random_opponent() - if not opponent: +func _execute_freeze_player(target: Node3D): + if not target: print("[SpecialTiles] No opponent found for FREEZE_PLAYER") return - # Freeze the opponent - if opponent.has_method("apply_freeze"): - opponent.apply_freeze(FREEZE_DURATION) + if target.has_method("apply_stagger"): # Stagger = Freeze roughly + target.rpc("apply_stagger", FREEZE_DURATION) else: - # Fallback: directly set frozen state - opponent.set("is_frozen", true) - _create_unfreeze_timer(opponent, FREEZE_DURATION) - - # Trigger screen shake on the frozen opponent - if opponent.is_multiplayer_authority(): - opponent.rpc("trigger_screen_shake", "targeted") - else: - opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted") - - print("[SpecialTiles] FREEZE_PLAYER: Froze %s for %ds" % [opponent.name, FREEZE_DURATION]) - player.rpc("display_message", "Froze %s!" % opponent.display_name) - opponent.rpc("display_message", "%s froze you!" % player.display_name) - - # Visual effect: Ice Blue - # Use RPC to sync visual effect to everyone (call_local handles our screen) - if opponent.has_method("sync_modulate"): - opponent.rpc("sync_modulate", Color(0.5, 0.8, 1.0)) - - # Standard players sync via network transform but modulation might not sync automatically unless handled. - # Let's hope basic property sync or local effect handles it enough for now, - # but ideally we should RPC a visual update method on the player. - # Checking player.gd again, there isn't a sync_modulate. - # We can just set it locally and rely on the RPCs below for syncing the EFFECT STATUS, - # but we should probably RPC the color change to be sure everyone sees it. - # Actually, since we don't have a generic sync_proeprty, we will just set it locally on the authority - # and rely on the target itself to perhaps propogate it? No, that won't work traversing network. - # We need a way to tell clients "Painter this player blue". - # The simplest safe way without modifying Player.gd extensively is to rpc a method call if available, - # or just set it on the proxy if we are the server. - # But special_tiles is running on the player who TRIGGERED it. - # If I am client A, targeting client B. I am authority of ME. B is authority of B. - # I can't set properties on B directly and expect them to sync. - # I must RPC B to freeze himself. - # The _execute_freeze_player logic calls opponent.apply_freeze or sets is_frozen. - # If opponent has authority, they will run their own logic? - # Wait, special_tiles_manager runs on the client who picked up the tile? - # "if player.is_multiplayer_authority(): rpc(...)" implies we are the authority of the player who picked it up. - # We find an opponent (which is a proxy version on our machine). - # We call methods on that proxy. - # "opponent.rpc(...)" sends a message to the authority of that opponent. - # So we should validly call an RPC on opponent to change color. - # But Player.gd doesn't have "set_modulate_rpc". - # Use "set" works locally. - # We need to add visual sync support to Player.gd or just rely on what we have. - # Given constraints, I'll add the modulate locally and maybe the opponent-side logic should handle it? - # _create_unfreeze_timer runs on OUR machine mostly? No, "await player.get_tree()..." - # If we are A, targeting B. - # We call opponent.apply_freeze(). If B has that method, good. - # If B lacks it, we set is_frozen on B's proxy and run a timer on A's machine? - # That only freezes B on A's screen if logic relies on is_frozen? - # Actually, `opponent.rpc("display_message", ...)` works. - # Let's add a `sync_visual_effect` to Player.gd if needed, or just standard property setting if supported. - # For now, I will just set it and see if I can add a dedicated RPC in Player.gd in the next step if this is insufficient, - # OR better: I'll use `opponent.rpc("sync_modulate", ...)` and add that method to Player.gd in a separate tool call. - # For this tool call, I'll update the text and set local modulate. - - -func _create_unfreeze_timer(target_player: Node3D, duration: float): - if not is_instance_valid(player) or not is_instance_valid(target_player): - return + target.set("is_frozen", true) + _create_unfreeze_timer(target, FREEZE_DURATION) - await player.get_tree().create_timer(duration).timeout - - if is_instance_valid(target_player): - target_player.set("is_frozen", false) - # Reset visuals - if target_player.has_method("sync_modulate"): - target_player.rpc("sync_modulate", Color.WHITE) - target_player.rpc("display_message", "Unfrozen!") + target.rpc("display_message", "Frozen by %s!" % player.display_name, 3) - -func _execute_block_floor(): - # NEW LOGIC: Block 3 to 9 tiles in a line (Horizontal/Vertical/Diagonal) - # Find valid start neighbor - var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0) - var valid_neighbors = neighbors.filter(func(n): return n.is_walkable) +func _execute_block_floor(target: Node3D): + # Make nearby tile non-walkable for 9 seconds + # Target the floor UNDER or NEAR the target? + # "block ( other player ) spawn BLOCK_FLOOR Make nearby tile non-walkable" + # Let's block the tile they are standing on + neighbors? + var center = target.current_position + var neighbors = enhanced_gridmap.get_neighbors(center, 1) # Include diagonals + neighbors.append({"position": center}) # Add center - if valid_neighbors.is_empty(): - return - - var start_neighbor = valid_neighbors[rng.randi() % valid_neighbors.size()] - var start_pos = start_neighbor.position - - # Random direction: H, V, D1, D2 - var directions = [ - Vector2i(1, 0), Vector2i(-1, 0), # Horizontal - Vector2i(0, 1), Vector2i(0, -1), # Vertical - Vector2i(1, 1), Vector2i(-1, -1), # Diagonal - Vector2i(1, -1), Vector2i(-1, 1) - ] - var dir = directions[rng.randi() % directions.size()] - var count = rng.randi_range(3, 9) - - var valid_block_count = 0 - - for i in range(count): - var target_pos_2d = start_pos + (dir * i) - # Check if valid grid position - if not enhanced_gridmap.is_position_valid(target_pos_2d): - break # Stop if we hit edge of map - - var block_pos = Vector3i(target_pos_2d.x, 0, target_pos_2d.y) - var original_item = enhanced_gridmap.get_cell_item(block_pos) + for n in neighbors: + var pos = n.position + var block_pos = Vector3i(pos.x, 0, pos.y) - # Make tile non-walkable - var blocked_item = 4 - if enhanced_gridmap.non_walkable_items.size() > 0: - blocked_item = enhanced_gridmap.non_walkable_items[0] - + # Block it if player.is_multiplayer_authority(): var main = player.get_tree().get_root().get_node_or_null("Main") if main: - main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, blocked_item) + # 4 = Blocked Tile ID usually + main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, 4) + var original_item = enhanced_gridmap.get_cell_item(block_pos) blocked_tiles.append({ "position": block_pos, - "original_item": original_item, + "original_item": original_item if original_item != 4 else 0, # Restore to 0 (floor) if confused "timer": BLOCK_DURATION }) - valid_block_count += 1 - - if valid_block_count > 0: - enhanced_gridmap.initialize_astar() - print("[SpecialTiles] BLOCK_FLOOR: Blocked line of %d tiles" % valid_block_count) - player.rpc("display_message", "Blocked a wall of tiles!") + + target.rpc("display_message", "Floor Blocked!", 3) -func _execute_invisible_mode(): - # Set invisible mode on player - # NEW LOGIC: Also enables auto-grab in _process - if player.has_method("apply_invisible_mode"): - player.apply_invisible_mode(INVISIBLE_DURATION) - else: - player.set("is_invisible", true) - player.set("original_movement_range", player.movement_range) - player.movement_range = player.movement_range + 2 - invisible_timer = INVISIBLE_DURATION +func _execute_invisible_mode(target: Node3D): + target.is_invisible = true + # Auto-disable after duration handled in Player._process or here? + # SpecialTilesManager seems to handle effect timers. + invisible_timer = INVISIBLE_DURATION + target.rpc("display_message", "Invisible!", 2) + +# ============================================================================= +# Helper: Spawn Powerups (For Super Push) +# ============================================================================= + +func spawn_powerups_around(center: Vector2i, force_powerups: bool = true): + # "spawn / replace your nearby tiles into power up ( special tiles )" + # PowerUp Tiles are 7, 8, 9, 10 (Heart, Diamond, Star, Coin) - print("[SpecialTiles] INVISIBLE_MODE: Activated") - player.rpc("display_message", "Invisible Mode Active!") + var radius = 2 + for x in range(-radius, radius + 1): + for y in range(-radius, radius + 1): + var pos = center + Vector2i(x, y) + if enhanced_gridmap.is_position_valid(pos): + # Random chance + if rng.randf() > 0.4: continue + + var item_id = rng.randi_range(7, 10) # 7-10 are the special tiles + var cell = Vector3i(pos.x, 1, pos.y) + + if player.is_multiplayer_authority(): + var main = player.get_tree().get_root().get_node_or_null("Main") + if main: + main.rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id) func _process(delta): @@ -355,10 +307,8 @@ func _update_invisible_timer(delta: float): if invisible_timer <= 0: invisible_timer = 0 if is_instance_valid(player): - player.set("is_invisible", false) - if player.get("original_movement_range"): - player.movement_range = player.original_movement_range - player.rpc("display_message", "Invisible mode ended!") + player.is_invisible = false + player.rpc("display_message", "Invisibility Ended") # ============================================================================= @@ -419,3 +369,12 @@ func check_shield_and_cancel_effect() -> bool: player.rpc("display_message", "Shield blocked an attack!") return true return false + +func _create_unfreeze_timer(target_player: Node3D, duration: float): + await player.get_tree().create_timer(duration).timeout + if is_instance_valid(target_player): + target_player.set("is_frozen", false) + # Reset visuals + if target_player.has_method("sync_modulate"): + target_player.rpc("sync_modulate", Color.WHITE) + target_player.rpc("display_message", "Unfrozen!") diff --git a/scripts/managers/ui_manager.gd b/scripts/managers/ui_manager.gd index 2837ca0..ac399ce 100644 --- a/scripts/managers/ui_manager.gd +++ b/scripts/managers/ui_manager.gd @@ -16,8 +16,14 @@ var move_button var grab_button var put_button var randomize_button -var arrange_button +var victory_ui_scene = preload("res://scenes/ui/victory_ui.tscn") +var powerup_inventory_ui_script = preload("res://scripts/ui/powerup_inventory_ui.gd") + +var main_menu_instance +var victory_ui_instance var playerboard_ui +var action_menu_instance +var powerup_inventory_ui var local_player_character var _previous_playerboard_state: Array = [] @@ -26,22 +32,31 @@ enum ActionState { NONE, MOVING, GRABBING, - PUTTING, - RANDOMIZING, ARRANGING, + RANDOMIZING } var current_action_state = ActionState.NONE -func initialize(main_node): +func initialize(player_node): + # Get PowerUp Inventory UI from scene + powerup_inventory_ui = player_node.get_node_or_null("PowerUpInventoryUI") + # Get node references from main scene - action_menu = main_node.get_node("ActionMenu") - move_button = main_node.get_node("ActionMenu/ActionButtonContainer/MoveButton") - grab_button = main_node.get_node("ActionMenu/ActionButtonContainer/GrabButton") - put_button = main_node.get_node("ActionMenu/ActionButtonContainer/PutButton") - randomize_button = main_node.get_node("ActionMenu/ActionButtonContainer/RandomizeButton") - arrange_button = main_node.get_node("ActionMenu/ActionButtonContainer/ArrangeButton") - playerboard_ui = main_node.get_node("PlayerboardUI") + randomize_button = player_node.get_node("ActionMenu/ActionButtonContainer/RandomizeButton") # renamed main_node to player_node which is Main + arrange_button = player_node.get_node("ActionMenu/ActionButtonContainer/ArrangeButton") + playerboard_ui = player_node.get_node("PlayerboardUI") + +# ... (skipping unchanged functions) ... + +func set_local_player(player): + local_player_character = player + + if powerup_inventory_ui: + powerup_inventory_ui.setup(player) + + # Connect to powerup signals with deferred call (manager needs time to initialize) + _connect_powerup_manager_deferred(player) func setup_action_buttons(action_state_callback): move_button.pressed.connect(func(): action_state_callback.call(ActionState.MOVING)) diff --git a/scripts/ui/powerup_inventory_ui.gd b/scripts/ui/powerup_inventory_ui.gd new file mode 100644 index 0000000..e6eaa96 --- /dev/null +++ b/scripts/ui/powerup_inventory_ui.gd @@ -0,0 +1,94 @@ +extends Control + +# PowerUpInventoryUI - Displays stored powerups and handles selection + +# UI References +var icon_containers: Dictionary = {} # { EffectEnum: TextureRect } +var selection_indicators: Dictionary = {} # { EffectEnum: Control } + +# Local State +var selected_effect: int = -1 + +signal effect_selected(effect: int) + +func _ready(): + # Wait for children to be ready + await get_tree().process_frame + + # Map Effect Enum to UI Nodes (Assumes specific names in main.tscn) + # Default structure: HBoxContainer > CoinIcon, HeartIcon, etc. + var container = get_node_or_null("HBoxContainer") + if not container: + print("PowerUpUI: Container not found") + return + + # SpecialEffect.BURN_TILES (0) -> Coin + _setup_icon(0, container.get_node_or_null("CoinIcon")) + # SpecialEffect.BLOCK_FLOOR (3) -> Heart + _setup_icon(3, container.get_node_or_null("HeartIcon")) + # SpecialEffect.FREEZE_PLAYER (2) -> Diamond + _setup_icon(2, container.get_node_or_null("DiamondIcon")) + # SpecialEffect.INVISIBLE_MODE (4) -> Star + _setup_icon(4, container.get_node_or_null("StarIcon")) + + # Note: SpecialEffect enum values from SpecialTilesManager: + # BURN_TILES = 0 + # SPAWN_TILES = 1 (we map Coin to 0, merged) + # FREEZE_PLAYER = 2 + # BLOCK_FLOOR = 3 + # INVISIBLE_MODE = 4 + +func _setup_icon(effect_id: int, node: Control): + if not node: return + + icon_containers[effect_id] = node + + # Assume node has specific children for selection state? + # "SelectRect" child? + var select_rect = node.get_node_or_null("SelectRect") + if select_rect: + selection_indicators[effect_id] = select_rect + select_rect.visible = false + + # Connect click event + if not node.gui_input.is_connected(_on_icon_input): + node.gui_input.connect(_on_icon_input.bind(effect_id)) + +func _on_icon_input(event, effect_id: int): + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + if selected_effect == effect_id: + deselect() + else: + select_effect(effect_id) + +func setup(player_node): + var special_manager = player_node.get_node_or_null("SpecialTilesManager") + if special_manager: + if not special_manager.is_connected("inventory_updated", _on_inventory_updated): + special_manager.connect("inventory_updated", _on_inventory_updated) + _on_inventory_updated(special_manager.inventory) + +func _on_inventory_updated(inventory: Dictionary): + # Update UI icons (Dimmed vs Lit) + for effect in icon_containers: + if inventory.has(effect): + var has_item = inventory[effect] + icon_containers[effect].modulate = Color.WHITE if has_item else Color(0.3, 0.3, 0.3, 0.5) + + if not has_item and selected_effect == effect: + deselect() + +func select_effect(effect: int): + # Check if we own it first? The UI click handler should check. + selected_effect = effect + emit_signal("effect_selected", effect) + _update_selection_visuals() + +func deselect(): + selected_effect = -1 + emit_signal("effect_selected", -1) + _update_selection_visuals() + +func _update_selection_visuals(): + for effect in selection_indicators: + selection_indicators[effect].visible = (effect == selected_effect) diff --git a/tiles_armagedon_a1.res b/tiles_armagedon_a1.res new file mode 100644 index 0000000000000000000000000000000000000000..32d3e43fd314e9860ef666c32bdebc99b0a5e53d GIT binary patch literal 11552 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 Kv4`n@qyG;N65=HQ literal 0 HcmV?d00001