refactor(all): only use single html, replace p5js control panel with plain html
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
145
css/style.css
145
css/style.css
@@ -44,16 +44,145 @@ canvas {
|
|||||||
height: min(62vh, 680px);
|
height: min(62vh, 680px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#p5-panel {
|
.control-panel {
|
||||||
display: flex;
|
padding: 16px;
|
||||||
justify-content: center;
|
overflow-y: auto;
|
||||||
padding: 8px 0 14px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#p5-panel canvas {
|
.control-group {
|
||||||
width: min(100%, 1000px);
|
margin-bottom: 20px;
|
||||||
height: auto;
|
}
|
||||||
border-radius: 12px;
|
|
||||||
|
.control-group h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #93a4b8;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-button {
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: #17314c;
|
||||||
|
border: 1px solid #284055;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #eef5ff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-button:hover {
|
||||||
|
background: #1f3a57;
|
||||||
|
border-color: #3a5070;
|
||||||
|
box-shadow: 0 4px 12px rgba(125, 180, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-button:active {
|
||||||
|
background: #17314c;
|
||||||
|
border-color: #7db4ff;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #dbe6f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-group label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-group input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
height: 6px;
|
||||||
|
background: #1f3a57;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-group input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #79aef2;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 6px rgba(121, 174, 242, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-group input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #79aef2;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 6px rgba(121, 174, 242, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-group span {
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: right;
|
||||||
|
color: #79aef2;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-display {
|
||||||
|
background: rgba(25, 40, 55, 0.5);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-line {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 0;
|
||||||
|
color: #dbe6f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-line strong {
|
||||||
|
color: #93a4b8;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-line span {
|
||||||
|
color: #79aef2;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
color: #91a4b8;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
76
index.html
76
index.html
@@ -1,27 +1,67 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" type="text/css" href="./css/style.css" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/style.css" />
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||||
<meta charset="utf-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>P5.js Template</title>
|
<title>Untitled Maze Game</title>
|
||||||
</head>
|
</head>
|
||||||
|
<body class="maze-page">
|
||||||
|
<main class="maze-layout">
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-label">Babylon Scene</div>
|
||||||
|
<canvas id="renderCanvas"></canvas>
|
||||||
|
</section>
|
||||||
|
|
||||||
<body>
|
<section class="panel">
|
||||||
<div class="options">
|
<div class="panel-label">Game Controls</div>
|
||||||
<div class="option">
|
<div id="control-panel" class="control-panel">
|
||||||
<div class="emoji">🐪</div>
|
<div class="control-group">
|
||||||
<div class="description">
|
<h3>Run Controls</h3>
|
||||||
<a href="single_sketch.html">Single sketch</a>
|
<div class="button-group">
|
||||||
</div>
|
<button id="btn-start" class="game-button">Start run</button>
|
||||||
</div>
|
<button id="btn-restart" class="game-button">Restart level</button>
|
||||||
|
<button id="btn-randomize" class="game-button">Randomize seed</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="option">
|
<div class="control-group">
|
||||||
<div class="emoji">🐫</div>
|
<h3>Settings</h3>
|
||||||
<div class="description">
|
<div class="slider-group">
|
||||||
<a href="multi_sketch.html">Multi sketch</a>
|
<label>Maze width:
|
||||||
|
<input id="slider-width" type="range" min="9" max="31" step="2" value="11" />
|
||||||
|
<span id="value-width">11</span> cells
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="slider-group">
|
||||||
|
<label>Maze height:
|
||||||
|
<input id="slider-height" type="range" min="9" max="31" step="2" value="11" />
|
||||||
|
<span id="value-height">11</span> cells
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="slider-group">
|
||||||
|
<label>Min chest dead-ends:
|
||||||
|
<input id="slider-deadends" type="range" min="1" max="10" step="1" value="2" />
|
||||||
|
<span id="value-deadends">2</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>Status</h3>
|
||||||
|
<div class="status-display">
|
||||||
|
<div class="status-line"><strong>Seed:</strong> <span id="status-seed">0</span></div>
|
||||||
|
<div class="status-line"><strong>Level:</strong> <span id="status-level">1</span></div>
|
||||||
|
<div class="status-line"><strong>Time:</strong> <span id="status-time">0.0</span>s</div>
|
||||||
|
<div class="status-line"><strong>Key:</strong> <span id="status-key">no</span></div>
|
||||||
|
<div class="status-message" id="status-message">Adjust settings, then start a run.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/src/multi_sketch.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/style.css" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Untitled Maze Game</title>
|
|
||||||
</head>
|
|
||||||
<body class="maze-page">
|
|
||||||
<main class="maze-layout">
|
|
||||||
<section class="panel">
|
|
||||||
<div class="panel-label">Babylon Scene</div>
|
|
||||||
<canvas id="renderCanvas"></canvas>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<div class="panel-label">p5 Control Panel</div>
|
|
||||||
<div id="p5-panel"></div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<script type="module" src="/src/multi_sketch.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Single sketch</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<script type="module" src="/src/single_sketch.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
322
src/babylon_setup.js
Normal file
322
src/babylon_setup.js
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
import * as BABYLON from "babylonjs";
|
||||||
|
|
||||||
|
// Shared game state
|
||||||
|
const sharedState = (window.mazeGameState ??= {
|
||||||
|
config: {
|
||||||
|
seed: Math.floor(Math.random() * 100000),
|
||||||
|
level: 1,
|
||||||
|
mazeWidth: 11,
|
||||||
|
mazeHeight: 11,
|
||||||
|
minChestDeadEnds: 2,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
runActive: false,
|
||||||
|
hasKey: false,
|
||||||
|
elapsedSeconds: 0,
|
||||||
|
message: "Adjust settings, then start a run.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize Babylon.js engine and scene
|
||||||
|
const canvas = document.getElementById("renderCanvas");
|
||||||
|
const engine = new BABYLON.Engine(canvas, true);
|
||||||
|
|
||||||
|
const scene = new BABYLON.Scene(engine);
|
||||||
|
scene.clearColor = new BABYLON.Color4(0.05, 0.07, 0.1, 1);
|
||||||
|
|
||||||
|
const camera = new BABYLON.ArcRotateCamera(
|
||||||
|
"cam",
|
||||||
|
-Math.PI / 2,
|
||||||
|
Math.PI / 2.4,
|
||||||
|
10,
|
||||||
|
BABYLON.Vector3.Zero(),
|
||||||
|
scene,
|
||||||
|
);
|
||||||
|
camera.attachControl(canvas, true);
|
||||||
|
|
||||||
|
new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
|
||||||
|
|
||||||
|
// Placeholder sphere
|
||||||
|
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene);
|
||||||
|
const sphereMaterial = new BABYLON.StandardMaterial("sphereMaterial", scene);
|
||||||
|
sphereMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.55, 0.95);
|
||||||
|
sphereMaterial.emissiveColor = new BABYLON.Color3(0.05, 0.12, 0.2);
|
||||||
|
sphere.material = sphereMaterial;
|
||||||
|
|
||||||
|
// Ground
|
||||||
|
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 14, height: 14 }, scene);
|
||||||
|
const groundMaterial = new BABYLON.StandardMaterial("groundMaterial", scene);
|
||||||
|
groundMaterial.diffuseColor = new BABYLON.Color3(0.12, 0.14, 0.17);
|
||||||
|
groundMaterial.specularColor = BABYLON.Color3.Black();
|
||||||
|
ground.material = groundMaterial;
|
||||||
|
|
||||||
|
// Main render loop
|
||||||
|
engine.runRenderLoop(() => {
|
||||||
|
const level = sharedState.config.level;
|
||||||
|
sphere.rotation.y += 0.01;
|
||||||
|
sphere.scaling.x = 1 + (level - 1) * 0.05;
|
||||||
|
sphere.scaling.z = 1 + (level - 1) * 0.05;
|
||||||
|
sphereMaterial.diffuseColor = sharedState.runtime.hasKey
|
||||||
|
? new BABYLON.Color3(0.25, 0.8, 0.45)
|
||||||
|
: new BABYLON.Color3(0.2, 0.55, 0.95);
|
||||||
|
scene.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
engine.resize();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Maze data structures
|
||||||
|
let levelMeshes = [];
|
||||||
|
let chestMap = new Map(); // key: "x,y" -> {mesh, opened}
|
||||||
|
let keyChestKey = null;
|
||||||
|
let exitBox = null;
|
||||||
|
const cellSize = 2;
|
||||||
|
|
||||||
|
function clearLevelMeshes() {
|
||||||
|
for (const m of levelMeshes) {
|
||||||
|
try { m.dispose(); } catch(e) {}
|
||||||
|
}
|
||||||
|
levelMeshes = [];
|
||||||
|
chestMap.clear();
|
||||||
|
keyChestKey = null;
|
||||||
|
if (exitBox) { try { exitBox.dispose(); } catch(e){}; exitBox = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple seeded RNG
|
||||||
|
function seededRng(seed) {
|
||||||
|
let s = seed % 2147483647;
|
||||||
|
if (s <= 0) s += 2147483646;
|
||||||
|
return function () {
|
||||||
|
s = (s * 16807) % 2147483647;
|
||||||
|
return (s - 1) / 2147483646;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maze generation (recursive backtracker) returning grid: 0 path, 1 wall
|
||||||
|
function generateMazeGrid(w, h, seed) {
|
||||||
|
if (w % 2 === 0) w += 1;
|
||||||
|
if (h % 2 === 0) h += 1;
|
||||||
|
const rng = seededRng(seed);
|
||||||
|
const grid = Array.from({ length: h }, () => Array(w).fill(1));
|
||||||
|
|
||||||
|
function carve(x, y) {
|
||||||
|
grid[y][x] = 0;
|
||||||
|
const dirs = [ [0,-2],[2,0],[0,2],[-2,0] ];
|
||||||
|
for (let i = dirs.length -1; i > 0; i--) {
|
||||||
|
const j = Math.floor(rng() * (i+1));
|
||||||
|
[dirs[i], dirs[j]] = [dirs[j], dirs[i]];
|
||||||
|
}
|
||||||
|
for (const [dx,dy] of dirs) {
|
||||||
|
const nx = x + dx;
|
||||||
|
const ny = y + dy;
|
||||||
|
if (nx > 0 && nx < w-1 && ny > 0 && ny < h-1 && grid[ny][nx] === 1) {
|
||||||
|
grid[y + dy/2][x + dx/2] = 0;
|
||||||
|
carve(nx, ny);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sx = 1 + Math.floor(rng() * ((w-1)/2)) * 2;
|
||||||
|
const sy = 1 + Math.floor(rng() * ((h-1)/2)) * 2;
|
||||||
|
carve(sx, sy);
|
||||||
|
|
||||||
|
const extra = Math.max(0, Math.floor((w*h) * 0.02));
|
||||||
|
for (let i = 0; i < extra; i++) {
|
||||||
|
const rx = 1 + Math.floor(rng() * ((w-1)/2)) * 2;
|
||||||
|
const ry = 1 + Math.floor(rng() * ((h-1)/2)) * 2;
|
||||||
|
const dirs = [ [0,-1],[1,0],[0,1],[-1,0] ];
|
||||||
|
const [dx,dy] = dirs[Math.floor(rng()*dirs.length)];
|
||||||
|
const nx = rx + dx;
|
||||||
|
const ny = ry + dy;
|
||||||
|
if (nx > 0 && nx < w-1 && ny > 0 && ny < h-1) grid[ny][nx] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDeadEnds(grid) {
|
||||||
|
const h = grid.length;
|
||||||
|
const w = grid[0].length;
|
||||||
|
const dead = [];
|
||||||
|
for (let y = 1; y < h-1; y++) {
|
||||||
|
for (let x = 1; x < w-1; x++) {
|
||||||
|
if (grid[y][x] !== 0) continue;
|
||||||
|
let neighbors = 0;
|
||||||
|
const deltas = [[0,1],[1,0],[0,-1],[-1,0]];
|
||||||
|
for (const [dx,dy] of deltas) if (grid[y+dy][x+dx] === 0) neighbors++;
|
||||||
|
if (neighbors === 1) dead.push([x,y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dead;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLevelFromGrid(grid) {
|
||||||
|
clearLevelMeshes();
|
||||||
|
const h = grid.length;
|
||||||
|
const w = grid[0].length;
|
||||||
|
const halfW = (w * cellSize) / 2;
|
||||||
|
const halfH = (h * cellSize) / 2;
|
||||||
|
|
||||||
|
const floor = BABYLON.MeshBuilder.CreateGround('levelGround', { width: w*cellSize, height: h*cellSize }, scene);
|
||||||
|
floor.position = new BABYLON.Vector3(0, 0, 0);
|
||||||
|
const fm = new BABYLON.StandardMaterial('floorMat', scene);
|
||||||
|
fm.diffuseColor = new BABYLON.Color3(0.08, 0.08, 0.09);
|
||||||
|
floor.material = fm;
|
||||||
|
levelMeshes.push(floor);
|
||||||
|
|
||||||
|
const wallMat = new BABYLON.StandardMaterial('wallMat', scene);
|
||||||
|
wallMat.diffuseColor = new BABYLON.Color3(0.33, 0.28, 0.22);
|
||||||
|
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
if (grid[y][x] === 1) {
|
||||||
|
const box = BABYLON.MeshBuilder.CreateBox(`wall_${x}_${y}`, { size: cellSize }, scene);
|
||||||
|
box.position = new BABYLON.Vector3(x*cellSize - halfW + cellSize/2, cellSize/2, y*cellSize - halfH + cellSize/2);
|
||||||
|
box.material = wallMat;
|
||||||
|
box.checkCollisions = true;
|
||||||
|
levelMeshes.push(box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeChestsOnDeadEnds(grid, deadEnds, minCount, seed) {
|
||||||
|
const rng = seededRng(seed);
|
||||||
|
for (let i = deadEnds.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(rng() * (i+1));
|
||||||
|
[deadEnds[i], deadEnds[j]] = [deadEnds[j], deadEnds[i]];
|
||||||
|
}
|
||||||
|
const chosen = deadEnds.slice(0, Math.min(minCount, deadEnds.length));
|
||||||
|
const halfW = (grid[0].length * cellSize) / 2;
|
||||||
|
const halfH = (grid.length * cellSize) / 2;
|
||||||
|
|
||||||
|
const chestMat = new BABYLON.StandardMaterial('chestMat', scene);
|
||||||
|
chestMat.diffuseColor = new BABYLON.Color3(0.75, 0.45, 0.15);
|
||||||
|
|
||||||
|
for (const [x,y] of chosen) {
|
||||||
|
const c = BABYLON.MeshBuilder.CreateBox(`chest_${x}_${y}`, { width: cellSize*0.8, height: cellSize*0.6, depth: cellSize*0.6 }, scene);
|
||||||
|
c.position = new BABYLON.Vector3(x*cellSize - halfW + cellSize/2, cellSize*0.3, y*cellSize - halfH + cellSize/2);
|
||||||
|
c.material = chestMat;
|
||||||
|
c.isPickable = true;
|
||||||
|
levelMeshes.push(c);
|
||||||
|
chestMap.set(`${x},${y}`, { mesh: c, opened: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chosen.length > 0) {
|
||||||
|
const k = Math.floor(rng() * chosen.length);
|
||||||
|
const [kx, ky] = chosen[k];
|
||||||
|
keyChestKey = `${kx},${ky}`;
|
||||||
|
const entry = chestMap.get(keyChestKey);
|
||||||
|
if (entry) {
|
||||||
|
const km = new BABYLON.StandardMaterial('keyChestMat', scene);
|
||||||
|
km.diffuseColor = new BABYLON.Color3(0.95, 0.8, 0.1);
|
||||||
|
entry.mesh.material = km;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeExit(grid, seed) {
|
||||||
|
const dead = findDeadEnds(grid);
|
||||||
|
const rng = seededRng(seed+1);
|
||||||
|
if (dead.length === 0) return;
|
||||||
|
const idx = Math.floor(rng() * dead.length);
|
||||||
|
const [x,y] = dead[idx];
|
||||||
|
const halfW = (grid[0].length * cellSize) / 2;
|
||||||
|
const halfH = (grid.length * cellSize) / 2;
|
||||||
|
const ex = x*cellSize - halfW + cellSize/2;
|
||||||
|
const ez = y*cellSize - halfH + cellSize/2;
|
||||||
|
const plane = BABYLON.MeshBuilder.CreateGround('exitZone', { width: cellSize*0.9, height: cellSize*0.9 }, scene);
|
||||||
|
plane.material = new BABYLON.StandardMaterial('exitMat', scene);
|
||||||
|
plane.material.emissiveColor = new BABYLON.Color3(0.9, 0.8, 0.2);
|
||||||
|
plane.position = new BABYLON.Vector3(ex, 0.01, ez);
|
||||||
|
exitBox = plane;
|
||||||
|
levelMeshes.push(plane);
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnCameraAt(grid) {
|
||||||
|
const h = grid.length, w = grid[0].length;
|
||||||
|
for (let y = 1; y < h-1; y++) {
|
||||||
|
for (let x = 1; x < w-1; x++) {
|
||||||
|
if (grid[y][x] === 0) {
|
||||||
|
const halfW = (w * cellSize) / 2;
|
||||||
|
const halfH = (h * cellSize) / 2;
|
||||||
|
const px = x*cellSize - halfW + cellSize/2;
|
||||||
|
const pz = y*cellSize - halfH + cellSize/2;
|
||||||
|
try {
|
||||||
|
if (camera && camera.position) {
|
||||||
|
camera.position = new BABYLON.Vector3(px, 1.6, pz);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateLevel() {
|
||||||
|
const cfg = sharedState.config;
|
||||||
|
const seed = cfg.seed;
|
||||||
|
const w = Math.max(9, cfg.mazeWidth);
|
||||||
|
const h = Math.max(9, cfg.mazeHeight);
|
||||||
|
const grid = generateMazeGrid(w, h, seed + cfg.level);
|
||||||
|
const dead = findDeadEnds(grid);
|
||||||
|
buildLevelFromGrid(grid);
|
||||||
|
placeChestsOnDeadEnds(grid, dead, cfg.minChestDeadEnds, seed + cfg.level);
|
||||||
|
placeExit(grid, seed + cfg.level);
|
||||||
|
spawnCameraAt(grid);
|
||||||
|
sharedState.runtime.message = `Level ${cfg.level} generated.`;
|
||||||
|
window.requestAnimationFrame(()=>{ /* let scene update */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose API for p5 to call
|
||||||
|
window.mazeGameApi = { generateLevel };
|
||||||
|
|
||||||
|
// Pointer interaction for chests
|
||||||
|
scene.onPointerObservable.add((pi) => {
|
||||||
|
if (pi.type !== BABYLON.PointerEventTypes.POINTERDOWN) return;
|
||||||
|
const pick = scene.pick(scene.pointerX, scene.pointerY);
|
||||||
|
if (!pick || !pick.hit || !pick.pickedMesh) return;
|
||||||
|
const m = pick.pickedMesh;
|
||||||
|
if (!m.name.startsWith('chest_')) return;
|
||||||
|
const coords = m.name.split('_').slice(1).join(',');
|
||||||
|
const entry = chestMap.get(coords);
|
||||||
|
if (!entry) return;
|
||||||
|
if (entry.opened) {
|
||||||
|
sharedState.runtime.runActive = false;
|
||||||
|
sharedState.runtime.message = 'Opened chest again — game over.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entry.opened = true;
|
||||||
|
const openedMat = new BABYLON.StandardMaterial('openedMat', scene);
|
||||||
|
openedMat.diffuseColor = new BABYLON.Color3(0.25,0.25,0.25);
|
||||||
|
entry.mesh.material = openedMat;
|
||||||
|
if (coords === keyChestKey) {
|
||||||
|
sharedState.runtime.hasKey = true;
|
||||||
|
sharedState.runtime.message = 'You found the key! Find the exit.';
|
||||||
|
} else {
|
||||||
|
sharedState.runtime.message = 'This chest was empty.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Level transition check
|
||||||
|
scene.registerBeforeRender(() => {
|
||||||
|
if (sharedState.runtime.runActive) {
|
||||||
|
sharedState.runtime.elapsedSeconds += engine.getDeltaTime() / 1000;
|
||||||
|
}
|
||||||
|
if (sharedState.runtime.hasKey && exitBox && camera && camera.position) {
|
||||||
|
const pos = camera.position;
|
||||||
|
const ex = exitBox.position.x, ez = exitBox.position.z;
|
||||||
|
const dist = Math.hypot(pos.x - ex, pos.z - ez);
|
||||||
|
if (dist < cellSize * 0.9) {
|
||||||
|
sharedState.config.level += 1;
|
||||||
|
sharedState.runtime.hasKey = false;
|
||||||
|
sharedState.runtime.elapsedSeconds = 0;
|
||||||
|
sharedState.runtime.message = `Level ${sharedState.config.level} starting.`;
|
||||||
|
generateLevel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export shared state for p5 to use
|
||||||
|
export { sharedState };
|
||||||
81
src/html_panel.js
Normal file
81
src/html_panel.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { sharedState } from "./babylon_setup.js";
|
||||||
|
|
||||||
|
// Handler functions (same as p5_panel but without p5 scoping)
|
||||||
|
function resetRun(message) {
|
||||||
|
sharedState.runtime.runActive = true;
|
||||||
|
sharedState.runtime.hasKey = false;
|
||||||
|
sharedState.runtime.elapsedSeconds = 0;
|
||||||
|
sharedState.runtime.message = message;
|
||||||
|
sharedState.config.level = 1;
|
||||||
|
try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); }
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartLevel(message) {
|
||||||
|
sharedState.runtime.hasKey = false;
|
||||||
|
sharedState.runtime.elapsedSeconds = 0;
|
||||||
|
sharedState.runtime.message = message;
|
||||||
|
try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); }
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomizeSeed() {
|
||||||
|
sharedState.config.seed = Math.floor(Math.random() * 100000);
|
||||||
|
sharedState.runtime.message = `Seed set to ${sharedState.config.seed}.`;
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display from shared state
|
||||||
|
function updateDisplay() {
|
||||||
|
document.getElementById("value-width").textContent = sharedState.config.mazeWidth;
|
||||||
|
document.getElementById("value-height").textContent = sharedState.config.mazeHeight;
|
||||||
|
document.getElementById("value-deadends").textContent = sharedState.config.minChestDeadEnds;
|
||||||
|
|
||||||
|
document.getElementById("status-seed").textContent = sharedState.config.seed;
|
||||||
|
document.getElementById("status-level").textContent = sharedState.config.level;
|
||||||
|
document.getElementById("status-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
|
||||||
|
document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no";
|
||||||
|
document.getElementById("status-message").textContent = sharedState.runtime.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize event listeners
|
||||||
|
document.getElementById("btn-start").addEventListener("click", () => {
|
||||||
|
resetRun("Run started.");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btn-restart").addEventListener("click", () => {
|
||||||
|
restartLevel("Level restarted.");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btn-randomize").addEventListener("click", () => {
|
||||||
|
randomizeSeed();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("slider-width").addEventListener("input", (e) => {
|
||||||
|
const value = parseInt(e.target.value) | 1;
|
||||||
|
sharedState.config.mazeWidth = value;
|
||||||
|
document.getElementById("value-width").textContent = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("slider-height").addEventListener("input", (e) => {
|
||||||
|
const value = parseInt(e.target.value) | 1;
|
||||||
|
sharedState.config.mazeHeight = value;
|
||||||
|
document.getElementById("value-height").textContent = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("slider-deadends").addEventListener("input", (e) => {
|
||||||
|
const value = parseInt(e.target.value);
|
||||||
|
sharedState.config.minChestDeadEnds = value;
|
||||||
|
document.getElementById("value-deadends").textContent = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update status display on game loop
|
||||||
|
setInterval(() => {
|
||||||
|
if (sharedState.runtime.runActive) {
|
||||||
|
document.getElementById("status-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
|
||||||
|
document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no";
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Initial display
|
||||||
|
updateDisplay();
|
||||||
@@ -1,258 +1,3 @@
|
|||||||
import * as BABYLON from "babylonjs";
|
// Import Babylon.js game logic and HTML control panel
|
||||||
import { sketch } from "p5js-wrapper";
|
import "./babylon_setup.js";
|
||||||
|
import "./html_panel.js";
|
||||||
const sharedState = (window.mazeGameState ??= {
|
|
||||||
config: {
|
|
||||||
seed: Math.floor(Math.random() * 100000),
|
|
||||||
level: 1,
|
|
||||||
mazeWidth: 11,
|
|
||||||
mazeHeight: 11,
|
|
||||||
minChestDeadEnds: 2,
|
|
||||||
},
|
|
||||||
runtime: {
|
|
||||||
runActive: false,
|
|
||||||
hasKey: false,
|
|
||||||
elapsedSeconds: 0,
|
|
||||||
message: "Adjust settings, then start a run.",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const canvas = document.getElementById("renderCanvas");
|
|
||||||
const engine = new BABYLON.Engine(canvas, true);
|
|
||||||
|
|
||||||
const scene = new BABYLON.Scene(engine);
|
|
||||||
scene.clearColor = new BABYLON.Color4(0.05, 0.07, 0.1, 1);
|
|
||||||
|
|
||||||
const camera = new BABYLON.ArcRotateCamera(
|
|
||||||
"cam",
|
|
||||||
-Math.PI / 2,
|
|
||||||
Math.PI / 2.4,
|
|
||||||
10,
|
|
||||||
BABYLON.Vector3.Zero(),
|
|
||||||
scene,
|
|
||||||
);
|
|
||||||
camera.attachControl(canvas, true);
|
|
||||||
|
|
||||||
new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
|
|
||||||
|
|
||||||
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene);
|
|
||||||
const sphereMaterial = new BABYLON.StandardMaterial("sphereMaterial", scene);
|
|
||||||
sphereMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.55, 0.95);
|
|
||||||
sphereMaterial.emissiveColor = new BABYLON.Color3(0.05, 0.12, 0.2);
|
|
||||||
sphere.material = sphereMaterial;
|
|
||||||
|
|
||||||
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 14, height: 14 }, scene);
|
|
||||||
const groundMaterial = new BABYLON.StandardMaterial("groundMaterial", scene);
|
|
||||||
groundMaterial.diffuseColor = new BABYLON.Color3(0.12, 0.14, 0.17);
|
|
||||||
groundMaterial.specularColor = BABYLON.Color3.Black();
|
|
||||||
ground.material = groundMaterial;
|
|
||||||
|
|
||||||
engine.runRenderLoop(() => {
|
|
||||||
const level = sharedState.config.level;
|
|
||||||
sphere.rotation.y += 0.01;
|
|
||||||
sphere.scaling.x = 1 + (level - 1) * 0.05;
|
|
||||||
sphere.scaling.z = 1 + (level - 1) * 0.05;
|
|
||||||
sphereMaterial.diffuseColor = sharedState.runtime.hasKey
|
|
||||||
? new BABYLON.Color3(0.25, 0.8, 0.45)
|
|
||||||
: new BABYLON.Color3(0.2, 0.55, 0.95);
|
|
||||||
scene.render();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
engine.resize();
|
|
||||||
});
|
|
||||||
|
|
||||||
const panelSize = { width: 1000, height: 270 };
|
|
||||||
const buttonRects = {};
|
|
||||||
const sliderRects = {};
|
|
||||||
let draggingSlider = null;
|
|
||||||
|
|
||||||
function clamp(value, min, max) {
|
|
||||||
return Math.min(max, Math.max(min, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
function valueToX(rect, min, max, value) {
|
|
||||||
const ratio = (value - min) / (max - min);
|
|
||||||
return rect.x + ratio * rect.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
function xToValue(rect, min, max, x) {
|
|
||||||
const ratio = clamp((x - rect.x) / rect.width, 0, 1);
|
|
||||||
return Math.round(min + ratio * (max - min));
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetRun(message) {
|
|
||||||
sharedState.runtime.runActive = true;
|
|
||||||
sharedState.runtime.hasKey = false;
|
|
||||||
sharedState.runtime.elapsedSeconds = 0;
|
|
||||||
sharedState.runtime.message = message;
|
|
||||||
sharedState.config.level = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function restartLevel(message) {
|
|
||||||
sharedState.runtime.hasKey = false;
|
|
||||||
sharedState.runtime.elapsedSeconds = 0;
|
|
||||||
sharedState.runtime.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomizeSeed() {
|
|
||||||
sharedState.config.seed = Math.floor(Math.random() * 100000);
|
|
||||||
sharedState.runtime.message = `Seed set to ${sharedState.config.seed}.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawButton(rect, label, active = false) {
|
|
||||||
push();
|
|
||||||
stroke(active ? "#7db4ff" : "#284055");
|
|
||||||
strokeWeight(2);
|
|
||||||
fill(active ? "#17314c" : "#101820");
|
|
||||||
rect(rect.x, rect.y, rect.w, rect.h, 12);
|
|
||||||
noStroke();
|
|
||||||
fill("#eef5ff");
|
|
||||||
textAlign(CENTER, CENTER);
|
|
||||||
textSize(16);
|
|
||||||
text(label, rect.x + rect.w / 2, rect.y + rect.h / 2 + 1);
|
|
||||||
pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawSlider(rect, label, value, min, max, suffix = "") {
|
|
||||||
const knobX = valueToX(rect, min, max, value);
|
|
||||||
push();
|
|
||||||
fill("#dbe6f2");
|
|
||||||
noStroke();
|
|
||||||
textAlign(LEFT, BOTTOM);
|
|
||||||
textSize(15);
|
|
||||||
text(`${label}: ${value}${suffix}`, rect.x, rect.y - 8);
|
|
||||||
stroke("#33495e");
|
|
||||||
strokeWeight(6);
|
|
||||||
line(rect.x, rect.y + rect.h / 2, rect.x + rect.w, rect.y + rect.h / 2);
|
|
||||||
noStroke();
|
|
||||||
fill("#79aef2");
|
|
||||||
circle(knobX, rect.y + rect.h / 2, 20);
|
|
||||||
pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
sketch.setup = function () {
|
|
||||||
createCanvas(panelSize.width, panelSize.height).parent("p5-panel");
|
|
||||||
pixelDensity(1);
|
|
||||||
textFont("monospace");
|
|
||||||
noLoop();
|
|
||||||
};
|
|
||||||
|
|
||||||
sketch.draw = function () {
|
|
||||||
background("#091018");
|
|
||||||
|
|
||||||
noStroke();
|
|
||||||
fill("#dce7f4");
|
|
||||||
textAlign(LEFT, TOP);
|
|
||||||
textSize(18);
|
|
||||||
text("Maze game settings", 18, 14);
|
|
||||||
|
|
||||||
fill("#91a4b8");
|
|
||||||
textSize(12);
|
|
||||||
text(
|
|
||||||
"This panel controls run settings and mirrors the shared state for the Babylon scene.",
|
|
||||||
18,
|
|
||||||
38,
|
|
||||||
);
|
|
||||||
|
|
||||||
const buttonY = 64;
|
|
||||||
buttonRects.start = { x: 18, y: buttonY, w: 140, h: 38 };
|
|
||||||
buttonRects.restart = { x: 168, y: buttonY, w: 140, h: 38 };
|
|
||||||
buttonRects.seed = { x: 318, y: buttonY, w: 160, h: 38 };
|
|
||||||
|
|
||||||
drawButton(buttonRects.start, "Start run", sharedState.runtime.runActive);
|
|
||||||
drawButton(buttonRects.restart, "Restart level");
|
|
||||||
drawButton(buttonRects.seed, "Randomize seed");
|
|
||||||
|
|
||||||
sliderRects.width = { x: 18, y: 154, w: 420, h: 22 };
|
|
||||||
sliderRects.height = { x: 18, y: 206, w: 420, h: 22 };
|
|
||||||
sliderRects.deadEnds = { x: 500, y: 154, w: 420, h: 22 };
|
|
||||||
|
|
||||||
drawSlider(sliderRects.width, "Maze width", sharedState.config.mazeWidth, 9, 31, " cells");
|
|
||||||
drawSlider(sliderRects.height, "Maze height", sharedState.config.mazeHeight, 9, 31, " cells");
|
|
||||||
drawSlider(
|
|
||||||
sliderRects.deadEnds,
|
|
||||||
"Minimum chest dead-ends",
|
|
||||||
sharedState.config.minChestDeadEnds,
|
|
||||||
1,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
fill("#dce7f4");
|
|
||||||
textSize(14);
|
|
||||||
textAlign(LEFT, TOP);
|
|
||||||
text(`Seed: ${sharedState.config.seed}`, 500, 196);
|
|
||||||
text(`Level: ${sharedState.config.level}`, 500, 220);
|
|
||||||
text(`Time: ${sharedState.runtime.elapsedSeconds.toFixed(1)}s`, 640, 220);
|
|
||||||
text(`Key: ${sharedState.runtime.hasKey ? "yes" : "no"}`, 780, 220);
|
|
||||||
|
|
||||||
fill("#93a4b8");
|
|
||||||
textSize(12);
|
|
||||||
text(`Status: ${sharedState.runtime.message}`, 18, 242, 960);
|
|
||||||
};
|
|
||||||
|
|
||||||
sketch.mousePressed = function () {
|
|
||||||
const x = mouseX;
|
|
||||||
const y = mouseY;
|
|
||||||
|
|
||||||
const isInside = (rect) => x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
|
|
||||||
|
|
||||||
if (isInside(buttonRects.start)) {
|
|
||||||
resetRun("Run started.");
|
|
||||||
redraw();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInside(buttonRects.restart)) {
|
|
||||||
restartLevel("Level restarted.");
|
|
||||||
redraw();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInside(buttonRects.seed)) {
|
|
||||||
randomizeSeed();
|
|
||||||
redraw();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInside(sliderRects.width)) {
|
|
||||||
draggingSlider = "width";
|
|
||||||
sharedState.config.mazeWidth = xToValue(sliderRects.width, 9, 31, x) | 1;
|
|
||||||
redraw();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInside(sliderRects.height)) {
|
|
||||||
draggingSlider = "height";
|
|
||||||
sharedState.config.mazeHeight = xToValue(sliderRects.height, 9, 31, x) | 1;
|
|
||||||
redraw();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isInside(sliderRects.deadEnds)) {
|
|
||||||
draggingSlider = "deadEnds";
|
|
||||||
sharedState.config.minChestDeadEnds = xToValue(sliderRects.deadEnds, 1, 10, x);
|
|
||||||
redraw();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sketch.mouseDragged = function () {
|
|
||||||
if (draggingSlider === "width") {
|
|
||||||
sharedState.config.mazeWidth = xToValue(sliderRects.width, 9, 31, mouseX) | 1;
|
|
||||||
redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draggingSlider === "height") {
|
|
||||||
sharedState.config.mazeHeight = xToValue(sliderRects.height, 9, 31, mouseX) | 1;
|
|
||||||
redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draggingSlider === "deadEnds") {
|
|
||||||
sharedState.config.minChestDeadEnds = xToValue(sliderRects.deadEnds, 1, 10, mouseX);
|
|
||||||
redraw();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sketch.mouseReleased = function () {
|
|
||||||
draggingSlider = null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import '../css/style.css';
|
|
||||||
import {sketch} from 'p5js-wrapper';
|
|
||||||
|
|
||||||
sketch.setup = function(){
|
|
||||||
createCanvas (800, 600);
|
|
||||||
}
|
|
||||||
|
|
||||||
sketch.draw= function(){
|
|
||||||
background(100);
|
|
||||||
fill(255, 0, 0);
|
|
||||||
noStroke();
|
|
||||||
rectMode(CENTER);
|
|
||||||
rect(mouseX, mouseY, 50, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
sketch.mousePressed = function(){
|
|
||||||
console.log('here');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -6,9 +6,7 @@ module.exports = defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
main: resolve(__dirname, 'index.html'),
|
main: resolve(__dirname, 'index.html')
|
||||||
single: resolve(__dirname, 'single_sketch.html'),
|
|
||||||
multi: resolve(__dirname, 'multi_sketch.html')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user