Update index.html
Browse files- index.html +646 -18
index.html
CHANGED
@@ -1,19 +1,647 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
</html>
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Mappy 3D - Synthwave Edition</title>
|
7 |
+
<style>
|
8 |
+
body {
|
9 |
+
margin: 0;
|
10 |
+
padding: 0;
|
11 |
+
display: flex;
|
12 |
+
justify-content: center;
|
13 |
+
align-items: center;
|
14 |
+
min-height: 100vh;
|
15 |
+
background-color: #1a0a2a; /* Dark synth purple */
|
16 |
+
font-family: 'Press Start 2P', cursive;
|
17 |
+
color: #fff;
|
18 |
+
overflow: hidden;
|
19 |
+
}
|
20 |
+
#game-container {
|
21 |
+
text-align: center;
|
22 |
+
}
|
23 |
+
canvas {
|
24 |
+
background-color: #1a1a2e;
|
25 |
+
display: block;
|
26 |
+
border: 4px solid #fff;
|
27 |
+
border-radius: 10px;
|
28 |
+
/* Synthwave glow */
|
29 |
+
box-shadow: 0 0 20px #fff, 0 0 30px #f0f, 0 0 40px #0ff;
|
30 |
+
}
|
31 |
+
#info-panel {
|
32 |
+
display: flex;
|
33 |
+
justify-content: space-between;
|
34 |
+
align-items: center;
|
35 |
+
width: 800px;
|
36 |
+
padding: 10px 0;
|
37 |
+
font-size: 24px;
|
38 |
+
position: absolute;
|
39 |
+
top: 10px;
|
40 |
+
left: 50%;
|
41 |
+
transform: translateX(-50%);
|
42 |
+
z-index: 10;
|
43 |
+
text-shadow: 0 0 5px #f0f;
|
44 |
+
}
|
45 |
+
#reset-button {
|
46 |
+
font-family: 'Press Start 2P', cursive;
|
47 |
+
background-color: #ff00ff;
|
48 |
+
color: #fff;
|
49 |
+
border: 2px solid #fff;
|
50 |
+
border-radius: 5px;
|
51 |
+
padding: 10px 15px;
|
52 |
+
cursor: pointer;
|
53 |
+
font-size: 16px;
|
54 |
+
box-shadow: 0 0 10px #f0f;
|
55 |
+
transition: all 0.2s ease;
|
56 |
+
}
|
57 |
+
#reset-button:hover {
|
58 |
+
background-color: #fff;
|
59 |
+
color: #ff00ff;
|
60 |
+
box-shadow: 0 0 20px #f0f, 0 0 30px #f0f;
|
61 |
+
}
|
62 |
+
#controls-info {
|
63 |
+
margin-top: 15px;
|
64 |
+
font-size: 16px;
|
65 |
+
color: #aaa;
|
66 |
+
position: absolute;
|
67 |
+
bottom: 10px;
|
68 |
+
width: 100%;
|
69 |
+
text-align: center;
|
70 |
+
}
|
71 |
+
#message-overlay {
|
72 |
+
position: absolute;
|
73 |
+
top: 0;
|
74 |
+
left: 0;
|
75 |
+
width: 100%;
|
76 |
+
height: 100%;
|
77 |
+
background-color: rgba(0, 0, 0, 0.7);
|
78 |
+
display: none; /* Hidden by default */
|
79 |
+
justify-content: center;
|
80 |
+
align-items: center;
|
81 |
+
text-align: center;
|
82 |
+
z-index: 20;
|
83 |
+
}
|
84 |
+
#message-overlay div {
|
85 |
+
font-size: 48px;
|
86 |
+
text-shadow: 0 0 10px #f0f, 0 0 20px #f0f;
|
87 |
+
}
|
88 |
+
#message-overlay span {
|
89 |
+
font-size: 24px;
|
90 |
+
color: #0ff; /* Cyan for secondary message */
|
91 |
+
margin-top: 20px;
|
92 |
+
display: block;
|
93 |
+
}
|
94 |
+
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
|
95 |
+
</style>
|
96 |
+
</head>
|
97 |
+
<body>
|
98 |
+
<div id="info-panel">
|
99 |
+
<div>
|
100 |
+
<span id="score">SCORE: 0</span>
|
101 |
+
<span id="lives" style="margin-left: 20px;">LIVES: 3</span>
|
102 |
+
</div>
|
103 |
+
<button id="reset-button">RESET</button>
|
104 |
+
</div>
|
105 |
+
<div id="game-container">
|
106 |
+
<canvas id="gameCanvas"></canvas>
|
107 |
+
</div>
|
108 |
+
<div id="message-overlay">
|
109 |
+
<div>
|
110 |
+
<div id="primary-message">MAPPY 3D</div>
|
111 |
+
<span id="secondary-message">Press Enter to Start</span>
|
112 |
+
</div>
|
113 |
+
</div>
|
114 |
+
<div id="controls-info">
|
115 |
+
🎹 Left/Right Arrow Keys to Move 🎷
|
116 |
+
</div>
|
117 |
+
|
118 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
119 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
|
120 |
+
|
121 |
+
<script>
|
122 |
+
// --- DOM Elements ---
|
123 |
+
const canvas = document.getElementById('gameCanvas');
|
124 |
+
const scoreElement = document.getElementById('score');
|
125 |
+
const livesElement = document.getElementById('lives');
|
126 |
+
const resetButton = document.getElementById('reset-button');
|
127 |
+
const messageOverlay = document.getElementById('message-overlay');
|
128 |
+
const primaryMessage = document.getElementById('primary-message');
|
129 |
+
const secondaryMessage = document.getElementById('secondary-message');
|
130 |
+
|
131 |
+
// --- Game Configuration ---
|
132 |
+
const NUM_FLOORS = 5;
|
133 |
+
const FLOOR_Y_POSITIONS = [-8, -4, 0, 4, 8];
|
134 |
+
const FLOOR_WIDTH = 40;
|
135 |
+
const FLOOR_DEPTH = 4;
|
136 |
+
const FLOOR_HEIGHT = 0.2;
|
137 |
+
const TRAMPOLINE_POS_X = 18;
|
138 |
+
const TRAMPOLINE_ALLEY_WIDTH = 6;
|
139 |
+
const PLAYER_MOVE_SPEED = 10;
|
140 |
+
const DEATH_Y_LEVEL = -15; // Y-coordinate for falling off world
|
141 |
+
|
142 |
+
// --- Game State ---
|
143 |
+
let score = 0;
|
144 |
+
let lives = 3;
|
145 |
+
let gameState = 'start';
|
146 |
+
let keys = {};
|
147 |
+
let gameObjects = [];
|
148 |
+
|
149 |
+
// --- Scene Setup ---
|
150 |
+
const scene = new THREE.Scene();
|
151 |
+
scene.background = new THREE.Color(0x1a0a2a);
|
152 |
+
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
153 |
+
camera.position.set(0, 2, 24);
|
154 |
+
const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
|
155 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
156 |
+
renderer.shadowMap.enabled = true;
|
157 |
+
|
158 |
+
// --- Lighting ---
|
159 |
+
const ambientLight = new THREE.AmbientLight(0x400080, 0.7);
|
160 |
+
scene.add(ambientLight);
|
161 |
+
const mainShadowLight = new THREE.PointLight(0xff00ff, 0.6, 50);
|
162 |
+
mainShadowLight.castShadow = true;
|
163 |
+
mainShadowLight.position.set(0, 5, 10);
|
164 |
+
scene.add(mainShadowLight);
|
165 |
+
|
166 |
+
// --- Physics Setup ---
|
167 |
+
const world = new CANNON.World();
|
168 |
+
world.gravity.set(0, -35, 0);
|
169 |
+
world.broadphase = new CANNON.NaiveBroadphase();
|
170 |
+
world.solver.iterations = 10;
|
171 |
+
|
172 |
+
// --- Materials ---
|
173 |
+
const wallMaterial = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8, roughness: 0.2 });
|
174 |
+
const roofMaterial = new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.9, roughness: 0.1 });
|
175 |
+
const trampolineMaterial = new THREE.MeshStandardMaterial({ color: 0xff00ff, emissive: 0xff00ff, emissiveIntensity: 0.8 });
|
176 |
+
const doorMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 0.6, transparent: true, opacity: 0.7 });
|
177 |
+
const windowFrameMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 1.0 });
|
178 |
+
|
179 |
+
// Physics Materials
|
180 |
+
const groundPhysMaterial = new CANNON.Material("groundMaterial");
|
181 |
+
const playerPhysMaterial = new CANNON.Material("playerMaterial");
|
182 |
+
const trampolinePhysMaterial = new CANNON.Material("trampolineMaterial");
|
183 |
+
|
184 |
+
world.addContactMaterial(new CANNON.ContactMaterial(groundPhysMaterial, playerPhysMaterial, { friction: 0.0, restitution: 0.1 }));
|
185 |
+
world.addContactMaterial(new CANNON.ContactMaterial(trampolinePhysMaterial, playerPhysMaterial, { friction: 0.3, restitution: 1.8 }));
|
186 |
+
|
187 |
+
// --- Helper function to add objects ---
|
188 |
+
function addObject(mesh, body, type, name = '') {
|
189 |
+
if(mesh) scene.add(mesh);
|
190 |
+
if(body) world.addBody(body);
|
191 |
+
gameObjects.push({ mesh, body, type, name, active: true });
|
192 |
+
}
|
193 |
+
|
194 |
+
// --- Character Creation Functions ---
|
195 |
+
function createMappy() {
|
196 |
+
const mappyGroup = new THREE.Group();
|
197 |
+
const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x4d96ff, emissive: 0x4d96ff, emissiveIntensity: 0.2 });
|
198 |
+
const detailMaterial = new THREE.MeshStandardMaterial({ color: 0x000000 });
|
199 |
+
|
200 |
+
// Body & Head
|
201 |
+
const body = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), bodyMaterial);
|
202 |
+
body.position.y = 0.5; body.castShadow = true; mappyGroup.add(body);
|
203 |
+
const head = new THREE.Mesh(new THREE.SphereGeometry(0.35, 16, 16), bodyMaterial);
|
204 |
+
head.position.y = 1.2; mappyGroup.add(head);
|
205 |
+
|
206 |
+
// Ears
|
207 |
+
const ear1 = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 0.1, 12), detailMaterial);
|
208 |
+
ear1.position.set(-0.3, 1.5, 0); mappyGroup.add(ear1);
|
209 |
+
const ear2 = ear1.clone(); ear2.position.x = 0.3; mappyGroup.add(ear2);
|
210 |
+
|
211 |
+
// Details
|
212 |
+
const nose = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), detailMaterial);
|
213 |
+
nose.position.set(0, 1.2, 0.35); mappyGroup.add(nose);
|
214 |
+
const eyeMaterial = new THREE.MeshBasicMaterial({color: 0xffffff});
|
215 |
+
const eye1 = new THREE.Mesh(new THREE.SphereGeometry(0.1, 8, 8), eyeMaterial);
|
216 |
+
eye1.position.set(-0.15, 1.3, 0.3); mappyGroup.add(eye1);
|
217 |
+
const eye2 = eye1.clone(); eye2.position.x = 0.15; mappyGroup.add(eye2);
|
218 |
+
|
219 |
+
// Hat
|
220 |
+
const hatTop = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.25, 0.3, 12), bodyMaterial);
|
221 |
+
hatTop.position.y = 1.6; mappyGroup.add(hatTop);
|
222 |
+
const hatBrim = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.3, 0.05, 12), detailMaterial);
|
223 |
+
hatBrim.position.y = 1.45; mappyGroup.add(hatBrim);
|
224 |
+
|
225 |
+
const shape = new CANNON.Sphere(0.7);
|
226 |
+
const physicsBody = new CANNON.Body({ mass: 5, shape, material: playerPhysMaterial, fixedRotation: true, linearDamping: 0.1 });
|
227 |
+
physicsBody.position.set(0, 0, 0);
|
228 |
+
addObject(mappyGroup, physicsBody, 'player', 'mappy');
|
229 |
+
}
|
230 |
+
|
231 |
+
function createCat(x, y, z) {
|
232 |
+
const catGroup = new THREE.Group();
|
233 |
+
const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xff66a3, emissive: 0xff66a3, emissiveIntensity: 0.3 });
|
234 |
+
const detailMaterial = new THREE.MeshStandardMaterial({ color: 0x222222 });
|
235 |
+
|
236 |
+
const body = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.8, 1.2), bodyMaterial);
|
237 |
+
body.position.y = 0.4; body.castShadow = true; catGroup.add(body);
|
238 |
+
const head = new THREE.Mesh(new THREE.SphereGeometry(0.4, 16, 16), bodyMaterial);
|
239 |
+
head.position.y = 1.0; head.position.z = 0.4; catGroup.add(head);
|
240 |
+
const earShape = new THREE.ConeGeometry(0.15, 0.3, 8);
|
241 |
+
const ear1 = new THREE.Mesh(earShape, detailMaterial);
|
242 |
+
ear1.position.set(-0.3, 1.4, 0.4); catGroup.add(ear1);
|
243 |
+
const ear2 = ear1.clone(); ear2.position.x = 0.3; catGroup.add(ear2);
|
244 |
+
|
245 |
+
// Details
|
246 |
+
const nose = new THREE.Mesh(new THREE.SphereGeometry(0.08, 8, 8), detailMaterial);
|
247 |
+
nose.position.set(0, 1.0, 0.8); catGroup.add(nose);
|
248 |
+
const eye1 = new THREE.Mesh(new THREE.SphereGeometry(0.1, 8, 8), detailMaterial);
|
249 |
+
eye1.scale.y = 1.5; // Oval eyes
|
250 |
+
eye1.position.set(-0.15, 1.1, 0.75); catGroup.add(eye1);
|
251 |
+
const eye2 = eye1.clone(); eye2.position.x = 0.15; catGroup.add(eye2);
|
252 |
+
|
253 |
+
// Tail
|
254 |
+
const tailSegment = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.2, 6), bodyMaterial);
|
255 |
+
let currentSegment = tailSegment;
|
256 |
+
currentSegment.position.set(0, 0.3, -0.7);
|
257 |
+
catGroup.add(currentSegment);
|
258 |
+
for(let i = 0; i < 4; i++) {
|
259 |
+
const nextSegment = tailSegment.clone();
|
260 |
+
nextSegment.position.y = -0.1;
|
261 |
+
nextSegment.rotation.x = Math.PI / 8;
|
262 |
+
currentSegment.add(nextSegment);
|
263 |
+
currentSegment = nextSegment;
|
264 |
+
}
|
265 |
+
|
266 |
+
|
267 |
+
const shape = new CANNON.Box(new CANNON.Vec3(0.4, 0.5, 0.6));
|
268 |
+
const physicsBody = new CANNON.Body({ mass: 3, material: groundPhysMaterial });
|
269 |
+
physicsBody.addShape(shape);
|
270 |
+
physicsBody.position.set(x, y, z);
|
271 |
+
physicsBody.fixedRotation = true; physicsBody.linearDamping = 0.5;
|
272 |
+
physicsBody.direction = Math.random() < 0.5 ? 1 : -1;
|
273 |
+
physicsBody.speed = 5 + Math.random() * 2;
|
274 |
+
addObject(catGroup, physicsBody, 'cat');
|
275 |
+
}
|
276 |
+
|
277 |
+
// --- Unique Artwork Creation Functions ---
|
278 |
+
function createArtwork(x, y, z, type) {
|
279 |
+
const artGroup = new THREE.Group();
|
280 |
+
const material1 = new THREE.MeshStandardMaterial({ color: 0x00ffff, metalness: 0.8, roughness: 0.1, emissive: 0x00ffff, emissiveIntensity: 0.5 });
|
281 |
+
const material2 = new THREE.MeshStandardMaterial({ color: 0xffffff, metalness: 0.8, roughness: 0.1 });
|
282 |
+
|
283 |
+
switch(type) {
|
284 |
+
case 0: // Twisted Tower
|
285 |
+
for(let i = 0; i < 5; i++) {
|
286 |
+
const box = new THREE.Mesh(new THREE.BoxGeometry(0.8 - i*0.1, 0.3, 0.8 - i*0.1), material1);
|
287 |
+
box.position.y = i * 0.3;
|
288 |
+
box.rotation.y = i * Math.PI / 4;
|
289 |
+
artGroup.add(box);
|
290 |
+
}
|
291 |
+
break;
|
292 |
+
case 1: // Geometric Stack
|
293 |
+
const base = new THREE.Mesh(new THREE.BoxGeometry(1, 0.2, 1), material2);
|
294 |
+
artGroup.add(base);
|
295 |
+
const sphere = new THREE.Mesh(new THREE.SphereGeometry(0.4, 16, 16), material1);
|
296 |
+
sphere.position.y = 0.5;
|
297 |
+
artGroup.add(sphere);
|
298 |
+
const cone = new THREE.Mesh(new THREE.ConeGeometry(0.3, 0.8, 12), material2);
|
299 |
+
cone.position.y = 1.1;
|
300 |
+
artGroup.add(cone);
|
301 |
+
break;
|
302 |
+
case 2: // Celestial Orb
|
303 |
+
const ring = new THREE.Mesh(new THREE.TorusGeometry(0.6, 0.05, 8, 32), material2);
|
304 |
+
ring.rotation.x = Math.PI / 2;
|
305 |
+
artGroup.add(ring);
|
306 |
+
const orb = new THREE.Mesh(new THREE.IcosahedronGeometry(0.4, 0), material1);
|
307 |
+
artGroup.add(orb);
|
308 |
+
break;
|
309 |
+
case 3: // Extruded Star
|
310 |
+
const starShape = new THREE.Shape();
|
311 |
+
const sides = 5;
|
312 |
+
const innerRadius = 0.2;
|
313 |
+
const outerRadius = 0.4;
|
314 |
+
starShape.moveTo(0, outerRadius);
|
315 |
+
for (let i = 0; i < sides; i++) {
|
316 |
+
let angle = (i / sides) * 2 * Math.PI;
|
317 |
+
starShape.lineTo(Math.sin(angle) * outerRadius, Math.cos(angle) * outerRadius);
|
318 |
+
angle += (1 / (sides * 2)) * 2 * Math.PI;
|
319 |
+
starShape.lineTo(Math.sin(angle) * innerRadius, Math.cos(angle) * innerRadius);
|
320 |
+
}
|
321 |
+
const extrudeSettings = { depth: 0.2, bevelEnabled: false };
|
322 |
+
const star = new THREE.Mesh(new THREE.ExtrudeGeometry(starShape, extrudeSettings), material1);
|
323 |
+
star.rotation.x = Math.PI/2;
|
324 |
+
artGroup.add(star);
|
325 |
+
break;
|
326 |
+
case 4: // Spiky Ball
|
327 |
+
const center = new THREE.Mesh(new THREE.SphereGeometry(0.3, 16, 16), material2);
|
328 |
+
artGroup.add(center);
|
329 |
+
for(let i=0; i<10; i++) {
|
330 |
+
const spike = new THREE.Mesh(new THREE.ConeGeometry(0.05, 0.5, 6), material1);
|
331 |
+
const randomDirection = new THREE.Vector3(
|
332 |
+
Math.random() * 2 - 1,
|
333 |
+
Math.random() * 2 - 1,
|
334 |
+
Math.random() * 2 - 1
|
335 |
+
).normalize();
|
336 |
+
spike.position.copy(randomDirection).multiplyScalar(0.4);
|
337 |
+
spike.lookAt(0,0,0);
|
338 |
+
spike.rotation.x += Math.PI/2;
|
339 |
+
artGroup.add(spike);
|
340 |
+
}
|
341 |
+
break;
|
342 |
+
}
|
343 |
+
|
344 |
+
artGroup.position.set(x, y, z);
|
345 |
+
artGroup.castShadow = true;
|
346 |
+
scene.add(artGroup);
|
347 |
+
const itemData = { mesh: artGroup, body: null, type: 'item', active: true, isBonus: false, bonusTimer: 0 };
|
348 |
+
gameObjects.push(itemData);
|
349 |
+
return itemData;
|
350 |
+
}
|
351 |
+
|
352 |
+
|
353 |
+
function createDoor(x, y, z) {
|
354 |
+
const doorGroup = new THREE.Group();
|
355 |
+
const doorMesh = new THREE.Mesh(new THREE.BoxGeometry(2, 3.5, 0.2), doorMaterial.clone());
|
356 |
+
doorMesh.castShadow = true;
|
357 |
+
doorGroup.add(doorMesh);
|
358 |
+
doorGroup.position.set(x, y, z);
|
359 |
+
const doorData = { mesh: doorGroup, body: null, type: 'door', active: true, opening: false, openAngle: 0 };
|
360 |
+
addObject(doorGroup, null, 'door');
|
361 |
+
return doorData;
|
362 |
+
}
|
363 |
+
|
364 |
+
// --- World Creation ---
|
365 |
+
function createWorld() {
|
366 |
+
const centralFloorWidth = FLOOR_WIDTH - TRAMPOLINE_ALLEY_WIDTH * 2;
|
367 |
+
const centralFloorShape = new CANNON.Box(new CANNON.Vec3(centralFloorWidth / 2, FLOOR_HEIGHT / 2, FLOOR_DEPTH / 2));
|
368 |
+
const gridMaterial = new THREE.MeshBasicMaterial({ color: 0xff00ff, wireframe: true });
|
369 |
+
|
370 |
+
for(let i = 0; i < NUM_FLOORS; i++) {
|
371 |
+
const y = FLOOR_Y_POSITIONS[i];
|
372 |
+
const floorBody = new CANNON.Body({ mass: 0, shape: centralFloorShape, material: groundPhysMaterial });
|
373 |
+
floorBody.position.set(0, y - FLOOR_HEIGHT / 2, 0);
|
374 |
+
const floorMesh = new THREE.Mesh(new THREE.BoxGeometry(centralFloorWidth, FLOOR_HEIGHT, FLOOR_DEPTH), gridMaterial);
|
375 |
+
addObject(floorMesh, floorBody, 'floor');
|
376 |
+
|
377 |
+
const light = new THREE.PointLight(0x00ffff, 0.2, 15);
|
378 |
+
light.position.set(0, y + 2, 5);
|
379 |
+
scene.add(light);
|
380 |
+
|
381 |
+
const wallHeight = (i === NUM_FLOORS - 1) ? 6 : 4;
|
382 |
+
const wallZ = -FLOOR_DEPTH / 2;
|
383 |
+
|
384 |
+
const wallShape = new CANNON.Box(new CANNON.Vec3(FLOOR_WIDTH / 2, wallHeight / 2, 0.1));
|
385 |
+
const wallBody = new CANNON.Body({ mass: 0, material: groundPhysMaterial });
|
386 |
+
wallBody.addShape(wallShape);
|
387 |
+
wallBody.position.set(0, y + wallHeight / 2, wallZ);
|
388 |
+
|
389 |
+
const backWallGroup = new THREE.Group();
|
390 |
+
const wallMesh = new THREE.Mesh(new THREE.BoxGeometry(FLOOR_WIDTH, wallHeight, 0.2), wallMaterial);
|
391 |
+
backWallGroup.add(wallMesh);
|
392 |
+
|
393 |
+
if (i === NUM_FLOORS - 1) {
|
394 |
+
[-10, 0, 10].forEach(wx => {
|
395 |
+
const arch = new THREE.Shape();
|
396 |
+
arch.moveTo(-1.5, 0);
|
397 |
+
arch.lineTo(-1.5, 2.5);
|
398 |
+
arch.absarc(0, 2.5, 1.5, Math.PI, 0, true);
|
399 |
+
arch.lineTo(1.5, 0);
|
400 |
+
arch.lineTo(-1.5, 0);
|
401 |
+
|
402 |
+
const frame = new THREE.Mesh(new THREE.ShapeGeometry(arch), windowFrameMaterial);
|
403 |
+
frame.position.set(wx, 0.1, 0.11);
|
404 |
+
backWallGroup.add(frame);
|
405 |
+
});
|
406 |
+
} else {
|
407 |
+
[-10, 10].forEach(wx => {
|
408 |
+
const frameGeo = new THREE.BoxGeometry(2.2, 2.2, 0.1);
|
409 |
+
const frame = new THREE.Mesh(frameGeo, windowFrameMaterial);
|
410 |
+
frame.position.set(wx, -0.5, 0.11);
|
411 |
+
backWallGroup.add(frame);
|
412 |
+
});
|
413 |
+
}
|
414 |
+
|
415 |
+
addObject(backWallGroup, wallBody, 'wall');
|
416 |
+
}
|
417 |
+
|
418 |
+
const topY = FLOOR_Y_POSITIONS[NUM_FLOORS - 1] + 6;
|
419 |
+
const roofShape = new CANNON.Box(new CANNON.Vec3(FLOOR_WIDTH / 2, 0.1, FLOOR_DEPTH / 2));
|
420 |
+
const roofBody = new CANNON.Body({ mass: 0, material: groundPhysMaterial });
|
421 |
+
roofBody.addShape(roofShape);
|
422 |
+
roofBody.position.set(0, topY, 0);
|
423 |
+
const roofMesh = new THREE.Mesh(new THREE.BoxGeometry(FLOOR_WIDTH, 0.2, FLOOR_DEPTH), roofMaterial);
|
424 |
+
addObject(roofMesh, roofBody, 'roof');
|
425 |
+
|
426 |
+
[-TRAMPOLINE_POS_X, TRAMPOLINE_POS_X].forEach(x => {
|
427 |
+
const trampBody = new CANNON.Body({ mass: 0, shape: new CANNON.Box(new CANNON.Vec3(TRAMPOLINE_ALLEY_WIDTH/2, 0.2, FLOOR_DEPTH)), material: trampolinePhysMaterial });
|
428 |
+
trampBody.position.set(x, FLOOR_Y_POSITIONS[0] - 1, 0);
|
429 |
+
const trampMesh = new THREE.Mesh(new THREE.BoxGeometry(TRAMPOLINE_ALLEY_WIDTH, 0.4, FLOOR_DEPTH * 2), trampolineMaterial);
|
430 |
+
trampMesh.receiveShadow = true;
|
431 |
+
addObject(trampMesh, trampBody, 'trampoline');
|
432 |
+
});
|
433 |
+
|
434 |
+
const sideWallHeight = topY - FLOOR_Y_POSITIONS[0] + 2;
|
435 |
+
const sideWallShape = new CANNON.Box(new CANNON.Vec3(0.5, sideWallHeight/2, FLOOR_DEPTH/2));
|
436 |
+
[-FLOOR_WIDTH/2 - 0.5, FLOOR_WIDTH/2 + 0.5].forEach(x => {
|
437 |
+
const wallBody = new CANNON.Body({ mass: 0, material: groundPhysMaterial});
|
438 |
+
wallBody.addShape(sideWallShape); wallBody.position.set(x, sideWallHeight/2 + FLOOR_Y_POSITIONS[0] - 1, 0);
|
439 |
+
addObject(new THREE.Mesh(new THREE.BoxGeometry(1, sideWallHeight, FLOOR_DEPTH), wallMaterial), wallBody, 'wall');
|
440 |
+
});
|
441 |
+
}
|
442 |
+
|
443 |
+
// --- Game Logic ---
|
444 |
+
function resetGame() {
|
445 |
+
score = 0;
|
446 |
+
lives = 3;
|
447 |
+
updateUI();
|
448 |
+
initLevel();
|
449 |
+
}
|
450 |
+
|
451 |
+
function initLevel() {
|
452 |
+
gameObjects.forEach(obj => {
|
453 |
+
if (obj.mesh) scene.remove(obj.mesh);
|
454 |
+
if (obj.body) world.remove(obj.body);
|
455 |
+
});
|
456 |
+
gameObjects = [];
|
457 |
+
|
458 |
+
createWorld();
|
459 |
+
createMappy();
|
460 |
+
|
461 |
+
FLOOR_Y_POSITIONS.forEach((y, i) => {
|
462 |
+
const catCount = (i === NUM_FLOORS - 1) ? 2 : 1;
|
463 |
+
for (let c = 0; c < catCount; c++) {
|
464 |
+
createCat((Math.random() - 0.5) * (FLOOR_WIDTH - TRAMPOLINE_ALLEY_WIDTH * 2 - 4), y + 1, 0);
|
465 |
+
}
|
466 |
+
if (i < NUM_FLOORS - 1) {
|
467 |
+
createDoor((Math.random() > 0.5 ? 1 : -1) * 8, y + 1.75, 0);
|
468 |
+
}
|
469 |
+
});
|
470 |
+
|
471 |
+
const items = [];
|
472 |
+
const artTypes = 5; // Number of createArtwork types
|
473 |
+
for (let i = 0; i < NUM_FLOORS; i++) {
|
474 |
+
const itemCount = (i === NUM_FLOORS - 1) ? 4 : 2;
|
475 |
+
for(let j=0; j<itemCount; j++){
|
476 |
+
const itemX = (Math.random() - 0.5) * (FLOOR_WIDTH - TRAMPOLINE_ALLEY_WIDTH * 2 - 4);
|
477 |
+
const artType = Math.floor(Math.random() * artTypes);
|
478 |
+
items.push(createArtwork(itemX, FLOOR_Y_POSITIONS[i] + 1.5, 0, artType));
|
479 |
+
}
|
480 |
+
}
|
481 |
+
const bonusItem = items[Math.floor(Math.random() * items.length)];
|
482 |
+
bonusItem.isBonus = true;
|
483 |
+
bonusItem.bonusTimer = 15;
|
484 |
+
bonusItem.mesh.children.forEach(child => {
|
485 |
+
child.material = new THREE.MeshStandardMaterial({ color: 0xff00ff, metalness: 1.0, roughness: 0.1, emissive: 0xff00ff, emissiveIntensity: 1.0 });
|
486 |
+
});
|
487 |
+
|
488 |
+
|
489 |
+
gameState = 'playing';
|
490 |
+
messageOverlay.style.display = 'none';
|
491 |
+
}
|
492 |
+
|
493 |
+
function resetPlayer() {
|
494 |
+
lives--;
|
495 |
+
updateUI();
|
496 |
+
if (lives <= 0) {
|
497 |
+
gameState = 'game-over';
|
498 |
+
showMessage('GAME OVER 😭', 'Press Enter to Restart');
|
499 |
+
} else {
|
500 |
+
const playerObj = gameObjects.find(obj => obj.type === 'player');
|
501 |
+
if (playerObj) {
|
502 |
+
playerObj.body.position.set(0, FLOOR_Y_POSITIONS[2] + 5, 0);
|
503 |
+
playerObj.body.velocity.set(0, 0, 0);
|
504 |
+
playerObj.body.angularVelocity.set(0, 0, 0);
|
505 |
+
}
|
506 |
+
}
|
507 |
+
}
|
508 |
+
|
509 |
+
function updateUI() {
|
510 |
+
scoreElement.textContent = `SCORE: ${score}`;
|
511 |
+
livesElement.textContent = `LIVES: ${lives}`;
|
512 |
+
}
|
513 |
+
|
514 |
+
function showMessage(primary, secondary) {
|
515 |
+
primaryMessage.textContent = primary;
|
516 |
+
secondaryMessage.textContent = secondary;
|
517 |
+
messageOverlay.style.display = 'flex';
|
518 |
+
}
|
519 |
+
|
520 |
+
// --- Main Game Loop ---
|
521 |
+
const clock = new THREE.Clock();
|
522 |
+
let cameraTarget = new THREE.Vector3();
|
523 |
+
|
524 |
+
function animate() {
|
525 |
+
requestAnimationFrame(animate);
|
526 |
+
const dt = clock.getDelta();
|
527 |
+
|
528 |
+
if (gameState === 'playing') {
|
529 |
+
world.step(1 / 60, dt);
|
530 |
+
|
531 |
+
const playerObj = gameObjects.find(obj => obj.type === 'player');
|
532 |
+
if (!playerObj) return;
|
533 |
+
|
534 |
+
// Check if player fell off the world
|
535 |
+
if (playerObj.body.position.y < DEATH_Y_LEVEL) {
|
536 |
+
resetPlayer();
|
537 |
+
}
|
538 |
+
|
539 |
+
const currentXVelocity = playerObj.body.velocity.x;
|
540 |
+
let targetXVelocity = 0;
|
541 |
+
if (keys['ArrowLeft']) {
|
542 |
+
targetXVelocity = -PLAYER_MOVE_SPEED;
|
543 |
+
} else if (keys['ArrowRight']) {
|
544 |
+
targetXVelocity = PLAYER_MOVE_SPEED;
|
545 |
+
}
|
546 |
+
playerObj.body.velocity.x = currentXVelocity + (targetXVelocity - currentXVelocity) * 0.3;
|
547 |
+
|
548 |
+
|
549 |
+
gameObjects.forEach(obj => {
|
550 |
+
if (!obj.active) return;
|
551 |
+
if (obj.mesh && obj.body) {
|
552 |
+
obj.mesh.position.copy(obj.body.position);
|
553 |
+
obj.mesh.quaternion.copy(obj.body.quaternion);
|
554 |
+
}
|
555 |
+
|
556 |
+
if (obj.type === 'cat') {
|
557 |
+
const centralFloorWidth = FLOOR_WIDTH - TRAMPOLINE_ALLEY_WIDTH * 2;
|
558 |
+
if (Math.abs(obj.body.position.x) > centralFloorWidth / 2 - 2) obj.body.direction *= -1;
|
559 |
+
obj.body.velocity.x = obj.body.direction * obj.body.speed;
|
560 |
+
if (playerObj.body.position.distanceTo(obj.body.position) < 1.2) resetPlayer();
|
561 |
+
}
|
562 |
+
|
563 |
+
if (obj.type === 'item') {
|
564 |
+
obj.mesh.rotation.y += 0.02;
|
565 |
+
if (obj.isBonus) {
|
566 |
+
obj.bonusTimer -= dt;
|
567 |
+
const intensity = Math.abs(Math.sin(obj.bonusTimer * 20)) * 1.5 + 0.5;
|
568 |
+
obj.mesh.children.forEach(child => {
|
569 |
+
if(child.material.emissive) child.material.emissiveIntensity = intensity
|
570 |
+
});
|
571 |
+
if (obj.bonusTimer <= 0) {
|
572 |
+
obj.isBonus = false;
|
573 |
+
// This part is tricky as materials are different. A full implementation would store original materials.
|
574 |
+
// For now, we just stop the flashing.
|
575 |
+
obj.mesh.children.forEach(child => {
|
576 |
+
if(child.material.emissive) child.material.emissiveIntensity = 0.5
|
577 |
+
});
|
578 |
+
}
|
579 |
+
}
|
580 |
+
if (playerObj.mesh.position.distanceTo(obj.mesh.position) < 1.5) {
|
581 |
+
obj.active = false; scene.remove(obj.mesh);
|
582 |
+
score += obj.isBonus ? 1000 : 100;
|
583 |
+
updateUI();
|
584 |
+
}
|
585 |
+
}
|
586 |
+
|
587 |
+
if (obj.type === 'door' && !obj.opening) {
|
588 |
+
if (playerObj.mesh.position.distanceTo(obj.mesh.position) < 1.5) {
|
589 |
+
obj.opening = true;
|
590 |
+
gameObjects.filter(o => o.type === 'cat' && o.active).forEach(cat => {
|
591 |
+
if (cat.mesh.position.distanceTo(obj.mesh.position) < 2.5) {
|
592 |
+
cat.active = false;
|
593 |
+
scene.remove(cat.mesh);
|
594 |
+
world.remove(cat.body);
|
595 |
+
score += 500;
|
596 |
+
updateUI();
|
597 |
+
}
|
598 |
+
});
|
599 |
+
}
|
600 |
+
}
|
601 |
+
if (obj.opening && obj.openAngle < Math.PI / 2) {
|
602 |
+
obj.openAngle += dt * 5;
|
603 |
+
obj.mesh.children[0].rotation.y = obj.openAngle;
|
604 |
+
}
|
605 |
+
});
|
606 |
+
|
607 |
+
if (gameObjects.filter(o => o.type === 'item' && o.active).length === 0) {
|
608 |
+
gameState = 'level-clear';
|
609 |
+
score += 1000;
|
610 |
+
updateUI();
|
611 |
+
showMessage('LEVEL CLEAR! ✨', 'Press Enter for Next Level');
|
612 |
+
}
|
613 |
+
|
614 |
+
cameraTarget.set(playerObj.mesh.position.x * 0.5, playerObj.mesh.position.y + 2, 24);
|
615 |
+
camera.position.lerp(cameraTarget, 0.05);
|
616 |
+
}
|
617 |
+
|
618 |
+
renderer.render(scene, camera);
|
619 |
+
}
|
620 |
+
|
621 |
+
// --- Event Listeners ---
|
622 |
+
resetButton.addEventListener('click', resetGame);
|
623 |
+
|
624 |
+
window.addEventListener('keydown', (e) => {
|
625 |
+
keys[e.key] = true;
|
626 |
+
if (e.key === 'Enter') {
|
627 |
+
if (gameState === 'start' || gameState === 'game-over') {
|
628 |
+
resetGame();
|
629 |
+
} else if (gameState === 'level-clear') {
|
630 |
+
initLevel();
|
631 |
+
}
|
632 |
+
}
|
633 |
+
});
|
634 |
+
window.addEventListener('keyup', (e) => { keys[e.key] = false; });
|
635 |
+
window.addEventListener('resize', () => {
|
636 |
+
camera.aspect = window.innerWidth / window.innerHeight;
|
637 |
+
camera.updateProjectionMatrix();
|
638 |
+
renderer.setSize(window.innerWidth, window.innerHeight);
|
639 |
+
}, false);
|
640 |
+
|
641 |
+
// --- Start Game ---
|
642 |
+
updateUI();
|
643 |
+
showMessage('MAPPY 3D', 'Press Enter to Start');
|
644 |
+
animate();
|
645 |
+
</script>
|
646 |
+
</body>
|
647 |
</html>
|