355 lines
12 KiB
GDScript
355 lines
12 KiB
GDScript
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()
|
|
AudioManager.play_deep_sea_ambient()
|
|
|
|
# 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")
|
|
|
|
# Save system
|
|
var save_mgr: Node = get_node_or_null("/root/SaveManager")
|
|
if save_mgr != null:
|
|
save_mgr.register_player(dolphin)
|
|
save_mgr.load_game()
|
|
|
|
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)
|
|
if dolphin.has_signal("use_consumable_requested"):
|
|
dolphin.use_consumable_requested.connect(_on_use_consumable)
|
|
|
|
|
|
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_block_break(hit_position)
|
|
_award_break_xp(broken_id, hit_position)
|
|
else:
|
|
_world_sync.server_break_block(hit_position)
|
|
AudioManager.play_block_break(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 base_gain: int = pp.XP_BREAK_BY_BLOCK.get(block_id, pp.XP_BREAK_DEFAULT)
|
|
var combo: Node = get_node_or_null("/root/ComboTracker")
|
|
var mult: float = 1.0
|
|
if combo != null:
|
|
mult = combo.bump()
|
|
var gain: int = int(round(base_gain * mult))
|
|
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)
|
|
var am: Node = get_node_or_null("/root/AchievementManager")
|
|
if am != null:
|
|
am.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)
|
|
|
|
|
|
const CONSUMABLE_EFFECTS: Dictionary = {
|
|
102: {"oxygen": 40.0, "hp": 0.0, "hunger": 0.0, "label": "+40 O₂", "color": Color(0.3, 0.9, 1.0)},
|
|
103: {"oxygen": 0.0, "hp": 5.0, "hunger": 30.0, "label": "+30 🍴", "color": Color(0.6, 1.0, 0.4)},
|
|
106: {"oxygen": 0.0, "hp": 50.0, "hunger": 0.0, "label": "+50 ❤", "color": Color(1.0, 0.5, 0.5)},
|
|
}
|
|
|
|
|
|
func _on_use_consumable() -> void:
|
|
if not is_instance_valid(_my_dolphin):
|
|
return
|
|
var slot: Variant = inventory.get_selected_item()
|
|
if slot == null:
|
|
return
|
|
var item_id: int = slot["item_id"]
|
|
if not CONSUMABLE_EFFECTS.has(item_id):
|
|
return
|
|
var effect: Dictionary = CONSUMABLE_EFFECTS[item_id]
|
|
if effect["oxygen"] > 0.0 and _my_dolphin.has_method("refill_oxygen"):
|
|
_my_dolphin.refill_oxygen(effect["oxygen"])
|
|
if effect["hp"] > 0.0 and _my_dolphin.has_method("heal"):
|
|
_my_dolphin.heal(effect["hp"])
|
|
if effect["hunger"] > 0.0 and _my_dolphin.has_method("feed"):
|
|
_my_dolphin.feed(effect["hunger"])
|
|
inventory.remove_item_from_slot(inventory.selected_hotbar, 1)
|
|
AudioManager.play_bubble_sfx(_my_dolphin.global_position)
|
|
_spawn_consumable_popup(effect["label"], effect["color"])
|
|
|
|
|
|
func _spawn_consumable_popup(label: String, color: Color) -> void:
|
|
if not is_instance_valid(_my_dolphin):
|
|
return
|
|
var popup_script: Script = load("res://scripts/progression/XpPopup.gd")
|
|
if popup_script == null:
|
|
return
|
|
popup_script.spawn(self, label, _my_dolphin.global_position + Vector3(0, 1.4, 0), color)
|
|
|
|
|
|
func _on_echolocation(position: Vector3, _radius: float) -> void:
|
|
# Echo ping is now fired directly from DolphinController; keep achievement hook
|
|
var am: Node = get_node_or_null("/root/AchievementManager")
|
|
if am != null:
|
|
am.note_echolocation()
|
|
|
|
|
|
# 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)
|