Files
dauphincraft/scripts/mobs/MobSpawner.gd

191 lines
5.4 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):
# Use variadic-safe connect: bind by checking signal params
mob.attacked_player.connect(_on_mob_attacked_player)
func _on_mob_attacked_player(dmg: float, kb_dir: Vector3 = Vector3.ZERO) -> 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