| | <!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> |
| |
|