"use strict"; const ElevatorObjectGame = (function(){ const STATES = { IDLE_CLOSED: "idle_closed", WAITING_ARRIVAL: "waiting_arrival", // Pequeña espera tras pulsar llamada OPENING: "opening", IDLE_OPEN: "idle_open", // Abierto esperando entrada CLOSING: "closing", TRANSIT: "transit" // Viaje a oscuras }; const ROTATIONS = { south: 0, north: Math.PI, east: Math.PI * 0.5, west: -Math.PI * 0.5 }; // Función de interpolación suave (aceleración y deceleración) const smoothstep = t => t * t * (3 - 2 * t); const ElevatorObjectGame = function(hollow_bond, model, material_factory, geometry_factory){ const self = this; this.model = model; this.state = STATES.IDLE_CLOSED; this.door_openness = 0.0; // 0 = cerrada, 1 = abierta this.timer = 0.0; const DOOR_TRANSITION_TIME = 1.25; // Duración de apertura/cierre en segundos const geom_fac = geometry_factory || (hollow_bond.geometries && hollow_bond.geometries.elevator) || new ElevatorGeometryGame(hollow_bond); const mat_fac = material_factory || (hollow_bond.materials && hollow_bond.materials.elevator) || new ElevatorMaterialGame(hollow_bond); // 1. Contenedor Raíz this.mesh = new THREE.Group(); const base_x = model.x + (model.width * 0.5); const base_y = model.y * hollow_bond.room_height; const base_z = model.z + (model.depth * 0.5); this.mesh.position.set(base_x, base_y, base_z); this.mesh.rotation.y = ROTATIONS[model.facing] || 0; // 2. Malla de la Cabina const cabin_geo = geom_fac.get_cabin_geometry(model); const cabin_mat = mat_fac.get_cabin_material(); this.cabin_mesh = new THREE.Mesh(cabin_geo, cabin_mat); this.mesh.add(this.cabin_mesh); // 3. Plafón Fluorescente const light_geo = geom_fac.get_light_geometry(model); this.light_mat = mat_fac.get_light_material(); this.light_mesh = new THREE.Mesh(light_geo, this.light_mat); this.mesh.add(this.light_mesh); // 4. Puertas Correderas (Hojas Izquierda y Derecha) const door_geo = geom_fac.get_door_leaf_geometry(model); const hd = (model.depth || hollow_bond.elevator_size || 2.0) * 0.5; this.door_left = new THREE.Mesh(door_geo, cabin_mat); this.door_left.position.set(-0.28, 0, hd - 0.04); this.mesh.add(this.door_left); this.door_right = new THREE.Mesh(door_geo, cabin_mat); this.door_right.position.set(0.28, 0, hd - 0.04); this.mesh.add(this.door_right); // 5. Marco Frontal const frame_geom_fac = (hollow_bond.geometries && hollow_bond.geometries.elevator_frame) || new ElevatorFrameGeometryGame(hollow_bond); const frame_geo = frame_geom_fac.get_for_elevator(model); this.frame_mesh = new THREE.Mesh(frame_geo, [cabin_mat, this.light_mat]); this.frame_mesh.position.set(0, 0, hd); this.mesh.add(this.frame_mesh); // 6. Collider físico dinámico this.collider = new ElevatorCollider(hollow_bond, model, self); /** * Comprueba si el rayo de la cámara apunta al ascensor (marco, puertas o botón) */ this.try_click_button = raycaster => { const hits = raycaster.intersectObject(self.mesh, true); // 1. Detección por rayo directo a cualquier parte de la cabina o puertas if(hits.length > 0 && hits[0].distance <= 3.2){ return self.trigger_interaction(); } // 2. Fallback por proximidad: si estás frente a la puerta (< 2.2m) y haces click const dungeon = hollow_bond.dungeon; if(dungeon && dungeon.camera){ const p_pos = dungeon.camera.position; const dist_sq = (p_pos.x - base_x) * (p_pos.x - base_x) + (p_pos.z - base_z) * (p_pos.z - base_z); const feet_y = p_pos.y - 1.75; if(dist_sq <= (2.4 * 2.4) && Math.abs(feet_y - base_y) < 1.0){ return self.trigger_interaction(); } } return false; }; /** * Transición de estados al pulsar el botón */ this.trigger_interaction = () => { const dungeon = hollow_bond.dungeon; if(!dungeon || !dungeon.camera) return false; const is_in = self.collider.is_inside(dungeon.camera.position.x, dungeon.camera.position.z); // Botón exterior (llamada) if(!is_in && self.state === STATES.IDLE_CLOSED){ self.state = STATES.WAITING_ARRIVAL; self.timer = 1.5; // Breve espera con sonido/parpadeo de llegada return true; } // Botón interior (cierre y despegue) if(is_in && (self.state === STATES.IDLE_OPEN || self.state === STATES.IDLE_CLOSED)){ self.state = STATES.CLOSING; self.timer = 0.0; return true; } return false; }; const execute_transit = () => { const map = hollow_bond.map; if(!map || !map.elevators) return; const group_cabins = []; for(const eid in map.elevators){ const el = map.elevators[eid]; if(el.group_id === model.group_id){ group_cabins.push(el); } } if(group_cabins.length <= 1) return; group_cabins.sort((a, b) => a.floor - b.floor); const cur_index = group_cabins.findIndex(c => c.id === model.id); const next_index = (cur_index + 1) % group_cabins.length; const target_model = group_cabins[next_index]; const dungeon = hollow_bond.dungeon; if(!dungeon || !dungeon.camera) return; const target_cx = target_model.x + (target_model.width * 0.5); const target_cz = target_model.z + (target_model.depth * 0.5); const target_by = target_model.y * hollow_bond.room_height; const eye_h = (hollow_bond.controls && hollow_bond.controls.game) ? hollow_bond.controls.game.current_height : 1.75; dungeon.camera.position.set(target_cx, target_by + eye_h, target_cz); const angle_delta = (ROTATIONS[target_model.facing] || 0) - (ROTATIONS[model.facing] || 0); if(hollow_bond.controls && hollow_bond.controls.game){ hollow_bond.controls.game.yaw = (hollow_bond.controls.game.yaw || 0) + angle_delta; } dungeon.check_room_transition && dungeon.check_room_transition(); dungeon.update_rooms_streaming && dungeon.update_rooms_streaming(); const target_obj = dungeon.objects.elevator ? dungeon.objects.elevator[target_model.id] : null; if(target_obj){ target_obj.state = STATES.OPENING; target_obj.timer = 0.0; } self.state = STATES.IDLE_CLOSED; self.door_openness = 0.0; }; /** * Actualización continua de estados y animación con Smoothstep */ this.update = delta => { switch(self.state){ case STATES.WAITING_ARRIVAL: self.timer -= delta; if(self.timer <= 0){ self.state = STATES.OPENING; self.timer = 0.0; } break; case STATES.OPENING: self.timer += delta; const open_t = Math.min(1.0, self.timer / DOOR_TRANSITION_TIME); self.door_openness = smoothstep(open_t); if(open_t >= 1.0){ self.state = STATES.IDLE_OPEN; self.timer = 5.0; // Se queda abierto 5 segundos } break; case STATES.IDLE_OPEN: self.timer -= delta; if(self.timer <= 0){ self.state = STATES.CLOSING; self.timer = 0.0; } break; case STATES.CLOSING: self.timer += delta; const close_t = Math.min(1.0, self.timer / DOOR_TRANSITION_TIME); self.door_openness = 1.0 - smoothstep(close_t); if(close_t >= 1.0){ self.door_openness = 0.0; const dungeon = hollow_bond.dungeon; const is_in = dungeon && dungeon.camera && self.collider.is_inside(dungeon.camera.position.x, dungeon.camera.position.z); if(is_in){ self.state = STATES.TRANSIT; self.timer = 2.4; } else { self.state = STATES.IDLE_CLOSED; } } break; case STATES.TRANSIT: self.timer -= delta; if(self.light_mesh){ self.light_mesh.visible = (Math.random() > 0.07); } if(self.timer <= 0){ if(self.light_mesh) self.light_mesh.visible = true; execute_transit(); } break; } // Desplazamiento visual de las hojas con holgura en el carril const slide = self.door_openness * 0.52; this.door_left.position.x = -0.28 - slide; this.door_right.position.x = 0.28 + slide; }; this.destroy = () => { if(self.mesh && self.mesh.parent){ self.mesh.parent.remove(self.mesh); } }; }; return ElevatorObjectGame; })();