Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Primitive Punch-Up!</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<style> | |
/* Basic styling for the body to remove default margins and overflows */ | |
body { | |
margin: 0; | |
overflow: hidden; /* Hide scrollbars */ | |
font-family: 'Inter', sans-serif; /* Use Inter font */ | |
background-color: #1a1a1a; /* Dark background for the page */ | |
} | |
/* Container for the game canvas and UI elements */ | |
#game-container { | |
position: relative; | |
width: 100vw; /* Full viewport width */ | |
height: 100vh; /* Full viewport height */ | |
display: flex; /* Use flexbox for centering */ | |
justify-content: center; /* Center horizontally */ | |
align-items: center; /* Center vertically */ | |
overflow: hidden; /* Ensure no overflow */ | |
} | |
/* Styling for the score displays */ | |
#score-left, #score-right { | |
position: absolute; | |
top: 20px; /* Distance from the top */ | |
color: white; /* White text color */ | |
font-size: 2.2em; /* Larger font size */ | |
font-weight: bold; /* Bold text */ | |
text-shadow: 3px 3px 6px rgba(0,0,0,0.8); /* Stronger text shadow for readability */ | |
z-index: 10; /* Ensure it's above the canvas */ | |
padding: 10px 15px; /* Padding around text */ | |
background-color: rgba(0, 0, 0, 0.4); /* Semi-transparent background */ | |
border-radius: 12px; /* Rounded corners */ | |
box-shadow: 0 4px 10px rgba(0,0,0,0.5); /* Subtle box shadow */ | |
} | |
#score-left { | |
left: 30px; /* Position on the left */ | |
} | |
#score-right { | |
right: 30px; /* Position on the right */ | |
} | |
/* Styling for the health bars */ | |
#health-bar-left, #health-bar-right { | |
position: absolute; | |
top: 90px; /* Position below scores */ | |
height: 35px; /* Height of the health bar */ | |
background-color: green; /* Default healthy color */ | |
border: 3px solid #ffffff; /* White border */ | |
border-radius: 10px; /* Rounded corners */ | |
transition: width 0.3s ease-out, background-color 0.3s ease-out; /* Smooth transitions for width and color */ | |
z-index: 10; /* Ensure it's above the canvas */ | |
box-shadow: 0 4px 10px rgba(0,0,0,0.5); /* Subtle box shadow */ | |
} | |
#health-bar-left { | |
left: 50%; /* Start from the center */ | |
transform: translateX(-105%); /* Shift left to align to the left of center */ | |
width: 250px; /* Max width for health bar */ | |
max-width: 250px; /* Ensure it doesn't exceed this width */ | |
} | |
#health-bar-right { | |
right: 50%; /* Start from the center */ | |
transform: translateX(5%); /* Shift right to align to the right of center */ | |
width: 250px; /* Max width for health bar */ | |
max-width: 250px; /* Ensure it doesn't exceed this width */ | |
} | |
/* Styling for the reset button */ | |
#reset-button { | |
position: absolute; | |
bottom: 40px; /* Distance from the bottom */ | |
left: 50%; /* Center horizontally */ | |
transform: translateX(-50%); /* Adjust for true centering */ | |
padding: 18px 35px; /* Generous padding */ | |
font-size: 1.8em; /* Larger font size */ | |
background: linear-gradient(145deg, #ff6b6b, #ee4444); /* Gradient background */ | |
color: white; /* White text */ | |
border: none; /* No border */ | |
border-radius: 15px; /* More rounded corners */ | |
cursor: pointer; /* Pointer cursor on hover */ | |
box-shadow: 0 8px 20px rgba(0,0,0,0.6); /* Stronger shadow */ | |
transition: background 0.3s ease, transform 0.1s ease, box-shadow 0.3s ease; /* Smooth transitions */ | |
z-index: 10; /* Ensure it's above the canvas */ | |
font-weight: bold; /* Bold text */ | |
letter-spacing: 1px; /* Slight letter spacing */ | |
text-transform: uppercase; /* Uppercase text */ | |
} | |
#reset-button:hover { | |
background: linear-gradient(145deg, #ff4d4d, #cc3333); /* Darker gradient on hover */ | |
transform: translateX(-50%) scale(1.05); /* Slightly enlarge on hover */ | |
box-shadow: 0 10px 25px rgba(0,0,0,0.8); /* Deeper shadow on hover */ | |
} | |
#reset-button:active { | |
transform: translateX(-50%) scale(0.98); /* Shrink slightly on click */ | |
box-shadow: 0 4px 10px rgba(0,0,0,0.4); /* Recessed shadow on click */ | |
} | |
/* Styling for the Three.js canvas */ | |
canvas { | |
display: block; /* Remove extra space below canvas */ | |
width: 100%; /* Make canvas fill its container */ | |
height: 100%; /* Make canvas fill its container */ | |
border-radius: 15px; /* Rounded corners for the canvas itself */ | |
box-shadow: 0 0 25px rgba(0,0,0,0.7); /* Shadow around the canvas */ | |
} | |
/* Controls display */ | |
#controls { | |
position: absolute; | |
bottom: 120px; /* Position above the reset button */ | |
left: 50%; | |
transform: translateX(-50%); | |
background-color: rgba(0, 0, 0, 0.6); | |
color: white; | |
padding: 15px 25px; | |
border-radius: 12px; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.7); | |
font-size: 1.1em; | |
text-align: center; | |
z-index: 10; | |
display: flex; | |
gap: 40px; /* Space between player control sections */ | |
} | |
#controls h3 { | |
margin-top: 0; | |
color: #ffd700; /* Gold color for headings */ | |
font-size: 1.3em; | |
margin-bottom: 10px; | |
} | |
#controls ul { | |
list-style: none; | |
padding: 0; | |
margin: 0; | |
text-align: left; | |
} | |
#controls li { | |
margin-bottom: 5px; | |
} | |
#controls span { | |
font-weight: bold; | |
color: #aaffaa; /* Light green for keys */ | |
display: inline-block; | |
width: 40px; /* Fixed width for key display */ | |
} | |
</style> | |
</head> | |
<body> | |
<div id="game-container"> | |
<div id="score-left">P1 Score: 0</div> | |
<div id="score-right">P2 Score: 0</div> | |
<div id="health-bar-left"></div> | |
<div id="health-bar-right"></div> | |
<button id="reset-button">Primitive Reset!</button> | |
<div id="controls"> | |
<div> | |
<h3>Player 1 Controls</h3> | |
<ul> | |
<li><span>WASD:</span> Move</li> | |
<li><span>E:</span> Attack</li> | |
<li><span>Q:</span> Block</li> | |
<li><span>C:</span> Change Gear</li> | |
</ul> | |
</div> | |
<div> | |
<h3>Player 2 Controls</h3> | |
<ul> | |
<li><span>IJKL:</span> Move</li> | |
<li><span>U:</span> Attack</li> | |
<li><span>O:</span> Block</li> | |
<li><span>M:</span> Change Gear</li> | |
</ul> | |
</div> | |
</div> | |
</div> | |
<script type="module"> | |
// Import Three.js library from a reliable CDN | |
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js'; | |
// Declare global variables for Three.js scene, camera, renderer, and game state | |
let scene, camera, renderer; | |
let player1, player2; // Our primitive-assembled characters | |
let player1Health = 100; | |
let player2Health = 100; | |
let player1Score = 0; | |
let player2Score = 0; | |
const maxHealth = 100; // Define max health for players | |
const movementSpeed = 0.3; // Adjust movement speed for players | |
const baseAttackDamage = 10; // Base damage dealt by an attack | |
const blockReduction = 0.5; // Percentage of damage reduced when blocking (e.g., 0.5 means 50% less damage) | |
const playerScaleFactor = 1.5; // Scale factor for players | |
const attackRange = 2 * (1.5 * playerScaleFactor); // About 2 player widths | |
// Projectile parameters | |
const projectileSpeed = 0.8; | |
const projectileLife = 120; // frames (2 seconds at 60fps) | |
const projectileRadius = 0.3; | |
// Object to keep track of pressed keys for smooth movement | |
const keys = { | |
w: false, a: false, s: false, d: false, | |
i: false, j: false, k: false, l: false, | |
p1_attack: false, p1_block: false, p1_change_gear: false, | |
p2_attack: false, p2_block: false, p2_change_gear: false | |
}; | |
// Weapon and Shield Definitions for DnD-like classes | |
const gearCombinations = [ | |
{ name: "Fighter (Sword & Kite Shield)", weapon: "sword", shield: "kite_shield" }, | |
{ name: "Barbarian (Great Axe)", weapon: "great_axe", shield: null }, | |
{ name: "Rogue (Daggers & Buckler)", weapon: "dagger", shield: "buckler" }, | |
{ name: "Paladin (Mace & Tower Shield)", weapon: "mace", shield: "tower_shield" }, | |
{ name: "Ranger (Longbow)", weapon: "longbow", shield: null } | |
]; | |
let player1GearIndex = 0; | |
let player2GearIndex = 0; | |
const activeProjectiles = []; | |
const impactParticlesGroup = new THREE.Group(); | |
const weaponRangeParticlesGroup = new THREE.Group(); // New group for weapon range particles | |
/** | |
* Generates a random integer within a specified range (inclusive). | |
* @param {number} min - The minimum value. | |
* @param {number} max - The maximum value. | |
* @returns {number} A random integer. | |
*/ | |
function getRandomInt(min, max) { | |
min = Math.ceil(min); | |
max = Math.floor(max); | |
return Math.floor(Math.random() * (max - min + 1)) + min; | |
} | |
/** | |
* Initializes the game environment, including the Three.js scene, camera, renderer, | |
* 3D objects (players, ground), UI elements, and event listeners. | |
*/ | |
function init() { | |
// Scene Setup: The container for all 3D objects, lights, and cameras | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x333333); // Dark grey background for the 3D scene | |
// Camera Setup: Orthographic camera for a top-down semi-isometric view | |
const aspectRatio = window.innerWidth / window.innerHeight; | |
const frustumSize = 30; // Adjusted frustum size for a slightly more zoomed in view | |
camera = new THREE.OrthographicCamera( | |
frustumSize * aspectRatio / - 2, | |
frustumSize * aspectRatio / 2, | |
frustumSize / 2, | |
frustumSize / - 2, | |
1, 1000 | |
); | |
// Position camera for a top-down semi-isometric view | |
camera.position.set(10, 30, 10); // Higher Y, and equal X/Z for a balanced angle | |
camera.lookAt(0, 0, 0); // Make the camera look directly at the origin | |
// Renderer Setup: Renders the 3D scene onto a 2D canvas | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.setPixelRatio(window.devicePixelRatio); | |
document.getElementById('game-container').appendChild(renderer.domElement); | |
// Lighting: Essential for seeing 3D objects | |
const ambientLight = new THREE.AmbientLight(0x404040); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(5, 10, 7); | |
scene.add(directionalLight); | |
// Ground/Arena: A simple plane representing the fighting stage | |
const groundGeometry = new THREE.PlaneGeometry(50, 50); // Larger ground for larger players | |
const groundMaterial = new THREE.MeshPhongMaterial({ color: 0x666666, side: THREE.DoubleSide }); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
scene.add(ground); | |
// Player 1 Character: Created using primitive shapes | |
player1 = createPrimitiveCharacter(0xff4500); // Red-orange color | |
player1.position.set(-10, 0.5 * playerScaleFactor, 0); // Start on the left side of the arena, adjusted for size | |
scene.add(player1); | |
// Player 2 Character: Created using primitive shapes | |
player2 = createPrimitiveCharacter(0x00aaff); // Blue color | |
player2.position.set(10, 0.5 * playerScaleFactor, 0); // Start on the right side of the arena, adjusted for size | |
scene.add(player2); | |
// Initialize players with their starting gear | |
updatePlayerGear(player1, player1GearIndex); | |
updatePlayerGear(player2, player2GearIndex); | |
// Add particle groups to the scene | |
scene.add(impactParticlesGroup); | |
scene.add(weaponRangeParticlesGroup); | |
// UI Elements Initialization and Event Listeners | |
document.getElementById('reset-button').addEventListener('click', resetGame); | |
updateHealthBars(); | |
updateScores(); | |
// Event Listeners for Keyboard Input | |
window.addEventListener('keydown', onKeyDown); | |
window.addEventListener('keyup', onKeyUp); | |
window.addEventListener('resize', onWindowResize); | |
// Start the animation loop | |
animate(); | |
} | |
/** | |
* Creates a "primitive assembled" character with body, head, arms, and legs using Three.js primitives. | |
* Each part is given a name and damage value for collision detection and health reduction. | |
* @param {number} color - Hex color for the character. | |
* @returns {THREE.Group} A group representing the player character. | |
*/ | |
function createPrimitiveCharacter(color) { | |
const character = new THREE.Group(); | |
character.parts = []; // Array to hold all individual body parts | |
character.missingParts = []; // Array to track removed parts | |
character.playerColor = color; // Store the player's main color | |
// Body: A central box | |
const bodyGeometry = new THREE.BoxGeometry(1.5 * playerScaleFactor, 2.5 * playerScaleFactor, 1.5 * playerScaleFactor); | |
const bodyMaterial = new THREE.MeshPhongMaterial({ color: color }); | |
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
body.position.y = 1.25 * playerScaleFactor; | |
body.name = 'body'; | |
body.damageValue = baseAttackDamage * 1.5; // Body hits deal more damage | |
character.add(body); | |
character.parts.push(body); | |
// Head: A sphere on top of the body | |
const headGeometry = new THREE.SphereGeometry(0.8 * playerScaleFactor, 24, 24); | |
const headMaterial = new THREE.MeshPhongMaterial({ color: color }); | |
const head = new THREE.Mesh(headGeometry, headMaterial); | |
head.position.y = 3.2 * playerScaleFactor; | |
head.name = 'head'; | |
head.damageValue = baseAttackDamage * 3; // Headshots deal critical damage | |
character.add(head); | |
character.parts.push(head); | |
const visorGeometry = new THREE.BoxGeometry(0.6 * playerScaleFactor, 0.2 * playerScaleFactor, 0.1 * playerScaleFactor); | |
const visorMaterial = new THREE.MeshPhongMaterial({ color: 0x333333 }); | |
const visor = new THREE.Mesh(visorGeometry, visorMaterial); | |
visor.position.set(0, 1.8 * playerScaleFactor, 0.5 * playerScaleFactor); | |
visor.name = 'visor'; // Not a critical part, no damageValue | |
character.add(visor); | |
character.parts.push(visor); | |
// Arms: Two cylinders for arms | |
const upperArmGeometry = new THREE.CylinderGeometry(0.3 * playerScaleFactor, 0.3 * playerScaleFactor, 0.7 * playerScaleFactor, 8); | |
const lowerArmGeometry = new THREE.CylinderGeometry(0.28 * playerScaleFactor, 0.28 * playerScaleFactor, 0.7 * playerScaleFactor, 8); | |
const armMaterial = new THREE.MeshPhongMaterial({ color: color }); | |
// Player's right arm (for weapon) | |
const rightArm = new THREE.Mesh(upperArmGeometry, armMaterial); | |
rightArm.position.set(-1.2 * playerScaleFactor, 2.2 * playerScaleFactor, 0); | |
rightArm.rotation.z = (color === 0xff4500) ? Math.PI / 6 : -Math.PI / 6; | |
rightArm.rotation.x = 0; | |
rightArm.name = 'rightArm'; | |
rightArm.damageValue = baseAttackDamage * 0.8; | |
character.add(rightArm); | |
character.parts.push(rightArm); | |
character.rightArm = rightArm; // Store reference for animation | |
character.rightArmInitialRotZ = rightArm.rotation.z; | |
character.rightArmInitialRotX = rightArm.rotation.x; | |
const rightLowerArm = new THREE.Mesh(lowerArmGeometry, armMaterial); | |
rightLowerArm.position.set(-1.7 * playerScaleFactor, 1.8 * playerScaleFactor, 0); | |
rightLowerArm.rotation.z = Math.PI / 8; | |
rightLowerArm.name = 'rightLowerArm'; | |
rightLowerArm.damageValue = baseAttackDamage * 0.8; | |
character.add(rightLowerArm); | |
character.parts.push(rightLowerArm); | |
// Player's left arm (for shield) | |
const leftArm = new THREE.Mesh(upperArmGeometry, armMaterial); | |
leftArm.position.set(1.2 * playerScaleFactor, 2.2 * playerScaleFactor, 0); | |
leftArm.rotation.z = (color === 0xff4500) ? -Math.PI / 6 : Math.PI / 6; | |
leftArm.rotation.x = 0; | |
leftArm.rotation.y = 0; | |
leftArm.name = 'leftArm'; | |
leftArm.damageValue = baseAttackDamage * 0.8; | |
character.add(leftArm); | |
character.parts.push(leftArm); | |
character.leftArm = leftArm; // Store reference for animation | |
character.leftArmInitialRotZ = leftArm.rotation.z; | |
character.leftArmInitialRotX = leftArm.rotation.x; | |
character.leftArmInitialRotY = leftArm.rotation.y; | |
const leftLowerArm = new THREE.Mesh(lowerArmGeometry, armMaterial); | |
leftLowerArm.position.set(1.7 * playerScaleFactor, 1.8 * playerScaleFactor, 0); | |
leftLowerArm.rotation.z = -Math.PI / 8; | |
leftLowerArm.name = 'leftLowerArm'; | |
leftLowerArm.damageValue = baseAttackDamage * 0.8; | |
character.add(leftLowerArm); | |
character.parts.push(leftLowerArm); | |
// Legs: Two cylinders for legs | |
const upperLegGeometry = new THREE.CylinderGeometry(0.25 * playerScaleFactor, 0.25 * playerScaleFactor, 0.8 * playerScaleFactor, 8); | |
const lowerLegGeometry = new THREE.CylinderGeometry(0.2 * playerScaleFactor, 0.2 * playerScaleFactor, 0.8 * playerScaleFactor, 8); | |
const legMaterial = new THREE.MeshPhongMaterial({ color: 0x777777 }); | |
const leg1 = new THREE.Mesh(upperLegGeometry, legMaterial); | |
leg1.position.set(-0.3 * playerScaleFactor, 0.4 * playerScaleFactor, 0); | |
leg1.name = 'leftUpperLeg'; | |
leg1.damageValue = baseAttackDamage * 0.5; | |
character.add(leg1); | |
character.parts.push(leg1); | |
const leg2 = new THREE.Mesh(lowerLegGeometry, legMaterial); | |
leg2.position.set(-0.3 * playerScaleFactor, -0.4 * playerScaleFactor, 0); | |
leg2.name = 'leftLowerLeg'; | |
leg2.damageValue = baseAttackDamage * 0.5; | |
character.add(leg2); | |
character.parts.push(leg2); | |
const leg3 = new THREE.Mesh(upperLegGeometry, legMaterial); | |
leg3.position.set(0.3 * playerScaleFactor, 0.4 * playerScaleFactor, 0); | |
leg3.name = 'rightUpperLeg'; | |
leg3.damageValue = baseAttackDamage * 0.5; | |
character.add(leg3); | |
character.parts.push(leg3); | |
const leg4 = new THREE.Mesh(lowerLegGeometry, legMaterial); | |
leg4.position.set(0.3 * playerScaleFactor, -0.4 * playerScaleFactor, 0); | |
leg4.name = 'rightLowerLeg'; | |
leg4.damageValue = baseAttackDamage * 0.5; | |
character.add(leg4); | |
character.parts.push(leg4); | |
// Backpack (Box) | |
const backpackGeometry = new THREE.BoxGeometry(0.8 * playerScaleFactor, 1 * playerScaleFactor, 0.4 * playerScaleFactor); | |
const backpackMaterial = new THREE.MeshPhongMaterial({ color: 0x444444 }); | |
const backpack = new THREE.Mesh(backpackGeometry, backpackMaterial); | |
backpack.position.set(0, 0.75 * playerScaleFactor, -0.7 * playerScaleFactor); | |
backpack.name = 'backpack'; // Not a critical part | |
character.add(backpack); | |
character.parts.push(backpack); | |
// Store initial positions and rotations for resetting | |
character.initialState = { | |
position: character.position.clone(), | |
parts: character.parts.map(part => ({ | |
name: part.name, | |
position: part.position.clone(), | |
rotation: part.rotation.clone(), | |
material: part.material // Store material reference | |
})) | |
}; | |
// Animation properties | |
character.isAttacking = false; | |
character.attackAnimationProgress = 0; | |
character.attackDuration = 15; // frames | |
character.isBlocking = false; | |
character.blockAnimationProgress = 0; | |
character.blockDuration = 10; // frames | |
// Store current weapon type for animation logic | |
character.currentWeaponType = null; | |
character.weaponMesh = null; // Reference to the actual weapon mesh | |
character.shieldMesh = null; // Reference to the actual shield mesh | |
return character; | |
} | |
/** | |
* Creates a weapon mesh based on the specified type. | |
* Includes a collision sphere for the weapon. | |
* @param {string} type - The type of weapon (e.g., "sword", "longbow"). | |
* @param {number} [color=0xcccccc] - Hex color for the weapon. | |
* @returns {THREE.Group} A group representing the weapon. | |
*/ | |
function createWeapon(type, color = 0xcccccc) { | |
const weaponGroup = new THREE.Group(); | |
const material = new THREE.MeshPhongMaterial({ color: color }); | |
const handleMaterial = new THREE.MeshPhongMaterial({ color: 0x663300 }); | |
const bladeMaterial = new THREE.MeshPhongMaterial({ color: 0x999999 }); | |
// Collision sphere for the weapon (relative to its local origin) | |
weaponGroup.collisionSphere = new THREE.Sphere(new THREE.Vector3(), 1.0); | |
switch (type) { | |
case "sword": | |
weaponGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.2, 2.0, 0.1), bladeMaterial)); // Blade | |
const hiltMesh = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.15, 0.5), handleMaterial); // Hilt | |
hiltMesh.position.y = -1.25; | |
weaponGroup.add(hiltMesh); | |
weaponGroup.position.set(0.5, -0.5, 0); | |
weaponGroup.rotation.z = -Math.PI / 2; | |
weaponGroup.collisionSphere.radius = 1.2; // Adjust collision sphere size | |
break; | |
case "great_axe": | |
weaponGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.2, 3.0, 0.2), handleMaterial)); // Handle | |
const axeHead = new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.5, 1.0), bladeMaterial); // Axe blade | |
axeHead.position.set(0, 1.5, 0.5); | |
weaponGroup.add(axeHead); | |
weaponGroup.position.set(0.5, -1.0, 0); | |
weaponGroup.rotation.z = -Math.PI / 2; | |
weaponGroup.collisionSphere.radius = 1.8; | |
break; | |
case "dagger": | |
weaponGroup.add(new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.8, 0.05), bladeMaterial)); // Blade | |
const daggerHiltMesh = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 0.3), handleMaterial); // Hilt | |
daggerHiltMesh.position.y = -0.55; | |
weaponGroup.add(daggerHiltMesh); | |
weaponGroup.position.set(0.3, -0.2, 0); | |
weaponGroup.rotation.z = -Math.PI / 2; | |
weaponGroup.collisionSphere.radius = 0.8; | |
break; | |
case "mace": | |
weaponGroup.add(new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.2, 1.5), handleMaterial)); // Handle | |
const maceHead = new THREE.Mesh(new THREE.SphereGeometry(0.5, 16, 16), bladeMaterial); // Spiked head | |
maceHead.position.y = 0.75; | |
weaponGroup.add(maceHead); | |
weaponGroup.position.set(0.5, -0.75, 0); | |
weaponGroup.rotation.z = -Math.PI / 2; | |
weaponGroup.collisionSphere.radius = 1.0; | |
break; | |
case "longbow": | |
const bowString = new THREE.Mesh(new THREE.CylinderGeometry(0.02, 0.02, 2.0), new THREE.MeshPhongMaterial({ color: 0x333333 })); | |
const bowBody = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.0, 0.1), new THREE.MeshPhongMaterial({ color: 0x8B4513 })); | |
bowString.position.x = 0.5; | |
bowBody.rotation.y = Math.PI / 2; | |
weaponGroup.add(bowBody); | |
weaponGroup.add(bowString); | |
weaponGroup.position.set(0.5, -0.5, 0); | |
weaponGroup.rotation.z = -Math.PI / 2; | |
weaponGroup.collisionSphere.radius = 1.5; | |
break; | |
default: | |
break; | |
} | |
weaponGroup.name = `weapon_${type}`; | |
weaponGroup.visible = false; | |
return weaponGroup; | |
} | |
/** | |
* Creates a shield mesh based on the specified type. | |
* Includes a collision sphere for the shield. | |
* @param {string} type - The type of shield (e.g., "kite_shield", "buckler"). | |
* @param {number} [color=0x888888] - Hex color for the shield. | |
* @returns {THREE.Group} A group representing the shield. | |
*/ | |
function createShield(type, color = 0x888888) { | |
const shieldGroup = new THREE.Group(); | |
const material = new THREE.MeshPhongMaterial({ color: color }); | |
shieldGroup.collisionSphere = new THREE.Sphere(new THREE.Vector3(), 1.5); // Default collision sphere | |
switch (type) { | |
case "kite_shield": | |
const pointsKite = [ | |
new THREE.Vector2(0, 1.5), new THREE.Vector2(0.8, 1.0), | |
new THREE.Vector2(0.8, -1.5), new THREE.Vector2(-0.8, -1.5), | |
new THREE.Vector2(-0.8, 1.0) | |
]; | |
const kiteShape = new THREE.Shape(pointsKite); | |
const shieldShapeKite = new THREE.ExtrudeGeometry(kiteShape, { depth: 0.1, bevelEnabled: false }); | |
shieldGroup.add(new THREE.Mesh(shieldShapeKite, material)); | |
shieldGroup.position.set(-0.5, -0.5, 0); | |
shieldGroup.rotation.y = Math.PI / 2; | |
shieldGroup.collisionSphere.radius = 1.8; // Adjust collision sphere size | |
break; | |
case "buckler": | |
const shieldShapeBuckler = new THREE.CylinderGeometry(0.7, 0.7, 0.1, 16); | |
shieldGroup.add(new THREE.Mesh(shieldShapeBuckler, material)); | |
shieldGroup.position.set(-0.5, -0.2, 0); | |
shieldGroup.rotation.y = Math.PI / 2; | |
shieldGroup.collisionSphere.radius = 0.9; | |
break; | |
case "tower_shield": | |
const shieldShapeTower = new THREE.BoxGeometry(1.2, 2.5, 0.1); | |
shieldGroup.add(new THREE.Mesh(shieldShapeTower, material)); | |
shieldGroup.position.set(-0.5, -1.0, 0); | |
shieldGroup.rotation.y = Math.PI / 2; | |
shieldGroup.collisionSphere.radius = 2.0; | |
break; | |
default: | |
break; | |
} | |
shieldGroup.name = `shield_${type}`; | |
shieldGroup.visible = false; | |
return shieldGroup; | |
} | |
/** | |
* Creates a projectile (a small sphere) to be fired. | |
* @param {THREE.Group} firingPlayer - The player who fired the projectile. | |
* @param {THREE.Group} targetPlayer - The intended target of the projectile. | |
*/ | |
function createProjectile(firingPlayer, targetPlayer) { | |
const projectileGeometry = new THREE.SphereGeometry(projectileRadius, 8, 8); | |
const projectileMaterial = new THREE.MeshPhongMaterial({ color: 0xffd700 }); // Gold color | |
const projectileMesh = new THREE.Mesh(projectileGeometry, projectileMaterial); | |
// Set projectile starting position from player's weapon tip or chest height | |
const startPosition = new THREE.Vector3(); | |
if (firingPlayer.weaponMesh) { | |
// Get weapon's world position and move it slightly forward | |
firingPlayer.weaponMesh.getWorldPosition(startPosition); | |
const forwardDir = new THREE.Vector3(); | |
firingPlayer.getWorldDirection(forwardDir); | |
startPosition.add(forwardDir.multiplyScalar(firingPlayer.weaponMesh.collisionSphere.radius * 0.5)); | |
} else { | |
// If no weapon, shoot from player's chest height | |
startPosition.copy(firingPlayer.position); | |
startPosition.y += 1.5 * playerScaleFactor; | |
} | |
projectileMesh.position.copy(startPosition); | |
// Calculate direction towards target (at target's chest height) | |
const targetPosition = targetPlayer.position.clone(); | |
targetPosition.y += 1.5 * playerScaleFactor; // Aim for chest height | |
const direction = new THREE.Vector3(); | |
direction.subVectors(targetPosition, projectileMesh.position).normalize(); | |
projectileMesh.velocity = direction.multiplyScalar(projectileSpeed); | |
projectileMesh.life = projectileLife; | |
projectileMesh.damage = baseAttackDamage; // Projectiles deal base damage | |
projectileMesh.originPlayer = firingPlayer; // Store who fired it | |
scene.add(projectileMesh); | |
activeProjectiles.push(projectileMesh); | |
} | |
/** | |
* Updates a player's equipped weapon and shield based on the gear index. | |
* @param {THREE.Group} player - The player character to update. | |
* @param {number} gearIndex - The index of the gear combination to equip. | |
*/ | |
function updatePlayerGear(player, gearIndex) { | |
// Remove existing weapon and shield meshes from the player's arms | |
if (player.weaponMesh && player.weaponMesh.parent) { | |
player.weaponMesh.parent.remove(player.weaponMesh); | |
} | |
if (player.shieldMesh && player.shieldMesh.parent) { | |
player.shieldMesh.parent.remove(player.shieldMesh); | |
} | |
const gear = gearCombinations[gearIndex]; | |
player.currentWeaponType = gear.weapon; // Store the current weapon type for animation logic | |
// Add new weapon if applicable | |
if (gear.weapon) { | |
const newWeapon = createWeapon(gear.weapon); | |
player.rightArm.add(newWeapon); // Attach to the player's right arm | |
player.weaponMesh = newWeapon; // Store reference | |
newWeapon.visible = true; | |
} else { | |
player.weaponMesh = null; | |
} | |
// Add new shield if applicable | |
if (gear.shield) { | |
const newShield = createShield(gear.shield); | |
player.leftArm.add(newShield); // Attach to the player's left arm | |
player.shieldMesh = newShield; // Store reference | |
newShield.visible = true; | |
} else { | |
player.shieldMesh = null; | |
} | |
} | |
/** | |
* Handles keydown events to update player movement and action states. | |
* @param {KeyboardEvent} event - The keyboard event. | |
*/ | |
function onKeyDown(event) { | |
switch (event.code) { | |
// Player 1 Movement (WASD) | |
case 'KeyW': keys.w = true; break; | |
case 'KeyA': keys.a = true; break; | |
case 'KeyS': keys.s = true; break; | |
case 'KeyD': keys.d = true; break; | |
// Player 1 Actions | |
case 'KeyE': // Attack | |
// Only allow attack if not already attacking or blocking | |
if (!player1.isAttacking && !player1.isBlocking) { | |
startAttackAnimation(player1); | |
createProjectile(player1, player2); // All attacks now fire projectiles | |
} | |
break; | |
case 'KeyQ': // Block | |
// Only allow block if not already blocking or attacking | |
if (!player1.isBlocking && !player1.isAttacking) { | |
startBlockAnimation(player1); | |
} | |
break; | |
case 'KeyC': // Change Gear | |
if (!keys.p1_change_gear) { // Prevent rapid gear changes | |
player1GearIndex = (player1GearIndex + 1) % gearCombinations.length; | |
updatePlayerGear(player1, player1GearIndex); | |
keys.p1_change_gear = true; // Set flag to prevent continuous change | |
} | |
break; | |
// Player 2 Movement (IJKL) | |
case 'KeyI': keys.i = true; break; | |
case 'KeyJ': keys.j = true; break; | |
case 'KeyK': keys.k = true; break; | |
case 'KeyL': keys.l = true; break; | |
// Player 2 Actions | |
case 'KeyU': // Attack | |
// Only allow attack if not already attacking or blocking | |
if (!player2.isAttacking && !player2.isBlocking) { | |
startAttackAnimation(player2); | |
createProjectile(player2, player1); // All attacks now fire projectiles | |
} | |
break; | |
case 'KeyO': // Block | |
// Only allow block if not already blocking or attacking | |
if (!player2.isBlocking && !player2.isAttacking) { | |
startBlockAnimation(player2); | |
} | |
break; | |
case 'KeyM': // Change Gear | |
if (!keys.p2_change_gear) { // Prevent rapid gear changes | |
player2GearIndex = (player2GearIndex + 1) % gearCombinations.length; | |
updatePlayerGear(player2, player2GearIndex); | |
keys.p2_change_gear = true; // Set flag to prevent continuous change | |
} | |
break; | |
} | |
} | |
/** | |
* Handles keyup events to reset player movement and action states. | |
* @param {KeyboardEvent} event - The keyboard event. | |
*/ | |
function onKeyUp(event) { | |
switch (event.code) { | |
// Player 1 Movement | |
case 'KeyW': keys.w = false; break; | |
case 'KeyA': keys.a = false; break; | |
case 'KeyS': keys.s = false; break; | |
case 'KeyD': keys.d = false; break; | |
// Player 1 Actions (resetting the "pressed" state for single-trigger actions) | |
case 'KeyE': /* Attack action is handled by animation state */ break; | |
case 'KeyQ': /* Block action is handled by animation state */ break; | |
case 'KeyC': keys.p1_change_gear = false; break; // Release for next change | |
// Player 2 Movement | |
case 'KeyI': keys.i = false; break; | |
case 'KeyJ': keys.j = false; break; | |
case 'KeyK': keys.k = false; break; | |
case 'KeyL': keys.l = false; break; | |
// Player 2 Actions | |
case 'KeyU': /* Attack action is handled by animation state */ break; | |
case 'KeyO': /* Block action is handled by animation state */ break; | |
case 'KeyM': keys.p2_change_gear = false; break; // Release for next change | |
} | |
} | |
/** | |
* Initiates the attack animation for a given player. | |
* @param {THREE.Group} player - The player initiating the attack. | |
*/ | |
function startAttackAnimation(player) { | |
player.isAttacking = true; | |
player.attackAnimationProgress = 0; // Reset animation progress | |
// Store initial arm rotation for smooth return | |
player.rightArm.initialRotationZ = player.rightArm.rotation.z; | |
player.rightArm.initialRotationX = player.rightArm.rotation.x; | |
// Create weapon range particles using the player's stored color | |
createWeaponRangeParticles(player.weaponMesh ? player.weaponMesh : player.position, player.playerColor); | |
} | |
/** | |
* Initiates the block animation for a given player. | |
* @param {THREE.Group} player - The player initiating the block. | |
*/ | |
function startBlockAnimation(player) { | |
player.isBlocking = true; | |
player.blockAnimationProgress = 0; // Reset animation progress | |
// Store initial arm rotation for smooth return | |
player.leftArm.initialRotationZ = player.leftArm.rotation.z; | |
player.leftArm.initialRotationX = player.leftArm.rotation.x; | |
player.leftArm.initialRotationY = player.leftArm.rotation.y; | |
} | |
/** | |
* Creates a burst of small particles at a given position and color. | |
* Used for visual feedback on hits/blocks. | |
* @param {THREE.Vector3} position - The world position for particles. | |
* @param {number} color - Hex color for the particles. | |
* @param {string} type - Type of particles ('impact' or 'blood'). | |
*/ | |
function createParticles(position, color, type = 'impact') { | |
const particleCount = (type === 'blood') ? 20 : 10; | |
const particleColor = (type === 'blood') ? 0x8B0000 : color; // Dark red for blood | |
const particleMaterial = new THREE.MeshBasicMaterial({ color: particleColor, transparent: true, opacity: 1 }); | |
const particleGeometry = new THREE.SphereGeometry(0.1, 8, 8); // Smaller for blood | |
for (let i = 0; i < particleCount; i++) { | |
const particle = new THREE.Mesh(particleGeometry, particleMaterial.clone()); | |
particle.position.copy(position); | |
particle.velocity = new THREE.Vector3( | |
(Math.random() - 0.5) * 0.8, // Random X velocity | |
Math.random() * 0.8 + 0.2, // Upward Y velocity | |
(Math.random() - 0.5) * 0.8 // Random Z velocity | |
); | |
particle.life = (type === 'blood') ? 60 : 40; // Longer life for blood | |
particle.initialLife = particle.life; // Store initial life for opacity calculation | |
impactParticlesGroup.add(particle); | |
} | |
} | |
/** | |
* Creates a temporary particle system to visualize weapon range/attack. | |
* @param {THREE.Object3D} originObject - The object from which particles emanate (weapon or player). | |
* @param {number} color - The hex color of the particles. | |
*/ | |
function createWeaponRangeParticles(originObject, color) { | |
const particleCount = 15; | |
const particleMaterial = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.8 }); | |
const particleGeometry = new THREE.SphereGeometry(0.15, 8, 8); | |
const originPosition = new THREE.Vector3(); | |
if (originObject instanceof THREE.Mesh || originObject instanceof THREE.Group) { // Check if it's a Three.js object | |
originObject.getWorldPosition(originPosition); | |
} else { // Assume it's a Vector3 if not a mesh/group | |
originPosition.copy(originObject); | |
} | |
for (let i = 0; i < particleCount; i++) { | |
const particle = new THREE.Mesh(particleGeometry, particleMaterial.clone()); | |
particle.position.copy(originPosition); | |
// Randomize initial direction slightly | |
const angle = Math.random() * Math.PI * 2; | |
const speed = 0.1 + Math.random() * 0.1; | |
particle.velocity = new THREE.Vector3( | |
Math.cos(angle) * speed, | |
0.1 + Math.random() * 0.1, // Slight upward motion | |
Math.sin(angle) * speed | |
); | |
particle.life = 30; // Short life for range visualization | |
particle.initialLife = particle.life; // Store initial life for opacity calculation | |
weaponRangeParticlesGroup.add(particle); | |
} | |
} | |
/** | |
* The main animation loop of the game. | |
* Updates player movement, animations, projectile physics, and renders the scene. | |
*/ | |
function animate() { | |
requestAnimationFrame(animate); | |
// Player 1 Movement | |
if (keys.w) player1.position.z -= movementSpeed; | |
if (keys.s) player1.position.z += movementSpeed; | |
if (keys.a) player1.position.x -= movementSpeed; | |
if (keys.d) player1.position.x += movementSpeed; | |
// Player 2 Movement | |
if (keys.i) player2.position.z -= movementSpeed; | |
if (keys.k) player2.position.z += movementSpeed; | |
if (keys.j) player2.position.x -= movementSpeed; | |
if (keys.l) player2.position.x += movementSpeed; | |
// Auto-rotate players to face each other (only if not attacking/blocking to prevent jitter) | |
if (!player1.isAttacking && !player1.isBlocking) { | |
player1.lookAt(player2.position.x, player1.position.y, player2.position.z); | |
} | |
if (!player2.isAttacking && !player2.isBlocking) { | |
player2.lookAt(player1.position.x, player2.position.y, player1.position.z); | |
} | |
// Attack Animation Logic for both players | |
[player1, player2].forEach(player => { | |
if (player.isAttacking) { | |
player.attackAnimationProgress++; | |
const progress = player.attackAnimationProgress / player.attackDuration; | |
const swingDirection = (player === player1) ? 1 : -1; // Adjust swing direction for each player | |
// Animate weapon arm based on weapon type | |
switch (player.currentWeaponType) { | |
case "sword": | |
case "great_axe": | |
case "mace": | |
// Sweeping motion: Z-axis for horizontal swing, X-axis for vertical tilt | |
player.rightArm.rotation.z = player.rightArmInitialRotZ + swingDirection * Math.sin(progress * Math.PI * 1.5) * Math.PI / 2; | |
player.rightArm.rotation.x = player.rightArmInitialRotX + Math.sin(progress * Math.PI * 2) * Math.PI / 12; | |
break; | |
case "dagger": | |
// Forward jab: X-axis for forward thrust | |
player.rightArm.rotation.x = player.rightArmInitialRotX + Math.sin(progress * Math.PI * 2) * Math.PI / 6; | |
player.rightArm.rotation.z = player.rightArmInitialRotZ + swingDirection * Math.sin(progress * Math.PI * 2) * Math.PI / 12; // Slight side movement | |
break; | |
case "longbow": | |
// Draw back and release: X-axis for pulling back, then forward | |
player.rightArm.rotation.x = player.rightArmInitialRotX + Math.sin(progress * Math.PI * 2) * Math.PI / 4; | |
player.rightArm.rotation.z = player.rightArmInitialRotZ; // Keep Z stable | |
break; | |
} | |
// End attack animation | |
if (player.attackAnimationProgress >= player.attackDuration) { | |
player.isAttacking = false; | |
// Reset arm rotations to initial state | |
player.rightArm.rotation.z = player.rightArmInitialRotZ; | |
player.rightArm.rotation.x = player.rightArmInitialRotX; | |
} | |
} | |
// Block Animation Logic | |
if (player.isBlocking) { | |
player.blockAnimationProgress++; | |
const progress = player.blockAnimationProgress / player.blockDuration; | |
const blockDirection = (player === player1) ? 1 : -1; // Adjust block direction | |
// Move shield arm to a defensive position | |
player.leftArm.rotation.z = player.leftArmInitialRotZ + blockDirection * Math.sin(progress * Math.PI) * Math.PI / 8; // Arm moves up/down slightly | |
player.leftArm.rotation.y = player.leftArmInitialRotY + blockDirection * Math.sin(progress * Math.PI) * Math.PI / 6; // Shield turns towards opponent | |
// End block animation | |
if (player.blockAnimationProgress >= player.blockDuration) { | |
player.isBlocking = false; | |
// Reset arm rotations to initial state | |
player.leftArm.rotation.z = player.leftArmInitialRotZ; | |
player.leftArm.rotation.x = player.leftArmInitialRotX; | |
player.leftArm.rotation.y = player.leftArmInitialRotY; | |
} | |
} | |
// Health decay from missing parts | |
const healthDecayRate = player.missingParts.length * 0.1; // 0.1 health per frame per missing part | |
if (healthDecayRate > 0) { | |
if (player === player1) { | |
player1Health = Math.max(0, player1Health - healthDecayRate); | |
if (player1Health === 0) { | |
player2Score++; | |
updateScores(); | |
resetGame(); | |
} | |
} else { | |
player2Health = Math.max(0, player2Health - healthDecayRate); | |
if (player2Health === 0) { | |
player1Score++; | |
updateScores(); | |
resetGame(); | |
} | |
} | |
updateHealthBars(); | |
} | |
}); | |
// Projectile Movement and Collision | |
for (let i = activeProjectiles.length - 1; i >= 0; i--) { | |
const projectile = activeProjectiles[i]; | |
projectile.position.add(projectile.velocity); | |
projectile.life--; | |
const targetPlayer = (projectile.originPlayer === player1) ? player2 : player1; | |
// Check for collision with target player's *individual* body parts | |
const projectileSphere = new THREE.Sphere(projectile.position, projectileRadius); | |
let hitOccurred = false; | |
let hitPart = null; | |
// Loop through each part of the target player | |
for (let j = targetPlayer.parts.length - 1; j >= 0; j--) { | |
const part = targetPlayer.parts[j]; | |
// Calculate world position and collision sphere for the part | |
const partWorldPosition = new THREE.Vector3(); | |
part.getWorldPosition(partWorldPosition); | |
const partCollisionSphere = new THREE.Sphere(partWorldPosition, part.geometry.parameters.radius || part.geometry.parameters.width / 2 || 0.5); // Estimate radius from geometry | |
if (projectileSphere.intersectsSphere(partCollisionSphere)) { | |
// Check for shield block first | |
if (targetPlayer.isBlocking && targetPlayer.shieldMesh) { | |
const targetShield = targetPlayer.shieldMesh; | |
const shieldWorldPosition = new THREE.Vector3(); | |
targetShield.getWorldPosition(shieldWorldPosition); | |
targetShield.collisionSphere.center.copy(shieldWorldPosition); | |
if (projectileSphere.intersectsSphere(targetShield.collisionSphere)) { | |
// Block successful! | |
applyDamage(targetPlayer, projectile.damage * (1 - blockReduction), projectile.originPlayer); | |
createParticles(shieldWorldPosition, 0x00ff00, 'impact'); // Green for block | |
hitOccurred = true; | |
break; // Shield blocked, no need to check other parts | |
} | |
} | |
// If not blocked by shield, apply damage to the part | |
if (!hitOccurred) { | |
hitPart = part; | |
applyDamage(targetPlayer, part.damageValue || projectile.damage, projectile.originPlayer); // Use part's damage value if available | |
createParticles(partWorldPosition, 0xffa500, 'impact'); // Orange for hit | |
createParticles(partWorldPosition, 0xff0000, 'blood'); // Red blood particles | |
hitOccurred = true; | |
// Remove the hit part from the player model | |
targetPlayer.remove(part); | |
targetPlayer.parts.splice(j, 1); // Remove from active parts array | |
targetPlayer.missingParts.push(part.name); // Add to missing parts list | |
// Dispose of the removed part's geometry and material | |
if (part.geometry) part.geometry.dispose(); | |
if (part.material) part.material.dispose(); | |
break; // Only one part hit per projectile | |
} | |
} | |
} | |
// Remove projectile on impact or if it goes too far | |
if (hitOccurred || projectile.life <= 0) { | |
scene.remove(projectile); | |
projectile.geometry.dispose(); | |
projectile.material.dispose(); | |
activeProjectiles.splice(i, 1); | |
} | |
} | |
// Update impact particles (move and fade out) | |
for (let i = impactParticlesGroup.children.length - 1; i >= 0; i--) { | |
const particle = impactParticlesGroup.children[i]; | |
particle.position.add(particle.velocity); | |
particle.velocity.y -= 0.02; // Simple gravity | |
particle.life--; | |
particle.material.opacity = particle.life / particle.initialLife; // Fade out over lifetime (assuming initialLife was stored) | |
if (particle.life <= 0) { | |
impactParticlesGroup.remove(particle); | |
particle.geometry.dispose(); | |
particle.material.dispose(); | |
} | |
} | |
// Update weapon range particles (expand and fade out) | |
for (let i = weaponRangeParticlesGroup.children.length - 1; i >= 0; i--) { | |
const particle = weaponRangeParticlesGroup.children[i]; | |
particle.position.add(particle.velocity); | |
particle.scale.setScalar(particle.scale.x + 0.05); // Expand | |
particle.material.opacity -= 0.03; // Fade out | |
if (particle.material.opacity <= 0) { | |
weaponRangeParticlesGroup.remove(particle); | |
particle.geometry.dispose(); | |
particle.material.dispose(); | |
} | |
} | |
renderer.render(scene, camera); | |
} | |
/** | |
* Applies damage to a target player, considering if they are blocking. | |
* If a player's health drops to 0, the game resets and the other player scores. | |
* @param {THREE.Group} targetPlayer - The player receiving damage. | |
* @param {number} amount - The base amount of damage. | |
* @param {THREE.Group} attackingPlayer - The player dealing damage. | |
*/ | |
function applyDamage(targetPlayer, amount, attackingPlayer) { | |
let actualDamage = amount; | |
if (targetPlayer.isBlocking) { | |
actualDamage = amount * (1 - blockReduction); // Reduce damage if blocking | |
} | |
if (targetPlayer === player1) { | |
player1Health = Math.max(0, player1Health - actualDamage); | |
if (player1Health === 0) { | |
player2Score++; | |
updateScores(); | |
resetGame(); | |
} | |
} else if (targetPlayer === player2) { | |
player2Health = Math.max(0, player2Health - actualDamage); | |
if (player2Health === 0) { | |
player1Score++; | |
updateScores(); | |
resetGame(); | |
} | |
} | |
updateHealthBars(); | |
} | |
/** | |
* Updates the visual width and color of the player health bars. | |
*/ | |
function updateHealthBars() { | |
const healthBarLeft = document.getElementById('health-bar-left'); | |
const healthBarRight = document.getElementById('health-bar-right'); | |
const maxWidth = 250; | |
healthBarLeft.style.width = `${(player1Health / maxHealth) * maxWidth}px`; | |
healthBarLeft.style.backgroundColor = player1Health > maxHealth * 0.6 ? '#28a745' : (player1Health > maxHealth * 0.3 ? '#ffc107' : '#dc3545'); | |
healthBarRight.style.width = `${(player2Health / maxHealth) * maxWidth}px`; | |
healthBarRight.style.backgroundColor = player2Health > maxHealth * 0.6 ? '#28a745' : (player2Health > maxHealth * 0.3 ? '#ffc107' : '#dc3545'); | |
} | |
/** | |
* Updates the displayed scores for Player 1 and Player 2. | |
*/ | |
function updateScores() { | |
document.getElementById('score-left').textContent = `P1 Score: ${player1Score}`; | |
document.getElementById('score-right').textContent = `P2 Score: ${player2Score}`; | |
} | |
/** | |
* Resets the game to its initial state, including player health, positions, | |
* and clearing all projectiles and particles. Also rebuilds player models. | |
*/ | |
function resetGame() { | |
player1Health = maxHealth; | |
player2Health = maxHealth; | |
updateHealthBars(); | |
// Rebuild players to restore missing parts | |
scene.remove(player1); | |
scene.remove(player2); | |
player1 = createPrimitiveCharacter(0xff4500); | |
player2 = createPrimitiveCharacter(0x00aaff); | |
scene.add(player1); | |
scene.add(player2); | |
player1.position.set(-10, 0.5 * playerScaleFactor, 0); | |
player2.position.set(10, 0.5 * playerScaleFactor, 0); | |
// Re-initialize gear | |
updatePlayerGear(player1, player1GearIndex); | |
updatePlayerGear(player2, player2GearIndex); | |
// Reset any ongoing animations and arm positions | |
[player1, player2].forEach(player => { | |
player.isAttacking = false; | |
player.isBlocking = false; | |
player.attackAnimationProgress = 0; | |
player.blockAnimationProgress = 0; | |
// Arm rotations are reset by updatePlayerGear and createPrimitiveCharacter | |
}); | |
// Remove all active projectiles from scene and array | |
while (activeProjectiles.length > 0) { | |
const projectile = activeProjectiles.pop(); | |
scene.remove(projectile); | |
projectile.geometry.dispose(); | |
projectile.material.dispose(); | |
} | |
// Clear all impact particles from scene and dispose | |
while(impactParticlesGroup.children.length > 0){ | |
const particle = impactParticlesGroup.children[0]; | |
impactParticlesGroup.remove(particle); | |
particle.geometry.dispose(); | |
particle.material.dispose(); | |
} | |
// Clear all weapon range particles | |
while(weaponRangeParticlesGroup.children.length > 0){ | |
const particle = weaponRangeParticlesGroup.children[0]; | |
weaponRangeParticlesGroup.remove(particle); | |
particle.geometry.dispose(); | |
particle.material.dispose(); | |
} | |
} | |
/** | |
* Handles window resize events to adjust the camera and renderer size. | |
*/ | |
function onWindowResize() { | |
const aspectRatio = window.innerWidth / window.innerHeight; | |
const frustumSize = 30; // Keep consistent with init | |
camera.left = frustumSize * aspectRatio / - 2; | |
camera.right = frustumSize * aspectRatio / 2; | |
camera.top = frustumSize / 2; | |
camera.bottom = frustumSize / - 2; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
// Initialize the game when the window loads | |
window.onload = init; | |
</script> | |
</body> | |
</html> | |