745 lines
32 KiB
JavaScript
745 lines
32 KiB
JavaScript
"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<RoomModel>} 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;
|
|
})(); |