HollowBond/Public/ecma/Managers/DungeonManager.ecma.js

704 lines
29 KiB
JavaScript

"use strict";
const DungeonManager = (function(){
const DungeonManager = function(hollow_bond){
const self = this;
let debug_hud = null; // <-- Referencia al HUD
this.scene = null;
this.camera = null;
this.renderer = null;
this.on = false;
this.floor_height = hollow_bond.room_height || 3;
// Variables de streaming por topología
this.current_room = null;
this.current_chunk = { x: 0, y: 0, z: 0 };
// Parámetros de niveles de salas (con fallback a 3 y 5)
hollow_bond.horizontal_loaded_rooms_levels = (hollow_bond.horizontal_loaded_rooms_levels !== undefined)
? hollow_bond.horizontal_loaded_rooms_levels
: 3;
hollow_bond.horizontal_unloaded_rooms_level = (hollow_bond.horizontal_unloaded_rooms_level !== undefined)
? hollow_bond.horizontal_unloaded_rooms_level
: 5;
// Gestores de geometrías y materiales
this.geometries = {
room : (typeof RoomGeometryGame !== "undefined") ? new RoomGeometryGame(hollow_bond) : null,
frame : (typeof FrameGeometryGame !== "undefined") ? new FrameGeometryGame(hollow_bond) : null,
hole : (typeof HoleGeometryGame !== "undefined") ? new HoleGeometryGame(hollow_bond) : null,
stair : (typeof StairGeometryGame !== "undefined") ? new StairGeometryGame(hollow_bond) : null,
elevator : (typeof ElevatorGeometryGame !== "undefined") ? new ElevatorGeometryGame(hollow_bond) : null,
railing : (typeof RailingGeometryGame !== "undefined") ? new RailingGeometryGame(hollow_bond) : null,
elevator_frame : (typeof ElevatorFrameGeometryGame !== "undefined") ? new ElevatorFrameGeometryGame(hollow_bond) : null
};
this.materials = {
room : (typeof RoomMaterialGame !== "undefined") ? new RoomMaterialGame(hollow_bond) : null,
frame : (typeof FrameMaterialGame !== "undefined") ? new FrameMaterialGame(hollow_bond) : null,
hole : (typeof HoleMaterialGame !== "undefined") ? new HoleMaterialGame(hollow_bond) : null,
stair : (typeof StairMaterialGame !== "undefined") ? new StairMaterialGame(hollow_bond) : null,
elevator : (typeof ElevatorMaterialGame !== "undefined") ? new ElevatorMaterialGame(hollow_bond) : null,
railing : (typeof RailingMaterialGame !== "undefined") ? new RailingMaterialGame(hollow_bond) : null,
};
// Diccionarios de objetos visuales actualmente instanciados en escena
this.objects = {
room : {},
frame : {},
hole : {},
stair : {},
elevator : {},
railing : {}
};
let last_frame_time = performance.now();
let animation_frame_id = null;
/**
* Inicializa la partida en primera persona con la altura seleccionada.
* @param {number} player_height
*/
this.start = (player_height = 1.75) => {
self.on = true;
setup_scene();
debug_hud = new DungeonHudView(hollow_bond, self);
if(hollow_bond.controls && hollow_bond.controls.game){
hollow_bond.controls.game.set_dimensions(player_height);
hollow_bond.controls.game.setup(self);
}
self.current_chunk = { x: 0, y: 0, z: 0 };
// Generamos el chunk inicial y sus vecinos en MapManager
hollow_bond.map.update_active_center(0, 0, 0, () => {
let spawn_room_model = null;
const center_chunk = hollow_bond.map.get_chunk(0, 0, 0);
if(center_chunk && center_chunk.rooms){
for(const id in center_chunk.rooms){
const r = center_chunk.rooms[id];
// Priorizamos sala segura sin agujeros de caída
if((!r.holes || r.holes.length === 0) && (!r.voids || r.voids.length === 0)){
spawn_room_model = r;
break;
}
}
if(!spawn_room_model){
const keys = Object.keys(center_chunk.rooms);
if(keys.length > 0) spawn_room_model = center_chunk.rooms[keys[0]];
}
}
if(spawn_room_model){
// 1. Instanciamos la sala inicial de aparición
const room_obj = new RoomObjectGame(
hollow_bond,
spawn_room_model,
self.geometries.room,
self.materials.room
);
self.objects.room[spawn_room_model.id] = room_obj;
self.scene.add(room_obj.mesh);
self.current_room = room_obj;
// 2. Ejecutamos el streaming para cargar las adyacentes hasta nivel 3
self.update_rooms_streaming();
// 3. Ubicamos al jugador en el centro de la sala
const spawn_x = spawn_room_model.x + spawn_room_model.width / 2;
const spawn_z = spawn_room_model.z + spawn_room_model.depth / 2;
const floor_y = room_obj.collider.get_floor_height();
const eye_height = hollow_bond.controls.game.standing_height;
self.camera.position.set(spawn_x, floor_y + eye_height, spawn_z);
}
if(hollow_bond.show_fps && hollow_bond.views && hollow_bond.views.fps){
hollow_bond.views.fps.go();
}
last_frame_time = performance.now();
render_loop();
});
};
/**
* Comprueba en qué sala cargada se encuentra el jugador.
*/
const check_room_transition = () => {
if(!self.current_room || !self.camera) return;
const pos = self.camera.position;
const eye_height = (hollow_bond.controls && hollow_bond.controls.game)
? hollow_bond.controls.game.current_height
: 1.75;
const feet_y = pos.y - eye_height;
// Si sigue dentro de la sala activa en 3D, no hacemos nada
if(self.current_room.collider.contains(pos.x, feet_y, pos.z)){
return;
}
// Buscamos sala candidata filtrando estrictamente por cota vertical primero
for(const rid in self.objects.room){
const room_obj = self.objects.room[rid];
if(room_obj === self.current_room) continue;
const room_floor_y = room_obj.collider.get_floor_height();
// Si la sala está en otro piso (a más de 1 metro de distancia vertical de los pies), SE IGNORA
if(Math.abs(feet_y - room_floor_y) > 1.0) continue;
if(room_obj.collider.contains(pos.x, feet_y, pos.z)){
self.set_current_room(room_obj);
break;
}
}
};
/**
* Asigna la nueva sala activa y actualiza el streaming y los Chunks si corresponde.
* @param {RoomObjectGame} room_obj
*/
this.set_current_room = room_obj => {
if(!room_obj || self.current_room === room_obj) return;
self.current_room = room_obj;
const chunk_size = hollow_bond.chunk_size || 16;
const chunk_height = (hollow_bond.chunk_height || (chunk_size * hollow_bond.room_height));
const cx = Math.floor((room_obj.model.x + room_obj.model.width / 2) / chunk_size);
const cz = Math.floor((room_obj.model.z + room_obj.model.depth / 2) / chunk_size);
const cy = Math.floor((room_obj.model.y + room_obj.model.height / 2) / chunk_height);
if(cx !== self.current_chunk.x || cy !== self.current_chunk.y || cz !== self.current_chunk.z){
self.current_chunk = { x: cx, y: cy, z: cz };
if(hollow_bond.map && hollow_bond.map.update_active_center){
hollow_bond.map.update_active_center(cx, cy, cz, () => {
self.update_rooms_streaming();
});
return;
}
}
self.update_rooms_streaming();
};
/**
* Algoritmo BFS para streaming de habitaciones y marcos por niveles de conectividad.
*/
this.update_rooms_streaming = () => {
if(!self.current_room || !hollow_bond.map) return;
const load_limit = hollow_bond.horizontal_loaded_rooms_levels || 3;
const unload_limit = hollow_bond.horizontal_unloaded_rooms_level || 5;
const queue = [{ id: self.current_room.model.id, depth: 0 }];
const room_depths = { [self.current_room.model.id]: 0 };
while(queue.length > 0){
const { id, depth } = queue.shift();
const room_model = hollow_bond.map.rooms ? hollow_bond.map.rooms[id] : null;
if(!room_model) continue;
// A) Expansión horizontal por MARCOS
if(room_model.frames){
for(const fid of room_model.frames){
const frame_model = hollow_bond.map.frames ? hollow_bond.map.frames[fid] : null;
if(!frame_model || !frame_model.rooms) continue;
for(const neighbor_id of frame_model.rooms){
if(neighbor_id && neighbor_id !== id && room_depths[neighbor_id] === undefined){
const next_depth = depth + 1;
room_depths[neighbor_id] = next_depth;
if(next_depth <= unload_limit) queue.push({ id: neighbor_id, depth: next_depth });
}
}
}
}
// B) Expansión vertical por HUECOS
if(room_model.holes){
for(const hid of room_model.holes){
const hole_model = hollow_bond.map.holes ? hollow_bond.map.holes[hid] : null;
if(!hole_model || !hole_model.rooms) continue;
for(const neighbor_id of hole_model.rooms){
if(neighbor_id && neighbor_id !== id && room_depths[neighbor_id] === undefined){
const next_depth = depth + 1;
room_depths[neighbor_id] = next_depth;
if(next_depth <= unload_limit) queue.push({ id: neighbor_id, depth: next_depth });
}
}
}
}
// C) Expansión vertical por ASCENSORES
if(room_model.elevators){
for(const eid of room_model.elevators){
const elev_model = hollow_bond.map.elevators ? hollow_bond.map.elevators[eid] : null;
if(!elev_model || !elev_model.rooms) continue;
for(const neighbor_id of elev_model.rooms){
if(neighbor_id && neighbor_id !== id && room_depths[neighbor_id] === undefined){
const next_depth = depth + 1;
room_depths[neighbor_id] = next_depth;
if(next_depth <= unload_limit) queue.push({ id: neighbor_id, depth: next_depth });
}
}
}
}
// D) Expansión vertical por ESCALERAS
if(room_model.stairs){
for(const sid of room_model.stairs){
const stair_model = hollow_bond.map.stairs ? hollow_bond.map.stairs[sid] : null;
if(!stair_model || !stair_model.rooms) continue;
for(const neighbor_id of stair_model.rooms){
if(neighbor_id && neighbor_id !== id && room_depths[neighbor_id] === undefined){
const next_depth = depth + 1;
room_depths[neighbor_id] = next_depth;
if(next_depth <= unload_limit) queue.push({ id: neighbor_id, depth: next_depth });
}
}
}
}
}
// 2. Instanciar salas dentro del rango de carga
for(const rid in room_depths){
const depth = room_depths[rid];
if(depth <= load_limit){
if(!self.objects.room[rid]){
const model = hollow_bond.map.rooms ? hollow_bond.map.rooms[rid] : null;
if(model){
const room_obj = new RoomObjectGame(
hollow_bond,
model,
self.geometries.room,
self.materials.room
);
self.objects.room[rid] = room_obj;
self.scene.add(room_obj.mesh);
}
}
}
}
// 3. Instanciar marcos de las salas cargadas
for(const rid in self.objects.room){
const r_obj = self.objects.room[rid];
if(r_obj.model && r_obj.model.frames){
for(const fid of r_obj.model.frames){
if(!self.objects.frame[fid]){
const frame_model = hollow_bond.map.frames ? hollow_bond.map.frames[fid] : null;
if(frame_model){
const frame_obj = new FrameObjectGame(
hollow_bond,
frame_model,
self.geometries.frame,
self.materials.frame
);
self.objects.frame[fid] = frame_obj;
self.scene.add(frame_obj.mesh);
}
}
}
}
}
// 4. Instanciar huecos de las salas cargadas
for(const rid in self.objects.room){
const r_obj = self.objects.room[rid];
if(r_obj.model && r_obj.model.holes){
for(const hid of r_obj.model.holes){
if(!self.objects.hole[hid]){
const hole_model = hollow_bond.map.holes ? hollow_bond.map.holes[hid] : null;
if(hole_model){
const hole_obj = new HoleObjectGame(
hollow_bond,
hole_model,
self.geometries.hole,
self.materials.hole
);
self.objects.hole[hid] = hole_obj;
self.scene.add(hole_obj.mesh);
}
}
}
}
}
// 5. Instanciar escaleras de los huecos / salas cargadas
for(const rid in self.objects.room){
const r_obj = self.objects.room[rid];
// Escaleras alojadas dentro de los huecos de la sala
if(r_obj.model && r_obj.model.holes){
for(const hid of r_obj.model.holes){
const hole_model = hollow_bond.map.holes ? hollow_bond.map.holes[hid] : null;
if(hole_model && hole_model.stairs){
for(const stair_model of hole_model.stairs){
if(!self.objects.stair[stair_model.id]){
const stair_obj = new StairObjectGame(
hollow_bond,
stair_model,
self.materials.stair,
self.geometries.stair
);
self.objects.stair[stair_model.id] = stair_obj;
self.scene.add(stair_obj.mesh);
}
}
}
}
}
// Escaleras registradas directamente en la sala
if(r_obj.model && r_obj.model.stairs){
for(const sid of r_obj.model.stairs){
if(!self.objects.stair[sid]){
const stair_model = hollow_bond.map.stairs ? hollow_bond.map.stairs[sid] : null;
if(stair_model){
const stair_obj = new StairObjectGame(
hollow_bond,
stair_model,
self.materials.stair,
self.geometries.stair
);
self.objects.stair[sid] = stair_obj;
self.scene.add(stair_obj.mesh);
}
}
}
}
}
// 5. Descargar salas fuera de rango
for(const rid in self.objects.room){
const depth = room_depths[rid];
if(depth === undefined || depth > unload_limit){
if(self.current_room && self.current_room.model.id === rid) continue;
const obj = self.objects.room[rid];
self.scene.remove(obj.mesh);
obj.destroy && obj.destroy();
delete self.objects.room[rid];
}
}
// 6. Descargar marcos huérfanos
for(const fid in self.objects.frame){
const f_obj = self.objects.frame[fid];
let is_needed = false;
if(f_obj.model && f_obj.model.rooms){
for(const rid of f_obj.model.rooms){
if(rid && self.objects.room[rid]){
is_needed = true;
break;
}
}
}
if(!is_needed){
self.scene.remove(f_obj.mesh);
f_obj.destroy && f_obj.destroy();
delete self.objects.frame[fid];
}
}
// 6. Instanciar ascensores de los chunks / mapa cargados
for(const rid in self.objects.room){
const r_obj = self.objects.room[rid];
if(r_obj.model && r_obj.model.elevators){
for(const eid of r_obj.model.elevators){
if(!self.objects.elevator[eid]){
const el_model = hollow_bond.map.elevators ? hollow_bond.map.elevators[eid] : null;
if(el_model){
const el_obj = new ElevatorObjectGame(
hollow_bond,
el_model,
self.materials.elevator,
self.geometries.elevator
);
self.objects.elevator[eid] = el_obj;
self.scene.add(el_obj.mesh);
}
}
}
}
}
// 7. Descargar huecos huérfanos
for(const hid in self.objects.hole){
const h_obj = self.objects.hole[hid];
let is_needed = false;
if(h_obj.model && h_obj.model.rooms){
for(const rid of h_obj.model.rooms){
if(rid && self.objects.room[rid]){
is_needed = true;
break;
}
}
}
if(!is_needed){
self.scene.remove(h_obj.mesh);
h_obj.destroy && h_obj.destroy();
delete self.objects.hole[hid];
}
}
// 8. Descargar escaleras huérfanas
for(const sid in self.objects.stair){
const s_obj = self.objects.stair[sid];
let is_needed = false;
if(s_obj.model){
// Si su foso sigue cargado en escena
if(s_obj.model.hole_id && self.objects.hole[s_obj.model.hole_id]){
is_needed = true;
} else if(s_obj.model.rooms){
for(const rid of s_obj.model.rooms){
if(rid && self.objects.room[rid]){
is_needed = true;
break;
}
}
}
}
if(!is_needed){
self.scene.remove(s_obj.mesh);
s_obj.destroy && s_obj.destroy();
delete self.objects.stair[sid];
}
}
// 9. Descargar ascensores huérfanos
for(const eid in self.objects.elevator){
const el_obj = self.objects.elevator[eid];
let is_needed = false;
for(const rid in self.objects.room){
const r_model = self.objects.room[rid].model;
if(r_model && r_model.elevators && r_model.elevators.includes(eid)){
is_needed = true;
break;
}
}
if(!is_needed){
self.scene.remove(el_obj.mesh);
el_obj.destroy && el_obj.destroy();
delete self.objects.elevator[eid];
}
}
};
/**
* Callback de integración con MapManager cuando se genera un Chunk.
*/
this.sync_chunk = chunk => {
if(self.on && self.current_room){
self.update_rooms_streaming();
}
};
/**
* Callback de integración con MapManager cuando se descarga un Chunk.
*/
this.remove_chunk = chunk => {
if(self.on && self.current_room){
self.update_rooms_streaming();
}
};
/**
* Detiene el bucle y limpia la sesión.
*/
this.stop = () => {
self.on = false;
if(animation_frame_id){
cancelAnimationFrame(animation_frame_id);
}
if(debug_hud){
debug_hud.destroy();
debug_hud = null;
}
self.reset();
};
/**
* Limpia todos los objetos 3D y buffers de GPU.
*/
this.reset = () => {
if(debug_hud){
debug_hud.destroy();
debug_hud = null;
}
for(const key in self.objects){
for(const id in self.objects[key]){
self.scene && self.scene.remove(self.objects[key][id].mesh);
self.objects[key][id].destroy && self.objects[key][id].destroy();
}
self.objects[key] = {};
}
for(const key in self.geometries){
if(self.geometries[key] && self.geometries[key].reset){
self.geometries[key].reset();
}
}
for(const key in self.materials){
if(self.materials[key] && self.materials[key].reset){
self.materials[key].reset();
}
}
};
const setup_scene = () => {
self.scene = new THREE.Scene();
self.scene.background = new THREE.Color(0x050505);
// Niebla volumétrica ligera acorde a los Backrooms
self.scene.fog = new THREE.FogExp2(0x050505, 0.035);
self.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
self.renderer = new THREE.WebGLRenderer({
antialias : hollow_bond.render_scale >= 100,
powerPreference : "high-performance"
});
self.renderer.domElement.className = "hb-canvas-dungeon";
self.renderer.domElement.tabIndex = 1;
hollow_bond.item_self.appendChild(self.renderer.domElement);
self.update_resolution();
const ambient = new THREE.AmbientLight(0xfff5e6, 0.45);
self.scene.add(ambient);
// window.addEventListener("keydown", e => {
// if(e.code === "KeyE" && self.camera){
// for(const eid in self.objects.elevator){
// if(self.objects.elevator[eid].interact(self.camera.position)){
// break;
// }
// }
// }
// });
// Interacción por click en el botón del ascensor
const raycaster = new THREE.Raycaster();
const center_screen = new THREE.Vector2(0, 0); // Centro exacto del punto de mira
window.addEventListener("mousedown", e => {
// Solo botón izquierdo del ratón (button === 0)
if(e.button !== 0 || !self.camera || !self.on) return;
raycaster.setFromCamera(center_screen, self.camera);
for(const eid in self.objects.elevator){
const el_obj = self.objects.elevator[eid];
if(el_obj.try_click_button(raycaster)){
break;
}
}
});
window.addEventListener("resize", self.update_resolution);
};
this.update_resolution = () => {
if(!self.renderer || !self.camera) return;
const w = window.innerWidth,
h = window.innerHeight,
scale = Math.max(0.03125, hollow_bond.render_scale / 100);
self.camera.aspect = w / h;
self.camera.updateProjectionMatrix();
self.renderer.setSize(Math.floor(w * scale), Math.floor(h * scale), false);
self.renderer.domElement.style.width = "100%";
self.renderer.domElement.style.height = "100%";
};
const render_loop = () => {
if(!self.on) return;
animation_frame_id = requestAnimationFrame(render_loop);
const now = performance.now(),
delta = Math.min((now - last_frame_time) / 1000, 0.1);
last_frame_time = now;
// 1. Detección y transición entre habitaciones
check_room_transition();
// // 2. Físicas y movimiento del jugador
// if(hollow_bond.controls && hollow_bond.controls.game){
// hollow_bond.controls.game.update(self, delta);
// // Colisiones contra escaleras instanciadas
// for(const sid in self.objects.stair){
// self.objects.stair[sid].collider.resolve_player(
// self.camera.position,
// hollow_bond.controls.game,
// 0.22
// );
// }
// }
// 2. Físicas y movimiento del jugador
if(hollow_bond.controls && hollow_bond.controls.game){
hollow_bond.controls.game.update(self, delta);
// Colisiones contra escaleras instanciadas
for(const sid in self.objects.stair){
self.objects.stair[sid].collider.resolve_player(
self.camera.position,
hollow_bond.controls.game,
0.22
);
}
// Animación y colisiones de ascensores instanciados
for(const eid in self.objects.elevator){
const el_obj = self.objects.elevator[eid];
el_obj.update(delta);
el_obj.collider.resolve_player(
self.camera.position,
hollow_bond.controls.game,
hollow_bond.controls.game.player_radius
);
}
}
// Actualizamos la telemetría en cada frame
debug_hud && debug_hud.tick();
if(hollow_bond.show_fps && hollow_bond.views && hollow_bond.views.fps){
hollow_bond.views.fps.tick();
}
self.renderer.render(self.scene, self.camera);
};
};
return DungeonManager;
})();