studio/src/editor/engine/ZombieSpawnerManager.js
МИН 31adbf151b Initial public release: Студия Рублокса v1.0
Open-source веб-студия для создания игр Рублокса, двойная лицензия
AGPL-3.0 + Коммерческая.

Главное:
- Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16
- Самодостаточный движок ~28к строк (66 файлов): BlockManager,
  TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController,
  ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов
- Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco)
- Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn)
- Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt)
- 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.)
- Конфигурируемый бэкенд через VITE_API_BASE — работает со staging
  (dev-api.rublox.pro) без настройки
- Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка
- Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING,
  SECURITY, CHANGELOG
- ESLint + Prettier + EditorConfig
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Перед публикацией:
- Все импорты из minecraftia заменены на локальные
- Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env
- Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо)
- AdminKubikonModeration не публикуется (модерация — в team.rublox.pro)
- 93 МБ ассетов public/kubikon-assets вынесены в .gitignore
  (раздаются через release artifact)
2026-05-27 23:41:10 +03:00

162 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ZombieSpawnerManager — спавн зомби с лимитом и периодом.
*
* Визуал: вместо character-d показываем светящийся диск (цилиндр) с
* пульсацией. Сама character-d модель скрывается.
*/
import { MeshBuilder, StandardMaterial, Color3 } from '@babylonjs/core';
const DEFAULTS = {
// Снижено с 10 → 3: меньше живых зомби на спавнер = меньше нагрузки и
// меньше шанс нарваться на невидимых из-за бага высоты на GLB-террейне.
maxAlive: 3,
spawnInterval: 10,
radius: 6,
zombieModelType: 'character-c',
};
let _spawnerIdSeq = 1;
export class ZombieSpawnerManager {
constructor(scene3d, zombieManager) {
this.scene3d = scene3d;
this.zombieManager = zombieManager;
/** @type {Map<number, object>} id → spawner state */
this.spawners = new Map();
this._lastTick = performance.now() / 1000;
this._renderHook = null;
// Подписываемся на смерть зомби — уменьшаем счётчик у спавнера
this.zombieManager.setOnDeath((zombie) => this.onZombieDied(zombie));
}
start() {
if (this._renderHook) return;
this._renderHook = () => this._tick();
this.scene3d.scene.registerBeforeRender(this._renderHook);
this._lastTick = performance.now() / 1000;
}
stop() {
if (this._renderHook) {
this.scene3d.scene.unregisterBeforeRender(this._renderHook);
this._renderHook = null;
}
// Удаляем всех зомби, которые были СПАВНУТЫ нашими спавнерами
// (но не трогаем зомби размещённых вручную — они переживают Play→Stop).
for (const sp of this.spawners.values()) {
for (const instId of (sp.spawnedZombieIds || [])) {
this.scene3d.modelManager?.removeInstance(instId);
}
}
this.spawners.clear();
}
/** Колбэк когда зомби умер — обновляем счётчик спавнера. */
onZombieDied(zombie) {
const sid = zombie.spawnerId;
if (sid == null) return;
const sp = this.spawners.get(sid);
if (!sp) return;
sp.aliveZombies = Math.max(0, sp.aliveZombies - 1);
// Также убираем из списка spawnedZombieIds (чтобы не пытаться удалить уже мёртвого)
const idx = sp.spawnedZombieIds.indexOf(zombie.instanceId);
if (idx >= 0) sp.spawnedZombieIds.splice(idx, 1);
}
/**
* Зарегистрировать модель сцены как спавнер.
* modelInstanceId — id из ModelManager.instances.
*/
register(modelInstanceId, options = {}) {
const data = this.scene3d.modelManager?.instances?.get(modelInstanceId);
if (!data) return null;
const id = _spawnerIdSeq++;
const opts = { ...DEFAULTS, ...options };
this.spawners.set(id, {
id,
instanceId: modelInstanceId,
data,
opts,
spawnedZombieIds: [], // ids зомби которых мы создали — для cleanup при stop
aliveZombies: 0,
lastSpawnTime: 0,
});
return id;
}
_tick() {
const now = performance.now() / 1000;
this._lastTick = now;
// Глобальный лимит зомби на тач-устройствах (low-perf): если суммарно
// живых зомби > MOBILE_TOTAL_LIMIT — не спавним больше. Это критично
// для мобилки на «зомби-острове»: иначе FPS просаживается до 5-10.
const lowPerf = !!this.scene3d?._lowPerfApplied;
const MOBILE_TOTAL_LIMIT = 6;
if (lowPerf) {
const total = this.zombieManager?.zombies?.size || 0;
if (total >= MOBILE_TOTAL_LIMIT) return;
}
for (const sp of this.spawners.values()) {
// На мобиле каждый спавнер ограничиваем 3 зомби (вне зависимости от opts).
const cap = lowPerf ? Math.min(3, sp.opts.maxAlive) : sp.opts.maxAlive;
if (sp.aliveZombies >= cap) continue;
if (now - sp.lastSpawnTime < sp.opts.spawnInterval) continue;
sp.lastSpawnTime = now;
this._spawnAt(sp);
}
}
async _spawnAt(sp) {
const { x, z } = sp.data;
const a = Math.random() * Math.PI * 2;
const r = Math.random() * sp.opts.radius;
let sx = x + Math.cos(a) * r;
let sz = z + Math.sin(a) * r;
// Не спавнить зомби за пределами карты (worldHalf по X и Z, отступ 1.5м).
const half = (this.scene3d?._worldHalf ?? 40) - 1.5;
if (sx > half) sx = half;
if (sx < -half) sx = -half;
if (sz > half) sz = half;
if (sz < -half) sz = -half;
// Y берём из ПОВЕРХНОСТИ под (sx, sz), а не из y спавнера. Иначе зомби
// спавнится на y=0 (под рельефом), его видимый меш под полом и
// невидимый хитбокс наносит урон сквозь землю.
const sy = this._surfaceHeightAt(sx, sz);
const mm = this.scene3d.modelManager;
if (!mm) return;
const modelType = sp.opts.zombieModelType || 'zombie';
const instId = await mm.addInstance(modelType, sx, sy, sz, Math.random() * Math.PI * 2);
if (instId == null) return;
// Помечаем data модели чтобы при сериализации НЕ сохранять этих зомби
const data = mm.instances.get(instId);
if (data) data._spawnedAtRuntime = true;
const zombieParams = {
spawnerId: sp.id,
hp: sp.opts.zombieHp ?? 50,
speed: sp.opts.zombieSpeed ?? 2.2,
attackDamage: sp.opts.zombieDamage ?? 10,
};
this.zombieManager.registerExisting(instId, zombieParams);
sp.aliveZombies += 1;
sp.spawnedZombieIds.push(instId);
// Новый меш в сцене — frozen active-meshes устарел
try { this.scene3d?.setActiveMeshesDirty?.(); } catch (e) {}
}
/** Вернуть y-поверхности (top самого высокого блока) под (x, z).
* Без блоков — fallback 0. Та же логика что в ZombieManager и worker'е. */
_surfaceHeightAt(x, z) {
const bm = this.scene3d?.blockManager;
if (!bm || !bm.blocks) return 0;
const gx = Math.round(x);
const gz = Math.round(z);
for (let y = 20; y >= -3; y--) {
const key = `${gx},${y},${gz}`;
const mesh = bm.blocks.get(key);
if (mesh && !mesh.metadata?.isWater) return y + 1;
}
return 0;
}
}