|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Blocky Brawl: Dungeon Dimensions</title> |
|
<style> |
|
|
|
body { |
|
margin: 0; |
|
overflow: hidden; |
|
font-family: 'Inter', sans-serif; |
|
background-color: #1a1a1a; |
|
} |
|
|
|
|
|
#game-container { |
|
position: relative; |
|
width: 100vw; |
|
height: 100vh; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
overflow: hidden; |
|
} |
|
|
|
|
|
#top-ui-container { |
|
position: absolute; |
|
top: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
gap: 15px; |
|
z-index: 10; |
|
pointer-events: none; |
|
} |
|
|
|
|
|
#player-stats-container { |
|
display: flex; |
|
justify-content: space-between; |
|
width: 100%; |
|
max-width: 800px; |
|
gap: 50px; |
|
pointer-events: auto; |
|
} |
|
|
|
|
|
.score-display { |
|
color: white; |
|
font-size: 2.2em; |
|
font-weight: bold; |
|
text-shadow: 3px 3px 6px rgba(0,0,0,0.8); |
|
padding: 10px 15px; |
|
background-color: rgba(0, 0, 0, 0.4); |
|
border-radius: 12px; |
|
box-shadow: 0 4px 10px rgba(0,0,0,0.5); |
|
text-align: center; |
|
min-width: 150px; |
|
} |
|
|
|
|
|
.player-bars { |
|
height: 70px; |
|
width: 250px; |
|
border: 3px solid #ffffff; |
|
border-radius: 10px; |
|
box-shadow: 0 4px 10px rgba(0,0,0,0.5); |
|
display: flex; |
|
flex-direction: column; |
|
justify-content: space-around; |
|
padding: 5px; |
|
box-sizing: border-box; |
|
background-color: rgba(0, 0, 0, 0.4); |
|
} |
|
|
|
.bar-container { |
|
width: 100%; |
|
height: 25px; |
|
background-color: #4a5568; |
|
border-radius: 8px; |
|
overflow: hidden; |
|
} |
|
|
|
.health-bar, .mana-bar { |
|
height: 100%; |
|
width: 100%; |
|
transition: width 0.3s ease-out, background-color 0.3s ease-out; |
|
border-radius: 8px; |
|
} |
|
|
|
.health-bar { background-color: #28a745; } |
|
.mana-bar { background-color: #3b82f6; } |
|
|
|
|
|
|
|
#reset-button { |
|
position: absolute; |
|
bottom: 40px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
padding: 18px 35px; |
|
font-size: 1.8em; |
|
background: linear-gradient(145deg, #ff6b6b, #ee4444); |
|
color: white; |
|
border: none; |
|
border-radius: 15px; |
|
cursor: pointer; |
|
box-shadow: 0 8px 20px rgba(0,0,0,0.6); |
|
transition: background 0.3s ease, transform 0.1s ease, box-shadow 0.3s ease; |
|
z-index: 10; |
|
font-weight: bold; |
|
letter-spacing: 1px; |
|
text-transform: uppercase; |
|
pointer-events: auto; |
|
} |
|
|
|
#reset-button:hover { |
|
background: linear-gradient(145deg, #ff4d4d, #cc3333); |
|
transform: translateX(-50%) scale(1.05); |
|
box-shadow: 0 10px 25px rgba(0,0,0,0.8); |
|
} |
|
|
|
#reset-button:active { |
|
transform: translateX(-50%) scale(0.98); |
|
box-shadow: 0 4px 10px rgba(0,0,0,0.4); |
|
} |
|
|
|
|
|
canvas { |
|
display: block; |
|
width: 100%; |
|
height: 100%; |
|
border-radius: 15px; |
|
box-shadow: 0 0 25px rgba(0,0,0,0.7); |
|
} |
|
|
|
|
|
#controls { |
|
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; |
|
pointer-events: auto; |
|
display: flex; |
|
gap: 40px; |
|
} |
|
|
|
#controls h3 { |
|
margin-top: 0; |
|
color: #ffd700; |
|
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; |
|
display: inline-block; |
|
width: 40px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="game-container"> |
|
<div id="top-ui-container"> |
|
<div id="player-stats-container"> |
|
<div> |
|
<div id="score-left" class="score-display">P1 Score: 0</div> |
|
<div id="player-left-bars" class="player-bars"> |
|
<div class="bar-container"><div id="health-bar-left" class="health-bar"></div></div> |
|
<div class="bar-container"><div id="mana-bar-left" class="mana-bar"></div></div> |
|
</div> |
|
</div> |
|
<div> |
|
<div id="score-right" class="score-display">P2 Score: 0</div> |
|
<div id="player-right-bars" class="player-bars"> |
|
<div class="bar-container"><div id="health-bar-right" class="health-bar"></div></div> |
|
<div class="bar-container"><div id="mana-bar-right" class="mana-bar"></div></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div id="controls"> |
|
<div> |
|
<h3>Player 1 Controls (Red Wizard)</h3> |
|
<ul> |
|
<li><span>WASD:</span> Move</li> |
|
<li><span>E:</span> Cast Spell</li> |
|
</ul> |
|
</div> |
|
<div> |
|
<h3>Player 2 Controls (Blue Wizard)</h3> |
|
<ul> |
|
<li><span>IJKL:</span> Move</li> |
|
<li><span>U:</span> Cast Spell</li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<button id="reset-button">Primitive Reset!</button> |
|
</div> |
|
|
|
<script type="module"> |
|
|
|
import * as THREE from 'https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js'; |
|
|
|
|
|
let scene, camera, renderer; |
|
let player1, player2; |
|
let monsters = []; |
|
let activeProjectiles = []; |
|
|
|
|
|
const gridSize = 10; |
|
const mapWidth = 20; |
|
const mapHeight = 20; |
|
const dungeonMap = []; |
|
|
|
|
|
const playerStats = { |
|
left: { |
|
health: 100, |
|
maxHealth: 100, |
|
mana: 50, |
|
maxMana: 50, |
|
score: 0, |
|
position: new THREE.Vector3(-5 * gridSize, 0, -5 * gridSize) |
|
}, |
|
right: { |
|
health: 100, |
|
maxHealth: 100, |
|
mana: 50, |
|
maxMana: 50, |
|
score: 0, |
|
position: new THREE.Vector3(5 * gridSize, 0, 5 * gridSize) |
|
} |
|
}; |
|
|
|
|
|
const playerMoveSpeed = 0.15 * gridSize; |
|
const monsterMoveSpeed = 0.05 * gridSize; |
|
|
|
|
|
const uiElements = { |
|
scoreLeft: document.getElementById('score-left'), |
|
scoreRight: document.getElementById('score-right'), |
|
healthBarLeft: document.getElementById('health-bar-left'), |
|
manaBarLeft: document.getElementById('mana-bar-left'), |
|
healthBarRight: document.getElementById('health-bar-right'), |
|
manaBarRight: document.getElementById('mana-bar-right'), |
|
resetButton: document.getElementById('reset-button') |
|
}; |
|
|
|
|
|
const keys = { |
|
|
|
w: false, a: false, s: false, d: false, |
|
|
|
p1_action: false, |
|
|
|
|
|
i: false, j: false, k: false, l: false, |
|
|
|
p2_action: false |
|
}; |
|
|
|
|
|
const spellProjectileSpeed = 0.8 * gridSize; |
|
const spellProjectileLife = 120; |
|
const spellProjectileRadius = 0.03 * gridSize; |
|
const spellDamage = 20; |
|
|
|
|
|
const playerHealingThreshold = gridSize * 2.5; |
|
const healingAmountPerFrame = 0.1; |
|
|
|
|
|
|
|
|
|
function onWindowResize() { |
|
const aspectRatio = window.innerWidth / window.innerHeight; |
|
const frustumSize = Math.max(mapWidth, mapHeight) * gridSize * 0.7; |
|
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); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onKeyDown(event) { |
|
switch (event.code) { |
|
|
|
case 'KeyW': keys.w = true; break; |
|
case 'KeyA': keys.a = true; break; |
|
case 'KeyS': keys.s = true; break; |
|
case 'KeyD': keys.d = true; break; |
|
|
|
case 'KeyE': keys.p1_action = true; break; |
|
|
|
|
|
case 'KeyI': keys.i = true; break; |
|
case 'KeyJ': keys.j = true; break; |
|
case 'KeyK': keys.k = true; break; |
|
case 'KeyL': keys.l = true; break; |
|
|
|
case 'KeyU': keys.p2_action = true; break; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onKeyUp(event) { |
|
switch (event.code) { |
|
case 'KeyW': keys.w = false; break; |
|
case 'KeyA': keys.a = false; break; |
|
case 'KeyS': keys.s = false; break; |
|
case 'KeyD': keys.d = false; break; |
|
|
|
case 'KeyE': keys.p1_action = false; break; |
|
|
|
case 'KeyI': keys.i = false; break; |
|
case 'KeyJ': keys.j = false; break; |
|
case 'KeyK': keys.k = false; break; |
|
case 'KeyL': keys.l = false; break; |
|
|
|
case 'KeyU': keys.p2_action = false; break; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function generateDungeon() { |
|
for (let y = 0; y < mapHeight; y++) { |
|
dungeonMap[y] = []; |
|
for (let x = 0; x < mapWidth; x++) { |
|
|
|
if (x === 0 || x === mapWidth - 1 || y === 0 || y === mapHeight - 1) { |
|
dungeonMap[y][x] = 1; |
|
createWall(x, y); |
|
} else if (Math.random() < 0.05) { |
|
dungeonMap[y][x] = 1; |
|
createWall(x, y); |
|
} else { |
|
dungeonMap[y][x] = 0; |
|
createFloor(x, y); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createFloor(x, y) { |
|
const geometry = new THREE.BoxGeometry(gridSize, 1, gridSize); |
|
const material = new THREE.MeshPhongMaterial({ |
|
color: 0x3d4a5c, |
|
specular: 0x111111, |
|
shininess: 30 |
|
}); |
|
const floor = new THREE.Mesh(geometry, material); |
|
|
|
floor.position.set( |
|
(x - mapWidth / 2 + 0.5) * gridSize, |
|
-0.5, |
|
(y - mapHeight / 2 + 0.5) * gridSize |
|
); |
|
scene.add(floor); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createWall(x, y) { |
|
const geometry = new THREE.BoxGeometry(gridSize, gridSize * 2, gridSize); |
|
const material = new THREE.MeshPhongMaterial({ |
|
color: 0x90a4ae, |
|
specular: 0x333333, |
|
shininess: 60 |
|
}); |
|
const wall = new THREE.Mesh(geometry, material); |
|
|
|
wall.position.set( |
|
(x - mapWidth / 2 + 0.5) * gridSize, |
|
gridSize, |
|
(y - mapHeight / 2 + 0.5) * gridSize |
|
); |
|
scene.add(wall); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function createWizard(color) { |
|
const wizardGroup = new THREE.Group(); |
|
const scaleFactor = 0.8; |
|
const bodyHeight = 1.0 * scaleFactor; |
|
const bodyRadiusTop = 0.3 * scaleFactor; |
|
const bodyRadiusBottom = 0.4 * scaleFactor; |
|
const headRadius = 0.25 * scaleFactor; |
|
const hatHeight = 0.6 * scaleFactor; |
|
const hatRadius = 0.35 * scaleFactor; |
|
const armRadius = 0.1 * scaleFactor; |
|
const armLength = 0.8 * scaleFactor; |
|
const staffHandleLength = 1.2 * scaleFactor; |
|
const staffOrbRadius = 0.15 * scaleFactor; |
|
|
|
|
|
const bodyCylinder = new THREE.Mesh( |
|
new THREE.CylinderGeometry(bodyRadiusTop, bodyRadiusBottom, bodyHeight, 32), |
|
new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 }) |
|
); |
|
bodyCylinder.position.y = bodyHeight / 2; |
|
wizardGroup.add(bodyCylinder); |
|
|
|
const bodyTopSphere = new THREE.Mesh( |
|
new THREE.SphereGeometry(bodyRadiusTop, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2), |
|
new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 }) |
|
); |
|
bodyTopSphere.position.y = bodyHeight; |
|
wizardGroup.add(bodyTopSphere); |
|
|
|
const bodyBottomSphere = new THREE.Mesh( |
|
new THREE.SphereGeometry(bodyRadiusBottom, 32, 16, 0, Math.PI * 2, Math.PI / 2, Math.PI / 2), |
|
new THREE.MeshPhongMaterial({ color: color, specular: 0x555555, shininess: 80 }) |
|
); |
|
bodyBottomSphere.position.y = 0; |
|
wizardGroup.add(bodyBottomSphere); |
|
|
|
|
|
|
|
const headGeometry = new THREE.SphereGeometry(headRadius, 64, 32); |
|
const headMaterial = new THREE.MeshPhongMaterial({ color: 0xffe0bd, specular: 0x222222, shininess: 50 }); |
|
const head = new THREE.Mesh(headGeometry, headMaterial); |
|
head.position.y = bodyHeight + headRadius; |
|
wizardGroup.add(head); |
|
|
|
|
|
const hatGeometry = new THREE.ConeGeometry(hatRadius, hatHeight, 32); |
|
const hatMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, specular: 0x111111, shininess: 70 }); |
|
const hat = new THREE.Mesh(hatGeometry, hatMaterial); |
|
hat.position.y = head.position.y + headRadius + hatHeight / 2; |
|
wizardGroup.add(hat); |
|
|
|
|
|
const armGeometry = new THREE.CylinderGeometry(armRadius, armRadius, armLength, 16); |
|
const armMaterial = new THREE.MeshPhongMaterial({ color: 0xaaaaaa, specular: 0x333333, shininess: 40 }); |
|
|
|
const leftArm = new THREE.Mesh(armGeometry, armMaterial); |
|
leftArm.position.set(-bodyRadiusTop - armRadius, bodyHeight * 0.7, 0); |
|
leftArm.rotation.z = Math.PI / 2; |
|
wizardGroup.add(leftArm); |
|
|
|
const rightArm = leftArm.clone(); |
|
rightArm.position.x = bodyRadiusTop + armRadius; |
|
rightArm.rotation.z = -Math.PI / 2; |
|
wizardGroup.add(rightArm); |
|
|
|
|
|
const staffHandleGeometry = new THREE.CylinderGeometry(0.005 * gridSize, 0.005 * gridSize, staffHandleLength, 16); |
|
const staffHandleMaterial = new THREE.MeshPhongMaterial({ color: 0x8b4513, specular: 0x333333, shininess: 40 }); |
|
const staffHandle = new THREE.Mesh(staffHandleGeometry, staffHandleMaterial); |
|
staffHandle.position.set(0.5 * scaleFactor, bodyHeight * 0.5, 0.5 * scaleFactor); |
|
staffHandle.rotation.x = Math.PI / 4; |
|
wizardGroup.add(staffHandle); |
|
|
|
const staffOrbGeometry = new THREE.SphereGeometry(staffOrbRadius, 32, 16); |
|
const staffOrbMaterial = new THREE.MeshPhongMaterial({ color: 0x00ffff, emissive: 0x00ffff, emissiveIntensity: 0.5, specular: 0xffffff, shininess: 100 }); |
|
const staffOrb = new THREE.Mesh(staffOrbGeometry, staffOrbMaterial); |
|
staffOrb.position.copy(staffHandle.position); |
|
staffOrb.position.y += staffHandleLength / 2; |
|
wizardGroup.add(staffOrb); |
|
|
|
wizardGroup.scale.set(gridSize, gridSize, gridSize); |
|
|
|
return wizardGroup; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createMonster(x, y, color = 0x800080) { |
|
const monsterGroup = new THREE.Group(); |
|
const scaleFactor = 0.7; |
|
|
|
|
|
const bodyGeometry = new THREE.SphereGeometry(0.5 * scaleFactor, 32, 16); |
|
const bodyMaterial = new THREE.MeshPhongMaterial({ color: color, specular: 0x444444, shininess: 50 }); |
|
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); |
|
body.position.y = 0.5 * scaleFactor; |
|
monsterGroup.add(body); |
|
|
|
|
|
const eyeGeometry = new THREE.SphereGeometry(0.1 * scaleFactor, 16, 8); |
|
const eyeMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.8 }); |
|
const pupilMaterial = new THREE.MeshPhongMaterial({ color: 0x000000 }); |
|
|
|
const eye1 = new THREE.Mesh(eyeGeometry, eyeMaterial); |
|
eye1.position.set(-0.2 * scaleFactor, 0.6 * scaleFactor, 0.4 * scaleFactor); |
|
monsterGroup.add(eye1); |
|
const pupil1 = new THREE.Mesh(eyeGeometry.clone(), pupilMaterial); |
|
pupil1.scale.set(0.5, 0.5, 0.5); |
|
pupil1.position.set(-0.2 * scaleFactor, 0.6 * scaleFactor, 0.45 * scaleFactor); |
|
monsterGroup.add(pupil1); |
|
|
|
const eye2 = eye1.clone(); |
|
eye2.position.x = 0.2 * scaleFactor; |
|
monsterGroup.add(eye2); |
|
const pupil2 = pupil1.clone(); |
|
pupil2.position.x = 0.2 * scaleFactor; |
|
monsterGroup.add(pupil2); |
|
|
|
|
|
const spikeGeometry = new THREE.ConeGeometry(0.15 * scaleFactor, 0.4 * scaleFactor, 8); |
|
const spikeMaterial = new THREE.MeshPhongMaterial({ color: 0x555555, specular: 0x222222, shininess: 30 }); |
|
|
|
const spike1 = new THREE.Mesh(spikeGeometry, spikeMaterial); |
|
spike1.position.set(0, 0.9 * scaleFactor, 0); |
|
monsterGroup.add(spike1); |
|
|
|
const spike2 = spike1.clone(); |
|
spike2.rotation.y = Math.PI / 2; |
|
spike2.position.set(0.5 * scaleFactor, 0.5 * scaleFactor, 0); |
|
monsterGroup.add(spike2); |
|
|
|
const spike3 = spike1.clone(); |
|
spike3.rotation.y = -Math.PI / 2; |
|
spike3.position.set(-0.5 * scaleFactor, 0.5 * scaleFactor, 0); |
|
monsterGroup.add(spike3); |
|
|
|
monsterGroup.scale.set(gridSize, gridSize, gridSize); |
|
|
|
|
|
monsterGroup.position.set( |
|
(x - mapWidth / 2 + 0.5) * gridSize, |
|
0, |
|
(y - mapHeight / 2 + 0.5) * gridSize |
|
); |
|
scene.add(monsterGroup); |
|
|
|
|
|
const monster = { |
|
mesh: monsterGroup, |
|
health: 50, |
|
maxHealth: 50, |
|
}; |
|
monsters.push(monster); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updateObjectPosition(objectMesh, worldX, worldZ) { |
|
objectMesh.position.x = worldX; |
|
objectMesh.position.z = worldZ; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function isCollidingWithWall(x, z) { |
|
|
|
const gridX = Math.floor(x / gridSize + mapWidth / 2); |
|
const gridY = Math.floor(z / gridSize + mapHeight / 2); |
|
|
|
|
|
if (gridX < 0 || gridX >= mapWidth || gridY < 0 || gridY >= mapHeight) { |
|
return true; |
|
} |
|
|
|
return dungeonMap[gridY][gridX] === 1; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function updatePlayerMovement(playerStat, playerMesh, moveX, moveZ) { |
|
let newX = playerStat.position.x + moveX; |
|
let newZ = playerStat.position.z + moveZ; |
|
|
|
let collidedX = isCollidingWithWall(newX, playerStat.position.z); |
|
let collidedZ = isCollidingWithWall(playerStat.position.x, newZ); |
|
|
|
if (!collidedX) { |
|
playerStat.position.x = newX; |
|
} |
|
if (!collidedZ) { |
|
playerStat.position.z = newZ; |
|
} |
|
|
|
updateObjectPosition(playerMesh, playerStat.position.x, playerStat.position.z); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createSpellEffect(position, color) { |
|
const geometry = new THREE.SphereGeometry(spellProjectileRadius * 2, 16, 16); |
|
const material = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.8 }); |
|
const spellEffect = new THREE.Mesh(geometry, material); |
|
spellEffect.position.copy(position); |
|
spellEffect.position.y = gridSize * 0.5; |
|
scene.add(spellEffect); |
|
|
|
|
|
let scale = 0.1; |
|
const fadeSpeed = 0.05; |
|
|
|
|
|
|
|
|
|
function animateEffect() { |
|
if (spellEffect.material.opacity > 0) { |
|
spellEffect.material.opacity -= fadeSpeed; |
|
scale += 0.05; |
|
spellEffect.scale.set(scale, scale, scale); |
|
requestAnimationFrame(animateEffect); |
|
} else { |
|
scene.remove(spellEffect); |
|
spellEffect.geometry.dispose(); |
|
spellEffect.material.dispose(); |
|
} |
|
} |
|
animateEffect(); |
|
} |
|
|
|
|
|
|
|
|
|
class Projectile extends THREE.Mesh { |
|
constructor(originPlayer, targetPosition, damage) { |
|
const geometry = new THREE.SphereGeometry(spellProjectileRadius, 16, 16); |
|
const material = new THREE.MeshPhongMaterial({ color: 0xffd700, emissive: 0xffd700, emissiveIntensity: 0.5 }); |
|
super(geometry, material); |
|
|
|
this.originPlayer = originPlayer; |
|
this.damage = damage; |
|
this.life = spellProjectileLife; |
|
|
|
|
|
this.position.copy(originPlayer.position); |
|
this.position.y = gridSize * 0.5; |
|
|
|
|
|
this.velocity = new THREE.Vector3(); |
|
this.velocity.subVectors(targetPosition, this.position).normalize().multiplyScalar(spellProjectileSpeed); |
|
|
|
scene.add(this); |
|
} |
|
|
|
update() { |
|
this.position.add(this.velocity); |
|
this.life--; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function updateUI() { |
|
|
|
uiElements.scoreLeft.textContent = `P1 Score: ${playerStats.left.score}`; |
|
uiElements.healthBarLeft.style.width = `${(playerStats.left.health / playerStats.left.maxHealth) * 100}%`; |
|
uiElements.healthBarLeft.style.backgroundColor = playerStats.left.health > playerStats.left.maxHealth * 0.6 ? '#28a745' : (playerStats.left.health > playerStats.left.maxHealth * 0.3 ? '#ffc107' : '#dc3545'); |
|
uiElements.manaBarLeft.style.width = `${(playerStats.left.mana / playerStats.left.maxMana) * 100}%`; |
|
|
|
|
|
uiElements.scoreRight.textContent = `P2 Score: ${playerStats.right.score}`; |
|
uiElements.healthBarRight.style.width = `${(playerStats.right.health / playerStats.right.maxHealth) * 100}%`; |
|
uiElements.healthBarRight.style.backgroundColor = playerStats.right.health > playerStats.right.maxHealth * 0.6 ? '#28a745' : (playerStats.right.health > playerStats.right.maxHealth * 0.3 ? '#ffc107' : '#dc3545'); |
|
uiElements.manaBarRight.style.width = `${(playerStats.right.mana / playerStats.right.maxMana) * 100}%`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function resetGame() { |
|
|
|
playerStats.left.health = playerStats.left.maxHealth; |
|
playerStats.left.mana = playerStats.left.maxMana; |
|
playerStats.left.score = 0; |
|
playerStats.left.position.set(-5 * gridSize, 0, -5 * gridSize); |
|
updateObjectPosition(player1, playerStats.left.position.x, playerStats.left.position.z); |
|
|
|
|
|
playerStats.right.health = playerStats.right.maxHealth; |
|
playerStats.right.mana = playerStats.right.maxMana; |
|
playerStats.right.score = 0; |
|
playerStats.right.position.set(5 * gridSize, 0, 5 * gridSize); |
|
updateObjectPosition(player2, playerStats.right.position.x, playerStats.right.position.z); |
|
|
|
|
|
monsters.forEach(monster => scene.remove(monster.mesh)); |
|
monsters = []; |
|
spawnMonsters(3); |
|
|
|
|
|
while (activeProjectiles.length > 0) { |
|
const projectile = activeProjectiles.pop(); |
|
scene.remove(projectile); |
|
projectile.geometry.dispose(); |
|
projectile.material.dispose(); |
|
} |
|
|
|
updateUI(); |
|
console.log("Game reset!"); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function spawnMonsters(count) { |
|
for (let i = 0; i < count; i++) { |
|
let spawned = false; |
|
while (!spawned) { |
|
|
|
const randomGridX = Math.floor(Math.random() * (mapWidth - 2)) + 1; |
|
const randomGridY = Math.floor(Math.random() * (mapHeight - 2)) + 1; |
|
|
|
|
|
const worldX = (randomGridX - mapWidth / 2 + 0.5) * gridSize; |
|
const worldZ = (randomGridY - mapHeight / 2 + 0.5) * gridSize; |
|
|
|
|
|
if (dungeonMap[randomGridY][randomGridX] === 0 && |
|
new THREE.Vector3(worldX, 0, worldZ).distanceTo(player1.position) > gridSize * 3 && |
|
new THREE.Vector3(worldX, 0, worldZ).distanceTo(player2.position) > gridSize * 3) { |
|
createMonster(randomGridX, randomGridY); |
|
spawned = true; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function init() { |
|
|
|
scene = new THREE.Scene(); |
|
scene.background = new THREE.Color(0x333333); |
|
|
|
|
|
const aspectRatio = window.innerWidth / window.innerHeight; |
|
const frustumSize = Math.max(mapWidth, mapHeight) * gridSize * 0.7; |
|
camera = new THREE.OrthographicCamera( |
|
frustumSize * aspectRatio / - 2, |
|
frustumSize * aspectRatio / 2, |
|
frustumSize / 2, |
|
frustumSize / - 2, |
|
1, 1000 |
|
); |
|
|
|
camera.position.set(0, 40, 40); |
|
camera.lookAt(0, 0, 0); |
|
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true }); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
renderer.setPixelRatio(window.devicePixelRatio); |
|
document.getElementById('game-container').appendChild(renderer.domElement); |
|
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 2); |
|
scene.add(ambientLight); |
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1); |
|
directionalLight.position.set(0, 100, 0); |
|
scene.add(directionalLight); |
|
|
|
|
|
generateDungeon(); |
|
|
|
|
|
player1 = createWizard(0xff4500); |
|
player1.position.copy(playerStats.left.position); |
|
scene.add(player1); |
|
|
|
|
|
player2 = createWizard(0x00aaff); |
|
player2.position.copy(playerStats.right.position); |
|
scene.add(player2); |
|
|
|
|
|
uiElements.resetButton.addEventListener('click', resetGame); |
|
|
|
|
|
window.addEventListener('keydown', onKeyDown); |
|
window.addEventListener('keyup', onKeyUp); |
|
window.addEventListener('resize', onWindowResize); |
|
|
|
|
|
spawnMonsters(3); |
|
|
|
|
|
updateUI(); |
|
|
|
|
|
animate(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function animate() { |
|
requestAnimationFrame(animate); |
|
|
|
|
|
let p1MoveX = 0, p1MoveZ = 0; |
|
if (keys.w) p1MoveZ -= playerMoveSpeed; |
|
if (keys.s) p1MoveZ += playerMoveSpeed; |
|
if (keys.a) p1MoveX -= playerMoveSpeed; |
|
if (keys.d) p1MoveX += playerMoveSpeed; |
|
|
|
|
|
if (p1MoveX !== 0 && p1MoveZ !== 0) { |
|
const length = Math.sqrt(p1MoveX * p1MoveX + p1MoveZ * p1MoveZ); |
|
p1MoveX /= length; |
|
p1MoveZ /= length; |
|
p1MoveX *= playerMoveSpeed; |
|
p1MoveZ *= playerMoveSpeed; |
|
} |
|
updatePlayerMovement(playerStats.left, player1, p1MoveX, p1MoveZ); |
|
|
|
|
|
|
|
let p2MoveX = 0, p2MoveZ = 0; |
|
if (keys.i) p2MoveZ -= playerMoveSpeed; |
|
if (keys.k) p2MoveZ += playerMoveSpeed; |
|
if (keys.j) p2MoveX -= playerMoveSpeed; |
|
if (keys.l) p2MoveX += playerMoveSpeed; |
|
|
|
|
|
if (p2MoveX !== 0 && p2MoveZ !== 0) { |
|
const length = Math.sqrt(p2MoveX * p2MoveX + p2MoveZ * p2MoveZ); |
|
p2MoveX /= length; |
|
p2MoveZ /= length; |
|
p2MoveX *= playerMoveSpeed; |
|
p2MoveZ *= playerMoveSpeed; |
|
} |
|
updatePlayerMovement(playerStats.right, player2, p2MoveX, p2MoveZ); |
|
|
|
|
|
player1.lookAt(player2.position.x, player1.position.y, player2.position.z); |
|
player2.lookAt(player1.position.x, player2.position.y, player1.position.z); |
|
|
|
|
|
if (keys.p1_action) { |
|
if (playerStats.left.mana >= 10) { |
|
playerStats.left.mana -= 10; |
|
console.log("Left player cast a spell!"); |
|
createSpellEffect(player1.position, 0xffa500); |
|
|
|
|
|
let closestMonster = null; |
|
let minDistance = Infinity; |
|
monsters.forEach(monster => { |
|
const dx = monster.mesh.position.x - player1.position.x; |
|
const dz = monster.mesh.position.z - player1.position.z; |
|
const distance = Math.sqrt(dx * dx + dz * dz); |
|
if (distance < minDistance) { |
|
minDistance = distance; |
|
closestMonster = monster; |
|
} |
|
}); |
|
|
|
if (closestMonster) { |
|
activeProjectiles.push(new Projectile(player1, closestMonster.mesh.position, spellDamage)); |
|
} else { |
|
console.log("No monster to target for Left player's spell."); |
|
} |
|
} else { |
|
console.log("Left player out of mana!"); |
|
} |
|
keys.p1_action = false; |
|
} |
|
|
|
if (keys.p2_action) { |
|
if (playerStats.right.mana >= 10) { |
|
playerStats.right.mana -= 10; |
|
console.log("Right player cast a spell!"); |
|
createSpellEffect(player2.position, 0x00ff00); |
|
|
|
|
|
let closestMonster = null; |
|
let minDistance = Infinity; |
|
monsters.forEach(monster => { |
|
const dx = monster.mesh.position.x - player2.position.x; |
|
const dz = monster.mesh.position.z - player2.position.z; |
|
const distance = Math.sqrt(dx * dx + dz * dz); |
|
if (distance < minDistance) { |
|
minDistance = distance; |
|
closestMonster = monster; |
|
} |
|
}); |
|
|
|
if (closestMonster) { |
|
activeProjectiles.push(new Projectile(player2, closestMonster.mesh.position, spellDamage)); |
|
} else { |
|
console.log("No monster to target for Right player's spell."); |
|
} |
|
} else { |
|
console.log("Right player out of mana!"); |
|
} |
|
keys.p2_action = false; |
|
} |
|
|
|
|
|
for (let i = activeProjectiles.length - 1; i >= 0; i--) { |
|
const projectile = activeProjectiles[i]; |
|
projectile.update(); |
|
|
|
let hit = false; |
|
|
|
for (let j = monsters.length - 1; j >= 0; j--) { |
|
const monster = monsters[j]; |
|
if (projectile.position.distanceTo(monster.mesh.position) < (spellProjectileRadius + monster.mesh.scale.x * 0.5 * gridSize)) { |
|
monster.health -= projectile.damage; |
|
createSpellEffect(monster.mesh.position, 0xff0000); |
|
if (monster.health <= 0) { |
|
console.log("Monster defeated!"); |
|
scene.remove(monster.mesh); |
|
monsters.splice(j, 1); |
|
|
|
if (projectile.originPlayer === player1) playerStats.left.score += 100; |
|
else if (projectile.originPlayer === player2) playerStats.right.score += 100; |
|
} |
|
hit = true; |
|
break; |
|
} |
|
} |
|
|
|
if (hit || projectile.life <= 0) { |
|
scene.remove(projectile); |
|
projectile.geometry.dispose(); |
|
projectile.material.dispose(); |
|
activeProjectiles.splice(i, 1); |
|
} |
|
} |
|
|
|
|
|
monsters.forEach(monster => { |
|
const distToP1 = monster.mesh.position.distanceTo(player1.position); |
|
const distToP2 = monster.mesh.position.distanceTo(player2.position); |
|
|
|
let targetPlayerMesh = null; |
|
let targetPlayerStat = null; |
|
|
|
if (distToP1 <= distToP2) { |
|
targetPlayerMesh = player1; |
|
targetPlayerStat = playerStats.left; |
|
} else { |
|
targetPlayerMesh = player2; |
|
targetPlayerStat = playerStats.right; |
|
} |
|
|
|
|
|
let monsterMoveX = 0; |
|
let monsterMoveZ = 0; |
|
|
|
if (monster.mesh.position.x < targetPlayerMesh.position.x) { |
|
monsterMoveX = monsterMoveSpeed; |
|
} else if (monster.mesh.position.x > targetPlayerMesh.position.x) { |
|
monsterMoveX = -monsterMoveSpeed; |
|
} |
|
|
|
if (monster.mesh.position.z < targetPlayerMesh.position.z) { |
|
monsterMoveZ = monsterMoveSpeed; |
|
} else if (monster.mesh.position.z > targetPlayerMesh.position.z) { |
|
monsterMoveZ = -monsterMoveSpeed; |
|
} |
|
|
|
|
|
if (monsterMoveX !== 0 && monsterMoveZ !== 0) { |
|
const length = Math.sqrt(monsterMoveX * monsterMoveX + monsterMoveZ * monsterMoveZ); |
|
monsterMoveX /= length; |
|
monsterMoveZ /= length; |
|
} |
|
|
|
|
|
let newMonsterX = monster.mesh.position.x + monsterMoveX; |
|
let newMonsterZ = monster.mesh.position.z + monsterMoveZ; |
|
|
|
let collidedMonsterX = isCollidingWithWall(newMonsterX, monster.mesh.position.z); |
|
let collidedMonsterZ = isCollidingWithWall(monster.mesh.position.x, newMonsterZ); |
|
|
|
if (!collidedMonsterX) { |
|
monster.mesh.position.x = newMonsterX; |
|
} |
|
if (!collidedMonsterZ) { |
|
monster.mesh.position.z = newMonsterZ; |
|
} |
|
|
|
|
|
if (monster.mesh.position.distanceTo(targetPlayerMesh.position) < gridSize * 1.2) { |
|
targetPlayerStat.health = Math.max(0, targetPlayerStat.health - 0.5); |
|
console.log(`Monster attacked ${targetPlayerStat === playerStats.left ? 'Left' : 'Right'} Player!`); |
|
} |
|
}); |
|
|
|
|
|
const distanceBetweenPlayers = player1.position.distanceTo(player2.position); |
|
if (distanceBetweenPlayers < playerHealingThreshold) { |
|
|
|
playerStats.right.health = Math.min(playerStats.right.maxHealth, playerStats.right.health + healingAmountPerFrame); |
|
|
|
playerStats.left.health = Math.min(playerStats.left.maxHealth, playerStats.left.health + healingAmountPerFrame); |
|
} |
|
|
|
|
|
if (playerStats.left.health <= 0 && playerStats.right.health <= 0) { |
|
console.log("Game Over! Both players incapacitated."); |
|
|
|
} |
|
|
|
updateUI(); |
|
renderer.render(scene, camera); |
|
} |
|
|
|
|
|
window.onload = init; |
|
</script> |
|
</body> |
|
</html> |
|
|