From d8e19932cc537c4013755c97eb3ab5f75bfc335b Mon Sep 17 00:00:00 2001 From: Floppyrj45 Date: Sun, 19 Apr 2026 18:12:37 +0200 Subject: [PATCH] feat(biome): abyssal bioluminescent + glow blocks + abyssal jellyfish + particles - BlockDatabase: GLOW_CORAL_CYAN(10), GLOW_CORAL_VIOLET(11), LAVA_VENT(12) with emission data - WorldGenerator: abyssal blocks at y<-40 (cyan 4%, violet 1%, vent 0.5% via 3D noise) - Chunk.gd: multi-surface mesh (solid + 3 emissive surfaces with StandardMaterial3D emission) - MobSpawner: abyssal jellyfish spawn when player y<-30, max 8 - AbyssalJellyfish: cyan fluo, slower, double damage (10), bioluminescent flash on attack - BioluminescentParticles: 200 cyan-green particles follow player, only emit in abyss Co-Authored-By: Claude Sonnet 4.6 --- scenes/mobs/AbyssalJellyfish.tscn | 11 ++ scripts/ambience/BioluminescentParticles.gd | 88 +++++++++++++++ scripts/mobs/AbyssalJellyfish.gd | 81 ++++++++++++++ scripts/mobs/MobSpawner.gd | 28 +++++ scripts/world/Chunk.gd | 118 +++++++++++++++----- scripts/world/WorldGenerator.gd | 26 ++++- 6 files changed, 324 insertions(+), 28 deletions(-) create mode 100644 scenes/mobs/AbyssalJellyfish.tscn create mode 100644 scripts/ambience/BioluminescentParticles.gd create mode 100644 scripts/mobs/AbyssalJellyfish.gd diff --git a/scenes/mobs/AbyssalJellyfish.tscn b/scenes/mobs/AbyssalJellyfish.tscn new file mode 100644 index 0000000..57bfac3 --- /dev/null +++ b/scenes/mobs/AbyssalJellyfish.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=2 format=3 uid="uid://abyssaljellyfish001"] + +[ext_resource type="Script" path="res://scripts/mobs/AbyssalJellyfish.gd" id="1_abyssaljelly"] + +[node name="AbyssalJellyfish" type="CharacterBody3D"] +script = ExtResource("1_abyssaljelly") +max_health = 25.0 +damage = 10.0 +detection_radius = 8.0 +attack_radius = 1.5 +move_speed = 0.6 diff --git a/scripts/ambience/BioluminescentParticles.gd b/scripts/ambience/BioluminescentParticles.gd new file mode 100644 index 0000000..caf5474 --- /dev/null +++ b/scripts/ambience/BioluminescentParticles.gd @@ -0,0 +1,88 @@ +extends GPUParticles3D + +# Bioluminescent floating particles — only active in abyss zone (player y < -30) + +const ABYSS_DEPTH_THRESHOLD: float = -30.0 + +var _player: Node3D = null + + +func _ready() -> void: + amount = 200 + lifetime = 12.0 + one_shot = false + explosiveness = 0.0 + randomness = 1.0 + visibility_aabb = AABB(Vector3(-30, -25, -30), Vector3(60, 50, 60)) + + var process_mat := ParticleProcessMaterial.new() + process_mat.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_BOX + process_mat.emission_box_extents = Vector3(28.0, 20.0, 28.0) + + process_mat.direction = Vector3(0.0, 1.0, 0.0) + process_mat.spread = 120.0 + process_mat.initial_velocity_min = 0.0 + process_mat.initial_velocity_max = 0.08 + + process_mat.linear_accel_min = -0.03 + process_mat.linear_accel_max = 0.03 + process_mat.radial_accel_min = -0.02 + process_mat.radial_accel_max = 0.02 + + process_mat.damping_min = 0.4 + process_mat.damping_max = 0.9 + + process_mat.scale_min = 0.03 + process_mat.scale_max = 0.12 + + # Cyan-green bioluminescent color + var grad := Gradient.new() + grad.set_color(0, Color(0.0, 0.8, 0.9, 0.0)) + grad.set_color(1, Color(0.0, 0.8, 0.9, 0.0)) + grad.add_point(0.1, Color(0.1, 0.9, 0.8, 0.7)) + grad.add_point(0.85, Color(0.2, 0.7, 1.0, 0.6)) + var grad_tex := GradientTexture1D.new() + grad_tex.gradient = grad + process_mat.color_ramp = grad_tex + process_mat.color = Color(0.1, 0.9, 0.8, 0.7) + + process_material = process_mat + + var quad := QuadMesh.new() + quad.size = Vector2(0.08, 0.08) + + var mat := StandardMaterial3D.new() + mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED + mat.emission_enabled = true + mat.emission = Color(0.2, 0.8, 1.0) + mat.emission_energy_multiplier = 3.0 + mat.albedo_color = Color(0.1, 0.9, 0.8, 0.7) + + quad.surface_set_material(0, mat) + draw_pass_1 = quad + + # Start inactive until we detect depth + emitting = false + _find_player() + + +func _find_player() -> void: + var players := get_tree().get_nodes_in_group("player") + if players.size() > 0: + _player = players[0] as Node3D + + +func _process(_delta: float) -> void: + if _player == null: + _find_player() + return + + var in_abyss: bool = _player.global_position.y < ABYSS_DEPTH_THRESHOLD + if in_abyss != emitting: + emitting = in_abyss + + # Follow player horizontally, float at player depth + if in_abyss: + global_position = _player.global_position diff --git a/scripts/mobs/AbyssalJellyfish.gd b/scripts/mobs/AbyssalJellyfish.gd new file mode 100644 index 0000000..c79d2de --- /dev/null +++ b/scripts/mobs/AbyssalJellyfish.gd @@ -0,0 +1,81 @@ +extends Jellyfish + +# Abyssal variant: slower, packs double damage, bioluminescent cyan flash on contact + +func _build_visuals() -> void: + # Cyan emissive dome material + var mat := StandardMaterial3D.new() + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + mat.albedo_color = Color(0.1, 0.9, 1.0, 0.6) + mat.emission_enabled = true + mat.emission = Color(0.2, 0.8, 1.0) + mat.emission_energy_multiplier = 2.5 + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + + _dome = MeshInstance3D.new() + var sphere := SphereMesh.new() + sphere.radius = 0.55 + sphere.height = 0.75 + _dome.mesh = sphere + _dome.material_override = mat + _dome.scale = Vector3(1.0, 0.7, 1.0) + add_child(_dome) + + # Thin luminous tentacles + var tent_mat := StandardMaterial3D.new() + tent_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + tent_mat.albedo_color = Color(0.1, 0.9, 1.0, 0.4) + tent_mat.emission_enabled = true + tent_mat.emission = Color(0.2, 0.8, 1.0) + tent_mat.emission_energy_multiplier = 2.0 + tent_mat.cull_mode = BaseMaterial3D.CULL_DISABLED + + var offsets: Array[Vector2] = [ + Vector2(0.25, 0.0), Vector2(-0.25, 0.0), + Vector2(0.0, 0.25), Vector2(0.0, -0.25), + Vector2(0.18, 0.18), Vector2(-0.18, -0.18) + ] + for off: Vector2 in offsets: + var tent := MeshInstance3D.new() + var cap := CapsuleMesh.new() + cap.radius = 0.02 + cap.height = 0.9 + tent.mesh = cap + tent.material_override = tent_mat + tent.position = Vector3(off.x, -0.65, off.y) + add_child(tent) + _tentacles.append(tent) + + # Collision + var col := CollisionShape3D.new() + var cshape := SphereShape3D.new() + cshape.radius = 0.6 + col.shape = cshape + add_child(col) + + +func _ready() -> void: + # Override stats before parent _ready to ensure double damage and slower speed + damage = 10.0 # double base jellyfish damage + move_speed = 0.6 # slower + detection_radius = 8.0 # wider detection in dark abyss + super._ready() + + +func _on_attack() -> void: + # Bioluminescent flash: briefly boost emission on dome + if _dome != null and _dome.material_override != null: + var mat := _dome.material_override as StandardMaterial3D + if mat != null: + mat.emission_energy_multiplier = 8.0 + await get_tree().create_timer(0.15).timeout + if is_instance_valid(self) and _dome != null and _dome.material_override != null: + mat.emission_energy_multiplier = 2.5 + + +func _physics_process(delta: float) -> void: + var was_attacking: bool = (_state == State.ATTACK) + super._physics_process(delta) + # Trigger flash when we just entered attack + if not was_attacking and _state == State.ATTACK: + _on_attack() diff --git a/scripts/mobs/MobSpawner.gd b/scripts/mobs/MobSpawner.gd index cceb71d..3fd8ed8 100644 --- a/scripts/mobs/MobSpawner.gd +++ b/scripts/mobs/MobSpawner.gd @@ -6,10 +6,12 @@ extends Node3D @export var max_fish_schools: int = 3 @export var max_jellyfish: int = 5 @export var max_sharks: int = 1 +@export var abyssal_jellyfish_max: int = 8 const FISH_SCHOOL_SCENE := "res://scenes/mobs/FishSchool.tscn" const JELLYFISH_SCENE := "res://scenes/mobs/Jellyfish.tscn" const SHARK_SCENE := "res://scenes/mobs/Shark.tscn" +const ABYSSAL_JELLYFISH_SCENE := "res://scenes/mobs/AbyssalJellyfish.tscn" var _player_node: CharacterBody3D = null var _chunk_manager: Node = null @@ -19,6 +21,7 @@ const SPAWN_INTERVAL: float = 5.0 var _fish_schools: Array[Node3D] = [] var _jellyfish_list: Array[Node3D] = [] var _sharks: Array[Node3D] = [] +var _abyssal_jellyfish_list: Array[Node3D] = [] func _ready() -> void: @@ -67,6 +70,7 @@ func _tick_spawner() -> void: _cull_array(_fish_schools, player_pos) _cull_array(_jellyfish_list, player_pos) _cull_array(_sharks, player_pos) + _cull_array(_abyssal_jellyfish_list, player_pos) # --- Spawn fish schools --- while _fish_schools.size() < max_fish_schools: @@ -100,6 +104,18 @@ func _tick_spawner() -> void: else: break + # --- Spawn abyssal jellyfish only in deep zone --- + if player_pos.y < -30.0: + while _abyssal_jellyfish_list.size() < abyssal_jellyfish_max: + var pos: Vector3 = _random_abyss_spawn_pos(player_pos) + if _is_valid_spawn(pos): + var inst: Node3D = _load_and_instantiate(ABYSSAL_JELLYFISH_SCENE, pos) + if inst != null: + _connect_mob_signal(inst) + _abyssal_jellyfish_list.append(inst) + else: + break + func _cull_array(arr: Array[Node3D], player_pos: Vector3) -> void: var i: int = arr.size() - 1 @@ -123,6 +139,18 @@ func _random_spawn_pos(player_pos: Vector3) -> Vector3: ) +func _random_abyss_spawn_pos(player_pos: Vector3) -> Vector3: + var angle: float = randf_range(0.0, TAU) + var dist: float = randf_range(10.0, spawn_radius) + var depth: float = randf_range(player_pos.y - 15.0, player_pos.y + 5.0) + depth = min(depth, -30.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 diff --git a/scripts/world/Chunk.gd b/scripts/world/Chunk.gd index a286442..8947b4b 100644 --- a/scripts/world/Chunk.gd +++ b/scripts/world/Chunk.gd @@ -44,13 +44,25 @@ func generate_mesh() -> void: _init_material() + var tile_w: float = BlockDatabase.ATLAS_TILE_SIZE.x + var tile_h: float = BlockDatabase.ATLAS_TILE_SIZE.y + + # Surface 0: solid blocks (atlas texture) var st: SurfaceTool = SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) - var has_faces: bool = false + # Emissive surfaces: cyan (1), violet (2), vent (3) + var st_cyan: SurfaceTool = SurfaceTool.new() + st_cyan.begin(Mesh.PRIMITIVE_TRIANGLES) + var st_violet: SurfaceTool = SurfaceTool.new() + st_violet.begin(Mesh.PRIMITIVE_TRIANGLES) + var st_vent: SurfaceTool = SurfaceTool.new() + st_vent.begin(Mesh.PRIMITIVE_TRIANGLES) - var tile_w: float = BlockDatabase.ATLAS_TILE_SIZE.x - var tile_h: float = BlockDatabase.ATLAS_TILE_SIZE.y + var has_faces: bool = false + var has_cyan: bool = false + var has_violet: bool = false + var has_vent: bool = false for x: int in range(CHUNK_SIZE): for y: int in range(CHUNK_SIZE): @@ -61,35 +73,80 @@ func generate_mesh() -> void: var uv_tl: Vector2 = BlockDatabase.get_atlas_uv(block_id) - if not _is_opaque_at(x, y + 1, z): - _add_face_top(st, x, y, z, uv_tl, tile_w, tile_h) - has_faces = true - if not _is_opaque_at(x, y - 1, z): - _add_face_bottom(st, x, y, z, uv_tl, tile_w, tile_h) - has_faces = true - if not _is_opaque_at(x + 1, y, z): - _add_face_right(st, x, y, z, uv_tl, tile_w, tile_h) - has_faces = true - if not _is_opaque_at(x - 1, y, z): - _add_face_left(st, x, y, z, uv_tl, tile_w, tile_h) - has_faces = true - if not _is_opaque_at(x, y, z + 1): - _add_face_front(st, x, y, z, uv_tl, tile_w, tile_h) - has_faces = true - if not _is_opaque_at(x, y, z - 1): - _add_face_back(st, x, y, z, uv_tl, tile_w, tile_h) - has_faces = true + var target_st: SurfaceTool = st + var emit_type: int = 0 # 0=none, 1=cyan, 2=violet, 3=vent + if block_id == BlockDatabase.BlockType.GLOW_CORAL_CYAN: + target_st = st_cyan + emit_type = 1 + elif block_id == BlockDatabase.BlockType.GLOW_CORAL_VIOLET: + target_st = st_violet + emit_type = 2 + elif block_id == BlockDatabase.BlockType.LAVA_VENT: + target_st = st_vent + emit_type = 3 - if not has_faces: + var added: bool = false + if not _is_opaque_at(x, y + 1, z): + _add_face_top(target_st, x, y, z, uv_tl, tile_w, tile_h) + added = true + if not _is_opaque_at(x, y - 1, z): + _add_face_bottom(target_st, x, y, z, uv_tl, tile_w, tile_h) + added = true + if not _is_opaque_at(x + 1, y, z): + _add_face_right(target_st, x, y, z, uv_tl, tile_w, tile_h) + added = true + if not _is_opaque_at(x - 1, y, z): + _add_face_left(target_st, x, y, z, uv_tl, tile_w, tile_h) + added = true + if not _is_opaque_at(x, y, z + 1): + _add_face_front(target_st, x, y, z, uv_tl, tile_w, tile_h) + added = true + if not _is_opaque_at(x, y, z - 1): + _add_face_back(target_st, x, y, z, uv_tl, tile_w, tile_h) + added = true + + if added: + match emit_type: + 0: has_faces = true + 1: has_cyan = true + 2: has_violet = true + 3: has_vent = true + + var mesh: ArrayMesh = ArrayMesh.new() + + if has_faces: + st.generate_normals() + mesh = st.commit() # surface 0 — will apply material via surface_set_material below + + if has_cyan: + st_cyan.generate_normals() + st_cyan.commit(mesh) + if has_violet: + st_violet.generate_normals() + st_violet.commit(mesh) + if has_vent: + st_vent.generate_normals() + st_vent.commit(mesh) + + if mesh.get_surface_count() == 0: return - st.generate_normals() - - var mesh: ArrayMesh = st.commit() + # Assign materials per surface + var surf_idx: int = 0 + if has_faces: + mesh.surface_set_material(surf_idx, shared_material) + surf_idx += 1 + if has_cyan: + mesh.surface_set_material(surf_idx, _make_emissive_mat(Color(0.2, 0.8, 1.0), 3.0)) + surf_idx += 1 + if has_violet: + mesh.surface_set_material(surf_idx, _make_emissive_mat(Color(0.7, 0.3, 0.9), 3.0)) + surf_idx += 1 + if has_vent: + mesh.surface_set_material(surf_idx, _make_emissive_mat(Color(1.0, 0.5, 0.1), 3.0)) _mesh_instance = MeshInstance3D.new() _mesh_instance.mesh = mesh - _mesh_instance.material_override = shared_material add_child(_mesh_instance) var shape: ConcavePolygonShape3D = ConcavePolygonShape3D.new() @@ -102,6 +159,15 @@ func generate_mesh() -> void: _static_body.add_child(col_shape) add_child(_static_body) + +func _make_emissive_mat(emission_color: Color, energy: float) -> StandardMaterial3D: + var mat: StandardMaterial3D = StandardMaterial3D.new() + mat.albedo_color = emission_color + mat.emission_enabled = true + mat.emission = emission_color + mat.emission_energy_multiplier = energy + return mat + func _is_opaque_at(x: int, y: int, z: int) -> bool: if x < 0 or x >= CHUNK_SIZE or y < 0 or y >= CHUNK_SIZE or z < 0 or z >= CHUNK_SIZE: return false diff --git a/scripts/world/WorldGenerator.gd b/scripts/world/WorldGenerator.gd index 10cdc20..925701c 100644 --- a/scripts/world/WorldGenerator.gd +++ b/scripts/world/WorldGenerator.gd @@ -26,6 +26,11 @@ static func generate_chunk(chunk_x: int, chunk_y: int, chunk_z: int, seed: int) noise_wreck.noise_type = FastNoiseLite.TYPE_CELLULAR noise_wreck.frequency = 0.03 + var noise_glow: FastNoiseLite = FastNoiseLite.new() + noise_glow.seed = seed + 7777 + noise_glow.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH + noise_glow.frequency = 0.08 + for lx: int in range(CHUNK_SIZE): for lz: int in range(CHUNK_SIZE): var wx: int = chunk_x * CHUNK_SIZE + lx @@ -45,7 +50,7 @@ static func generate_chunk(chunk_x: int, chunk_y: int, chunk_z: int, seed: int) var idx: int = lx + ly * CHUNK_SIZE + lz * 256 data[idx] = _get_block_at(wy, ground_height, biome, wx, wz, lx, lz, - is_wreck_center, noise_detail, seed) + is_wreck_center, noise_detail, noise_glow, seed) return data @@ -61,11 +66,28 @@ static func _get_biome(biome_val: float, ground_height: int) -> int: static func _get_block_at(wy: int, ground_height: int, biome: int, wx: int, wz: int, lx: int, lz: int, - is_wreck_center: bool, noise_detail: FastNoiseLite, seed: int) -> int: + is_wreck_center: bool, noise_detail: FastNoiseLite, noise_glow: FastNoiseLite, seed: int) -> int: if wy <= -60: return BlockDatabase.BlockType.BEDROCK + # --- Abyssal bioluminescent decorations (deep zone) --- + if ground_height < -30 and wy >= ground_height and wy < ground_height + 3: + if wy < -40: + var glow_val: float = noise_glow.get_noise_3d(float(wx), float(wy), float(wz)) + # GLOW_CORAL_CYAN patches — 4% density on surfaces + if glow_val > 0.65: + return BlockDatabase.BlockType.GLOW_CORAL_CYAN + # GLOW_CORAL_VIOLET rare columns — 1% + var violet_val: float = noise_glow.get_noise_3d(float(wx) * 1.3, float(wy) * 0.5, float(wz) * 1.3) + if violet_val > 0.82: + return BlockDatabase.BlockType.GLOW_CORAL_VIOLET + # LAVA_VENT columns — 0.5% density, very deep + if wy < -50 and wy < ground_height + 3: + var vent_val: float = noise_glow.get_noise_3d(float(wx) * 0.5, float(wy) * 2.0, float(wz) * 0.5) + if vent_val > 0.88: + return BlockDatabase.BlockType.LAVA_VENT + if wy < ground_height - 5: return BlockDatabase.BlockType.ROCK