Nouvel autoload AchievementManager avec 12 succès (Premier éclat, Mineur confirmé, Chasseur de perles, Fouilleur d'épaves, Abysses, Légende des océans, etc.). Chaque déblocage donne un bonus XP et joue un son. HUD: toasts dorés avec icône + titre + desc + fade-in/out, empilés en haut centre. Son bulle au déblocage. Long-terme: cible des milestones cumulatifs qui donnent un sentiment de progression durable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
304 lines
9.8 KiB
GDScript
304 lines
9.8 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()
|
|
|
|
# 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)
|
|
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)
|
|
|
|
|
|
func _on_echolocation(position: Vector3, _radius: float) -> void:
|
|
AudioManager.play_bubble_sfx(position)
|
|
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)
|