feat: Introduce Stop N Go game mode with new managers for phases, missions, and special tiles.

This commit is contained in:
Yogi Wiguna
2026-03-25 15:21:13 +08:00
parent f482909df5
commit 0616d3a20a
3 changed files with 143 additions and 74 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ signal scarcity_mode_changed(mode: String)
var disconnect_reason: String = ""
# Stop N Go settings
var sng_go_duration: int = 15
var sng_go_duration: int = 20
var sng_stop_duration: int = 4
var sng_required_goals: int = 8
+122 -64
View File
@@ -35,14 +35,22 @@ signal powerup_unlocked(effect: int, level: int)
func get_skill_affected_area(effect: int, center_pos: Vector2i) -> Array[Vector2i]:
var area: Array[Vector2i] = []
match effect:
SpecialEffect.AREA_FREEZE:
# Preview 2 blocks ahead of current hover (if we ever re-enable targeting)
area.append(center_pos)
SpecialEffect.BLOCK_FLOOR:
# Preview just the single block
area.append(center_pos)
if effect == SpecialEffect.AREA_FREEZE:
var radius = 2 # 5x5 area for freeze
for x in range(-radius, radius + 1):
for z in range(-radius, radius + 1):
area.append(center_pos + Vector2i(x, z))
elif effect == SpecialEffect.BLOCK_FLOOR:
# Wall logic: project full line based on player orientation
var last_dir = player.movement_manager.last_move_direction if (player and player.movement_manager) else Vector2i(0, 1)
if last_dir.x != 0:
# Vertical Wall
for z in range(enhanced_gridmap.rows):
area.append(Vector2i(center_pos.x, z))
else:
# Horizontal Wall
for x in range(enhanced_gridmap.columns):
area.append(Vector2i(x, center_pos.y))
return area
@@ -97,6 +105,11 @@ var active_freeze_zones: Array = [] # Array of {center, radius, timer}
var invisible_timer: float = 0.0
var global_cooldown_timer: float = 0.0
# Targeting Mode
var is_targeting_mode: bool = false
var targeting_effect: int = -1
var target_indicator_pos: Vector2i = Vector2i.ZERO
# INVENTORY SYSTEM
# Stores count of each power-up type. Max 1 per type as per user request?
# "player can store 1 of each different power up"
@@ -225,11 +238,15 @@ func activate_effect(effect: int, target_player: Node3D = null):
SpecialEffect.FASTER_SPEED:
_execute_faster_speed()
SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR:
# Execute immediately based on direction instead of entering Targeting Mode
if effect == SpecialEffect.BLOCK_FLOOR:
_execute_block_floor()
# TWO-CLICK LOGIC: First click starts Targeting Mode (Projection), second click executes.
if is_targeting_mode and targeting_effect == effect:
# SECOND CLICK: Execute at current indicator position
_execute_targeted_effect_v2(effect, target_indicator_pos)
_exit_targeting_mode()
else:
_execute_area_freeze()
# FIRST CLICK: Start targeting
_enter_targeting_mode(effect)
return # Don't apply cooldown or generic effects yet
SpecialEffect.INVISIBLE_MODE:
_execute_invisible_mode(player)
@@ -245,6 +262,61 @@ func activate_effect(effect: int, target_player: Node3D = null):
print("[SpecialTiles] Consuming %s after successful activation." % SpecialEffect.keys()[effect])
remove_powerup(effect)
# =============================================================================
# Targeting Mode Helpers
# =============================================================================
func _enter_targeting_mode(effect: int):
is_targeting_mode = true
targeting_effect = effect
print("[SpecialTiles] Entered Targeting Mode for %s" % SpecialEffect.keys()[effect])
# Visual feedback if needed
func _exit_targeting_mode():
if is_targeting_mode:
is_targeting_mode = false
targeting_effect = -1
if player.action_manager:
player.action_manager.clear_highlights()
print("[SpecialTiles] Exited Targeting Mode.")
func _update_targeting_preview():
if not player.is_multiplayer_authority(): return
# Logic for projection:
# Freeze: 4 tiles ahead
# Wall: Always behind player
var distance = 4
if targeting_effect == SpecialEffect.BLOCK_FLOOR:
distance = -1 # 1 tile behind
var move_dir = Vector2i.ZERO
if player.movement_manager:
move_dir = player.movement_manager.current_move_direction
if move_dir == Vector2i.ZERO:
move_dir = player.movement_manager.last_move_direction
if move_dir == Vector2i.ZERO:
move_dir = Vector2i(0, 1) # Fallback
var center_pos = player.current_position + (move_dir * distance)
target_indicator_pos = center_pos
# Show highlights via ActionManager
var area = get_skill_affected_area(targeting_effect, center_pos)
# User Request: Use yellow/orange like hover (ID 1 in EnhancedGridMap)
var indicator_id = 1
if player.action_manager:
player.action_manager.highlight_cells_if_authorized(area, indicator_id)
func _execute_targeted_effect_v2(effect: int, target_pos: Vector2i):
match effect:
SpecialEffect.AREA_FREEZE:
_execute_area_freeze(target_pos)
SpecialEffect.BLOCK_FLOOR:
_execute_block_floor(target_pos)
# =============================================================================
# Check if item is a holo tile
@@ -269,38 +341,22 @@ func _execute_faster_speed():
SfxManager.rpc("play_rpc", "speed")
NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP)
func _execute_area_freeze(target_pos: Vector2i = Vector2i.ZERO):
func _execute_area_freeze(target_pos: Vector2i = Vector2i(-9999, -9999)):
# VFX: show freeze initiator on all peers
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
player.rpc("play_skill_vfx", "skill_freeze")
elif player.has_method("play_skill_vfx"):
player.play_skill_vfx("skill_freeze")
# Determine Position: if passed Sentinel, fallback to projection ahead
var center_pos = target_pos
if center_pos.x == -9999:
var last_dir = player.movement_manager.last_move_direction if (player and player.movement_manager) else Vector2i(0, 1)
center_pos = player.current_position + (last_dir * 4)
# Determine Level early for distance calculation
# Determine Level/Radius
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
if center_pos == Vector2i.ZERO:
# Updated: Always 4 tiles ahead as per user request
var distance = 4
print("[SpecialTiles] Area Freeze logic executing with distance: %d" % distance)
var movement = player.movement_manager
if movement and movement.current_move_direction != Vector2i.ZERO:
center_pos = player.current_position + movement.current_move_direction * distance
else:
# Fallback if standing still
var last_dir = player.movement_manager.last_move_direction if movement else Vector2i(0, 1)
center_pos = player.current_position + last_dir * distance
# Allow spawning Area Freeze even out of bounds (user request)
if not enhanced_gridmap.is_position_valid(center_pos):
print("[SpecialTiles] Spawning Area Freeze at out-of-bounds position: ", center_pos)
# 3. Determine Radius based on Level
var radius = 1
if current_lvl >= 5:
radius = 2 # Bigger area at high levels
var radius = 2 # Always 5x5 (radius 2)
print("Player %s executing Area Freeze at %s (Lvl %d, Rad %d)" % [player.name, center_pos, current_lvl, radius])
@@ -344,39 +400,39 @@ func _execute_area_freeze(target_pos: Vector2i = Vector2i.ZERO):
else:
NotificationManager.send_message(player, "Hit %d Players!" % hit_count, NotificationManager.MessageType.GOAL)
# Visual Feedback (Overlay Layer 2)
# Visual Feedback (Overlay Layer 1 - Ground Level Overlay)
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main and main.has_method("sync_grid_items_batch"):
var main_node = get_node_or_null("/root/Main")
if main_node and main_node.has_method("sync_grid_items_batch"):
var batch_data = []
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center_pos + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
# Use Item 5 (Freeze Floor) on Layer 2
batch_data.append({"x": pos.x, "y": 2, "z": pos.y, "item": 5})
for rx in range(-radius, radius + 1):
for rz in range(-radius, radius + 1):
var cell_x = center_pos.x + rx
var cell_z = center_pos.y + rz
# Changed to Y=1 (Floor 1) to match Wall visual logic
batch_data.append({"x": cell_x, "y": 1, "z": cell_z, "item": 5})
if not batch_data.is_empty():
main.rpc("sync_grid_items_batch", batch_data)
main_node.rpc("sync_grid_items_batch", batch_data)
# Cleanup visual timer (managed locally by author)
# Removal timer
get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func():
var restore_batch = []
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center_pos + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
# Check if it is STILL Freeze Overlay
var current_check = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 2, pos.y))
if current_check == 5:
restore_batch.append({"x": pos.x, "y": 2, "z": pos.y, "item": - 1})
if not restore_batch.is_empty():
main.rpc("sync_grid_items_batch", restore_batch)
var cl_node = get_node_or_null("/root/Main")
if not cl_node: return
var clear_batch = []
for rx in range(-radius, radius + 1):
for rz in range(-radius, radius + 1):
var cx = center_pos.x + rx
var cz = center_pos.y + rz
# Check if it is STILL Freeze Overlay on Layer 1
if enhanced_gridmap.get_cell_item(Vector3i(cx, 1, cz)) == 5:
clear_batch.append({"x": cx, "y": 1, "z": cz, "item": -1})
if not clear_batch.is_empty():
cl_node.rpc("sync_grid_items_batch", clear_batch)
)
func _execute_block_floor(target_pos: Vector2i = Vector2i.ZERO):
func _execute_block_floor(target_pos: Vector2i = Vector2i(-999, -999)):
# VFX: show wall initiator on all peers
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
player.rpc("play_skill_vfx", "skill_wall")
@@ -386,14 +442,13 @@ func _execute_block_floor(target_pos: Vector2i = Vector2i.ZERO):
var behind_pos = target_pos
var last_dir = player.movement_manager.last_move_direction if player.movement_manager else Vector2i(0, 1)
if behind_pos == Vector2i.ZERO:
if behind_pos == Vector2i(-999, -999):
behind_pos = player.current_position - last_dir
if not enhanced_gridmap.is_position_valid(behind_pos):
return
print("Player %s activated Wall Block behind at %s" % [player.name, behind_pos])
# Rollback: Should be vertical or horizontal based on direction
var neighbors = []
if last_dir.x != 0:
# Moving on X-axis (Columns) -> Vertical Wall (Fixed X, all Z)
@@ -405,7 +460,7 @@ func _execute_block_floor(target_pos: Vector2i = Vector2i.ZERO):
neighbors.append({"position": Vector2i(x, behind_pos.y)})
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
var main = get_node_or_null("/root/Main")
if main and main.has_method("sync_grid_items_batch"):
var batch_data = []
for n in neighbors:
@@ -598,6 +653,9 @@ func _process(delta):
_update_freeze_zones(delta)
_check_for_icy_floor()
if is_targeting_mode:
_update_targeting_preview()
func _apply_slow_mo(target_player: Node3D):
if target_player.has_method("apply_stagger") and target_player.is_frozen:
return # Already fully frozen/staggered
+18 -7
View File
@@ -29,7 +29,7 @@ const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [
]
var current_phase: Phase = Phase.GO
var phase_timer: float = 15.0 # Initialized dynamically later
var phase_timer: float = 20.0 # Initialized dynamically later
var is_active: bool = false
var player_missions: Dictionary = {} # player_id -> {target_tile: int, required: int, current: int}
@@ -667,17 +667,29 @@ func _scatter_player_tiles(player_node: Node):
var peer_id = player_node.name.to_int()
var playerboard = player_node.playerboard
var tiles_to_scatter: Array[int] = []
# Collect all non-empty tiles from playerboard
# Collect all non-empty tile indices from playerboard
var occupied_indices: Array[int] = []
for i in range(playerboard.size()):
if playerboard[i] != -1:
tiles_to_scatter.append(playerboard[i])
playerboard[i] = -1
occupied_indices.append(i)
if tiles_to_scatter.is_empty():
if occupied_indices.is_empty():
return # Nothing to scatter
# Select up to 3 random tiles to scatter
var rng = RandomNumberGenerator.new()
rng.randomize()
occupied_indices.shuffle()
var tiles_to_scatter: Array[int] = []
var scatter_count = min(occupied_indices.size(), 3)
for i in range(scatter_count):
var board_idx = occupied_indices[i]
tiles_to_scatter.append(playerboard[board_idx])
playerboard[board_idx] = -1 # Remove only the scattered tiles from board
# Find valid nearby positions to drop tiles (within radius 3 of player)
var center = player_node.current_position
var valid_drop_positions: Array[Vector2i] = []
@@ -698,7 +710,6 @@ func _scatter_player_tiles(player_node: Node):
valid_drop_positions.append(pos)
# Scatter tiles onto valid positions
var rng = RandomNumberGenerator.new()
rng.randomize()
for tile in tiles_to_scatter: