"use strict"; const InspectorManager = (function(){ const InspectorManager = function(hollow_bond){ const self = this; this.scene = null; this.camera = null; this.renderer = null; this.on = false; // Configuración de visualización vertical this.current_floor = 0; this.vertical_layers_range = 2; // Cantidad de pisos arriba y abajo a mostrar this.floor_height = hollow_bond.room_height || 3; this.geometries = { frame : new FrameGeometryDebug(hollow_bond), room : new RoomGeometryDebug(hollow_bond), hole : new HoleGeometryDebug(hollow_bond), stair : new StairGeometryDebug(hollow_bond), elevator : new ElevatorGeometryDebug(hollow_bond) }; this.materials = { frame : new FrameMaterialDebug(), room : new RoomMaterialDebug(), hole : new HoleMaterialDebug(), stair : new StairMaterialDebug(), elevator : new ElevatorMaterialDebug() }; // Diccionarios de objetos visuales en la escena this.objects = { frame : {}, room : {}, hole : {}, stair : {}, elevator : {} }; let floor_indicator_plane = null, hud_element = null; let last_frame_time = performance.now(); /** * Inicializa la vista de inspección con vuelo libre. */ this.start = () => { self.on = true; setup_scene(); setup_hud(); hollow_bond.controls.debug.setup(self); // Poblamos entidades que ya existan en el mapa self.synchronize_all(); // Activamos contador de FPS si está habilitado hollow_bond.views.fps.go(); render_loop(); }; /** * Sincroniza todas las entidades disponibles en el mapa global. */ this.synchronize_all = () => { synchronize_holes(); synchronize_elevators(); synchronize_stairs(); synchronize_frames(); synchronize_rooms(); self.update_layer_visibility(); }; /** * Sincroniza de forma incremental un Chunk específico recién generado. * @param {ChunkModel} chunk */ this.sync_chunk = chunk => { if(!chunk) return; // 1. Huecos if(chunk.holes){ for(const hid in chunk.holes){ if(!self.objects.hole[hid]){ const model = chunk.holes[hid]; const obj = new HoleObjectDebug(hollow_bond, model, self.geometries.hole, self.materials.hole); obj.model = model; self.objects.hole[hid] = obj; self.scene.add(obj.mesh); } } } // 2. Ascensores if(chunk.elevators){ for(const eid in chunk.elevators){ if(!self.objects.elevator[eid]){ const model = chunk.elevators[eid]; const obj = new ElevatorObjectDebug(hollow_bond, model, self.geometries.elevator, self.materials.elevator); obj.model = model; self.objects.elevator[eid] = obj; self.scene.add(obj.mesh); } } } // 3. Escaleras if(chunk.stairs){ for(const sid in chunk.stairs){ if(!self.objects.stair[sid]){ const model = chunk.stairs[sid]; const obj = new StairObjectDebug(hollow_bond, model); obj.model = model; self.objects.stair[sid] = obj; self.scene.add(obj.mesh); } } } // 4. Marcos (con visibilidad de capa inmediata) if(chunk.frames){ for(const fid in chunk.frames){ if(!self.objects.frame[fid]){ const model = chunk.frames[fid]; const obj = new FrameObjectDebug(hollow_bond, model, self.geometries.frame, self.materials.frame); obj.model = model; const dist = Math.abs(model.y - self.current_floor); if(dist > self.vertical_layers_range){ obj.mesh.visible = false; } else { obj.mesh.visible = true; obj.set_layer_distance && obj.set_layer_distance(dist); } self.objects.frame[fid] = obj; self.scene.add(obj.mesh); } } } // 5. Habitaciones (con visibilidad de capa inmediata) if(chunk.rooms){ for(const rid in chunk.rooms){ if(!self.objects.room[rid]){ const model = chunk.rooms[rid]; const obj = new RoomObjectDebug(hollow_bond, model); obj.model = model; const floor_index = Math.floor(model.y / self.floor_height); const dist = Math.abs(floor_index - self.current_floor); obj.mesh.visible = (dist <= self.vertical_layers_range); self.objects.room[rid] = obj; self.scene.add(obj.mesh); } } } }; /** * Retira y destruye las mallas 3D asociadas a un Chunk descargado. * @param {ChunkModel} chunk */ this.remove_chunk = chunk => { if(!chunk) return; const remove_group = (type, source) => { if(!source) return; for(const id in source){ if(self.objects[type][id]){ self.scene && self.scene.remove(self.objects[type][id].mesh); self.objects[type][id].destroy && self.objects[type][id].destroy(); delete self.objects[type][id]; } } }; remove_group("frame", chunk.frames); remove_group("room", chunk.rooms); remove_group("hole", chunk.holes); remove_group("elevator", chunk.elevators); remove_group("stair", chunk.stairs); }; /** * Limpia por completo la escena y libera memoria de GPU. */ this.reset = () => { // 1. Limpieza de mallas en la escena 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] = {}; }; // 2. Liberación de buffers geométricos for(const key in self.geometries) self.geometries[key].reset && self.geometries[key].reset(); // 3. Liberación de shaders y materiales for(const key in self.materials) self.materials[key].reset && self.materials[key].reset(); }; const setup_scene = () => { const chunk_height = hollow_bond.chunk_size * hollow_bond.room_height; self.scene = new THREE.Scene(); self.scene.background = new THREE.Color(0x0a0a0d); self.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1500); self.renderer = new THREE.WebGLRenderer({ antialias : hollow_bond.render_scale >= 100, powerPreference : "high-performance" }); self.renderer.domElement.className = "hb-canvas-inspector"; self.renderer.domElement.tabIndex = 1; hollow_bond.item_self.appendChild(self.renderer.domElement); self.update_resolution(); // Iluminación self.scene.add(new THREE.AmbientLight(0xffffff, 0.85)); const sun = new THREE.DirectionalLight(0xd6d0ba, 1.2); sun.position.set(25, 60, 25); self.scene.add(sun); // Cajas guía de los Chunks iniciales [-1, 0, 1].forEach(cy => { const wire_box = new THREE.Mesh( new THREE.BoxGeometry(hollow_bond.chunk_size, chunk_height, hollow_bond.chunk_size), new THREE.MeshBasicMaterial({ color : cy === 0 ? 0x3d4450 : 0x1f232b, wireframe : true, transparent : true, opacity : cy === 0 ? 0.35 : 0.15 }) ); wire_box.position.set( hollow_bond.chunk_size / 2, (cy * chunk_height) + (chunk_height / 2), hollow_bond.chunk_size / 2 ); self.scene.add(wire_box); }); // Plano visual sutil que marca la cota del piso seleccionado floor_indicator_plane = new THREE.GridHelper(hollow_bond.chunk_size * 3, hollow_bond.chunk_size * 3, 0x00bcd4, 0x1e293b); floor_indicator_plane.position.set(hollow_bond.chunk_size / 2, 0, hollow_bond.chunk_size / 2); self.scene.add(floor_indicator_plane); // Posición inicial de la cámara self.camera.position.set(hollow_bond.chunk_size / 2, 5, hollow_bond.chunk_size / 2 + 10); 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%"; }; /** * Ajusta la cota activa y recalcula las opacidades verticales. */ this.set_floor = floor_y => { self.current_floor = floor_y; if(floor_indicator_plane){ floor_indicator_plane.position.y = self.current_floor * self.floor_height; } self.update_layer_visibility(); self.update_hud(); }; /** * Aplica la visibilidad y opacidad degradada a marcos y habitaciones. */ this.update_layer_visibility = () => { const range = self.vertical_layers_range; // 1. Filtrado y gradiente de marcos for(const id in self.objects.frame){ const frame_obj = self.objects.frame[id], dist = Math.abs(frame_obj.model.y - self.current_floor); if(dist > range){ frame_obj.mesh.visible = false; } else { frame_obj.mesh.visible = true; frame_obj.set_layer_distance && frame_obj.set_layer_distance(dist); } } // 2. Filtrado de habitaciones for(const id in self.objects.room){ const room_obj = self.objects.room[id], floor_index = Math.floor(room_obj.model.y / self.floor_height), dist = Math.abs(floor_index - self.current_floor); room_obj.mesh.visible = (dist <= range); } }; // --- Sincronizadores Globales --- const synchronize_frames = () => { if(!hollow_bond.map || !hollow_bond.map.frames) return; for(const frame_id in hollow_bond.map.frames){ if(!self.objects.frame[frame_id]){ const frame_model = hollow_bond.map.frames[frame_id]; const frame_obj = new FrameObjectDebug( hollow_bond, frame_model, self.geometries.frame, self.materials.frame ); frame_obj.model = frame_model; const dist = Math.abs(frame_model.y - self.current_floor); if(dist > self.vertical_layers_range){ frame_obj.mesh.visible = false; } else { frame_obj.mesh.visible = true; frame_obj.set_layer_distance && frame_obj.set_layer_distance(dist); } self.objects.frame[frame_id] = frame_obj; self.scene.add(frame_obj.mesh); } } }; const synchronize_rooms = () => { if(!hollow_bond.map || !hollow_bond.map.rooms) return; for(const room_id in hollow_bond.map.rooms){ if(!self.objects.room[room_id]){ const room_model = hollow_bond.map.rooms[room_id]; const room_obj = new RoomObjectDebug(hollow_bond, room_model); room_obj.model = room_model; const floor_index = Math.floor(room_model.y / self.floor_height), dist = Math.abs(floor_index - self.current_floor); room_obj.mesh.visible = (dist <= self.vertical_layers_range); self.objects.room[room_id] = room_obj; self.scene.add(room_obj.mesh); } } }; const synchronize_holes = () => { if(!hollow_bond.map || !hollow_bond.map.chunks) return; for(const cid in hollow_bond.map.chunks){ const chunk = hollow_bond.map.chunks[cid]; if(!chunk || !chunk.holes) continue; for(const hid in chunk.holes){ if(!self.objects.hole[hid]){ const hole_model = chunk.holes[hid]; const hole_obj = new HoleObjectDebug( hollow_bond, hole_model, self.geometries.hole, self.materials.hole ); hole_obj.model = hole_model; self.objects.hole[hid] = hole_obj; self.scene.add(hole_obj.mesh); } } } }; const synchronize_elevators = () => { if(!hollow_bond.map || !hollow_bond.map.chunks) return; for(const cid in hollow_bond.map.chunks){ const chunk = hollow_bond.map.chunks[cid]; if(!chunk || !chunk.elevators) continue; for(const eid in chunk.elevators){ if(!self.objects.elevator[eid]){ const elevator_model = chunk.elevators[eid]; const elevator_obj = new ElevatorObjectDebug( hollow_bond, elevator_model, self.geometries.elevator, self.materials.elevator ); elevator_obj.model = elevator_model; self.objects.elevator[eid] = elevator_obj; self.scene.add(elevator_obj.mesh); } } } }; const synchronize_stairs = () => { if(!hollow_bond.map || !hollow_bond.map.chunks) return; for(const cid in hollow_bond.map.chunks){ const chunk = hollow_bond.map.chunks[cid]; if(!chunk || !chunk.stairs) continue; for(const sid in chunk.stairs){ if(!self.objects.stair[sid]){ const stair_model = chunk.stairs[sid]; const stair_obj = new StairObjectDebug(hollow_bond, stair_model); stair_obj.model = stair_model; self.objects.stair[sid] = stair_obj; self.scene.add(stair_obj.mesh); } } } }; const setup_hud = () => { hud_element = hollow_bond.html(hollow_bond.item_self, ["div", {"class" : "inspector-hud"}, [ ["div", {"class" : "hud-title"}, "MODO INSPECCIÓN TÉCNICA"], ["div", {"class" : "hud-row"}, [ ["span", {}, "Piso enfocado (Y): "], ["b", {"class" : "hud-floor-label"}, String(self.current_floor)] ]], ["div", {"class" : "hud-row"}, [ ["span", {}, "Capas adyacentes: ±"], ["b", {"class" : "hud-range-label"}, String(self.vertical_layers_range)] ]], ["div", {"class" : "hud-controls-hint"}, "[Q/E] Subir/Bajar piso | [0..4] Rango de capas | [WASD+Ratón] Volar"] ]])[0]; }; this.update_hud = () => { if(!hud_element) return; hud_element.querySelector(".hud-floor-label").textContent = String(self.current_floor); hud_element.querySelector(".hud-range-label").textContent = String(self.vertical_layers_range); }; const render_loop = () => { requestAnimationFrame(render_loop); const now = performance.now(), delta = Math.min((now - last_frame_time) / 1000, 0.1); last_frame_time = now; hollow_bond.controls.debug.update_movement(self, delta); hollow_bond.show_fps && hollow_bond.views.fps && hollow_bond.views.fps.tick(); self.renderer.render(self.scene, self.camera); }; }; return InspectorManager; })();