"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; })();