"use strict"; const StairGeometryGame = (function(){ const StairGeometryGame = function(hollow_bond){ const cache = {}; // Utilidad para añadir un prisma ortogonal (caja AABB) const append_box = (buffers, min_x, max_x, min_y, max_y, min_z, max_z) => { const { positions, normals, uvs, indices } = buffers; let offset = positions.length / 3; const add_quad = (p1, p2, p3, p4, n) => { positions.push(...p1, ...p2, ...p3, ...p4); normals.push(...n, ...n, ...n, ...n); uvs.push(0, 0, 1, 0, 1, 1, 0, 1); indices.push(offset, offset + 2, offset + 1, offset, offset + 3, offset + 2); offset += 4; }; add_quad([min_x, max_y, max_z], [max_x, max_y, max_z], [max_x, max_y, min_z], [min_x, max_y, min_z], [0, 1, 0]); add_quad([min_x, min_y, min_z], [max_x, min_y, min_z], [max_x, min_y, max_z], [min_x, min_y, max_z], [0, -1, 0]); add_quad([max_x, min_y, min_z], [min_x, min_y, min_z], [min_x, max_y, min_z], [max_x, max_y, min_z], [0, 0, -1]); add_quad([min_x, min_y, max_z], [max_x, min_y, max_z], [max_x, max_y, max_z], [min_x, max_y, max_z], [0, 0, 1]); add_quad([min_x, min_y, min_z], [min_x, min_y, max_z], [min_x, max_y, max_z], [min_x, max_y, min_z], [-1, 0, 0]); add_quad([max_x, min_y, max_z], [max_x, min_y, min_z], [max_x, max_y, min_z], [max_x, max_y, max_z], [1, 0, 0]); }; // Genera una barra inclinada continua conectando (L0, Y0) con (L1, Y1) const append_inclined_bar = (buffers, is_x, l0, y0, l1, y1, c_min, c_max, bar_h) => { const { positions, normals, uvs, indices } = buffers; let offset = positions.length / 3; const add_quad = (p1, p2, p3, p4, n) => { positions.push(...p1, ...p2, ...p3, ...p4); normals.push(...n, ...n, ...n, ...n); uvs.push(0, 0, 1, 0, 1, 1, 0, 1); indices.push(offset, offset + 2, offset + 1, offset, offset + 3, offset + 2); offset += 4; }; const make_pt = (l, y, c) => is_x ? [l, y, c] : [c, y, l]; const p0 = make_pt(l0, y0, c_min); const p1 = make_pt(l0, y0, c_max); const p2 = make_pt(l0, y0 + bar_h, c_max); const p3 = make_pt(l0, y0 + bar_h, c_min); const p4 = make_pt(l1, y1, c_min); const p5 = make_pt(l1, y1, c_max); const p6 = make_pt(l1, y1 + bar_h, c_max); const p7 = make_pt(l1, y1 + bar_h, c_min); // Caras longitudinales add_quad(p3, p7, p6, p2, [0, 1, 0]); // Superior add_quad(p0, p1, p5, p4, [0, -1, 0]); // Inferior add_quad(p0, p4, p7, p3, is_x ? [0, 0, -1] : [-1, 0, 0]); // Lateral C_min add_quad(p1, p2, p6, p5, is_x ? [0, 0, 1] : [1, 0, 0]); // Lateral C_max // Tapas en los extremos add_quad(p0, p3, p2, p1, is_x ? [-1, 0, 0] : [0, 0, -1]); add_quad(p4, p5, p6, p7, is_x ? [1, 0, 0] : [0, 0, 1]); }; this.get_for_stair = (stair, hole) => { const is_x = (stair.wall === "north" || stair.wall === "south"); const length = is_x ? stair.width : stair.depth; const rise_height = hollow_bond.room_height; const landing_size = hollow_bond.stairs_landing_size || 1.0; const key = `stair_v2_${stair.wall}_dir${stair.direction}_len${length}_h${rise_height}`; if(cache[key]) return cache[key]; const buffers = { positions: [], normals: [], uvs: [], indices: [] }; const half_w = 0.5; const flight_len = Math.max(1.0, length - (landing_size * 2)); const dir = stair.direction; // 1 o -1 // Orientación: qué lado da al vacío del foso const inner_sign = (stair.wall === "north" || stair.wall === "west") ? 1 : -1; const inner_c = inner_sign * (half_w - 0.04); const outer_c = -inner_sign * (half_w - 0.04); // 1. DESCANGILLOS (Plataformas horizontales de 1m) const bot_l_start = (dir === 1) ? 0 : (length - landing_size); const top_l_start = (dir === 1) ? (length - landing_size) : 0; const add_platform = (l_start, y_level) => { const l_end = l_start + landing_size; if(is_x){ append_box(buffers, l_start, l_end, y_level - 0.05, y_level, -half_w, half_w); } else { append_box(buffers, -half_w, half_w, y_level - 0.05, y_level, l_start, l_end); } }; add_platform(bot_l_start, 0); add_platform(top_l_start, rise_height); // 2. PELDAÑOS NUMEROSOS (~0.18m contrahuella) const step_count = Math.max(12, Math.round(rise_height / 0.1875)); const step_rise = rise_height / step_count; const step_run = flight_len / step_count; for(let i = 0; i < step_count; i++){ let l_min, l_max; if(dir === 1){ l_min = landing_size + (i * step_run); l_max = l_min + step_run; } else { l_max = (length - landing_size) - (i * step_run); l_min = l_max - step_run; } const y_top = (i + 1) * step_rise; const y_bot = y_top - 0.035; if(is_x){ append_box(buffers, l_min, l_max, y_bot, y_top, -half_w + 0.06, half_w - 0.06); } else { append_box(buffers, -half_w + 0.06, half_w - 0.06, y_bot, y_top, l_min, l_max); } } // 3. ZANCAS / BARRAS INCLINADAS QUE UNEN LOS PELDAÑOS const flight_l_bot = (dir === 1) ? landing_size : (length - landing_size); const flight_l_top = (dir === 1) ? (length - landing_size) : landing_size; // Zanca exterior (lado pared) append_inclined_bar( buffers, is_x, flight_l_bot, 0, flight_l_top, rise_height, outer_c - 0.025, outer_c + 0.025, 0.07 ); // Zanca interior (lado foso) append_inclined_bar( buffers, is_x, flight_l_bot, 0, flight_l_top, rise_height, inner_c - 0.025, inner_c + 0.025, 0.07 ); // 4. BARANDILLA INCLINADA Y POSTES (1.0m de altura, solo lado vacío) const rail_h = 1.0; // Pasamanos superior (1.0m) append_inclined_bar( buffers, is_x, flight_l_bot, rail_h - 0.04, flight_l_top, rise_height + rail_h - 0.04, inner_c - 0.02, inner_c + 0.02, 0.04 ); // Travesaño intermedio (0.5m) append_inclined_bar( buffers, is_x, flight_l_bot, rail_h * 0.5 - 0.02, flight_l_top, rise_height + rail_h * 0.5 - 0.02, inner_c - 0.015, inner_c + 0.015, 0.03 ); // Postes verticales que nacen de la zanca y van al pasamanos const post_count = Math.max(4, Math.round(flight_len / 0.75) + 1); for(let k = 0; k < post_count; k++){ const t = k / (post_count - 1); const l_post = (dir === 1) ? (landing_size + t * flight_len) : ((length - landing_size) - t * flight_len); const y_base = t * rise_height; if(is_x){ append_box(buffers, l_post - 0.02, l_post + 0.02, y_base, y_base + rail_h, inner_c - 0.02, inner_c + 0.02); } else { append_box(buffers, inner_c - 0.02, inner_c + 0.02, y_base, y_base + rail_h, l_post - 0.02, l_post + 0.02); } } // 5. BARANDILLAS EN LOS DESCANSOS (Lado interior) const add_landing_rail = (l_start, y_level) => { const l_end = l_start + landing_size; // Pasamanos y travesaño if(is_x){ append_box(buffers, l_start, l_end, y_level + rail_h - 0.04, y_level + rail_h, inner_c - 0.02, inner_c + 0.02); append_box(buffers, l_start, l_end, y_level + rail_h * 0.5 - 0.02, y_level + rail_h * 0.5 + 0.01, inner_c - 0.015, inner_c + 0.015); append_box(buffers, l_start - 0.02, l_start + 0.02, y_level, y_level + rail_h, inner_c - 0.02, inner_c + 0.02); append_box(buffers, l_end - 0.02, l_end + 0.02, y_level, y_level + rail_h, inner_c - 0.02, inner_c + 0.02); } else { append_box(buffers, inner_c - 0.02, inner_c + 0.02, y_level + rail_h - 0.04, y_level + rail_h, l_start, l_end); append_box(buffers, inner_c - 0.015, inner_c + 0.015, y_level + rail_h * 0.5 - 0.02, y_level + rail_h * 0.5 + 0.01, l_start, l_end); append_box(buffers, inner_c - 0.02, inner_c + 0.02, y_level, y_level + rail_h, l_start - 0.02, l_start + 0.02); append_box(buffers, inner_c - 0.02, inner_c + 0.02, y_level, y_level + rail_h, l_end - 0.02, l_end + 0.02); } }; add_landing_rail(bot_l_start, 0); add_landing_rail(top_l_start, rise_height); // 6. CIERRE FRONTAL (si el descansillo da al vacío y no a una pared del hueco) if(hole){ const at_corner_start = is_x ? (stair.xf === hole.xf) : (stair.zf === hole.zf); const at_corner_end = is_x ? (stair.xt === hole.xt) : (stair.zt === hole.zt); // Cierre en extremo 0 (L = 0) if(!at_corner_start){ const y_base = (dir === 1) ? 0 : rise_height; if(is_x){ append_box(buffers, -0.02, 0.02, y_base + rail_h - 0.04, y_base + rail_h, -half_w, half_w); append_box(buffers, -0.02, 0.02, y_base, y_base + rail_h, outer_c - 0.02, outer_c + 0.02); } else { append_box(buffers, -half_w, half_w, y_base + rail_h - 0.04, y_base + rail_h, -0.02, 0.02); append_box(buffers, outer_c - 0.02, outer_c + 0.02, y_base, y_base + rail_h, -0.02, 0.02); } } // Cierre en extremo length (L = length) if(!at_corner_end){ const y_base = (dir === 1) ? rise_height : 0; if(is_x){ append_box(buffers, length - 0.02, length + 0.02, y_base + rail_h - 0.04, y_base + rail_h, -half_w, half_w); append_box(buffers, length - 0.02, length + 0.02, y_base, y_base + rail_h, outer_c - 0.02, outer_c + 0.02); } else { append_box(buffers, -half_w, half_w, y_base + rail_h - 0.04, y_base + rail_h, length - 0.02, length + 0.02); append_box(buffers, outer_c - 0.02, outer_c + 0.02, y_base, y_base + rail_h, length - 0.02, length + 0.02); } } } const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.Float32BufferAttribute(buffers.positions, 3)); geometry.setAttribute("normal", new THREE.Float32BufferAttribute(buffers.normals, 3)); geometry.setAttribute("uv", new THREE.Float32BufferAttribute(buffers.uvs, 2)); geometry.setIndex(buffers.indices); cache[key] = geometry; return geometry; }; this.reset = () => { for(const k in cache){ cache[k].dispose(); delete cache[k]; } }; }; return StairGeometryGame; })();