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 <noreply@anthropic.com>
This commit is contained in:
81
scripts/mobs/AbyssalJellyfish.gd
Normal file
81
scripts/mobs/AbyssalJellyfish.gd
Normal file
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user