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 = ""
|
var disconnect_reason: String = ""
|
||||||
|
|
||||||
# Stop N Go settings
|
# Stop N Go settings
|
||||||
var sng_go_duration: int = 15
|
var sng_go_duration: int = 20
|
||||||
var sng_stop_duration: int = 4
|
var sng_stop_duration: int = 4
|
||||||
var sng_required_goals: int = 8
|
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]:
|
func get_skill_affected_area(effect: int, center_pos: Vector2i) -> Array[Vector2i]:
|
||||||
var area: Array[Vector2i] = []
|
var area: Array[Vector2i] = []
|
||||||
|
|
||||||
match effect:
|
if effect == SpecialEffect.AREA_FREEZE:
|
||||||
SpecialEffect.AREA_FREEZE:
|
var radius = 2 # 5x5 area for freeze
|
||||||
# Preview 2 blocks ahead of current hover (if we ever re-enable targeting)
|
for x in range(-radius, radius + 1):
|
||||||
area.append(center_pos)
|
for z in range(-radius, radius + 1):
|
||||||
|
area.append(center_pos + Vector2i(x, z))
|
||||||
SpecialEffect.BLOCK_FLOOR:
|
elif effect == SpecialEffect.BLOCK_FLOOR:
|
||||||
# Preview just the single block
|
# Wall logic: project full line based on player orientation
|
||||||
area.append(center_pos)
|
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
|
return area
|
||||||
|
|
||||||
@@ -97,6 +105,11 @@ var active_freeze_zones: Array = [] # Array of {center, radius, timer}
|
|||||||
var invisible_timer: float = 0.0
|
var invisible_timer: float = 0.0
|
||||||
var global_cooldown_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
|
# INVENTORY SYSTEM
|
||||||
# Stores count of each power-up type. Max 1 per type as per user request?
|
# Stores count of each power-up type. Max 1 per type as per user request?
|
||||||
# "player can store 1 of each different power up"
|
# "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:
|
SpecialEffect.FASTER_SPEED:
|
||||||
_execute_faster_speed()
|
_execute_faster_speed()
|
||||||
SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR:
|
SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR:
|
||||||
# Execute immediately based on direction instead of entering Targeting Mode
|
# TWO-CLICK LOGIC: First click starts Targeting Mode (Projection), second click executes.
|
||||||
if effect == SpecialEffect.BLOCK_FLOOR:
|
if is_targeting_mode and targeting_effect == effect:
|
||||||
_execute_block_floor()
|
# SECOND CLICK: Execute at current indicator position
|
||||||
|
_execute_targeted_effect_v2(effect, target_indicator_pos)
|
||||||
|
_exit_targeting_mode()
|
||||||
else:
|
else:
|
||||||
_execute_area_freeze()
|
# FIRST CLICK: Start targeting
|
||||||
|
_enter_targeting_mode(effect)
|
||||||
|
return # Don't apply cooldown or generic effects yet
|
||||||
SpecialEffect.INVISIBLE_MODE:
|
SpecialEffect.INVISIBLE_MODE:
|
||||||
_execute_invisible_mode(player)
|
_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])
|
print("[SpecialTiles] Consuming %s after successful activation." % SpecialEffect.keys()[effect])
|
||||||
remove_powerup(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
|
# Check if item is a holo tile
|
||||||
@@ -269,38 +341,22 @@ func _execute_faster_speed():
|
|||||||
SfxManager.rpc("play_rpc", "speed")
|
SfxManager.rpc("play_rpc", "speed")
|
||||||
NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP)
|
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
|
# VFX: show freeze initiator on all peers
|
||||||
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
||||||
player.rpc("play_skill_vfx", "skill_freeze")
|
player.rpc("play_skill_vfx", "skill_freeze")
|
||||||
elif player.has_method("play_skill_vfx"):
|
elif player.has_method("play_skill_vfx"):
|
||||||
player.play_skill_vfx("skill_freeze")
|
player.play_skill_vfx("skill_freeze")
|
||||||
|
|
||||||
|
# Determine Position: if passed Sentinel, fallback to projection ahead
|
||||||
var center_pos = target_pos
|
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)
|
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
|
||||||
|
var radius = 2 # Always 5x5 (radius 2)
|
||||||
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
|
|
||||||
|
|
||||||
print("Player %s executing Area Freeze at %s (Lvl %d, Rad %d)" % [player.name, center_pos, current_lvl, radius])
|
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:
|
else:
|
||||||
NotificationManager.send_message(player, "Hit %d Players!" % hit_count, NotificationManager.MessageType.GOAL)
|
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():
|
if player.is_multiplayer_authority():
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
var main_node = get_node_or_null("/root/Main")
|
||||||
if main and main.has_method("sync_grid_items_batch"):
|
if main_node and main_node.has_method("sync_grid_items_batch"):
|
||||||
var batch_data = []
|
var batch_data = []
|
||||||
for x in range(-radius, radius + 1):
|
for rx in range(-radius, radius + 1):
|
||||||
for y in range(-radius, radius + 1):
|
for rz in range(-radius, radius + 1):
|
||||||
var pos = center_pos + Vector2i(x, y)
|
var cell_x = center_pos.x + rx
|
||||||
if enhanced_gridmap.is_position_valid(pos):
|
var cell_z = center_pos.y + rz
|
||||||
# Use Item 5 (Freeze Floor) on Layer 2
|
# Changed to Y=1 (Floor 1) to match Wall visual logic
|
||||||
batch_data.append({"x": pos.x, "y": 2, "z": pos.y, "item": 5})
|
batch_data.append({"x": cell_x, "y": 1, "z": cell_z, "item": 5})
|
||||||
|
|
||||||
if not batch_data.is_empty():
|
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():
|
get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func():
|
||||||
var restore_batch = []
|
var cl_node = get_node_or_null("/root/Main")
|
||||||
for x in range(-radius, radius + 1):
|
if not cl_node: return
|
||||||
for y in range(-radius, radius + 1):
|
var clear_batch = []
|
||||||
var pos = center_pos + Vector2i(x, y)
|
for rx in range(-radius, radius + 1):
|
||||||
if enhanced_gridmap.is_position_valid(pos):
|
for rz in range(-radius, radius + 1):
|
||||||
# Check if it is STILL Freeze Overlay
|
var cx = center_pos.x + rx
|
||||||
var current_check = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 2, pos.y))
|
var cz = center_pos.y + rz
|
||||||
if current_check == 5:
|
# Check if it is STILL Freeze Overlay on Layer 1
|
||||||
restore_batch.append({"x": pos.x, "y": 2, "z": pos.y, "item": - 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 restore_batch.is_empty():
|
if not clear_batch.is_empty():
|
||||||
main.rpc("sync_grid_items_batch", restore_batch)
|
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
|
# VFX: show wall initiator on all peers
|
||||||
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
||||||
player.rpc("play_skill_vfx", "skill_wall")
|
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 behind_pos = target_pos
|
||||||
var last_dir = player.movement_manager.last_move_direction if player.movement_manager else Vector2i(0, 1)
|
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
|
behind_pos = player.current_position - last_dir
|
||||||
|
|
||||||
if not enhanced_gridmap.is_position_valid(behind_pos):
|
if not enhanced_gridmap.is_position_valid(behind_pos):
|
||||||
return
|
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 = []
|
var neighbors = []
|
||||||
if last_dir.x != 0:
|
if last_dir.x != 0:
|
||||||
# Moving on X-axis (Columns) -> Vertical Wall (Fixed X, all Z)
|
# 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)})
|
neighbors.append({"position": Vector2i(x, behind_pos.y)})
|
||||||
|
|
||||||
if player.is_multiplayer_authority():
|
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"):
|
if main and main.has_method("sync_grid_items_batch"):
|
||||||
var batch_data = []
|
var batch_data = []
|
||||||
for n in neighbors:
|
for n in neighbors:
|
||||||
@@ -597,6 +652,9 @@ func _process(delta):
|
|||||||
_update_invisible_timer(delta)
|
_update_invisible_timer(delta)
|
||||||
_update_freeze_zones(delta)
|
_update_freeze_zones(delta)
|
||||||
_check_for_icy_floor()
|
_check_for_icy_floor()
|
||||||
|
|
||||||
|
if is_targeting_mode:
|
||||||
|
_update_targeting_preview()
|
||||||
|
|
||||||
func _apply_slow_mo(target_player: Node3D):
|
func _apply_slow_mo(target_player: Node3D):
|
||||||
if target_player.has_method("apply_stagger") and target_player.is_frozen:
|
if target_player.has_method("apply_stagger") and target_player.is_frozen:
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const PERMANENT_POWERUP_LOCATIONS: Array[Vector2i] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
var current_phase: Phase = Phase.GO
|
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 is_active: bool = false
|
||||||
|
|
||||||
var player_missions: Dictionary = {} # player_id -> {target_tile: int, required: int, current: int}
|
var player_missions: Dictionary = {} # player_id -> {target_tile: int, required: int, current: int}
|
||||||
@@ -667,16 +667,28 @@ func _scatter_player_tiles(player_node: Node):
|
|||||||
|
|
||||||
var peer_id = player_node.name.to_int()
|
var peer_id = player_node.name.to_int()
|
||||||
var playerboard = player_node.playerboard
|
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()):
|
for i in range(playerboard.size()):
|
||||||
if playerboard[i] != -1:
|
if playerboard[i] != -1:
|
||||||
tiles_to_scatter.append(playerboard[i])
|
occupied_indices.append(i)
|
||||||
playerboard[i] = -1
|
|
||||||
|
|
||||||
if tiles_to_scatter.is_empty():
|
if occupied_indices.is_empty():
|
||||||
return # Nothing to scatter
|
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)
|
# Find valid nearby positions to drop tiles (within radius 3 of player)
|
||||||
var center = player_node.current_position
|
var center = player_node.current_position
|
||||||
@@ -698,7 +710,6 @@ func _scatter_player_tiles(player_node: Node):
|
|||||||
valid_drop_positions.append(pos)
|
valid_drop_positions.append(pos)
|
||||||
|
|
||||||
# Scatter tiles onto valid positions
|
# Scatter tiles onto valid positions
|
||||||
var rng = RandomNumberGenerator.new()
|
|
||||||
rng.randomize()
|
rng.randomize()
|
||||||
|
|
||||||
for tile in tiles_to_scatter:
|
for tile in tiles_to_scatter:
|
||||||
|
|||||||
Reference in New Issue
Block a user