From 982a4ec23d7617d2d77a06ace26f5c089bab4efa Mon Sep 17 00:00:00 2001 From: "Poulpe (Silver Surfer deploy)" Date: Mon, 20 Apr 2026 18:01:57 +0000 Subject: [PATCH] feat(gameplay): perles collectibles + recette amulette de soin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Area3D Pearl flottant/tournant qui émet une couleur nacrée, spawned périodiquement près du joueur (PearlSpawner, 3 max, intervalle 9s, zone haute du seafloor). Collecte automatique au contact, ajout à l'inventaire (item 105 Perle). Nouvelle recette : 2 perles + 1 corail rouge → Amulette de soin (item 106, consommable). Spawner désactivé sur serveur dédié headless. Co-Authored-By: Claude Opus 4.7 (1M context) --- scenes/world/Pearl.tscn | 6 ++ scripts/Main.gd | 16 ++++ scripts/inventory/CraftingRecipes.gd | 5 ++ scripts/inventory/ItemDatabase.gd | 6 +- scripts/world/Pearl.gd | 62 ++++++++++++++ scripts/world/PearlSpawner.gd | 116 +++++++++++++++++++++++++++ 6 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 scenes/world/Pearl.tscn create mode 100644 scripts/world/Pearl.gd create mode 100644 scripts/world/PearlSpawner.gd diff --git a/scenes/world/Pearl.tscn b/scenes/world/Pearl.tscn new file mode 100644 index 0000000..005f70c --- /dev/null +++ b/scenes/world/Pearl.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://dauphincraft_pearl"] + +[ext_resource type="Script" path="res://scripts/world/Pearl.gd" id="1_pearl"] + +[node name="Pearl" type="Area3D"] +script = ExtResource("1_pearl") diff --git a/scripts/Main.gd b/scripts/Main.gd index fb80c6a..1b0da15 100644 --- a/scripts/Main.gd +++ b/scripts/Main.gd @@ -56,6 +56,10 @@ func _ready() -> void: _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") @@ -158,6 +162,18 @@ func _spawn_mobs() -> void: 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) diff --git a/scripts/inventory/CraftingRecipes.gd b/scripts/inventory/CraftingRecipes.gd index 8668c86..a0f8b15 100644 --- a/scripts/inventory/CraftingRecipes.gd +++ b/scripts/inventory/CraftingRecipes.gd @@ -27,6 +27,11 @@ static var RECIPES: Array = [ "inputs": [{"item_id": 4, "count": 4}, {"item_id": 7, "count": 2}], "output": {"item_id": 104, "count": 1} }, + { + "name": "Amulette de soin", + "inputs": [{"item_id": 105, "count": 2}, {"item_id": 4, "count": 1}], + "output": {"item_id": 106, "count": 1} + }, ] diff --git a/scripts/inventory/ItemDatabase.gd b/scripts/inventory/ItemDatabase.gd index a7329e8..80b5eee 100644 --- a/scripts/inventory/ItemDatabase.gd +++ b/scripts/inventory/ItemDatabase.gd @@ -16,6 +16,8 @@ const ITEM_NAMES: Dictionary = { 102: "Bulle d'air", 103: "Algue cuisinee", 104: "Armure ecailles", + 105: "Perle", + 106: "Amulette de soin", } const ITEM_COLORS: Dictionary = { @@ -34,10 +36,12 @@ const ITEM_COLORS: Dictionary = { 102: Color(0.2, 0.9, 0.9), 103: Color(0.05, 0.4, 0.05), 104: Color(0.1, 0.55, 0.5), + 105: Color(0.95, 0.95, 1.0), + 106: Color(1.0, 0.85, 0.4), } const PLACEABLE_IDS: Array = [2, 3, 4, 5, 6, 7, 8] -const CONSUMABLE_IDS: Array = [102, 103] +const CONSUMABLE_IDS: Array = [102, 103, 106] const TOOL_IDS: Array = [100, 101, 104] diff --git a/scripts/world/Pearl.gd b/scripts/world/Pearl.gd new file mode 100644 index 0000000..ae1de15 --- /dev/null +++ b/scripts/world/Pearl.gd @@ -0,0 +1,62 @@ +extends Area3D + +signal collected(item_id: int) + +const ITEM_ID_PEARL: int = 105 + +var _time: float = 0.0 +var _mesh: MeshInstance3D = null +var _collected: bool = false + + +func _ready() -> void: + monitoring = true + monitorable = false + body_entered.connect(_on_body_entered) + _build_visual() + _build_shape() + + +func _build_visual() -> void: + _mesh = MeshInstance3D.new() + var sphere := SphereMesh.new() + sphere.radius = 0.22 + sphere.height = 0.44 + sphere.radial_segments = 12 + sphere.rings = 8 + _mesh.mesh = sphere + + var mat := StandardMaterial3D.new() + mat.albedo_color = Color(0.96, 0.96, 1.0, 1.0) + mat.metallic = 0.3 + mat.roughness = 0.08 + mat.emission_enabled = true + mat.emission = Color(0.85, 0.92, 1.0) + mat.emission_energy_multiplier = 0.8 + _mesh.material_override = mat + + add_child(_mesh) + + +func _build_shape() -> void: + var shape := CollisionShape3D.new() + var s := SphereShape3D.new() + s.radius = 0.75 + shape.shape = s + add_child(shape) + + +func _process(delta: float) -> void: + _time += delta + if _mesh != null: + _mesh.position.y = sin(_time * 2.0) * 0.18 + _mesh.rotation.y += delta * 1.4 + + +func _on_body_entered(body: Node) -> void: + if _collected: + return + if body.is_in_group("player"): + _collected = true + collected.emit(ITEM_ID_PEARL) + queue_free() diff --git a/scripts/world/PearlSpawner.gd b/scripts/world/PearlSpawner.gd new file mode 100644 index 0000000..ccb88cf --- /dev/null +++ b/scripts/world/PearlSpawner.gd @@ -0,0 +1,116 @@ +extends Node3D + +@export var player: NodePath = NodePath("") +@export var spawn_radius: float = 28.0 +@export var despawn_radius: float = 55.0 +@export var max_pearls: int = 3 + +const PEARL_SCENE: String = "res://scenes/world/Pearl.tscn" +const SPAWN_INTERVAL: float = 9.0 + +var _player_node: CharacterBody3D = null +var _chunk_manager: Node = null +var _pearls: Array[Area3D] = [] +var _timer: float = 0.0 + + +func _ready() -> void: + _resolve_player() + _find_chunk_manager() + + +func _resolve_player() -> void: + if not player.is_empty(): + var n: Node = get_node_or_null(player) + if n is CharacterBody3D: + _player_node = n + if _player_node == null: + var players := get_tree().get_nodes_in_group("player") + if players.size() > 0: + _player_node = players[0] as CharacterBody3D + + +func _find_chunk_manager() -> void: + var cm: Node = get_node_or_null("/root/Main/World/ChunkManager") + if cm != null: + _chunk_manager = cm + + +func _physics_process(delta: float) -> void: + _timer += delta + if _timer < SPAWN_INTERVAL: + return + _timer = 0.0 + _resolve_player() + _tick() + + +func _tick() -> void: + if not is_instance_valid(_player_node): + return + _cull() + if _pearls.size() >= max_pearls: + return + var pos: Vector3 = _random_spawn_pos() + if not _is_valid_spawn(pos): + return + var packed: PackedScene = load(PEARL_SCENE) + if packed == null: + return + var pearl: Area3D = packed.instantiate() as Area3D + if pearl == null: + return + get_tree().current_scene.add_child(pearl) + pearl.global_position = pos + if pearl.has_signal("collected"): + pearl.collected.connect(_on_pearl_collected) + _pearls.append(pearl) + + +func _random_spawn_pos() -> Vector3: + var player_pos: Vector3 = _player_node.global_position + var angle: float = randf_range(0.0, TAU) + var dist: float = randf_range(12.0, spawn_radius) + # Pearls float just above the seabed in the shallow-to-mid zone. + var depth: float = randf_range(-22.0, 0.0) + return Vector3( + player_pos.x + cos(angle) * dist, + depth, + player_pos.z + sin(angle) * dist + ) + + +func _is_valid_spawn(pos: Vector3) -> bool: + if _chunk_manager == null: + return true + if not _chunk_manager.has_method("get_block"): + return true + var block_id: int = _chunk_manager.call("get_block", pos) + # Avoid spawning inside solid blocks (ids 2..9) + if block_id >= 2 and block_id <= 9: + return false + return true + + +func _cull() -> void: + if not is_instance_valid(_player_node): + return + var ppos: Vector3 = _player_node.global_position + var i: int = _pearls.size() - 1 + while i >= 0: + if not is_instance_valid(_pearls[i]): + _pearls.remove_at(i) + elif _pearls[i].global_position.distance_to(ppos) > despawn_radius: + _pearls[i].queue_free() + _pearls.remove_at(i) + i -= 1 + + +func _on_pearl_collected(item_id: int) -> void: + var main: Node = get_tree().get_first_node_in_group("main") + if main == null or not ("inventory" in main): + return + main.inventory.add_item(item_id, 1) + var am: Node = get_node_or_null("/root/AudioManager") + if am != null and am.has_method("play_bubble_sfx") and is_instance_valid(_player_node): + am.call("play_bubble_sfx", _player_node.global_position)