awacke1 commited on
Commit
ef90db1
·
verified ·
1 Parent(s): 5a05419

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1205 -18
index.html CHANGED
@@ -1,19 +1,1206 @@
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>Primitive Punch-Up!</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
8
+ <style>
9
+ /* Basic styling for the body to remove default margins and overflows */
10
+ body {
11
+ margin: 0;
12
+ overflow: hidden; /* Hide scrollbars */
13
+ font-family: 'Inter', sans-serif; /* Use Inter font */
14
+ background-color: #1a1a1a; /* Dark background for the page */
15
+ }
16
+
17
+ /* Container for the game canvas and UI elements */
18
+ #game-container {
19
+ position: relative;
20
+ width: 100vw; /* Full viewport width */
21
+ height: 100vh; /* Full viewport height */
22
+ display: flex; /* Use flexbox for centering */
23
+ justify-content: center; /* Center horizontally */
24
+ align-items: center; /* Center vertically */
25
+ overflow: hidden; /* Ensure no overflow */
26
+ }
27
+
28
+ /* Styling for the score displays */
29
+ #score-left, #score-right {
30
+ position: absolute;
31
+ top: 20px; /* Distance from the top */
32
+ color: white; /* White text color */
33
+ font-size: 2.2em; /* Larger font size */
34
+ font-weight: bold; /* Bold text */
35
+ text-shadow: 3px 3px 6px rgba(0,0,0,0.8); /* Stronger text shadow for readability */
36
+ z-index: 10; /* Ensure it's above the canvas */
37
+ padding: 10px 15px; /* Padding around text */
38
+ background-color: rgba(0, 0, 0, 0.4); /* Semi-transparent background */
39
+ border-radius: 12px; /* Rounded corners */
40
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5); /* Subtle box shadow */
41
+ }
42
+
43
+ #score-left {
44
+ left: 30px; /* Position on the left */
45
+ }
46
+
47
+ #score-right {
48
+ right: 30px; /* Position on the right */
49
+ }
50
+
51
+ /* Styling for the health bars */
52
+ #health-bar-left, #health-bar-right {
53
+ position: absolute;
54
+ top: 90px; /* Position below scores */
55
+ height: 35px; /* Height of the health bar */
56
+ background-color: green; /* Default healthy color */
57
+ border: 3px solid #ffffff; /* White border */
58
+ border-radius: 10px; /* Rounded corners */
59
+ transition: width 0.3s ease-out, background-color 0.3s ease-out; /* Smooth transitions for width and color */
60
+ z-index: 10; /* Ensure it's above the canvas */
61
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5); /* Subtle box shadow */
62
+ }
63
+
64
+ #health-bar-left {
65
+ left: 50%; /* Start from the center */
66
+ transform: translateX(-105%); /* Shift left to align to the left of center */
67
+ width: 250px; /* Max width for health bar */
68
+ max-width: 250px; /* Ensure it doesn't exceed this width */
69
+ }
70
+
71
+ #health-bar-right {
72
+ right: 50%; /* Start from the center */
73
+ transform: translateX(5%); /* Shift right to align to the right of center */
74
+ width: 250px; /* Max width for health bar */
75
+ max-width: 250px; /* Ensure it doesn't exceed this width */
76
+ }
77
+
78
+ /* Styling for the reset button */
79
+ #reset-button {
80
+ position: absolute;
81
+ bottom: 40px; /* Distance from the bottom */
82
+ left: 50%; /* Center horizontally */
83
+ transform: translateX(-50%); /* Adjust for true centering */
84
+ padding: 18px 35px; /* Generous padding */
85
+ font-size: 1.8em; /* Larger font size */
86
+ background: linear-gradient(145deg, #ff6b6b, #ee4444); /* Gradient background */
87
+ color: white; /* White text */
88
+ border: none; /* No border */
89
+ border-radius: 15px; /* More rounded corners */
90
+ cursor: pointer; /* Pointer cursor on hover */
91
+ box-shadow: 0 8px 20px rgba(0,0,0,0.6); /* Stronger shadow */
92
+ transition: background 0.3s ease, transform 0.1s ease, box-shadow 0.3s ease; /* Smooth transitions */
93
+ z-index: 10; /* Ensure it's above the canvas */
94
+ font-weight: bold; /* Bold text */
95
+ letter-spacing: 1px; /* Slight letter spacing */
96
+ text-transform: uppercase; /* Uppercase text */
97
+ }
98
+
99
+ #reset-button:hover {
100
+ background: linear-gradient(145deg, #ff4d4d, #cc3333); /* Darker gradient on hover */
101
+ transform: translateX(-50%) scale(1.05); /* Slightly enlarge on hover */
102
+ box-shadow: 0 10px 25px rgba(0,0,0,0.8); /* Deeper shadow on hover */
103
+ }
104
+
105
+ #reset-button:active {
106
+ transform: translateX(-50%) scale(0.98); /* Shrink slightly on click */
107
+ box-shadow: 0 4px 10px rgba(0,0,0,0.4); /* Recessed shadow on click */
108
+ }
109
+
110
+ /* Styling for the Three.js canvas */
111
+ canvas {
112
+ display: block; /* Remove extra space below canvas */
113
+ width: 100%; /* Make canvas fill its container */
114
+ height: 100%; /* Make canvas fill its container */
115
+ border-radius: 15px; /* Rounded corners for the canvas itself */
116
+ box-shadow: 0 0 25px rgba(0,0,0,0.7); /* Shadow around the canvas */
117
+ }
118
+
119
+ /* Controls display */
120
+ #controls {
121
+ position: absolute;
122
+ bottom: 120px; /* Position above the reset button */
123
+ left: 50%;
124
+ transform: translateX(-50%);
125
+ background-color: rgba(0, 0, 0, 0.6);
126
+ color: white;
127
+ padding: 15px 25px;
128
+ border-radius: 12px;
129
+ box-shadow: 0 4px 15px rgba(0,0,0,0.7);
130
+ font-size: 1.1em;
131
+ text-align: center;
132
+ z-index: 10;
133
+ display: flex;
134
+ gap: 40px; /* Space between player control sections */
135
+ }
136
+
137
+ #controls h3 {
138
+ margin-top: 0;
139
+ color: #ffd700; /* Gold color for headings */
140
+ font-size: 1.3em;
141
+ margin-bottom: 10px;
142
+ }
143
+
144
+ #controls ul {
145
+ list-style: none;
146
+ padding: 0;
147
+ margin: 0;
148
+ text-align: left;
149
+ }
150
+
151
+ #controls li {
152
+ margin-bottom: 5px;
153
+ }
154
+
155
+ #controls span {
156
+ font-weight: bold;
157
+ color: #aaffaa; /* Light green for keys */
158
+ display: inline-block;
159
+ width: 40px; /* Fixed width for key display */
160
+ }
161
+ </style>
162
+ </head>
163
+ <body>
164
+ <div id="game-container">
165
+ <div id="score-left">P1 Score: 0</div>
166
+ <div id="score-right">P2 Score: 0</div>
167
+
168
+ <div id="health-bar-left"></div>
169
+ <div id="health-bar-right"></div>
170
+
171
+ <button id="reset-button">Primitive Reset!</button>
172
+
173
+ <div id="controls">
174
+ <div>
175
+ <h3>Player 1 Controls</h3>
176
+ <ul>
177
+ <li><span>WASD:</span> Move</li>
178
+ <li><span>E:</span> Attack</li>
179
+ <li><span>Q:</span> Block</li>
180
+ <li><span>C:</span> Change Gear</li>
181
+ </ul>
182
+ </div>
183
+ <div>
184
+ <h3>Player 2 Controls</h3>
185
+ <ul>
186
+ <li><span>IJKL:</span> Move</li>
187
+ <li><span>U:</span> Attack</li>
188
+ <li><span>O:</span> Block</li>
189
+ <li><span>M:</span> Change Gear</li>
190
+ </ul>
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ <script type="module">
196
+ // Import Three.js library from a reliable CDN
197
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js';
198
+
199
+ // Declare global variables for Three.js scene, camera, renderer, and game state
200
+ let scene, camera, renderer;
201
+ let player1, player2; // Our primitive-assembled characters
202
+ let player1Health = 100;
203
+ let player2Health = 100;
204
+ let player1Score = 0;
205
+ let player2Score = 0;
206
+
207
+ const maxHealth = 100; // Define max health for players
208
+
209
+ const movementSpeed = 0.3; // Adjust movement speed for players
210
+ const baseAttackDamage = 10; // Base damage dealt by an attack
211
+ const blockReduction = 0.5; // Percentage of damage reduced when blocking (e.g., 0.5 means 50% less damage)
212
+ const playerScaleFactor = 1.5; // Scale factor for players
213
+ const attackRange = 2 * (1.5 * playerScaleFactor); // About 2 player widths
214
+
215
+ // Projectile parameters
216
+ const projectileSpeed = 0.8;
217
+ const projectileLife = 120; // frames (2 seconds at 60fps)
218
+ const projectileRadius = 0.3;
219
+
220
+ // Object to keep track of pressed keys for smooth movement
221
+ const keys = {
222
+ w: false, a: false, s: false, d: false,
223
+ i: false, j: false, k: false, l: false,
224
+ p1_attack: false, p1_block: false, p1_change_gear: false,
225
+ p2_attack: false, p2_block: false, p2_change_gear: false
226
+ };
227
+
228
+ // Weapon and Shield Definitions for DnD-like classes
229
+ const gearCombinations = [
230
+ { name: "Fighter (Sword & Kite Shield)", weapon: "sword", shield: "kite_shield" },
231
+ { name: "Barbarian (Great Axe)", weapon: "great_axe", shield: null },
232
+ { name: "Rogue (Daggers & Buckler)", weapon: "dagger", shield: "buckler" },
233
+ { name: "Paladin (Mace & Tower Shield)", weapon: "mace", shield: "tower_shield" },
234
+ { name: "Ranger (Longbow)", weapon: "longbow", shield: null }
235
+ ];
236
+
237
+ let player1GearIndex = 0;
238
+ let player2GearIndex = 0;
239
+
240
+ const activeProjectiles = [];
241
+ const impactParticlesGroup = new THREE.Group();
242
+ const weaponRangeParticlesGroup = new THREE.Group(); // New group for weapon range particles
243
+
244
+ /**
245
+ * Generates a random integer within a specified range (inclusive).
246
+ * @param {number} min - The minimum value.
247
+ * @param {number} max - The maximum value.
248
+ * @returns {number} A random integer.
249
+ */
250
+ function getRandomInt(min, max) {
251
+ min = Math.ceil(min);
252
+ max = Math.floor(max);
253
+ return Math.floor(Math.random() * (max - min + 1)) + min;
254
+ }
255
+
256
+ /**
257
+ * Initializes the game environment, including the Three.js scene, camera, renderer,
258
+ * 3D objects (players, ground), UI elements, and event listeners.
259
+ */
260
+ function init() {
261
+ // Scene Setup: The container for all 3D objects, lights, and cameras
262
+ scene = new THREE.Scene();
263
+ scene.background = new THREE.Color(0x333333); // Dark grey background for the 3D scene
264
+
265
+ // Camera Setup: Orthographic camera for a top-down semi-isometric view
266
+ const aspectRatio = window.innerWidth / window.innerHeight;
267
+ const frustumSize = 30; // Adjusted frustum size for a slightly more zoomed in view
268
+ camera = new THREE.OrthographicCamera(
269
+ frustumSize * aspectRatio / - 2,
270
+ frustumSize * aspectRatio / 2,
271
+ frustumSize / 2,
272
+ frustumSize / - 2,
273
+ 1, 1000
274
+ );
275
+ // Position camera for a top-down semi-isometric view
276
+ camera.position.set(10, 30, 10); // Higher Y, and equal X/Z for a balanced angle
277
+ camera.lookAt(0, 0, 0); // Make the camera look directly at the origin
278
+
279
+ // Renderer Setup: Renders the 3D scene onto a 2D canvas
280
+ renderer = new THREE.WebGLRenderer({ antialias: true });
281
+ renderer.setSize(window.innerWidth, window.innerHeight);
282
+ renderer.setPixelRatio(window.devicePixelRatio);
283
+ document.getElementById('game-container').appendChild(renderer.domElement);
284
+
285
+ // Lighting: Essential for seeing 3D objects
286
+ const ambientLight = new THREE.AmbientLight(0x404040);
287
+ scene.add(ambientLight);
288
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
289
+ directionalLight.position.set(5, 10, 7);
290
+ scene.add(directionalLight);
291
+
292
+ // Ground/Arena: A simple plane representing the fighting stage
293
+ const groundGeometry = new THREE.PlaneGeometry(50, 50); // Larger ground for larger players
294
+ const groundMaterial = new THREE.MeshPhongMaterial({ color: 0x666666, side: THREE.DoubleSide });
295
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
296
+ ground.rotation.x = -Math.PI / 2;
297
+ scene.add(ground);
298
+
299
+ // Player 1 Character: Created using primitive shapes
300
+ player1 = createPrimitiveCharacter(0xff4500); // Red-orange color
301
+ player1.position.set(-10, 0.5 * playerScaleFactor, 0); // Start on the left side of the arena, adjusted for size
302
+ scene.add(player1);
303
+
304
+ // Player 2 Character: Created using primitive shapes
305
+ player2 = createPrimitiveCharacter(0x00aaff); // Blue color
306
+ player2.position.set(10, 0.5 * playerScaleFactor, 0); // Start on the right side of the arena, adjusted for size
307
+ scene.add(player2);
308
+
309
+ // Initialize players with their starting gear
310
+ updatePlayerGear(player1, player1GearIndex);
311
+ updatePlayerGear(player2, player2GearIndex);
312
+
313
+ // Add particle groups to the scene
314
+ scene.add(impactParticlesGroup);
315
+ scene.add(weaponRangeParticlesGroup);
316
+
317
+ // UI Elements Initialization and Event Listeners
318
+ document.getElementById('reset-button').addEventListener('click', resetGame);
319
+ updateHealthBars();
320
+ updateScores();
321
+
322
+ // Event Listeners for Keyboard Input
323
+ window.addEventListener('keydown', onKeyDown);
324
+ window.addEventListener('keyup', onKeyUp);
325
+ window.addEventListener('resize', onWindowResize);
326
+
327
+ // Start the animation loop
328
+ animate();
329
+ }
330
+
331
+ /**
332
+ * Creates a "primitive assembled" character with body, head, arms, and legs using Three.js primitives.
333
+ * Each part is given a name and damage value for collision detection and health reduction.
334
+ * @param {number} color - Hex color for the character.
335
+ * @returns {THREE.Group} A group representing the player character.
336
+ */
337
+ function createPrimitiveCharacter(color) {
338
+ const character = new THREE.Group();
339
+ character.parts = []; // Array to hold all individual body parts
340
+ character.missingParts = []; // Array to track removed parts
341
+ character.playerColor = color; // Store the player's main color
342
+
343
+ // Body: A central box
344
+ const bodyGeometry = new THREE.BoxGeometry(1.5 * playerScaleFactor, 2.5 * playerScaleFactor, 1.5 * playerScaleFactor);
345
+ const bodyMaterial = new THREE.MeshPhongMaterial({ color: color });
346
+ const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
347
+ body.position.y = 1.25 * playerScaleFactor;
348
+ body.name = 'body';
349
+ body.damageValue = baseAttackDamage * 1.5; // Body hits deal more damage
350
+ character.add(body);
351
+ character.parts.push(body);
352
+
353
+ // Head: A sphere on top of the body
354
+ const headGeometry = new THREE.SphereGeometry(0.8 * playerScaleFactor, 24, 24);
355
+ const headMaterial = new THREE.MeshPhongMaterial({ color: color });
356
+ const head = new THREE.Mesh(headGeometry, headMaterial);
357
+ head.position.y = 3.2 * playerScaleFactor;
358
+ head.name = 'head';
359
+ head.damageValue = baseAttackDamage * 3; // Headshots deal critical damage
360
+ character.add(head);
361
+ character.parts.push(head);
362
+
363
+ const visorGeometry = new THREE.BoxGeometry(0.6 * playerScaleFactor, 0.2 * playerScaleFactor, 0.1 * playerScaleFactor);
364
+ const visorMaterial = new THREE.MeshPhongMaterial({ color: 0x333333 });
365
+ const visor = new THREE.Mesh(visorGeometry, visorMaterial);
366
+ visor.position.set(0, 1.8 * playerScaleFactor, 0.5 * playerScaleFactor);
367
+ visor.name = 'visor'; // Not a critical part, no damageValue
368
+ character.add(visor);
369
+ character.parts.push(visor);
370
+
371
+ // Arms: Two cylinders for arms
372
+ const upperArmGeometry = new THREE.CylinderGeometry(0.3 * playerScaleFactor, 0.3 * playerScaleFactor, 0.7 * playerScaleFactor, 8);
373
+ const lowerArmGeometry = new THREE.CylinderGeometry(0.28 * playerScaleFactor, 0.28 * playerScaleFactor, 0.7 * playerScaleFactor, 8);
374
+ const armMaterial = new THREE.MeshPhongMaterial({ color: color });
375
+
376
+ // Player's right arm (for weapon)
377
+ const rightArm = new THREE.Mesh(upperArmGeometry, armMaterial);
378
+ rightArm.position.set(-1.2 * playerScaleFactor, 2.2 * playerScaleFactor, 0);
379
+ rightArm.rotation.z = (color === 0xff4500) ? Math.PI / 6 : -Math.PI / 6;
380
+ rightArm.rotation.x = 0;
381
+ rightArm.name = 'rightArm';
382
+ rightArm.damageValue = baseAttackDamage * 0.8;
383
+ character.add(rightArm);
384
+ character.parts.push(rightArm);
385
+ character.rightArm = rightArm; // Store reference for animation
386
+ character.rightArmInitialRotZ = rightArm.rotation.z;
387
+ character.rightArmInitialRotX = rightArm.rotation.x;
388
+
389
+ const rightLowerArm = new THREE.Mesh(lowerArmGeometry, armMaterial);
390
+ rightLowerArm.position.set(-1.7 * playerScaleFactor, 1.8 * playerScaleFactor, 0);
391
+ rightLowerArm.rotation.z = Math.PI / 8;
392
+ rightLowerArm.name = 'rightLowerArm';
393
+ rightLowerArm.damageValue = baseAttackDamage * 0.8;
394
+ character.add(rightLowerArm);
395
+ character.parts.push(rightLowerArm);
396
+
397
+
398
+ // Player's left arm (for shield)
399
+ const leftArm = new THREE.Mesh(upperArmGeometry, armMaterial);
400
+ leftArm.position.set(1.2 * playerScaleFactor, 2.2 * playerScaleFactor, 0);
401
+ leftArm.rotation.z = (color === 0xff4500) ? -Math.PI / 6 : Math.PI / 6;
402
+ leftArm.rotation.x = 0;
403
+ leftArm.rotation.y = 0;
404
+ leftArm.name = 'leftArm';
405
+ leftArm.damageValue = baseAttackDamage * 0.8;
406
+ character.add(leftArm);
407
+ character.parts.push(leftArm);
408
+ character.leftArm = leftArm; // Store reference for animation
409
+ character.leftArmInitialRotZ = leftArm.rotation.z;
410
+ character.leftArmInitialRotX = leftArm.rotation.x;
411
+ character.leftArmInitialRotY = leftArm.rotation.y;
412
+
413
+ const leftLowerArm = new THREE.Mesh(lowerArmGeometry, armMaterial);
414
+ leftLowerArm.position.set(1.7 * playerScaleFactor, 1.8 * playerScaleFactor, 0);
415
+ leftLowerArm.rotation.z = -Math.PI / 8;
416
+ leftLowerArm.name = 'leftLowerArm';
417
+ leftLowerArm.damageValue = baseAttackDamage * 0.8;
418
+ character.add(leftLowerArm);
419
+ character.parts.push(leftLowerArm);
420
+
421
+
422
+ // Legs: Two cylinders for legs
423
+ const upperLegGeometry = new THREE.CylinderGeometry(0.25 * playerScaleFactor, 0.25 * playerScaleFactor, 0.8 * playerScaleFactor, 8);
424
+ const lowerLegGeometry = new THREE.CylinderGeometry(0.2 * playerScaleFactor, 0.2 * playerScaleFactor, 0.8 * playerScaleFactor, 8);
425
+ const legMaterial = new THREE.MeshPhongMaterial({ color: 0x777777 });
426
+
427
+ const leg1 = new THREE.Mesh(upperLegGeometry, legMaterial);
428
+ leg1.position.set(-0.3 * playerScaleFactor, 0.4 * playerScaleFactor, 0);
429
+ leg1.name = 'leftUpperLeg';
430
+ leg1.damageValue = baseAttackDamage * 0.5;
431
+ character.add(leg1);
432
+ character.parts.push(leg1);
433
+
434
+ const leg2 = new THREE.Mesh(lowerLegGeometry, legMaterial);
435
+ leg2.position.set(-0.3 * playerScaleFactor, -0.4 * playerScaleFactor, 0);
436
+ leg2.name = 'leftLowerLeg';
437
+ leg2.damageValue = baseAttackDamage * 0.5;
438
+ character.add(leg2);
439
+ character.parts.push(leg2);
440
+
441
+ const leg3 = new THREE.Mesh(upperLegGeometry, legMaterial);
442
+ leg3.position.set(0.3 * playerScaleFactor, 0.4 * playerScaleFactor, 0);
443
+ leg3.name = 'rightUpperLeg';
444
+ leg3.damageValue = baseAttackDamage * 0.5;
445
+ character.add(leg3);
446
+ character.parts.push(leg3);
447
+
448
+ const leg4 = new THREE.Mesh(lowerLegGeometry, legMaterial);
449
+ leg4.position.set(0.3 * playerScaleFactor, -0.4 * playerScaleFactor, 0);
450
+ leg4.name = 'rightLowerLeg';
451
+ leg4.damageValue = baseAttackDamage * 0.5;
452
+ character.add(leg4);
453
+ character.parts.push(leg4);
454
+
455
+ // Backpack (Box)
456
+ const backpackGeometry = new THREE.BoxGeometry(0.8 * playerScaleFactor, 1 * playerScaleFactor, 0.4 * playerScaleFactor);
457
+ const backpackMaterial = new THREE.MeshPhongMaterial({ color: 0x444444 });
458
+ const backpack = new THREE.Mesh(backpackGeometry, backpackMaterial);
459
+ backpack.position.set(0, 0.75 * playerScaleFactor, -0.7 * playerScaleFactor);
460
+ backpack.name = 'backpack'; // Not a critical part
461
+ character.add(backpack);
462
+ character.parts.push(backpack);
463
+
464
+ // Store initial positions and rotations for resetting
465
+ character.initialState = {
466
+ position: character.position.clone(),
467
+ parts: character.parts.map(part => ({
468
+ name: part.name,
469
+ position: part.position.clone(),
470
+ rotation: part.rotation.clone(),
471
+ material: part.material // Store material reference
472
+ }))
473
+ };
474
+
475
+ // Animation properties
476
+ character.isAttacking = false;
477
+ character.attackAnimationProgress = 0;
478
+ character.attackDuration = 15; // frames
479
+
480
+ character.isBlocking = false;
481
+ character.blockAnimationProgress = 0;
482
+ character.blockDuration = 10; // frames
483
+
484
+ // Store current weapon type for animation logic
485
+ character.currentWeaponType = null;
486
+ character.weaponMesh = null; // Reference to the actual weapon mesh
487
+ character.shieldMesh = null; // Reference to the actual shield mesh
488
+
489
+ return character;
490
+ }
491
+
492
+ /**
493
+ * Creates a weapon mesh based on the specified type.
494
+ * Includes a collision sphere for the weapon.
495
+ * @param {string} type - The type of weapon (e.g., "sword", "longbow").
496
+ * @param {number} [color=0xcccccc] - Hex color for the weapon.
497
+ * @returns {THREE.Group} A group representing the weapon.
498
+ */
499
+ function createWeapon(type, color = 0xcccccc) {
500
+ const weaponGroup = new THREE.Group();
501
+ const material = new THREE.MeshPhongMaterial({ color: color });
502
+ const handleMaterial = new THREE.MeshPhongMaterial({ color: 0x663300 });
503
+ const bladeMaterial = new THREE.MeshPhongMaterial({ color: 0x999999 });
504
+
505
+ // Collision sphere for the weapon (relative to its local origin)
506
+ weaponGroup.collisionSphere = new THREE.Sphere(new THREE.Vector3(), 1.0);
507
+
508
+ switch (type) {
509
+ case "sword":
510
+ weaponGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.2, 2.0, 0.1), bladeMaterial)); // Blade
511
+ const hiltMesh = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.15, 0.5), handleMaterial); // Hilt
512
+ hiltMesh.position.y = -1.25;
513
+ weaponGroup.add(hiltMesh);
514
+ weaponGroup.position.set(0.5, -0.5, 0);
515
+ weaponGroup.rotation.z = -Math.PI / 2;
516
+ weaponGroup.collisionSphere.radius = 1.2; // Adjust collision sphere size
517
+ break;
518
+ case "great_axe":
519
+ weaponGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.2, 3.0, 0.2), handleMaterial)); // Handle
520
+ const axeHead = new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.5, 1.0), bladeMaterial); // Axe blade
521
+ axeHead.position.set(0, 1.5, 0.5);
522
+ weaponGroup.add(axeHead);
523
+ weaponGroup.position.set(0.5, -1.0, 0);
524
+ weaponGroup.rotation.z = -Math.PI / 2;
525
+ weaponGroup.collisionSphere.radius = 1.8;
526
+ break;
527
+ case "dagger":
528
+ weaponGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.8, 0.05), bladeMaterial)); // Blade
529
+ const daggerHiltMesh = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 0.3), handleMaterial); // Hilt
530
+ daggerHiltMesh.position.y = -0.55;
531
+ weaponGroup.add(daggerHiltMesh);
532
+ weaponGroup.position.set(0.3, -0.2, 0);
533
+ weaponGroup.rotation.z = -Math.PI / 2;
534
+ weaponGroup.collisionSphere.radius = 0.8;
535
+ break;
536
+ case "mace":
537
+ weaponGroup.add(new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 1.5), handleMaterial)); // Handle
538
+ const maceHead = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), bladeMaterial); // Spiked head
539
+ maceHead.position.y = 0.75;
540
+ weaponGroup.add(maceHead);
541
+ weaponGroup.position.set(0.5, -0.75, 0);
542
+ weaponGroup.rotation.z = -Math.PI / 2;
543
+ weaponGroup.collisionSphere.radius = 1.0;
544
+ break;
545
+ case "longbow":
546
+ const bowString = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 2.0), new THREE.MeshPhongMaterial({ color: 0x333333 }));
547
+ const bowBody = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.0, 0.1), new THREE.MeshPhongMaterial({ color: 0x8B4513 }));
548
+ bowString.position.x = 0.5;
549
+ bowBody.rotation.y = Math.PI / 2;
550
+ weaponGroup.add(bowBody);
551
+ weaponGroup.add(bowString);
552
+ weaponGroup.position.set(0.5, -0.5, 0);
553
+ weaponGroup.rotation.z = -Math.PI / 2;
554
+ weaponGroup.collisionSphere.radius = 1.5;
555
+ break;
556
+ default:
557
+ break;
558
+ }
559
+ weaponGroup.name = `weapon_${type}`;
560
+ weaponGroup.visible = false;
561
+ return weaponGroup;
562
+ }
563
+
564
+ /**
565
+ * Creates a shield mesh based on the specified type.
566
+ * Includes a collision sphere for the shield.
567
+ * @param {string} type - The type of shield (e.g., "kite_shield", "buckler").
568
+ * @param {number} [color=0x888888] - Hex color for the shield.
569
+ * @returns {THREE.Group} A group representing the shield.
570
+ */
571
+ function createShield(type, color = 0x888888) {
572
+ const shieldGroup = new THREE.Group();
573
+ const material = new THREE.MeshPhongMaterial({ color: color });
574
+ shieldGroup.collisionSphere = new THREE.Sphere(new THREE.Vector3(), 1.5); // Default collision sphere
575
+
576
+ switch (type) {
577
+ case "kite_shield":
578
+ const pointsKite = [
579
+ new THREE.Vector2(0, 1.5), new THREE.Vector2(0.8, 1.0),
580
+ new THREE.Vector2(0.8, -1.5), new THREE.Vector2(-0.8, -1.5),
581
+ new THREE.Vector2(-0.8, 1.0)
582
+ ];
583
+ const kiteShape = new THREE.Shape(pointsKite);
584
+ const shieldShapeKite = new THREE.ExtrudeGeometry(kiteShape, { depth: 0.1, bevelEnabled: false });
585
+ shieldGroup.add(new THREE.Mesh(shieldShapeKite, material));
586
+ shieldGroup.position.set(-0.5, -0.5, 0);
587
+ shieldGroup.rotation.y = Math.PI / 2;
588
+ shieldGroup.collisionSphere.radius = 1.8; // Adjust collision sphere size
589
+ break;
590
+ case "buckler":
591
+ const shieldShapeBuckler = new THREE.CylinderGeometry(0.7, 0.7, 0.1, 16);
592
+ shieldGroup.add(new THREE.Mesh(shieldShapeBuckler, material));
593
+ shieldGroup.position.set(-0.5, -0.2, 0);
594
+ shieldGroup.rotation.y = Math.PI / 2;
595
+ shieldGroup.collisionSphere.radius = 0.9;
596
+ break;
597
+ case "tower_shield":
598
+ const shieldShapeTower = new THREE.BoxGeometry(1.2, 2.5, 0.1);
599
+ shieldGroup.add(new THREE.Mesh(shieldShapeTower, material));
600
+ shieldGroup.position.set(-0.5, -1.0, 0);
601
+ shieldGroup.rotation.y = Math.PI / 2;
602
+ shieldGroup.collisionSphere.radius = 2.0;
603
+ break;
604
+ default:
605
+ break;
606
+ }
607
+ shieldGroup.name = `shield_${type}`;
608
+ shieldGroup.visible = false;
609
+ return shieldGroup;
610
+ }
611
+
612
+ /**
613
+ * Creates a projectile (a small sphere) to be fired.
614
+ * @param {THREE.Group} firingPlayer - The player who fired the projectile.
615
+ * @param {THREE.Group} targetPlayer - The intended target of the projectile.
616
+ */
617
+ function createProjectile(firingPlayer, targetPlayer) {
618
+ const projectileGeometry = new THREE.SphereGeometry(projectileRadius, 8, 8);
619
+ const projectileMaterial = new THREE.MeshPhongMaterial({ color: 0xffd700 }); // Gold color
620
+ const projectileMesh = new THREE.Mesh(projectileGeometry, projectileMaterial);
621
+
622
+ // Set projectile starting position from player's weapon tip or chest height
623
+ const startPosition = new THREE.Vector3();
624
+ if (firingPlayer.weaponMesh) {
625
+ // Get weapon's world position and move it slightly forward
626
+ firingPlayer.weaponMesh.getWorldPosition(startPosition);
627
+ const forwardDir = new THREE.Vector3();
628
+ firingPlayer.getWorldDirection(forwardDir);
629
+ startPosition.add(forwardDir.multiplyScalar(firingPlayer.weaponMesh.collisionSphere.radius * 0.5));
630
+ } else {
631
+ // If no weapon, shoot from player's chest height
632
+ startPosition.copy(firingPlayer.position);
633
+ startPosition.y += 1.5 * playerScaleFactor;
634
+ }
635
+ projectileMesh.position.copy(startPosition);
636
+
637
+ // Calculate direction towards target (at target's chest height)
638
+ const targetPosition = targetPlayer.position.clone();
639
+ targetPosition.y += 1.5 * playerScaleFactor; // Aim for chest height
640
+ const direction = new THREE.Vector3();
641
+ direction.subVectors(targetPosition, projectileMesh.position).normalize();
642
+ projectileMesh.velocity = direction.multiplyScalar(projectileSpeed);
643
+ projectileMesh.life = projectileLife;
644
+ projectileMesh.damage = baseAttackDamage; // Projectiles deal base damage
645
+ projectileMesh.originPlayer = firingPlayer; // Store who fired it
646
+
647
+ scene.add(projectileMesh);
648
+ activeProjectiles.push(projectileMesh);
649
+ }
650
+
651
+ /**
652
+ * Updates a player's equipped weapon and shield based on the gear index.
653
+ * @param {THREE.Group} player - The player character to update.
654
+ * @param {number} gearIndex - The index of the gear combination to equip.
655
+ */
656
+ function updatePlayerGear(player, gearIndex) {
657
+ // Remove existing weapon and shield meshes from the player's arms
658
+ if (player.weaponMesh && player.weaponMesh.parent) {
659
+ player.weaponMesh.parent.remove(player.weaponMesh);
660
+ }
661
+ if (player.shieldMesh && player.shieldMesh.parent) {
662
+ player.shieldMesh.parent.remove(player.shieldMesh);
663
+ }
664
+
665
+ const gear = gearCombinations[gearIndex];
666
+ player.currentWeaponType = gear.weapon; // Store the current weapon type for animation logic
667
+
668
+ // Add new weapon if applicable
669
+ if (gear.weapon) {
670
+ const newWeapon = createWeapon(gear.weapon);
671
+ player.rightArm.add(newWeapon); // Attach to the player's right arm
672
+ player.weaponMesh = newWeapon; // Store reference
673
+ newWeapon.visible = true;
674
+ } else {
675
+ player.weaponMesh = null;
676
+ }
677
+
678
+ // Add new shield if applicable
679
+ if (gear.shield) {
680
+ const newShield = createShield(gear.shield);
681
+ player.leftArm.add(newShield); // Attach to the player's left arm
682
+ player.shieldMesh = newShield; // Store reference
683
+ newShield.visible = true;
684
+ } else {
685
+ player.shieldMesh = null;
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Handles keydown events to update player movement and action states.
691
+ * @param {KeyboardEvent} event - The keyboard event.
692
+ */
693
+ function onKeyDown(event) {
694
+ switch (event.code) {
695
+ // Player 1 Movement (WASD)
696
+ case 'KeyW': keys.w = true; break;
697
+ case 'KeyA': keys.a = true; break;
698
+ case 'KeyS': keys.s = true; break;
699
+ case 'KeyD': keys.d = true; break;
700
+
701
+ // Player 1 Actions
702
+ case 'KeyE': // Attack
703
+ // Only allow attack if not already attacking or blocking
704
+ if (!player1.isAttacking && !player1.isBlocking) {
705
+ startAttackAnimation(player1);
706
+ createProjectile(player1, player2); // All attacks now fire projectiles
707
+ }
708
+ break;
709
+ case 'KeyQ': // Block
710
+ // Only allow block if not already blocking or attacking
711
+ if (!player1.isBlocking && !player1.isAttacking) {
712
+ startBlockAnimation(player1);
713
+ }
714
+ break;
715
+ case 'KeyC': // Change Gear
716
+ if (!keys.p1_change_gear) { // Prevent rapid gear changes
717
+ player1GearIndex = (player1GearIndex + 1) % gearCombinations.length;
718
+ updatePlayerGear(player1, player1GearIndex);
719
+ keys.p1_change_gear = true; // Set flag to prevent continuous change
720
+ }
721
+ break;
722
+
723
+ // Player 2 Movement (IJKL)
724
+ case 'KeyI': keys.i = true; break;
725
+ case 'KeyJ': keys.j = true; break;
726
+ case 'KeyK': keys.k = true; break;
727
+ case 'KeyL': keys.l = true; break;
728
+
729
+ // Player 2 Actions
730
+ case 'KeyU': // Attack
731
+ // Only allow attack if not already attacking or blocking
732
+ if (!player2.isAttacking && !player2.isBlocking) {
733
+ startAttackAnimation(player2);
734
+ createProjectile(player2, player1); // All attacks now fire projectiles
735
+ }
736
+ break;
737
+ case 'KeyO': // Block
738
+ // Only allow block if not already blocking or attacking
739
+ if (!player2.isBlocking && !player2.isAttacking) {
740
+ startBlockAnimation(player2);
741
+ }
742
+ break;
743
+ case 'KeyM': // Change Gear
744
+ if (!keys.p2_change_gear) { // Prevent rapid gear changes
745
+ player2GearIndex = (player2GearIndex + 1) % gearCombinations.length;
746
+ updatePlayerGear(player2, player2GearIndex);
747
+ keys.p2_change_gear = true; // Set flag to prevent continuous change
748
+ }
749
+ break;
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Handles keyup events to reset player movement and action states.
755
+ * @param {KeyboardEvent} event - The keyboard event.
756
+ */
757
+ function onKeyUp(event) {
758
+ switch (event.code) {
759
+ // Player 1 Movement
760
+ case 'KeyW': keys.w = false; break;
761
+ case 'KeyA': keys.a = false; break;
762
+ case 'KeyS': keys.s = false; break;
763
+ case 'KeyD': keys.d = false; break;
764
+
765
+ // Player 1 Actions (resetting the "pressed" state for single-trigger actions)
766
+ case 'KeyE': /* Attack action is handled by animation state */ break;
767
+ case 'KeyQ': /* Block action is handled by animation state */ break;
768
+ case 'KeyC': keys.p1_change_gear = false; break; // Release for next change
769
+
770
+ // Player 2 Movement
771
+ case 'KeyI': keys.i = false; break;
772
+ case 'KeyJ': keys.j = false; break;
773
+ case 'KeyK': keys.k = false; break;
774
+ case 'KeyL': keys.l = false; break;
775
+
776
+ // Player 2 Actions
777
+ case 'KeyU': /* Attack action is handled by animation state */ break;
778
+ case 'KeyO': /* Block action is handled by animation state */ break;
779
+ case 'KeyM': keys.p2_change_gear = false; break; // Release for next change
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Initiates the attack animation for a given player.
785
+ * @param {THREE.Group} player - The player initiating the attack.
786
+ */
787
+ function startAttackAnimation(player) {
788
+ player.isAttacking = true;
789
+ player.attackAnimationProgress = 0; // Reset animation progress
790
+ // Store initial arm rotation for smooth return
791
+ player.rightArm.initialRotationZ = player.rightArm.rotation.z;
792
+ player.rightArm.initialRotationX = player.rightArm.rotation.x;
793
+
794
+ // Create weapon range particles using the player's stored color
795
+ createWeaponRangeParticles(player.weaponMesh ? player.weaponMesh : player.position, player.playerColor);
796
+ }
797
+
798
+ /**
799
+ * Initiates the block animation for a given player.
800
+ * @param {THREE.Group} player - The player initiating the block.
801
+ */
802
+ function startBlockAnimation(player) {
803
+ player.isBlocking = true;
804
+ player.blockAnimationProgress = 0; // Reset animation progress
805
+ // Store initial arm rotation for smooth return
806
+ player.leftArm.initialRotationZ = player.leftArm.rotation.z;
807
+ player.leftArm.initialRotationX = player.leftArm.rotation.x;
808
+ player.leftArm.initialRotationY = player.leftArm.rotation.y;
809
+ }
810
+
811
+ /**
812
+ * Creates a burst of small particles at a given position and color.
813
+ * Used for visual feedback on hits/blocks.
814
+ * @param {THREE.Vector3} position - The world position for particles.
815
+ * @param {number} color - Hex color for the particles.
816
+ * @param {string} type - Type of particles ('impact' or 'blood').
817
+ */
818
+ function createParticles(position, color, type = 'impact') {
819
+ const particleCount = (type === 'blood') ? 20 : 10;
820
+ const particleColor = (type === 'blood') ? 0x8B0000 : color; // Dark red for blood
821
+ const particleMaterial = new THREE.MeshBasicMaterial({ color: particleColor, transparent: true, opacity: 1 });
822
+ const particleGeometry = new THREE.SphereGeometry(0.1, 8, 8); // Smaller for blood
823
+
824
+ for (let i = 0; i < particleCount; i++) {
825
+ const particle = new THREE.Mesh(particleGeometry, particleMaterial.clone());
826
+ particle.position.copy(position);
827
+ particle.velocity = new THREE.Vector3(
828
+ (Math.random() - 0.5) * 0.8, // Random X velocity
829
+ Math.random() * 0.8 + 0.2, // Upward Y velocity
830
+ (Math.random() - 0.5) * 0.8 // Random Z velocity
831
+ );
832
+ particle.life = (type === 'blood') ? 60 : 40; // Longer life for blood
833
+ particle.initialLife = particle.life; // Store initial life for opacity calculation
834
+ impactParticlesGroup.add(particle);
835
+ }
836
+ }
837
+
838
+ /**
839
+ * Creates a temporary particle system to visualize weapon range/attack.
840
+ * @param {THREE.Object3D} originObject - The object from which particles emanate (weapon or player).
841
+ * @param {number} color - The hex color of the particles.
842
+ */
843
+ function createWeaponRangeParticles(originObject, color) {
844
+ const particleCount = 15;
845
+ const particleMaterial = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.8 });
846
+ const particleGeometry = new THREE.SphereGeometry(0.15, 8, 8);
847
+
848
+ const originPosition = new THREE.Vector3();
849
+ if (originObject instanceof THREE.Mesh || originObject instanceof THREE.Group) { // Check if it's a Three.js object
850
+ originObject.getWorldPosition(originPosition);
851
+ } else { // Assume it's a Vector3 if not a mesh/group
852
+ originPosition.copy(originObject);
853
+ }
854
+
855
+ for (let i = 0; i < particleCount; i++) {
856
+ const particle = new THREE.Mesh(particleGeometry, particleMaterial.clone());
857
+ particle.position.copy(originPosition);
858
+ // Randomize initial direction slightly
859
+ const angle = Math.random() * Math.PI * 2;
860
+ const speed = 0.1 + Math.random() * 0.1;
861
+ particle.velocity = new THREE.Vector3(
862
+ Math.cos(angle) * speed,
863
+ 0.1 + Math.random() * 0.1, // Slight upward motion
864
+ Math.sin(angle) * speed
865
+ );
866
+ particle.life = 30; // Short life for range visualization
867
+ particle.initialLife = particle.life; // Store initial life for opacity calculation
868
+ weaponRangeParticlesGroup.add(particle);
869
+ }
870
+ }
871
+
872
+
873
+ /**
874
+ * The main animation loop of the game.
875
+ * Updates player movement, animations, projectile physics, and renders the scene.
876
+ */
877
+ function animate() {
878
+ requestAnimationFrame(animate);
879
+
880
+ // Player 1 Movement
881
+ if (keys.w) player1.position.z -= movementSpeed;
882
+ if (keys.s) player1.position.z += movementSpeed;
883
+ if (keys.a) player1.position.x -= movementSpeed;
884
+ if (keys.d) player1.position.x += movementSpeed;
885
+
886
+ // Player 2 Movement
887
+ if (keys.i) player2.position.z -= movementSpeed;
888
+ if (keys.k) player2.position.z += movementSpeed;
889
+ if (keys.j) player2.position.x -= movementSpeed;
890
+ if (keys.l) player2.position.x += movementSpeed;
891
+
892
+ // Auto-rotate players to face each other (only if not attacking/blocking to prevent jitter)
893
+ if (!player1.isAttacking && !player1.isBlocking) {
894
+ player1.lookAt(player2.position.x, player1.position.y, player2.position.z);
895
+ }
896
+ if (!player2.isAttacking && !player2.isBlocking) {
897
+ player2.lookAt(player1.position.x, player2.position.y, player1.position.z);
898
+ }
899
+
900
+ // Attack Animation Logic for both players
901
+ [player1, player2].forEach(player => {
902
+ if (player.isAttacking) {
903
+ player.attackAnimationProgress++;
904
+ const progress = player.attackAnimationProgress / player.attackDuration;
905
+ const swingDirection = (player === player1) ? 1 : -1; // Adjust swing direction for each player
906
+
907
+ // Animate weapon arm based on weapon type
908
+ switch (player.currentWeaponType) {
909
+ case "sword":
910
+ case "great_axe":
911
+ case "mace":
912
+ // Sweeping motion: Z-axis for horizontal swing, X-axis for vertical tilt
913
+ player.rightArm.rotation.z = player.rightArmInitialRotZ + swingDirection * Math.sin(progress * Math.PI * 1.5) * Math.PI / 2;
914
+ player.rightArm.rotation.x = player.rightArmInitialRotX + Math.sin(progress * Math.PI * 2) * Math.PI / 12;
915
+ break;
916
+ case "dagger":
917
+ // Forward jab: X-axis for forward thrust
918
+ player.rightArm.rotation.x = player.rightArmInitialRotX + Math.sin(progress * Math.PI * 2) * Math.PI / 6;
919
+ player.rightArm.rotation.z = player.rightArmInitialRotZ + swingDirection * Math.sin(progress * Math.PI * 2) * Math.PI / 12; // Slight side movement
920
+ break;
921
+ case "longbow":
922
+ // Draw back and release: X-axis for pulling back, then forward
923
+ player.rightArm.rotation.x = player.rightArmInitialRotX + Math.sin(progress * Math.PI * 2) * Math.PI / 4;
924
+ player.rightArm.rotation.z = player.rightArmInitialRotZ; // Keep Z stable
925
+ break;
926
+ }
927
+
928
+ // End attack animation
929
+ if (player.attackAnimationProgress >= player.attackDuration) {
930
+ player.isAttacking = false;
931
+ // Reset arm rotations to initial state
932
+ player.rightArm.rotation.z = player.rightArmInitialRotZ;
933
+ player.rightArm.rotation.x = player.rightArmInitialRotX;
934
+ }
935
+ }
936
+
937
+ // Block Animation Logic
938
+ if (player.isBlocking) {
939
+ player.blockAnimationProgress++;
940
+ const progress = player.blockAnimationProgress / player.blockDuration;
941
+ const blockDirection = (player === player1) ? 1 : -1; // Adjust block direction
942
+
943
+ // Move shield arm to a defensive position
944
+ player.leftArm.rotation.z = player.leftArmInitialRotZ + blockDirection * Math.sin(progress * Math.PI) * Math.PI / 8; // Arm moves up/down slightly
945
+ player.leftArm.rotation.y = player.leftArmInitialRotY + blockDirection * Math.sin(progress * Math.PI) * Math.PI / 6; // Shield turns towards opponent
946
+
947
+ // End block animation
948
+ if (player.blockAnimationProgress >= player.blockDuration) {
949
+ player.isBlocking = false;
950
+ // Reset arm rotations to initial state
951
+ player.leftArm.rotation.z = player.leftArmInitialRotZ;
952
+ player.leftArm.rotation.x = player.leftArmInitialRotX;
953
+ player.leftArm.rotation.y = player.leftArmInitialRotY;
954
+ }
955
+ }
956
+
957
+ // Health decay from missing parts
958
+ const healthDecayRate = player.missingParts.length * 0.1; // 0.1 health per frame per missing part
959
+ if (healthDecayRate > 0) {
960
+ if (player === player1) {
961
+ player1Health = Math.max(0, player1Health - healthDecayRate);
962
+ if (player1Health === 0) {
963
+ player2Score++;
964
+ updateScores();
965
+ resetGame();
966
+ }
967
+ } else {
968
+ player2Health = Math.max(0, player2Health - healthDecayRate);
969
+ if (player2Health === 0) {
970
+ player1Score++;
971
+ updateScores();
972
+ resetGame();
973
+ }
974
+ }
975
+ updateHealthBars();
976
+ }
977
+ });
978
+
979
+ // Projectile Movement and Collision
980
+ for (let i = activeProjectiles.length - 1; i >= 0; i--) {
981
+ const projectile = activeProjectiles[i];
982
+ projectile.position.add(projectile.velocity);
983
+ projectile.life--;
984
+
985
+ const targetPlayer = (projectile.originPlayer === player1) ? player2 : player1;
986
+
987
+ // Check for collision with target player's *individual* body parts
988
+ const projectileSphere = new THREE.Sphere(projectile.position, projectileRadius);
989
+ let hitOccurred = false;
990
+ let hitPart = null;
991
+
992
+ // Loop through each part of the target player
993
+ for (let j = targetPlayer.parts.length - 1; j >= 0; j--) {
994
+ const part = targetPlayer.parts[j];
995
+ // Calculate world position and collision sphere for the part
996
+ const partWorldPosition = new THREE.Vector3();
997
+ part.getWorldPosition(partWorldPosition);
998
+ const partCollisionSphere = new THREE.Sphere(partWorldPosition, part.geometry.parameters.radius || part.geometry.parameters.width / 2 || 0.5); // Estimate radius from geometry
999
+
1000
+ if (projectileSphere.intersectsSphere(partCollisionSphere)) {
1001
+ // Check for shield block first
1002
+ if (targetPlayer.isBlocking && targetPlayer.shieldMesh) {
1003
+ const targetShield = targetPlayer.shieldMesh;
1004
+ const shieldWorldPosition = new THREE.Vector3();
1005
+ targetShield.getWorldPosition(shieldWorldPosition);
1006
+ targetShield.collisionSphere.center.copy(shieldWorldPosition);
1007
+
1008
+ if (projectileSphere.intersectsSphere(targetShield.collisionSphere)) {
1009
+ // Block successful!
1010
+ applyDamage(targetPlayer, projectile.damage * (1 - blockReduction), projectile.originPlayer);
1011
+ createParticles(shieldWorldPosition, 0x00ff00, 'impact'); // Green for block
1012
+ hitOccurred = true;
1013
+ break; // Shield blocked, no need to check other parts
1014
+ }
1015
+ }
1016
+
1017
+ // If not blocked by shield, apply damage to the part
1018
+ if (!hitOccurred) {
1019
+ hitPart = part;
1020
+ applyDamage(targetPlayer, part.damageValue || projectile.damage, projectile.originPlayer); // Use part's damage value if available
1021
+ createParticles(partWorldPosition, 0xffa500, 'impact'); // Orange for hit
1022
+ createParticles(partWorldPosition, 0xff0000, 'blood'); // Red blood particles
1023
+ hitOccurred = true;
1024
+
1025
+ // Remove the hit part from the player model
1026
+ targetPlayer.remove(part);
1027
+ targetPlayer.parts.splice(j, 1); // Remove from active parts array
1028
+ targetPlayer.missingParts.push(part.name); // Add to missing parts list
1029
+
1030
+ // Dispose of the removed part's geometry and material
1031
+ if (part.geometry) part.geometry.dispose();
1032
+ if (part.material) part.material.dispose();
1033
+ break; // Only one part hit per projectile
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ // Remove projectile on impact or if it goes too far
1039
+ if (hitOccurred || projectile.life <= 0) {
1040
+ scene.remove(projectile);
1041
+ projectile.geometry.dispose();
1042
+ projectile.material.dispose();
1043
+ activeProjectiles.splice(i, 1);
1044
+ }
1045
+ }
1046
+
1047
+ // Update impact particles (move and fade out)
1048
+ for (let i = impactParticlesGroup.children.length - 1; i >= 0; i--) {
1049
+ const particle = impactParticlesGroup.children[i];
1050
+ particle.position.add(particle.velocity);
1051
+ particle.velocity.y -= 0.02; // Simple gravity
1052
+ particle.life--;
1053
+ particle.material.opacity = particle.life / particle.initialLife; // Fade out over lifetime (assuming initialLife was stored)
1054
+ if (particle.life <= 0) {
1055
+ impactParticlesGroup.remove(particle);
1056
+ particle.geometry.dispose();
1057
+ particle.material.dispose();
1058
+ }
1059
+ }
1060
+
1061
+ // Update weapon range particles (expand and fade out)
1062
+ for (let i = weaponRangeParticlesGroup.children.length - 1; i >= 0; i--) {
1063
+ const particle = weaponRangeParticlesGroup.children[i];
1064
+ particle.position.add(particle.velocity);
1065
+ particle.scale.setScalar(particle.scale.x + 0.05); // Expand
1066
+ particle.material.opacity -= 0.03; // Fade out
1067
+ if (particle.material.opacity <= 0) {
1068
+ weaponRangeParticlesGroup.remove(particle);
1069
+ particle.geometry.dispose();
1070
+ particle.material.dispose();
1071
+ }
1072
+ }
1073
+
1074
+ renderer.render(scene, camera);
1075
+ }
1076
+
1077
+ /**
1078
+ * Applies damage to a target player, considering if they are blocking.
1079
+ * If a player's health drops to 0, the game resets and the other player scores.
1080
+ * @param {THREE.Group} targetPlayer - The player receiving damage.
1081
+ * @param {number} amount - The base amount of damage.
1082
+ * @param {THREE.Group} attackingPlayer - The player dealing damage.
1083
+ */
1084
+ function applyDamage(targetPlayer, amount, attackingPlayer) {
1085
+ let actualDamage = amount;
1086
+ if (targetPlayer.isBlocking) {
1087
+ actualDamage = amount * (1 - blockReduction); // Reduce damage if blocking
1088
+ }
1089
+
1090
+ if (targetPlayer === player1) {
1091
+ player1Health = Math.max(0, player1Health - actualDamage);
1092
+ if (player1Health === 0) {
1093
+ player2Score++;
1094
+ updateScores();
1095
+ resetGame();
1096
+ }
1097
+ } else if (targetPlayer === player2) {
1098
+ player2Health = Math.max(0, player2Health - actualDamage);
1099
+ if (player2Health === 0) {
1100
+ player1Score++;
1101
+ updateScores();
1102
+ resetGame();
1103
+ }
1104
+ }
1105
+ updateHealthBars();
1106
+ }
1107
+
1108
+ /**
1109
+ * Updates the visual width and color of the player health bars.
1110
+ */
1111
+ function updateHealthBars() {
1112
+ const healthBarLeft = document.getElementById('health-bar-left');
1113
+ const healthBarRight = document.getElementById('health-bar-right');
1114
+
1115
+ const maxWidth = 250;
1116
+
1117
+ healthBarLeft.style.width = `${(player1Health / maxHealth) * maxWidth}px`;
1118
+ healthBarLeft.style.backgroundColor = player1Health > maxHealth * 0.6 ? '#28a745' : (player1Health > maxHealth * 0.3 ? '#ffc107' : '#dc3545');
1119
+
1120
+ healthBarRight.style.width = `${(player2Health / maxHealth) * maxWidth}px`;
1121
+ healthBarRight.style.backgroundColor = player2Health > maxHealth * 0.6 ? '#28a745' : (player2Health > maxHealth * 0.3 ? '#ffc107' : '#dc3545');
1122
+ }
1123
+
1124
+ /**
1125
+ * Updates the displayed scores for Player 1 and Player 2.
1126
+ */
1127
+ function updateScores() {
1128
+ document.getElementById('score-left').textContent = `P1 Score: ${player1Score}`;
1129
+ document.getElementById('score-right').textContent = `P2 Score: ${player2Score}`;
1130
+ }
1131
+
1132
+ /**
1133
+ * Resets the game to its initial state, including player health, positions,
1134
+ * and clearing all projectiles and particles. Also rebuilds player models.
1135
+ */
1136
+ function resetGame() {
1137
+ player1Health = maxHealth;
1138
+ player2Health = maxHealth;
1139
+ updateHealthBars();
1140
+
1141
+ // Rebuild players to restore missing parts
1142
+ scene.remove(player1);
1143
+ scene.remove(player2);
1144
+ player1 = createPrimitiveCharacter(0xff4500);
1145
+ player2 = createPrimitiveCharacter(0x00aaff);
1146
+ scene.add(player1);
1147
+ scene.add(player2);
1148
+
1149
+ player1.position.set(-10, 0.5 * playerScaleFactor, 0);
1150
+ player2.position.set(10, 0.5 * playerScaleFactor, 0);
1151
+
1152
+ // Re-initialize gear
1153
+ updatePlayerGear(player1, player1GearIndex);
1154
+ updatePlayerGear(player2, player2GearIndex);
1155
+
1156
+ // Reset any ongoing animations and arm positions
1157
+ [player1, player2].forEach(player => {
1158
+ player.isAttacking = false;
1159
+ player.isBlocking = false;
1160
+ player.attackAnimationProgress = 0;
1161
+ player.blockAnimationProgress = 0;
1162
+ // Arm rotations are reset by updatePlayerGear and createPrimitiveCharacter
1163
+ });
1164
+
1165
+ // Remove all active projectiles from scene and array
1166
+ while (activeProjectiles.length > 0) {
1167
+ const projectile = activeProjectiles.pop();
1168
+ scene.remove(projectile);
1169
+ projectile.geometry.dispose();
1170
+ projectile.material.dispose();
1171
+ }
1172
+ // Clear all impact particles from scene and dispose
1173
+ while(impactParticlesGroup.children.length > 0){
1174
+ const particle = impactParticlesGroup.children[0];
1175
+ impactParticlesGroup.remove(particle);
1176
+ particle.geometry.dispose();
1177
+ particle.material.dispose();
1178
+ }
1179
+ // Clear all weapon range particles
1180
+ while(weaponRangeParticlesGroup.children.length > 0){
1181
+ const particle = weaponRangeParticlesGroup.children[0];
1182
+ weaponRangeParticlesGroup.remove(particle);
1183
+ particle.geometry.dispose();
1184
+ particle.material.dispose();
1185
+ }
1186
+ }
1187
+
1188
+ /**
1189
+ * Handles window resize events to adjust the camera and renderer size.
1190
+ */
1191
+ function onWindowResize() {
1192
+ const aspectRatio = window.innerWidth / window.innerHeight;
1193
+ const frustumSize = 30; // Keep consistent with init
1194
+ camera.left = frustumSize * aspectRatio / - 2;
1195
+ camera.right = frustumSize * aspectRatio / 2;
1196
+ camera.top = frustumSize / 2;
1197
+ camera.bottom = frustumSize / - 2;
1198
+ camera.updateProjectionMatrix();
1199
+ renderer.setSize(window.innerWidth, window.innerHeight);
1200
+ }
1201
+
1202
+ // Initialize the game when the window loads
1203
+ window.onload = init;
1204
+ </script>
1205
+ </body>
1206
  </html>