Gauntlet UI fixes and cleanser improvements
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
@rpc("any_peer", "call_local")
|
||||||
|
func remove_slow_effect():
|
||||||
|
slow_timer = 0.0
|
||||||
|
self.is_slowed = false
|
||||||
|
if movement_manager:
|
||||||
|
movement_manager.set_speed_multiplier(1.0)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func sync_clear_sticky_cell(pos: Vector2i) -> void:
|
||||||
|
sticky_cells.erase(pos)
|
||||||
|
mark_cleansed(pos)
|
||||||
|
if gridmap:
|
||||||
|
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), -1)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="1_font"]
|
||||||
|
|
||||||
|
[node name="GauntletHUD" type="CanvasLayer"]
|
||||||
|
layer = 5
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="TopContainer" type="CenterContainer" parent="."]
|
||||||
|
anchors_preset = 5
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_top = 70.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
|
||||||
|
[node name="SlowMoLabel" type="Label" parent="TopContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
theme_override_colors/font_color = Color(0.3, 0.5, 1.0, 1)
|
||||||
|
theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
|
||||||
|
theme_override_constants/outline_size = 4
|
||||||
|
theme_override_fonts/font = ExtResource("1_font")
|
||||||
|
text = "SLOW-MO"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="BottomContainer" type="CenterContainer" parent="."]
|
||||||
|
anchors_preset = 7
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = -120.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="BottomContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 4
|
||||||
|
|
||||||
|
[node name="PhaseLabel" type="Label" parent="BottomContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
theme_override_colors/font_color = Color(1, 0.6, 0.8, 1)
|
||||||
|
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 = "🍬 OPEN ARENA"
|
||||||
|
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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
func _find_valid_drop_position() -> Vector2i:
|
||||||
|
# Try random adjacent cells
|
||||||
|
var neighbors = enhanced_gridmap.get_neighbors(current_position, 0)
|
||||||
|
neighbors.shuffle()
|
||||||
|
|
||||||
|
for neighbor in neighbors:
|
||||||
|
var pos = neighbor.position
|
||||||
|
# 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):
|
||||||
|
# Gauntlet Mode explicit overrides
|
||||||
|
var gm = null
|
||||||
|
var main_gauntlet = get_tree().root.get_node_or_null("Main")
|
||||||
|
if main_gauntlet and main_gauntlet.get("gauntlet_manager"):
|
||||||
|
gm = main_gauntlet.gauntlet_manager
|
||||||
|
if gm and gm.is_active:
|
||||||
|
if pos.x == 0 or pos.x == gm.ARENA_COLUMNS - 1 or pos.y == 0 or pos.y == gm.ARENA_ROWS - 1:
|
||||||
|
continue
|
||||||
|
if gm._is_npc_zone(pos):
|
||||||
|
continue
|
||||||
|
return pos
|
||||||
|
|
||||||
|
return Vector2i(-1, -1)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@rpc("any_peer", "call_local")
|
||||||
|
func remove_slow_effect():
|
||||||
|
slow_timer = 0.0
|
||||||
|
self.is_slowed = false
|
||||||
|
if movement_manager:
|
||||||
|
# INSTANT response: restore speed multiplier to 1.0 immediately
|
||||||
|
movement_manager.set_speed_multiplier(1.0)
|
||||||
|
print("Player %s slow effect removed early" % name)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/func _find_valid_drop_position/,/return Vector2i(-1, -1)/c\
|
||||||
|
func _find_valid_drop_position() -> Vector2i:\
|
||||||
|
# Try random adjacent cells\
|
||||||
|
var neighbors = enhanced_gridmap.get_neighbors(current_position, 0)\
|
||||||
|
neighbors.shuffle()\
|
||||||
|
\
|
||||||
|
for neighbor in neighbors:\
|
||||||
|
var pos = neighbor.position\
|
||||||
|
# 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):\
|
||||||
|
# Gauntlet Mode explicit overrides\
|
||||||
|
var gm = null\
|
||||||
|
var main_gauntlet = get_tree().root.get_node_or_null("Main")\
|
||||||
|
if main_gauntlet and main_gauntlet.get("gauntlet_manager"):\
|
||||||
|
gm = main_gauntlet.gauntlet_manager\
|
||||||
|
if gm and gm.is_active:\
|
||||||
|
if pos.x == 0 or pos.x == gm.ARENA_COLUMNS - 1 or pos.y == 0 or pos.y == gm.ARENA_ROWS - 1:\
|
||||||
|
continue\
|
||||||
|
if gm._is_npc_zone(pos):\
|
||||||
|
continue\
|
||||||
|
return pos\
|
||||||
|
\
|
||||||
|
return Vector2i(-1, -1)
|
||||||
@@ -1,13 +1,78 @@
|
|||||||
[gd_scene load_steps=3 format=3 uid="uid://ddy2r7xto80gq"]
|
[gd_scene load_steps=10 format=3 uid="uid://ddy2r7xto80gq"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/controllers/candy_cannon_controller.gd" id="1_canon"]
|
[ext_resource type="Script" path="res://scripts/controllers/candy_cannon_controller.gd" id="1_canon"]
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="BoxMesh_canon"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_base"]
|
||||||
size = Vector3(1.5, 3, 1.5)
|
albedo_color = Color(0.15, 0.15, 0.2, 1)
|
||||||
|
metallic = 0.8
|
||||||
|
roughness = 0.2
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="CylinderMesh_base"]
|
||||||
|
material = SubResource("StandardMaterial3D_base")
|
||||||
|
top_radius = 0.8
|
||||||
|
bottom_radius = 1.2
|
||||||
|
height = 0.5
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_tank"]
|
||||||
|
albedo_color = Color(1, 0.4, 0.8, 1)
|
||||||
|
metallic = 0.2
|
||||||
|
roughness = 0.1
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(1, 0.4, 0.8, 1)
|
||||||
|
emission_energy_multiplier = 2.0
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="SphereMesh_tank"]
|
||||||
|
material = SubResource("StandardMaterial3D_tank")
|
||||||
|
radius = 0.9
|
||||||
|
height = 1.8
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pipe"]
|
||||||
|
albedo_color = Color(0.7, 0.7, 0.8, 1)
|
||||||
|
metallic = 1.0
|
||||||
|
roughness = 0.2
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="CylinderMesh_pipe"]
|
||||||
|
material = SubResource("StandardMaterial3D_pipe")
|
||||||
|
top_radius = 0.25
|
||||||
|
bottom_radius = 0.4
|
||||||
|
height = 1.5
|
||||||
|
|
||||||
|
[sub_resource type="TorusMesh" id="TorusMesh_ring"]
|
||||||
|
material = SubResource("StandardMaterial3D_pipe")
|
||||||
|
inner_radius = 0.95
|
||||||
|
outer_radius = 1.15
|
||||||
|
rings = 32
|
||||||
|
tube_segments = 12
|
||||||
|
|
||||||
[node name="CandyCannon" type="Node3D"]
|
[node name="CandyCannon" type="Node3D"]
|
||||||
script = ExtResource("1_canon")
|
script = ExtResource("1_canon")
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
[node name="Base" type="MeshInstance3D" parent="."]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.25, 0)
|
||||||
mesh = SubResource("BoxMesh_canon")
|
mesh = SubResource("CylinderMesh_base")
|
||||||
|
|
||||||
|
[node name="Tank" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4, 0)
|
||||||
|
mesh = SubResource("SphereMesh_tank")
|
||||||
|
|
||||||
|
[node name="Pipe" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.7, 0)
|
||||||
|
mesh = SubResource("CylinderMesh_pipe")
|
||||||
|
|
||||||
|
[node name="Ring1" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4, 0)
|
||||||
|
mesh = SubResource("TorusMesh_ring")
|
||||||
|
|
||||||
|
[node name="Ring2" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 0, 0, 1, 0, 1.4, 0)
|
||||||
|
mesh = SubResource("TorusMesh_ring")
|
||||||
|
|
||||||
|
[node name="Ring3" type="MeshInstance3D" parent="."]
|
||||||
|
transform = Transform3D(0.707107, -0.707107, 0, 0.707107, 0.707107, 0, 0, 0, 1, 0, 1.4, 0)
|
||||||
|
mesh = SubResource("TorusMesh_ring")
|
||||||
|
|
||||||
|
[node name="OmniLight3D" type="OmniLight3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8, 0)
|
||||||
|
light_color = Color(1, 0.4, 0.8, 1)
|
||||||
|
light_energy = 3.0
|
||||||
|
omni_range = 8.0
|
||||||
|
|||||||
+43
-38
@@ -13,44 +13,6 @@ anchor_right = 0.5
|
|||||||
offset_top = 70.0
|
offset_top = 70.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
|
|
||||||
[node name="PhaseLabel" type="Label" parent="TopContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
theme_override_font_sizes/font_size = 24
|
|
||||||
theme_override_colors/font_color = Color(1, 0.6, 0.8, 1)
|
|
||||||
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 = "🍬 OPEN ARENA"
|
|
||||||
horizontal_alignment = 1
|
|
||||||
|
|
||||||
[node name="BottomContainer" type="CenterContainer" parent="."]
|
|
||||||
anchors_preset = 7
|
|
||||||
anchor_left = 0.5
|
|
||||||
anchor_top = 1.0
|
|
||||||
anchor_right = 0.5
|
|
||||||
anchor_bottom = 1.0
|
|
||||||
offset_top = -90.0
|
|
||||||
grow_horizontal = 2
|
|
||||||
grow_vertical = 0
|
|
||||||
|
|
||||||
[node name="CleanserHBox" type="HBoxContainer" parent="BottomContainer"]
|
|
||||||
layout_mode = 2
|
|
||||||
theme_override_constants/separation = 6
|
|
||||||
|
|
||||||
[node name="CleanserIcon" type="TextureRect" parent="BottomContainer/CleanserHBox"]
|
|
||||||
layout_mode = 2
|
|
||||||
custom_minimum_size = Vector2(20, 20)
|
|
||||||
stretch_mode = 5
|
|
||||||
|
|
||||||
[node name="CleanserLabel" type="Label" parent="BottomContainer/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 = "Cleanser: 0"
|
|
||||||
horizontal_alignment = 1
|
|
||||||
|
|
||||||
[node name="SlowMoLabel" type="Label" parent="TopContainer"]
|
[node name="SlowMoLabel" type="Label" parent="TopContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_font_sizes/font_size = 18
|
theme_override_font_sizes/font_size = 18
|
||||||
@@ -61,3 +23,46 @@ theme_override_fonts/font = ExtResource("1_font")
|
|||||||
text = "SLOW-MO"
|
text = "SLOW-MO"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
visible = false
|
visible = false
|
||||||
|
|
||||||
|
[node name="BottomContainer" type="CenterContainer" parent="."]
|
||||||
|
anchors_preset = 7
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = -120.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="BottomContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 4
|
||||||
|
|
||||||
|
[node name="PhaseLabel" type="Label" parent="BottomContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_font_sizes/font_size = 24
|
||||||
|
theme_override_colors/font_color = Color(1, 0.6, 0.8, 1)
|
||||||
|
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 = "🍬 OPEN ARENA"
|
||||||
|
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
|
||||||
|
|||||||
@@ -1054,6 +1054,16 @@ func apply_slow_effect(duration: float = 3.0):
|
|||||||
|
|
||||||
print("Player %s is slowed for %.1f seconds" % [name, duration])
|
print("Player %s is slowed for %.1f seconds" % [name, duration])
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local")
|
||||||
|
@rpc("any_peer", "call_local")
|
||||||
|
func remove_slow_effect():
|
||||||
|
slow_timer = 0.0
|
||||||
|
self.is_slowed = false
|
||||||
|
if movement_manager:
|
||||||
|
# INSTANT response: restore speed multiplier to 1.0 immediately
|
||||||
|
movement_manager.set_speed_multiplier(1.0)
|
||||||
|
print("Player %s slow effect removed early" % name)
|
||||||
|
|
||||||
func playerboard_is_empty() -> bool:
|
func playerboard_is_empty() -> bool:
|
||||||
for item in playerboard:
|
for item in playerboard:
|
||||||
if item != -1:
|
if item != -1:
|
||||||
@@ -1131,6 +1141,16 @@ func _find_valid_drop_position() -> Vector2i:
|
|||||||
var item_cell = Vector3i(pos.x, 1, pos.y)
|
var item_cell = Vector3i(pos.x, 1, pos.y)
|
||||||
if enhanced_gridmap.get_cell_item(item_cell) == -1:
|
if enhanced_gridmap.get_cell_item(item_cell) == -1:
|
||||||
if not is_position_occupied(pos):
|
if not is_position_occupied(pos):
|
||||||
|
# Gauntlet Mode explicit overrides
|
||||||
|
var gm = null
|
||||||
|
var main_gauntlet = get_tree().root.get_node_or_null("Main")
|
||||||
|
if main_gauntlet and main_gauntlet.get("gauntlet_manager"):
|
||||||
|
gm = main_gauntlet.gauntlet_manager
|
||||||
|
if gm and gm.is_active:
|
||||||
|
if pos.x == 0 or pos.x == gm.ARENA_COLUMNS - 1 or pos.y == 0 or pos.y == gm.ARENA_ROWS - 1:
|
||||||
|
continue
|
||||||
|
if gm._is_npc_zone(pos):
|
||||||
|
continue
|
||||||
return pos
|
return pos
|
||||||
|
|
||||||
return Vector2i(-1, -1)
|
return Vector2i(-1, -1)
|
||||||
|
|||||||
@@ -3,44 +3,97 @@ class_name CandyCannonController
|
|||||||
|
|
||||||
@export var is_static_turret: bool = true
|
@export var is_static_turret: bool = true
|
||||||
|
|
||||||
|
@onready var ring1 = $Ring1 if has_node("Ring1") else null
|
||||||
|
@onready var ring2 = $Ring2 if has_node("Ring2") else null
|
||||||
|
@onready var ring3 = $Ring3 if has_node("Ring3") else null
|
||||||
|
@onready var tank = $Tank if has_node("Tank") else null
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if ring1: ring1.rotate_y(delta * 1.5)
|
||||||
|
if ring2: ring2.rotate_x(delta * -1.0)
|
||||||
|
if ring3: ring3.rotate_z(delta * 1.2)
|
||||||
|
|
||||||
|
if tank and tank.mesh and tank.mesh.material:
|
||||||
|
var mat = tank.mesh.material as StandardMaterial3D
|
||||||
|
if mat:
|
||||||
|
# Gentle pulse of the candy tank
|
||||||
|
var pulse = (sin(Time.get_ticks_msec() / 300.0) + 1.0) * 0.5
|
||||||
|
mat.emission_energy_multiplier = 1.0 + (pulse * 2.0)
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func play_animation_rpc(anim_name: String) -> void:
|
func play_animation_rpc(anim_name: String) -> void:
|
||||||
# Stub for future model animations
|
# Stub for future model animations
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
func spawn_projectile(target_world_pos: Vector3, duration: float) -> void:
|
||||||
func spawn_projectile_rpc(target_world_pos: Vector3, duration: float) -> void:
|
|
||||||
var projectile = MeshInstance3D.new()
|
var projectile = MeshInstance3D.new()
|
||||||
var sphere = BoxMesh.new()
|
var sphere = SphereMesh.new()
|
||||||
sphere.size = Vector3(0.4, 0.4, 0.4)
|
sphere.radius = 0.3
|
||||||
|
sphere.height = 0.6
|
||||||
projectile.mesh = sphere
|
projectile.mesh = sphere
|
||||||
|
|
||||||
var mat = StandardMaterial3D.new()
|
var mat = StandardMaterial3D.new()
|
||||||
mat.albedo_color = Color(1.0, 0.4, 0.8) # Candy pink for Gauntlet
|
mat.albedo_color = Color(1.0, 0.4, 0.8) # Candy pink for Gauntlet
|
||||||
|
mat.emission_enabled = true
|
||||||
|
mat.emission = Color(1.0, 0.4, 0.8)
|
||||||
|
mat.emission_energy_multiplier = 3.0
|
||||||
projectile.material_override = mat
|
projectile.material_override = mat
|
||||||
|
|
||||||
get_tree().get_root().add_child(projectile)
|
get_tree().get_root().add_child(projectile)
|
||||||
|
|
||||||
# Start projectile slightly above the cannon center
|
# Start projectile slightly above the cannon center
|
||||||
projectile.global_position = global_position + Vector3(0, 2.0, 0)
|
projectile.global_position = global_position + Vector3(0, 3.0, 0)
|
||||||
|
|
||||||
var tween = create_tween()
|
var tween = create_tween()
|
||||||
if not tween:
|
if not tween:
|
||||||
projectile.queue_free()
|
projectile.queue_free()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# VFX trail
|
||||||
|
var particles = GPUParticles3D.new()
|
||||||
|
var pmat = ParticleProcessMaterial.new()
|
||||||
|
pmat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
||||||
|
pmat.emission_sphere_radius = 0.3
|
||||||
|
pmat.gravity = Vector3(0, 0, 0)
|
||||||
|
pmat.scale_min = 0.1
|
||||||
|
pmat.scale_max = 0.3
|
||||||
|
particles.process_material = pmat
|
||||||
|
|
||||||
|
var pmesh = SphereMesh.new()
|
||||||
|
pmesh.radius = 0.1
|
||||||
|
pmesh.height = 0.2
|
||||||
|
var spatial_mat = StandardMaterial3D.new()
|
||||||
|
spatial_mat.albedo_color = Color(1.0, 0.6, 0.9)
|
||||||
|
spatial_mat.emission_enabled = true
|
||||||
|
spatial_mat.emission = Color(1.0, 0.6, 0.9)
|
||||||
|
pmesh.material = spatial_mat
|
||||||
|
particles.draw_pass_1 = pmesh
|
||||||
|
|
||||||
|
particles.amount = 16
|
||||||
|
particles.lifetime = 0.4
|
||||||
|
projectile.add_child(particles)
|
||||||
|
|
||||||
tween.set_parallel(true)
|
tween.set_parallel(true)
|
||||||
tween.tween_property(projectile, "global_position:x", target_world_pos.x, duration).set_trans(Tween.TRANS_LINEAR)
|
tween.tween_property(projectile, "global_position:x", target_world_pos.x, duration).set_trans(Tween.TRANS_LINEAR)
|
||||||
tween.tween_property(projectile, "global_position:z", target_world_pos.z, duration).set_trans(Tween.TRANS_LINEAR)
|
tween.tween_property(projectile, "global_position:z", target_world_pos.z, duration).set_trans(Tween.TRANS_LINEAR)
|
||||||
|
|
||||||
var mid_y = max(global_position.y, target_world_pos.y) + 4.0
|
var mid_y = max(global_position.y, target_world_pos.y) + 4.0
|
||||||
var tween_y = create_tween()
|
var tween_y = create_tween()
|
||||||
tween_y.tween_property(projectile, "global_position:y", mid_y, duration / 2.0).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
tween_y.tween_property(projectile, "global_position:y", mid_y, duration / 2.0).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
|
||||||
tween_y.tween_property(projectile, "global_position:y", target_world_pos.y, duration / 2.0).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN).set_delay(duration / 2.0)
|
tween_y.tween_property(projectile, "global_position:y", target_world_pos.y, duration / 2.0).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN).set_delay(duration / 2.0)
|
||||||
|
|
||||||
tween.chain().tween_callback(projectile.queue_free)
|
# Add some spin to the projectile
|
||||||
|
var spin_tween = create_tween()
|
||||||
|
spin_tween.set_loops()
|
||||||
|
spin_tween.tween_property(projectile, "rotation", Vector3(PI*2, PI*2, PI*2), 0.5).as_relative()
|
||||||
|
|
||||||
|
tween.chain().tween_callback(func():
|
||||||
|
spin_tween.kill()
|
||||||
|
projectile.queue_free()
|
||||||
|
)
|
||||||
|
|
||||||
func can_rpc() -> bool:
|
func can_rpc() -> bool:
|
||||||
if not multiplayer.has_multiplayer_peer(): return false
|
if not multiplayer.has_multiplayer_peer(): return false
|
||||||
|
|||||||
@@ -413,13 +413,15 @@ func _apply_arena_setup() -> void:
|
|||||||
|
|
||||||
# Center 3x3 block: NPC obstacle (Candy Pump)
|
# Center 3x3 block: NPC obstacle (Candy Pump)
|
||||||
if _is_npc_zone(pos):
|
if _is_npc_zone(pos):
|
||||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_OBSTACLE)
|
# Make the floor walkable visually instead of obstacle red
|
||||||
|
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Boundary walls: perimeter (row 0, row 19, col 0, col 19)
|
# Boundary walls: perimeter (row 0, row 19, col 0, col 19)
|
||||||
if x == 0 or x == ARENA_COLUMNS - 1 or z == 0 or z == ARENA_ROWS - 1:
|
if x == 0 or x == ARENA_COLUMNS - 1 or z == 0 or z == ARENA_ROWS - 1:
|
||||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_OBSTACLE)
|
# Also make border walls visually walkable floors instead of red blocks
|
||||||
|
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -511,9 +513,13 @@ func _spawn_mission_tiles() -> void:
|
|||||||
if _is_npc_zone(pos):
|
if _is_npc_zone(pos):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check base floor — don't spawn on obstacles or void
|
# Check base floor — don't spawn on void (or walls if they were still obstacles)
|
||||||
var base_tile = gridmap.get_cell_item(Vector3i(x, 0, z))
|
var base_tile = gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||||
if base_tile == TILE_OBSTACLE or base_tile == -1:
|
if base_tile == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ensure we don't spawn powerups on the perimeter walls even though they look like floors
|
||||||
|
if x == 0 or x == ARENA_COLUMNS - 1 or z == 0 or z == ARENA_ROWS - 1:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip if something already exists on Layer 1
|
# Skip if something already exists on Layer 1
|
||||||
@@ -852,6 +858,15 @@ func sync_growth_telegraph(cells: Array) -> void:
|
|||||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_TELEGRAPH)
|
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_TELEGRAPH)
|
||||||
_spawn_telegraph_highlight(pos)
|
_spawn_telegraph_highlight(pos)
|
||||||
|
|
||||||
|
# NEW: Throw projectile from pump for normal growth
|
||||||
|
if pump_instance and pump_instance.has_method("spawn_projectile"):
|
||||||
|
var target_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
|
||||||
|
)
|
||||||
|
pump_instance.spawn_projectile(target_world_pos, telegraph_duration)
|
||||||
|
|
||||||
# Audio: warning pulse
|
# Audio: warning pulse
|
||||||
if SfxManager:
|
if SfxManager:
|
||||||
SfxManager.rpc("play_rpc", "generate_tile") if _can_rpc() else SfxManager.play("generate_tile")
|
SfxManager.rpc("play_rpc", "generate_tile") if _can_rpc() else SfxManager.play("generate_tile")
|
||||||
@@ -959,6 +974,17 @@ func _spawn_impact_particles(targets: Array) -> void:
|
|||||||
material.scale_min = 0.1
|
material.scale_min = 0.1
|
||||||
material.scale_max = 0.3
|
material.scale_max = 0.3
|
||||||
|
|
||||||
|
# Define visual mesh
|
||||||
|
var mesh = BoxMesh.new()
|
||||||
|
mesh.size = Vector3(0.2, 0.2, 0.2)
|
||||||
|
var spatial_mat = StandardMaterial3D.new()
|
||||||
|
spatial_mat.albedo_color = Color(1.0, 0.6, 0.8) # Candy pink
|
||||||
|
spatial_mat.emission_enabled = true
|
||||||
|
spatial_mat.emission = Color(1.0, 0.6, 0.8)
|
||||||
|
spatial_mat.emission_energy_multiplier = 2.0
|
||||||
|
mesh.material = spatial_mat
|
||||||
|
particles.draw_pass_1 = mesh
|
||||||
|
|
||||||
particles.process_material = material
|
particles.process_material = material
|
||||||
particles.position = world_pos
|
particles.position = world_pos
|
||||||
|
|
||||||
@@ -970,8 +996,57 @@ func _spawn_impact_particles(targets: Array) -> void:
|
|||||||
particles.queue_free()
|
particles.queue_free()
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Sticky / Trap System
|
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
|
||||||
|
|
||||||
func is_sticky_cell(pos: Vector2i) -> bool:
|
func is_sticky_cell(pos: Vector2i) -> bool:
|
||||||
return sticky_cells.has(pos)
|
return sticky_cells.has(pos)
|
||||||
@@ -1222,6 +1297,13 @@ func _generate_bubble_candidates() -> Array:
|
|||||||
var pos := Vector2i(x, z)
|
var pos := Vector2i(x, z)
|
||||||
if cell_state(pos) != CellState.SAFE:
|
if cell_state(pos) != CellState.SAFE:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# NEW: Ensure bubbles never pick boundary tiles or NPC area as center
|
||||||
|
if x == 0 or x == ARENA_COLUMNS - 1 or z == 0 or z == ARENA_ROWS - 1:
|
||||||
|
continue
|
||||||
|
if _is_npc_zone(pos):
|
||||||
|
continue
|
||||||
|
|
||||||
candidates.append({"pos": pos, "score": _calculate_bubble_score(pos, player_cells)})
|
candidates.append({"pos": pos, "score": _calculate_bubble_score(pos, player_cells)})
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
@@ -1431,6 +1513,15 @@ func sync_bubble_spawn(center: Vector2i, cells: Array) -> void:
|
|||||||
if SfxManager:
|
if SfxManager:
|
||||||
SfxManager.rpc("play_rpc", "generate_tile") if _can_rpc() else SfxManager.play("generate_tile")
|
SfxManager.rpc("play_rpc", "generate_tile") if _can_rpc() else SfxManager.play("generate_tile")
|
||||||
|
|
||||||
|
# NEW: VFX projectile from center pump if it exists
|
||||||
|
if pump_instance and pump_instance.has_method("spawn_projectile"):
|
||||||
|
var target_world_pos = Vector3(
|
||||||
|
center.x * gridmap.cell_size.x + gridmap.cell_size.x / 2.0,
|
||||||
|
0.5,
|
||||||
|
center.y * gridmap.cell_size.z + gridmap.cell_size.z / 2.0
|
||||||
|
)
|
||||||
|
pump_instance.spawn_projectile(target_world_pos, BUBBLE_GROW_DURATION)
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_bubble_explode(center: Vector2i, cells: Array) -> void:
|
func sync_bubble_explode(center: Vector2i, cells: Array) -> void:
|
||||||
"""Apply the 3x3 sticky overlay + explosion VFX on all clients."""
|
"""Apply the 3x3 sticky overlay + explosion VFX on all clients."""
|
||||||
@@ -1439,6 +1530,7 @@ func sync_bubble_explode(center: Vector2i, cells: Array) -> void:
|
|||||||
for c in cells:
|
for c in cells:
|
||||||
var pos = c as Vector2i
|
var pos = c as Vector2i
|
||||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_STICKY)
|
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_STICKY)
|
||||||
|
sticky_cells[pos] = true
|
||||||
# Medium shake — bubbles hit harder than a normal growth tick.
|
# Medium shake — bubbles hit harder than a normal growth tick.
|
||||||
if main_scene and main_scene.get("screen_shake_manager"):
|
if main_scene and main_scene.get("screen_shake_manager"):
|
||||||
main_scene.screen_shake_manager.shake(0.3, 0.6)
|
main_scene.screen_shake_manager.shake(0.3, 0.6)
|
||||||
@@ -1532,14 +1624,30 @@ func _trap_player(player: Node) -> void:
|
|||||||
|
|
||||||
func clear_sticky_cell(pos: Vector2i) -> void:
|
func clear_sticky_cell(pos: Vector2i) -> void:
|
||||||
"""Used by Cleanser power-up to remove a sticky cell."""
|
"""Used by Cleanser power-up to remove a sticky cell."""
|
||||||
|
if _can_rpc():
|
||||||
|
if multiplayer.is_server():
|
||||||
|
rpc("sync_clear_sticky_cell", pos)
|
||||||
|
else:
|
||||||
|
sync_clear_sticky_cell(pos) # Predictive local clear
|
||||||
|
rpc("rpc_use_cleanser", pos)
|
||||||
|
else:
|
||||||
|
sync_clear_sticky_cell(pos)
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
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 (v2)
|
||||||
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)
|
||||||
|
|
||||||
# Sync removal to clients
|
# Play VFX and SFX
|
||||||
if main_scene and _can_rpc():
|
_spawn_cleanser_particles(pos)
|
||||||
main_scene.rpc("sync_grid_item", pos.x, 2, pos.y, -1)
|
if SfxManager:
|
||||||
|
SfxManager.play("pick_up_power_tile")
|
||||||
|
|
||||||
|
# Sync removal to main scene's gridmap if needed
|
||||||
|
if main_scene and main_scene.has_method("sync_grid_item"):
|
||||||
|
main_scene.sync_grid_item(pos.x, 2, pos.y, -1)
|
||||||
|
|
||||||
func _try_use_cleanser() -> void:
|
func _try_use_cleanser() -> void:
|
||||||
"""Local player attempts to activate Cleanser for 5-cell sticky immunity."""
|
"""Local player attempts to activate Cleanser for 5-cell sticky immunity."""
|
||||||
@@ -1560,10 +1668,6 @@ func _try_use_cleanser() -> void:
|
|||||||
return
|
return
|
||||||
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
|
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
|
||||||
return
|
return
|
||||||
# Already active
|
|
||||||
if cleanser_active.has(local_pid):
|
|
||||||
return
|
|
||||||
|
|
||||||
# 0.3s activation delay
|
# 0.3s activation delay
|
||||||
await get_tree().create_timer(CLEANSER_ACTIVATION_DELAY).timeout
|
await get_tree().create_timer(CLEANSER_ACTIVATION_DELAY).timeout
|
||||||
|
|
||||||
@@ -1573,23 +1677,23 @@ func _try_use_cleanser() -> void:
|
|||||||
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
|
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Activate cleanser immunity
|
# Consume cleanser from inventory (only if client, host relies on rpc)
|
||||||
cleanser_active[local_pid] = true
|
if not multiplayer.is_server():
|
||||||
cleanser_cells_left[local_pid] = CLEANSER_MAX_CELLS
|
cleanser_active[local_pid] = true
|
||||||
|
cleanser_cells_left[local_pid] = CLEANSER_MAX_CELLS
|
||||||
# Consume cleanser from inventory
|
player_cleansers[local_pid] = max(0, player_cleansers[local_pid] - 1)
|
||||||
player_cleansers[local_pid] = 0
|
update_cleanser_ui(player_cleansers[local_pid])
|
||||||
update_cleanser_ui(0)
|
|
||||||
|
|
||||||
# Sync to server/clients
|
# Sync to server/clients
|
||||||
if not multiplayer.is_server() and _can_rpc():
|
if not multiplayer.is_server() and _can_rpc():
|
||||||
rpc("rpc_activate_cleanser", local_pid)
|
rpc("rpc_activate_cleanser", local_pid)
|
||||||
elif multiplayer.is_server():
|
elif multiplayer.is_server():
|
||||||
if _can_rpc():
|
# Call RPC logic directly for host (it will set active/cells_left/consume)
|
||||||
rpc("sync_cleanser_count", local_pid, 0)
|
rpc_activate_cleanser(local_pid)
|
||||||
|
|
||||||
NotificationManager.send_message(local_player, "Cleanser Active! (5 cells)", NotificationManager.MessageType.POWERUP)
|
NotificationManager.send_message(local_player, "Cleanser Active! (5 cells)", NotificationManager.MessageType.POWERUP)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func deactivate_cleanser(player_id: int) -> void:
|
func deactivate_cleanser(player_id: int) -> void:
|
||||||
"""Deactivate cleanser immunity for a player."""
|
"""Deactivate cleanser immunity for a player."""
|
||||||
cleanser_active.erase(player_id)
|
cleanser_active.erase(player_id)
|
||||||
@@ -1605,30 +1709,67 @@ func use_cleanser_cell(player_id: int) -> bool:
|
|||||||
return false
|
return false
|
||||||
cleanser_cells_left[player_id] -= 1
|
cleanser_cells_left[player_id] -= 1
|
||||||
if cleanser_cells_left[player_id] <= 0:
|
if cleanser_cells_left[player_id] <= 0:
|
||||||
deactivate_cleanser(player_id)
|
if _can_rpc():
|
||||||
|
rpc("deactivate_cleanser", player_id)
|
||||||
|
else:
|
||||||
|
deactivate_cleanser(player_id)
|
||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
|
|
||||||
func notify_movement_stopped(player_id: int, pos: Vector2i) -> void:
|
func notify_movement_stopped(player_id: int, pos: Vector2i) -> void:
|
||||||
"""Cleanser also ends when the player comes to rest on a safe (non-sticky)
|
"""Called from PlayerMovementManager when a move chain settles.
|
||||||
cell — they're clear of the candy, so immunity is no longer needed (#072).
|
Previously deactivated cleanser here, but now immunity persists
|
||||||
Called from PlayerMovementManager when a move chain settles. Gauntlet-only;
|
until charges run out to allow repeated use across safe gaps."""
|
||||||
a no-op when the player has no active cleanser."""
|
pass
|
||||||
if not cleanser_active.has(player_id):
|
|
||||||
return
|
|
||||||
if not is_sticky_cell(pos):
|
|
||||||
deactivate_cleanser(player_id)
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func rpc_activate_cleanser(pid: int) -> void:
|
func rpc_activate_cleanser(pid: int) -> void:
|
||||||
"""RPC for clients to activate cleanser on server."""
|
"""RPC for clients to activate cleanser on server."""
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
if not cleanser_active.has(pid):
|
# Verify they actually have a cleanser charge (prevents spam/cheats)
|
||||||
cleanser_active[pid] = true
|
if player_cleansers.get(pid, 0) <= 0:
|
||||||
cleanser_cells_left[pid] = CLEANSER_MAX_CELLS
|
return
|
||||||
player_cleansers[pid] = 0
|
|
||||||
if _can_rpc():
|
# Always apply the state and AoE, since this is the server authority
|
||||||
rpc("sync_cleanser_count", pid, 0)
|
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")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func rpc_use_cleanser(pos: Vector2i) -> void:
|
func rpc_use_cleanser(pos: Vector2i) -> void:
|
||||||
@@ -1729,9 +1870,9 @@ func _setup_hud() -> void:
|
|||||||
hud_layer = hud_instance
|
hud_layer = hud_instance
|
||||||
hud_layer.visible = false
|
hud_layer.visible = false
|
||||||
add_child(hud_layer)
|
add_child(hud_layer)
|
||||||
phase_label = hud_layer.get_node("TopContainer/PhaseLabel")
|
phase_label = hud_layer.get_node("BottomContainer/VBoxContainer/PhaseLabel")
|
||||||
cleanser_icon = hud_layer.get_node("BottomContainer/CleanserHBox/CleanserIcon")
|
cleanser_icon = hud_layer.get_node("BottomContainer/VBoxContainer/CleanserHBox/CleanserIcon")
|
||||||
cleanser_label = hud_layer.get_node("BottomContainer/CleanserHBox/CleanserLabel")
|
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()
|
_generate_cleanser_icon()
|
||||||
|
|
||||||
@@ -1770,7 +1911,7 @@ func _update_hud_phase(phase_name: String) -> void:
|
|||||||
func update_cleanser_ui(count: int) -> void:
|
func update_cleanser_ui(count: int) -> void:
|
||||||
cleanser_count = count
|
cleanser_count = count
|
||||||
if cleanser_label:
|
if cleanser_label:
|
||||||
cleanser_label.text = "Cleanser: %d" % count
|
cleanser_label.text = "[E] Cleanser (%d)" % count
|
||||||
# Show/hide icon based on availability
|
# Show/hide icon based on availability
|
||||||
if cleanser_icon:
|
if cleanser_icon:
|
||||||
cleanser_icon.visible = count > 0
|
cleanser_icon.visible = count > 0
|
||||||
@@ -1814,15 +1955,16 @@ 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 (max 1)
|
# Grant cleanser 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):
|
if not player_cleansers.has(peer_id):
|
||||||
player_cleansers[peer_id] = 0
|
player_cleansers[peer_id] = 0
|
||||||
if player_cleansers[peer_id] < 1:
|
|
||||||
player_cleansers[peer_id] = 1
|
# Allow stacking cleanser charges instead of capping at 1
|
||||||
emit_signal("cleanser_granted", peer_id)
|
player_cleansers[peer_id] += 1
|
||||||
print("[Gauntlet] Player %d granted Cleanser (mission %d)" % [peer_id, completions])
|
emit_signal("cleanser_granted", peer_id)
|
||||||
|
print("[Gauntlet] Player %d granted Cleanser (Total: %d) (mission %d)" % [peer_id, player_cleansers[peer_id], completions])
|
||||||
|
|
||||||
# Sync cleanser count to HUD
|
# Sync cleanser count to HUD
|
||||||
rpc("sync_cleanser_count", peer_id, player_cleansers.get(peer_id, 0))
|
rpc("sync_cleanser_count", peer_id, player_cleansers.get(peer_id, 0))
|
||||||
|
|||||||
@@ -111,11 +111,22 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
is_wall_passable = false
|
is_wall_passable = false
|
||||||
print("[MovementManager] Hard block at %s. Ghost pass denied." % grid_position)
|
print("[MovementManager] Hard block at %s. Ghost pass denied." % grid_position)
|
||||||
|
|
||||||
|
var gm = null
|
||||||
|
var main_gauntlet = player.get_tree().root.get_node_or_null("Main")
|
||||||
|
if main_gauntlet and main_gauntlet.get("gauntlet_manager"):
|
||||||
|
gm = main_gauntlet.gauntlet_manager
|
||||||
|
|
||||||
# Check Floor 0 (Basic Walkability/Void)
|
# Check Floor 0 (Basic Walkability/Void)
|
||||||
if (cell_floor == -1 or cell_floor in enhanced_gridmap.non_walkable_items) and not is_wall_passable:
|
if (cell_floor == -1 or cell_floor in enhanced_gridmap.non_walkable_items) and not is_wall_passable:
|
||||||
print("[Move] Failed: Floor Item %d is non-walkable" % cell_floor)
|
print("[Move] Failed: Floor Item %d is non-walkable" % cell_floor)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# Gauntlet Mode explicit wall overrides (since we visually removed the wall blocks)
|
||||||
|
if gm and gm.is_active:
|
||||||
|
if gm._is_npc_zone(grid_position):
|
||||||
|
print("[Move] Failed: Blocked by Gauntlet NPC center at %s" % grid_position)
|
||||||
|
return false
|
||||||
|
|
||||||
# Check Floor 1 (Obstacles/Walls)
|
# Check Floor 1 (Obstacles/Walls)
|
||||||
if (cell_item != -1 and cell_item in [4, 16]) and not is_wall_passable:
|
if (cell_item != -1 and cell_item in [4, 16]) and not is_wall_passable:
|
||||||
print("[Move] Failed: Blocked by Item %d on Floor 1" % cell_item)
|
print("[Move] Failed: Blocked by Item %d on Floor 1" % cell_item)
|
||||||
@@ -131,11 +142,6 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
if not try_push(grid_position, push_dir):
|
if not try_push(grid_position, push_dir):
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var gm = null
|
|
||||||
var main_gauntlet = player.get_tree().root.get_node_or_null("Main")
|
|
||||||
if main_gauntlet and main_gauntlet.get("gauntlet_manager"):
|
|
||||||
gm = main_gauntlet.gauntlet_manager
|
|
||||||
|
|
||||||
# Sticky no longer hard-traps — players are slowed instead and can move freely.
|
# Sticky no longer hard-traps — players are slowed instead and can move freely.
|
||||||
|
|
||||||
# Check for Tekton interaction (Charged Strike Mode)
|
# Check for Tekton interaction (Charged Strike Mode)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://byv3h6b2mfdui
|
||||||
@@ -49,10 +49,11 @@ func test_cleanser_granted_after_two_missions():
|
|||||||
assert_eq(manager.player_cleansers.get(7, 0), 1, "Cleanser granted after 2 missions")
|
assert_eq(manager.player_cleansers.get(7, 0), 1, "Cleanser granted after 2 missions")
|
||||||
|
|
||||||
func test_cleanser_inventory_capped_at_one():
|
func test_cleanser_inventory_capped_at_one():
|
||||||
# Four missions would be two grants, but inventory caps at 1 (not consumed).
|
# Four missions would be two grants. The new rule allows them to stack.
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
|
manager.player_mission_completions[7] = i + 1
|
||||||
manager._on_goal_count_updated(7, i + 1)
|
manager._on_goal_count_updated(7, i + 1)
|
||||||
assert_eq(manager.player_cleansers.get(7, 0), 1, "Inventory never exceeds 1")
|
assert_eq(manager.player_cleansers.get(7, 0), 2, "Cleansers now stack")
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Activation / immunity lifecycle
|
# Activation / immunity lifecycle
|
||||||
@@ -96,11 +97,12 @@ func test_clear_sticky_cell_removes_and_protects():
|
|||||||
# Safe-stop early termination (#072 acceptance: ends when stopping on safe cell)
|
# Safe-stop early termination (#072 acceptance: ends when stopping on safe cell)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func test_stop_on_safe_cell_ends_cleanser():
|
func test_stop_on_safe_cell_keeps_cleanser():
|
||||||
manager.cleanser_active[8] = true
|
manager.cleanser_active[8] = true
|
||||||
manager.cleanser_cells_left[8] = 3
|
manager.cleanser_cells_left[8] = 3
|
||||||
|
|
||||||
manager.notify_movement_stopped(8, Vector2i(5, 5)) # safe cell
|
manager.notify_movement_stopped(8, Vector2i(5, 5)) # safe cell
|
||||||
assert_false(manager.is_cleanser_active(8), "Cleanser ends on safe-cell stop")
|
assert_true(manager.is_cleanser_active(8), "Cleanser NO LONGER ends on safe-cell stop (persists charges)")
|
||||||
|
|
||||||
func test_stop_on_sticky_cell_keeps_cleanser():
|
func test_stop_on_sticky_cell_keeps_cleanser():
|
||||||
manager.sticky_cells[Vector2i(6, 6)] = true
|
manager.sticky_cells[Vector2i(6, 6)] = true
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dqvm86t7rr3t
|
||||||
Reference in New Issue
Block a user