3esign's picture
Add 3 files
3ed905b verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fire Escape Simulator</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/PointerLockControls.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tweakpane/[email protected]/dist/tweakpane.min.js"></script>
<style>
body { margin: 0; overflow: hidden; }
#container { position: relative; }
#ui {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
color: white;
padding: 10px;
border-radius: 5px;
font-family: Arial, sans-serif;
}
#health-bar {
width: 200px;
height: 20px;
background: #333;
border-radius: 3px;
margin-top: 5px;
}
#health-fill {
height: 100%;
width: 100%;
background: #4CAF50;
border-radius: 3px;
transition: width 0.3s;
}
#smoke-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(50,50,50,0);
pointer-events: none;
transition: background 0.5s;
}
#start-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
font-family: Arial, sans-serif;
}
.btn {
background: #f97316;
color: white;
border: none;
padding: 10px 20px;
margin: 10px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
}
.btn:hover {
background: #ea580c;
}
#metrics {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
color: white;
padding: 10px;
border-radius: 5px;
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<div id="container">
<div id="ui">
<div>Fire Escape Simulator</div>
<div>Health: <span id="health-value">100</span>%</div>
<div id="health-bar"><div id="health-fill"></div></div>
<div>Time: <span id="time-value">0</span>s</div>
<div>Distance to Exit: <span id="distance-value">0</span>m</div>
</div>
<div id="smoke-overlay"></div>
<div id="metrics">
<div>Metrics:</div>
<div>Path Efficiency: <span id="efficiency-value">0</span>%</div>
<div>O2 Used: <span id="oxygen-value">0</span>%</div>
<div>Hazards Encountered: <span id="hazards-value">0</span></div>
</div>
<div id="start-screen">
<h1 class="text-4xl font-bold mb-8">Fire Escape Simulator</h1>
<p class="mb-8 text-center max-w-lg">A scientific instrument for studying evacuation behavior in emergency situations.<br>Test different scenarios and measure performance metrics.</p>
<div class="flex flex-wrap justify-center">
<button class="btn" onclick="startSimulation('fire')">Fire Scenario</button>
<button class="btn" onclick="startSimulation('smoke')">Smoke Scenario</button>
<button class="btn" onclick="startSimulation('equipment')">Equipment Training</button>
<button class="btn" onclick="startSimulation('full')">Full Simulation</button>
</div>
<div class="mt-8 text-sm opacity-70">
<p>Controls: WASD to move, SPACE to jump, MOUSE to look</p>
<p>ESC to pause/unpause</p>
</div>
</div>
</div>
<script>
// Core variables
let scene, camera, renderer, controls;
let maze = [];
let player = { x: 0, z: 0, health: 100 };
let exitPosition = { x: 0, z: 0 };
let simulationTime = 0;
let timerInterval;
let scenarioType = '';
let hazardsEncountered = 0;
let oxygenUsed = 0;
let pathHistory = [];
let optimalPathLength = 0;
// Initialize the scene
function init() {
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x333333);
// Create camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = 1.6;
// Create renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(renderer.domElement);
// Add basic lighting
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(0, 1, 0);
scene.add(directionalLight);
// Add fog for smoke effect
scene.fog = new THREE.FogExp2(0x999999, 0.01);
// Handle window resize
window.addEventListener('resize', onWindowResize, false);
// Add click handler to start pointer lock
document.addEventListener('click', () => {
if (scenarioType) {
document.body.requestPointerLock = document.body.requestPointerLock ||
document.body.mozRequestPointerLock ||
document.body.webkitRequestPointerLock;
document.body.requestPointerLock();
}
});
// Add pointer lock change listener
document.addEventListener('pointerlockchange', lockChangeAlert, false);
document.addEventListener('mozpointerlockchange', lockChangeAlert, false);
document.addEventListener('webkitpointerlockchange', lockChangeAlert, false);
}
function lockChangeAlert() {
if (document.pointerLockElement === document.body ||
document.mozPointerLockElement === document.body ||
document.webkitPointerLockElement === document.body) {
// Pointer was locked
document.addEventListener('mousemove', updatePosition, false);
} else {
// Pointer was unlocked
document.removeEventListener('mousemove', updatePosition, false);
}
}
function updatePosition(e) {
if (controls) {
controls.lookAround(e.movementX, e.movementY);
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Generate a simple maze
function generateMaze(size) {
maze = [];
for (let i = 0; i < size; i++) {
maze[i] = [];
for (let j = 0; j < size; j++) {
// 30% chance of a wall, except for start and exit positions
maze[i][j] = Math.random() < 0.3 ? 1 : 0;
}
}
// Ensure start (0,0) and exit (size-1, size-1) are clear
maze[0][0] = 0;
maze[size-1][size-1] = 0;
// Set player and exit positions
player.x = 0;
player.z = 0;
exitPosition.x = size - 1;
exitPosition.z = size - 1;
// Calculate optimal path length (Manhattan distance for simplicity)
optimalPathLength = (size - 1) * 2;
// Add walls to the scene
addWallsToScene();
// Add exit marker
addExitMarker();
// Add some hazards based on scenario
addScenarioHazards();
}
function addWallsToScene() {
// Clear existing walls
scene.children = scene.children.filter(obj => obj.userData.type !== 'wall');
const wallGeometry = new THREE.BoxGeometry(1, 2, 1);
const wallMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513 });
for (let i = 0; i < maze.length; i++) {
for (let j = 0; j < maze[i].length; j++) {
if (maze[i][j] === 1) {
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
wall.position.set(i, 0, j);
wall.userData.type = 'wall';
scene.add(wall);
}
}
}
// Add floor
const floorGeometry = new THREE.PlaneGeometry(maze.length, maze[0].length);
const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x555555, side: THREE.DoubleSide });
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2;
floor.position.set(maze.length / 2 - 0.5, -0.5, maze[0].length / 2 - 0.5);
scene.add(floor);
}
function addExitMarker() {
// Clear existing exit marker
scene.children = scene.children.filter(obj => obj.userData.type !== 'exit');
const exitGeometry = new THREE.BoxGeometry(0.5, 0.1, 0.5);
const exitMaterial = new THREE.MeshPhongMaterial({ color: 0x00FF00 });
const exitMarker = new THREE.Mesh(exitGeometry, exitMaterial);
exitMarker.position.set(exitPosition.x, 0.1, exitPosition.z);
exitMarker.userData.type = 'exit';
scene.add(exitMarker);
}
function addScenarioHazards() {
// Clear existing hazards
scene.children = scene.children.filter(obj => obj.userData.type !== 'hazard');
if (scenarioType === 'fire' || scenarioType === 'full') {
// Add fire hazards
const fireGeometry = new THREE.BoxGeometry(0.8, 0.8, 0.8);
const fireMaterial = new THREE.MeshPhongMaterial({ color: 0xFF4500, transparent: true, opacity: 0.8 });
for (let i = 0; i < 5; i++) {
const x = Math.floor(Math.random() * (maze.length - 2)) + 1;
const z = Math.floor(Math.random() * (maze[0].length - 2)) + 1;
if (maze[x][z] === 0 && !(x === 0 && z === 0) && !(x === exitPosition.x && z === exitPosition.z)) {
const fire = new THREE.Mesh(fireGeometry, fireMaterial);
fire.position.set(x, 0.5, z);
fire.userData.type = 'hazard';
fire.userData.damage = 5;
scene.add(fire);
}
}
}
if (scenarioType === 'smoke' || scenarioType === 'full') {
// Add smoke areas (will be handled in update function)
}
if (scenarioType === 'equipment' || scenarioType === 'full') {
// Add fire extinguishers
const extGeometry = new THREE.BoxGeometry(0.3, 0.6, 0.3);
const extMaterial = new THREE.MeshPhongMaterial({ color: 0xFF0000 });
for (let i = 0; i < 3; i++) {
const x = Math.floor(Math.random() * (maze.length - 2)) + 1;
const z = Math.floor(Math.random() * (maze[0].length - 2)) + 1;
if (maze[x][z] === 0 && !(x === 0 && z === 0) && !(x === exitPosition.x && z === exitPosition.z)) {
const extinguisher = new THREE.Mesh(extGeometry, extMaterial);
extinguisher.position.set(x, 0.3, z);
extinguisher.userData.type = 'equipment';
scene.add(extinguisher);
}
}
}
}
function startSimulation(type) {
scenarioType = type;
document.getElementById('start-screen').style.display = 'none';
// Generate maze (10x10 for this demo)
generateMaze(10);
// Reset player stats
player.health = 100;
hazardsEncountered = 0;
oxygenUsed = 0;
simulationTime = 0;
pathHistory = [{x: player.x, z: player.z}];
// Update UI
updateUI();
// Create controls
controls = new THREE.PointerLockControls(camera, document.body);
// Start timer
timerInterval = setInterval(() => {
simulationTime++;
updateUI();
// Oxygen consumption in smoke scenario
if (scenarioType === 'smoke' || scenarioType === 'full') {
oxygenUsed += 0.5;
if (oxygenUsed >= 100) {
player.health -= 1;
}
}
// Check if player reached exit
if (Math.abs(player.x - exitPosition.x) < 0.5 && Math.abs(player.z - exitPosition.z) < 0.5) {
endSimulation(true);
}
// Check if player died
if (player.health <= 0) {
endSimulation(false);
}
}, 1000);
// Start animation loop
animate();
}
function endSimulation(success) {
clearInterval(timerInterval);
// Remove controls
if (controls) {
controls.dispose();
controls = null;
}
// Show results
const startScreen = document.getElementById('start-screen');
startScreen.style.display = 'flex';
const efficiency = Math.round((optimalPathLength / pathHistory.length) * 100);
startScreen.innerHTML = `
<h1 class="text-4xl font-bold mb-8">${success ? 'ESCAPE SUCCESSFUL!' : 'ESCAPE FAILED'}</h1>
<div class="bg-gray-800 p-6 rounded-lg mb-8">
<h2 class="text-2xl font-semibold mb-4">Simulation Metrics</h2>
<div class="grid grid-cols-2 gap-4">
<div>Scenario:</div><div>${scenarioType}</div>
<div>Time:</div><div>${simulationTime}s</div>
<div>Health Remaining:</div><div>${player.health}%</div>
<div>Path Efficiency:</div><div>${efficiency}%</div>
<div>O2 Used:</div><div>${oxygenUsed}%</div>
<div>Hazards Encountered:</div><div>${hazardsEncountered}</div>
</div>
</div>
<button class="btn" onclick="location.reload()">Run New Simulation</button>
`;
}
function updateUI() {
document.getElementById('health-value').textContent = player.health;
document.getElementById('health-fill').style.width = `${player.health}%`;
document.getElementById('time-value').textContent = simulationTime;
// Calculate distance to exit
const distance = Math.sqrt(
Math.pow(player.x - exitPosition.x, 2) +
Math.pow(player.z - exitPosition.z, 2)
);
document.getElementById('distance-value').textContent = distance.toFixed(1);
// Update metrics
const efficiency = Math.round((optimalPathLength / pathHistory.length) * 100);
document.getElementById('efficiency-value').textContent = efficiency;
document.getElementById('oxygen-value').textContent = oxygenUsed;
document.getElementById('hazards-value').textContent = hazardsEncountered;
// Update smoke overlay for smoke scenario
if (scenarioType === 'smoke' || scenarioType === 'full') {
const smokeIntensity = Math.min(oxygenUsed / 100, 0.7);
document.getElementById('smoke-overlay').style.background = `rgba(50,50,50,${smokeIntensity})`;
}
}
function animate() {
requestAnimationFrame(animate);
if (controls && controls.isLocked) {
// Simple collision detection
const moveSpeed = 0.1;
const newPosition = {
x: camera.position.x,
z: camera.position.z
};
// Check keyboard input
if (keyState['w']) {
newPosition.x -= Math.sin(camera.rotation.y) * moveSpeed;
newPosition.z -= Math.cos(camera.rotation.y) * moveSpeed;
}
if (keyState['s']) {
newPosition.x += Math.sin(camera.rotation.y) * moveSpeed;
newPosition.z += Math.cos(camera.rotation.y) * moveSpeed;
}
if (keyState['a']) {
newPosition.x -= Math.cos(camera.rotation.y) * moveSpeed;
newPosition.z += Math.sin(camera.rotation.y) * moveSpeed;
}
if (keyState['d']) {
newPosition.x += Math.cos(camera.rotation.y) * moveSpeed;
newPosition.z -= Math.sin(camera.rotation.y) * moveSpeed;
}
// Check if new position is valid (not inside a wall)
const gridX = Math.round(newPosition.x);
const gridZ = Math.round(newPosition.z);
if (gridX >= 0 && gridX < maze.length && gridZ >= 0 && gridZ < maze[0].length) {
if (maze[gridX][gridZ] === 0) {
camera.position.x = newPosition.x;
camera.position.z = newPosition.z;
player.x = camera.position.x;
player.z = camera.position.z;
// Record path
const lastPos = pathHistory[pathHistory.length - 1];
if (Math.abs(lastPos.x - player.x) > 0.3 || Math.abs(lastPos.z - player.z) > 0.3) {
pathHistory.push({x: player.x, z: player.z});
}
}
}
// Check for hazards
scene.children.forEach(obj => {
if (obj.userData.type === 'hazard') {
const distance = Math.sqrt(
Math.pow(camera.position.x - obj.position.x, 2) +
Math.pow(camera.position.z - obj.position.z, 2)
);
if (distance < 1) {
player.health -= obj.userData.damage * 0.1;
hazardsEncountered += 0.1;
updateUI();
}
}
});
}
renderer.render(scene, camera);
}
// Keyboard state tracking
const keyState = {};
document.addEventListener('keydown', (e) => {
keyState[e.key.toLowerCase()] = true;
// Space to jump
if (e.key === ' ' && controls && controls.isLocked) {
camera.position.y = 1.6; // Simple jump reset
}
// ESC to unlock pointer
if (e.key === 'Escape') {
document.exitPointerLock = document.exitPointerLock ||
document.mozExitPointerLock ||
document.webkitExitPointerLock;
document.exitPointerLock();
}
});
document.addEventListener('keyup', (e) => {
keyState[e.key.toLowerCase()] = false;
});
// Initialize the app
init();
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=3esign/bim-fire-escape-simulator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>