Spaces:
Sleeping
Sleeping
<html> | |
<head> | |
<title>Apartment Simulator Level Editor</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script> | |
<style> | |
body { | |
margin: 0; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
font-family: Arial, sans-serif; | |
} | |
#controls { | |
margin: 10px; | |
padding: 10px; | |
background: #f0f0f0; | |
border-radius: 5px; | |
} | |
button { | |
margin: 0 5px; | |
padding: 5px 10px; | |
cursor: pointer; | |
} | |
select, input[type="number"] { | |
margin: 0 5px; | |
padding: 5px; | |
} | |
.active { | |
background: #4CAF50; | |
color: white; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="controls"> | |
<button id="wallBtn" onclick="setTool('wall')">Wall</button> | |
<button id="doorBtn" onclick="setTool('door')">Door</button> | |
<button id="eraseBtn" onclick="setTool('erase')">Erase</button> | |
<button id="characterBtn" onclick="setTool('character')">Place Character</button> | |
| | |
<button id="yellowBtn" onclick="setTool('yellow')" style="background-color: #ffeb3b">Yellow</button> | |
<button id="blueBtn" onclick="setTool('blue')" style="background-color: #2196f3">Blue</button> | |
<button id="greenBtn" onclick="setTool('green')" style="background-color: #4caf50">Green</button> | |
<button id="redBtn" onclick="setTool('red')" style="background-color: #f44336">Red</button> | |
| | |
<input type="text" id="roomName" placeholder="Room name"> | |
<button id="assignRoom" onclick="setTool('assign')">Assign Room</button> | |
<select id="roomSelect"> | |
<option value="">Select Destination Room</option> | |
</select> | |
<select id="viaRoomSelect"> | |
<option value="">Via Room (Optional)</option> | |
</select> | |
<button id="removeRoom" onclick="removeSelectedRoom()">Remove Room</button> | |
<button id="goBtn" onclick="startPathfinding()">Go!</button> | |
| | |
<button onclick="saveMap()">Save Map</button> | |
<button onclick="loadMap()">Load Map</button> | |
| | |
<input type="number" id="gridCols" value="40" min="1" style="width: 60px"> × | |
<input type="number" id="gridRows" value="20" min="1" style="width: 60px"> | |
<button onclick="resizeGrid()">Resize Grid</button> | |
</div> | |
<!-- Insert a new container for the P5 sketch --> | |
<div id="sketch-container"></div> | |
<!-- Move the JSON text areas here so they appear after the editor and canvas --> | |
<div style="margin-top: 20px; width: 100%; max-width: 1200px; text-align: center;"> | |
<textarea id="roomsJson" readonly style="width: 90%; height: 100px; margin: 10px; padding: 10px;" placeholder="Rooms will appear here in JSON format"></textarea> | |
<textarea id="gameJson" readonly style="width: 90%; height: 200px; margin: 10px; padding: 10px;" placeholder="Complete game definition will appear here in JSON format"></textarea> | |
</div> | |
<script> | |
const CELL_SIZE = 30; | |
let GRID_COLS = 40; | |
let GRID_ROWS = 20; | |
let grid = []; | |
let rooms = new Map(); // roomName -> array of cells | |
let currentTool = 'wall'; | |
let characterPos = null; | |
let isDragging = false; | |
let roomBeingAssigned = null; | |
let path = []; | |
let isMoving = false; | |
let moveInterval = null; | |
function setup() { | |
GRID_COLS = parseInt(document.getElementById('gridCols').value) || 40; | |
GRID_ROWS = parseInt(document.getElementById('gridRows').value) || 20; | |
// Attach the P5 canvas to #sketch-container | |
createCanvas(GRID_COLS * CELL_SIZE, GRID_ROWS * CELL_SIZE) | |
.parent('sketch-container'); | |
initGrid(); | |
} | |
function initGrid() { | |
grid = []; // Clear the grid first | |
for (let y = 0; y < GRID_ROWS; y++) { | |
grid[y] = []; | |
for (let x = 0; x < GRID_COLS; x++) { | |
grid[y][x] = { | |
type: 'empty', | |
room: null, | |
color: null | |
}; | |
} | |
} | |
} | |
function draw() { | |
background(255); | |
drawGrid(); | |
drawRooms(); | |
drawWallsAndDoors(); | |
drawPath(); | |
drawCharacter(); | |
// Draw hover effect | |
if (mouseInCanvas()) { | |
const [gridX, gridY] = getGridCoord(mouseX, mouseY); | |
noFill(); | |
stroke(100); | |
rect(gridX * CELL_SIZE, gridY * CELL_SIZE, CELL_SIZE, CELL_SIZE); | |
} | |
} | |
function drawGrid() { | |
stroke(200); | |
for (let x = 0; x <= GRID_COLS; x++) { | |
line(x * CELL_SIZE, 0, x * CELL_SIZE, height); | |
} | |
for (let y = 0; y <= GRID_ROWS; y++) { | |
line(0, y * CELL_SIZE, width, y * CELL_SIZE); | |
} | |
} | |
function drawWallsAndDoors() { | |
for (let y = 0; y < GRID_ROWS; y++) { | |
for (let x = 0; x < GRID_COLS; x++) { | |
const cell = grid[y][x]; | |
if (cell.type === 'wall') { | |
fill(0); | |
noStroke(); | |
rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); | |
} else if (cell.type === 'door') { | |
fill(139, 69, 19); | |
noStroke(); | |
rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); | |
} else if (cell.color) { | |
noStroke(); | |
switch(cell.color) { | |
case 'yellow': | |
fill('#ffeb3b'); | |
break; | |
case 'blue': | |
fill('#2196f3'); | |
break; | |
case 'green': | |
fill('#4caf50'); | |
break; | |
case 'red': | |
fill('#f44336'); | |
break; | |
} | |
rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE); | |
} | |
} | |
} | |
} | |
function drawRooms() { | |
for (let [roomName, cells] of rooms) { | |
const hue = stringToHue(roomName); | |
fill(hue, 30, 95, 0.3); | |
noStroke(); | |
for (let cell of cells) { | |
rect(cell.x * CELL_SIZE, cell.y * CELL_SIZE, CELL_SIZE, CELL_SIZE); | |
} | |
// Draw room name on first cell | |
if (cells.length > 0) { | |
fill(0); | |
textAlign(CENTER, CENTER); | |
textSize(10); | |
text(roomName, cells[0].x * CELL_SIZE + CELL_SIZE/2, cells[0].y * CELL_SIZE + CELL_SIZE/2); | |
} | |
} | |
} | |
function drawCharacter() { | |
if (characterPos) { | |
textSize(CELL_SIZE * 0.8); | |
textAlign(CENTER, CENTER); | |
text('👧', | |
characterPos.x * CELL_SIZE + CELL_SIZE/2, | |
characterPos.y * CELL_SIZE + CELL_SIZE/2 | |
); | |
} | |
} | |
function mousePressed() { | |
if (!mouseInCanvas()) return; | |
isDragging = true; | |
handleDraw(); | |
} | |
function mouseDragged() { | |
if (isDragging) handleDraw(); | |
} | |
function mouseReleased() { | |
isDragging = false; | |
if (currentTool === 'assign') { | |
const roomName = document.getElementById('roomName').value; | |
if (roomName && roomBeingAssigned && roomBeingAssigned.length > 0) { | |
rooms.set(roomName, roomBeingAssigned); | |
updateRoomDropdown(); | |
} | |
roomBeingAssigned = null; | |
} | |
} | |
function handleDraw() { | |
if (!mouseInCanvas()) return; | |
const [gridX, gridY] = getGridCoord(mouseX, mouseY); | |
switch(currentTool) { | |
case 'wall': | |
grid[gridY][gridX].type = 'wall'; | |
grid[gridY][gridX].color = null; | |
break; | |
case 'door': | |
grid[gridY][gridX].type = 'door'; | |
grid[gridY][gridX].color = null; | |
break; | |
case 'erase': | |
grid[gridY][gridX].type = 'empty'; | |
grid[gridY][gridX].color = null; | |
break; | |
case 'character': | |
characterPos = {x: gridX, y: gridY}; | |
currentTool = 'wall'; // Reset to wall tool after placing character | |
break; | |
case 'assign': | |
if (!roomBeingAssigned) roomBeingAssigned = []; | |
roomBeingAssigned.push({x: gridX, y: gridY}); | |
break; | |
case 'yellow': | |
case 'blue': | |
case 'green': | |
case 'red': | |
grid[gridY][gridX].type = 'empty'; | |
grid[gridY][gridX].color = currentTool; | |
break; | |
} | |
updateGameJson(); // Update game JSON after any grid changes | |
} | |
function setTool(tool) { | |
currentTool = tool; | |
// Reset room being assigned if switching tools | |
if (tool !== 'assign') roomBeingAssigned = null; | |
// Update button states | |
document.querySelectorAll('button').forEach(btn => btn.classList.remove('active')); | |
document.getElementById(tool + 'Btn')?.classList.add('active'); | |
} | |
function mouseInCanvas() { | |
return mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height; | |
} | |
function getGridCoord(x, y) { | |
const gridX = Math.floor(x / CELL_SIZE); | |
const gridY = Math.floor(y / CELL_SIZE); | |
return [ | |
Math.min(Math.max(gridX, 0), GRID_COLS - 1), | |
Math.min(Math.max(gridY, 0), GRID_ROWS - 1) | |
]; | |
} | |
function stringToHue(str) { | |
let hash = 0; | |
for (let i = 0; i < str.length; i++) { | |
hash = str.charCodeAt(i) + ((hash << 5) - hash); | |
} | |
return hash % 360; | |
} | |
function updateRoomDropdown() { | |
const select = document.getElementById('roomSelect'); | |
const viaSelect = document.getElementById('viaRoomSelect'); | |
select.innerHTML = '<option value="">Select Destination Room</option>'; | |
viaSelect.innerHTML = '<option value="">Via Room (Optional)</option>'; | |
for (let roomName of rooms.keys()) { | |
const option = document.createElement('option'); | |
option.value = roomName; | |
option.textContent = roomName; | |
select.appendChild(option.cloneNode(true)); | |
viaSelect.appendChild(option); | |
} | |
updateRoomsJson(); | |
} | |
function updateRoomsJson() { | |
const roomsTextArea = document.getElementById('roomsJson'); | |
const roomsList = Array.from(rooms.keys()); | |
roomsTextArea.value = JSON.stringify(roomsList, null, 2); | |
updateGameJson(); // Update game JSON whenever rooms are updated | |
} | |
function updateGameJson() { | |
const gameTextArea = document.getElementById('gameJson'); | |
const gameData = { | |
grid: grid, | |
rooms: Array.from(rooms.entries()), | |
characterPos: characterPos, | |
gridCols: GRID_COLS, | |
gridRows: GRID_ROWS | |
}; | |
gameTextArea.value = JSON.stringify(gameData, null, 2); | |
} | |
function startPathfinding() { | |
const targetRoom = document.getElementById('roomSelect').value; | |
const viaRoom = document.getElementById('viaRoomSelect').value; | |
if (!targetRoom || !characterPos) return; | |
const targetCells = rooms.get(targetRoom); | |
if (!targetCells || targetCells.length === 0) return; | |
if (viaRoom && viaRoom !== targetRoom) { | |
const viaCells = rooms.get(viaRoom); | |
if (!viaCells || viaCells.length === 0) return; | |
// Find path to via room first | |
const pathToVia = findPath(characterPos, viaCells[0]); | |
if (pathToVia.length === 0) return; // No path to via room | |
// Find path from via room to target | |
const pathFromVia = findPath(viaCells[0], targetCells[0]); | |
if (pathFromVia.length === 0) return; // No path from via to target | |
// Combine paths (remove duplicate via point) | |
path = [...pathToVia, ...pathFromVia.slice(1)]; | |
} else { | |
// Direct path to target | |
path = findPath(characterPos, targetCells[0]); | |
} | |
if (path.length > 0) { | |
isMoving = true; | |
moveCharacterAlongPath(); | |
} | |
} | |
function findPath(start, end) { | |
const queue = [[start]]; | |
const visited = new Set(); | |
const key = pos => `${pos.x},${pos.y}`; | |
visited.add(key(start)); | |
while (queue.length > 0) { | |
const currentPath = queue.shift(); | |
const current = currentPath[currentPath.length - 1]; | |
if (current.x === end.x && current.y === end.y) { | |
return currentPath; | |
} | |
// Get neighbors (up, right, down, left) | |
const neighbors = [ | |
{x: current.x, y: current.y - 1}, | |
{x: current.x + 1, y: current.y}, | |
{x: current.x, y: current.y + 1}, | |
{x: current.x - 1, y: current.y} | |
]; | |
for (const next of neighbors) { | |
if (next.x < 0 || next.x >= GRID_COLS || next.y < 0 || next.y >= GRID_ROWS) continue; | |
const nextKey = key(next); | |
if (visited.has(nextKey)) continue; | |
// Check if the cell is walkable (empty or door) | |
const cell = grid[next.y][next.x]; | |
if (cell.type === 'wall') continue; | |
visited.add(nextKey); | |
queue.push([...currentPath, next]); | |
} | |
} | |
return []; // No path found | |
} | |
function moveCharacterAlongPath() { | |
if (moveInterval) clearInterval(moveInterval); | |
moveInterval = setInterval(() => { | |
if (path.length === 0) { | |
isMoving = false; | |
clearInterval(moveInterval); | |
return; | |
} | |
const nextPos = path.shift(); | |
characterPos = nextPos; | |
}, 200); // Move every 200ms | |
} | |
function drawPath() { | |
if (path.length > 0 && isMoving) { | |
noFill(); | |
stroke(0, 255, 0); | |
strokeWeight(2); | |
// Draw line from character to first path point | |
line( | |
characterPos.x * CELL_SIZE + CELL_SIZE/2, | |
characterPos.y * CELL_SIZE + CELL_SIZE/2, | |
path[0].x * CELL_SIZE + CELL_SIZE/2, | |
path[0].y * CELL_SIZE + CELL_SIZE/2 | |
); | |
// Draw lines between path points | |
for (let i = 0; i < path.length - 1; i++) { | |
line( | |
path[i].x * CELL_SIZE + CELL_SIZE/2, | |
path[i].y * CELL_SIZE + CELL_SIZE/2, | |
path[i + 1].x * CELL_SIZE + CELL_SIZE/2, | |
path[i + 1].y * CELL_SIZE + CELL_SIZE/2 | |
); | |
} | |
strokeWeight(1); | |
} | |
} | |
function saveMap() { | |
const mapData = { | |
grid: grid, | |
rooms: Array.from(rooms.entries()), | |
characterPos: characterPos, | |
gridCols: GRID_COLS, | |
gridRows: GRID_ROWS | |
}; | |
localStorage.setItem('apartmentMap', JSON.stringify(mapData)); | |
alert('Map saved!'); | |
updateGameJson(); // Update game JSON after saving | |
} | |
function loadMap() { | |
const mapData = localStorage.getItem('apartmentMap'); | |
if (!mapData) { | |
alert('No saved map found!'); | |
return; | |
} | |
const data = JSON.parse(mapData); | |
// Update grid dimensions and input fields | |
GRID_COLS = data.gridCols || 40; | |
GRID_ROWS = data.gridRows || 20; | |
document.getElementById('gridCols').value = GRID_COLS; | |
document.getElementById('gridRows').value = GRID_ROWS; | |
// Resize canvas and initialize grid | |
resizeCanvas(GRID_COLS * CELL_SIZE, GRID_ROWS * CELL_SIZE); | |
initGrid(); | |
// Load saved data | |
grid = data.grid; | |
rooms = new Map(data.rooms); | |
characterPos = data.characterPos; | |
updateRoomDropdown(); | |
updateGameJson(); // Update game JSON after loading | |
} | |
function removeSelectedRoom() { | |
const roomName = document.getElementById('roomSelect').value; | |
if (!roomName) return; | |
// Remove room from the rooms Map | |
rooms.delete(roomName); | |
// Update the dropdown | |
updateRoomDropdown(); | |
} | |
function resizeGrid() { | |
const newCols = parseInt(document.getElementById('gridCols').value) || 40; | |
const newRows = parseInt(document.getElementById('gridRows').value) || 20; | |
// Save old grid data | |
const oldGrid = JSON.parse(JSON.stringify(grid)); // Deep copy | |
const oldCols = GRID_COLS; | |
const oldRows = GRID_ROWS; | |
// Update dimensions | |
GRID_COLS = newCols; | |
GRID_ROWS = newRows; | |
// Initialize new grid first | |
initGrid(); | |
// Copy old data to new grid where possible | |
for (let y = 0; y < Math.min(oldRows, GRID_ROWS); y++) { | |
for (let x = 0; x < Math.min(oldCols, GRID_COLS); x++) { | |
if (oldGrid[y] && oldGrid[y][x]) { | |
grid[y][x] = oldGrid[y][x]; | |
} | |
} | |
} | |
// Resize canvas | |
resizeCanvas(GRID_COLS * CELL_SIZE, GRID_ROWS * CELL_SIZE); | |
// Adjust character position if outside new bounds | |
if (characterPos) { | |
if (characterPos.x >= GRID_COLS) characterPos.x = GRID_COLS - 1; | |
if (characterPos.y >= GRID_ROWS) characterPos.y = GRID_ROWS - 1; | |
} | |
// Adjust room cells to fit new grid | |
for (let [roomName, cells] of rooms) { | |
rooms.set(roomName, cells.filter(cell => | |
cell.x < GRID_COLS && cell.y < GRID_ROWS | |
)); | |
} | |
// Clear path if any | |
path = []; | |
if (moveInterval) clearInterval(moveInterval); | |
isMoving = false; | |
updateGameJson(); // Update game JSON after resizing | |
} | |
</script> | |
</body> | |
</html> |