"use strict"; const RoomsBuilder = (function(){ const RoomsBuilder = function(hollow_bond){ const self = this; const get_hole_reserved_bounds_by_side = (hole, elevators, py) => { const maximum_margin_x = Math.max(hollow_bond.room_minimum_distance_hole, (hollow_bond.room_maximum_size - Math.abs(hole.xt - hole.xf)) * 0.5), maximum_margin_z = Math.max(hollow_bond.room_minimum_distance_hole, (hollow_bond.room_maximum_size - Math.abs(hole.zt - hole.zf)) * 0.5), bounds = { xf : hole.xf - Common.random_decay(hollow_bond.hash.rooms(hole.xf, py, hole.zf, 1), hollow_bond.room_minimum_distance_hole, maximum_margin_x, hollow_bond.room_size_decay) | 0, xt : hole.xt + Common.random_decay(hollow_bond.hash.rooms(hole.xt, py, hole.zt, 2), hollow_bond.room_minimum_distance_hole, maximum_margin_x, hollow_bond.room_size_decay) | 0, zf : hole.zf - Common.random_decay(hollow_bond.hash.rooms(hole.xf, py, hole.zf, 3), hollow_bond.room_minimum_distance_hole, maximum_margin_z, hollow_bond.room_size_decay) | 0, zt : hole.zt + Common.random_decay(hollow_bond.hash.rooms(hole.xt, py, hole.zt, 4), hollow_bond.room_minimum_distance_hole, maximum_margin_z, hollow_bond.room_size_decay) | 0 }; for(const elevator of elevators) if( bounds.xt > elevator.xf && bounds.xf < elevator.xt && bounds.zt > elevator.zf && bounds.zf < elevator.zt ){ const distance = { xf : elevator.xt - bounds.xf, xt : bounds.xt - elevator.xf, zf : elevator.zt - bounds.zf, zt : bounds.zt - elevator.zf }; switch(Math.min(distance.xf, distance.xt, distance.zf, distance.zt)){ case distance.xf: bounds.xf = elevator.xt; break; case distance.xt: bounds.xt = elevator.xf; break; case distance.zf: bounds.zf = elevator.zt; break; case distance.zt: bounds.zt = elevator.zf; break; }; }; return bounds; }; const cut_z = (cx, rectangle) => [ {xf : rectangle.xf, xt : cx, zf : rectangle.zf, zt : rectangle.zt, done : false}, {xf : cx, xt : rectangle.xt, zf : rectangle.zf, zt : rectangle.zt, done : false} ]; const cut_x = (cz, rectangle) => [ {xf : rectangle.xf, xt : rectangle.xt, zf : rectangle.zf, zt : cz, done : false}, {xf : rectangle.xf, xt : rectangle.xt, zf : cz, zt : rectangle.zt, done : false} ]; const obstacules_cut_z = (cx, obstacules, rectangle) => obstacules.some(obstacule => ( cx > obstacule.xf && cx < obstacule.xt && Math.max(rectangle.zf, obstacule.zf) < Math.min(rectangle.zt, obstacule.zt) )) ? [false, null] : [true, cut_z(cx, rectangle)]; const obstacules_cut_x = (cz, obstacules, rectangle) => obstacules.some(obstacule => ( cz > obstacule.zf && cz < obstacule.zt && Math.max(rectangle.xf, obstacule.xf) < Math.min(rectangle.xt, obstacule.xt) )) ? [false, null] : [true, cut_x(cz, rectangle)]; const subdivide_rooms = (rectangles, py) => { let has_changes = false; for(const rectangle of rectangles){ if(!rectangle.free) rectangle.done = true; else if(rectangle.done === undefined) rectangle.done = !rectangle.free; }; do{ const new_rooms = []; has_changes = false; for(const room of rectangles){ if(room.done || !room.free) continue; const width = Math.abs(room.xt - room.xf), depth = Math.abs(room.zt - room.zf); const can_split_x = width >= hollow_bond.room_minimum_size * 2, can_split_z = depth >= hollow_bond.room_minimum_size * 2; if(!can_split_x && !can_split_z){ room.done = true; continue; }; const x_oversized = width > hollow_bond.room_maximum_size, z_oversized = depth > hollow_bond.room_maximum_size, is_oversized = x_oversized || z_oversized, // 1. Hash con firma 2D completa (evita cuadrículas alineadas entre vecinos) seed_x = (room.xf * 73856093) ^ (room.xt * 19349663), seed_z = (room.zf * 83492791) ^ (room.zt * 50624027); let should_split = is_oversized; if(!should_split) should_split = hollow_bond.hash.rooms(seed_x, py, seed_z, 10) < (Math.max(width, depth) - hollow_bond.room_minimum_size * 2) / Math.max(1, hollow_bond.room_maximum_size - hollow_bond.room_minimum_size * 2); if (!should_split) { room.done = true; continue; }; // 2. Selección de eje: romper la relación de aspecto para forzar habitaciones cuadradas let split_axis = null; if(can_split_x && can_split_z){ // Si uno supera el máximo y el otro no, cortar obligatoriamente el sobredimensionado if(x_oversized && !z_oversized) split_axis = 'x'; else if(z_oversized && !x_oversized) split_axis = 'z'; else // Ponderación cuadrática por aspecto para evitar tiras/pasillos split_axis = hollow_bond.hash.rooms(seed_x, py, seed_z, 11) < width * width / (width * width + depth * depth) ? 'x' : 'z'; }else split_axis = can_split_x ? 'x' : 'z'; // 3. Punto de corte: oscilar cerca del centro en lugar de pelar los bordes if(split_axis === 'x'){ const split_x = (room.xf + Math.max(hollow_bond.room_minimum_size, Math.min(width - hollow_bond.room_minimum_size, width * .5 + Common.random_decay(hollow_bond.hash.rooms(seed_x, py, seed_z, 12), 0, (width - hollow_bond.room_minimum_size * 2) * .5, hollow_bond.room_size_decay) * (hollow_bond.hash.rooms(seed_x, py, seed_z, 14) < .5 ? -1 : 1)))) | 0, old_xt = room.xt; room.xt = split_x; room.done = false; new_rooms.push({ xf : split_x, xt : old_xt, zf : room.zf, zt : room.zt, free : true, done : false }); }else{ const split_z = (room.zf + Math.max(hollow_bond.room_minimum_size, Math.min(depth - hollow_bond.room_minimum_size, depth * .5 + Common.random_decay(hollow_bond.hash.rooms(seed_z, py, seed_x, 13), 0, depth - hollow_bond.room_minimum_size * 2 * .5, hollow_bond.room_size_decay) * (hollow_bond.hash.rooms(seed_z, py, seed_x, 15) < .5 ? -1 : 1)))) | 0, old_zt = room.zt; room.zt = split_z; room.done = false; new_rooms.push({ xf : room.xf, xt : room.xt, zf : split_z, zt : old_zt, free : true, done : false }); }; has_changes = true; }; new_rooms.length > 0 && rectangles.push(...new_rooms); }while(has_changes); return rectangles; }; const divide_plane = (chunk, py) => { const elevators = Object.values(chunk.elevators).filter(elevator => elevator.y == py).map(elevator => ({ xf : elevator.x, xt : elevator.x + elevator.width, zf : elevator.z, zt : elevator.z + elevator.depth })), holes_y = Object.values(chunk.holes).filter(hole => py >= hole.yf && py < hole.yt), holes = holes_y.map(hole => get_hole_reserved_bounds_by_side(hole, elevators, py)), obstacules = elevators.concat(holes), queue = [{ xf : chunk.xf, xt : chunk.xt, zf : chunk.zf, zt : chunk.zt }], obstacules_cut = { x : obstacules_cut_x, z : obstacules_cut_z }, // 3. Ajuste de habitaciones sobredimensionadas que contienen huecos new_rectangles = []; let rectangles = []; // 1. Partición de espacios con cola estricta while(queue.length > 0){ const rectangle = queue.pop(), local_obstacules = obstacules.filter(obstacule => obstacule.xt > rectangle.xf && obstacule.xf < rectangle.xt && obstacule.zt > rectangle.zf && obstacule.zf < rectangle.zt ); if(local_obstacules.length === 0){ rectangles.push({...rectangle, free : true}); continue; }; if( local_obstacules.length === 1 && rectangle.xf === local_obstacules[0].xf && rectangle.xt === local_obstacules[0].xt && rectangle.zf === local_obstacules[0].zf && rectangle.zt === local_obstacules[0].zt ){ rectangles.push({...rectangle, free : false}); continue; }; let split_found = false; for(const obstacule of local_obstacules){ for (const [axis, value] of [].concat( [["z", obstacule.xf, 0], ["z", obstacule.xt, 1]].filter(([_, x]) => x > rectangle.xf && x < rectangle.xt), [["x", obstacule.zf, 2], ["x", obstacule.zt, 3]].filter(([_, z]) => z > rectangle.zf && z < rectangle.zt) ).sort((a, b) => hollow_bond.hash.room_cut_axis(py, a, b))){ const [ok, subrectangles] = obstacules_cut[axis](value, local_obstacules, rectangle); if(ok){ queue.push(...subrectangles); split_found = true; break; }; }; if(split_found) break; }; split_found || rectangles.push({...rectangle, free : !obstacules.some(obstacule => rectangle.xf >= obstacule.xf && rectangle.xt <= obstacule.xt && rectangle.zf >= obstacule.zf && rectangle.zt <= obstacule.zt )}); }; // 2. Descartar los rectángulos que corresponden exactamente a los ascensores rectangles = rectangles.filter(rectangle => !(!rectangle.free && elevators.some(elevator => ( rectangle.xf === elevator.xf && rectangle.xt === elevator.xt && rectangle.zf === elevator.zf && rectangle.zt === elevator.zt )))); for(let i = 0; i < rectangles.length; i ++){ // Obtener los agujeros reales dentro de este rectángulo const contained_holes = holes_y.filter(hole => ( rectangles[i].xf <= hole.xf && rectangles[i].xt >= hole.xt && rectangles[i].zf <= hole.zf && rectangles[i].zt >= hole.zt )); if(contained_holes.length === 0) continue; let width = Math.abs(rectangles[i].xt - rectangles[i].xf), depth = Math.abs(rectangles[i].zt - rectangles[i].zf); if(width > hollow_bond.room_maximum_size || depth > hollow_bond.room_maximum_size){ // Bounding box protector envolvente de todos los agujeros contenidos const minimum = { xf : Math.min(...contained_holes.map(hole => hole.xf)) - hollow_bond.room_minimum_distance_hole, xt : Math.max(...contained_holes.map(hole => hole.xt)) + hollow_bond.room_minimum_distance_hole, zf : Math.min(...contained_holes.map(hole => hole.zf)) - hollow_bond.room_minimum_distance_hole, zt : Math.max(...contained_holes.map(hole => hole.zt)) + hollow_bond.room_minimum_distance_hole }; // Recortes en X if(width > hollow_bond.room_maximum_size){ if(rectangles[i].xf < minimum.xf){ const subrectangles = cut_z(minimum.xf, rectangles[i]); width -= Math.abs(minimum.xf - rectangles[i].xf); rectangles[i] = subrectangles[1]; new_rectangles.push(subrectangles[0]); }; if(width > hollow_bond.room_maximum_size && rectangles[i].xt > minimum.xt){ const subrectangles = cut_z(minimum.xt, rectangles[i]); rectangles[i] = subrectangles[0]; new_rectangles.push(subrectangles[1]); }; }; // Recortes en Z if(depth > hollow_bond.room_maximum_size){ if(rectangles[i].zf < minimum.zf){ const subrectangles = cut_x(minimum.zf, rectangles[i]); depth -= Math.abs(minimum.zf - rectangles[i].zf); rectangles[i] = subrectangles[1]; new_rectangles.push(subrectangles[0]); }; if(depth > hollow_bond.room_maximum_size && rectangles[i].zt > minimum.zt){ const subrectangles = cut_x(minimum.zt, rectangles[i]); rectangles[i] = subrectangles[0]; new_rectangles.push(subrectangles[1]); }; }; }; }; rectangles.push(...new_rectangles); // Blindar las habitaciones que contienen o tocan fosos para que no sean cortadas for(const rect of rectangles){ const touches_hole = holes_y.some(hole => ( rect.xt > hole.xf && rect.xf < hole.xt && rect.zt > hole.zf && rect.zf < hole.zt )); if(touches_hole){ rect.done = true; rect.free = false; }; }; return subdivide_rooms(rectangles, py); }; const get_bounds = (x, z, axis) => { const margin = hollow_bond.frame_padding + hollow_bond.room_padding return { xf : axis === "horizontal" ? x : x - margin, xt : axis === "horizontal" ? x + hollow_bond.frame_width : x + margin, zf : axis === "horizontal" ? z - margin : z, zt : axis === "horizontal" ? z + margin : z + hollow_bond.frame_width }; }; const collides = (b1, b2) => ( b1.xt > b2.xf && b1.xf < b2.xt && b1.zt > b2.zf && b1.zf < b2.zt ); /** * Genera los marcos interiores entre habitaciones en un piso determinado. * @param {ChunkModel} chunk - Instancia del Chunk actual. * @param {Array} rooms - Habitaciones del piso actual. * @param {number} py - Índice del piso. */ const generate_frames = (chunk, rooms, py) => { chunk.frames = chunk.frames || {}; const margin = hollow_bond.frame_padding + hollow_bond.room_padding, chunk_xf = chunk.xf, chunk_xt = chunk.xt, chunk_zf = chunk.zf, chunk_zt = chunk.zt, // Estructura para registrar los AABB de los marcos ya aceptados en este piso accepted_bounds = [], // Evaluamos cada par de habitaciones del piso una sola vez total_rooms = rooms.length; for(let i = 0; i < total_rooms; i ++){ const room_a = rooms[i]; for(const room_b of rooms){ let candidate = null; // 1. Comprobar pared vertical compartida (normal en X, discurre a lo largo de Z) if(room_a.x + room_a.width === room_b.x || room_b.x + room_b.width === room_a.x){ const wall_x = (room_a.x + room_a.width === room_b.x) ? room_b.x : room_a.x; // Descartar si coincide con el límite exterior del Chunk if(wall_x > chunk_xf && wall_x < chunk_xt){ // Dejamos margen para no pegar el marco a las esquinas ni al borde del Chunk const minimum_z = Math.max(Math.max(room_a.z, room_b.z) + margin, chunk_zf + margin), maximum_z = Math.min(Math.min(room_a.z + room_a.depth, room_b.z + room_b.depth) - margin, chunk_zt - margin), span = maximum_z - minimum_z; if(span >= hollow_bond.frame_width && hollow_bond.hash.rooms(wall_x, py, (minimum_z + maximum_z) | 0, 20) < hollow_bond.frame_wall_probability) candidate = { axis : "vertical", x : wall_x, z : minimum_z + (Common.random_decay(hollow_bond.hash.rooms(wall_x, py, (minimum_z + maximum_z) | 0, 21), 0, span - hollow_bond.frame_width, hollow_bond.frame_wall_decay) | 0), room_a : room_a, room_b : room_b }; }; }; // 2. Comprobar pared horizontal compartida (normal en Z, discurre a lo largo de X) if(!candidate && (room_a.z + room_a.depth === room_b.z || room_b.z + room_b.depth === room_a.z)){ const wall_z = room_a.z + room_a.depth === room_b.z ? room_b.z : room_a.z; // Descartar si coincide con el límite exterior del Chunk if(wall_z > chunk_zf && wall_z < chunk_zt){ const minimum_x = Math.max(Math.max(room_a.x, room_b.x) + margin, chunk_xf + margin), maximum_x = Math.min(Math.min(room_a.x + room_a.width, room_b.x + room_b.width) - margin, chunk_xt - margin), span = maximum_x - minimum_x; if(span >= hollow_bond.frame_width && hollow_bond.hash.rooms((minimum_x + maximum_x) | 0, py, wall_z, 22) < hollow_bond.frame_wall_probability) candidate = { axis : "horizontal", x : minimum_x + (Common.random_decay(hollow_bond.hash.rooms((minimum_x + maximum_x) | 0, py, wall_z, 23), 0, span - hollow_bond.frame_width, hollow_bond.frame_wall_decay) | 0), z : wall_z, room_a : room_a, room_b : room_b }; }; }; // 3. Si hay candidato, verificar que no colisiona con ningún marco existente if(candidate){ const room_b_can = get_bounds(candidate.x, candidate.z, candidate.axis); if(!accepted_bounds.some(bound => collides(bound, room_b_can))){ const frame = new FrameModel(hollow_bond, { x: candidate.x, y: py, z: candidate.z, axis: candidate.axis, rooms: [candidate.room_a.id, candidate.room_b.id] }); accepted_bounds.push(room_b_can); // Vinculación bidireccional candidate.room_a.frames.push(frame.id); candidate.room_b.frames.push(frame.id); // Registro en el Chunk y en el mapa global chunk.frames[frame.id] = frame; if(hollow_bond.map && hollow_bond.map.frames) hollow_bond.map.frames[frame.id] = frame; }; }; }; }; }; /** * Conecta los marcos perimetrales del Chunk con las habitaciones correspondientes. * @param {ChunkModel} chunk - Instancia del Chunk actual. */ const connect_chunk_frames = chunk => { const eps = .001, // Obtenemos todos los marcos del Chunk que tengan conexiones libres ([null, null] o [id, null]) chunk_frames = Object.values(chunk.frames).filter(frame => frame.rooms && frame.rooms.includes(null)); if(chunk_frames.length === 0) return; // Buscamos contra todas las habitaciones disponibles (las del mapa global para unir con vecinos, o las del chunk) const available_rooms = Object.values(chunk.rooms); for(const frame of chunk_frames) for(const room of available_rooms) // 1. Comprobar que coincidan en el piso if(Math.round(room.y / (room.height || hollow_bond.room_height)) === frame.y && (frame.axis === "horizontal" ? ( (Math.abs(room.z + room.depth - frame.z) < eps || Math.abs(room.z - frame.z) < eps) && frame.x >= room.x - eps && frame.x + frame.width <= room.x + room.width + eps ) : ( (Math.abs(room.x + room.width - frame.x) < eps || Math.abs(room.x - frame.x) < eps) && frame.z >= room.z - eps && frame.z + frame.width <= room.z + room.depth + eps ))){ // Vinculación bidireccional frame.connect_room(room.id); room.frames.includes(frame.id) || room.frames.push(frame.id); // Si el marco ya conectó sus 2 habitaciones, no hace falta evaluar más para este marco if(!frame.rooms.includes(null)) break; }; }; const get_frame = (chunk, id) => chunk.frames[id] || hollow_bond.map.frames[id]; // Criterio para identificar si un marco da al exterior del Chunk const is_chunk_frame = (chunk, frame) => { if(!frame) return false; if( (chunk.chunk_frames && Object.values(chunk.chunk_frames).some(f => f.id === frame.id)) || frame.is_chunk_frame || (frame.rooms && frame.rooms.includes(null)) ) return true; // Comprobación geométrica: el marco reposa sobre los bordes del Chunk const eps = .5; return ( Math.abs(frame.x - chunk.xf) < eps || Math.abs(frame.x - chunk.xt) < eps || Math.abs(frame.z - chunk.zf) < eps || Math.abs(frame.z - chunk.zt) < eps ); }; /** * Elimina habitaciones aisladas e islas cerradas dentro del Chunk. * @param {ChunkModel} chunk - Instancia del Chunk actual. */ const cleanup_unconnected_rooms = chunk => { // 1. Identificar las habitaciones "raíz" (tienen salida directa garantizada) const reachable = new Set(), queue = []; for(const room_id in chunk.rooms) if( (chunk.rooms[room_id].elevators && chunk.rooms[room_id].elevators.length > 0) || (chunk.rooms[room_id].holes && chunk.rooms[room_id].holes.length > 0) || (chunk.rooms[room_id].frames && chunk.rooms[room_id].frames.some(frame_id => is_chunk_frame(chunk, get_frame(chunk, frame_id)))) ){ reachable.add(chunk.rooms[room_id].id); queue.push(chunk.rooms[room_id]); }; // 2. Propagar la alcanzabilidad mediante BFS a través de los marcos transitables while(queue.length > 0){ const current = queue.shift(); if(!current.frames) continue; for(const frame_id of current.frames){ const frame = get_frame(chunk, frame_id); if(!frame || !frame.rooms) continue; for(const neighbor_id of frame.rooms) // Si el vecino pertenece a este Chunk y aún no fue alcanzado if(neighbor_id && chunk.rooms[neighbor_id] && !reachable.has(neighbor_id)){ reachable.add(neighbor_id); queue.push(chunk.rooms[neighbor_id]); }; }; }; // 3. Purgar las habitaciones inalcanzables y sus marcos huérfanos const frames_to_delete = new Set(); for(const room_id in chunk.rooms) if(!reachable.has(room_id)){ // Marcar sus marcos para eliminarlos if (chunk.rooms[room_id].frames) for (const frame_id of chunk.rooms[room_id].frames) frames_to_delete.add(frame_id); // Eliminar habitación del Chunk y del mapa global delete chunk.rooms[room_id]; delete hollow_bond.map.rooms[room_id]; }; // 4. Limpiar los marcos que pertenecían a las habitaciones eliminadas for(const frame_id of frames_to_delete){ delete chunk.frames[frame_id]; delete hollow_bond.map.frames[frame_id]; }; }; const generate_floor = (chunk, py) => { const eps = .05; // Delegación directa a tu método en HollowBond const rectangles = divide_plane(chunk, py), rooms = [], floor_elevators = Object.values(chunk.elevators).filter(elevator => elevator.y == py); if(!Array.isArray(rectangles)) return; for(let i = 0; i < rectangles.length; i ++){ const width = rectangles[i].xt - rectangles[i].xf, depth = rectangles[i].zt - rectangles[i].zf; if(width <= 0 || depth <= 0) continue; // Asociar huecos que toquen o queden dentro de este rectángulo en este piso const associated_holes = [], // Asociar ascensores presentes en la habitación en este piso associated_elevators = []; let room; for(const elevator of floor_elevators){ let is_connected = false; switch(elevator.facing){ case "north": is_connected = Math.abs(rectangles[i].zt - elevator.z) < eps && (Math.max(rectangles[i].xf, elevator.x) < Math.min(rectangles[i].xt, elevator.x + elevator.width)); break; case "south": is_connected = Math.abs(rectangles[i].zf - (elevator.z + elevator.depth)) < eps && (Math.max(rectangles[i].xf, elevator.x) < Math.min(rectangles[i].xt, elevator.x + elevator.width)); break; case "west": is_connected = Math.abs(rectangles[i].xt - elevator.x) < eps && (Math.max(rectangles[i].zf, elevator.z) < Math.min(rectangles[i].zt, elevator.z + elevator.depth)); break; case "east": is_connected = Math.abs(rectangles[i].xf - (elevator.x + elevator.width)) < eps && (Math.max(rectangles[i].zf, elevator.z) < Math.min(rectangles[i].zt, elevator.z + elevator.depth)); break; }; is_connected && associated_elevators.push(elevator.id); }; for(const hole_id in chunk.holes) !(py < chunk.holes[hole_id].yf || py >= chunk.holes[hole_id].yt) && ( rectangles[i].xt > chunk.holes[hole_id].xf && rectangles[i].xf < chunk.holes[hole_id].xt && rectangles[i].zt > chunk.holes[hole_id].zf && rectangles[i].zf < chunk.holes[hole_id].zt ) && associated_holes.push(chunk.holes[hole_id].id || hole_id); rooms.push(room = new RoomModel(hollow_bond, { x : rectangles[i].xf, y : py * hollow_bond.room_height, z : rectangles[i].zf, width : width, height : hollow_bond.room_height, depth : depth, padding : hollow_bond.room_padding, holes : associated_holes, elevators : associated_elevators, frames : [] })); // Registro en el Chunk y en el consolidado global de MapManager chunk.rooms[room.id] = room; for(const id of associated_holes) chunk.holes[id].rooms.push(room.id); for(const id of associated_elevators) chunk.elevators[id].room = room.id; if(hollow_bond.map && hollow_bond.map.rooms) hollow_bond.map.rooms[room.id] = room.id; }; generate_frames(chunk, rooms, py); }; this.generate_asyncronous = (chunk, callback) => new Promise(resolve => { let py = chunk.yf; const secure_time = 5, tick = () => { const start_time = performance.now(); while(py < chunk.yt && performance.now() - start_time < secure_time) generate_floor(chunk, py ++); if(py < chunk.yt) requestAnimationFrame(tick); else{ connect_chunk_frames(chunk); cleanup_unconnected_rooms(chunk); callback(); resolve(); }; }; requestAnimationFrame(tick); }); /** * Genera las instancias de RoomModel a partir de los rectángulos devueltos por divide_plane. * @param {ChunkModel} chunk - Instancia del Chunk padre. */ this.generate = chunk => { for(let py = chunk.yf; py < chunk.yt; py ++) generate_floor(chunk, py); connect_chunk_frames(chunk); cleanup_unconnected_rooms(chunk); }; }; return RoomsBuilder; })();