extends Node3D @onready var world: Node3D = $World/ChunkManager @onready var plankton_follower: Node3D = $PlanktonFollower var inventory: Inventory = Inventory.new() var _inventory_ui: CanvasLayer = null var _pause_menu: Control = null var _last_chunk_update_pos: Vector3 = Vector3(99999, 99999, 99999) # Multiplayer state var _my_dolphin: CharacterBody3D = null var _peer_dolphins: Dictionary = {} # peer_id -> CharacterBody3D var _world_sync: Node = null var _chat_manager: Node = null # Dolphin scene cached var _dolphin_scene: PackedScene = preload("res://scenes/Dolphin.tscn") func _ready() -> void: add_to_group("main") # Setup WorldSyncComponent on ChunkManager _world_sync = load("res://scripts/net/WorldSyncComponent.gd").new() _world_sync.name = "WorldSyncComponent" world.add_child(_world_sync) world.world_seed = 12345 var mode := NetworkManager.current_mode if NetworkManager.is_solo(): _spawn_local_dolphin() _spawn_mobs() elif NetworkManager.is_server(): # HOST or DEDICATED: spawn mobs, connect peer signals, spawn dolphin for existing peers _spawn_mobs() NetworkManager.peer_connected.connect(_on_net_peer_connected) NetworkManager.peer_disconnected.connect(_on_net_peer_disconnected) if mode == NetworkManager.Mode.HOST: # Host also plays — spawn own dolphin _spawn_networked_dolphin(multiplayer.get_unique_id()) # Spawn dolphins for any peers already connected (edge case) for peer_id in multiplayer.get_peers(): _spawn_networked_dolphin(peer_id) elif NetworkManager.is_client(): # CLIENT: only dolphin for self, world updates come from server NetworkManager.peer_disconnected.connect(_on_net_peer_disconnected) _spawn_networked_dolphin(multiplayer.get_unique_id()) # Chat (all modes except dedicated headless) if mode != NetworkManager.Mode.DEDICATED or not DisplayServer.get_name() == "headless": _chat_manager = load("res://scripts/net/ChatManager.gd").new() _chat_manager.name = "ChatManager" add_child(_chat_manager) # Pearl spawner — skip on dedicated headless (no local player) if not (mode == NetworkManager.Mode.DEDICATED and DisplayServer.get_name() == "headless"): _spawn_pearl_system() # Audio AudioManager.play_ambient_loop("underwater_ambient") AudioManager.play_music("underwater_theme") AudioManager.play_whale_call_random() # Starting inventory inventory.add_item(2, 10) inventory.add_item(6, 5) inventory.add_item(8, 2) func _spawn_local_dolphin() -> void: var dolphin: CharacterBody3D = _dolphin_scene.instantiate() dolphin.name = "Dolphin" dolphin.position = Vector3(0, 55, 0) add_child(dolphin) _my_dolphin = dolphin _connect_dolphin_signals(dolphin) var hud: CanvasLayer = dolphin.get_node_or_null("HUD") if is_instance_valid(hud) and hud.has_method("connect_to_dolphin"): hud.connect_to_dolphin(dolphin) dolphin.add_to_group("player") var ui_scene: PackedScene = load("res://scenes/InventoryUI.tscn") if ui_scene != null: _inventory_ui = ui_scene.instantiate() as CanvasLayer add_child(_inventory_ui) _inventory_ui.setup(inventory) else: push_error("Main: could not load InventoryUI.tscn") var pause_scene: PackedScene = preload("res://scenes/PauseMenu.tscn") _pause_menu = pause_scene.instantiate() as Control add_child(_pause_menu) _pause_menu.hide() plankton_follower.global_position = dolphin.position func _spawn_networked_dolphin(peer_id: int) -> void: var dolphin: CharacterBody3D = _dolphin_scene.instantiate() dolphin.name = "Dolphin_%d" % peer_id dolphin.position = Vector3(0, 55, 0) add_child(dolphin) # Attach sync component var sync_comp: Node = load("res://scripts/net/PlayerSyncComponent.gd").new() sync_comp.name = "PlayerSyncComponent" dolphin.add_child(sync_comp) sync_comp.setup(peer_id) var my_id := multiplayer.get_unique_id() if peer_id == my_id: _my_dolphin = dolphin _connect_dolphin_signals(dolphin) dolphin.add_to_group("player") var hud: CanvasLayer = dolphin.get_node_or_null("HUD") if is_instance_valid(hud) and hud.has_method("connect_to_dolphin"): hud.connect_to_dolphin(dolphin) var ui_scene: PackedScene = load("res://scenes/InventoryUI.tscn") if ui_scene != null: _inventory_ui = ui_scene.instantiate() as CanvasLayer add_child(_inventory_ui) _inventory_ui.setup(inventory) var pause_scene: PackedScene = preload("res://scenes/PauseMenu.tscn") _pause_menu = pause_scene.instantiate() as Control add_child(_pause_menu) _pause_menu.hide() plankton_follower.global_position = dolphin.position else: # Remote dolphin: disable input processing dolphin.set_process_unhandled_input(false) if dolphin.has_method("set_physics_process"): # Keep physics but no input pass _peer_dolphins[peer_id] = dolphin func _despawn_peer_dolphin(peer_id: int) -> void: if _peer_dolphins.has(peer_id): var d: CharacterBody3D = _peer_dolphins[peer_id] if is_instance_valid(d): d.queue_free() _peer_dolphins.erase(peer_id) func _spawn_mobs() -> void: var MobSpawnerScene: PackedScene = load("res://scenes/mobs/MobSpawner.tscn") if MobSpawnerScene == null: return var mob_spawner: Node3D = MobSpawnerScene.instantiate() add_child(mob_spawner) if is_instance_valid(_my_dolphin): mob_spawner.player = mob_spawner.get_path_to(_my_dolphin) func _spawn_pearl_system() -> void: var spawner_script: Script = load("res://scripts/world/PearlSpawner.gd") if spawner_script == null: return var spawner := Node3D.new() spawner.name = "PearlSpawner" spawner.set_script(spawner_script) add_child(spawner) if is_instance_valid(_my_dolphin): spawner.player = spawner.get_path_to(_my_dolphin) func _connect_dolphin_signals(dolphin: CharacterBody3D) -> void: if dolphin.has_signal("block_break_requested"): dolphin.block_break_requested.connect(_on_block_break) if dolphin.has_signal("block_place_requested"): dolphin.block_place_requested.connect(_on_block_place) if dolphin.has_signal("echolocation_triggered"): dolphin.echolocation_triggered.connect(_on_echolocation) if dolphin.has_signal("hotbar_scroll"): dolphin.hotbar_scroll.connect(inventory.scroll_hotbar) func _on_net_peer_connected(peer_id: int) -> void: if NetworkManager.is_server(): _spawn_networked_dolphin(peer_id) func _on_net_peer_disconnected(peer_id: int) -> void: _despawn_peer_dolphin(peer_id) func _process(_delta: float) -> void: if not is_instance_valid(_my_dolphin): return if _my_dolphin.global_position.distance_to(_last_chunk_update_pos) > 4.0: world.update_player_position(_my_dolphin.global_position) _last_chunk_update_pos = _my_dolphin.global_position plankton_follower.global_position = _my_dolphin.global_position func _unhandled_input(event: InputEvent) -> void: if not event.is_action_pressed("escape"): return if _inventory_ui != null and _inventory_ui.get("_is_open"): _inventory_ui.call("_toggle_inventory") get_viewport().set_input_as_handled() return if is_instance_valid(_pause_menu): if _pause_menu.visible: _pause_menu.hide_pause() else: _pause_menu.show_pause() get_viewport().set_input_as_handled() func _on_block_break(hit_position: Vector3, _normal: Vector3) -> void: var broken_id: int = 0 if NetworkManager.is_solo(): broken_id = world.break_block(hit_position) if broken_id > 0: inventory.add_item(broken_id, 1) AudioManager.play_bubble_sfx(hit_position) _award_break_xp(broken_id, hit_position) else: _world_sync.server_break_block(hit_position) AudioManager.play_bubble_sfx(hit_position) # Block break particle burst var bbp_script := load("res://scripts/dolphin/BlockBreakParticles.gd") var burst := GPUParticles3D.new() burst.set_script(bbp_script) add_child(burst) var broken_color: Color = Color(0.8, 0.6, 0.3) # default sand if broken_id > 0: broken_color = BlockDatabase.get_color(broken_id) burst.emit_burst(hit_position, broken_color) func _award_break_xp(block_id: int, hit_position: Vector3) -> void: var pp: Node = get_node_or_null("/root/PlayerProgress") if pp == null: return var gain: int = pp.XP_BREAK_BY_BLOCK.get(block_id, pp.XP_BREAK_DEFAULT) pp.award(gain, "bloc", hit_position) _spawn_xp_popup(gain, hit_position) var qm: Node = get_node_or_null("/root/QuestManager") if qm != null: qm.note_block_break(block_id) func _spawn_xp_popup(amount: int, world_pos: Vector3) -> void: if amount <= 0: return var popup_script: Script = load("res://scripts/progression/XpPopup.gd") if popup_script == null: return popup_script.spawn(self, "+%d XP" % amount, world_pos + Vector3(0, 0.8, 0)) func _on_block_place(hit_position: Vector3, normal: Vector3) -> void: var selected: Variant = inventory.get_selected_item() if selected == null: return if not ItemDatabase.is_placeable(selected["item_id"]): return var place_pos := hit_position + normal * 0.5 var block_coord := Vector3(floor(place_pos.x), floor(place_pos.y), floor(place_pos.z)) var block_center := block_coord + Vector3(0.5, 0.5, 0.5) if is_instance_valid(_my_dolphin): var dolphin_pos: Vector3 = _my_dolphin.global_position var dist := block_center - dolphin_pos if abs(dist.x) < 1.0 and abs(dist.y) < 1.8 and abs(dist.z) < 1.0: return if NetworkManager.is_solo(): if world.place_block(place_pos, selected["item_id"]): inventory.remove_item_from_slot(inventory.selected_hotbar, 1) else: _world_sync.server_place_block(place_pos, selected["item_id"]) inventory.remove_item_from_slot(inventory.selected_hotbar, 1) func _on_echolocation(position: Vector3, _radius: float) -> void: AudioManager.play_bubble_sfx(position) # Called by WorldSyncComponent when server confirms a block was broken by this client func _on_remote_block_broken(broken_id: int) -> void: if broken_id > 0: inventory.add_item(broken_id, 1)