From 7423e29443ce5eace2a048e514dec9eee62cc8d2 Mon Sep 17 00:00:00 2001 From: adtpdn Date: Thu, 1 Jan 2026 05:21:25 +0800 Subject: [PATCH] update holiday --- _daily_basis/nakama_local_setup.md | 54 +++++++++++++++++++ _daily_basis/report_2026-01-01.md | 41 ++++++++++++++ scenes/lobby.gd | 39 +++++++++++--- scenes/main.gd | 12 ++--- scenes/player.gd | 60 ++++++++++++++++++--- scripts/managers/lobby_manager.gd | 18 +++++-- scripts/managers/player_movement_manager.gd | 9 ++-- scripts/managers/playerboard_manager.gd | 12 ++--- scripts/managers/powerup_manager.gd | 6 +-- scripts/managers/ui_manager.gd | 6 ++- scripts/nakama_manager.gd | 2 +- server/docker-compose.yaml | 51 ++++++++++++++++++ 12 files changed, 273 insertions(+), 37 deletions(-) create mode 100644 _daily_basis/nakama_local_setup.md create mode 100644 _daily_basis/report_2026-01-01.md create mode 100644 server/docker-compose.yaml diff --git a/_daily_basis/nakama_local_setup.md b/_daily_basis/nakama_local_setup.md new file mode 100644 index 0000000..a981b5c --- /dev/null +++ b/_daily_basis/nakama_local_setup.md @@ -0,0 +1,54 @@ +# Nakama Local Development Setup Checklist + +## Prerequisites +- [ ] Docker Desktop installed (Windows/Mac) +- [ ] Docker Desktop running +- [ ] SSH client available (for localhost.run tunneling) + +## Setup Steps + +### 1. Create docker-compose.yaml +- [ ] Create `server/docker-compose.yaml` in project root +- [ ] Configure Nakama service (port 7350) +- [ ] Configure PostgreSQL service (port 5432) +- [ ] Set up persistent volumes for PostgreSQL data + +### 2. Start Nakama Locally +- [ ] Open terminal in `server/` directory +- [ ] Run `docker-compose up -d` +- [ ] Verify Nakama console at http://localhost:7351 +- [ ] Test API endpoint at http://localhost:7350 + +### 3. Tunnel for Remote Testing +- [ ] Run: `ssh -R 80:localhost:7350 nokey@localhost.run` +- [ ] Copy the generated public URL (e.g., `https://xxxxx.lhr.life`) +- [ ] Update `nakama_manager.gd` with tunnel URL for testing + +### 4. Configure Godot Client +- [ ] Update `NAKAMA_HOST` in `nakama_manager.gd` +- [ ] For local: `localhost`, port `7350`, scheme `http` +- [ ] For tunnel: use tunnel hostname, port `443`, scheme `https` + +### 5. PostgreSQL Learning Notes +- [ ] Database: `nakama` (created by Nakama on startup) +- [ ] Access via: `docker exec -it nakama-postgres psql -U postgres -d nakama` +- [ ] Key tables: `users`, `user_edge`, `storage`, `leaderboard`, `wallet_ledger` + +## Quick Commands + +```bash +# Start services +docker-compose up -d + +# Stop services +docker-compose down + +# View logs +docker-compose logs -f nakama + +# Access PostgreSQL +docker exec -it nakama-postgres psql -U postgres -d nakama + +# Tunnel port 7350 +ssh -R 80:localhost:7350 nokey@localhost.run +``` diff --git a/_daily_basis/report_2026-01-01.md b/_daily_basis/report_2026-01-01.md new file mode 100644 index 0000000..e4f04bb --- /dev/null +++ b/_daily_basis/report_2026-01-01.md @@ -0,0 +1,41 @@ +[ ADT's Report ] + +Updated the `tekton-enet` ( Armageddon Multiplayer ) on branch `launcher` + +**Network Animation Synchronization** + +✅ **Animation RPC Functions** - Added `sync_walk_animation()`, `sync_pickup_animation()`, `sync_put_animation()`, and `sync_special_animation()` RPC functions to `player.gd` for network-synced animations. + +✅ **Manager Updates** - Updated `player_movement_manager.gd`, `playerboard_manager.gd`, and `powerup_manager.gd` to call RPC animation sync functions instead of local-only animation methods. Now all players see each other's walking, grab, put, and power-up animations. + +**Username Synchronization** + +✅ **Display Name Variable** - Added `display_name` variable to `player.gd` that stores the player's actual username from their profile. + +✅ **Lobby Name Sync** - Updated `lobby_manager.gd` to send the client's username and character to the host when requesting room info via `request_room_info()`. Host now correctly displays client usernames instead of "Player 12345". + +✅ **Auto-Use Profile Name** - Updated `lobby.gd` to hide the name input field for logged-in users and automatically use `UserProfileManager.get_display_name()`. Only guest users see and can edit the name input. + +✅ **In-Game Name Display** - Updated `player.gd` `_ready()` to look up display name from `LobbyManager.get_players()` and sync via `sync_display_name()` RPC. Player name labels and message bar now show actual usernames. + +**Leaderboard Name Fix** + +✅ **Display Name Usage** - Updated `main.gd` to use `player.display_name` instead of `player.name` (peer ID) in 6 locations: `_on_leaderboard_updated()`, `_show_game_over_panel()`, `request_leaderboard_sync()`, `sync_leaderboard_data()`, and `_update_leaderboard_display()`. + +**Player Rotation Sync** + +✅ **Rotation RPC** - Updated `player_movement_manager.gd` to call `rpc("sync_rotation", target_rotation)` when player rotates toward a target. Other clients now see correct facing direction. + +✅ **Smooth Interpolation** - Updated `sync_rotation()` RPC in `player.gd` to also set `movement_manager.target_rotation` for smooth rotation interpolation on remote clients. + +--- + +**Nakama Local Development Checklist** (Next Steps) + +⬜ **Docker Compose Setup** - Create `server/docker-compose.yaml` with Nakama + PostgreSQL services for local development on Docker Desktop (Windows/Mac). + +⬜ **Run Locally** - Start Nakama on localhost:7350 via `docker-compose up -d`. + +⬜ **Tunnel with localhost.run** - Use `ssh -R 80:localhost:7350 nokey@localhost.run` to expose port 7350 for remote device testing. + +⬜ **PostgreSQL Learning** - Study the Nakama PostgreSQL schema and flow for managing user data, storage, and leaderboards. Goal: Make the online dedicated server more flexible with PostgreSQL. diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 69035a5..df2fae6 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -74,9 +74,24 @@ func _ready(): # Get player slot references _setup_player_slots() - # Set player name from profile + # Set player name from profile and configure input visibility if player_name_input: - player_name_input.text = UserProfileManager.get_display_name() + # Get the parent container for the input to hide/show properly + var input_section = player_name_input.get_parent() + + if AuthManager.is_guest: + # Guest user - show name input and let them enter a name + if input_section: + input_section.visible = true + player_name_input.text = "Guest" + player_name_input.editable = true + else: + # Logged-in user - hide name input and use profile name automatically + if input_section: + input_section.visible = false + player_name_input.text = UserProfileManager.get_display_name() + # Also set the LobbyManager name immediately + LobbyManager.local_player_name = UserProfileManager.get_display_name() # Connect button signals - Main Menu create_room_btn.pressed.connect(_on_create_room_pressed) @@ -168,9 +183,13 @@ func _show_panel(panel_name: String) -> void: # ============================================================================= func _on_create_room_pressed() -> void: - LobbyManager.local_player_name = player_name_input.text.strip_edges() - if LobbyManager.local_player_name.is_empty(): - LobbyManager.local_player_name = "Host" + # Use profile name for logged-in users, or input name for guests + if AuthManager.is_guest: + LobbyManager.local_player_name = player_name_input.text.strip_edges() + if LobbyManager.local_player_name.is_empty(): + LobbyManager.local_player_name = "Guest" + else: + LobbyManager.local_player_name = UserProfileManager.get_display_name() connection_status.text = "Creating room..." LobbyManager.create_room("Room %d" % randi_range(1000, 9999)) @@ -206,9 +225,13 @@ func _on_join_pressed() -> void: connection_status.text = "No room selected" return - LobbyManager.local_player_name = player_name_input.text.strip_edges() - if LobbyManager.local_player_name.is_empty(): - LobbyManager.local_player_name = "Player" + # Use profile name for logged-in users, or input name for guests + if AuthManager.is_guest: + LobbyManager.local_player_name = player_name_input.text.strip_edges() + if LobbyManager.local_player_name.is_empty(): + LobbyManager.local_player_name = "Guest" + else: + LobbyManager.local_player_name = UserProfileManager.get_display_name() connection_status.text = "Joining room..." LobbyManager.join_room(match_id) diff --git a/scenes/main.gd b/scenes/main.gd index c551924..c170527 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -957,7 +957,7 @@ func _on_leaderboard_updated(sorted_scores: Array): for p in get_tree().get_nodes_in_group("Players"): player_data.append({ "peer_id": p.get_multiplayer_authority(), - "name": p.name, + "name": p.display_name if not p.display_name.is_empty() else str(p.name), "score": goals_cycle_manager.get_player_score(p.get_multiplayer_authority()) if goals_cycle_manager else 0 }) rpc("sync_leaderboard_data", player_data) @@ -1049,7 +1049,7 @@ func _show_game_over_panel(): var player_scores = [] for p in get_tree().get_nodes_in_group("Players"): player_scores.append({ - "name": p.name, + "name": p.display_name if not p.display_name.is_empty() else str(p.name), "score": goals_cycle_manager.get_player_score(p.get_multiplayer_authority()) if goals_cycle_manager else 0 }) player_scores.sort_custom(func(a, b): return a.score > b.score) @@ -1068,7 +1068,7 @@ func _show_game_over_panel(): entry.add_child(rank_label) var name_label = Label.new() - name_label.text = "Player %s" % player_scores[i].name + name_label.text = player_scores[i].name name_label.add_theme_font_size_override("font_size", 28) name_label.add_theme_color_override("font_color", rank_colors[i]) name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -1149,7 +1149,7 @@ func request_leaderboard_sync(): for p in get_tree().get_nodes_in_group("Players"): player_data.append({ "peer_id": p.get_multiplayer_authority(), - "name": p.name, + "name": p.display_name if not p.display_name.is_empty() else str(p.name), "score": goals_cycle_manager.get_player_score(p.get_multiplayer_authority()) if goals_cycle_manager else 0 }) rpc_id(sender_id, "sync_leaderboard_data", player_data) @@ -1183,7 +1183,7 @@ func sync_leaderboard_data(player_data: Array): if rank_label: rank_label.text = _get_ordinal(i + 1) if name_label: - name_label.text = "Player " + str(data.name) + name_label.text = str(data.name) if score_label: score_label.text = str(data.score) @@ -1211,7 +1211,7 @@ func _update_leaderboard_display(): for p in all_players: var peer_id = p.get_multiplayer_authority() var score = goals_cycle_manager.get_player_score(peer_id) if goals_cycle_manager else 0 - player_data.append({"peer_id": peer_id, "name": p.name, "score": score}) + player_data.append({"peer_id": peer_id, "name": p.display_name if not p.display_name.is_empty() else str(p.name), "score": score}) # Sort by score descending player_data.sort_custom(func(a, b): return a.score > b.score) diff --git a/scenes/player.gd b/scenes/player.gd index a2564c1..648a1d0 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -12,6 +12,9 @@ var powerup_manager # Score tracking var score: int = 0 +# Display name (synced across network) +var display_name: String = "" + # Special effect states var is_frozen: bool = false var is_invisible: bool = false @@ -115,9 +118,22 @@ const AVAILABLE_CHARACTERS: Array[String] = ["Bob", "Masbro", "Gatot", "Oldpop"] # Delegated to RaceManager func _ready(): - # Ensure name is set first + # Ensure name is set first (node name = authority ID for multiplayer identification) name = str(get_multiplayer_authority()) - $Name.text = str(name) + + # Look up player's display name from LobbyManager + var my_id = get_multiplayer_authority() + for player_data in LobbyManager.get_players(): + if player_data.get("id") == my_id: + display_name = player_data.get("name", "Player") + break + if display_name.is_empty(): + display_name = "Player %d" % my_id + $Name.text = display_name + + # Sync name to other peers if this is our local player + if is_multiplayer_authority(): + rpc("sync_display_name", display_name) # Wait briefly to ensure proper scene setup await get_tree().create_timer(0.1).timeout @@ -354,6 +370,30 @@ func play_idle_animation() -> void: if anim_player and anim_player.has_animation("animation-pack/idle"): anim_player.play("animation-pack/idle") +# ============================================================================= +# Network-Synced Animation Functions +# ============================================================================= + +@rpc("any_peer", "call_local", "reliable") +func sync_walk_animation() -> void: + """Sync walk animation across network.""" + play_walk_animation() + +@rpc("any_peer", "call_local", "reliable") +func sync_pickup_animation() -> void: + """Sync pickup/grab animation across network.""" + play_pickup_animation() + +@rpc("any_peer", "call_local", "reliable") +func sync_put_animation() -> void: + """Sync put/drop animation across network.""" + play_put_animation() + +@rpc("any_peer", "call_local", "reliable") +func sync_special_animation() -> void: + """Sync special ability animation across network.""" + play_special_animation() + # ============================================================================= # Screen Shake # ============================================================================= @@ -486,10 +526,16 @@ func sync_bot_status(is_bot_status: bool): behavior_tree.set_physics_process(false) behavior_tree.set_process(false) +@rpc("any_peer", "call_local", "reliable") +func sync_display_name(new_name: String) -> void: + """Sync display name across network.""" + display_name = new_name + $Name.text = display_name + func _process(delta): if is_multiplayer_authority(): - # Visual debugging - show connection status in name label - $Name.text = str(name) + "\n(Auth: " + str(get_multiplayer_authority()) + ")" + # Visual debugging - show display name with connection status + $Name.text = display_name if not display_name.is_empty() else str(name) # Periodically verify our existence to others _verify_timer += delta @@ -819,8 +865,8 @@ func display_message(message, type: int = 0): # Send message to the main scene's message bar instead of player bubble var main = get_tree().get_root().get_node_or_null("Main") if main and main.has_method("add_message_to_bar"): - var player_name = $Name.text.split("\n")[0] if $Name else str(name) - main.add_message_to_bar(player_name, message, type) + var player_name_str = display_name if not display_name.is_empty() else str(name) + main.add_message_to_bar(player_name_str, message, type) func initialize_random_goals(_size: int, min_value: int, max_value: int, null_count: float) -> Array[int]: goals.clear() @@ -1171,6 +1217,8 @@ func _highlight_adjacent_playerboard_slots(): func sync_rotation(new_rotation: float): if not is_multiplayer_authority(): rotation.y = new_rotation + if movement_manager: + movement_manager.target_rotation = new_rotation @rpc("any_peer", "call_local", "reliable") func sync_grid_item(x: int, y: int, z: int, item: int): diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 8dc2419..ddc4503 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -309,13 +309,24 @@ func _on_match_joined(match_id: String) -> void: # Client will request room info when peer connection is established @rpc("any_peer", "reliable") -func request_room_info(requester_id: int) -> void: - """Client requests room info from host.""" +func request_room_info(requester_id: int, requester_name: String, requester_character: String) -> void: + """Client requests room info from host, sending their name and character.""" if not multiplayer.is_server(): return + # Update the player's name and character in the list + for player in players_in_room: + if player["id"] == requester_id: + player["name"] = requester_name + player["character"] = requester_character + break + # Send room data to requester rpc_id(requester_id, "receive_room_info", current_room, players_in_room) + + # Also sync updated player list to all other clients + rpc("sync_player_list", players_in_room) + emit_signal("player_list_changed") @rpc("reliable") func receive_room_info(room_data: Dictionary, player_list: Array) -> void: @@ -346,7 +357,8 @@ func _on_peer_connected(peer_id: int) -> void: if peer_id == 1 and not is_host: # Wait a frame to ensure connection is stable await get_tree().process_frame - rpc_id(1, "request_room_info", multiplayer.get_unique_id()) + # Send our actual name and character to the host + rpc_id(1, "request_room_info", multiplayer.get_unique_id(), local_player_name, available_characters[local_character_index]) func _on_peer_disconnected(peer_id: int) -> void: """Called when peer disconnects.""" diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 9e1cfb6..3f847c3 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -32,6 +32,9 @@ func rotate_towards_target(target_pos: Vector2i): var direction = Vector3(target_pos.x - player.current_position.x, 0, target_pos.y - player.current_position.y) if direction != Vector3.ZERO: target_rotation = atan2(direction.x, direction.z) + # Sync rotation to other clients + if player.is_multiplayer_authority(): + player.rpc("sync_rotation", target_rotation) func simple_move_to(grid_position: Vector2i) -> bool: if not player.is_multiplayer_authority() or is_moving: @@ -70,9 +73,9 @@ func simple_move_to(grid_position: Vector2i) -> bool: # All checks passed, perform move rotate_towards_target(grid_position) - # Play walk animation - if player.has_method("play_walk_animation"): - player.play_walk_animation() + # Play walk animation (synced across network) + if player.is_multiplayer_authority() and 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() diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index 2606322..c3c3cce 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -54,9 +54,9 @@ func grab_item(grid_position: Vector2i) -> bool: if not player.is_multiplayer_authority(): return false - # Play pickup animation - if player.has_method("play_pickup_animation"): - player.play_pickup_animation() + # Play pickup animation (synced across network) + if player.is_multiplayer_authority() and player.has_method("sync_pickup_animation"): + player.rpc("sync_pickup_animation") # === Optimistic Local Update (immediate visual feedback) === # Apply changes locally first, server will validate/sync @@ -316,9 +316,9 @@ func auto_put_item() -> bool: var cell = Vector3i(target_pos.x, 1, target_pos.y) if player.is_multiplayer_authority(): - # Play put animation - if player.has_method("play_put_animation"): - player.play_put_animation() + # Play put animation (synced across network) + if player.is_multiplayer_authority() and player.has_method("sync_put_animation"): + player.rpc("sync_put_animation") # === Optimistic Local Update (immediate visual feedback) === enhanced_gridmap.set_cell_item(cell, item) # Add item to grid visually immediately diff --git a/scripts/managers/powerup_manager.gd b/scripts/managers/powerup_manager.gd index 921a9a2..aa7c567 100644 --- a/scripts/managers/powerup_manager.gd +++ b/scripts/managers/powerup_manager.gd @@ -100,9 +100,9 @@ func use_special_effect(): # Start cooldown special_cooldown_timer = SPECIAL_COOLDOWN - # Play special animation (backflip) - if player.has_method("play_special_animation"): - player.play_special_animation() + # Play special animation (backflip) - synced across network + if player.is_multiplayer_authority() and player.has_method("sync_special_animation"): + player.rpc("sync_special_animation") # Trigger random special effect via SpecialTilesManager var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") diff --git a/scripts/managers/ui_manager.gd b/scripts/managers/ui_manager.gd index ce911d1..ee6257c 100644 --- a/scripts/managers/ui_manager.gd +++ b/scripts/managers/ui_manager.gd @@ -375,7 +375,11 @@ func initialize_leaderboard_with_players(players: Array): var score_label = entry.get_node_or_null("ScoreLabel") if name_label: - name_label.text = str(player.name) if player else "Player " + str(i + 1) + # Use display_name if available, otherwise fallback to node name + var player_display_name = player.display_name if player and player.get("display_name") else "" + if player_display_name.is_empty(): + player_display_name = str(player.name) if player else "Player " + str(i + 1) + name_label.text = player_display_name if score_label: score_label.text = str(player.score) if player and player.get("score") else "0" diff --git a/scripts/nakama_manager.gd b/scripts/nakama_manager.gd index 09fda89..dd2d492 100644 --- a/scripts/nakama_manager.gd +++ b/scripts/nakama_manager.gd @@ -2,7 +2,7 @@ extends Node # Standard Nakama Configuration const NAKAMA_SERVER_KEY = "defaultkey" -const NAKAMA_HOST = "77.237.232.232" +const NAKAMA_HOST = "localhost" const NAKAMA_PORT = 7350 const NAKAMA_SCHEME = "http" diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml new file mode 100644 index 0000000..8b9e11c --- /dev/null +++ b/server/docker-compose.yaml @@ -0,0 +1,51 @@ +version: '3.8' + +services: + postgres: + container_name: nakama-postgres + image: postgres:15-alpine + environment: + - POSTGRES_DB=nakama + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=localdb + volumes: + - nakama-data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "nakama"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + nakama: + container_name: nakama + image: heroiclabs/nakama:3.24.2 + depends_on: + postgres: + condition: service_healthy + entrypoint: + - "/bin/sh" + - "-ecx" + - > + /nakama/nakama migrate up --database.address postgres:localdb@postgres:5432/nakama && + exec /nakama/nakama --name nakama + --database.address postgres:localdb@postgres:5432/nakama + --logger.level DEBUG + --session.token_expiry_sec 7200 + --console.username admin + --console.password password + ports: + - "7349:7349" # gRPC API + - "7350:7350" # HTTP API (main client port) + - "7351:7351" # Console + healthcheck: + test: ["CMD", "/nakama/nakama", "healthcheck"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + +volumes: + nakama-data: