feat(gauntlet): replace Cleanser with Ghost powerup sticky bypass (v2.4.2)

- Remove entire Cleanser system (signal, vars, HUD, input, RPCs, bot AI)
- Ghost (Invisible Mode) now bypasses sticky tiles in Gauntlet
- Grant Ghost powerup every 2 missions instead of Cleanser charges
- Ghost tiles spawn naturally on Gauntlet arena (15% chance)
- Bots use Ghost powerup when boxed in by sticky tiles
- Players pushed into sticky while Ghost are not slowed
- Remove use_cleanser input action from project.godot
- Remove CleanserHBox UI from gauntlet_hud.tscn
- Bump version to 2.4.2
This commit is contained in:
2026-07-03 14:54:39 +08:00
parent 5ba7de3fd6
commit 19e7f619ab
10 changed files with 137 additions and 394 deletions
+11 -1
View File
@@ -152,7 +152,17 @@
- Fragment Craft system — collect drops to craft exclusive skins - Fragment Craft system — collect drops to craft exclusive skins
- Fixed boot screen stuck on "Checking versions..." - Fixed boot screen stuck on "Checking versions..."
## [2.4.1] — $(date +"%Y-%m-%d") ## [2.4.2] — 2026-07-03
- Replaced the **Cleanser** mechanic in Gauntlet mode with a **Ghost powerup** sticky-bypass system.
- Ghost (Invisible Mode) now lets players walk through sticky candy tiles in Gauntlet — no more hard block.
- Players earn a Ghost powerup every 2 completed missions in Gauntlet (replaces Cleanser charge grants).
- Ghost powerup tiles now spawn naturally on the Gauntlet arena (15% chance alongside common tiles).
- Removed Cleanser HUD elements (icon, label, charge counter) from the Gauntlet overlay.
- Removed `use_cleanser` input action — Ghost uses the existing powerup activation keybind.
- Bots now activate Ghost powerup when boxed in by sticky tiles instead of using Cleanser.
- Players pushed into sticky tiles while in Ghost mode are no longer slowed.
## [2.4.1] — 2026-06-28
- Fixed Gauntlet map layout to remove red unpassable barrier blocks and center blocks. They are now standard walkable floors but act as hard blockers in physics so players cannot pass them. - Fixed Gauntlet map layout to remove red unpassable barrier blocks and center blocks. They are now standard walkable floors but act as hard blockers in physics so players cannot pass them.
- Fixed Gauntlet mode to prevent powerups or sticky bubbles from spawning on boundary tiles or under the central cannon. - Fixed Gauntlet mode to prevent powerups or sticky bubbles from spawning on boundary tiles or under the central cannon.
- Center Candy Cannon now shoots actual projectiles that fly towards sticky cells and leave a VFX trail behind them. - Center Candy Cannon now shoots actual projectiles that fly towards sticky cells and leave a VFX trail behind them.
+16 -1
View File
@@ -1,7 +1,22 @@
{ {
"latest_version": "2.4.1", "latest_version": "2.4.2",
"minimum_app_version": "2.1.0", "minimum_app_version": "2.1.0",
"releases": [ "releases": [
{
"version": "2.4.2",
"date": "2026-07-03",
"pck_url": "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/patch.pck",
"pck_size": 0,
"changelog": [
"Replaced Cleanser mechanic in Gauntlet with Ghost powerup sticky-bypass system.",
"Ghost (Invisible Mode) now lets players walk through sticky candy tiles in Gauntlet.",
"Players earn a Ghost powerup every 2 completed missions in Gauntlet.",
"Ghost powerup tiles now spawn naturally on the Gauntlet arena (15% chance).",
"Removed Cleanser HUD elements from Gauntlet overlay.",
"Bots now activate Ghost powerup when boxed in by sticky tiles.",
"Players pushed into sticky tiles while in Ghost mode are no longer slowed."
]
},
{ {
"version": "2.4.1", "version": "2.4.1",
"date": "2026-06-28", "date": "2026-06-28",
+23 -16
View File
@@ -1,14 +1,20 @@
[runnable_presets]
"Windows Desktop"="Windows Desktop"
Android="Android"
macOS="macOS"
Linux="Linux/X11"
[preset.0] [preset.0]
name="Windows Desktop" name="Windows Desktop"
platform="Windows Desktop" platform="Windows Desktop"
runnable=true
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/tekton_armageddon_v2.4.1.exe" export_path="build/windows/tekton_armageddon_v2.4.2.exe"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -42,8 +48,8 @@ application/modify_resources=false
application/icon="" application/icon=""
application/console_wrapper_icon="" application/console_wrapper_icon=""
application/icon_interpolation=4 application/icon_interpolation=4
application/file_version="2.4.1" application/file_version="2.4.2"
application/product_version="2.4.1" application/product_version="2.4.2"
application/company_name="DanchieGo" application/company_name="DanchieGo"
application/product_name="Tekton Armageddon" application/product_name="Tekton Armageddon"
application/file_description="" application/file_description=""
@@ -74,13 +80,12 @@ Remove-Item -Recurse -Force '{temp_dir}'"
name="Android" name="Android"
platform="Android" platform="Android"
runnable=true
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/tekton-dash-armageddon-v.2.4.1.apk" export_path="build/tekton-dash-armageddon-v.2.4.2.apk"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -111,7 +116,7 @@ architectures/arm64-v8a=true
architectures/x86=false architectures/x86=false
architectures/x86_64=false architectures/x86_64=false
version/code=3 version/code=3
version/name="2.4.1" version/name="2.4.2"
package/unique_name="com.danchiego.$genname" package/unique_name="com.danchiego.$genname"
package/name="Tekton Dash Armageddon" package/name="Tekton Dash Armageddon"
package/signed=true package/signed=true
@@ -136,11 +141,12 @@ screen/support_normal=true
screen/support_large=true screen/support_large=true
screen/support_xlarge=true screen/support_xlarge=true
screen/background_color=Color(0, 0, 0, 1) screen/background_color=Color(0, 0, 0, 1)
splash_screen/disable_godot_boot_splash=false
splash_screen/icon=""
splash_screen/branding_image=""
splash_screen/background_color=Color(0, 0, 0, 1)
user_data_backup/allow=false user_data_backup/allow=false
command_line/extra_args="" command_line/extra_args=""
apk_expansion/enable=false
apk_expansion/SALT=""
apk_expansion/public_key=""
permissions/custom_permissions=PackedStringArray() permissions/custom_permissions=PackedStringArray()
permissions/access_checkin_properties=false permissions/access_checkin_properties=false
permissions/access_coarse_location=false permissions/access_coarse_location=false
@@ -295,18 +301,20 @@ permissions/write_sms=false
permissions/write_social_stream=false permissions/write_social_stream=false
permissions/write_sync_settings=false permissions/write_sync_settings=false
permissions/write_user_dictionary=false permissions/write_user_dictionary=false
apk_expansion/enable=false
apk_expansion/SALT=""
apk_expansion/public_key=""
[preset.2] [preset.2]
name="macOS" name="macOS"
platform="macOS" platform="macOS"
runnable=true
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/tekton_armageddon_v2.4.1.zip" export_path="build/tekton_armageddon_v2.4.2.zip"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -565,8 +573,8 @@ codesign/digest_algorithm=1
codesign/identity_type=0 codesign/identity_type=0
application/modify_resources=false application/modify_resources=false
application/console_wrapper_icon="" application/console_wrapper_icon=""
application/file_version="2.4.1" application/file_version="2.4.2"
application/product_version="2.4.1" application/product_version="2.4.2"
application/company_name="DanchieGo" application/company_name="DanchieGo"
application/product_name="Tekton Armageddon" application/product_name="Tekton Armageddon"
application/file_description="" application/file_description=""
@@ -576,13 +584,12 @@ application/trademarks=""
name="Linux/X11" name="Linux/X11"
platform="Linux" platform="Linux"
runnable=true
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="build/tekton_armageddon_v2.4.1.x86_64" export_path="build/linux/tekton_armageddon_v2.4.22.x86_64"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
+7 -10
View File
@@ -17,7 +17,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
config/name="Tekton Dash Armageddon" config/name="Tekton Dash Armageddon"
config/version="2.4.1" config/version="2.4.1"
run/main_scene="res://scenes/ui/boot_screen.tscn" run/main_scene="res://scenes/ui/boot_screen.tscn"
config/features=PackedStringArray("4.6", "Forward Plus") config/features=PackedStringArray("4.7", "Forward Plus")
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1) boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
boot_splash/stretch_mode=0 boot_splash/stretch_mode=0
boot_splash/image="uid://b10e6kr508642" boot_splash/image="uid://b10e6kr508642"
@@ -27,7 +27,7 @@ boot_splash/minimum_display_time=2
[autoload] [autoload]
GameUpdateManager="*res://scripts/managers/game_update_manager.gd" GameUpdateManager="*res://scripts/managers/game_update_manager.gd"
Nakama="*res://addons/com.heroiclabs.nakama/Nakama.gd" Nakama="*uid://bueyqhhvxe0tx"
NakamaManager="*res://scripts/nakama_manager.gd" NakamaManager="*res://scripts/nakama_manager.gd"
AuthManager="*res://scripts/managers/auth_manager.gd" AuthManager="*res://scripts/managers/auth_manager.gd"
LobbyManager="*res://scripts/managers/lobby_manager.gd" LobbyManager="*res://scripts/managers/lobby_manager.gd"
@@ -37,7 +37,7 @@ TurnManager="*res://scripts/managers/turn_manager.gd"
GoalManager="*res://scripts/managers/goal_manager.gd" GoalManager="*res://scripts/managers/goal_manager.gd"
PlayerManager="*res://scripts/managers/player_manager.gd" PlayerManager="*res://scripts/managers/player_manager.gd"
GoalsCycleManager="*res://scripts/managers/goals_cycle_manager.gd" GoalsCycleManager="*res://scripts/managers/goals_cycle_manager.gd"
Satori="*res://addons/com.heroiclabs.nakama/Satori.gd" Satori="*uid://b8vev00s34b7"
SettingsManager="*res://scripts/managers/settings_manager.gd" SettingsManager="*res://scripts/managers/settings_manager.gd"
SfxManager="*res://scripts/managers/sfx_manager.gd" SfxManager="*res://scripts/managers/sfx_manager.gd"
NameGenerator="*res://scripts/generators/name_generator.gd" NameGenerator="*res://scripts/generators/name_generator.gd"
@@ -67,8 +67,11 @@ enabled=PackedStringArray("res://addons/com.heroiclabs.nakama/plugin.cfg", "res:
[file_customization] [file_customization]
folder_colors={ folder_colors={
"res://addons/": "pink",
"res://assets/": "purple", "res://assets/": "purple",
"res://scenes/": "green" "res://scenes/": "green",
"res://scripts/": "green",
"res://server/": "blue"
} }
[input] [input]
@@ -140,12 +143,6 @@ use_powerup={
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":false,"script":null) , Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":3,"pressure":0.0,"pressed":false,"script":null)
] ]
} }
use_cleanser={
"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)
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
]
}
action_grab_tekton={ action_grab_tekton={
"deadzone": 0.5, "deadzone": 0.5,
"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":71,"physical_keycode":0,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null) "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":71,"physical_keycode":0,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null)
-19
View File
@@ -47,22 +47,3 @@ theme_override_constants/outline_size = 6
theme_override_fonts/font = ExtResource("1_font") theme_override_fonts/font = ExtResource("1_font")
text = "🍬 OPEN ARENA" text = "🍬 OPEN ARENA"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="CleanserHBox" type="HBoxContainer" parent="BottomContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 6
alignment = 1
[node name="CleanserIcon" type="TextureRect" parent="BottomContainer/VBoxContainer/CleanserHBox"]
layout_mode = 2
custom_minimum_size = Vector2(20, 20)
stretch_mode = 5
[node name="CleanserLabel" type="Label" parent="BottomContainer/VBoxContainer/CleanserHBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 20
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
theme_override_constants/outline_size = 6
theme_override_fonts/font = ExtResource("1_font")
text = "[E] Cleanser (0)"
horizontal_alignment = 1
+19 -22
View File
@@ -210,9 +210,9 @@ func _run_ai_tick():
print("[BotController] Action Taken: Attack Pursuit") print("[BotController] Action Taken: Attack Pursuit")
return return
# Priority 0.5: Gauntlet (#075) — burn Cleanser if boxed in # Priority 0.5: Gauntlet (#075) — use Ghost powerup if boxed in
if await _try_activate_cleanser(): if await _try_activate_ghost():
print("[BotController] Action Taken: Cleanser (trapped)") print("[BotController] Action Taken: Ghost (trapped)")
return return
# Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying) # Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying)
@@ -260,27 +260,24 @@ func _run_ai_tick():
return return
# ============================================================================= # =============================================================================
# Gauntlet (#075) — Cleanser + Sticky Avoidance wiring # Gauntlet (#075) — Ghost Powerup + Sticky Avoidance wiring
# ============================================================================= # =============================================================================
func _try_activate_cleanser() -> bool: func _try_activate_ghost() -> bool:
"""Activate Cleanser when the planner reports imminent danger. """Activate Ghost powerup when the planner reports imminent danger.
Server-authoritative RPC; we only request it. Returns true if the request Uses the existing SpecialTilesManager to activate the held ghost powerup.
was sent successfully (not a guarantee it landed on a sticky cell).""" Returns true if activation was triggered."""
if not strategic_planner or not strategic_planner.is_gauntlet_mode(): if not strategic_planner or not strategic_planner.is_gauntlet_mode():
return false return false
if not strategic_planner.should_activate_cleanser_now(): if not strategic_planner.should_activate_ghost_now():
return false return false
var gm = strategic_planner._get_gauntlet_manager() var stm = actor.get_node_or_null("SpecialTilesManager")
if not gm: if not stm:
return false return false
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int() if stm.has_method("activate_effect"):
if pid == null or pid < 0: stm.activate_effect(stm.SpecialEffect.INVISIBLE_MODE)
return false print("[BotController] %s activated Ghost powerup (trapped)" % actor.name)
if gm.has_method("rpc_activate_cleanser"):
gm.rpc_activate_cleanser(pid)
print("[BotController] %s requested Cleanser activation (trapped)" % actor.name)
return true return true
return false return false
@@ -292,14 +289,14 @@ func _on_step_onto_unsafe() -> bool:
var here = actor.current_position if "current_position" in actor else Vector2i(-1, -1) var here = actor.current_position if "current_position" in actor else Vector2i(-1, -1)
if here == Vector2i(-1, -1): if here == Vector2i(-1, -1):
return false return false
# Post-move guard: if we somehow landed on a sticky without cleanser active, # Post-move guard: if we somehow landed on a sticky without ghost active,
# burn Cleanser to clear ourselves out next tick. # burn Ghost powerup to phase through next tick.
if strategic_planner.is_gauntlet_mode() and strategic_planner._is_overlay_unsafe(here): if strategic_planner.is_gauntlet_mode() and strategic_planner._is_overlay_unsafe(here):
if not strategic_planner._is_bot_cleanser_active(): if not strategic_planner._is_bot_ghost_active():
var gm = strategic_planner._get_gauntlet_manager() var gm = strategic_planner._get_gauntlet_manager()
if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(here): if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(here):
print("[BotController] %s stepped onto sticky at %sburning Cleanser" % [actor.name, here]) print("[BotController] %s stepped onto sticky at %sactivating Ghost" % [actor.name, here])
return _try_activate_cleanser() return _try_activate_ghost()
return false return false
# ============================================================================= # =============================================================================
+16 -23
View File
@@ -17,7 +17,7 @@ const GOAL_TILES = [7, 8, 9, 10] # Heart, Diamond, Star, Coin
const HOLO_TILES = [11, 12, 13, 14] # Power-up holo tiles const HOLO_TILES = [11, 12, 13, 14] # Power-up holo tiles
# Gauntlet overlay layer (v2 ground-growth model — sticky/telegraph on layer 2). # Gauntlet overlay layer (v2 ground-growth model — sticky/telegraph on layer 2).
# Bots must avoid these cells or burn a Cleanser charge to cross. # Bots must avoid these cells or use Ghost mode to cross.
const GAUNTLET_OVERLAY_LAYER: int = 2 const GAUNTLET_OVERLAY_LAYER: int = 2
const TILE_STICKY: int = 17 const TILE_STICKY: int = 17
const TILE_TELEGRAPH: int = 18 const TILE_TELEGRAPH: int = 18
@@ -61,23 +61,16 @@ func _get_gauntlet_manager() -> Node:
return gm2 return gm2
return null return null
func _bot_has_cleanser_charge() -> bool: func _bot_has_ghost_powerup() -> bool:
var gm = _get_gauntlet_manager() """Check if the bot has a ghost powerup in its SpecialTilesManager inventory."""
if not gm or not "player_cleansers" in gm: var stm = actor.get_node_or_null("SpecialTilesManager")
if not stm:
return false return false
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int() return stm.inventory.get(stm.SpecialEffect.INVISIBLE_MODE, false)
if pid == null or pid < 0:
return false
return gm.player_cleansers.get(pid, 0) > 0
func _is_bot_cleanser_active() -> bool: func _is_bot_ghost_active() -> bool:
var gm = _get_gauntlet_manager() """Check if the bot is currently in ghost (invisible) mode."""
if not gm: return actor.get("is_invisible") == true
return false
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
if pid == null or pid < 0:
return false
return gm.is_cleanser_active(pid)
func _is_overlay_unsafe(pos: Vector2i) -> bool: func _is_overlay_unsafe(pos: Vector2i) -> bool:
"""True if the cell carries a sticky or telegraphed overlay on layer 2.""" """True if the cell carries a sticky or telegraphed overlay on layer 2."""
@@ -88,10 +81,10 @@ func _is_overlay_unsafe(pos: Vector2i) -> bool:
func _is_cell_unsafe_in_gauntlet(pos: Vector2i) -> bool: func _is_cell_unsafe_in_gauntlet(pos: Vector2i) -> bool:
"""Cell is unsafe in Gauntlet if it's sticky/telegraphed — unless the bot's """Cell is unsafe in Gauntlet if it's sticky/telegraphed — unless the bot's
Cleanser is active (grants temporary immunity).""" Ghost mode is active (grants sticky bypass)."""
if not is_gauntlet_mode(): if not is_gauntlet_mode():
return false return false
if _is_bot_cleanser_active(): if _is_bot_ghost_active():
return false return false
var gm = _get_gauntlet_manager() var gm = _get_gauntlet_manager()
if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(pos): if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(pos):
@@ -106,13 +99,13 @@ func _count_unsafe_neighbors(pos: Vector2i) -> int:
count += 1 count += 1
return count return count
func should_activate_cleanser_now() -> bool: func should_activate_ghost_now() -> bool:
"""True if the bot is boxed in / about to be sealed and should burn Cleanser.""" """True if the bot is boxed in / about to be sealed and should use Ghost powerup."""
if not is_gauntlet_mode(): if not is_gauntlet_mode():
return false return false
if not _bot_has_cleanser_charge(): if not _bot_has_ghost_powerup():
return false return false
if _is_bot_cleanser_active(): if _is_bot_ghost_active():
return false return false
var here = actor.current_position if actor and "current_position" in actor else Vector2i(-1, -1) var here = actor.current_position if actor and "current_position" in actor else Vector2i(-1, -1)
if here == Vector2i(-1, -1): if here == Vector2i(-1, -1):
@@ -623,7 +616,7 @@ func _is_valid_move_target(pos: Vector2i, ignore_players: bool = false) -> bool:
# Gauntlet mode (#075): reject cells that are sticky or telegraphed — # Gauntlet mode (#075): reject cells that are sticky or telegraphed —
# stepping onto them either traps the bot or strands it within 1s. # stepping onto them either traps the bot or strands it within 1s.
# Safety applies even when ignore_players is true (a sticky cell is unsafe # Safety applies even when ignore_players is true (a sticky cell is unsafe
# regardless of whether another player is on it). Cleanser-active bots are # regardless of whether another player is on it). Ghost-active bots are
# exempt via the helper. # exempt via the helper.
if _is_cell_unsafe_in_gauntlet(pos): if _is_cell_unsafe_in_gauntlet(pos):
return false return false
+31 -273
View File
@@ -7,7 +7,7 @@ class_name GauntletManager
signal phase_changed(phase_index: int, phase_name: String) signal phase_changed(phase_index: int, phase_name: String)
signal growth_tick(cells: Array) signal growth_tick(cells: Array)
signal player_trapped(player_id: int) signal player_trapped(player_id: int)
signal cleanser_granted(player_id: int) signal ghost_granted(player_id: int)
# ============================================================================= # =============================================================================
# Constants # Constants
@@ -31,10 +31,9 @@ enum CellState {
STICKY, # Covered in sticky candy, blocks + traps STICKY, # Covered in sticky candy, blocks + traps
BUBBLE_GROWING, # Candy bubble growing, not yet exploded BUBBLE_GROWING, # Candy bubble growing, not yet exploded
BLOCKED, # NPC zone or permanent obstacle BLOCKED, # NPC zone or permanent obstacle
CLEANSED, # Recently cleaned by Cleanser (temp protection)
} }
# Cells temporarily protected after a Cleanser pass (Vector2i -> time remaining). # Cells temporarily protected after Ghost-clearing (not used — kept for compat).
var cleansed_cells: Dictionary = {} var cleansed_cells: Dictionary = {}
const CLEANSED_PROTECTION_TIME: float = 5.0 const CLEANSED_PROTECTION_TIME: float = 5.0
@@ -146,15 +145,10 @@ const SMACK_COOLDOWN: float = 8.0
const SMACK_CHARGE_WINDOW: float = 3.0 const SMACK_CHARGE_WINDOW: float = 3.0
# ============================================================================= # =============================================================================
# Cleanser Tracking # Ghost Reward Tracking (replaces Cleanser)
# ============================================================================= # =============================================================================
var player_mission_completions: Dictionary = {} # player_id → int var player_mission_completions: Dictionary = {} # player_id → int
var player_cleansers: Dictionary = {} # player_id → int (0 or 1)
var cleanser_active: Dictionary = {} # player_id → true when immunity active
var cleanser_cells_left: Dictionary = {} # player_id → int (cells remaining)
const CLEANSER_MAX_CELLS: int = 5
const CLEANSER_ACTIVATION_DELAY: float = 0.3
# ============================================================================= # =============================================================================
# Trapped Players # Trapped Players
@@ -189,9 +183,6 @@ var pump_instance: Node3D = null
# HUD # HUD
var hud_layer: CanvasLayer var hud_layer: CanvasLayer
var phase_label: Label var phase_label: Label
var cleanser_label: Label
var cleanser_icon: TextureRect
var cleanser_count: int = 0
var slowmo_label: Label var slowmo_label: Label
var _gauntlet_hud_scene: PackedScene = preload("res://scenes/gauntlet_hud.tscn") var _gauntlet_hud_scene: PackedScene = preload("res://scenes/gauntlet_hud.tscn")
@@ -284,10 +275,6 @@ func _process(delta: float) -> void:
elif not multiplayer.is_server(): elif not multiplayer.is_server():
player.sync_modulate(Color.WHITE) player.sync_modulate(Color.WHITE)
# Cleanser input (local player only)
if Input.is_action_just_pressed("use_cleanser"):
_try_use_cleanser()
# Slow-mo timer (all peers for visual consistency) # Slow-mo timer (all peers for visual consistency)
if slowmo_active: if slowmo_active:
slowmo_timer -= delta slowmo_timer -= delta
@@ -1016,56 +1003,6 @@ func _spawn_impact_particles(targets: Array) -> void:
if particles and is_instance_valid(particles): if particles and is_instance_valid(particles):
particles.queue_free() particles.queue_free()
# =============================================================================
func _spawn_cleanser_particles(pos: Vector2i) -> void:
"""Spawn bright cleansing particles when sticky is cleared."""
if not main_scene or not gridmap:
return
var world_pos = Vector3(
pos.x * gridmap.cell_size.x + gridmap.cell_size.x / 2.0,
0.5,
pos.y * gridmap.cell_size.z + gridmap.cell_size.z / 2.0
)
var particles = GPUParticles3D.new()
particles.emitting = true
particles.one_shot = true
particles.amount = 12
particles.lifetime = 0.6
particles.explosiveness = 0.9
var material = ParticleProcessMaterial.new()
material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
material.emission_sphere_radius = 0.3
material.direction = Vector3(0, 1, 0)
material.spread = 180.0
material.initial_velocity_min = 3.0
material.initial_velocity_max = 5.0
material.gravity = Vector3(0, -5.0, 0)
material.scale_min = 0.05
material.scale_max = 0.15
var mesh = SphereMesh.new()
mesh.radius = 0.2
mesh.height = 0.4
var spatial_mat = StandardMaterial3D.new()
spatial_mat.albedo_color = Color(0.2, 1.0, 1.0) # Cyan/Blue for cleanser
spatial_mat.emission_enabled = true
spatial_mat.emission = Color(0.2, 1.0, 1.0)
spatial_mat.emission_energy_multiplier = 3.0
mesh.material = spatial_mat
particles.draw_pass_1 = mesh
particles.process_material = material
particles.position = world_pos
main_scene.add_child(particles)
await get_tree().create_timer(1.2).timeout
if particles and is_instance_valid(particles):
particles.queue_free()
# ============================================================================= # =============================================================================
# Sticky / Trap System # Sticky / Trap System
@@ -1085,7 +1022,7 @@ func cell_state(pos: Vector2i) -> CellState:
if is_sticky_cell(pos): if is_sticky_cell(pos):
return CellState.STICKY return CellState.STICKY
if cleansed_cells.has(pos): if cleansed_cells.has(pos):
return CellState.CLEANSED return CellState.BLOCKED # Protected from regrowth temporarily
if telegraphed_cells.has(pos): if telegraphed_cells.has(pos):
return CellState.TELEGRAPHED return CellState.TELEGRAPHED
if bubble_cells.has(pos): if bubble_cells.has(pos):
@@ -1383,11 +1320,11 @@ func _calculate_bubble_score(pos: Vector2i, player_cells: Array = []) -> float:
return score return score
func _bubble_score_camping(pos: Vector2i) -> float: func _bubble_score_camping(pos: Vector2i) -> float:
"""Reward targeting campers. +40 >5s, +60 >8s, +80 >10s-with-cleanser.""" """Reward targeting campers. +40 >5s, +60 >8s, +80 >10s-with-ghost."""
var t := _camp_time_for_region(_region_of(pos)) var t := _camp_time_for_region(_region_of(pos))
if t > 10.0: if t > 10.0:
# Stronger only if a nearby player actually holds a cleanser. # Stronger only if a nearby player is in ghost mode.
if _any_cleanser_holder_near(pos): if _any_ghost_player_near(pos):
return 80.0 return 80.0
return 60.0 return 60.0
elif t > 8.0: elif t > 8.0:
@@ -1455,13 +1392,10 @@ func _bubble_blast_cells(center: Vector2i) -> Array:
func _bubble_footprint(center: Vector2i) -> Array: func _bubble_footprint(center: Vector2i) -> Array:
return _bubble_blast_cells(center) return _bubble_blast_cells(center)
func _any_cleanser_holder_near(pos: Vector2i) -> bool: func _any_ghost_player_near(pos: Vector2i) -> bool:
"""True if a player holding a Cleanser charge is within the camping region.""" """True if a player in ghost mode is within the camping region."""
for player in get_tree().get_nodes_in_group("Players"): for player in get_tree().get_nodes_in_group("Players"):
var pid = player.get("peer_id") if "peer_id" in player else -1 if not player.get("is_invisible"):
if pid == -1:
continue
if player_cleansers.get(pid, 0) <= 0:
continue continue
if "current_position" in player and player.current_position != null: if "current_position" in player and player.current_position != null:
if _region_of(player.current_position) == _region_of(pos): if _region_of(player.current_position) == _region_of(pos):
@@ -1557,7 +1491,7 @@ func _explode_bubble(center: Vector2i, cells: Array) -> void:
if "current_position" in player and player.current_position != null: if "current_position" in player and player.current_position != null:
if blast.has(player.current_position): if blast.has(player.current_position):
var pid = player.get("peer_id") if "peer_id" in player else -1 var pid = player.get("peer_id") if "peer_id" in player else -1
if pid != -1 and is_cleanser_active(pid): if pid != -1 and player.get("is_invisible"):
continue continue
apply_sticky_slow(player) apply_sticky_slow(player)
@@ -1664,8 +1598,8 @@ func _check_all_players_trapped() -> void:
var pos = player.current_position if player.get("current_position") else Vector2i(-1, -1) var pos = player.current_position if player.get("current_position") else Vector2i(-1, -1)
if is_sticky_cell(pos): if is_sticky_cell(pos):
var pid = player.get("peer_id") if "peer_id" in player else -1 var pid = player.get("peer_id") if "peer_id" in player else -1
if pid != -1 and is_cleanser_active(pid): if pid != -1 and player.get("is_invisible"):
continue # cleansing players are immune to the slow continue # ghost players are immune to the slow
apply_sticky_slow(player) apply_sticky_slow(player)
func apply_sticky_slow(player: Node) -> void: func apply_sticky_slow(player: Node) -> void:
@@ -1697,25 +1631,22 @@ func _trap_player(player: Node) -> void:
NotificationManager.send_message(player, "Stuck in Candy!", NotificationManager.MessageType.WARNING) NotificationManager.send_message(player, "Stuck in Candy!", NotificationManager.MessageType.WARNING)
func clear_sticky_cell(pos: Vector2i) -> void: func clear_sticky_cell(pos: Vector2i) -> void:
"""Used by Cleanser power-up to remove a sticky cell.""" """Remove a sticky cell (used when ghost player walks through)."""
if _can_rpc(): if _can_rpc():
if multiplayer.is_server(): if multiplayer.is_server():
rpc("sync_clear_sticky_cell", pos) rpc("sync_clear_sticky_cell", pos)
else: else:
sync_clear_sticky_cell(pos) # Predictive local clear sync_clear_sticky_cell(pos) # Predictive local clear
rpc("rpc_use_cleanser", pos)
else: else:
sync_clear_sticky_cell(pos) sync_clear_sticky_cell(pos)
@rpc("authority", "call_local", "reliable") @rpc("authority", "call_local", "reliable")
func sync_clear_sticky_cell(pos: Vector2i) -> void: func sync_clear_sticky_cell(pos: Vector2i) -> void:
sticky_cells.erase(pos) sticky_cells.erase(pos)
mark_cleansed(pos) # temporary regrowth protection (v2) mark_cleansed(pos) # temporary regrowth protection
if gridmap: if gridmap:
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1) gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1)
# Play VFX and SFX
_spawn_cleanser_particles(pos)
if SfxManager: if SfxManager:
SfxManager.play("pick_up_power_tile") SfxManager.play("pick_up_power_tile")
@@ -1723,142 +1654,6 @@ func sync_clear_sticky_cell(pos: Vector2i) -> void:
if main_scene and main_scene.has_method("sync_grid_item"): if main_scene and main_scene.has_method("sync_grid_item"):
main_scene.sync_grid_item(pos.x, 2, pos.y, -1) main_scene.sync_grid_item(pos.x, 2, pos.y, -1)
func _try_use_cleanser() -> void:
"""Local player attempts to activate Cleanser for 5-cell sticky immunity."""
var local_pid = multiplayer.get_unique_id()
var count = player_cleansers.get(local_pid, 0)
if count <= 0:
return
# Block activation during stun
var all_players = get_tree().get_nodes_in_group("Players")
var local_player = null
for p in all_players:
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
if pid == local_pid:
local_player = p
break
if not local_player:
return
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
return
# 0.3s activation delay
await get_tree().create_timer(CLEANSER_ACTIVATION_DELAY).timeout
# Re-validate after delay
if not is_instance_valid(local_player):
return
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
return
# Consume cleanser from inventory (only if client, host relies on rpc)
if not multiplayer.is_server():
cleanser_active[local_pid] = true
cleanser_cells_left[local_pid] = CLEANSER_MAX_CELLS
player_cleansers[local_pid] = max(0, player_cleansers[local_pid] - 1)
update_cleanser_ui(player_cleansers[local_pid])
NotificationManager.send_message(local_player, "Cleanser Used! (5 charges)", NotificationManager.MessageType.POWERUP)
# Sync to server/clients
if not multiplayer.is_server() and _can_rpc():
rpc("rpc_activate_cleanser", local_pid)
elif multiplayer.is_server():
# Call RPC logic directly for host (it will set active/cells_left/consume)
rpc_activate_cleanser(local_pid)
@rpc("any_peer", "call_local", "reliable")
func deactivate_cleanser(player_id: int) -> void:
"""Deactivate cleanser immunity for a player."""
cleanser_active.erase(player_id)
cleanser_cells_left.erase(player_id)
func is_cleanser_active(player_id: int) -> bool:
"""Check if a player has active cleanser immunity."""
return cleanser_active.has(player_id)
func use_cleanser_cell(player_id: int) -> bool:
"""Use one cleanser cell. Returns true if still active, false if exhausted."""
if not cleanser_active.has(player_id):
return false
cleanser_cells_left[player_id] -= 1
if cleanser_cells_left[player_id] <= 0:
if _can_rpc():
rpc("deactivate_cleanser", player_id)
else:
deactivate_cleanser(player_id)
return false
return true
func notify_movement_stopped(player_id: int, pos: Vector2i) -> void:
"""Called from PlayerMovementManager when a move chain settles.
Previously deactivated cleanser here, but now immunity persists
until charges run out to allow repeated use across safe gaps."""
pass
@rpc("any_peer", "call_local", "reliable")
func rpc_activate_cleanser(pid: int) -> void:
"""RPC for clients to activate cleanser on server."""
if multiplayer.is_server():
# Verify they actually have a cleanser charge (prevents spam/cheats)
if player_cleansers.get(pid, 0) <= 0:
return
# Always apply the state and AoE, since this is the server authority
cleanser_active[pid] = true
cleanser_cells_left[pid] = CLEANSER_MAX_CELLS
player_cleansers[pid] = max(0, player_cleansers[pid] - 1)
if _can_rpc():
rpc("sync_cleanser_count", pid, player_cleansers[pid])
# NEW: Clear 3x3 area around player
var all_players = get_tree().get_nodes_in_group("Players")
var target_player = null
for p in all_players:
var target_pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
if target_pid == pid:
target_player = p
break
if gridmap and is_instance_valid(target_player):
var map_pos = gridmap.local_to_map(target_player.global_position)
var center_pos = Vector2i(map_pos.x, map_pos.z)
# 3x3 neighborhood
for dx in range(-1, 2):
for dz in range(-1, 2):
var check_pos = center_pos + Vector2i(dx, dz)
if is_sticky_cell(check_pos):
clear_sticky_cell(check_pos)
# Remove slow effect for any player in the cleansed area
for p in all_players:
if is_instance_valid(p) and p.has_method("remove_slow_effect"):
if gridmap:
var p_map_pos = gridmap.local_to_map(p.global_position)
var p_cell_pos = Vector2i(p_map_pos.x, p_map_pos.z)
if abs(p_cell_pos.x - center_pos.x) <= 1 and abs(p_cell_pos.y - center_pos.y) <= 1:
if _can_rpc():
p.rpc("remove_slow_effect")
else:
p.remove_slow_effect()
print("[Cleanser] Server cleared 3x3 area around %s for player %d" % [center_pos, pid])
@rpc("any_peer", "call_local", "reliable")
func rpc_use_cleanser(pos: Vector2i) -> void:
"""RPC for clients to clear a sticky cell via Cleanser."""
if multiplayer.is_server():
clear_sticky_cell(pos)
@rpc("any_peer", "call_local", "reliable")
func rpc_consume_cleanser(pid: int) -> void:
"""RPC for clients to report Cleanser consumption to server."""
if multiplayer.is_server():
player_cleansers[pid] = 0
if _can_rpc():
rpc("sync_cleanser_count", pid, 0)
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func rpc_trigger_slowmo() -> void: func rpc_trigger_slowmo() -> void:
"""RPC for clients to request slow-mo from server.""" """RPC for clients to request slow-mo from server."""
@@ -1945,25 +1740,7 @@ func _setup_hud() -> void:
hud_layer.visible = false hud_layer.visible = false
add_child(hud_layer) add_child(hud_layer)
phase_label = hud_layer.get_node("BottomContainer/VBoxContainer/PhaseLabel") phase_label = hud_layer.get_node("BottomContainer/VBoxContainer/PhaseLabel")
cleanser_icon = hud_layer.get_node("BottomContainer/VBoxContainer/CleanserHBox/CleanserIcon")
cleanser_label = hud_layer.get_node("BottomContainer/VBoxContainer/CleanserHBox/CleanserLabel")
slowmo_label = hud_layer.get_node_or_null("TopContainer/SlowMoLabel") slowmo_label = hud_layer.get_node_or_null("TopContainer/SlowMoLabel")
_generate_cleanser_icon()
func _generate_cleanser_icon() -> void:
var icon_img = Image.create(16, 16, false, Image.FORMAT_RGBA8)
icon_img.fill(Color(0.4, 0.9, 1.0))
icon_img.blend_rect(icon_img, Rect2i(2, 2, 12, 12), Vector2i(1, 1))
for x in range(16):
icon_img.set_pixel(x, 0, Color(0.2, 0.6, 0.7))
icon_img.set_pixel(x, 15, Color(0.2, 0.6, 0.7))
for y in range(16):
icon_img.set_pixel(0, y, Color(0.2, 0.6, 0.7))
icon_img.set_pixel(15, y, Color(0.2, 0.6, 0.7))
for i in range(4, 12):
icon_img.set_pixel(i, 7, Color(1.0, 1.0, 1.0, 0.8))
icon_img.set_pixel(7, i, Color(1.0, 1.0, 1.0, 0.8))
cleanser_icon.texture = ImageTexture.create_from_image(icon_img)
func _update_hud_phase(phase_name: String) -> void: func _update_hud_phase(phase_name: String) -> void:
if phase_label: if phase_label:
@@ -1982,20 +1759,6 @@ func _update_hud_phase(phase_name: String) -> void:
# Animate phase label with bounce effect # Animate phase label with bounce effect
_animate_phase_label() _animate_phase_label()
func update_cleanser_ui(count: int) -> void:
cleanser_count = count
if cleanser_label:
cleanser_label.text = "[E] Cleanser (%d)" % count
# Show/hide icon based on availability
if cleanser_icon:
cleanser_icon.visible = count > 0
if count > 0:
# Pulse animation when cleanser is available
var tween = create_tween()
tween.set_loops(2)
tween.tween_property(cleanser_icon, "modulate", Color(1.5, 1.5, 1.5, 1), 0.3)
tween.tween_property(cleanser_icon, "modulate", Color.WHITE, 0.3)
func _animate_phase_label() -> void: func _animate_phase_label() -> void:
"""Animate phase label with bounce effect.""" """Animate phase label with bounce effect."""
if not phase_label: if not phase_label:
@@ -2020,7 +1783,7 @@ func _animate_phase_label() -> void:
# ============================================================================= # =============================================================================
func _on_goal_count_updated(peer_id: int, count: int) -> void: func _on_goal_count_updated(peer_id: int, count: int) -> void:
"""Called when a player completes a goal cycle. Grant cleanser every 2 missions.""" """Called when a player completes a goal cycle. Grant ghost powerup every 2 missions."""
if not multiplayer.is_server(): if not multiplayer.is_server():
return return
@@ -2029,34 +1792,29 @@ func _on_goal_count_updated(peer_id: int, count: int) -> void:
player_mission_completions[peer_id] = 0 player_mission_completions[peer_id] = 0
player_mission_completions[peer_id] += 1 player_mission_completions[peer_id] += 1
# Grant cleanser every 2 missions # Grant ghost powerup every 2 missions
var completions = player_mission_completions[peer_id] var completions = player_mission_completions[peer_id]
if completions % 2 == 0: if completions % 2 == 0:
if not player_cleansers.has(peer_id): _grant_ghost_powerup(peer_id)
player_cleansers[peer_id] = 0
# Allow stacking cleanser charges instead of capping at 1 func _grant_ghost_powerup(peer_id: int) -> void:
player_cleansers[peer_id] += 1 """Grant the ghost (invisible mode) powerup to a player."""
emit_signal("cleanser_granted", peer_id) var all_players = get_tree().get_nodes_in_group("Players")
print("[Gauntlet] Player %d granted Cleanser (Total: %d) (mission %d)" % [peer_id, player_cleansers[peer_id], completions]) for p in all_players:
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
# Sync cleanser count to HUD if pid == peer_id:
rpc("sync_cleanser_count", peer_id, player_cleansers.get(peer_id, 0)) var stm = p.get_node_or_null("SpecialTilesManager")
if stm and stm.has_method("add_powerup_from_item"):
stm.add_powerup_from_item(14) # 14 = Ghost / INVISIBLE_MODE
emit_signal("ghost_granted", peer_id)
print("[Gauntlet] Player %d granted Ghost powerup (mission %d)" % [peer_id, player_mission_completions[peer_id]])
NotificationManager.send_message(p, "Ghost Power Earned!", NotificationManager.MessageType.POWERUP)
break
func _on_score_updated(peer_id: int, new_score: int) -> void: func _on_score_updated(peer_id: int, new_score: int) -> void:
"""Called when a player's score is updated.""" """Called when a player's score is updated."""
pass # Score sync handled by GoalsCycleManager pass # Score sync handled by GoalsCycleManager
@rpc("authority", "call_local", "reliable")
func sync_cleanser_count(peer_id: int, count: int) -> void:
"""Sync cleanser count to HUD for specific player."""
# Update local player's cleanser UI
var local_pid = multiplayer.get_unique_id()
if peer_id == local_pid:
update_cleanser_ui(count)
# ============================================================================= # =============================================================================
# Utility # Utility
# ============================================================================= # =============================================================================
+8 -25
View File
@@ -155,15 +155,12 @@ func simple_move_to(grid_position: Vector2i) -> bool:
player.knock_tekton() player.knock_tekton()
return false # Don't move into the tile, just knock return false # Don't move into the tile, just knock
# If moving into a sticky cell: slow the player (unless cleanser active, # If moving into a sticky cell: block movement unless player is in ghost
# which clears the cell instead). Sticky no longer hard-traps. # mode (is_invisible), which lets them bypass sticky tiles in gauntlet.
if gm and gm.is_active and gm.is_sticky_cell(grid_position): if gm and gm.is_active and gm.is_sticky_cell(grid_position):
var pid = player.get("peer_id") if "peer_id" in player else -1 if player.get("is_invisible"):
if pid != -1 and gm.is_cleanser_active(pid): # Ghost mode: walk through sticky tile freely
# Cleanser immunity: clear sticky cell, use one cell, don't slow print("[Move] Ghost mode bypassed sticky cell at %s" % grid_position)
gm.clear_sticky_cell(grid_position)
gm.use_cleanser_cell(pid)
print("[Move] Cleanser cleared sticky cell at %s (%d cells left)" % [grid_position, gm.cleanser_cells_left.get(pid, 0)])
else: else:
print("[Move] Failed: Blocked by Gauntlet Sticky cell at %s" % grid_position) print("[Move] Failed: Blocked by Gauntlet Sticky cell at %s" % grid_position)
return false return false
@@ -341,12 +338,9 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
if main_sticky and main_sticky.get("gauntlet_manager"): if main_sticky and main_sticky.get("gauntlet_manager"):
var gm_sticky = main_sticky.gauntlet_manager var gm_sticky = main_sticky.gauntlet_manager
if gm_sticky.is_active and gm_sticky.is_sticky_cell(pushed_to_pos): if gm_sticky.is_active and gm_sticky.is_sticky_cell(pushed_to_pos):
var push_pid = other_player.get("peer_id") if "peer_id" in other_player else -1 if other_player.get("is_invisible"):
if push_pid != -1 and gm_sticky.is_cleanser_active(push_pid): # Ghost mode: pushed player bypasses sticky
# Cleanser immunity: clear sticky cell, use one cell print("[Move] Ghost mode bypassed push-into-sticky at %s" % pushed_to_pos)
gm_sticky.clear_sticky_cell(pushed_to_pos)
gm_sticky.use_cleanser_cell(push_pid)
print("[Move] Cleanser cleared push-into-sticky at %s" % pushed_to_pos)
else: else:
print("[Move] Player pushed into sticky cell at %s — slowed" % pushed_to_pos) print("[Move] Player pushed into sticky cell at %s — slowed" % pushed_to_pos)
if multiplayer.is_server() or other_player.is_multiplayer_authority(): if multiplayer.is_server() or other_player.is_multiplayer_authority():
@@ -406,17 +400,6 @@ func _on_movement_finished():
emit_signal("movement_finished") emit_signal("movement_finished")
else: else:
current_move_direction = Vector2i.ZERO current_move_direction = Vector2i.ZERO
# Gauntlet (#072): a Cleanser ends early once the player rests on a safe
# cell. Gated on gm.is_active so other game modes are never affected.
var gm = null
var main_node = player.get_tree().root.get_node_or_null("Main")
if main_node and main_node.get("gauntlet_manager"):
gm = main_node.gauntlet_manager
if gm and gm.is_active and player.get("current_position") != null:
var mpid = player.get("peer_id") if "peer_id" in player else -1
if mpid != -1 and gm.is_cleanser_active(mpid):
if multiplayer.is_server() or player.is_multiplayer_authority():
gm.notify_movement_stopped(mpid, player.current_position)
emit_signal("movement_finished") emit_signal("movement_finished")
func move_to_clicked_position(grid_position: Vector2i) -> bool: func move_to_clicked_position(grid_position: Vector2i) -> bool:
+4 -2
View File
@@ -547,9 +547,11 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c
# Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request) # Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request)
item_id = rng.randi_range(7, 10) item_id = rng.randi_range(7, 10)
elif LobbyManager.is_game_mode(GameMode.Mode.GAUNTLET): elif LobbyManager.is_game_mode(GameMode.Mode.GAUNTLET):
# Gauntlet mode: No power-up tile spawns from world. # Gauntlet mode: mostly common tiles, but ghost (14) can spawn too.
# Only common tiles (7-10) spawn; Smack/Cleanser are handled separately. if rng.randf() < 0.85:
item_id = rng.randi_range(7, 10) item_id = rng.randi_range(7, 10)
else:
item_id = 14 # Ghost powerup only
else: else:
# Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp # Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp
if rng.randf() < 0.8: if rng.randf() < 0.8: