| <!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/three@0.128.0/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> |
|
|