- 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>
190 lines
5.3 KiB
GDScript
190 lines
5.3 KiB
GDScript
extends Node3D
|
|
|
|
@export var player: NodePath = NodePath("")
|
|
@export var spawn_radius: float = 25.0
|
|
@export var despawn_radius: float = 40.0
|
|
@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
|
|
var _timer: float = 0.0
|
|
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:
|
|
_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 as CharacterBody3D
|
|
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:
|
|
# Try common path
|
|
var cm: Node = get_node_or_null("/root/Main/World/ChunkManager")
|
|
if cm != null:
|
|
_chunk_manager = cm
|
|
return
|
|
# Fallback: search by class name
|
|
var candidates := get_tree().get_nodes_in_group("chunk_manager")
|
|
if candidates.size() > 0:
|
|
_chunk_manager = candidates[0]
|
|
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
_timer += delta
|
|
if _timer >= SPAWN_INTERVAL:
|
|
_timer = 0.0
|
|
_resolve_player()
|
|
_tick_spawner()
|
|
|
|
|
|
func _tick_spawner() -> void:
|
|
if not is_instance_valid(_player_node):
|
|
return
|
|
|
|
var player_pos: Vector3 = _player_node.global_position
|
|
|
|
# --- Despawn far mobs ---
|
|
_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:
|
|
var pos: Vector3 = _random_spawn_pos(player_pos)
|
|
if _is_valid_spawn(pos):
|
|
var inst: Node3D = _load_and_instantiate(FISH_SCHOOL_SCENE, pos)
|
|
if inst != null:
|
|
_fish_schools.append(inst)
|
|
else:
|
|
break
|
|
|
|
# --- Spawn jellyfish ---
|
|
while _jellyfish_list.size() < max_jellyfish:
|
|
var pos: Vector3 = _random_spawn_pos(player_pos)
|
|
if _is_valid_spawn(pos):
|
|
var inst: Node3D = _load_and_instantiate(JELLYFISH_SCENE, pos)
|
|
if inst != null:
|
|
_connect_mob_signal(inst)
|
|
_jellyfish_list.append(inst)
|
|
else:
|
|
break
|
|
|
|
# --- Spawn sharks ---
|
|
while _sharks.size() < max_sharks:
|
|
var pos: Vector3 = _random_spawn_pos(player_pos)
|
|
if _is_valid_spawn(pos):
|
|
var inst: Node3D = _load_and_instantiate(SHARK_SCENE, pos)
|
|
if inst != null:
|
|
_connect_mob_signal(inst)
|
|
_sharks.append(inst)
|
|
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
|
|
while i >= 0:
|
|
if not is_instance_valid(arr[i]):
|
|
arr.remove_at(i)
|
|
elif arr[i].global_position.distance_to(player_pos) > despawn_radius:
|
|
arr[i].queue_free()
|
|
arr.remove_at(i)
|
|
i -= 1
|
|
|
|
|
|
func _random_spawn_pos(player_pos: Vector3) -> Vector3:
|
|
var angle: float = randf_range(0.0, TAU)
|
|
var dist: float = randf_range(15.0, spawn_radius)
|
|
var depth: float = randf_range(-40.0, 50.0)
|
|
return Vector3(
|
|
player_pos.x + cos(angle) * dist,
|
|
depth,
|
|
player_pos.z + sin(angle) * dist
|
|
)
|
|
|
|
|
|
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
|
|
if not _chunk_manager.has_method("get_block"):
|
|
return true
|
|
var block_id: int = _chunk_manager.call("get_block", pos)
|
|
# Block IDs 2..8 are solid — don't spawn inside solid blocks
|
|
if block_id >= 2 and block_id <= 8:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _load_and_instantiate(scene_path: String, pos: Vector3) -> Node3D:
|
|
var packed: PackedScene = load(scene_path)
|
|
if packed == null:
|
|
return null
|
|
var inst: Node3D = packed.instantiate() as Node3D
|
|
if inst == null:
|
|
return null
|
|
get_tree().current_scene.add_child(inst)
|
|
inst.global_position = pos
|
|
return inst
|
|
|
|
|
|
func _connect_mob_signal(mob: Node3D) -> void:
|
|
if mob.has_signal("attacked_player"):
|
|
if not mob.is_connected("attacked_player", _on_mob_attacked_player):
|
|
mob.attacked_player.connect(_on_mob_attacked_player)
|
|
|
|
|
|
func _on_mob_attacked_player(dmg: float) -> void:
|
|
if is_instance_valid(_player_node):
|
|
if _player_node.has_method("take_damage"):
|
|
_player_node.take_damage(dmg)
|
|
elif "health" in _player_node:
|
|
_player_node.health -= dmg
|