<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<title>消消乐</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.min.js"></script>
<style>
:root {
--primary-color: #4caf50;
--secondary-color: #007bff;
--danger-color: #f44336;
--background-color: #000033;
--light-text: #ffffff;
--dark-text: #333333;
--overlay-bg: rgba(0, 0, 0, 0.85);
--win-overlay-bg: rgba(20, 100, 20, 0.9);
}
body {
margin: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
background-color: var(--background-color);
color: var(--light-text);
position: fixed;
width: 100%;
height: 100%;
}
.screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
background-color: var(--background-color);
transition: opacity 0.5s ease;
opacity: 0;
}
.screen.active {
display: flex;
opacity: 1;
}
#splash-screen {
background: radial-gradient(
circle at center,
#6b46c1 0%,
#3b82f6 25%,
#6b46c1 50%,
#3b82f6 75%,
#6b46c1 100%
);
background-size: 100% 100%;
position: relative;
overflow: hidden;
}
#splash-screen::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 200%;
height: 200%;
background: conic-gradient(
from 0deg,
#6b46c1 0deg 22.5deg,
#3b82f6 22.5deg 45deg,
#6b46c1 45deg 67.5deg,
#3b82f6 67.5deg 90deg,
#6b46c1 90deg 112.5deg,
#3b82f6 112.5deg 135deg,
#6b46c1 135deg 157.5deg,
#3b82f6 157.5deg 180deg,
#6b46c1 180deg 202.5deg,
#3b82f6 202.5deg 225deg,
#6b46c1 225deg 247.5deg,
#3b82f6 247.5deg 270deg,
#6b46c1 270deg 292.5deg,
#3b82f6 292.5deg 315deg,
#6b46c1 315deg 337.5deg,
#3b82f6 337.5deg 360deg
);
transform: translate(-50%, -50%);
z-index: 1;
animation: rotate 20s linear infinite;
}
@keyframes rotate {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
#splash-3d-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
pointer-events: none;
}
.splash-content {
position: relative;
z-index: 10;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 500px;
}
.title-container {
text-align: center;
margin-bottom: 40px;
position: relative;
z-index: 10;
width: 100%;
}
.splash-title {
font-size: clamp(2.5rem, 10vw, 4.5rem);
font-weight: 900;
color: #ffffff;
text-shadow: 4px 4px 0px #000000, -4px -4px 0px #000000,
4px -4px 0px #000000, -4px 4px 0px #000000,
6px 6px 10px rgba(0, 0, 0, 0.8);
letter-spacing: 0.05em;
margin: 0;
line-height: 0.9;
}
.splash-subtitle {
font-size: clamp(1rem, 4vw, 1.5rem);
color: #ffffff;
margin-top: 10px;
margin-bottom: 0;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
font-weight: 600;
}
.menu-container {
width: 100%;
max-width: 380px;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
position: relative;
z-index: 10;
margin: 0 auto;
}
.btn {
padding: 18px 24px;
border-radius: 15px;
border: 4px solid #000000;
color: #ffffff;
cursor: pointer;
font-size: 1.4rem;
font-weight: 900;
text-transform: uppercase;
transition: transform 0.2s ease, background-color 0.3s ease,
border-color 0.3s ease;
text-shadow: 2px 2px 0px #000000;
letter-spacing: 0.1em;
position: relative;
overflow: hidden;
pointer-events: all;
width: 100%;
max-width: 280px;
text-align: center;
}
.btn:hover {
transform: translateY(-3px) scale(1.05);
}
.btn:active {
transform: scale(0.96) translateY(0);
}
.btn-primary {
background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
border-color: #000000;
box-shadow: 0 6px 0 #92400e, 0 8px 15px rgba(0, 0, 0, 0.3);
}
.btn-primary:hover {
box-shadow: 0 8px 0 #92400e, 0 12px 20px rgba(0, 0, 0, 0.4);
}
.btn-secondary {
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
border-color: #000000;
box-shadow: 0 6px 0 #1e3a8a, 0 8px 15px rgba(0, 0, 0, 0.3);
}
.btn-secondary:hover {
box-shadow: 0 8px 0 #1e3a8a, 0 12px 20px rgba(0, 0, 0, 0.4);
}
.btn-danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
border-color: #000000;
box-shadow: 0 6px 0 #991b1b, 0 8px 15px rgba(0, 0, 0, 0.3);
}
.btn-danger:hover {
box-shadow: 0 8px 0 #991b1b, 0 12px 20px rgba(0, 0, 0, 0.4);
}
.floating-shapes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 3;
}
.floating-shape {
position: absolute;
animation: float 8s ease-in-out infinite;
opacity: 0.6;
}
.floating-shape:nth-child(2n) {
animation-direction: reverse;
animation-duration: 10s;
}
.floating-shape:nth-child(3n) {
animation-delay: -3s;
}
.floating-shape:nth-child(4n) {
animation-delay: -6s;
}
@keyframes float {
0%,
100% {
transform: translateY(0px) rotate(0deg) scale(1);
}
33% {
transform: translateY(-30px) rotate(120deg) scale(1.1);
}
66% {
transform: translateY(15px) rotate(240deg) scale(0.9);
}
}
.shape-circle {
width: 40px;
height: 40px;
border-radius: 50%;
border: 6px solid;
}
.shape-square {
width: 35px;
height: 35px;
border: 6px solid;
border-radius: 6px;
}
.shape-triangle {
width: 0;
height: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
border-bottom: 35px solid;
}
.color-red {
border-color: #ef4444;
}
.color-blue {
border-color: #3b82f6;
}
.color-green {
border-color: #10b981;
}
.color-yellow {
border-color: #fbbf24;
}
.color-purple {
border-color: #8b5cf6;
}
.color-orange {
border-color: #f97316;
}
.color-red.shape-triangle {
border-bottom-color: #ef4444;
border-left-color: transparent;
border-right-color: transparent;
}
.color-blue.shape-triangle {
border-bottom-color: #3b82f6;
border-left-color: transparent;
border-right-color: transparent;
}
.color-green.shape-triangle {
border-bottom-color: #10b981;
border-left-color: transparent;
border-right-color: transparent;
}
#settings-screen .menu-container {
background-color: rgba(0, 0, 0, 0.8);
padding: 30px;
border-radius: 15px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
border: 4px solid #ffffff;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.setting-item label {
font-size: 1.1rem;
font-weight: 600;
}
.setting-item select {
padding: 10px;
border-radius: 8px;
border: 2px solid #000000;
background-color: #ffffff;
color: #000000;
font-size: 1rem;
font-weight: 600;
width: 100px;
text-align: center;
}
#game-screen {
padding: 0;
cursor: default;
}
#container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#game-ui {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 20px;
pointer-events: none;
}
.rotation-controls {
display: flex;
flex-direction: row;
gap: 10px;
pointer-events: all;
}
.rotate-btn {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.7);
color: var(--light-text);
border: 2px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
transition: all 0.2s ease;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
}
.rotate-btn:hover {
transform: scale(1.1);
background-color: rgba(0, 0, 0, 0.9);
border-color: rgba(255, 255, 255, 0.6);
}
.rotate-btn:active {
transform: scale(0.95);
}
.rotate-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
#game-menu-btn,
#game-score {
pointer-events: all;
font-size: clamp(1.2rem, 5vw, 1.8rem);
font-weight: bold;
color: var(--light-text);
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 18px;
border-radius: 12px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7);
transition: transform 0.2s ease;
}
#game-menu-btn:hover {
transform: scale(1.05);
}
#game-menu-btn {
cursor: pointer;
}
.overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
width: 90%;
max-width: 400px;
background-color: var(--overlay-bg);
padding: 30px;
border-radius: 20px;
text-align: center;
z-index: 200;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
display: none;
opacity: 0;
transition: transform 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28),
opacity 0.3s ease;
}
.overlay.visible {
display: block;
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
#gameWin {
background-color: var(--win-overlay-bg);
}
.overlay h2 {
margin-top: 0;
font-size: clamp(1.8rem, 8vw, 2.5rem);
}
.overlay p {
font-size: clamp(1rem, 4vw, 1.2rem);
margin: 20px 0;
}
.overlay .btn {
margin-top: 10px;
width: 100%;
}
</style>
</head>
<body>
<div id="splash-screen" class="screen active">
<div id="splash-3d-container"></div>
<div class="floating-shapes">
<div
class="floating-shape shape-circle color-red"
style="top: 10%; left: 15%"
></div>
<div
class="floating-shape shape-square color-blue"
style="top: 20%; right: 20%"
></div>
<div
class="floating-shape shape-triangle color-green"
style="top: 70%; left: 10%"
></div>
<div
class="floating-shape shape-circle color-yellow"
style="top: 80%; right: 15%"
></div>
<div
class="floating-shape shape-square color-purple"
style="top: 15%; left: 80%"
></div>
</div>
<div class="splash-content">
<div class="title-container">
<h1 class="splash-title">消消乐</h1>
</div>
<div class="menu-container">
<button id="start-game-btn" class="btn btn-primary">开始游戏</button>
<button id="settings-btn" class="btn btn-secondary">设置</button>
</div>
</div>
</div>
<div id="settings-screen" class="screen">
<h1 class="splash-title">设置</h1>
<div class="menu-container">
<div class="setting-item">
<label for="rows-setting">行数</label>
<select id="rows-setting">
<option value="7">7</option>
<option value="9">9</option>
<option value="11" selected>11</option>
<option value="13">13</option>
<option value="15">15</option>
</select>
</div>
<div class="setting-item">
<label for="cols-setting">列数</label>
<select id="cols-setting">
<option value="7">7</option>
<option value="9">9</option>
<option value="11" selected>11</option>
<option value="13">13</option>
<option value="15">15</option>
</select>
</div>
<div class="setting-item">
<label for="template-setting">形状</label>
<select id="template-setting">
<option value="rectangle">矩形</option>
<option value="diamond">Diamond</option>
<option value="cross">十字</option>
<option value="circle">圆形</option>
<option value="random">随机</option>
</select>
</div>
<div class="setting-item">
<label for="mingroup-setting">最小组大小</label>
<select id="mingroup-setting">
<option value="2" selected>2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>
<button id="back-to-splash-btn" class="btn btn-danger">返回</button>
</div>
</div>
<div id="game-screen" class="screen">
<div id="container"></div>
<div id="game-ui">
<div id="game-menu-btn">☰</div>
<div class="rotation-controls">
<button id="rotate-left-btn" class="rotate-btn" title="Rotate Left">
↺
</button>
<button id="rotate-right-btn" class="rotate-btn" title="Rotate Right">
↻
</button>
</div>
<div id="game-score">0</div>
</div>
<div id="gameOver" class="overlay">
<h2>游戏结束!</h2>
<p>没有更多操作了。</p>
<button id="restartGame" class="btn btn-primary">重新开始</button>
</div>
<div id="gameWin" class="overlay">
<h2>关卡完成!</h2>
<p>你收集了所有奖励!</p>
<button id="nextLevel" class="btn btn-primary">下一关</button>
</div>
</div>
<script>
const screens = document.querySelectorAll(".screen");
const gameOverOverlay = document.getElementById("gameOver");
const gameWinOverlay = document.getElementById("gameWin");
function showScreen(screenId) {
screens.forEach((screen) => screen.classList.remove("active"));
const activeScreen = document.getElementById(screenId);
if (activeScreen) activeScreen.classList.add("active");
if (screenId === "splash-screen" && window.splashScene) {
window.splashScene.startAnimation();
} else if (window.splashScene) {
window.splashScene.stopAnimation();
}
}
function showOverlay(overlayElement) {
overlayElement.classList.add("visible");
}
function hideOverlay(overlayElement) {
overlayElement.classList.remove("visible");
}
class SplashScene {
constructor() {
this.container = document.getElementById("splash-3d-container");
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setClearColor(0x000000, 0);
this.container.appendChild(this.renderer.domElement);
this.camera.position.z = 8;
this.shapes = [];
this.isAnimating = false;
this.shapeAtlas = [
{
name: "Sphere",
color: 0xff4444,
geometryFactory: (s) => new THREE.SphereGeometry(s, 16, 12),
},
{
name: "Cube",
color: 0x44ff44,
geometryFactory: (s) => new THREE.BoxGeometry(s, s, s),
},
{
name: "Tetra",
color: 0x4488ff,
geometryFactory: (s) => new THREE.TetrahedronGeometry(s),
},
{
name: "Octa",
color: 0xffff44,
geometryFactory: (s) => new THREE.OctahedronGeometry(s),
},
{
name: "Icosa",
color: 0xff44ff,
geometryFactory: (s) => new THREE.IcosahedronGeometry(s),
},
{
name: "Dodeca",
color: 0x44ffff,
geometryFactory: (s) => new THREE.DodecahedronGeometry(s),
},
];
this.setupLighting();
this.createFloatingShapes();
window.addEventListener("resize", this.onWindowResize.bind(this));
}
setupLighting() {
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 5, 5);
this.scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0x6b46c1, 0.4);
directionalLight2.position.set(-5, -5, 3);
this.scene.add(directionalLight2);
}
createFloatingShapes() {
const positions = [
{ x: -6, y: 3, z: -2 },
{ x: 6, y: -2, z: -1 },
{ x: -4, y: -3, z: 1 },
{ x: 4, y: 4, z: 0 },
{ x: -2, y: 1, z: -3 },
{ x: 2, y: -4, z: 2 },
{ x: 7, y: 1, z: -2 },
{ x: -7, y: -1, z: 1 },
{ x: 0, y: 5, z: -1 },
{ x: -1, y: -5, z: 0 },
{ x: 5, y: 3, z: 1 },
{ x: -5, y: 2, z: -1 },
];
positions.forEach((pos, i) => {
const shapeData = this.shapeAtlas[i % this.shapeAtlas.length];
const size = 0.4 + Math.random() * 0.3;
const geometry = shapeData.geometryFactory(size);
const material = new THREE.MeshPhongMaterial({
color: shapeData.color,
shininess: 60,
specular: 0x444444,
transparent: true,
opacity: 0.8,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(pos.x, pos.y, pos.z);
mesh.rotation.set(
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2,
Math.random() * Math.PI * 2
);
mesh.userData = {
originalPosition: { ...pos },
floatSpeed: 0.5 + Math.random() * 1.5,
rotationSpeed: {
x: (Math.random() - 0.5) * 0.02,
y: (Math.random() - 0.5) * 0.02,
z: (Math.random() - 0.5) * 0.02,
},
floatOffset: Math.random() * Math.PI * 2,
};
this.shapes.push(mesh);
this.scene.add(mesh);
mesh.scale.set(0.01, 0.01, 0.01);
new TWEEN.Tween(mesh.scale)
.to({ x: 1, y: 1, z: 1 }, 800)
.easing(TWEEN.Easing.Elastic.Out)
.delay(i * 100)
.start();
});
}
startAnimation() {
this.isAnimating = true;
this.animate();
}
stopAnimation() {
this.isAnimating = false;
}
animate() {
if (!this.isAnimating) return;
requestAnimationFrame(this.animate.bind(this));
TWEEN.update();
const time = Date.now() * 0.001;
this.shapes.forEach((shape) => {
const userData = shape.userData;
shape.position.y =
userData.originalPosition.y +
Math.sin(time * userData.floatSpeed + userData.floatOffset) * 0.5;
shape.position.x =
userData.originalPosition.x +
Math.cos(
time * userData.floatSpeed * 0.7 + userData.floatOffset
) *
0.2;
shape.rotation.x += userData.rotationSpeed.x;
shape.rotation.y += userData.rotationSpeed.y;
shape.rotation.z += userData.rotationSpeed.z;
});
this.renderer.render(this.scene, this.camera);
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
}
document.addEventListener("DOMContentLoaded", () => {
window.splashScene = new SplashScene();
window.splashScene.startAnimation();
const game = new BubblePopper();
game.animate();
document
.getElementById("start-game-btn")
.addEventListener("click", () => {
showScreen("game-screen");
game.createNewBoard();
});
document
.getElementById("settings-btn")
.addEventListener("click", () => showScreen("settings-screen"));
document
.getElementById("back-to-splash-btn")
.addEventListener("click", () => showScreen("splash-screen"));
document
.getElementById("game-menu-btn")
.addEventListener("click", () => {
game.pauseGame(true);
showScreen("splash-screen");
});
document.getElementById("restartGame").addEventListener("click", () => {
hideOverlay(gameOverOverlay);
game.createNewBoard();
});
document.getElementById("nextLevel").addEventListener("click", () => {
hideOverlay(gameWinOverlay);
game.createNewBoard();
});
document
.getElementById("rotate-left-btn")
.addEventListener("click", () => {
game.rotateBoard("left");
});
document
.getElementById("rotate-right-btn")
.addEventListener("click", () => {
game.rotateBoard("right");
});
});
class TubeConnection {
constructor(start, end, scene) {
this.startShape = start;
this.endShape = end;
this.scene = scene;
this.create();
}
create() {
const startPos = this.startShape.mesh.position;
const endPos = this.endShape.mesh.position;
const distance = startPos.distanceTo(endPos);
const tubeRadius = this.startShape.size * 0.15;
const geometry = new THREE.CylinderGeometry(
tubeRadius,
tubeRadius,
distance,
8,
1,
false
);
const material = new THREE.MeshPhongMaterial({
color: this.startShape.color,
transparent: true,
opacity: 0.7,
shininess: 80,
emissive: this.startShape.color,
emissiveIntensity: 0.4,
});
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.position.lerpVectors(startPos, endPos, 0.5);
const direction = new THREE.Vector3()
.subVectors(endPos, startPos)
.normalize();
const quaternion = new THREE.Quaternion().setFromUnitVectors(
new THREE.Vector3(0, 1, 0),
direction
);
this.mesh.setRotationFromQuaternion(quaternion);
this.scene.add(this.mesh);
}
remove() {
if (this.mesh) {
this.scene.remove(this.mesh);
this.mesh.geometry.dispose();
this.mesh.material.dispose();
this.mesh = null;
}
}
}
class Shape {
constructor(x, y, z, size, shapeData, row, col) {
this.geometry = shapeData.geometryFactory(size * 0.8);
this.material = new THREE.MeshPhongMaterial({
color: shapeData.color,
shininess: 30,
specular: 0x444444,
});
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.mesh.position.set(x, y, z);
if (shapeData.name !== "Sphere") {
this.mesh.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
}
this.shapeType = shapeData.name;
this.color = shapeData.color;
this.row = row;
this.col = col;
this.size = size;
this.originalScale = { x: 1, y: 1, z: 1 };
}
popIn(delay = 0) {
this.mesh.scale.set(0.01, 0.01, 0.01);
new TWEEN.Tween(this.mesh.scale)
.to({ x: 1, y: 1, z: 1 }, 500)
.easing(TWEEN.Easing.Elastic.Out)
.delay(delay)
.start();
}
explode(delay = 0, onComplete) {
const growTween = new TWEEN.Tween(this.mesh.scale)
.to({ x: 1.5, y: 1.5, z: 1.5 }, 200)
.easing(TWEEN.Easing.Quadratic.Out)
.delay(delay);
const popTween = new TWEEN.Tween(this.mesh.scale)
.to({ x: 0.01, y: 0.01, z: 0.01 }, 300)
.easing(TWEEN.Easing.Back.In)
.onComplete(() => {
if (onComplete) onComplete();
});
growTween.chain(popTween);
growTween.start();
}
popOut(onComplete) {
new TWEEN.Tween(this.mesh.scale)
.to({ x: 0.01, y: 0.01, z: 0.01 }, 300)
.easing(TWEEN.Easing.Back.In)
.onComplete(() => {
if (onComplete) onComplete();
})
.start();
}
dispose() {
this.geometry.dispose();
this.material.dispose();
}
}
class Prize {
constructor(x, y, z, size) {
this.geometry = new THREE.TorusGeometry(size, size / 4, 8, 50);
this.material = new THREE.MeshPhongMaterial({
color: 0xffd700,
shininess: 100,
specular: 0xffffff,
});
this.mesh = new THREE.Mesh(this.geometry, this.material);
this.mesh.position.set(x, y, z);
}
collect() {
const flyUp = new TWEEN.Tween(this.mesh.position)
.to({ z: this.mesh.position.z + 5 }, 500)
.easing(TWEEN.Easing.Circular.In);
new TWEEN.Tween(this.mesh.scale)
.to({ x: 0.01, y: 0.01, z: 0.01 }, 500)
.onComplete(() => this.dispose())
.chain(flyUp)
.start();
}
rotate() {
this.mesh.rotation.y += 0.02;
this.mesh.rotation.x += 0.01;
}
dispose() {
if (this.mesh.parent) this.mesh.parent.remove(this.mesh);
this.geometry.dispose();
this.material.dispose();
}
}
class BubblePopper {
constructor() {
this.init();
}
init() {
this.container = document.getElementById("container");
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x000033);
this.camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.container.appendChild(this.renderer.domElement);
this.raycaster = new THREE.Raycaster();
this.pointerPos = new THREE.Vector2();
this.hoveredShape = null;
this.tubeConnections = [];
this.shapes = [];
this.prizes = [];
this.shapeAtlas = [
{
name: "Sphere",
color: 0xff0000,
geometryFactory: (s) => new THREE.SphereGeometry(s * 1.1, 20, 16),
},
{
name: "Cube",
color: 0x00ff00,
geometryFactory: (s) =>
new THREE.BoxGeometry(s * 1.5, s * 1.5, s * 1.5),
},
{
name: "Tetra",
color: 0x0080ff,
geometryFactory: (s) => new THREE.TetrahedronGeometry(s * 1.4),
},
{
name: "Octa",
color: 0xffff00,
geometryFactory: (s) => new THREE.OctahedronGeometry(s * 1.5),
},
{
name: "Icosa",
color: 0xff00ff,
geometryFactory: (s) => new THREE.IcosahedronGeometry(s * 1.25),
},
{
name: "Dodeca",
color: 0xffffff,
geometryFactory: (s) =>
new THREE.DodecahedronGeometry(s * 1.1, 0),
},
];
this.isAnimating = false;
this.gamePaused = false;
this.scoreElement = document.getElementById("game-score");
this.gameOverElement = document.getElementById("gameOver");
this.gameWinElement = document.getElementById("gameWin");
this.rotateLeftBtn = document.getElementById("rotate-left-btn");
this.rotateRightBtn = document.getElementById("rotate-right-btn");
this.scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 7.5);
this.scene.add(directionalLight);
this.defineBoardTemplates();
window.addEventListener("resize", this.onWindowResize.bind(this));
this.container.addEventListener(
"mousedown",
this.onPointerDown.bind(this)
);
this.container.addEventListener(
"mousemove",
this.onPointerMove.bind(this)
);
this.container.addEventListener(
"touchstart",
this.onPointerDown.bind(this),
{ passive: false }
);
this.container.addEventListener(
"touchmove",
this.onPointerMove.bind(this),
{ passive: false }
);
}
defineBoardTemplates() {
this.boardTemplates = {
rectangle: (r, c) =>
Array(r)
.fill()
.map(() => Array(c).fill(true)),
diamond: (r, c) => {
const layout = Array(r)
.fill()
.map(() => Array(c).fill(false));
const midR = Math.floor(r / 2),
midC = Math.floor(c / 2);
const maxDist = Math.min(midR, midC);
for (let i = 0; i < r; i++)
for (let j = 0; j < c; j++)
if (Math.abs(i - midR) + Math.abs(j - midC) <= maxDist)
layout[i][j] = true;
return layout;
},
cross: (r, c) => {
const layout = Array(r)
.fill()
.map(() => Array(c).fill(false));
const midR = Math.floor(r / 2),
midC = Math.floor(c / 2);
const arm = Math.max(1, Math.floor(Math.min(r, c) / 5));
for (let i = 0; i < r; i++)
for (let j = 0; j < c; j++)
if (Math.abs(i - midR) <= arm || Math.abs(j - midC) <= arm)
layout[i][j] = true;
return layout;
},
circle: (r, c) => {
const layout = Array(r)
.fill()
.map(() => Array(c).fill(false));
const cR = (r - 1) / 2,
cC = (c - 1) / 2;
const radius = Math.min(r, c) / 2 - 0.5;
for (let i = 0; i < r; i++)
for (let j = 0; j < c; j++)
if (Math.sqrt((i - cR) ** 2 + (j - cC) ** 2) <= radius)
layout[i][j] = true;
return layout;
},
random: (r, c) =>
Array(r)
.fill()
.map(() => Array(c).fill(false))
.map((row) => row.map(() => Math.random() > 0.4)),
};
}
getPointerCoordinates(e) {
const eventX = e.touches ? e.touches[0].clientX : e.clientX;
const eventY = e.touches ? e.touches[0].clientY : e.clientY;
this.pointerPos.x = (eventX / window.innerWidth) * 2 - 1;
this.pointerPos.y = -(eventY / window.innerHeight) * 2 + 1;
}
onPointerMove(e) {
if (this.isAnimating || this.gamePaused) return;
this.getPointerCoordinates(e);
this.raycaster.setFromCamera(this.pointerPos, this.camera);
const shapeMeshes = [];
for (let i = 0; i < this.shapes.length; i++) {
for (let j = 0; j < this.shapes[i].length; j++) {
if (this.shapes[i]?.[j]?.mesh) {
shapeMeshes.push(this.shapes[i][j].mesh);
}
}
}
const intersects = this.raycaster.intersectObjects(shapeMeshes);
let newHoveredShape = null;
if (intersects.length > 0) {
const obj = intersects[0].object;
for (let i = 0; i < this.shapes.length; i++)
for (let j = 0; j < this.shapes[i].length; j++) {
if (this.shapes[i]?.[j]?.mesh === obj) {
newHoveredShape = this.shapes[i][j];
break;
}
}
}
if (newHoveredShape !== this.hoveredShape) {
this.clearTubeConnections();
if (newHoveredShape) {
this.hoveredShape = newHoveredShape;
this.highlightConnectedGroup(newHoveredShape);
} else {
this.hoveredShape = null;
}
}
}
onPointerDown(e) {
if (e.type === "touchstart") e.preventDefault();
if (this.isAnimating || this.gamePaused) return;
this.getPointerCoordinates(e);
this.raycaster.setFromCamera(this.pointerPos, this.camera);
const shapeMeshes = [];
for (let i = 0; i < this.shapes.length; i++) {
for (let j = 0; j < this.shapes[i].length; j++) {
if (this.shapes[i]?.[j]?.mesh) {
shapeMeshes.push(this.shapes[i][j].mesh);
}
}
}
const intersects = this.raycaster.intersectObjects(shapeMeshes);
if (intersects.length > 0) {
const clickedObject = intersects[0].object;
for (let i = 0; i < this.shapes.length; i++)
for (let j = 0; j < this.shapes[i].length; j++) {
if (this.shapes[i]?.[j]?.mesh === clickedObject) {
this.clearTubeConnections();
this.processShapeClick(i, j);
return;
}
}
}
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.positionBoardElements();
if (window.splashScene) {
window.splashScene.onWindowResize();
}
}
positionBoardElements() {
if (this.shapes.length === 0 && this.prizes.length === 0) return;
this.spacing = 1.1;
this.pieceSize = this.spacing * 0.5;
const boardWidth = this.columns * this.spacing;
const boardHeight = this.rows * this.spacing;
const fovInRadians = THREE.MathUtils.degToRad(this.camera.fov);
const distH = boardHeight / 2 / Math.tan(fovInRadians / 2);
const distW =
boardWidth / 2 / (Math.tan(fovInRadians / 2) * this.camera.aspect);
this.camera.position.z = Math.max(distH, distW) * 1.15;
this.camera.lookAt(0, 0, 0);
const offsetX = -((this.columns - 1) * this.spacing) / 2;
const offsetY = ((this.rows - 1) * this.spacing) / 2;
for (let i = 0; i < this.rows; i++)
for (let j = 0; j < this.columns; j++) {
if (this.shapes[i]?.[j]) {
this.shapes[i][j].mesh.position.x = offsetX + j * this.spacing;
this.shapes[i][j].mesh.position.y = offsetY - i * this.spacing;
}
}
this.prizes.forEach((p) => {
if (p.prizeObj) {
p.prizeObj.mesh.position.x = offsetX + p.col * this.spacing;
p.prizeObj.mesh.position.y = offsetY - p.row * this.spacing;
}
});
}
clearBoard(fullClear = true) {
this.clearTubeConnections();
const toRemove = [];
this.scene.traverse((o) => {
if (o.type === "Mesh" || o.type === "Sprite") toRemove.push(o);
});
toRemove.forEach((o) => {
if (o === this.scene.background || !o.parent) return;
o.parent.remove(o);
if (o.geometry) o.geometry.dispose();
if (o.material) {
if (Array.isArray(o.material))
o.material.forEach((m) => m.dispose());
else o.material.dispose();
}
});
this.shapes = [];
this.prizes = [];
if (fullClear) TWEEN.removeAll();
}
createNewBoard() {
this.clearBoard();
this.score = 0;
this.updateScore();
this.pauseGame(false);
this.isAnimating = true;
this.setRotationButtonsEnabled(false);
this.rows = parseInt(document.getElementById("rows-setting").value);
this.columns = parseInt(
document.getElementById("cols-setting").value
);
this.boardTemplate =
document.getElementById("template-setting").value;
this.minGroupSize = parseInt(
document.getElementById("mingroup-setting").value
);
let attempts = 0;
do {
this.generateBoardState();
attempts++;
} while (!this.hasValidMoves() && attempts < 50);
if (attempts >= 50)
console.error("Failed to generate a valid board.");
this.positionBoardElements();
this.animateShapesIn();
}
generateBoardState() {
this.clearBoard(false);
this.boardLayout = this.boardTemplates[this.boardTemplate](
this.rows,
this.columns
);
this.shapes = Array(this.rows)
.fill()
.map(() => Array(this.columns).fill(null));
const offsetX = -((this.columns - 1) * 1.1) / 2;
const offsetY = ((this.rows - 1) * 1.1) / 2;
const activeCells = [];
for (let i = 0; i < this.rows; i++)
for (let j = 0; j < this.columns; j++) {
if (this.boardLayout[i][j]) activeCells.push({ row: i, col: j });
}
this.prizes = [];
const prizesCount = Math.min(Math.floor(activeCells.length / 5), 10);
activeCells.sort(() => 0.5 - Math.random());
for (let p = 0; p < Math.min(prizesCount, activeCells.length); p++) {
this.prizes.push({ ...activeCells[p], collected: false });
}
for (let i = 0; i < this.rows; i++)
for (let j = 0; j < this.columns; j++) {
if (!this.boardLayout[i][j]) continue;
const shapeData =
this.shapeAtlas[
Math.floor(Math.random() * this.shapeAtlas.length)
];
const x = offsetX + j * 1.1;
const y = offsetY - i * 1.1;
const shape = new Shape(x, y, 0, 0.55, shapeData, i, j);
this.shapes[i][j] = shape;
this.scene.add(shape.mesh);
const prizeData = this.prizes.find(
(p) => p.row === i && p.col === j
);
if (prizeData) {
const prize = new Prize(x, y, 0.1, 0.55 * 1.2);
this.scene.add(prize.mesh);
prizeData.prizeObj = prize;
}
}
}
animateShapesIn() {
let maxDelay = 0;
for (let i = 0; i < this.rows; i++)
for (let j = 0; j < this.columns; j++) {
if (this.shapes[i]?.[j]) {
const delay = (i + j) * 20;
this.shapes[i][j].popIn(delay);
if (delay > maxDelay) maxDelay = delay;
}
}
setTimeout(() => {
this.isAnimating = false;
this.setRotationButtonsEnabled(true);
}, maxDelay + 500);
}
rotateBoard(direction) {
if (this.isAnimating || this.gamePaused) return;
this.isAnimating = true;
this.clearTubeConnections();
this.setRotationButtonsEnabled(false);
this.lastRotationDirection = direction;
this.animateVisualRotation(() => {
const rotatedLayout = this.rotateArray2D(
this.boardLayout,
direction
);
const rotatedShapes = this.rotateArray2D(this.shapes, direction);
const oldRows = this.rows;
const oldCols = this.columns;
this.rows = oldCols;
this.columns = oldRows;
this.boardLayout = rotatedLayout;
this.shapes = rotatedShapes;
this.updateShapePositions(direction);
this.rotatePrizes(direction, oldRows, oldCols);
this.positionBoardElements();
setTimeout(() => this.applyGravity(), 200);
});
}
rotateArray2D(array, direction) {
const rows = array.length;
const cols = array[0].length;
const rotated = Array(cols)
.fill()
.map(() => Array(rows).fill(null));
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (direction === "right") {
rotated[j][rows - 1 - i] = array[i][j];
} else {
rotated[cols - 1 - j][i] = array[i][j];
}
}
}
return rotated;
}
updateShapePositions(direction) {
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.columns; j++) {
const shape = this.shapes[i]?.[j];
if (shape) {
shape.row = i;
shape.col = j;
}
}
}
}
rotatePrizes(direction, oldRows, oldCols) {
this.prizes.forEach((prize) => {
const oldRow = prize.row;
const oldCol = prize.col;
if (direction === "right") {
prize.row = oldCol;
prize.col = oldRows - 1 - oldRow;
} else {
prize.row = oldCols - 1 - oldCol;
prize.col = oldRow;
}
});
}
animateVisualRotation(onComplete) {
let animationsPending = 0;
const animationDuration = 800;
const boardCenterX = 0;
const boardCenterY = 0;
const onAnimationComplete = () => {
if (--animationsPending === 0) {
if (onComplete) onComplete();
}
};
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.columns; j++) {
const shape = this.shapes[i]?.[j];
if (shape) {
const currentPos = shape.mesh.position;
const relativeX = currentPos.x - boardCenterX;
const relativeY = currentPos.y - boardCenterY;
let newRelativeX, newRelativeY;
if (this.lastRotationDirection === "right") {
newRelativeX = relativeY;
newRelativeY = -relativeX;
} else {
newRelativeX = -relativeY;
newRelativeY = relativeX;
}
const finalX = boardCenterX + newRelativeX;
const finalY = boardCenterY + newRelativeY;
animationsPending++;
new TWEEN.Tween(shape.mesh.position)
.to({ x: finalX, y: finalY }, animationDuration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onComplete(onAnimationComplete)
.start();
}
}
}
this.prizes.forEach((prize) => {
if (prize.prizeObj) {
const currentPos = prize.prizeObj.mesh.position;
const relativeX = currentPos.x - boardCenterX;
const relativeY = currentPos.y - boardCenterY;
let newRelativeX, newRelativeY;
if (this.lastRotationDirection === "right") {
newRelativeX = relativeY;
newRelativeY = -relativeX;
} else {
newRelativeX = -relativeY;
newRelativeY = relativeX;
}
const finalX = boardCenterX + newRelativeX;
const finalY = boardCenterY + newRelativeY;
animationsPending++;
new TWEEN.Tween(prize.prizeObj.mesh.position)
.to({ x: finalX, y: finalY }, animationDuration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onComplete(onAnimationComplete)
.start();
}
});
if (animationsPending === 0) {
if (onComplete) onComplete();
}
}
setRotationButtonsEnabled(enabled) {
this.rotateLeftBtn.disabled = !enabled;
this.rotateRightBtn.disabled = !enabled;
}
processShapeClick(row, col) {
const shape = this.shapes[row]?.[col];
if (!shape) return;
const connected = this.findConnectedShapes(row, col, shape.shapeType);
if (connected.length < this.minGroupSize) return;
this.isAnimating = true;
this.setRotationButtonsEnabled(false);
const clickedRow = row;
const clickedCol = col;
const explosionWaves = this.groupShapesByDistance(
connected,
clickedRow,
clickedCol
);
this.triggerFireworksExplosion(explosionWaves, connected);
}
groupShapesByDistance(connected, clickedRow, clickedCol) {
const waves = {};
connected.forEach(({ row, col }) => {
const distance = Math.max(
Math.abs(row - clickedRow),
Math.abs(col - clickedCol)
);
if (!waves[distance]) waves[distance] = [];
waves[distance].push({ row, col });
});
return waves;
}
triggerFireworksExplosion(explosionWaves, allConnected) {
this.score += allConnected.length * 10;
let callbacksPending = allConnected.length;
const onExplosionComplete = () => {
if (--callbacksPending === 0) {
allConnected.forEach(({ row: r, col: c }) => {
if (this.shapes[r]) this.shapes[r][c] = null;
});
this.updateScore();
setTimeout(() => this.applyGravity(), 200);
}
};
const waveDelays = Object.keys(explosionWaves).sort(
(a, b) => parseInt(a) - parseInt(b)
);
waveDelays.forEach((distance, waveIndex) => {
const wave = explosionWaves[distance];
const baseDelay = waveIndex * 80;
wave.forEach((pos, pieceIndex) => {
const { row: r, col: c } = pos;
const shapeToRemove = this.shapes[r]?.[c];
if (shapeToRemove) {
const pieceDelay = baseDelay + pieceIndex * 15;
const prizeIndex = this.prizes.findIndex(
(p) => p.row === r && p.col === c && !p.collected
);
if (prizeIndex !== -1) {
this.prizes[prizeIndex].collected = true;
this.score += 50;
if (this.prizes[prizeIndex].prizeObj) {
setTimeout(() => {
this.prizes[prizeIndex].prizeObj.collect();
}, pieceDelay);
}
}
shapeToRemove.explode(pieceDelay, () => {
this.scene.remove(shapeToRemove.mesh);
shapeToRemove.dispose();
onExplosionComplete();
});
} else {
onExplosionComplete();
}
});
});
}
findConnectedShapes(row, col, targetShapeType) {
const visited = new Set();
const connected = [];
const stack = [{ row, col }];
visited.add(`${row},${col}`);
while (stack.length > 0) {
const { row: r, col: c } = stack.pop();
if (
!this.boardLayout[r]?.[c] ||
this.shapes[r]?.[c]?.shapeType !== targetShapeType
)
continue;
connected.push({ row: r, col: c });
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const nr = r + dr,
nc = c + dc;
if (
nr >= 0 &&
nr < this.rows &&
nc >= 0 &&
nc < this.columns &&
!visited.has(`${nr},${nc}`) &&
this.shapes[nr]?.[nc]
) {
visited.add(`${nr},${nc}`);
stack.push({ row: nr, col: nc });
}
}
}
return connected;
}
applyGravity() {
let animationsPending = 0;
const offsetY = ((this.rows - 1) * this.spacing) / 2;
const onGravityComplete = () => {
if (--animationsPending === 0) this.removeEmptyColumns();
};
for (let j = 0; j < this.columns; j++) {
let emptySpaces = 0;
for (let i = this.rows - 1; i >= 0; i--) {
if (!this.boardLayout[i]?.[j]) {
emptySpaces = 0;
continue;
}
if (!this.shapes[i][j]) {
emptySpaces++;
} else if (emptySpaces > 0) {
const shape = this.shapes[i][j];
const targetRow = i + emptySpaces;
this.shapes[targetRow][j] = shape;
this.shapes[i][j] = null;
shape.row = targetRow;
const newY = offsetY - targetRow * this.spacing;
animationsPending++;
new TWEEN.Tween(shape.mesh.position)
.to({ y: newY }, 500)
.easing(TWEEN.Easing.Bounce.Out)
.onComplete(onGravityComplete)
.start();
this.prizes
.filter((p) => p.row === i && p.col === j && !p.collected)
.forEach((p) => {
p.row = targetRow;
if (p.prizeObj)
new TWEEN.Tween(p.prizeObj.mesh.position)
.to({ y: newY }, 500)
.easing(TWEEN.Easing.Bounce.Out)
.start();
});
}
}
}
if (animationsPending === 0) this.removeEmptyColumns();
}
removeEmptyColumns() {
const emptyCols = [];
for (let j = 0; j < this.columns; j++) {
if (
this.boardLayout.some((r) => r[j]) &&
!this.shapes.some((r) => r[j])
) {
emptyCols.push(j);
}
}
if (emptyCols.length > 0) {
this.score += emptyCols.length * 100;
this.updateScore();
let animationsPending = 0;
const newColCount = this.columns - emptyCols.length;
const offsetX = -((newColCount - 1) * this.spacing) / 2;
const onShiftComplete = () => {
if (--animationsPending === 0) {
this.finalizeColumnRemoval(emptyCols);
}
};
for (let j = 0; j < this.columns; j++) {
const shift = emptyCols.filter((c) => c < j).length;
if (shift > 0) {
const newCol = j - shift;
const newX = offsetX + newCol * this.spacing;
for (let i = 0; i < this.rows; i++) {
const shape = this.shapes[i][j];
if (shape) {
animationsPending++;
new TWEEN.Tween(shape.mesh.position)
.to({ x: newX }, 500)
.easing(TWEEN.Easing.Quadratic.Out)
.onComplete(onShiftComplete)
.start();
const prize = this.prizes.find(
(p) => p.row === i && p.col === j && p.prizeObj
);
if (prize) {
new TWEEN.Tween(prize.prizeObj.mesh.position)
.to({ x: newX }, 500)
.easing(TWEEN.Easing.Quadratic.Out)
.start();
}
}
}
}
}
if (animationsPending === 0) this.finalizeColumnRemoval(emptyCols);
} else {
this.checkGameState();
}
}
finalizeColumnRemoval(emptyCols) {
emptyCols
.sort((a, b) => b - a)
.forEach((col) => {
for (let i = 0; i < this.rows; i++) {
this.shapes[i].splice(col, 1);
this.boardLayout[i].splice(col, 1);
}
});
this.columns -= emptyCols.length;
for (let i = 0; i < this.rows; i++)
for (let j = 0; j < this.columns; j++) {
if (this.shapes[i]?.[j]) this.shapes[i][j].col = j;
}
this.prizes.forEach((p) => {
const shift = emptyCols.filter((c) => c < p.col).length;
if (shift > 0) p.col -= shift;
});
this.positionBoardElements();
this.checkGameState();
}
checkGameState() {
this.isAnimating = false;
this.setRotationButtonsEnabled(true);
if (this.prizes.length > 0 && this.prizes.every((p) => p.collected)) {
this.winLevel();
return;
}
if (!this.hasValidMoves()) {
this.endGame();
}
}
hasValidMoves() {
for (let i = 0; i < this.rows; i++)
for (let j = 0; j < this.columns; j++) {
const s = this.shapes[i]?.[j];
if (
s &&
this.findConnectedShapes(i, j, s.shapeType).length >=
this.minGroupSize
)
return true;
}
return false;
}
endGame() {
this.pauseGame(true);
this.setRotationButtonsEnabled(false);
showOverlay(this.gameOverElement);
}
winLevel() {
this.pauseGame(true);
this.setRotationButtonsEnabled(false);
showOverlay(this.gameWinElement);
}
pauseGame(isPaused) {
this.gamePaused = isPaused;
this.isAnimating = isPaused;
this.setRotationButtonsEnabled(!isPaused);
}
updateScore() {
this.scoreElement.textContent = this.score;
}
clearTubeConnections() {
this.tubeConnections.forEach((t) => t.remove());
this.tubeConnections = [];
this.shapes
.flat()
.filter((s) => s)
.forEach((s) => s.mesh.material.emissive.setHex(0x000000));
}
highlightConnectedGroup(shape) {
const connected = this.findConnectedShapes(
shape.row,
shape.col,
shape.shapeType
);
if (connected.length < this.minGroupSize) return;
connected
.map(({ row, col }) => this.shapes[row][col])
.forEach((s) => s.mesh.material.emissive.setHex(0x555555));
this.createTubesBetweenConnected(connected);
}
createTubesBetweenConnected(points) {
const pointMemo = new Set(
points.map((pos) => `${pos.row},${pos.col}`)
);
const connections = new Set();
points.forEach(({ row, col }) => {
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const nr = row + dr,
nc = col + dc;
if (pointMemo.has(`${nr},${nc}`)) {
const id = [row, col, nr, nc].sort((a, b) => a - b).join(",");
if (!connections.has(id)) {
connections.add(id);
this.tubeConnections.push(
new TubeConnection(
this.shapes[row][col],
this.shapes[nr][nc],
this.scene
)
);
}
}
}
});
}
animate() {
requestAnimationFrame(this.animate.bind(this));
TWEEN.update();
if (!this.gamePaused) {
this.prizes.forEach((p) => {
if (!p.collected && p.prizeObj) p.prizeObj.rotate();
});
}
this.renderer.render(this.scene, this.camera);
}
}
</script>
</body>
</html>