Files
dauphincraft/scripts/Main.gd
Poulpe (Silver Surfer deploy) 7f6811995d feat(progression): succès/achievements + toasts notification
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>
2026-04-20 18:11:33 +00:00

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)