attempt to make stop n go gamemode

This commit is contained in:
2026-02-19 03:42:59 +08:00
parent 4ce917db0f
commit a7a8106b7e
11 changed files with 516 additions and 28 deletions
+31
View File
@@ -41,9 +41,14 @@ signal scarcity_mode_changed(mode: String)
# Character and area selection
var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"]
var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"]
var available_game_modes: Array[String] = ["Freemode", "Stop n Go"]
var selected_area: String = "Desert" # Host-controlled
var game_mode: String = "Freemode" # Host-controlled
var local_character_index: int = 0 # Local player's character index
# Signals
signal game_mode_changed(mode: String)
# Ready to start game check
var _all_ready: bool = false
@@ -345,6 +350,29 @@ func sync_area(area_name: String) -> void:
selected_area = area_name
emit_signal("area_changed", area_name)
# =============================================================================
# Game Mode Selection (Host Only)
# =============================================================================
func set_game_mode(mode: String) -> void:
"""Host sets the game mode. Syncs to all clients."""
if not is_host:
push_warning("Only host can change game mode")
return
if mode not in available_game_modes:
push_error("Invalid game mode: " + mode)
return
game_mode = mode
rpc("sync_game_mode", mode)
@rpc("authority", "call_local", "reliable")
func sync_game_mode(mode: String) -> void:
"""Sync game mode selection from host to clients."""
game_mode = mode
emit_signal("game_mode_changed", mode)
func start_game() -> void:
"""Host triggers game start (transitions all players to main.tscn)."""
if not is_host:
@@ -361,6 +389,8 @@ func start_game() -> void:
rpc("sync_enable_cycle_timer", enable_cycle_timer)
# Sync scarcity mode
rpc("sync_scarcity_mode", scarcity_mode)
# Sync game mode
rpc("sync_game_mode", game_mode)
# Notify all clients to start
rpc("_on_game_starting")
@@ -484,3 +514,4 @@ func reset() -> void:
selected_area = "Desert"
local_character_index = 0 # Default to "Copper"
enable_cycle_timer = false
game_mode = "Freemode"
+15 -5
View File
@@ -54,6 +54,14 @@ func simple_move_to(grid_position: Vector2i) -> bool:
if player.get("is_frozen"):
return false
# Stop n Go Mode Violation Check
var main = player.get_tree().root.get_node_or_null("Main")
if main:
var manager = main.get_node_or_null("StopNGoManager")
if manager and manager.has_method("check_movement_violation"):
if manager.check_movement_violation(player.name.to_int(), player.current_position, grid_position):
return false
var distance: int
if use_diagonal_movement:
distance = max(abs(grid_position.x - player.current_position.x), abs(grid_position.y - player.current_position.y))
@@ -100,15 +108,17 @@ func simple_move_to(grid_position: Vector2i) -> bool:
rotate_towards_target(grid_position)
if player.is_multiplayer_authority() and player.has_method("sync_walk_animation"):
player.rpc("sync_walk_animation")
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
if player.has_method("sync_walk_animation"):
player.rpc("sync_walk_animation")
var path = [Vector2(player.current_position.x, player.current_position.y), Vector2(grid_position.x, grid_position.y)]
path.pop_front()
current_move_direction = grid_position - player.current_position
player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots")))
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots")))
return true
@@ -419,8 +429,8 @@ func _is_position_blocked_by_physics(target_pos: Vector2i) -> bool:
var space_state = player.get_world_3d().direct_space_state
var center_x = target_pos.x + 0.5
var center_z = target_pos.y + 0.5
var from = Vector3(center_x, 1.0, center_z)
var to = Vector3(center_x, 0.1, center_z)
var from = Vector3(center_x, 1.0, center_z)
var to = Vector3(center_x, 0.1, center_z)
var query = PhysicsRayQueryParameters3D.create(from, to)
query.collide_with_areas = false
+12
View File
@@ -90,6 +90,13 @@ func grab_item(grid_position: Vector2i) -> bool:
else:
# Normal Tile: Add to playerboard
player.playerboard[target_slot] = item
# Stop n Go Mission Progress
var sng_main = player.get_tree().root.get_node_or_null("Main")
if sng_main:
var sng_manager = sng_main.get_node_or_null("StopNGoManager")
if sng_manager and sng_manager.has_method("update_mission_progress"):
sng_manager.update_mission_progress(player.name.to_int(), item)
# Update UI immediately for responsiveness
@@ -174,6 +181,11 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int):
# Do not add to playerboard
else:
player.playerboard[target_slot] = item_id
# Stop n Go Mission Progress
var sng_manager = main.get_node_or_null("StopNGoManager")
if sng_manager and sng_manager.has_method("update_mission_progress"):
sng_manager.update_mission_progress(player.name.to_int(), item_id)
# 3c. Broadcast the new playerboard state to all clients
var peer_id = player.name.to_int()
+8 -6
View File
@@ -165,7 +165,7 @@ func add_powerup_from_item(item_id: int):
emit_signal("powerup_unlocked", effect, powerup_levels[effect])
print("Player %s leveled up %s to Lvl %d" % [player.name, SpecialEffect.keys()[effect], powerup_levels[effect]])
if player.is_multiplayer_authority():
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
rpc("sync_inventory_add", effect, powerup_levels[effect])
@rpc("any_peer", "call_local", "reliable")
@@ -254,7 +254,7 @@ func activate_effect(effect: int, target_player: Node3D = null):
_execute_invisible_mode(player)
# Play generic cast animation or sound?
if player.is_multiplayer_authority():
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
player.rpc("trigger_screen_shake", "light")
# Sync cooldown to others not strictly needed unless UI shows it?
# Probably local UI only.
@@ -308,7 +308,8 @@ func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO):
# If inside square radius
if dx <= radius and dy <= radius:
p.rpc("apply_slow_effect", FREEZE_SLOW_DURATION)
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
p.rpc("apply_slow_effect", FREEZE_SLOW_DURATION)
NotificationManager.send_message(p, "Caught in Freeze Zone!", NotificationManager.MessageType.WARNING)
if p != player: # Don't score for freezing self (unless desired?) - Assuming enemies
hit_count += 1
@@ -336,7 +337,8 @@ func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO):
var current_item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
if current_item != 4: # 4 is Wall Block
# Use Item 12 (Blue Freeze Tile) on Layer 0 (Floor)
main.rpc("sync_grid_item", pos.x, 0, pos.y, 12)
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
main.rpc("sync_grid_item", pos.x, 0, pos.y, 12)
# Cleanup visual timer (managed locally by author)
get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func():
@@ -404,7 +406,7 @@ func _execute_block_floor(target_pos: Vector2i):
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, 4)
# Record for restoration
@@ -462,7 +464,7 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true):
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id)
+265
View File
@@ -0,0 +1,265 @@
extends Node
class_name StopNGoManager
# StopNGoManager - Handles phase transitions, missions, and movement penalties
signal phase_changed(new_phase: String, remaining_time: float)
signal mission_status_updated(player_id: int, completed: bool)
signal player_penalized(player_id: int)
enum Phase {GO, STOP}
const GO_DURATION: float = 12.0
const STOP_DURATION: float = 6.0
var current_phase: Phase = Phase.GO
var phase_timer: float = GO_DURATION
var is_active: bool = false
var player_missions: Dictionary = {} # player_id -> {tile_id: count, current: count}
var finish_line_x: int = 21 # Right side of the map for win condition
# Tile IDs (Assumed based on analysis)
const TILE_WALKABLE = 0
const TILE_SAFE = 2 # Green Safe Zone
const TILE_OBSTACLE = 4 # Black Obstacle
var hud_layer: CanvasLayer
var phase_label: Label
var mission_label: Label
func _ready():
set_process(false)
_setup_hud()
func _setup_hud():
hud_layer = CanvasLayer.new()
hud_layer.visible = false
add_child(hud_layer)
var vbox = VBoxContainer.new()
vbox.set_anchors_preset(Control.PRESET_TOP_RIGHT)
vbox.offset_right = -20
vbox.offset_top = 100
hud_layer.add_child(vbox)
# Style for HUD
var style = StyleBoxFlat.new()
style.bg_color = Color(0, 0, 0, 0.4)
style.content_margin_left = 10
style.content_margin_top = 10
style.content_margin_right = 10
style.content_margin_bottom = 10
var panel = PanelContainer.new()
panel.add_theme_stylebox_override("panel", style)
vbox.add_child(panel)
var inner_vbox = VBoxContainer.new()
panel.add_child(inner_vbox)
phase_label = Label.new()
phase_label.text = "PHASE: GO"
phase_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
phase_label.add_theme_font_size_override("font_size", 32)
phase_label.add_theme_color_override("font_color", Color.GREEN)
inner_vbox.add_child(phase_label)
mission_label = Label.new()
mission_label.text = "MISSION: Collect 3 Hearts"
mission_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
inner_vbox.add_child(mission_label)
func _process(delta):
if not is_active:
return
if multiplayer.is_server():
phase_timer -= delta
if phase_timer <= 0:
if current_phase == Phase.GO:
_start_phase(Phase.STOP)
else:
_start_phase(Phase.GO)
# Update HUD locally
_update_hud_visuals()
func _update_hud_visuals():
var phase_name = "GO" if current_phase == Phase.GO else "STOP"
if phase_label:
phase_label.text = "PHASE: %s (%.0fs)" % [phase_name, max(0, phase_timer)]
phase_label.add_theme_color_override("font_color", Color.GREEN if current_phase == Phase.GO else Color.RED)
var my_id = multiplayer.get_unique_id()
if mission_label and player_missions.has(my_id):
var mission = player_missions[my_id]
mission_label.text = "Tiles: %d / %d" % [mission["current"], mission["required"]]
if mission["current"] >= mission["required"]:
mission_label.text = "MISSION COMPLETE! REACH FINISH!"
mission_label.add_theme_color_override("font_color", Color.GOLD)
func activate_client_side():
is_active = true
if hud_layer:
hud_layer.visible = true
set_process(true)
func start_game_mode():
activate_client_side()
if multiplayer.is_server():
_setup_arena()
_assign_missions()
_start_phase(Phase.GO)
func _start_phase(phase: Phase):
current_phase = phase
phase_timer = GO_DURATION if phase == Phase.GO else STOP_DURATION
var phase_name = "GO" if phase == Phase.GO else "STOP"
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
rpc("sync_phase", phase_name, phase_timer)
emit_signal("phase_changed", phase_name, phase_timer)
@rpc("authority", "call_local", "reliable")
func sync_phase(phase_name: String, duration: float):
current_phase = Phase.GO if phase_name == "GO" else Phase.STOP
phase_timer = duration
func _setup_arena():
var gridmap = get_node("/root/Main/EnhancedGridMap")
if not gridmap: return
print("[StopNGo] Setting up 22x10 Accurate Horizontal Arena...")
# Set Size for Stop n Go
gridmap.columns = 22
gridmap.rows = 10
# Clear existing items on all layers
gridmap.clear()
# Create bands based on X (Horizontal Progress)
for x in range(gridmap.columns):
var tile_id = TILE_WALKABLE
# Green Safe Zones at X=7,8 and X=14,15
if x == 7 or x == 8 or x == 14 or x == 15:
tile_id = TILE_SAFE
for z in range(gridmap.rows):
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
# Place Specific Obstacles (Black Bars) to match user images
# Lane 1 (X=2..6) - Vertical bar at X=4
for z in range(0, 4):
gridmap.set_cell_item(Vector3i(4, 0, z), TILE_OBSTACLE)
for z in range(6, 10):
gridmap.set_cell_item(Vector3i(4, 0, z), TILE_OBSTACLE)
# Lane 2 (X=9..13) - Vertical bar at X=11
for z in range(3, 7):
gridmap.set_cell_item(Vector3i(11, 0, z), TILE_OBSTACLE)
# Lane 3 (X=16..20) - L-shape at top
for z in range(0, 3):
gridmap.set_cell_item(Vector3i(18, 0, z), TILE_OBSTACLE)
gridmap.set_cell_item(Vector3i(19, 0, 2), TILE_OBSTACLE)
gridmap.set_cell_item(Vector3i(20, 0, 2), TILE_OBSTACLE)
# Another bar at bottom of Lane 3
for z in range(6, 9):
gridmap.set_cell_item(Vector3i(18, 0, z), TILE_OBSTACLE)
gridmap.update_grid_data()
gridmap.initialize_astar()
# Spawn tiles for mission (Heart = 7)
if multiplayer.is_server():
_spawn_mission_tiles()
# Sync the WHOLE grid to all clients to ensure size and stripes are correct
var main = get_node("/root/Main")
if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
# Gather all floor 0 and floor 1 data
var floor0_data = gridmap.get_floor_data(0)
var floor1_data = gridmap.get_floor_data(1)
main.rpc("sync_full_grid_data_stop_n_go", floor0_data, floor1_data, gridmap.columns, gridmap.rows)
func _spawn_mission_tiles():
var gridmap = get_node("/root/Main/EnhancedGridMap")
var count = 0
while count < 40: # Spawn plenty of hearts
var x = randi() % gridmap.columns
var z = randi() % gridmap.rows
# Only spawn in walkable areas, not safe zones or start/finish
if gridmap.get_cell_item(Vector3i(x, 0, z)) == TILE_WALKABLE:
if gridmap.get_cell_item(Vector3i(x, 1, z)) == -1:
gridmap.set_cell_item(Vector3i(x, 1, z), ScarcityModel.TILE_HEART)
count += 1
func _assign_missions():
# Each player needs to collect 3 specific tiles (e.g. Heart)
var players = GameStateManager.players
for p_id in players:
player_missions[p_id] = {
"target_tile": ScarcityModel.TILE_HEART,
"required": 3,
"current": 0
}
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
rpc("sync_missions", player_missions)
@rpc("authority", "call_local", "reliable")
func sync_missions(missions: Dictionary):
player_missions = missions
func check_movement_violation(player_id: int, from: Vector2i, to: Vector2i) -> bool:
"""Check if movement is illegal (during STOP phase and not in safe zone)."""
if current_phase == Phase.STOP:
var main = get_node("/root/Main")
var gridmap = main.get_node("EnhancedGridMap") if main else null
if gridmap:
var tile_from = gridmap.get_cell_item(Vector3i(from.x, 0, from.y))
if tile_from != TILE_SAFE:
_penalize_player(player_id)
return true
return false
func _penalize_player(player_id: int):
if not multiplayer.is_server(): return
var main = get_node("/root/Main")
if not main: return
var player_node = main.get_node_or_null(str(player_id))
if player_node:
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
player_node.rpc("on_stop_phase_violation")
emit_signal("player_penalized", player_id)
func update_mission_progress(player_id: int, tile_id: int):
if not player_missions.has(player_id): return
var mission = player_missions[player_id]
if tile_id == mission["target_tile"]:
mission["current"] = min(mission["current"] + 1, mission["required"])
if mission["current"] >= mission["required"]:
emit_signal("mission_status_updated", player_id, true)
if multiplayer.is_server() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
rpc("sync_mission_progress", player_id, mission["current"])
@rpc("any_peer", "call_local", "reliable")
func sync_mission_progress(player_id: int, current: int):
if player_missions.has(player_id):
player_missions[player_id]["current"] = current
func check_win_condition(player_id: int, position: Vector2i) -> bool:
if not player_missions.has(player_id): return false
var mission = player_missions[player_id]
if mission["current"] >= mission["required"]:
# Win when reaching X >= 21
if position.x >= finish_line_x:
return true
return false
@@ -0,0 +1 @@
uid://kiv6fpqym1c0
+8 -6
View File
@@ -51,15 +51,16 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool:
# 1. Authenticate
if email == "":
var device_id = OS.get_unique_id()
# For testing, append a random number to device ID to simulate unique users on one machine
device_id = device_id + str(randi())
# Use a more stable ID for testing instead of randi() every call
# If you need multiple clients on one machine, consider a command line arg or config
session = await client.authenticate_device_async(device_id)
else:
session = await client.authenticate_email_async(email, password)
if session.is_exception():
printerr("Auth Error: ", session.get_exception().message)
emit_signal("connection_failed", session.get_exception().message)
var err = session.get_exception()
printerr("[NakamaManager] Auth Error: %s (Code: %s)" % [err.message, err.status_code])
emit_signal("connection_failed", err.message)
return false
# 2. Connect Socket
@@ -67,8 +68,9 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool:
var socket_result = await socket.connect_async(session)
if socket_result.is_exception():
printerr("Socket Error: ", socket_result.get_exception().message)
emit_signal("connection_failed", socket_result.get_exception().message)
var err = socket_result.get_exception()
printerr("[NakamaManager] Socket Error: %s (Code: %s)" % [err.message, err.status_code])
emit_signal("connection_failed", err.message)
return false
# 3. Initialize Multiplayer Bridge