awacke1 commited on
Commit
0c1ca25
·
verified ·
1 Parent(s): da18cd8

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +831 -18
index.html CHANGED
@@ -1,19 +1,832 @@
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>The Leaning Tower of Pisa: Castle Builder</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <style>
11
+ /* Apply Inter font and basic reset */
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ margin: 0;
15
+ overflow: hidden; /* Prevent scrollbars due to canvas */
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ justify-content: center;
20
+ min-height: 100vh;
21
+ background-color: #1a202c; /* Dark background */
22
+ color: #e2e8f0; /* Light text color */
23
+ }
24
+
25
+ /* Canvas styling to fill the screen and be responsive */
26
+ canvas {
27
+ display: block;
28
+ width: 100%;
29
+ height: 100%;
30
+ background-color: #2d3748; /* Slightly lighter dark background for canvas */
31
+ border-radius: 1rem; /* Rounded corners */
32
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3); /* Subtle shadow */
33
+ }
34
+
35
+ /* Game UI container */
36
+ #game-container {
37
+ position: relative;
38
+ width: 100vw;
39
+ height: calc(100vh - 80px); /* Adjust height to make space for controls */
40
+ display: grid; /* Use grid for layout */
41
+ grid-template-columns: 1fr 200px; /* Canvas takes remaining space, panel takes 200px */
42
+ gap: 0; /* No gap between canvas and panel */
43
+ margin-bottom: 1rem;
44
+ max-width: 1200px; /* Limit max width for better aesthetics */
45
+ border-radius: 1rem;
46
+ overflow: hidden; /* Ensure rounded corners apply */
47
+ }
48
+
49
+ #gameCanvas {
50
+ grid-column: 1 / 2; /* Canvas in the first column */
51
+ width: 100%;
52
+ height: 100%;
53
+ }
54
+
55
+ /* Score displays */
56
+ .score-display {
57
+ position: absolute;
58
+ font-size: 1.5rem;
59
+ font-weight: bold;
60
+ background-color: rgba(45, 55, 72, 0.8); /* Semi-transparent dark background */
61
+ padding: 0.75rem 1.25rem;
62
+ border-radius: 0.75rem;
63
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
64
+ z-index: 10;
65
+ }
66
+
67
+ #player1-score {
68
+ top: 1rem;
69
+ left: 1rem;
70
+ color: #63b3ed; /* Blue for Player 1 */
71
+ }
72
+
73
+ #player2-score {
74
+ top: 1rem;
75
+ right: 1rem;
76
+ color: #f6ad55; /* Orange for Player 2 */
77
+ }
78
+
79
+ /* Controls Panel */
80
+ #controls-panel {
81
+ grid-column: 2 / 3; /* Panel in the second column */
82
+ width: 100%; /* Fill its grid cell */
83
+ height: 100%;
84
+ padding: 1rem;
85
+ background-color: #2d3748; /* Dark background */
86
+ border-radius: 0 1rem 1rem 0; /* Rounded right corners */
87
+ box-shadow: inset 4px 0 14px rgba(0, 0, 0, 0.3); /* Inner shadow */
88
+ display: flex;
89
+ flex-direction: column;
90
+ align-items: center;
91
+ text-align: center;
92
+ font-size: 0.9rem; /* Small text */
93
+ overflow-y: auto; /* Scroll if content overflows */
94
+ }
95
+
96
+ #controls-panel h3 {
97
+ font-size: 1.2rem;
98
+ font-weight: bold;
99
+ margin-bottom: 1rem;
100
+ color: #e2e8f0;
101
+ }
102
+
103
+ #controls-panel h4 {
104
+ font-size: 1rem;
105
+ font-weight: bold;
106
+ margin-top: 0.75rem;
107
+ margin-bottom: 0.25rem;
108
+ }
109
+
110
+ .player-controls-section {
111
+ margin-bottom: 1rem;
112
+ padding-bottom: 1rem;
113
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
114
+ width: 100%;
115
+ }
116
+
117
+ .player-controls-section:last-child {
118
+ border-bottom: none;
119
+ margin-bottom: 0;
120
+ padding-bottom: 0;
121
+ }
122
+
123
+ .movement-buttons-grid {
124
+ display: grid;
125
+ grid-template-columns: repeat(3, 1fr);
126
+ gap: 0.3rem;
127
+ width: 100px; /* Fixed width for the grid of buttons */
128
+ margin: 0.5rem auto; /* Center the grid */
129
+ }
130
+
131
+ .movement-buttons-grid .grid-cell {
132
+ display: flex;
133
+ justify-content: center;
134
+ align-items: center;
135
+ }
136
+
137
+ .control-btn {
138
+ padding: 0.4rem 0.6rem;
139
+ font-size: 0.75rem; /* Really small text for buttons */
140
+ font-weight: bold;
141
+ background-color: #4a5568; /* Darker gray */
142
+ color: #e2e8f0;
143
+ border: none;
144
+ border-radius: 0.4rem;
145
+ cursor: pointer;
146
+ transition: all 0.1s ease-in-out;
147
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
148
+ width: 100%; /* Make buttons fill grid cell */
149
+ text-align: center;
150
+ line-height: 1; /* Adjust line height for small text */
151
+ }
152
+
153
+ .control-btn:hover {
154
+ background-color: #63b3ed; /* Default blue on hover */
155
+ transform: translateY(-1px);
156
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
157
+ }
158
+
159
+ .control-btn:active {
160
+ transform: translateY(0);
161
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
162
+ }
163
+
164
+ /* Specific hover colors for players */
165
+ .control-btn[data-player="1"]:hover {
166
+ background-color: #63b3ed; /* Blue for P1 */
167
+ }
168
+ .control-btn[data-player="2"]:hover {
169
+ background-color: #f6ad55; /* Orange for P2 */
170
+ }
171
+
172
+ .rules-text {
173
+ font-size: 0.7rem; /* Really small text for rules */
174
+ color: #a0aec0;
175
+ line-height: 1.3;
176
+ }
177
+
178
+ /* Control and Reset button container (bottom) */
179
+ #controls-container {
180
+ display: flex;
181
+ flex-direction: column;
182
+ align-items: center;
183
+ gap: 0.75rem;
184
+ padding: 1rem;
185
+ background-color: #2d3748;
186
+ border-radius: 1rem;
187
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
188
+ width: 90%;
189
+ max-width: 600px;
190
+ margin-top: 1rem; /* Space between canvas and controls */
191
+ }
192
+
193
+ /* Reset button */
194
+ #reset-button {
195
+ padding: 0.75rem 1.5rem;
196
+ font-size: 1.1rem;
197
+ font-weight: bold;
198
+ background: linear-gradient(145deg, #f56565, #e53e3e); /* Red gradient */
199
+ color: white;
200
+ border: none;
201
+ border-radius: 0.75rem;
202
+ cursor: pointer;
203
+ transition: all 0.2s ease-in-out;
204
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
205
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
206
+ }
207
+
208
+ #reset-button:hover {
209
+ background: linear-gradient(145deg, #e53e3e, #c53030);
210
+ transform: translateY(-2px);
211
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
212
+ }
213
+
214
+ #reset-button:active {
215
+ transform: translateY(0);
216
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
217
+ }
218
+
219
+ /* Game Over message */
220
+ #game-over-message {
221
+ position: absolute;
222
+ top: 50%;
223
+ left: 50%;
224
+ transform: translate(-50%, -50%);
225
+ background-color: rgba(0, 0, 0, 0.8);
226
+ color: white;
227
+ padding: 2rem;
228
+ border-radius: 1rem;
229
+ text-align: center;
230
+ font-size: 2.5rem;
231
+ font-weight: bold;
232
+ display: none; /* Hidden by default */
233
+ z-index: 20;
234
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5);
235
+ animation: fadeIn 0.5s ease-out;
236
+ }
237
+
238
+ @keyframes fadeIn {
239
+ from { opacity: 0; transform: translate(-50%, -60%); }
240
+ to { opacity: 1; transform: translate(-50%, -50%); }
241
+ }
242
+
243
+ /* Responsive adjustments */
244
+ @media (max-width: 768px) {
245
+ #game-container {
246
+ grid-template-columns: 1fr; /* Stack vertically on small screens */
247
+ grid-template-rows: 1fr auto; /* Canvas takes space, panel takes auto height */
248
+ height: calc(100vh - 160px); /* Adjust height for controls at bottom */
249
+ max-width: 100vw; /* Full width */
250
+ border-radius: 0; /* No rounded corners for full screen */
251
+ }
252
+ #gameCanvas {
253
+ grid-row: 1 / 2;
254
+ border-radius: 0;
255
+ }
256
+ #controls-panel {
257
+ grid-row: 2 / 3;
258
+ width: 100%; /* Full width at bottom */
259
+ height: auto; /* Auto height */
260
+ border-radius: 1rem 1rem 0 0; /* Rounded top corners */
261
+ box-shadow: 0 -4px 14px rgba(0, 0, 0, 0.3); /* Shadow from bottom */
262
+ }
263
+
264
+ .score-display {
265
+ font-size: 1.2rem;
266
+ padding: 0.5rem 1rem;
267
+ }
268
+
269
+ #game-over-message {
270
+ font-size: 1.8rem;
271
+ padding: 1.5rem;
272
+ }
273
+
274
+ #reset-button {
275
+ font-size: 1rem;
276
+ padding: 0.6rem 1.2rem;
277
+ }
278
+ }
279
+ </style>
280
+ </head>
281
+ <body>
282
+ <div id="game-container">
283
+ <div id="player1-score" class="score-display">P1: 0</div>
284
+ <div id="player2-score" class="score-display">P2: 0</div>
285
+ <canvas id="gameCanvas"></canvas>
286
+
287
+ <div id="controls-panel">
288
+ <h3>Controls & Rules</h3>
289
+
290
+ <div class="player-controls-section">
291
+ <h4 class="text-blue-300">Player 1 (Blue)</h4>
292
+ <p class="text-gray-300 text-xs">Keyboard: WASD to move, E to Release Part</p>
293
+ <div class="movement-buttons-grid">
294
+ <div class="grid-cell"></div>
295
+ <div class="grid-cell"><button class="control-btn" data-player="1" data-action="move-forward">W</button></div>
296
+ <div class="grid-cell"></div>
297
+ <div class="grid-cell"><button class="control-btn" data-player="1" data-action="move-left">A</button></div>
298
+ <div class="grid-cell"><button class="control-btn" data-player="1" data-action="move-backward">S</button></div>
299
+ <div class="grid-cell"><button class="control-btn" data-player="1" data-action="move-right">D</button></div>
300
+ </div>
301
+ <button class="control-btn w-3/4 mx-auto block mt-2" data-player="1" data-action="release">Release Part (E)</button>
302
+ </div>
303
+
304
+ <div class="player-controls-section">
305
+ <h4 class="text-orange-300">Player 2 (Orange)</h4>
306
+ <p class="text-gray-300 text-xs">Keyboard: IJKL to move, U to Release Part</p>
307
+ <div class="movement-buttons-grid">
308
+ <div class="grid-cell"></div>
309
+ <div class="grid-cell"><button class="control-btn" data-player="2" data-action="move-forward">I</button></div>
310
+ <div class="grid-cell"></div>
311
+ <div class="grid-cell"><button class="control-btn" data-player="2" data-action="move-left">J</button></div>
312
+ <div class="grid-cell"><button class="control-btn" data-player="2" data-action="move-backward">K</button></div>
313
+ <div class="grid-cell"><button class="control-btn" data-player="2" data-action="move-right">L</button></div>
314
+ </div>
315
+ <button class="control-btn w-3/4 mx-auto block mt-2" data-player="2" data-action="release">Release Part (U)</button>
316
+ </div>
317
+
318
+ <div class="flex-grow"></div> <div class="w-full">
319
+ <h4 class="text-white">Game Rules</h4>
320
+ <p class="rules-text">
321
+ Control your character to guide falling castle parts.
322
+ <br>Release parts to build your tower.
323
+ <br>Build the tallest stable castle.
324
+ <br>If any castle collapses, game over!
325
+ <br>Players take turns building.
326
+ </p>
327
+ </div>
328
+ </div>
329
+
330
+ <div id="game-over-message">Game Over! A Castle Toppled!</div>
331
+ </div>
332
+
333
+ <div id="controls-container">
334
+ <button id="reset-button">Reset Game</button>
335
+ </div>
336
+
337
+ <script>
338
+ // Global variables for Three.js and Cannon.js
339
+ let scene, camera, renderer;
340
+ let world; // Cannon.js physics world
341
+
342
+ // Game state variables
343
+ let blocks = []; // Array to hold all placed blocks (Three.js mesh and Cannon.js body)
344
+ let currentFallingBlock = null; // The block currently falling and being controlled by the player
345
+ let playerCharacters = {}; // Stores player characters (Three.js mesh and Cannon.js body)
346
+ let playerScores = {
347
+ player1: 0,
348
+ player2: 0
349
+ };
350
+ let currentPlayer = 1; // Start with Player 1
351
+ let isGameOver = false;
352
+
353
+ // Block properties
354
+ const BLOCK_SPAWN_HEIGHT = 15; // Y-coordinate where blocks appear
355
+ const FALL_THRESHOLD = -5; // If a block falls below this Y-coordinate, game over
356
+
357
+ // Player properties
358
+ const PLAYER_MOVE_SPEED = 0.15; // Character movement speed
359
+ const PLAYER_SPAWN_OFFSET_X = 8; // How far players spawn from the center
360
+ const CASTLE_BUILD_OFFSET_X = 5; // How far building areas are from the center
361
+
362
+ // HTML elements
363
+ const player1ScoreElement = document.getElementById('player1-score');
364
+ const player2ScoreElement = document.getElementById('player2-score');
365
+ const resetButton = document.getElementById('reset-button');
366
+ const gameOverMessage = document.getElementById('game-over-message');
367
+ const canvas = document.getElementById('gameCanvas');
368
+
369
+ // Keyboard state
370
+ let keysPressed = {};
371
+
372
+ // Castle Part Definitions
373
+ const castlePartTypes = [
374
+ {
375
+ name: "Wall",
376
+ color: 0x9e9e9e, // Grey
377
+ geometry: new THREE.BoxGeometry(3, 1.5, 0.8),
378
+ shape: new CANNON.Box(new CANNON.Vec3(1.5, 0.75, 0.4)),
379
+ mass: 2
380
+ },
381
+ {
382
+ name: "Tower Base",
383
+ color: 0x757575, // Darker Grey
384
+ geometry: new THREE.CylinderGeometry(1.2, 1.2, 2.5, 16),
385
+ shape: new CANNON.Cylinder(1.2, 1.2, 2.5, 16),
386
+ mass: 3
387
+ },
388
+ {
389
+ name: "Catapult Base",
390
+ color: 0x5d4037, // Brown
391
+ geometry: new THREE.BoxGeometry(2.5, 1.0, 2.5),
392
+ shape: new CANNON.Box(new CANNON.Vec3(1.25, 0.5, 1.25)),
393
+ mass: 2.5
394
+ }
395
+ ];
396
+ let currentPartIndex = 0; // To cycle through castle parts
397
+
398
+ // --- Initialization ---
399
+ function init() {
400
+ // Scene setup
401
+ scene = new THREE.Scene();
402
+ scene.background = new THREE.Color(0x333333); // Dark grey background for the 3D scene
403
+
404
+ // Camera setup (PerspectiveCamera for a more dynamic 3D feel)
405
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
406
+ camera.position.set(0, 20, 20); // Position above and slightly in front
407
+ camera.lookAt(0, 0, 0); // Look at the origin
408
+
409
+ // Renderer setup
410
+ renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
411
+ renderer.setSize(canvas.clientWidth, canvas.clientHeight);
412
+ renderer.setPixelRatio(window.devicePixelRatio);
413
+ renderer.shadowMap.enabled = true; // Enable shadows
414
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Soft shadows
415
+
416
+ // Lighting
417
+ const ambientLight = new THREE.AmbientLight(0x404040, 1.5); // Soft white light
418
+ scene.add(ambientLight);
419
+
420
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
421
+ directionalLight.position.set(10, 30, 10);
422
+ directionalLight.castShadow = true; // Enable shadow casting for the light
423
+ // Configure shadow properties for better quality
424
+ directionalLight.shadow.mapSize.width = 2048;
425
+ directionalLight.shadow.mapSize.height = 2048;
426
+ directionalLight.shadow.camera.near = 0.5;
427
+ directionalLight.shadow.camera.far = 50;
428
+ directionalLight.shadow.camera.left = -20;
429
+ directionalLight.shadow.camera.right = 20;
430
+ directionalLight.shadow.camera.top = 20;
431
+ directionalLight.shadow.camera.bottom = -20;
432
+ scene.add(directionalLight);
433
+
434
+ // Cannon.js physics world setup
435
+ world = new CANNON.World();
436
+ world.gravity.set(0, -9.82, 0); // Set gravity (m/s^2)
437
+ world.broadphase = new CANNON.SAPBroadphase(world); // Improve collision detection performance
438
+ world.solver.iterations = 10; // Increase solver iterations for better stability
439
+
440
+ // Create the ground/base platforms for each castle
441
+ createGround();
442
+
443
+ // Create player characters (static)
444
+ playerCharacters.player1 = createPrimitiveCharacter(0x63b3ed, -PLAYER_SPAWN_OFFSET_X); // Blue, left side
445
+ playerCharacters.player2 = createPrimitiveCharacter(0xf6ad55, PLAYER_SPAWN_OFFSET_X); // Orange, right side
446
+ scene.add(playerCharacters.player1.mesh);
447
+ world.addBody(playerCharacters.player1.body);
448
+ scene.add(playerCharacters.player2.mesh);
449
+ world.addBody(playerCharacters.player2.body);
450
+
451
+ // Event Listeners
452
+ window.addEventListener('resize', onWindowResize);
453
+ document.addEventListener('keydown', onKeyDown);
454
+ document.addEventListener('keyup', onKeyUp);
455
+ resetButton.addEventListener('click', resetGame);
456
+
457
+ // Add event listeners for control buttons
458
+ document.querySelectorAll('.control-btn').forEach(button => {
459
+ button.addEventListener('click', (event) => {
460
+ const playerNum = parseInt(event.target.dataset.player);
461
+ const action = event.target.dataset.action;
462
+
463
+ if (action === 'release') {
464
+ // Only allow release if it's the current player's turn to influence
465
+ if (currentPlayer === playerNum) {
466
+ handleReleaseAction();
467
+ }
468
+ } else {
469
+ // For movement buttons, apply a temporary force
470
+ applyPlayerMovementForce(playerNum, action);
471
+ }
472
+ });
473
+ });
474
+
475
+ // Start the first block
476
+ newBlock();
477
+
478
+ // Start the animation loop
479
+ animate();
480
+ }
481
+
482
+ // --- Ground Creation ---
483
+ function createGround() {
484
+ // Main arena ground
485
+ const arenaGeometry = new THREE.BoxGeometry(30, 1, 20);
486
+ const arenaMaterial = new THREE.MeshStandardMaterial({ color: 0x4a5568, roughness: 0.8, metalness: 0.1 });
487
+ const arenaMesh = new THREE.Mesh(arenaGeometry, arenaMaterial);
488
+ arenaMesh.position.y = -0.5;
489
+ arenaMesh.receiveShadow = true;
490
+ scene.add(arenaMesh);
491
+
492
+ const arenaShape = new CANNON.Box(new CANNON.Vec3(15, 0.5, 10));
493
+ const arenaBody = new CANNON.Body({ mass: 0, shape: arenaShape, material: new CANNON.Material('groundMaterial') });
494
+ arenaBody.position.copy(arenaMesh.position);
495
+ world.addBody(arenaBody);
496
+
497
+ // Player 1 building platform (blue)
498
+ const p1PlatformGeometry = new THREE.BoxGeometry(6, 0.2, 6);
499
+ const p1PlatformMaterial = new THREE.MeshStandardMaterial({ color: 0x3182ce, roughness: 0.5, metalness: 0.1 });
500
+ const p1PlatformMesh = new THREE.Mesh(p1PlatformGeometry, p1PlatformMaterial);
501
+ p1PlatformMesh.position.set(-CASTLE_BUILD_OFFSET_X, 0.05, 0); // Slightly above main ground
502
+ p1PlatformMesh.receiveShadow = true;
503
+ scene.add(p1PlatformMesh);
504
+
505
+ const p1PlatformShape = new CANNON.Box(new CANNON.Vec3(3, 0.1, 3));
506
+ const p1PlatformBody = new CANNON.Body({ mass: 0, shape: p1PlatformShape, material: new CANNON.Material('groundMaterial') });
507
+ p1PlatformBody.position.copy(p1PlatformMesh.position);
508
+ world.addBody(p1PlatformBody);
509
+
510
+ // Player 2 building platform (orange)
511
+ const p2PlatformGeometry = new THREE.BoxGeometry(6, 0.2, 6);
512
+ const p2PlatformMaterial = new THREE.MeshStandardMaterial({ color: 0xea8a00, roughness: 0.5, metalness: 0.1 });
513
+ const p2PlatformMesh = new THREE.Mesh(p2PlatformGeometry, p2PlatformMaterial);
514
+ p2PlatformMesh.position.set(CASTLE_BUILD_OFFSET_X, 0.05, 0); // Slightly above main ground
515
+ p2PlatformMesh.receiveShadow = true;
516
+ scene.add(p2PlatformMesh);
517
+
518
+ const p2PlatformShape = new CANNON.Box(new CANNON.Vec3(3, 0.1, 3));
519
+ const p2PlatformBody = new CANNON.Body({ mass: 0, shape: p2PlatformShape, material: new CANNON.Material('groundMaterial') });
520
+ p2PlatformBody.position.copy(p2PlatformMesh.position);
521
+ world.addBody(p2PlatformBody);
522
+ }
523
+
524
+ // --- Primitive Character Creation (Static) ---
525
+ function createPrimitiveCharacter(color, initialX) {
526
+ const characterGroup = new THREE.Group();
527
+ const scaleFactor = 1.0;
528
+
529
+ // Body: A central box
530
+ const bodyGeometry = new THREE.BoxGeometry(1.0 * scaleFactor, 1.8 * scaleFactor, 1.0 * scaleFactor);
531
+ const bodyMaterial = new THREE.MeshStandardMaterial({ color: color, roughness: 0.7, metalness: 0.2 });
532
+ const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
533
+ bodyMesh.position.y = 0.9 * scaleFactor;
534
+ characterGroup.add(bodyMesh);
535
+
536
+ // Head: A sphere on top of the body
537
+ const headGeometry = new THREE.SphereGeometry(0.5 * scaleFactor, 16, 16);
538
+ const headMaterial = new THREE.MeshStandardMaterial({ color: color, roughness: 0.7, metalness: 0.2 });
539
+ const headMesh = new THREE.Mesh(headGeometry, headMaterial);
540
+ headMesh.position.y = 1.8 * scaleFactor;
541
+ characterGroup.add(headMesh);
542
+
543
+ // Arms (purely visual, no physics interaction)
544
+ const armGeometry = new THREE.CylinderGeometry(0.2 * scaleFactor, 0.2 * scaleFactor, 1.0 * scaleFactor);
545
+ const armMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.7, metalness: 0.2 });
546
+
547
+ const rightArm = new THREE.Mesh(armGeometry, armMaterial);
548
+ rightArm.position.set(-0.6 * scaleFactor, 1.3 * scaleFactor, 0);
549
+ rightArm.rotation.z = Math.PI / 2;
550
+ characterGroup.add(rightArm);
551
+ characterGroup.rightArm = rightArm; // Store reference for animation
552
+
553
+ const leftArm = new THREE.Mesh(armGeometry, armMaterial);
554
+ leftArm.position.set(0.6 * scaleFactor, 1.3 * scaleFactor, 0);
555
+ leftArm.rotation.z = -Math.PI / 2;
556
+ characterGroup.add(leftArm);
557
+ characterGroup.leftArm = leftArm; // Store reference for animation
558
+
559
+ // Cannon.js body for the character (STATIC)
560
+ const characterShape = new CANNON.Box(new CANNON.Vec3(0.5 * scaleFactor, 0.9 * scaleFactor, 0.5 * scaleFactor));
561
+ const characterBody = new CANNON.Body({
562
+ mass: 0, // Mass 0 makes it static
563
+ type: CANNON.Body.STATIC, // Explicitly set as static
564
+ shape: characterShape,
565
+ material: new CANNON.Material('playerMaterial')
566
+ });
567
+ characterBody.position.set(initialX, 0.9 * scaleFactor, 0);
568
+
569
+ // Animation properties for visual punch (no physics interaction)
570
+ characterGroup.isPunching = false;
571
+ characterGroup.punchAnimationProgress = 0;
572
+ characterGroup.punchDuration = 15; // frames
573
+
574
+ return { mesh: characterGroup, body: characterBody };
575
+ }
576
+
577
+
578
+ // --- Block Creation (Castle Parts) ---
579
+ function newBlock() {
580
+ if (isGameOver) return;
581
+
582
+ const partDef = castlePartTypes[currentPartIndex];
583
+ const mesh = new THREE.Mesh(partDef.geometry, new THREE.MeshStandardMaterial({ color: partDef.color, roughness: 0.7, metalness: 0.2 }));
584
+ mesh.castShadow = true;
585
+ scene.add(mesh);
586
+
587
+ // Get current player's character position for spawning the block
588
+ const playerCharBody = playerCharacters[`player${currentPlayer}`].body;
589
+ const spawnX = playerCharBody.position.x;
590
+ const spawnZ = playerCharBody.position.z;
591
+
592
+ const body = new CANNON.Body({ mass: partDef.mass, shape: partDef.shape, material: new CANNON.Material('blockMaterial') });
593
+ body.fixedRotation = false; // Allow rotation
594
+ body.position.set(spawnX, BLOCK_SPAWN_HEIGHT, spawnZ); // Spawn at player's horizontal position
595
+ world.addBody(body);
596
+
597
+ currentFallingBlock = {
598
+ mesh: mesh,
599
+ body: body,
600
+ player: currentPlayer,
601
+ height: partDef.geometry.parameters.height || (partDef.geometry.parameters.radius * 2), // Use height from geometry
602
+ };
603
+
604
+ // Add the falling block to the main blocks array
605
+ blocks.push(currentFallingBlock);
606
+
607
+ // No need to set mass to 0 initially or restore it, as it's always falling.
608
+ // The "release" action will now just switch turns.
609
+ }
610
+
611
+ // --- Game Loop ---
612
+ const timeStep = 1 / 60; // seconds
613
+ let lastTime;
614
+
615
+ function animate(time) {
616
+ if (isGameOver) {
617
+ return;
618
+ }
619
+
620
+ requestAnimationFrame(animate);
621
+
622
+ if (lastTime !== undefined) {
623
+ const dt = (time - lastTime) / 1000;
624
+ world.step(timeStep, dt); // Update physics
625
+ }
626
+ lastTime = time;
627
+
628
+ // Update Three.js meshes from Cannon.js bodies
629
+ for (let i = 0; i < blocks.length; i++) {
630
+ blocks[i].mesh.position.copy(blocks[i].body.position);
631
+ blocks[i].mesh.quaternion.copy(blocks[i].body.quaternion);
632
+
633
+ // Check if any block has fallen too far (game over condition)
634
+ if (blocks[i].body.position.y < FALL_THRESHOLD) {
635
+ endGame();
636
+ return;
637
+ }
638
+
639
+ // Update score based on the highest block in each player's castle
640
+ const blockTopY = blocks[i].body.position.y + blocks[i].height / 2;
641
+ if (blocks[i].player === 1) {
642
+ playerScores.player1 = Math.max(playerScores.player1, blockTopY);
643
+ } else if (blocks[i].player === 2) {
644
+ playerScores.player2 = Math.max(playerScores.player2, blockTopY);
645
+ }
646
+ }
647
+ updateScoreDisplay();
648
+
649
+ // Synchronize player character meshes with their physics bodies (even if static, for visual updates)
650
+ playerCharacters.player1.mesh.position.copy(playerCharacters.player1.body.position);
651
+ playerCharacters.player1.mesh.quaternion.copy(playerCharacters.player1.body.quaternion);
652
+ playerCharacters.player2.mesh.position.copy(playerCharacters.player2.body.position);
653
+ playerCharacters.player2.mesh.quaternion.copy(playerCharacters.player2.body.quaternion);
654
+
655
+ // Handle player movement input
656
+ handlePlayerMovement(playerCharacters.player1, 1);
657
+ handlePlayerMovement(playerCharacters.player2, 2);
658
+
659
+ // Handle punch animations (visual only for characters)
660
+ updatePunchAnimation(playerCharacters.player1.mesh);
661
+ updatePunchAnimation(playerCharacters.player2.mesh);
662
+
663
+ renderer.render(scene, camera);
664
+ }
665
+
666
+ // --- Player Movement Logic ---
667
+ function handlePlayerMovement(playerChar, playerNum) {
668
+ const playerBody = playerChar.body;
669
+ let moveX = 0;
670
+ let moveZ = 0;
671
+
672
+ if (playerNum === 1) {
673
+ if (keysPressed['w']) moveZ = -PLAYER_MOVE_SPEED;
674
+ if (keysPressed['s']) moveZ = PLAYER_MOVE_SPEED;
675
+ if (keysPressed['a']) moveX = -PLAYER_MOVE_SPEED;
676
+ if (keysPressed['d']) moveX = PLAYER_MOVE_SPEED;
677
+ } else if (playerNum === 2) {
678
+ if (keysPressed['i']) moveZ = -PLAYER_MOVE_SPEED;
679
+ if (keysPressed['k']) moveZ = PLAYER_MOVE_SPEED;
680
+ if (keysPressed['j']) moveX = -PLAYER_MOVE_SPEED;
681
+ if (keysPressed['l']) moveX = PLAYER_MOVE_SPEED;
682
+ }
683
+
684
+ // Move the player character
685
+ playerBody.position.x += moveX * 5; // Direct position update for static body
686
+ playerBody.position.z += moveZ * 5;
687
+
688
+ // Keep player character within arena bounds
689
+ const arenaHalfWidth = 14; // Half width of the arena ground
690
+ const arenaHalfDepth = 9; // Half depth of the arena ground
691
+ playerBody.position.x = Math.max(-arenaHalfWidth, Math.min(arenaHalfWidth, playerBody.position.x));
692
+ playerBody.position.z = Math.max(-arenaHalfDepth, Math.min(arenaHalfDepth, playerBody.position.z));
693
+
694
+ // Make player character face the direction of movement
695
+ if (moveX !== 0 || moveZ !== 0) {
696
+ const targetQuaternion = new THREE.Quaternion().setFromUnitVectors(
697
+ new THREE.Vector3(0, 0, 1), // Default forward direction
698
+ new THREE.Vector3(moveX, 0, moveZ).normalize()
699
+ );
700
+ playerChar.mesh.quaternion.slerp(targetQuaternion, 0.1); // Smooth rotation
701
+ }
702
+ }
703
+
704
+ // --- Punch Animation Logic (Visual only) ---
705
+ function updatePunchAnimation(playerMesh) {
706
+ if (playerMesh.isPunching) {
707
+ playerMesh.punchAnimationProgress++;
708
+ const progress = playerMesh.punchAnimationProgress / playerMesh.punchDuration;
709
+
710
+ // Simple punch animation: arm moves forward and back
711
+ const armSwing = Math.sin(progress * Math.PI) * Math.PI / 4; // Swing from 0 to PI/4 and back
712
+ playerMesh.rightArm.rotation.x = armSwing; // Rotate arm forward
713
+
714
+ if (playerMesh.punchAnimationProgress >= playerMesh.punchDuration) {
715
+ playerMesh.isPunching = false;
716
+ playerMesh.punchAnimationProgress = 0;
717
+ playerMesh.rightArm.rotation.x = 0; // Reset arm position
718
+ }
719
+ }
720
+ }
721
+
722
+ // --- Release Action Handler (Now "Confirm Placement" and turn switch) ---
723
+ function handleReleaseAction() {
724
+ // Start punch animation for the current player's character
725
+ const playerCharMesh = playerCharacters[`player${currentPlayer}`].mesh;
726
+ if (!playerCharMesh.isPunching) {
727
+ playerCharMesh.isPunching = true;
728
+ playerCharMesh.punchAnimationProgress = 0;
729
+ }
730
+
731
+ // Switch player for the next block
732
+ currentPlayer = currentPlayer === 1 ? 2 : 1;
733
+
734
+ // Generate a new block after a short delay to allow physics to settle
735
+ // This new block will immediately start falling at the new player's location
736
+ setTimeout(newBlock, 500);
737
+ }
738
+
739
+ // --- Event Handlers ---
740
+ function onWindowResize() {
741
+ const gameContainer = document.getElementById('game-container');
742
+ const controlsPanel = document.getElementById('controls-panel');
743
+
744
+ let canvasWidth, canvasHeight;
745
+
746
+ if (window.innerWidth > 768) {
747
+ canvasWidth = gameContainer.clientWidth - (controlsPanel.clientWidth || 0);
748
+ canvasHeight = gameContainer.clientHeight;
749
+ } else {
750
+ canvasWidth = gameContainer.clientWidth;
751
+ canvasHeight = gameContainer.clientHeight - (controlsPanel.clientHeight || 0);
752
+ }
753
+
754
+ canvasWidth = Math.max(1, canvasWidth);
755
+ canvasHeight = Math.max(1, canvasHeight);
756
+
757
+ camera.aspect = canvasWidth / canvasHeight;
758
+ camera.updateProjectionMatrix();
759
+ renderer.setSize(canvasWidth, canvasHeight);
760
+ }
761
+
762
+ function onKeyDown(event) {
763
+ keysPressed[event.key.toLowerCase()] = true;
764
+
765
+ // Handle release actions for keyboard
766
+ if (event.key.toLowerCase() === 'e' && currentPlayer === 1) {
767
+ handleReleaseAction();
768
+ }
769
+ if (event.key.toLowerCase() === 'u' && currentPlayer === 2) {
770
+ handleReleaseAction();
771
+ }
772
+ }
773
+
774
+ function onKeyUp(event) {
775
+ keysPressed[event.key.toLowerCase()] = false;
776
+ }
777
+
778
+ function updateScoreDisplay() {
779
+ player1ScoreElement.textContent = `P1: ${playerScores.player1.toFixed(2)}`;
780
+ player2ScoreElement.textContent = `P2: ${playerScores.player2.toFixed(2)}`;
781
+ }
782
+
783
+ function endGame() {
784
+ isGameOver = true;
785
+ gameOverMessage.style.display = 'block';
786
+ }
787
+
788
+ function resetGame() {
789
+ // Clear all blocks from scene and world
790
+ blocks.forEach(block => {
791
+ scene.remove(block.mesh);
792
+ world.removeBody(block.body);
793
+ });
794
+ blocks = []; // Clear the array
795
+
796
+ // Reset scores
797
+ playerScores = { player1: 0, player2: 0 };
798
+ updateScoreDisplay();
799
+
800
+ // Reset game state
801
+ isGameOver = false;
802
+ gameOverMessage.style.display = 'none';
803
+ currentPlayer = 1; // Start with Player 1
804
+ currentPartIndex = 0; // Reset part cycle
805
+
806
+ // Reset player character positions
807
+ playerCharacters.player1.body.position.set(-PLAYER_SPAWN_OFFSET_X, 0.9, 0);
808
+ playerCharacters.player1.mesh.position.copy(playerCharacters.player1.body.position);
809
+ playerCharacters.player1.mesh.rotation.set(0,0,0); // Reset mesh rotation
810
+
811
+ playerCharacters.player2.body.position.set(PLAYER_SPAWN_OFFSET_X, 0.9, 0);
812
+ playerCharacters.player2.mesh.position.copy(playerCharacters.player2.body.position);
813
+ playerCharacters.player2.mesh.rotation.set(0,0,0); // Reset mesh rotation
814
+
815
+ // Generate a new block to start
816
+ newBlock();
817
+
818
+ // Restart animation loop if it was stopped
819
+ if (!isGameOver && lastTime === undefined) {
820
+ animate();
821
+ }
822
+ }
823
+
824
+ // --- Start the game when the window loads ---
825
+ window.onload = function () {
826
+ init();
827
+ updateScoreDisplay(); // Initialize score display
828
+ onWindowResize(); // Call once to set initial canvas size correctly
829
+ };
830
+ </script>
831
+ </body>
832
  </html>