console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D 坦克大战 (高级版 V2)</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: sans-serif;
background-color: #222;
color: white;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
body.state-settings #game-container,
body.state-settings #info,
body.state-settings #winner-message {
display: none;
}
body.state-playing #settings-panel {
display: none;
}
body.state-gameover #info {
opacity: 0.7;
}
body.state-gameover #settings-panel { display: none; }
#game-container {
width: 100vw;
height: 100vh;
display: block;
position: absolute;
top: 0;
left: 0;
}
#settings-panel {
background-color: rgba(40, 40, 40, 0.9);
padding: 30px 40px;
border-radius: 10px;
border: 1px solid #555;
box-shadow: 0 5px 20px rgba(0,0,0,0.4);
z-index: 200;
max-width: 700px;
max-height: 90vh;
overflow-y: auto;
color: #eee;
}
#settings-panel h2 {
text-align: center;
margin-top: 0;
color: #ffeb3b;
border-bottom: 1px solid #555;
padding-bottom: 10px;
margin-bottom: 20px;
}
.settings-group {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px dashed #444;
}
.settings-group:last-of-type {
border-bottom: none;
margin-bottom: 15px;
}
.settings-group label {
display: inline-block;
width: 160px;
margin-bottom: 8px;
font-weight: bold;
vertical-align: middle;
}
.settings-group input[type="number"],
.settings-group input[type="text"],
.settings-group select {
padding: 6px 8px;
border-radius: 4px;
border: 1px solid #666;
background-color: #333;
color: #eee;
margin-left: 10px;
vertical-align: middle;
box-sizing: border-box;
}
.settings-group input[type="number"] {
width: 70px;
}
.settings-group input[type="text"] {
width: 90px;
text-transform: capitalize;
}
.settings-row {
margin-bottom: 10px;
display: flex;
align-items: center;
}
.settings-row label { margin-bottom: 0; }
.player-controls {
margin-top: 15px;
padding-left: 20px;
border-left: 3px solid #555;
}
.player-controls h4 { margin-top: 0; margin-bottom: 10px; color: #ddd; }
.player-controls label { width: 70px; font-weight: normal; margin-bottom: 5px; }
.player-controls div { margin-bottom: 5px; }
#start-button {
display: block;
width: 100%;
padding: 12px 20px;
font-size: 1.2em;
font-weight: bold;
color: #111;
background-color: #ffeb3b;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 20px;
transition: background-color 0.2s ease;
}
#start-button:hover {
background-color: #fdd835;
}
.key-hint { font-size: 0.8em; color: #aaa; display: block; margin-top: 5px; padding-left: 130px; }
#info {
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.7);
padding: 5px 10px;
border-radius: 0 0 8px 0;
color: white;
z-index: 100;
max-width: 100%;
display: flex;
flex-wrap: wrap;
}
.player-info-block {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 5px;
padding: 8px 12px;
margin: 5px;
min-width: 180px;
transition: opacity 0.5s ease;
}
.player-info-block.hidden { display: none; }
.player-info-block.respawning { opacity: 0.5; border-style: dashed; }
.player-info-block h5 { margin: 0 0 5px 0; padding-bottom: 3px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); font-size: 1em; }
.info-line { font-size: 0.9em; margin-bottom: 3px; }
.info-line strong { display: inline-block; width: 45px; }
#player1-info-block h5 { color: #4caf50; }
#player2-info-block h5 { color: #2196f3; }
#player3-info-block h5 { color: #ff9800; }
#player4-info-block h5 { color: #9c27b0; }
.weapon-info { font-style: italic; color: #ccc; }
.weapon-active { color: #ffeb3b !important; }
.low-health span { color: #ffcc00 !important; font-weight: bolder; }
.destroyed span { color: #e53935 !important; font-style: italic; }
.ammo-low span { color: #ff9800; }
.ammo-out span { color: #e53935; font-weight: bold;}
#timer-info-container { padding: 8px 12px; margin: 5px; font-weight: bold; background-color: rgba(50,50,50,0.5); border-radius: 5px; }
#timer-info-container span { color: #ffeb3b;}
#winner-message{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:3em;color:yellow;background-color:rgba(0,0,0,.75);padding:30px;border-radius:10px;text-align:center;z-index:101;border:4px solid gold;box-shadow:0 5px 15px rgba(0,0,0,.5);display:none;min-width:300px}
</style>
</head>
<body class="state-settings">
<div id="settings-panel">
<h2>坦克大战 - 设置</h2>
<div class="settings-group">
<div class="settings-row">
<label for="num-players">玩家数量:</label>
<input type="number" id="num-players" min="2" max="4" value="2">
</div>
<div class="settings-row">
<label for="start-health">初始生命:</label>
<input type="number" id="start-health" min="10" max="1000" value="100" step="10">
</div>
<div class="settings-row">
<label for="initial-ammo">初始炮弹:</label>
<input type="number" id="initial-ammo" min="5" max="200" value="30" step="5">
</div>
<div class="settings-row">
<label for="game-duration">游戏时间 (秒):</label>
<input type="number" id="game-duration" min="30" max="600" value="120" step="15">
</div>
<div class="settings-row">
<label for="wall-density">墙体密度 (0-10):</label>
<input type="number" id="wall-density" min="0" max="10" value="4" step="1">
</div>
<div class="settings-row">
<label for="ammo-crate-interval">弹药箱刷新间隔 (秒):</label>
<input type="number" id="ammo-crate-interval" min="5" max="120" value="15" step="1">
</div>
</div>
<div class="settings-group">
<h3>按键设置 (使用 <a href="https://keycode.info/" target="_blank" title="Find key codes">event.code</a> 值)</h3>
<div id="controls-container">
<div class="player-controls" id="player1-controls">
<h4>玩家 1</h4>
<div><label for="p1-forward">前进:</label><input type="text" id="p1-forward" value="KeyW" size="8"></div>
<div><label for="p1-backward">后退:</label><input type="text" id="p1-backward" value="KeyS" size="8"></div>
<div><label for="p1-left">左转:</label><input type="text" id="p1-left" value="KeyA" size="8"></div>
<div><label for="p1-right">右转:</label><input type="text" id="p1-right" value="KeyD" size="8"></div>
<div><label for="p1-fire">开火:</label><input type="text" id="p1-fire" value="Space" size="8"></div>
</div>
<div class="player-controls" id="player2-controls">
<h4>玩家 2</h4>
<div><label for="p2-forward">前进:</label><input type="text" id="p2-forward" value="ArrowUp" size="8"></div>
<div><label for="p2-backward">后退:</label><input type="text" id="p2-backward" value="ArrowDown" size="8"></div>
<div><label for="p2-left">左转:</label><input type="text" id="p2-left" value="ArrowLeft" size="8"></div>
<div><label for="p2-right">右转:</label><input type="text" id="p2-right" value="ArrowRight" size="8"></div>
<div><label for="p2-fire">开火:</label><input type="text" id="p2-fire" value="Enter" size="8"></div>
</div>
<div class="player-controls" id="player3-controls" style="display: none;">
<h4>玩家 3</h4>
<div><label for="p3-forward">前进:</label><input type="text" id="p3-forward" value="Numpad8" size="8"></div>
<div><label for="p3-backward">后退:</label><input type="text" id="p3-backward" value="Numpad5" size="8"></div>
<div><label for="p3-left">左转:</label><input type="text" id="p3-left" value="Numpad4" size="8"></div>
<div><label for="p3-right">右转:</label><input type="text" id="p3-right" value="Numpad6" size="8"></div>
<div><label for="p3-fire">开火:</label><input type="text" id="p3-fire" value="Numpad0" size="8"></div>
</div>
<div class="player-controls" id="player4-controls" style="display: none;">
<h4>玩家 4</h4>
<div><label for="p4-forward">前进:</label><input type="text" id="p4-forward" value="KeyI" size="8"></div>
<div><label for="p4-backward">后退:</label><input type="text" id="p4-backward" value="KeyK" size="8"></div>
<div><label for="p4-left">左转:</label><input type="text" id="p4-left" value="KeyJ" size="8"></div>
<div><label for="p4-right">右转:</label><input type="text" id="p4-right" value="KeyL" size="8"></div>
<div><label for="p4-fire">开火:</label><input type="text" id="p4-fire" value="Semicolon" size="8"></div>
</div>
</div>
</div>
<button id="start-button">开始游戏</button>
</div>
<div id="info">
<div class="player-info-block" id="player1-info-block">
<h5>玩家 1</h5>
<div class="info-line health-info"><strong>生命:</strong> <span id="p1-health">100</span></div>
<div class="info-line score-info"><strong>得分:</strong> <span id="p1-score">0</span></div>
<div class="info-line ammo-info"><strong>炮弹:</strong> <span id="p1-ammo">30</span></div>
<div class="info-line weapon-info">武器: <span id="p1-weapon">无</span></div>
</div>
<div class="player-info-block" id="player2-info-block">
<h5>玩家 2</h5>
<div class="info-line health-info"><strong>生命:</strong> <span id="p2-health">100</span></div>
<div class="info-line score-info"><strong>得分:</strong> <span id="p2-score">0</span></div>
<div class="info-line ammo-info"><strong>炮弹:</strong> <span id="p2-ammo">30</span></div>
<div class="info-line weapon-info">武器: <span id="p2-weapon">无</span></div>
</div>
<div class="player-info-block hidden" id="player3-info-block">
<h5>玩家 3</h5>
<div class="info-line health-info"><strong>生命:</strong> <span id="p3-health">100</span></div>
<div class="info-line score-info"><strong>得分:</strong> <span id="p3-score">0</span></div>
<div class="info-line ammo-info"><strong>炮弹:</strong> <span id="p3-ammo">30</span></div>
<div class="info-line weapon-info">武器: <span id="p3-weapon">无</span></div>
</div>
<div class="player-info-block hidden" id="player4-info-block">
<h5>玩家 4</h5>
<div class="info-line health-info"><strong>生命:</strong> <span id="p4-health">100</span></div>
<div class="info-line score-info"><strong>得分:</strong> <span id="p4-score">0</span></div>
<div class="info-line ammo-info"><strong>炮弹:</strong> <span id="p4-ammo">30</span></div>
<div class="info-line weapon-info">武器: <span id="p4-weapon">无</span></div>
</div>
<div id="timer-info-container">
时间: <span id="time-left">120</span>s
</div>
</div>
<audio id="fire-sound" src="fire.mp3" preload="auto"></audio>
<audio id="hit-sound" src="hit.mp3" preload="auto"></audio>
<audio id="wall-hit-sound" src="hit_wall.mp3" preload="auto"></audio>
<audio id="wall-destroy-sound" src="destroy.mp3" preload="auto"></audio>
<div id="game-container"></div>
<div id="winner-message"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
let scene, camera, renderer;
let gameContainer, clock, keysPressed = {};
let tanks = [];
let bullets = [], activePowerups = [];
let walls = [];
let gameOver = false, gameStarted = false, nextPowerupId = 0;
let numPlayers = 2;
let startHealth = 100;
let initialAmmoSetting = 30;
let gameDurationSetting = 120;
let wallDensitySetting = 4;
let ammoCrateSpawnIntervalSetting = 15;
let playerControls = [
{ id: 1, forward: 'KeyW', backward: 'KeyS', left: 'KeyA', right: 'KeyD', fire: 'Space' },
{ id: 2, forward: 'ArrowUp', backward: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight', fire: 'Enter' },
{ id: 3, forward: 'Numpad8', backward: 'Numpad5', left: 'Numpad4', right: 'Numpad6', fire: 'Numpad0' },
{ id: 4, forward: 'KeyI', backward: 'KeyK', left: 'KeyJ', right: 'KeyL', fire: 'Semicolon' }
];
const BASE_TANK_SPEED = 20, BASE_RELOAD_TIME = 0.4, TANK_TURN_SPEED = Math.PI / 1.5;
const BULLET_SPEED = 60, BULLET_DAMAGE = 15;
const GROUND_SIZE = 120;
const BOUNDARY_LIMIT = GROUND_SIZE / 2 - 4;
const WALL_BOUNDARY_LIMIT = GROUND_SIZE / 2 - 5;
const DAMAGE_THRESHOLD_1 = 65, DAMAGE_THRESHOLD_2 = 30;
const MIN_SPEED_MULTIPLIER = 0.4, MAX_RELOAD_MULTIPLIER = 2.5;
const RESPAWN_DELAY = 3.0;
const POWERUP_TYPES = ['shotgun', 'double', 'missile', 'health'];
const POWERUP_COLORS = { shotgun: 0xffa500, double: 0x00ffff, missile: 0xff00ff, health: 0x00ff00, ammo: 0xaaaaaa };
const POWERUP_SPAWN_INTERVAL = 8;
const MAX_ACTIVE_POWERUPS = 6;
const POWERUP_LIFETIME = 15;
const WEAPON_DURATION = 15, HEALTH_POTION_AMOUNT = 35;
const AMMO_CRATE_AMOUNT = 15;
const SHOTGUN_SPREAD_ANGLE = Math.PI / 18, SHOTGUN_BULLET_COUNT = 5;
const DOUBLE_SHOT_DELAY = 0.15;
const MISSILE_SPEED = 45, MISSILE_TURN_RATE = 2.5;
const MISSILE_MAX_RANGE = GROUND_SIZE * 1.0;
const MISSILE_LIFETIME = MISSILE_MAX_RANGE / MISSILE_SPEED;
const MISSILE_DAMAGE_MULTIPLIER = 1.5;
const WALL_TYPES = { DIRT: 'dirt', STEEL: 'steel' };
const DIRT_WALL_HEALTH = 1;
const STEEL_WALL_HEALTH = 3;
const WALL_HEIGHT = 4;
const WALL_THICKNESS = 1.5;
const WALL_SEGMENT_LENGTH = 6;
const MIN_WALL_SPAWN_DIST_FROM_CENTER = 15;
let fireSound, hitSound, wallHitSound, wallDestroySound;
let timeLeftSpan;
let gameStartTime, audioInitialized = false;
let lastPowerupSpawnTime = 0;
let lastAmmoCrateSpawnTime = 0;
let animationFrameId = null;
let infoPanel, winnerMessageDiv;
let uiHealthSpans = [], uiScoreSpans = [], uiAmmoSpans = [], uiWeaponSpans = [], uiPlayerBlocks = [];
const settingsPanel = document.getElementById('settings-panel');
const numPlayersInput = document.getElementById('num-players');
const startHealthInput = document.getElementById('start-health');
const initialAmmoInput = document.getElementById('initial-ammo');
const gameDurationInput = document.getElementById('game-duration');
const wallDensityInput = document.getElementById('wall-density');
const ammoCrateIntervalInput = document.getElementById('ammo-crate-interval');
const controlsContainer = document.getElementById('controls-container');
const startButton = document.getElementById('start-button');
numPlayersInput.addEventListener('change', () => {
const count = parseInt(numPlayersInput.value, 10);
for (let i = 1; i <= 4; i++) {
const controlsDiv = document.getElementById(`player${i}-controls`);
if (controlsDiv) controlsDiv.style.display = (i <= count) ? 'block' : 'none';
}
});
numPlayersInput.dispatchEvent(new Event('change'));
startButton.addEventListener('click', startGame);
function readSettings() {
numPlayers = Math.max(2, Math.min(4, parseInt(numPlayersInput.value, 10) || 2));
startHealth = Math.max(10, parseInt(startHealthInput.value, 10) || 100);
initialAmmoSetting = Math.max(5, parseInt(initialAmmoInput.value, 10) || 30);
gameDurationSetting = Math.max(30, parseInt(gameDurationInput.value, 10) || 120);
wallDensitySetting = Math.max(0, Math.min(10, parseInt(wallDensityInput.value, 10) || 4));
ammoCrateSpawnIntervalSetting = Math.max(5, parseInt(ammoCrateIntervalInput.value, 10) || 15);
playerControls = [];
for (let i = 1; i <= numPlayers; i++) {
const controls = { id: i };
controls.forward = document.getElementById(`p${i}-forward`).value.trim() || `KeyW`;
controls.backward = document.getElementById(`p${i}-backward`).value.trim() || `KeyS`;
controls.left = document.getElementById(`p${i}-left`).value.trim() || `KeyA`;
controls.right = document.getElementById(`p${i}-right`).value.trim() || `KeyD`;
controls.fire = document.getElementById(`p${i}-fire`).value.trim() || `Space`;
if (i === 2 && !playerControls.some(c => c.forward === 'ArrowUp')) {
controls.forward = document.getElementById(`p${i}-forward`).value.trim() || `ArrowUp`;
controls.backward = document.getElementById(`p${i}-backward`).value.trim() || `ArrowDown`;
controls.left = document.getElementById(`p${i}-left`).value.trim() || `ArrowLeft`;
controls.right = document.getElementById(`p${i}-right`).value.trim() || `ArrowRight`;
controls.fire = document.getElementById(`p${i}-fire`).value.trim() || `Enter`;
} else if (i === 3 && !playerControls.some(c => c.forward === 'Numpad8')) {
controls.forward = document.getElementById(`p${i}-forward`).value.trim() || `Numpad8`;
controls.backward = document.getElementById(`p${i}-backward`).value.trim() || `Numpad5`;
controls.left = document.getElementById(`p${i}-left`).value.trim() || `Numpad4`;
controls.right = document.getElementById(`p${i}-right`).value.trim() || `Numpad6`;
controls.fire = document.getElementById(`p${i}-fire`).value.trim() || `Numpad0`;
} else if (i === 4 && !playerControls.some(c => c.forward === 'KeyI')) {
controls.forward = document.getElementById(`p${i}-forward`).value.trim() || `KeyI`;
controls.backward = document.getElementById(`p${i}-backward`).value.trim() || `KeyK`;
controls.left = document.getElementById(`p${i}-left`).value.trim() || `KeyJ`;
controls.right = document.getElementById(`p${i}-right`).value.trim() || `KeyL`;
controls.fire = document.getElementById(`p${i}-fire`).value.trim() || `Semicolon`;
}
playerControls.push(controls);
}
console.log("Using Settings:", { numPlayers, startHealth, initialAmmoSetting, gameDurationSetting, wallDensitySetting, ammoCrateSpawnIntervalSetting });
console.log("Using Controls:", playerControls);
}
function startGame() {
if (gameStarted) return;
readSettings();
document.body.classList.remove('state-settings', 'state-gameover');
document.body.classList.add('state-playing');
gameStarted = true;
init();
}
function init() {
gameContainer = document.getElementById('game-container');
clock = new THREE.Clock();
gameStartTime = clock.getElapsedTime();
lastPowerupSpawnTime = gameStartTime - POWERUP_SPAWN_INTERVAL + 3;
lastAmmoCrateSpawnTime = gameStartTime - ammoCrateSpawnIntervalSetting + 5;
gameOver = false;
infoPanel = document.getElementById('info');
winnerMessageDiv = document.getElementById('winner-message');
winnerMessageDiv.style.display = 'none';
timeLeftSpan = document.getElementById('time-left');
timeLeftSpan.textContent = gameDurationSetting;
uiHealthSpans = []; uiScoreSpans = []; uiAmmoSpans = []; uiWeaponSpans = []; uiPlayerBlocks = [];
for(let i=1; i<=4; i++){
const block = document.getElementById(`player${i}-info-block`);
uiPlayerBlocks.push(block);
uiHealthSpans.push(document.getElementById(`p${i}-health`));
uiScoreSpans.push(document.getElementById(`p${i}-score`));
uiAmmoSpans.push(document.getElementById(`p${i}-ammo`));
uiWeaponSpans.push(document.getElementById(`p${i}-weapon`));
if(block) block.classList.toggle('hidden', i > numPlayers);
if (block) block.classList.remove('respawning', 'destroyed', 'low-health', 'ammo-low', 'ammo-out');
}
fireSound = document.getElementById('fire-sound');
hitSound = document.getElementById('hit-sound');
wallHitSound = document.getElementById('wall-hit-sound');
wallDestroySound = document.getElementById('wall-destroy-sound');
audioInitialized = false;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x6688aa);
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1500);
camera.position.set(0, GROUND_SIZE * 0.8, 0);
camera.lookAt(scene.position);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
gameContainer.innerHTML = '';
gameContainer.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.7);
directionalLight.position.set(GROUND_SIZE*0.4, GROUND_SIZE*0.6, GROUND_SIZE*0.3);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 10; directionalLight.shadow.camera.far = GROUND_SIZE * 1.5;
directionalLight.shadow.camera.left = -GROUND_SIZE*0.8; directionalLight.shadow.camera.right = GROUND_SIZE*0.8;
directionalLight.shadow.camera.top = GROUND_SIZE*0.8; directionalLight.shadow.camera.bottom = -GROUND_SIZE*0.8;
scene.add(directionalLight);
const groundGeometry = new THREE.PlaneGeometry(GROUND_SIZE, GROUND_SIZE);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x99aa88, roughness: 0.9, metalness: 0.1 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
walls.forEach(wallData => scene.remove(wallData.mesh)); walls = [];
generateWalls(wallDensitySetting);
tanks = []; bullets = []; activePowerups = [];
const startPositions = generateStartPositions(numPlayers, GROUND_SIZE * 0.7);
const tankColors = [0x4caf50, 0x2196f3, 0xff9800, 0x9c27b0];
for (let i = 0; i < numPlayers; i++) {
createTank(tankColors[i % tankColors.length], startPositions[i], i + 1);
}
updateAllUI();
document.removeEventListener('keydown', onKeyDown); document.removeEventListener('keyup', onKeyUp); window.removeEventListener('resize', onWindowResize);
document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); window.addEventListener('resize', onWindowResize);
if(animationFrameId) cancelAnimationFrame(animationFrameId);
animate();
}
function generateStartPositions(count, radius) {
const positions = []; const angleIncrement = (Math.PI * 2) / count;
for (let i = 0; i < count; i++) {
const angle = i * angleIncrement;
const spawnRadius = Math.max(radius * 0.5, MIN_WALL_SPAWN_DIST_FROM_CENTER + 5);
const x = Math.cos(angle) * spawnRadius; const z = Math.sin(angle) * spawnRadius;
positions.push(new THREE.Vector3(x, 0, z));
} return positions;
}
function createTank(color, position, playerId) {
const tank = new THREE.Group();
const baseMaterial = new THREE.MeshStandardMaterial({ color: color, roughness: 0.5, metalness: 0.3 });
const damagedMaterial1 = baseMaterial.clone(); damagedMaterial1.color.multiplyScalar(0.8); damagedMaterial1.roughness = 0.7;
const damagedMaterial2 = baseMaterial.clone(); damagedMaterial2.color.multiplyScalar(0.6); damagedMaterial2.roughness = 0.9; damagedMaterial2.metalness = 0.1;
const bodyGeometry=new THREE.BoxGeometry(4.5,1.5,6);const body=new THREE.Mesh(bodyGeometry,baseMaterial.clone());body.position.y=.75;body.castShadow=true;body.receiveShadow=true;body.name="tank_body";tank.add(body);const turretGeometry=new THREE.CylinderGeometry(1.8,1.5,1.2,16);const turretMaterial=new THREE.MeshStandardMaterial({color:0x666666,roughness:.4,metalness:.6});const turret=new THREE.Mesh(turretGeometry,turretMaterial);turret.position.y=2.1;turret.castShadow=true;turret.receiveShadow=true;tank.add(turret);const barrelGeometry=new THREE.BoxGeometry(.6,.6,4.5);const barrelMaterial=new THREE.MeshStandardMaterial({color:0x444444,roughness:.3,metalness:.7});const barrel=new THREE.Mesh(barrelGeometry,barrelMaterial);barrel.position.y=2.1;barrel.position.z=2.55;barrel.castShadow=true;tank.add(barrel);
tank.position.copy(position); tank.position.y = 0;
tank.rotation.y = Math.atan2(-position.x, -position.z);
tank.userData = {
playerId: playerId, health: startHealth, maxHealth: startHealth, ammo: initialAmmoSetting, score: 0, lastShotTime: -BASE_RELOAD_TIME,
forward: 0, turn: 0, speedMultiplier: 1.0, reloadMultiplier: 1.0, materials: { clean: baseMaterial, damaged1: damagedMaterial1, damaged2: damagedMaterial2 },
currentDamageState: 0, activeWeapon: null, weaponEndTime: 0, missilesLeft: 0, waitingForSecondShot: false, secondShotTime: 0, isRespawning: false, respawnTime: 0
};
tank.name = `TankGroup_Player${playerId}`; scene.add(tank); tanks[playerId - 1] = tank;
}
function generateWalls(density) {
if (density <= 0) return;
const cellSize = 10; const gridMax = Math.floor(WALL_BOUNDARY_LIMIT / cellSize);
const wallProbability = density / 15;
const wallGeometry = new THREE.BoxGeometry(WALL_SEGMENT_LENGTH, WALL_HEIGHT, WALL_THICKNESS);
const dirtMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513, roughness: 0.9, metalness: 0 });
const steelMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.4, metalness: 0.8 });
console.log(`Generating walls with density ${density}...`);
for (let gx = -gridMax; gx <= gridMax; gx++) {
for (let gz = -gridMax; gz <= gridMax; gz++) {
const cellCenterX = gx * cellSize + cellSize / 2; const cellCenterZ = gz * cellSize + cellSize / 2;
if (Math.sqrt(cellCenterX*cellCenterX + cellCenterZ*cellCenterZ) < MIN_WALL_SPAWN_DIST_FROM_CENTER) continue;
if (Math.random() < wallProbability) {
const isSteel = Math.random() < 0.3; const wallType = isSteel ? WALL_TYPES.STEEL : WALL_TYPES.DIRT;
const wallHealth = isSteel ? STEEL_WALL_HEALTH : DIRT_WALL_HEALTH; const material = isSteel ? steelMaterial : dirtMaterial;
const wallMesh = new THREE.Mesh(wallGeometry, material.clone());
wallMesh.position.set(cellCenterX, WALL_HEIGHT / 2, cellCenterZ); wallMesh.rotation.y = Math.random() < 0.5 ? 0 : Math.PI / 2;
wallMesh.castShadow = true; wallMesh.receiveShadow = true;
let overlap = false; const minDistSq = (WALL_SEGMENT_LENGTH * 0.6) ** 2;
for(const existingWall of walls){ if(wallMesh.position.distanceToSquared(existingWall.mesh.position) < minDistSq){ overlap = true; break; } }
if(!overlap){ walls.push({ mesh: wallMesh, type: wallType, health: wallHealth }); scene.add(wallMesh); }
}
}
} console.log(`Generated ${walls.length} wall segments.`);
}
function onKeyDown(event) {
if (gameOver || !gameStarted) return;
if (!audioInitialized) {
const sounds = [fireSound, hitSound, wallHitSound, wallDestroySound].filter(s => s);
let promiseChain = Promise.resolve();
sounds.forEach(sound => { promiseChain = promiseChain.then(() => sound.play()).then(() => { sound.pause(); sound.currentTime = 0; }).catch(e => console.warn("Audio init failed:", e)); });
promiseChain.then(() => { audioInitialized = true; console.log("Audio context initialized."); });
}
keysPressed[event.code] = true;
for (let i = 0; i < numPlayers; i++) {
if (playerControls[i] && tanks[i] && !tanks[i].userData.isRespawning && event.code === playerControls[i].fire && !event.repeat) { fireBullet(tanks[i]); break; }
}
}
function onKeyUp(event) { if (!gameStarted) return; keysPressed[event.code] = false; }
function onWindowResize() { if (!renderer || !camera) return; camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
function spawnRegularPowerup() {
if(activePowerups.length >= MAX_ACTIVE_POWERUPS || POWERUP_TYPES.length === 0) return;
const typeIndex = Math.floor(Math.random() * POWERUP_TYPES.length);
const type = POWERUP_TYPES[typeIndex];
const color = POWERUP_COLORS[type];
spawnPowerupObject(type, color);
}
function spawnAmmoCrate() {
if(activePowerups.length >= MAX_ACTIVE_POWERUPS) return;
spawnPowerupObject('ammo', POWERUP_COLORS['ammo']);
console.log("Spawned Ammo Crate");
}
function spawnPowerupObject(type, color) {
const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
const material = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.4, metalness: 0.1, roughness: 0.6 });
const powerupMesh = new THREE.Mesh(geometry, material);
powerupMesh.position.x = THREE.MathUtils.randFloat(-BOUNDARY_LIMIT, BOUNDARY_LIMIT);
powerupMesh.position.z = THREE.MathUtils.randFloat(-BOUNDARY_LIMIT, BOUNDARY_LIMIT);
powerupMesh.position.y = 1;
powerupMesh.castShadow = true;
const powerupData = { id: nextPowerupId++, mesh: powerupMesh, type: type, spawnTime: clock.getElapsedTime() };
activePowerups.push(powerupData);
scene.add(powerupMesh);
}
function updatePowerupsAndAmmoCrates(deltaTime, currentTime) {
if(currentTime - lastPowerupSpawnTime > POWERUP_SPAWN_INTERVAL){
spawnRegularPowerup();
lastPowerupSpawnTime = currentTime;
}
if(currentTime - lastAmmoCrateSpawnTime > ammoCrateSpawnIntervalSetting){
spawnAmmoCrate();
lastAmmoCrateSpawnTime = currentTime;
}
for(let i = activePowerups.length - 1; i >= 0; i--){
const powerup = activePowerups[i];
powerup.mesh.rotation.y += deltaTime * 1.5;
powerup.mesh.rotation.x += deltaTime * 0.5;
if(currentTime - powerup.spawnTime > POWERUP_LIFETIME){ scene.remove(powerup.mesh); activePowerups.splice(i, 1); }
}
}
function fireBullet(tank) {
if (!tank || !tank.userData || tank.userData.health <= 0 || tank.userData.isRespawning || tank.userData.waitingForSecondShot) return;
if (tank.userData.ammo <= 0) return;
const now = clock.getElapsedTime();
let effectiveReloadTime = BASE_RELOAD_TIME * tank.userData.reloadMultiplier;
const activeWeapon = tank.userData.activeWeapon;
if (activeWeapon === 'missile') {
if (tank.userData.missilesLeft > 0) {
let targetTank = null; let minDistanceSq = Infinity;
for (let i = 0; i < numPlayers; i++) { const otherTank = tanks[i]; if (otherTank && otherTank.userData && otherTank.userData.playerId !== tank.userData.playerId && !otherTank.userData.isRespawning && otherTank.userData.health > 0) { const distSq = tank.position.distanceToSquared(otherTank.position); if (distSq < minDistanceSq) { minDistanceSq = distSq; targetTank = otherTank; } } }
if(targetTank){
const missileGeometry = new THREE.SphereGeometry(0.5, 12, 12); const missileMaterial = new THREE.MeshLambertMaterial({ color: 0xff3300, emissive: 0xcc3300 }); const missile = new THREE.Mesh(missileGeometry, missileMaterial); const barrel = tank.children.find(child => child.geometry instanceof THREE.BoxGeometry && child.position.z > 1); if (!barrel) return; const barrelWorldPosition = new THREE.Vector3(); barrel.getWorldPosition(barrelWorldPosition); missile.position.copy(barrelWorldPosition); const initialDirection = targetTank.position.clone().sub(missile.position).normalize(); missile.userData = { ownerId: tank.userData.playerId, isMissile: true, targetTank: targetTank, velocity: initialDirection.multiplyScalar(MISSILE_SPEED), spawnTime: now, maxLifetime: MISSILE_LIFETIME }; missile.castShadow = true; bullets.push(missile); scene.add(missile);
tank.userData.missilesLeft = 0; tank.userData.activeWeapon = null; tank.userData.weaponEndTime = 0; updateWeaponUI();
if (fireSound && audioInitialized) { fireSound.currentTime = 0; fireSound.volume = 0.9; fireSound.play().catch(e => {}); }
tank.userData.lastShotTime = now;
tank.userData.ammo--; updateAmmoUI();
return;
} else { tank.userData.missilesLeft = 0; tank.userData.activeWeapon = null; tank.userData.weaponEndTime = 0; updateWeaponUI(); return; }
} else { tank.userData.activeWeapon = null; tank.userData.weaponEndTime = 0; updateWeaponUI(); }
}
if (now - tank.userData.lastShotTime < effectiveReloadTime) return;
if (fireSound && audioInitialized) { fireSound.currentTime=0;fireSound.volume=.7;fireSound.play().catch(e=>{}); }
tank.userData.ammo--; updateAmmoUI();
if (activeWeapon === 'shotgun') spawnShotgunBlast(tank, now);
else if (activeWeapon === 'double') { spawnSingleBullet(tank, now); tank.userData.waitingForSecondShot = true; tank.userData.secondShotTime = now + DOUBLE_SHOT_DELAY; }
else spawnSingleBullet(tank, now);
tank.userData.lastShotTime = now;
}
function spawnSingleBullet(tank, time, overrideDirection = null) {
const bulletGeometry = new THREE.SphereGeometry(.4, 10, 10); const bulletMaterial = new THREE.MeshBasicMaterial({color: 0xffff00 }); const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); const barrel = tank.children.find(child => child.geometry instanceof THREE.BoxGeometry && child.position.z > 1); if (!barrel) return; const barrelWorldPosition = new THREE.Vector3(); barrel.getWorldPosition(barrelWorldPosition); const direction = overrideDirection ? overrideDirection.clone() : (new THREE.Vector3(0, 0, 1)).applyQuaternion(tank.quaternion); const barrelLength = barrel.geometry.parameters.depth; bullet.position.copy(barrelWorldPosition).add(direction.clone().multiplyScalar(barrelLength * 0.6)); bullet.userData = { ownerId: tank.userData.playerId, velocity: direction.normalize().multiplyScalar(BULLET_SPEED) }; bullets.push(bullet); scene.add(bullet); setTimeout(() => { if (scene.getObjectById(bullet.id)) { scene.remove(bullet); bullets = bullets.filter(b => b && b.id !== bullet.id); } }, 4000);
}
function spawnShotgunBlast(tank, time) {
const baseDirection = (new THREE.Vector3(0, 0, 1)).applyQuaternion(tank.quaternion); const tankUp = (new THREE.Vector3(0, 1, 0)).applyQuaternion(tank.quaternion); for (let i = 0; i < SHOTGUN_BULLET_COUNT; i++) { const angle = (i - Math.floor(SHOTGUN_BULLET_COUNT / 2)) * SHOTGUN_SPREAD_ANGLE * (0.6 + Math.random() * 0.8); const spreadDirection = baseDirection.clone().applyAxisAngle(tankUp, angle); spawnSingleBullet(tank, time, spreadDirection); }
}
function updateTanks(deltaTime, currentTime) {
tanks.forEach(tank => {
if (!tank || !tank.userData || tank.userData.isRespawning) return;
if (tank.userData.activeWeapon && tank.userData.activeWeapon !== 'missile' && tank.userData.activeWeapon !== 'health' && tank.userData.activeWeapon !== 'ammo') {
if (currentTime > tank.userData.weaponEndTime) { tank.userData.activeWeapon = null; tank.userData.weaponEndTime = 0; updateWeaponUI(); }
}
if (tank.userData.waitingForSecondShot && currentTime >= tank.userData.secondShotTime) { spawnSingleBullet(tank, currentTime); tank.userData.waitingForSecondShot = false; if (fireSound && audioInitialized) { fireSound.currentTime=0;fireSound.volume=.6;fireSound.play().catch(e=>{}); } }
});
for (let i = 0; i < numPlayers; i++) {
const tank = tanks[i];
if (!tank || !tank.userData || tank.userData.isRespawning) continue;
let targetForward = 0; let targetTurn = 0; const controls = playerControls[i]; if (!controls) continue;
if (keysPressed[controls.forward]) targetForward = 1; else if (keysPressed[controls.backward]) targetForward = -1;
if (keysPressed[controls.left]) targetTurn = 1; else if (keysPressed[controls.right]) targetTurn = -1;
tank.userData.forward = targetForward; tank.userData.turn = targetTurn;
if (tank.userData.turn !== 0) { tank.rotation.y += tank.userData.turn * TANK_TURN_SPEED * deltaTime; }
if (tank.userData.forward !== 0) {
const currentSpeed = BASE_TANK_SPEED * tank.userData.speedMultiplier; let moveDistance = tank.userData.forward * currentSpeed * deltaTime;
const moveDirection = new THREE.Vector3(0, 0, 1).applyQuaternion(tank.quaternion); const potentialPosition = tank.position.clone().add(moveDirection.multiplyScalar(moveDistance));
let collision = false; const tankBox = new THREE.Box3(); const tankSize = new THREE.Vector3(4.5, 2.5, 6.5); tankBox.setFromCenterAndSize(potentialPosition.clone().add(new THREE.Vector3(0, tankSize.y / 2, 0)), tankSize);
for (const wallData of walls) { const wallBox = new THREE.Box3().setFromObject(wallData.mesh); if (tankBox.intersectsBox(wallBox)) { collision = true; break; } }
if (!collision) {
tank.position.copy(potentialPosition);
const limit = BOUNDARY_LIMIT; tank.position.x = Math.max(-limit, Math.min(limit, tank.position.x)); tank.position.z = Math.max(-limit, Math.min(limit, tank.position.z)); tank.position.y = 0;
}
}
const tankRadius = 2.5;
for (let j = activePowerups.length - 1; j >= 0; j--) {
const powerup = activePowerups[j]; const distanceSq = tank.position.distanceToSquared(powerup.mesh.position); const pickupRadiusSq = (tankRadius + 1.0) * (tankRadius + 1.0);
if (distanceSq < pickupRadiusSq) {
console.log(`Player ${tank.userData.playerId} collected ${powerup.type}!`);
if (powerup.type === 'health') { tank.userData.health = Math.min(tank.userData.maxHealth, tank.userData.health + HEALTH_POTION_AMOUNT); updateDamageVisuals(tank); updateHealthDisplay(); }
else if (powerup.type === 'ammo') { tank.userData.ammo += AMMO_CRATE_AMOUNT; updateAmmoUI(); }
else { tank.userData.activeWeapon = powerup.type; tank.userData.waitingForSecondShot = false; if (powerup.type === 'missile') { tank.userData.missilesLeft = 1; tank.userData.weaponEndTime = Infinity; } else { tank.userData.missilesLeft = 0; tank.userData.weaponEndTime = currentTime + WEAPON_DURATION; } updateWeaponUI(); }
scene.remove(powerup.mesh); activePowerups.splice(j, 1); break;
}
}
}
}
function updateBullets(deltaTime) {
const currentTime = clock.getElapsedTime();
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i]; if (!bullet || !bullet.userData) { if(bullet && scene.getObjectById(bullet.id)) scene.remove(bullet); bullets.splice(i, 1); continue; }
if (bullet.userData.isMissile) { if (currentTime - bullet.userData.spawnTime > bullet.userData.maxLifetime) { if(scene.getObjectById(bullet.id)) scene.remove(bullet); bullets.splice(i, 1); continue; } let targetTank = bullet.userData.targetTank; let targetIsValid = false; if (targetTank && targetTank.userData && !targetTank.userData.isRespawning && targetTank.userData.health > 0) { const currentTargetRef = tanks[targetTank.userData.playerId - 1]; if (currentTargetRef === targetTank) { targetIsValid = true; } else { targetTank = null; bullet.userData.targetTank = null; } } else { targetTank = null; bullet.userData.targetTank = null; } if (targetIsValid && targetTank) { const missilePos = bullet.position; const targetPos = targetTank.position; const idealDirection = targetPos.clone().sub(missilePos).normalize(); const currentDirection = bullet.userData.velocity.clone().normalize(); const lerpAmount = Math.min(1, MISSILE_TURN_RATE * deltaTime); const newDirection = currentDirection.lerp(idealDirection, lerpAmount).normalize(); bullet.userData.velocity.copy(newDirection.multiplyScalar(MISSILE_SPEED)); bullet.lookAt(bullet.position.clone().add(bullet.userData.velocity)); } }
if (bullet.userData.velocity) { bullet.position.add(bullet.userData.velocity.clone().multiplyScalar(deltaTime)); } else { console.warn("Bullet/Missile missing velocity:", bullet.id); if(scene.getObjectById(bullet.id)) scene.remove(bullet); bullets.splice(i, 1); continue; }
const bulletBoundary = GROUND_SIZE / 2 + 10; if (Math.abs(bullet.position.x) > bulletBoundary || Math.abs(bullet.position.z) > bulletBoundary) { if(scene.getObjectById(bullet.id)) scene.remove(bullet); bullets.splice(i, 1); continue; }
const hit = checkBulletCollision(bullet); if (hit) { if(scene.getObjectById(bullet.id)) scene.remove(bullet); bullets.splice(i, 1); }
}
}
function checkBulletCollision(bullet) {
if (!bullet || !bullet.userData) return false; const bulletOwnerId = bullet.userData.ownerId; const isMissile = bullet.userData.isMissile; const damage = isMissile ? BULLET_DAMAGE * MISSILE_DAMAGE_MULTIPLIER : BULLET_DAMAGE; const bulletRadius = isMissile ? 0.6 : 0.4; const bulletSphere = new THREE.Sphere(bullet.position, bulletRadius);
for (let k = walls.length - 1; k >= 0; k--) { const wallData = walls[k]; if (!wallData || !wallData.mesh) continue; const wallBox = new THREE.Box3().setFromObject(wallData.mesh); if (wallBox.intersectsSphere(bulletSphere)) { if (wallHitSound && audioInitialized) { wallHitSound.currentTime=0;wallHitSound.volume=0.6;wallHitSound.play().catch(e=>{}); } wallData.health--; if (wallData.health <= 0) { if (wallDestroySound && audioInitialized) { wallDestroySound.currentTime=0;wallDestroySound.volume=0.8;wallDestroySound.play().catch(e=>{}); } scene.remove(wallData.mesh); walls.splice(k, 1); } return true; } }
for (let j = 0; j < numPlayers; j++) { const tank = tanks[j]; if (!tank || !tank.userData || tank.userData.isRespawning || tank.userData.health <= 0 || tank.userData.playerId === bulletOwnerId) continue; const tankBox = new THREE.Box3().setFromObject(tank); if (tankBox.intersectsSphere(bulletSphere)) { if (hitSound && audioInitialized) { hitSound.currentTime=0;hitSound.volume=.8;hitSound.play().catch(e=>{}); } tank.userData.health -= damage; tank.userData.health = Math.max(0, tank.userData.health); updateDamageMultipliers(tank); updateDamageVisuals(tank); updateHealthDisplay(); if (tank.userData.health <= 0) { startRespawn(tank); const attackerTank = tanks[bulletOwnerId - 1]; if (attackerTank && attackerTank.userData) { attackerTank.userData.score++; updateScoreUI(); } } return true; } } return false;
}
function startRespawn(tank) {
if (!tank || !tank.userData) return; tank.visible = false; tank.userData.isRespawning = true; tank.userData.respawnTime = clock.getElapsedTime() + RESPAWN_DELAY; tank.userData.activeWeapon = null; tank.userData.missilesLeft = 0; tank.userData.weaponEndTime = 0; tank.userData.waitingForSecondShot = false; updateWeaponUI(); updatePlayerBlockStyle(tank.userData.playerId, true); updateHealthDisplay(); updateAmmoUI(); console.log(`Player ${tank.userData.playerId} respawning...`);
}
function updateRespawns(currentTime) {
for (let i = 0; i < numPlayers; i++) { const tank = tanks[i]; if (tank && tank.userData && tank.userData.isRespawning && currentTime >= tank.userData.respawnTime) { completeRespawn(tank); } }
}
function completeRespawn(tank) {
console.log(`Player ${tank.userData.playerId} respawned!`); tank.userData.health = tank.userData.maxHealth; tank.userData.ammo = initialAmmoSetting; tank.userData.isRespawning = false; tank.visible = true; updatePlayerBlockStyle(tank.userData.playerId, false); tank.userData.currentDamageState = 0; updateDamageMultipliers(tank); const bodyMesh = tank.children.find(child => child.name === "tank_body"); if (bodyMesh) bodyMesh.material = tank.userData.materials.clean;
let safePositionFound = false; let attempt = 0; const maxAttempts = 30; const minSpawnDistSqTank = 15 * 15; const tankSizeApprox = new THREE.Vector3(5, 3, 7);
while (!safePositionFound && attempt < maxAttempts) { attempt++; const randomX = THREE.MathUtils.randFloat(-BOUNDARY_LIMIT * 0.9, BOUNDARY_LIMIT * 0.9); const randomZ = THREE.MathUtils.randFloat(-BOUNDARY_LIMIT * 0.9, BOUNDARY_LIMIT * 0.9); const potentialPos = new THREE.Vector3(randomX, 0, randomZ); let isSafe = true;
for(let j=0; j<numPlayers; j++){ const otherTank = tanks[j]; if (otherTank && otherTank.userData && otherTank !== tank && otherTank.visible && !otherTank.userData.isRespawning) { if (potentialPos.distanceToSquared(otherTank.position) < minSpawnDistSqTank) { isSafe = false; break; } } } if (!isSafe) continue;
const spawnBox = new THREE.Box3().setFromCenterAndSize(potentialPos.clone().add(new THREE.Vector3(0, tankSizeApprox.y / 2, 0)), tankSizeApprox); for (const wallData of walls) { const wallBox = new THREE.Box3().setFromObject(wallData.mesh); if (spawnBox.intersectsBox(wallBox)) { isSafe = false; break; } }
if (isSafe) { tank.position.copy(potentialPos); tank.rotation.y = Math.random() * Math.PI * 2; safePositionFound = true; }
} if (!safePositionFound) { console.warn(`Could not find safe spawn for Player ${tank.userData.playerId}, spawning near center.`); tank.position.set(Math.random()*5 - 2.5, 0, Math.random()*5 - 2.5); tank.rotation.y = Math.random() * Math.PI * 2; }
updateAllUI();
}
function updateDamageMultipliers(tank) { if (!tank || !tank.userData) return; const healthPercent = Math.max(0, tank.userData.health) / tank.userData.maxHealth; tank.userData.speedMultiplier = MIN_SPEED_MULTIPLIER + (1 - MIN_SPEED_MULTIPLIER) * healthPercent; tank.userData.reloadMultiplier = 1 + (MAX_RELOAD_MULTIPLIER - 1) * (1 - healthPercent); }
function updateDamageVisuals(tank) { if (!tank || !tank.userData || tank.userData.isRespawning) return; let newDamageState = 0; const currentHealth = tank.userData.health; const maxHealth = tank.userData.maxHealth; if (currentHealth <= DAMAGE_THRESHOLD_2 * maxHealth / 100) newDamageState = 2; else if (currentHealth <= DAMAGE_THRESHOLD_1 * maxHealth / 100) newDamageState = 1; if (newDamageState !== tank.userData.currentDamageState) { tank.userData.currentDamageState = newDamageState; const bodyMesh = tank.children.find(child => child.name === "tank_body"); if (bodyMesh) { if (newDamageState === 2) bodyMesh.material = tank.userData.materials.damaged2; else if (newDamageState === 1) bodyMesh.material = tank.userData.materials.damaged1; else bodyMesh.material = tank.userData.materials.clean; } } }
function updateAllUI() { if (!gameStarted && !gameOver) return; updateHealthDisplay(); updateScoreUI(); updateAmmoUI(); updateWeaponUI(); }
function updateHealthDisplay() { for (let i = 0; i < numPlayers; i++) { const span = uiHealthSpans[i]; const block = uiPlayerBlocks[i]; const tank = tanks[i]; if (!span || !block) continue; block.classList.remove('low-health', 'destroyed'); if (tank && tank.userData && !tank.userData.isRespawning) { const health = tank.userData.health; span.textContent = health; if (health <= DAMAGE_THRESHOLD_2 * tank.userData.maxHealth / 100) block.classList.add('low-health'); } else if (tank && tank.userData && tank.userData.isRespawning) { span.textContent = "重生中"; block.classList.add('destroyed'); } else { span.textContent = "----"; block.classList.add('destroyed'); } } }
function updateScoreUI() { for (let i = 0; i < numPlayers; i++) { const span = uiScoreSpans[i]; const tank = tanks[i]; if (span && tank && tank.userData) { span.textContent = tank.userData.score; } else if (span) { span.textContent = "-"; } } }
function updateAmmoUI() { for (let i = 0; i < numPlayers; i++) { const span = uiAmmoSpans[i]; const block = uiPlayerBlocks[i]; const tank = tanks[i]; if (!span || !block) continue; block.classList.remove('ammo-low', 'ammo-out'); if (tank && tank.userData && !tank.userData.isRespawning) { const ammo = tank.userData.ammo; span.textContent = ammo; if (ammo <= 0) block.classList.add('ammo-out'); else if (ammo <= 5) block.classList.add('ammo-low'); } else if (tank && tank.userData && tank.userData.isRespawning) { span.textContent = "--"; } else { span.textContent = "--"; } } }
function updateWeaponUI() { const now = clock.getElapsedTime(); const weaponNames = { shotgun: '霰弹枪', double: '双连发', missile: '导弹 (1)', health: '生命恢复', ammo: '弹药补充' }; for (let index = 0; index < numPlayers; index++) { const span = uiWeaponSpans[index]; if (!span) continue; const tank = tanks[index]; const weaponInfoDiv = span.closest('.weapon-info'); if(weaponInfoDiv) weaponInfoDiv.classList.remove('weapon-active'); if (tank && tank.userData && tank.userData.activeWeapon) { let name = weaponNames[tank.userData.activeWeapon] || '未知'; let timeLeft = ''; if (tank.userData.activeWeapon === 'missile') { if (tank.userData.missilesLeft <= 0) name = '无'; } else if (tank.userData.activeWeapon === 'health' || tank.userData.activeWeapon === 'ammo') { name = '无'; } else { const remaining = Math.max(0, tank.userData.weaponEndTime - now); if (remaining > 0) timeLeft = ` (${Math.ceil(remaining)}s)`; else name = '无'; } span.textContent = name + timeLeft; if (name !== '无' && weaponInfoDiv) weaponInfoDiv.classList.add('weapon-active'); } else { span.textContent = '无'; } } }
function updatePlayerBlockStyle(playerId, isRespawning) { const block = uiPlayerBlocks[playerId - 1]; if (block) { block.classList.toggle('respawning', isRespawning); updateHealthDisplay(); updateAmmoUI(); } }
function announceWinner(message) { if (gameOver) return; console.log("Game Over:", message); gameOver = true; gameStarted = false; document.body.classList.remove('state-playing'); document.body.classList.add('state-gameover'); winnerMessageDiv.textContent = message; winnerMessageDiv.style.display = 'block'; if (fireSound) fireSound.pause(); if (hitSound) hitSound.pause(); if(wallHitSound) wallHitSound.pause(); if(wallDestroySound) wallDestroySound.pause(); if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } }
function animate() {
animationFrameId = requestAnimationFrame(animate);
if (gameOver || !gameStarted) { if (gameOver && animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } return; }
const deltaTime = Math.min(0.05, clock.getDelta());
const currentTime = clock.getElapsedTime();
let remainingTime = gameDurationSetting - (currentTime - gameStartTime); remainingTime = Math.max(0, remainingTime);
if (timeLeftSpan) timeLeftSpan.textContent = Math.ceil(remainingTime);
if (remainingTime <= 0 && !gameOver) {
let highestScore = -1; let winners = [];
for(let i=0; i<numPlayers; i++){ const tank = tanks[i]; if(tank && tank.userData){ if(tank.userData.score > highestScore){ highestScore = tank.userData.score; winners = [tank.userData.playerId]; } else if (tank.userData.score === highestScore && highestScore !== -1) { winners.push(tank.userData.playerId); } } }
let winMessage = "时间到! "; if (winners.length === 0 || highestScore < 0) { winMessage += "没有玩家得分!"; } else if (winners.length === 1) { winMessage += `玩家 ${winners[0]} 以 ${highestScore} 分获胜!`; } else { winMessage += `玩家 ${winners.join(' 和 ')} 以 ${highestScore} 分平局!`; } announceWinner(winMessage); return;
}
updateRespawns(currentTime);
updatePowerupsAndAmmoCrates(deltaTime, currentTime);
updateTanks(deltaTime, currentTime);
updateBullets(deltaTime);
if (renderer && scene && camera) { renderer.render(scene, camera); }
}
</script>
</body>
</html>