feat: Introduce Stop N Go game mode with new managers for phases, missions, and special tiles.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
)
|
||||
# Removal timer
|
||||
get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func():
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user