awacke1 commited on
Commit
60c08e9
·
verified ·
1 Parent(s): bf40a38

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1078 -18
index.html CHANGED
@@ -1,19 +1,1079 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Blocky Brawl: Dungeon Dimensions</title>
7
+ <style>
8
+ /* Basic styling for the body to remove default margins and overflows */
9
+ body {
10
+ margin: 0;
11
+ overflow: hidden; /* Hide scrollbars */
12
+ font-family: 'Inter', sans-serif; /* Use Inter font */
13
+ background-color: #1a1a1a; /* Dark background for the page */
14
+ }
15
+
16
+ /* Container for the game canvas and UI elements */
17
+ #game-container {
18
+ position: relative;
19
+ width: 100vw; /* Full viewport width */
20
+ height: 100vh; /* Full viewport height */
21
+ display: flex; /* Use flexbox for centering */
22
+ justify-content: center; /* Center horizontally */
23
+ align-items: center; /* Center vertically */
24
+ overflow: hidden; /* Ensure no overflow */
25
+ }
26
+
27
+ /* Main UI container at the top center */
28
+ #top-ui-container {
29
+ position: absolute;
30
+ top: 20px; /* Distance from the top */
31
+ left: 50%;
32
+ transform: translateX(-50%);
33
+ display: flex;
34
+ flex-direction: column;
35
+ align-items: center;
36
+ gap: 15px; /* Space between UI sections */
37
+ z-index: 10;
38
+ pointer-events: none; /* Allow interaction with elements inside */
39
+ }
40
+
41
+ /* Container for scores and health/mana bars */
42
+ #player-stats-container {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ width: 100%; /* Will be set dynamically by JS to fit content */
46
+ max-width: 800px; /* Max width for the stats section */
47
+ gap: 50px; /* Space between player stat blocks */
48
+ pointer-events: auto; /* Re-enable pointer events for UI elements */
49
+ }
50
+
51
+ /* Styling for the score displays */
52
+ .score-display {
53
+ color: white; /* White text color */
54
+ font-size: 2.2em; /* Larger font size */
55
+ font-weight: bold; /* Bold text */
56
+ text-shadow: 3px 3px 6px rgba(0,0,0,0.8); /* Stronger text shadow for readability */
57
+ padding: 10px 15px; /* Padding around text */
58
+ background-color: rgba(0, 0, 0, 0.4); /* Semi-transparent background */
59
+ border-radius: 12px; /* Rounded corners */
60
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5); /* Subtle box shadow */
61
+ text-align: center;
62
+ min-width: 150px; /* Ensure minimum width */
63
+ }
64
+
65
+ /* Styling for the health and mana bars container */
66
+ .player-bars {
67
+ height: 70px; /* Height of the bar container */
68
+ width: 250px; /* Max width for bars */
69
+ border: 3px solid #ffffff; /* White border */
70
+ border-radius: 10px; /* Rounded corners */
71
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5); /* Subtle box shadow */
72
+ display: flex;
73
+ flex-direction: column;
74
+ justify-content: space-around;
75
+ padding: 5px;
76
+ box-sizing: border-box;
77
+ background-color: rgba(0, 0, 0, 0.4); /* Background for the bars container */
78
+ }
79
+
80
+ .bar-container {
81
+ width: 100%;
82
+ height: 25px; /* Height for individual bars */
83
+ background-color: #4a5568; /* Grey background for empty bar */
84
+ border-radius: 8px;
85
+ overflow: hidden;
86
+ }
87
+
88
+ .health-bar, .mana-bar {
89
+ height: 100%;
90
+ width: 100%; /* Initial width, will be updated by JS */
91
+ transition: width 0.3s ease-out, background-color 0.3s ease-out; /* Smooth transitions */
92
+ border-radius: 8px;
93
+ }
94
+
95
+ .health-bar { background-color: #28a745; } /* Green for health */
96
+ .mana-bar { background-color: #3b82f6; } /* Blue for mana */
97
+
98
+
99
+ /* Styling for the reset button */
100
+ #reset-button {
101
+ position: absolute;
102
+ bottom: 40px; /* Distance from the bottom */
103
+ left: 50%; /* Center horizontally */
104
+ transform: translateX(-50%); /* Adjust for true centering */
105
+ padding: 18px 35px; /* Generous padding */
106
+ font-size: 1.8em; /* Larger font size */
107
+ background: linear-gradient(145deg, #ff6b6b, #ee4444); /* Gradient background */
108
+ color: white; /* White text */
109
+ border: none; /* No border */
110
+ border-radius: 15px; /* More rounded corners */
111
+ cursor: pointer; /* Pointer cursor on hover */
112
+ box-shadow: 0 8px 20px rgba(0,0,0,0.6); /* Stronger shadow */
113
+ transition: background 0.3s ease, transform 0.1s ease, box-shadow 0.3s ease; /* Smooth transitions */
114
+ z-index: 10; /* Ensure it's above the canvas */
115
+ font-weight: bold; /* Bold text */
116
+ letter-spacing: 1px; /* Slight letter spacing */
117
+ text-transform: uppercase; /* Uppercase text */
118
+ pointer-events: auto; /* Allow interaction */
119
+ }
120
+
121
+ #reset-button:hover {
122
+ background: linear-gradient(145deg, #ff4d4d, #cc3333); /* Darker gradient on hover */
123
+ transform: translateX(-50%) scale(1.05); /* Slightly enlarge on hover */
124
+ box-shadow: 0 10px 25px rgba(0,0,0,0.8); /* Deeper shadow on hover */
125
+ }
126
+
127
+ #reset-button:active {
128
+ transform: translateX(-50%) scale(0.98); /* Shrink slightly on click */
129
+ box-shadow: 0 4px 10px rgba(0,0,0,0.4); /* Recessed shadow on click */
130
+ }
131
+
132
+ /* Styling for the Three.js canvas */
133
+ canvas {
134
+ display: block; /* Remove extra space below canvas */
135
+ width: 100%; /* Make canvas fill its container */
136
+ height: 100%; /* Make canvas fill its container */
137
+ border-radius: 15px; /* Rounded corners for the canvas itself */
138
+ box-shadow: 0 0 25px rgba(0,0,0,0.7); /* Shadow around the canvas */
139
+ }
140
+
141
+ /* Controls display */
142
+ #controls {
143
+ background-color: rgba(0, 0, 0, 0.6);
144
+ color: white;
145
+ padding: 15px 25px;
146
+ border-radius: 12px;
147
+ box-shadow: 0 4px 15px rgba(0,0,0,0.7);
148
+ font-size: 1.1em;
149
+ text-align: center;
150
+ pointer-events: auto; /* Allow interaction */
151
+ display: flex;
152
+ gap: 40px; /* Space between player control sections */
153
+ }
154
+
155
+ #controls h3 {
156
+ margin-top: 0;
157
+ color: #ffd700; /* Gold color for headings */
158
+ font-size: 1.3em;
159
+ margin-bottom: 10px;
160
+ }
161
+
162
+ #controls ul {
163
+ list-style: none;
164
+ padding: 0;
165
+ margin: 0;
166
+ text-align: left;
167
+ }
168
+
169
+ #controls li {
170
+ margin-bottom: 5px;
171
+ }
172
+
173
+ #controls span {
174
+ font-weight: bold;
175
+ color: #aaffaa; /* Light green for keys */
176
+ display: inline-block;
177
+ width: 40px; /* Fixed width for key display */
178
+ }
179
+ </style>
180
+ </head>
181
+ <body>
182
+ <div id="game-container">
183
+ <div id="top-ui-container">
184
+ <div id="player-stats-container">
185
+ <div>
186
+ <div id="score-left" class="score-display">P1 Score: 0</div>
187
+ <div id="player-left-bars" class="player-bars">
188
+ <div class="bar-container"><div id="health-bar-left" class="health-bar"></div></div>
189
+ <div class="bar-container"><div id="mana-bar-left" class="mana-bar"></div></div>
190
+ </div>
191
+ </div>
192
+ <div>
193
+ <div id="score-right" class="score-display">P2 Score: 0</div>
194
+ <div id="player-right-bars" class="player-bars">
195
+ <div class="bar-container"><div id="health-bar-right" class="health-bar"></div></div>
196
+ <div class="bar-container"><div id="mana-bar-right" class="mana-bar"></div></div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <div id="controls">
202
+ <div>
203
+ <h3>Player 1 Controls (Red Wizard)</h3>
204
+ <ul>
205
+ <li><span>WASD:</span> Move</li>
206
+ <li><span>E:</span> Cast Spell</li>
207
+ </ul>
208
+ </div>
209
+ <div>
210
+ <h3>Player 2 Controls (Blue Wizard)</h3>
211
+ <ul>
212
+ <li><span>IJKL:</span> Move</li>
213
+ <li><span>U:</span> Cast Spell</li>
214
+ </ul>
215
+ </div>
216
+ </div>
217
+ </div>
218
+
219
+ <button id="reset-button">Primitive Reset!</button>
220
+ </div>
221
+
222
+ <script type="module">
223
+ // Import Three.js library from a reliable CDN
224
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
225
+
226
+ // Declare global variables for Three.js scene, camera, renderer, and game state
227
+ let scene, camera, renderer;
228
+ let player1, player2; // Our primitive-assembled characters (Wizards)
229
+ let monsters = []; // Array to hold monster objects
230
+ let activeProjectiles = []; // Array to hold active spell projectiles
231
+
232
+ // Game grid dimensions and unit size
233
+ const gridSize = 10; // Size of one grid square in Three.js units
234
+ const mapWidth = 20; // Number of grid squares wide
235
+ const mapHeight = 20; // Number of grid squares high
236
+ const dungeonMap = []; // 2D array to represent the dungeon layout (0: floor, 1: wall)
237
+
238
+ // Player statistics
239
+ const playerStats = {
240
+ left: {
241
+ health: 100,
242
+ maxHealth: 100,
243
+ mana: 50,
244
+ maxMana: 50,
245
+ score: 0,
246
+ position: new THREE.Vector3(-5 * gridSize, 0, -5 * gridSize) // Initial world position
247
+ },
248
+ right: {
249
+ health: 100,
250
+ maxHealth: 100,
251
+ mana: 50,
252
+ maxMana: 50,
253
+ score: 0,
254
+ position: new THREE.Vector3(5 * gridSize, 0, 5 * gridSize) // Initial world position
255
+ }
256
+ };
257
+
258
+ // Movement speed for players and monsters
259
+ const playerMoveSpeed = 0.15 * gridSize; // Adjust for desired player speed relative to grid
260
+ const monsterMoveSpeed = 0.05 * gridSize; // Adjust for desired monster speed relative to grid
261
+
262
+ // References to UI HTML elements
263
+ const uiElements = {
264
+ scoreLeft: document.getElementById('score-left'),
265
+ scoreRight: document.getElementById('score-right'),
266
+ healthBarLeft: document.getElementById('health-bar-left'),
267
+ manaBarLeft: document.getElementById('mana-bar-left'),
268
+ healthBarRight: document.getElementById('health-bar-right'),
269
+ manaBarRight: document.getElementById('mana-bar-right'),
270
+ resetButton: document.getElementById('reset-button')
271
+ };
272
+
273
+ // Object to keep track of pressed keys for smooth movement
274
+ const keys = {
275
+ // Player 1 movement
276
+ w: false, a: false, s: false, d: false,
277
+ // Player 1 action
278
+ p1_action: false,
279
+
280
+ // Player 2 movement
281
+ i: false, j: false, k: false, l: false,
282
+ // Player 2 action
283
+ p2_action: false
284
+ };
285
+
286
+ // Spell projectile parameters
287
+ const spellProjectileSpeed = 0.8 * gridSize;
288
+ const spellProjectileLife = 120; // frames (2 seconds at 60fps)
289
+ const spellProjectileRadius = 0.03 * gridSize; // Adjusted for grid size
290
+ const spellDamage = 20; // Damage dealt by a spell
291
+
292
+ // Player-to-player healing parameters
293
+ const playerHealingThreshold = gridSize * 2.5; // Players heal each other if within this distance
294
+ const healingAmountPerFrame = 0.1; // Small continuous heal
295
+
296
+ /**
297
+ * Handles window resize events to keep the camera and renderer aspect ratio correct.
298
+ */
299
+ function onWindowResize() {
300
+ const aspectRatio = window.innerWidth / window.innerHeight;
301
+ const frustumSize = Math.max(mapWidth, mapHeight) * gridSize * 0.7;
302
+ camera.left = frustumSize * aspectRatio / - 2;
303
+ camera.right = frustumSize * aspectRatio / 2;
304
+ camera.top = frustumSize / 2;
305
+ camera.bottom = frustumSize / - 2;
306
+ camera.updateProjectionMatrix();
307
+ renderer.setSize(window.innerWidth, window.innerHeight);
308
+ }
309
+
310
+ /**
311
+ * Handles keydown events, setting the corresponding key in the `keys` object to true.
312
+ * @param {KeyboardEvent} event - The keyboard event.
313
+ */
314
+ function onKeyDown(event) {
315
+ switch (event.code) {
316
+ // Player 1 Movement
317
+ case 'KeyW': keys.w = true; break;
318
+ case 'KeyA': keys.a = true; break;
319
+ case 'KeyS': keys.s = true; break;
320
+ case 'KeyD': keys.d = true; break;
321
+ // Player 1 Action
322
+ case 'KeyE': keys.p1_action = true; break;
323
+
324
+ // Player 2 Movement
325
+ case 'KeyI': keys.i = true; break;
326
+ case 'KeyJ': keys.j = true; break;
327
+ case 'KeyK': keys.k = true; break;
328
+ case 'KeyL': keys.l = true; break;
329
+ // Player 2 Action
330
+ case 'KeyU': keys.p2_action = true; break;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Handles keyup events, setting the corresponding key in the `keys` object to false.
336
+ * @param {KeyboardEvent} event - The keyboard event.
337
+ */
338
+ function onKeyUp(event) {
339
+ switch (event.code) {
340
+ case 'KeyW': keys.w = false; break;
341
+ case 'KeyA': keys.a = false; break;
342
+ case 'KeyS': keys.s = false; break;
343
+ case 'KeyD': keys.d = false; break;
344
+
345
+ case 'KeyE': keys.p1_action = false; break;
346
+
347
+ case 'KeyI': keys.i = false; break;
348
+ case 'KeyJ': keys.j = false; break;
349
+ case 'KeyK': keys.k = false; break;
350
+ case 'KeyL': keys.l = false; break;
351
+
352
+ case 'KeyU': keys.p2_action = false; break;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Generates a simple dungeon layout with floor and wall tiles.
358
+ * The dungeon is a large open room with a border of walls and some random inner walls.
359
+ */
360
+ function generateDungeon() {
361
+ for (let y = 0; y < mapHeight; y++) {
362
+ dungeonMap[y] = []; // Initialize inner array for each row
363
+ for (let x = 0; x < mapWidth; x++) {
364
+ // Create a border of walls around the map
365
+ if (x === 0 || x === mapWidth - 1 || y === 0 || y === mapHeight - 1) {
366
+ dungeonMap[y][x] = 1; // Mark as wall
367
+ createWall(x, y); // Create the 3D wall object
368
+ } else if (Math.random() < 0.05) { // 5% chance of a random inner wall
369
+ dungeonMap[y][x] = 1; // Mark as wall
370
+ createWall(x, y); // Create the 3D wall object
371
+ } else {
372
+ dungeonMap[y][x] = 0; // Mark as floor
373
+ createFloor(x, y); // Create the 3D floor object
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Creates a 3D floor tile at the given grid coordinates.
381
+ * Uses MeshPhongMaterial for better lighting.
382
+ * @param {number} x - The x-coordinate on the dungeon grid.
383
+ * @param {number} y - The y-coordinate on the dungeon grid.
384
+ */
385
+ function createFloor(x, y) {
386
+ const geometry = new THREE.BoxGeometry(gridSize, 1, gridSize); // Flat box for floor
387
+ const material = new THREE.MeshPhongMaterial({
388
+ color: 0x3d4a5c, // Darker grey floor color
389
+ specular: 0x111111, // Slight specular highlight
390
+ shininess: 30 // Moderate shininess
391
+ });
392
+ const floor = new THREE.Mesh(geometry, material);
393
+ // Position the floor tile correctly in the 3D world
394
+ floor.position.set(
395
+ (x - mapWidth / 2 + 0.5) * gridSize, // Center X of the grid square
396
+ -0.5, // Half height of the floor to be below the ground plane (y=0)
397
+ (y - mapHeight / 2 + 0.5) * gridSize // Center Z of the grid square
398
+ );
399
+ scene.add(floor); // Add to the scene
400
+ }
401
+
402
+ /**
403
+ * Creates a 3D wall tile at the given grid coordinates.
404
+ * Uses MeshPhongMaterial for better lighting.
405
+ * @param {number} x - The x-coordinate on the dungeon grid.
406
+ * @param {number} y - The y-coordinate on the dungeon grid.
407
+ */
408
+ function createWall(x, y) {
409
+ const geometry = new THREE.BoxGeometry(gridSize, gridSize * 2, gridSize); // Taller box for walls
410
+ const material = new THREE.MeshPhongMaterial({
411
+ color: 0x90a4ae, // Lighter grey wall color
412
+ specular: 0x333333, // More pronounced specular highlight
413
+ shininess: 60 // Higher shininess for walls
414
+ });
415
+ const wall = new THREE.Mesh(geometry, material);
416
+ // Position the wall tile correctly in the 3D world
417
+ wall.position.set(
418
+ (x - mapWidth / 2 + 0.5) * gridSize,
419
+ gridSize, // Position walls so they sit on the floor (half of wall height from y=0)
420
+ (y - mapHeight / 2 + 0.5) * gridSize
421
+ );
422
+ scene.add(wall); // Add to the scene
423
+ }
424
+
425
+ /**
426
+ * Creates a 3D wizard character assembled from primitive shapes, made less blocky.
427
+ * @param {number} color - The base color for the wizard's body.
428
+ * @returns {THREE.Group} A Three.js Group containing all wizard parts.
429
+ */
430
+ function createWizard(color) {
431
+ const wizardGroup = new THREE.Group();
432
+ const scaleFactor = 0.8; // Overall scale for the wizard
433
+ const bodyHeight = 1.0 * scaleFactor;
434
+ const bodyRadiusTop = 0.3 * scaleFactor;
435
+ const bodyRadiusBottom = 0.4 * scaleFactor;
436
+ const headRadius = 0.25 * scaleFactor;
437
+ const hatHeight = 0.6 * scaleFactor;
438
+ const hatRadius = 0.35 * scaleFactor;
439
+ const armRadius = 0.1 * scaleFactor;
440
+ const armLength = 0.8 * scaleFactor;
441
+ const staffHandleLength = 1.2 * scaleFactor;
442
+ const staffOrbRadius = 0.15 * scaleFactor;
443
+
444
+ // Body (Capsule-like: Cylinder + two Hemispheres)
445
+ const bodyCylinder = new THREE.Mesh(
446
+ new THREE.CylinderGeometry(bodyRadiusTop, bodyRadiusBottom, bodyHeight, 32), // High segments for smooth body
447
+ new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 })
448
+ );
449
+ bodyCylinder.position.y = bodyHeight / 2;
450
+ wizardGroup.add(bodyCylinder);
451
+
452
+ const bodyTopSphere = new THREE.Mesh(
453
+ new THREE.SphereGeometry(bodyRadiusTop, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2), // Top hemisphere
454
+ new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 })
455
+ );
456
+ bodyTopSphere.position.y = bodyHeight;
457
+ wizardGroup.add(bodyTopSphere);
458
+
459
+ const bodyBottomSphere = new THREE.Mesh(
460
+ new THREE.SphereGeometry(bodyRadiusBottom, 32, 16, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2), // Bottom hemisphere
461
+ new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 })
462
+ );
463
+ bodyBottomSphere.position.y = 0;
464
+ wizardGroup.add(bodyBottomSphere);
465
+
466
+
467
+ // Head (Sphere)
468
+ const headGeometry = new THREE.SphereGeometry(headRadius, 64, 32); // Very high segments for smooth head
469
+ const headMaterial = new THREE.MeshPhongMaterial({ color: 0xffe0bd, specular: 0x222222, shininess: 50 }); // Skin tone
470
+ const head = new THREE.Mesh(headGeometry, headMaterial);
471
+ head.position.y = bodyHeight + headRadius; // On top of body
472
+ wizardGroup.add(head);
473
+
474
+ // Hat (Cone)
475
+ const hatGeometry = new THREE.ConeGeometry(hatRadius, hatHeight, 32); // High segments for smooth hat
476
+ const hatMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, specular: 0x111111, shininess: 70 }); // Dark hat
477
+ const hat = new THREE.Mesh(hatGeometry, hatMaterial);
478
+ hat.position.y = head.position.y + headRadius + hatHeight / 2; // On top of head
479
+ wizardGroup.add(hat);
480
+
481
+ // Arms (Cylinders)
482
+ const armGeometry = new THREE.CylinderGeometry(armRadius, armRadius, armLength, 16);
483
+ const armMaterial = new THREE.MeshPhongMaterial({ color: 0xaaaaaa, specular: 0x333333, shininess: 40 });
484
+
485
+ const leftArm = new THREE.Mesh(armGeometry, armMaterial);
486
+ leftArm.position.set(-bodyRadiusTop - armRadius, bodyHeight * 0.7, 0);
487
+ leftArm.rotation.z = Math.PI / 2; // Point outwards
488
+ wizardGroup.add(leftArm);
489
+
490
+ const rightArm = leftArm.clone();
491
+ rightArm.position.x = bodyRadiusTop + armRadius;
492
+ rightArm.rotation.z = -Math.PI / 2; // Point outwards
493
+ wizardGroup.add(rightArm);
494
+
495
+ // Staff (Cylinder and Sphere)
496
+ const staffHandleGeometry = new THREE.CylinderGeometry(0.005 * gridSize, 0.005 * gridSize, staffHandleLength, 16);
497
+ const staffHandleMaterial = new THREE.MeshPhongMaterial({ color: 0x8b4513, specular: 0x333333, shininess: 40 }); // Brown
498
+ const staffHandle = new THREE.Mesh(staffHandleGeometry, staffHandleMaterial);
499
+ staffHandle.position.set(0.5 * scaleFactor, bodyHeight * 0.5, 0.5 * scaleFactor); // Position relative to wizard
500
+ staffHandle.rotation.x = Math.PI / 4; // Angle the staff
501
+ wizardGroup.add(staffHandle);
502
+
503
+ const staffOrbGeometry = new THREE.SphereGeometry(staffOrbRadius, 32, 16); // High segments for smooth orb
504
+ const staffOrbMaterial = new THREE.MeshPhongMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 0.5, specular: 0xffffff, shininess: 100 }); // Glowing Cyan orb
505
+ const staffOrb = new THREE.Mesh(staffOrbGeometry, staffOrbMaterial);
506
+ staffOrb.position.copy(staffHandle.position);
507
+ staffOrb.position.y += staffHandleLength / 2; // At the top of the staff
508
+ wizardGroup.add(staffOrb);
509
+
510
+ wizardGroup.scale.set(gridSize, gridSize, gridSize); // Scale the entire wizard to fit the grid unit
511
+
512
+ return wizardGroup; // Return the group containing all parts
513
+ }
514
+
515
+ /**
516
+ * Creates a 3D monster assembled from primitive shapes, made less blocky.
517
+ * @param {number} x - The x-coordinate on the dungeon grid.
518
+ * @param {number} y - The y-coordinate on the dungeon grid.
519
+ * @param {number} color - The color of the monster.
520
+ */
521
+ function createMonster(x, y, color = 0x800080) { // Default purple monster
522
+ const monsterGroup = new THREE.Group();
523
+ const scaleFactor = 0.7; // Overall scale for the monster
524
+
525
+ // Main Body (Sphere)
526
+ const bodyGeometry = new THREE.SphereGeometry(0.5 * scaleFactor, 32, 16); // Smoother sphere
527
+ const bodyMaterial = new THREE.MeshPhongMaterial({ color: color, specular: 0x444444, shininess: 50 });
528
+ const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
529
+ body.position.y = 0.5 * scaleFactor; // Center on the ground
530
+ monsterGroup.add(body);
531
+
532
+ // Eyes (small spheres)
533
+ const eyeGeometry = new THREE.SphereGeometry(0.1 * scaleFactor, 16, 8);
534
+ const eyeMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.8 });
535
+ const pupilMaterial = new THREE.MeshPhongMaterial({ color: 0x000000 });
536
+
537
+ const eye1 = new THREE.Mesh(eyeGeometry, eyeMaterial);
538
+ eye1.position.set(-0.2 * scaleFactor, 0.6 * scaleFactor, 0.4 * scaleFactor);
539
+ monsterGroup.add(eye1);
540
+ const pupil1 = new THREE.Mesh(eyeGeometry.clone(), pupilMaterial);
541
+ pupil1.scale.set(0.5, 0.5, 0.5);
542
+ pupil1.position.set(-0.2 * scaleFactor, 0.6 * scaleFactor, 0.45 * scaleFactor);
543
+ monsterGroup.add(pupil1);
544
+
545
+ const eye2 = eye1.clone();
546
+ eye2.position.x = 0.2 * scaleFactor;
547
+ monsterGroup.add(eye2);
548
+ const pupil2 = pupil1.clone();
549
+ pupil2.position.x = 0.2 * scaleFactor;
550
+ monsterGroup.add(pupil2);
551
+
552
+ // Spikes (Cones)
553
+ const spikeGeometry = new THREE.ConeGeometry(0.15 * scaleFactor, 0.4 * scaleFactor, 8);
554
+ const spikeMaterial = new THREE.MeshPhongMaterial({ color: 0x555555, specular: 0x222222, shininess: 30 });
555
+
556
+ const spike1 = new THREE.Mesh(spikeGeometry, spikeMaterial);
557
+ spike1.position.set(0, 0.9 * scaleFactor, 0);
558
+ monsterGroup.add(spike1);
559
+
560
+ const spike2 = spike1.clone();
561
+ spike2.rotation.y = Math.PI / 2;
562
+ spike2.position.set(0.5 * scaleFactor, 0.5 * scaleFactor, 0);
563
+ monsterGroup.add(spike2);
564
+
565
+ const spike3 = spike1.clone();
566
+ spike3.rotation.y = -Math.PI / 2;
567
+ spike3.position.set(-0.5 * scaleFactor, 0.5 * scaleFactor, 0);
568
+ monsterGroup.add(spike3);
569
+
570
+ monsterGroup.scale.set(gridSize, gridSize, gridSize); // Scale the entire monster to fit the grid unit
571
+
572
+ // Position the monster mesh in the 3D world
573
+ monsterGroup.position.set(
574
+ (x - mapWidth / 2 + 0.5) * gridSize,
575
+ 0, // Base of monster at y=0
576
+ (y - mapHeight / 2 + 0.5) * gridSize
577
+ );
578
+ scene.add(monsterGroup); // Add to the scene
579
+
580
+ // Create a monster object to hold its mesh, stats
581
+ const monster = {
582
+ mesh: monsterGroup,
583
+ health: 50,
584
+ maxHealth: 50,
585
+ };
586
+ monsters.push(monster); // Add to the monsters array
587
+ }
588
+
589
+ /**
590
+ * Updates the 3D position of a player/monster mesh based on its game world coordinates.
591
+ * @param {THREE.Group} objectMesh - The Three.js Group representing the object.
592
+ * @param {number} worldX - The x-coordinate in the 3D world.
593
+ * @param {number} worldZ - The z-coordinate in the 3D world.
594
+ */
595
+ function updateObjectPosition(objectMesh, worldX, worldZ) {
596
+ objectMesh.position.x = worldX;
597
+ objectMesh.position.z = worldZ;
598
+ }
599
+
600
+ /**
601
+ * Checks for collision with walls for a given position.
602
+ * Returns true if the position is inside a wall, false otherwise.
603
+ * @param {number} x - World X coordinate.
604
+ * @param {number} z - World Z coordinate.
605
+ * @returns {boolean}
606
+ */
607
+ function isCollidingWithWall(x, z) {
608
+ // Convert world coordinates to grid coordinates
609
+ const gridX = Math.floor(x / gridSize + mapWidth / 2);
610
+ const gridY = Math.floor(z / gridSize + mapHeight / 2); // Z maps to Y in our 2D map
611
+
612
+ // Check boundaries
613
+ if (gridX < 0 || gridX >= mapWidth || gridY < 0 || gridY >= mapHeight) {
614
+ return true; // Out of bounds is a collision
615
+ }
616
+ // Check if it's a wall tile
617
+ return dungeonMap[gridY][gridX] === 1;
618
+ }
619
+
620
+ /**
621
+ * Updates player position with collision detection and sliding.
622
+ * @param {object} playerStat - The player's statistics object (playerStats.left or playerStats.right).
623
+ * @param {THREE.Group} playerMesh - The player's Three.js mesh.
624
+ * @param {number} moveX - Desired movement in X direction.
625
+ * @param {number} moveZ - Desired movement in Z direction.
626
+ */
627
+ function updatePlayerMovement(playerStat, playerMesh, moveX, moveZ) {
628
+ let newX = playerStat.position.x + moveX;
629
+ let newZ = playerStat.position.z + moveZ; // Use .z for world Z
630
+
631
+ let collidedX = isCollidingWithWall(newX, playerStat.position.z);
632
+ let collidedZ = isCollidingWithWall(playerStat.position.x, newZ);
633
+
634
+ if (!collidedX) {
635
+ playerStat.position.x = newX;
636
+ }
637
+ if (!collidedZ) {
638
+ playerStat.position.z = newZ;
639
+ }
640
+
641
+ updateObjectPosition(playerMesh, playerStat.position.x, playerStat.position.z);
642
+ }
643
+
644
+ /**
645
+ * Creates a temporary visual spell effect at a given position.
646
+ * The effect expands and fades out over time.
647
+ * @param {THREE.Vector3} position - The world position where the spell effect should appear.
648
+ * @param {number} color - The color of the spell effect.
649
+ */
650
+ function createSpellEffect(position, color) {
651
+ const geometry = new THREE.SphereGeometry(spellProjectileRadius * 2, 16, 16); // Sphere for the effect
652
+ const material = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.8 });
653
+ const spellEffect = new THREE.Mesh(geometry, material);
654
+ spellEffect.position.copy(position); // Copy player's position
655
+ spellEffect.position.y = gridSize * 0.5; // Slightly above ground
656
+ scene.add(spellEffect);
657
+
658
+ // Animation variables for scaling and fading
659
+ let scale = 0.1;
660
+ const fadeSpeed = 0.05;
661
+
662
+ /**
663
+ * Animates the spell effect (scaling up and fading out).
664
+ */
665
+ function animateEffect() {
666
+ if (spellEffect.material.opacity > 0) {
667
+ spellEffect.material.opacity -= fadeSpeed; // Reduce opacity
668
+ scale += 0.05; // Increase scale
669
+ spellEffect.scale.set(scale, scale, scale); // Apply new scale
670
+ requestAnimationFrame(animateEffect); // Continue animation
671
+ } else {
672
+ scene.remove(spellEffect); // Remove from scene when fully faded
673
+ spellEffect.geometry.dispose(); // Clean up geometry
674
+ spellEffect.material.dispose(); // Clean up material
675
+ }
676
+ }
677
+ animateEffect(); // Start the effect animation
678
+ }
679
+
680
+ /**
681
+ * Class to represent a spell projectile.
682
+ */
683
+ class Projectile extends THREE.Mesh {
684
+ constructor(originPlayer, targetPosition, damage) {
685
+ const geometry = new THREE.SphereGeometry(spellProjectileRadius, 16, 16);
686
+ const material = new THREE.MeshPhongMaterial({ color: 0xffd700, emissive: 0xffd700, emissiveIntensity: 0.5 });
687
+ super(geometry, material);
688
+
689
+ this.originPlayer = originPlayer;
690
+ this.damage = damage;
691
+ this.life = spellProjectileLife;
692
+
693
+ // Set initial position near the origin player, slightly above ground
694
+ this.position.copy(originPlayer.position);
695
+ this.position.y = gridSize * 0.5;
696
+
697
+ // Calculate velocity towards the target position
698
+ this.velocity = new THREE.Vector3();
699
+ this.velocity.subVectors(targetPosition, this.position).normalize().multiplyScalar(spellProjectileSpeed);
700
+
701
+ scene.add(this); // Add projectile to the scene
702
+ }
703
+
704
+ update() {
705
+ this.position.add(this.velocity);
706
+ this.life--;
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Updates the health, mana, and score displayed in the UI.
712
+ */
713
+ function updateUI() {
714
+ // Update Left Player UI
715
+ uiElements.scoreLeft.textContent = `P1 Score: ${playerStats.left.score}`;
716
+ uiElements.healthBarLeft.style.width = `${(playerStats.left.health / playerStats.left.maxHealth) * 100}%`;
717
+ uiElements.healthBarLeft.style.backgroundColor = playerStats.left.health > playerStats.left.maxHealth * 0.6 ? '#28a745' : (playerStats.left.health > playerStats.left.maxHealth * 0.3 ? '#ffc107' : '#dc3545');
718
+ uiElements.manaBarLeft.style.width = `${(playerStats.left.mana / playerStats.left.maxMana) * 100}%`;
719
+
720
+ // Update Right Player UI
721
+ uiElements.scoreRight.textContent = `P2 Score: ${playerStats.right.score}`;
722
+ uiElements.healthBarRight.style.width = `${(playerStats.right.health / playerStats.right.maxHealth) * 100}%`;
723
+ uiElements.healthBarRight.style.backgroundColor = playerStats.right.health > playerStats.right.maxHealth * 0.6 ? '#28a745' : (playerStats.right.health > playerStats.right.maxHealth * 0.3 ? '#ffc107' : '#dc3545');
724
+ uiElements.manaBarRight.style.width = `${(playerStats.right.mana / playerStats.right.maxMana) * 100}%`;
725
+ }
726
+
727
+ /**
728
+ * Resets the game state: restores player health/mana, resets scores,
729
+ * moves players to starting positions, and respawns monsters.
730
+ */
731
+ function resetGame() {
732
+ // Reset Left Player stats and position
733
+ playerStats.left.health = playerStats.left.maxHealth;
734
+ playerStats.left.mana = playerStats.left.maxMana;
735
+ playerStats.left.score = 0;
736
+ playerStats.left.position.set(-5 * gridSize, 0, -5 * gridSize);
737
+ updateObjectPosition(player1, playerStats.left.position.x, playerStats.left.position.z);
738
+
739
+ // Reset Right Player stats and position
740
+ playerStats.right.health = playerStats.right.maxHealth;
741
+ playerStats.right.mana = playerStats.right.maxMana;
742
+ playerStats.right.score = 0;
743
+ playerStats.right.position.set(5 * gridSize, 0, 5 * gridSize);
744
+ updateObjectPosition(player2, playerStats.right.position.x, playerStats.right.position.z);
745
+
746
+ // Remove all existing monsters from the scene and array
747
+ monsters.forEach(monster => scene.remove(monster.mesh));
748
+ monsters = [];
749
+ spawnMonsters(3); // Spawn a new set of monsters
750
+
751
+ // Remove all active projectiles
752
+ while (activeProjectiles.length > 0) {
753
+ const projectile = activeProjectiles.pop();
754
+ scene.remove(projectile);
755
+ projectile.geometry.dispose();
756
+ projectile.material.dispose();
757
+ }
758
+
759
+ updateUI(); // Update UI to reflect reset stats
760
+ console.log("Game reset!");
761
+ }
762
+
763
+ /**
764
+ * Spawns a specified number of monsters at random valid locations in the dungeon.
765
+ * @param {number} count - The number of monsters to spawn.
766
+ */
767
+ function spawnMonsters(count) {
768
+ for (let i = 0; i < count; i++) {
769
+ let spawned = false;
770
+ while (!spawned) {
771
+ // Generate random grid coordinates, avoiding the border walls
772
+ const randomGridX = Math.floor(Math.random() * (mapWidth - 2)) + 1;
773
+ const randomGridY = Math.floor(Math.random() * (mapHeight - 2)) + 1;
774
+
775
+ // Convert to world coordinates for spawning check
776
+ const worldX = (randomGridX - mapWidth / 2 + 0.5) * gridSize;
777
+ const worldZ = (randomGridY - mapHeight / 2 + 0.5) * gridSize;
778
+
779
+ // Ensure the spawn location is a floor tile and not too close to players
780
+ if (dungeonMap[randomGridY][randomGridX] === 0 &&
781
+ new THREE.Vector3(worldX, 0, worldZ).distanceTo(player1.position) > gridSize * 3 &&
782
+ new THREE.Vector3(worldX, 0, worldZ).distanceTo(player2.position) > gridSize * 3) {
783
+ createMonster(randomGridX, randomGridY); // Create the monster
784
+ spawned = true; // Monster successfully spawned
785
+ }
786
+ }
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Initializes the Three.js scene, camera, renderer, and game elements.
792
+ */
793
+ function init() {
794
+ // Scene Setup: The container for all 3D objects, lights, and cameras
795
+ scene = new THREE.Scene();
796
+ scene.background = new THREE.Color(0x333333); // Dark grey background for the 3D scene
797
+
798
+ // Camera Setup: Orthographic camera for a top-down semi-isometric view
799
+ const aspectRatio = window.innerWidth / window.innerHeight;
800
+ const frustumSize = Math.max(mapWidth, mapHeight) * gridSize * 0.7; // Adjusted frustum size for dungeon
801
+ camera = new THREE.OrthographicCamera(
802
+ frustumSize * aspectRatio / - 2,
803
+ frustumSize * aspectRatio / 2,
804
+ frustumSize / 2,
805
+ frustumSize / - 2,
806
+ 1, 1000
807
+ );
808
+ // Position camera for a top-down semi-isometric view
809
+ camera.position.set(0, 40, 40); // Higher Y, and equal X/Z for a balanced angle
810
+ camera.lookAt(0, 0, 0); // Make the camera look directly at the origin
811
+
812
+ // Renderer Setup: Renders the 3D scene onto a 2D canvas
813
+ renderer = new THREE.WebGLRenderer({ antialias: true });
814
+ renderer.setSize(window.innerWidth, window.innerHeight);
815
+ renderer.setPixelRatio(window.devicePixelRatio);
816
+ document.getElementById('game-container').appendChild(renderer.domElement);
817
+
818
+ // Lighting: Essential for seeing 3D objects
819
+ const ambientLight = new THREE.AmbientLight(0x404040, 2); // Soft ambient light, affects all objects evenly
820
+ scene.add(ambientLight);
821
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // Directional light, like sunlight
822
+ directionalLight.position.set(0, 100, 0); // Positioned above the scene
823
+ scene.add(directionalLight);
824
+
825
+ // Generate the dungeon environment
826
+ generateDungeon();
827
+
828
+ // Player 1 Character: Wizard
829
+ player1 = createWizard(0xff4500); // Red-orange color
830
+ player1.position.copy(playerStats.left.position);
831
+ scene.add(player1);
832
+
833
+ // Player 2 Character: Wizard
834
+ player2 = createWizard(0x00aaff); // Blue color
835
+ player2.position.copy(playerStats.right.position);
836
+ scene.add(player2);
837
+
838
+ // UI Elements Initialization and Event Listeners
839
+ uiElements.resetButton.addEventListener('click', resetGame);
840
+
841
+ // Event Listeners for Keyboard Input
842
+ window.addEventListener('keydown', onKeyDown);
843
+ window.addEventListener('keyup', onKeyUp);
844
+ window.addEventListener('resize', onWindowResize);
845
+
846
+ // Spawn initial monsters
847
+ spawnMonsters(3);
848
+
849
+ // Update UI for the first time
850
+ updateUI();
851
+
852
+ // Start the animation loop
853
+ animate();
854
+ }
855
+
856
+ /**
857
+ * The main animation loop for Three.js.
858
+ * It continuously renders the scene and calls the game logic.
859
+ */
860
+ function animate() {
861
+ requestAnimationFrame(animate);
862
+
863
+ // Player 1 Movement
864
+ let p1MoveX = 0, p1MoveZ = 0;
865
+ if (keys.w) p1MoveZ -= playerMoveSpeed; // Up
866
+ if (keys.s) p1MoveZ += playerMoveSpeed; // Down
867
+ if (keys.a) p1MoveX -= playerMoveSpeed; // Left
868
+ if (keys.d) p1MoveX += playerMoveSpeed; // Right
869
+
870
+ // Normalize diagonal movement speed
871
+ if (p1MoveX !== 0 && p1MoveZ !== 0) {
872
+ const length = Math.sqrt(p1MoveX * p1MoveX + p1MoveZ * p1MoveZ);
873
+ p1MoveX /= length;
874
+ p1MoveZ /= length;
875
+ p1MoveX *= playerMoveSpeed;
876
+ p1MoveZ *= playerMoveSpeed;
877
+ }
878
+ updatePlayerMovement(playerStats.left, player1, p1MoveX, p1MoveZ);
879
+
880
+
881
+ // Player 2 Movement
882
+ let p2MoveX = 0, p2MoveZ = 0;
883
+ if (keys.i) p2MoveZ -= playerMoveSpeed; // Up
884
+ if (keys.k) p2MoveZ += playerMoveSpeed; // Down
885
+ if (keys.j) p2MoveX -= playerMoveSpeed; // Left
886
+ if (keys.l) p2MoveX += playerMoveSpeed; // Right
887
+
888
+ // Normalize diagonal movement speed
889
+ if (p2MoveX !== 0 && p2MoveZ !== 0) {
890
+ const length = Math.sqrt(p2MoveX * p2MoveX + p2MoveZ * p2MoveZ);
891
+ p2MoveX /= length;
892
+ p2MoveZ /= length;
893
+ p2MoveX *= playerMoveSpeed;
894
+ p2MoveZ *= playerMoveSpeed;
895
+ }
896
+ updatePlayerMovement(playerStats.right, player2, p2MoveX, p2MoveZ);
897
+
898
+ // Auto-rotate players to face each other
899
+ player1.lookAt(player2.position.x, player1.position.y, player2.position.z);
900
+ player2.lookAt(player1.position.x, player2.position.y, player1.position.z);
901
+
902
+ // Player Actions
903
+ if (keys.p1_action) {
904
+ if (playerStats.left.mana >= 10) {
905
+ playerStats.left.mana -= 10;
906
+ console.log("Left player cast a spell!");
907
+ createSpellEffect(player1.position, 0xffa500); // Visual effect at cast
908
+
909
+ // Find closest monster for projectile target
910
+ let closestMonster = null;
911
+ let minDistance = Infinity;
912
+ monsters.forEach(monster => {
913
+ const dx = monster.mesh.position.x - player1.position.x;
914
+ const dz = monster.mesh.position.z - player1.position.z;
915
+ const distance = Math.sqrt(dx * dx + dz * dz);
916
+ if (distance < minDistance) {
917
+ minDistance = distance;
918
+ closestMonster = monster;
919
+ }
920
+ });
921
+
922
+ if (closestMonster) {
923
+ activeProjectiles.push(new Projectile(player1, closestMonster.mesh.position, spellDamage));
924
+ } else {
925
+ console.log("No monster to target for Left player's spell.");
926
+ }
927
+ } else {
928
+ console.log("Left player out of mana!");
929
+ }
930
+ keys.p1_action = false; // Prevent continuous casting on key hold
931
+ }
932
+
933
+ if (keys.p2_action) {
934
+ if (playerStats.right.mana >= 10) {
935
+ playerStats.right.mana -= 10;
936
+ console.log("Right player cast a spell!");
937
+ createSpellEffect(player2.position, 0x00ff00); // Visual effect at cast
938
+
939
+ // Find closest monster for projectile target
940
+ let closestMonster = null;
941
+ let minDistance = Infinity;
942
+ monsters.forEach(monster => {
943
+ const dx = monster.mesh.position.x - player2.position.x;
944
+ const dz = monster.mesh.position.z - player2.position.z;
945
+ const distance = Math.sqrt(dx * dx + dz * dz);
946
+ if (distance < minDistance) {
947
+ minDistance = distance;
948
+ closestMonster = monster;
949
+ }
950
+ });
951
+
952
+ if (closestMonster) {
953
+ activeProjectiles.push(new Projectile(player2, closestMonster.mesh.position, spellDamage));
954
+ } else {
955
+ console.log("No monster to target for Right player's spell.");
956
+ }
957
+ } else {
958
+ console.log("Right player out of mana!");
959
+ }
960
+ keys.p2_action = false; // Prevent continuous casting on key hold
961
+ }
962
+
963
+ // Update and check collisions for projectiles
964
+ for (let i = activeProjectiles.length - 1; i >= 0; i--) {
965
+ const projectile = activeProjectiles[i];
966
+ projectile.update();
967
+
968
+ let hit = false;
969
+ // Check collision with monsters
970
+ for (let j = monsters.length - 1; j >= 0; j--) {
971
+ const monster = monsters[j];
972
+ if (projectile.position.distanceTo(monster.mesh.position) < (spellProjectileRadius + monster.mesh.scale.x * 0.5 * gridSize)) {
973
+ monster.health -= projectile.damage;
974
+ createSpellEffect(monster.mesh.position, 0xff0000); // Red impact for damage
975
+ if (monster.health <= 0) {
976
+ console.log("Monster defeated!");
977
+ scene.remove(monster.mesh);
978
+ monsters.splice(j, 1);
979
+ // Award score to the player who cast the spell
980
+ if (projectile.originPlayer === player1) playerStats.left.score += 100;
981
+ else if (projectile.originPlayer === player2) playerStats.right.score += 100;
982
+ }
983
+ hit = true;
984
+ break; // Only hit one monster per projectile
985
+ }
986
+ }
987
+
988
+ if (hit || projectile.life <= 0) {
989
+ scene.remove(projectile);
990
+ projectile.geometry.dispose();
991
+ projectile.material.dispose();
992
+ activeProjectiles.splice(i, 1);
993
+ }
994
+ }
995
+
996
+ // Monster AI: Move towards and attack closest player
997
+ monsters.forEach(monster => {
998
+ const distToP1 = monster.mesh.position.distanceTo(player1.position);
999
+ const distToP2 = monster.mesh.position.distanceTo(player2.position);
1000
+
1001
+ let targetPlayerMesh = null;
1002
+ let targetPlayerStat = null;
1003
+
1004
+ if (distToP1 <= distToP2) {
1005
+ targetPlayerMesh = player1;
1006
+ targetPlayerStat = playerStats.left;
1007
+ } else {
1008
+ targetPlayerMesh = player2;
1009
+ targetPlayerStat = playerStats.right;
1010
+ }
1011
+
1012
+ // Monster movement direction
1013
+ let monsterMoveX = 0;
1014
+ let monsterMoveZ = 0;
1015
+
1016
+ if (monster.mesh.position.x < targetPlayerMesh.position.x) {
1017
+ monsterMoveX = monsterMoveSpeed;
1018
+ } else if (monster.mesh.position.x > targetPlayerMesh.position.x) {
1019
+ monsterMoveX = -monsterMoveSpeed;
1020
+ }
1021
+
1022
+ if (monster.mesh.position.z < targetPlayerMesh.position.z) {
1023
+ monsterMoveZ = monsterMoveSpeed;
1024
+ } else if (monster.mesh.position.z > targetPlayerMesh.position.z) {
1025
+ monsterMoveZ = -monsterMoveSpeed;
1026
+ }
1027
+
1028
+ // Normalize diagonal movement for monsters
1029
+ if (monsterMoveX !== 0 && monsterMoveZ !== 0) {
1030
+ const length = Math.sqrt(monsterMoveX * monsterMoveX + monsterMoveZ * monsterMoveZ);
1031
+ monsterMoveX /= length;
1032
+ monsterMoveZ /= length;
1033
+ }
1034
+
1035
+ // Apply monster movement with collision detection
1036
+ let newMonsterX = monster.mesh.position.x + monsterMoveX;
1037
+ let newMonsterZ = monster.mesh.position.z + monsterMoveZ;
1038
+
1039
+ let collidedMonsterX = isCollidingWithWall(newMonsterX, monster.mesh.position.z);
1040
+ let collidedMonsterZ = isCollidingWithWall(monster.mesh.position.x, newMonsterZ);
1041
+
1042
+ if (!collidedMonsterX) {
1043
+ monster.mesh.position.x = newMonsterX;
1044
+ }
1045
+ if (!collidedMonsterZ) {
1046
+ monster.mesh.position.z = newMonsterZ;
1047
+ }
1048
+
1049
+ // Monster attack logic
1050
+ if (monster.mesh.position.distanceTo(targetPlayerMesh.position) < gridSize * 1.2) { // Attack if close to player
1051
+ targetPlayerStat.health = Math.max(0, targetPlayerStat.health - 0.5); // Deal small continuous damage
1052
+ console.log(`Monster attacked ${targetPlayerStat === playerStats.left ? 'Left' : 'Right'} Player!`);
1053
+ }
1054
+ });
1055
+
1056
+ // Player-to-player healing when close
1057
+ const distanceBetweenPlayers = player1.position.distanceTo(player2.position);
1058
+ if (distanceBetweenPlayers < playerHealingThreshold) {
1059
+ // Player 1 heals Player 2
1060
+ playerStats.right.health = Math.min(playerStats.right.maxHealth, playerStats.right.health + healingAmountPerFrame);
1061
+ // Player 2 heals Player 1
1062
+ playerStats.left.health = Math.min(playerStats.left.maxHealth, playerStats.left.health + healingAmountPerFrame);
1063
+ }
1064
+
1065
+ // Check for game over condition
1066
+ if (playerStats.left.health <= 0 && playerStats.right.health <= 0) {
1067
+ console.log("Game Over! Both players incapacitated.");
1068
+ // In a full game, you'd show a game over screen here.
1069
+ }
1070
+
1071
+ updateUI(); // Update UI every frame
1072
+ renderer.render(scene, camera);
1073
+ }
1074
+
1075
+ // Initialize the game when the window loads
1076
+ window.onload = init;
1077
+ </script>
1078
+ </body>
1079
  </html>