|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Lake Minnetonka: Sailing Tower Defense</title> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> |
|
<style> |
|
body { |
|
margin: 0; |
|
padding: 0; |
|
background: #1a1a2e; |
|
overflow: hidden; |
|
font-family: 'Georgia', serif; |
|
user-select: none; |
|
} |
|
|
|
canvas { |
|
display: block; |
|
cursor: crosshair; |
|
} |
|
|
|
.ui-container { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
pointer-events: none; |
|
z-index: 100; |
|
} |
|
|
|
.ui-panel { |
|
pointer-events: auto; |
|
} |
|
|
|
.controls { |
|
position: absolute; |
|
top: 20px; |
|
left: 20px; |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 15px; |
|
border-radius: 10px; |
|
border: 2px solid #8B4513; |
|
min-width: 200px; |
|
} |
|
|
|
.controls h3 { |
|
margin: 0 0 10px 0; |
|
color: #daa520; |
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.8); |
|
} |
|
|
|
.controls p { |
|
margin: 5px 0; |
|
font-size: 12px; |
|
text-shadow: 1px 1px 2px rgba(0,0,0,0.8); |
|
} |
|
|
|
.resource-panel { |
|
position: absolute; |
|
top: 20px; |
|
right: 20px; |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 15px; |
|
border-radius: 10px; |
|
border: 2px solid #8B4513; |
|
min-width: 200px; |
|
} |
|
|
|
.tower-shop { |
|
position: absolute; |
|
bottom: 20px; |
|
left: 20px; |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 15px; |
|
border-radius: 10px; |
|
border: 2px solid #8B4513; |
|
display: flex; |
|
gap: 10px; |
|
} |
|
|
|
.tower-button { |
|
background: rgba(139, 69, 19, 0.8); |
|
border: 2px solid #d4af37; |
|
color: #d4af37; |
|
padding: 10px; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
font-family: Georgia, serif; |
|
font-size: 12px; |
|
transition: all 0.3s; |
|
} |
|
|
|
.tower-button:hover { |
|
background: rgba(218, 165, 32, 0.3); |
|
border-color: #daa520; |
|
} |
|
|
|
.tower-button.selected { |
|
background: rgba(218, 165, 32, 0.6); |
|
border-color: #FFD700; |
|
} |
|
|
|
.tower-button.disabled { |
|
opacity: 0.5; |
|
cursor: not-allowed; |
|
} |
|
|
|
.wave-info { |
|
position: absolute; |
|
bottom: 20px; |
|
right: 20px; |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 15px; |
|
border-radius: 10px; |
|
border: 2px solid #8B4513; |
|
} |
|
|
|
.environment-info { |
|
position: absolute; |
|
bottom: 140px; |
|
left: 20px; |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 15px; |
|
border-radius: 10px; |
|
border: 2px solid #8B4513; |
|
} |
|
|
|
.wind-indicator { |
|
position: absolute; |
|
top: 20px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
color: #87CEEB; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 10px; |
|
border-radius: 50%; |
|
border: 2px solid #87CEEB; |
|
width: 80px; |
|
height: 80px; |
|
text-align: center; |
|
font-size: 16px; |
|
} |
|
|
|
.area-indicator { |
|
position: absolute; |
|
top: 120px; |
|
left: 50%; |
|
transform: translateX(-50%); |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 10px; |
|
border-radius: 10px; |
|
border: 2px solid #8B4513; |
|
text-align: center; |
|
min-width: 150px; |
|
} |
|
|
|
.health-bar { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
width: 300px; |
|
height: 20px; |
|
background: rgba(139, 0, 0, 0.8); |
|
border: 2px solid #8B4513; |
|
border-radius: 10px; |
|
overflow: hidden; |
|
} |
|
|
|
.health-fill { |
|
height: 100%; |
|
background: linear-gradient(90deg, #FF6B35, #DAA520); |
|
transition: width 0.5s; |
|
} |
|
|
|
.game-over { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.9); |
|
padding: 30px; |
|
border-radius: 15px; |
|
border: 3px solid #8B4513; |
|
text-align: center; |
|
display: none; |
|
} |
|
|
|
.restart-button { |
|
background: rgba(139, 69, 19, 0.8); |
|
border: 2px solid #d4af37; |
|
color: #d4af37; |
|
padding: 15px 30px; |
|
border-radius: 10px; |
|
cursor: pointer; |
|
font-family: Georgia, serif; |
|
font-size: 16px; |
|
margin-top: 20px; |
|
} |
|
|
|
.cargo-panel { |
|
position: absolute; |
|
top: 180px; |
|
right: 20px; |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 15px; |
|
border-radius: 10px; |
|
border: 2px solid #8B4513; |
|
min-width: 200px; |
|
max-height: 300px; |
|
overflow-y: auto; |
|
} |
|
|
|
.cargo-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin: 5px 0; |
|
padding: 5px; |
|
background: rgba(139, 69, 19, 0.3); |
|
border-radius: 5px; |
|
font-size: 14px; |
|
} |
|
|
|
.cargo-emoji { |
|
font-size: 18px; |
|
margin-right: 8px; |
|
} |
|
|
|
.island-trade-panel { |
|
position: absolute; |
|
bottom: 180px; |
|
right: 20px; |
|
color: #d4af37; |
|
background: rgba(0, 0, 0, 0.9); |
|
padding: 15px; |
|
border-radius: 10px; |
|
border: 2px solid #FFD700; |
|
min-width: 250px; |
|
display: none; |
|
} |
|
|
|
.trade-button { |
|
background: rgba(0, 128, 0, 0.8); |
|
border: 2px solid #32CD32; |
|
color: #d4af37; |
|
padding: 5px 10px; |
|
border-radius: 3px; |
|
cursor: pointer; |
|
font-family: Georgia, serif; |
|
font-size: 12px; |
|
margin: 2px; |
|
} |
|
|
|
.trade-button:hover { |
|
background: rgba(0, 200, 0, 0.8); |
|
} |
|
|
|
.trade-button.buy { |
|
background: rgba(0, 0, 128, 0.8); |
|
border-color: #4169E1; |
|
} |
|
|
|
.trade-button.buy:hover { |
|
background: rgba(0, 0, 200, 0.8); |
|
} |
|
|
|
.island-indicator { |
|
position: absolute; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
color: #FFD700; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 10px 20px; |
|
border-radius: 10px; |
|
border: 2px solid #FFD700; |
|
font-size: 18px; |
|
display: none; |
|
z-index: 200; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="ui-container"> |
|
<div class="controls ui-panel"> |
|
<h3>⛵ Ship Controls</h3> |
|
<p>🎮 WASD: Direct ship movement</p> |
|
<p>🎯 Click: Place towers</p> |
|
<p>⚡ Tab: Cycle towers</p> |
|
<p>🔄 Shift: Boost speed</p> |
|
</div> |
|
|
|
<div class="resource-panel ui-panel"> |
|
<h3>⚓ Ship Status</h3> |
|
<p>💰 Gold: <span id="gold">500</span></p> |
|
<p>🏴☠️ Lives: <span id="lives">3</span></p> |
|
<p>⚔️ Wave: <span id="wave">1</span></p> |
|
<p>🎯 Score: <span id="score">0</span></p> |
|
<p>📦 Cargo: <span id="cargo-count">0</span>/<span id="cargo-capacity">6</span></p> |
|
</div> |
|
|
|
<div class="cargo-panel ui-panel"> |
|
<h3>📦 Cargo Hold</h3> |
|
<div id="cargo-list"> |
|
<p style="font-style: italic; color: #888;">Empty hold</p> |
|
</div> |
|
</div> |
|
|
|
<div class="wind-indicator ui-panel"> |
|
<div id="wind-arrow" style="font-size: 24px;">💨</div> |
|
<div style="font-size: 12px; margin-top: 5px;"> |
|
<span id="wind-speed">5</span> kts |
|
</div> |
|
</div> |
|
|
|
<div class="area-indicator ui-panel"> |
|
<h4 id="current-area">Upper Lake</h4> |
|
<p id="area-effect">Calm Waters</p> |
|
</div> |
|
|
|
<div class="tower-shop ui-panel"> |
|
<button class="tower-button" data-tower="cannon" data-cost="100"> |
|
🏰 Cannon<br/>$100 |
|
</button> |
|
<button class="tower-button" data-tower="harpoon" data-cost="150"> |
|
🎯 Harpoon<br/>$150 |
|
</button> |
|
<button class="tower-button" data-tower="net" data-cost="200"> |
|
🕸️ Net Trap<br/>$200 |
|
</button> |
|
<button class="tower-button" data-tower="lighthouse" data-cost="300"> |
|
🗼 Lighthouse<br/>$300 |
|
</button> |
|
</div> |
|
|
|
<div class="environment-info ui-panel"> |
|
<h4>🌊 Environment</h4> |
|
<p>Ship Speed: <span id="ship-speed">0</span> kts</p> |
|
<p>Tide: <span id="tide-info">Normal</span></p> |
|
<p>Wind Effect: <span id="wind-effect">+0%</span></p> |
|
</div> |
|
|
|
<div class="wave-info ui-panel"> |
|
<h4>🌊 Wave Status</h4> |
|
<p>Enemies: <span id="enemies-left">0</span></p> |
|
<p>Next Wave: <span id="wave-timer">30</span>s</p> |
|
<button id="start-wave" class="tower-button">Start Wave</button> |
|
</div> |
|
|
|
<div class="island-trade-panel ui-panel" id="trade-panel"> |
|
<h3>🏝️ Island Trading Post</h3> |
|
<div id="trade-content"></div> |
|
</div> |
|
|
|
<div class="island-indicator ui-panel" id="island-indicator"> |
|
⚓ Press E to Trade ⚓ |
|
</div> |
|
|
|
<div class="health-bar ui-panel"> |
|
<div class="health-fill" id="health-fill" style="width: 100%"></div> |
|
</div> |
|
|
|
<div class="game-over ui-panel" id="game-over"> |
|
<h2>⚓ Game Over ⚓</h2> |
|
<p>Your fleet has been defeated!</p> |
|
<p>Final Score: <span id="final-score">0</span></p> |
|
<button class="restart-button" onclick="restartGame()">Set Sail Again</button> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
let gameState = { |
|
gold: 500, |
|
lives: 3, |
|
wave: 1, |
|
score: 0, |
|
health: 100, |
|
selectedTower: null, |
|
gameRunning: true, |
|
waveActive: false, |
|
enemies: [], |
|
towers: [], |
|
projectiles: [], |
|
particles: [], |
|
windDirection: 0, |
|
windSpeed: 5, |
|
currentArea: "Upper Lake", |
|
shipSpeed: 0, |
|
cargo: [], |
|
cargoCapacity: 6, |
|
nearIsland: null |
|
}; |
|
|
|
|
|
const cargoTypes = { |
|
fish: { emoji: '🐟', name: 'Fresh Fish', baseValue: 30, rarity: 0.4 }, |
|
lumber: { emoji: '🪵', name: 'Lumber', baseValue: 25, rarity: 0.3 }, |
|
grain: { emoji: '🌾', name: 'Grain', baseValue: 20, rarity: 0.5 }, |
|
gems: { emoji: '💎', name: 'Gems', baseValue: 100, rarity: 0.05 }, |
|
gold: { emoji: '🪙', name: 'Gold Coins', baseValue: 75, rarity: 0.1 }, |
|
spices: { emoji: '🌶️', name: 'Spices', baseValue: 50, rarity: 0.15 }, |
|
pottery: { emoji: '🏺', name: 'Pottery', baseValue: 35, rarity: 0.2 }, |
|
tools: { emoji: '🔨', name: 'Tools', baseValue: 40, rarity: 0.25 }, |
|
cloth: { emoji: '🧶', name: 'Fine Cloth', baseValue: 45, rarity: 0.18 }, |
|
wine: { emoji: '🍷', name: 'Wine', baseValue: 60, rarity: 0.12 } |
|
}; |
|
|
|
|
|
const lakeAreas = { |
|
"Upper Lake": { |
|
center: { x: 0, z: 0 }, |
|
radius: 50, |
|
windMultiplier: 1.0, |
|
tideEffect: 0, |
|
description: "Calm Waters", |
|
color: 0x87CEEB |
|
}, |
|
"Wayzata Bay": { |
|
center: { x: -80, z: -60 }, |
|
radius: 40, |
|
windMultiplier: 1.5, |
|
tideEffect: 0.2, |
|
description: "Strong Winds", |
|
color: 0x4682B4 |
|
}, |
|
"Crystal Bay": { |
|
center: { x: 70, z: -50 }, |
|
radius: 35, |
|
windMultiplier: 0.7, |
|
tideEffect: -0.1, |
|
description: "Sheltered Cove", |
|
color: 0x20B2AA |
|
}, |
|
"Carmans Bay": { |
|
center: { x: -50, z: 80 }, |
|
radius: 30, |
|
windMultiplier: 0.8, |
|
tideEffect: 0.3, |
|
description: "Choppy Waters", |
|
color: 0x008B8B |
|
}, |
|
"Smithtown Bay": { |
|
center: { x: 60, z: 70 }, |
|
radius: 32, |
|
windMultiplier: 1.3, |
|
tideEffect: -0.2, |
|
description: "Swift Currents", |
|
color: 0x5F9EA0 |
|
} |
|
}; |
|
|
|
|
|
const scene = new THREE.Scene(); |
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); |
|
const renderer = new THREE.WebGLRenderer({ antialias: true }); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
renderer.shadowMap.enabled = true; |
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap; |
|
document.body.appendChild(renderer.domElement); |
|
|
|
|
|
let cameraOffset = new THREE.Vector3(0, 25, 30); |
|
|
|
|
|
let playerShip; |
|
let shipRotation = 0; |
|
const keys = {}; |
|
const baseSpeed = 0.4; |
|
|
|
|
|
scene.fog = new THREE.Fog(0x2d1810, 50, 1000); |
|
|
|
|
|
const skyGeometry = new THREE.SphereGeometry(1000, 32, 32); |
|
const skyMaterial = new THREE.ShaderMaterial({ |
|
uniforms: { |
|
time: { value: 0 } |
|
}, |
|
vertexShader: ` |
|
varying vec3 vWorldPosition; |
|
void main() { |
|
vec4 worldPosition = modelMatrix * vec4(position, 1.0); |
|
vWorldPosition = worldPosition.xyz; |
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); |
|
} |
|
`, |
|
fragmentShader: ` |
|
uniform float time; |
|
varying vec3 vWorldPosition; |
|
void main() { |
|
vec3 direction = normalize(vWorldPosition); |
|
float elevation = direction.y; |
|
|
|
vec3 dayBlue = vec3(0.3, 0.6, 0.9); |
|
vec3 horizonGold = vec3(0.9, 0.7, 0.3); |
|
vec3 deepBlue = vec3(0.1, 0.2, 0.4); |
|
|
|
float horizonGlow = exp(-abs(elevation) * 1.5); |
|
vec3 color = mix(deepBlue, dayBlue, elevation + 0.3); |
|
color = mix(color, horizonGold, horizonGlow * 0.6); |
|
|
|
gl_FragColor = vec4(color, 1.0); |
|
} |
|
`, |
|
side: THREE.BackSide |
|
}); |
|
const sky = new THREE.Mesh(skyGeometry, skyMaterial); |
|
scene.add(sky); |
|
|
|
|
|
const lakeGeometry = new THREE.PlaneGeometry(2000, 2000, 200, 200); |
|
const lakeMaterial = new THREE.ShaderMaterial({ |
|
uniforms: { |
|
time: { value: 0 }, |
|
playerPos: { value: new THREE.Vector2(0, 0) } |
|
}, |
|
vertexShader: ` |
|
uniform float time; |
|
uniform vec2 playerPos; |
|
varying vec2 vUv; |
|
varying vec3 vPosition; |
|
varying float vDistanceToPlayer; |
|
|
|
void main() { |
|
vUv = uv; |
|
vec3 pos = position; |
|
|
|
// Distance-based wave intensity |
|
float distToPlayer = length(vec2(pos.x, pos.y) - playerPos); |
|
vDistanceToPlayer = distToPlayer; |
|
|
|
// Area-specific wave patterns |
|
float wave1 = sin(pos.x * 0.02 + time * 0.5) * 0.2; |
|
float wave2 = sin(pos.y * 0.015 + time * 0.7) * 0.15; |
|
float wave3 = sin((pos.x + pos.y) * 0.01 + time * 0.3) * 0.25; |
|
|
|
// Stronger waves in certain areas |
|
float areaEffect = sin(pos.x * 0.005) * sin(pos.y * 0.004) * 0.1; |
|
|
|
pos.z = wave1 + wave2 + wave3 + areaEffect; |
|
|
|
vPosition = pos; |
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); |
|
} |
|
`, |
|
fragmentShader: ` |
|
uniform float time; |
|
uniform vec2 playerPos; |
|
varying vec2 vUv; |
|
varying vec3 vPosition; |
|
varying float vDistanceToPlayer; |
|
|
|
void main() { |
|
vec3 deepWater = vec3(0.1, 0.3, 0.5); |
|
vec3 shallowWater = vec3(0.3, 0.5, 0.7); |
|
vec3 currentWater = vec3(0.2, 0.6, 0.8); |
|
|
|
// Area-based water coloring |
|
float areaBlend = sin(vPosition.x * 0.003 + time * 0.1) * sin(vPosition.y * 0.002 + time * 0.15); |
|
|
|
float wave = sin(vPosition.x * 0.1 + time) * sin(vPosition.y * 0.08 + time); |
|
vec3 baseColor = mix(deepWater, shallowWater, wave * 0.5 + 0.5); |
|
|
|
// Add current effect in certain areas |
|
vec3 finalColor = mix(baseColor, currentWater, areaBlend * 0.3); |
|
|
|
gl_FragColor = vec4(finalColor, 0.8); |
|
} |
|
`, |
|
transparent: true |
|
}); |
|
lakeGeometry.rotateX(-Math.PI / 2); |
|
const lake = new THREE.Mesh(lakeGeometry, lakeMaterial); |
|
scene.add(lake); |
|
|
|
|
|
Object.entries(lakeAreas).forEach(([name, area]) => { |
|
const sectionGeometry = new THREE.RingGeometry(area.radius * 0.9, area.radius, 32); |
|
const sectionMaterial = new THREE.MeshBasicMaterial({ |
|
color: area.color, |
|
transparent: true, |
|
opacity: 0.15, |
|
side: THREE.DoubleSide |
|
}); |
|
const section = new THREE.Mesh(sectionGeometry, sectionMaterial); |
|
section.rotation.x = -Math.PI / 2; |
|
section.position.set(area.center.x, 0.1, area.center.z); |
|
section.userData = { name: name, area: area }; |
|
scene.add(section); |
|
}); |
|
|
|
|
|
function createIsland(x, z, size, name = null) { |
|
const islandGeometry = new THREE.CylinderGeometry(size, size * 1.2, 2, 16); |
|
const islandMaterial = new THREE.MeshLambertMaterial({ color: 0x4d5d3d }); |
|
const island = new THREE.Mesh(islandGeometry, islandMaterial); |
|
island.position.set(x, 0, z); |
|
island.castShadow = true; |
|
|
|
|
|
const availableCargo = []; |
|
const demandedCargo = []; |
|
|
|
Object.keys(cargoTypes).forEach(type => { |
|
if (Math.random() < cargoTypes[type].rarity) { |
|
availableCargo.push({ |
|
type: type, |
|
quantity: Math.floor(Math.random() * 3) + 1, |
|
price: Math.floor(cargoTypes[type].baseValue * (0.7 + Math.random() * 0.6)) |
|
}); |
|
} |
|
|
|
if (Math.random() < 0.3) { |
|
demandedCargo.push({ |
|
type: type, |
|
quantity: Math.floor(Math.random() * 2) + 1, |
|
price: Math.floor(cargoTypes[type].baseValue * (1.2 + Math.random() * 0.8)) |
|
}); |
|
} |
|
}); |
|
|
|
island.userData = { |
|
type: 'island', |
|
name: name || `Island ${Math.floor(Math.random() * 100)}`, |
|
size: size, |
|
availableCargo: availableCargo, |
|
demandedCargo: demandedCargo, |
|
lastRestock: Date.now() |
|
}; |
|
|
|
|
|
availableCargo.forEach((cargo, index) => { |
|
const indicatorGeometry = new THREE.PlaneGeometry(1, 1); |
|
const indicatorMaterial = new THREE.MeshBasicMaterial({ |
|
map: createCargoTexture(cargoTypes[cargo.type].emoji), |
|
transparent: true |
|
}); |
|
const indicator = new THREE.Mesh(indicatorGeometry, indicatorMaterial); |
|
indicator.position.set( |
|
x + (Math.cos(index * Math.PI / 2) * size * 0.7), |
|
3, |
|
z + (Math.sin(index * Math.PI / 2) * size * 0.7) |
|
); |
|
indicator.rotation.x = -Math.PI / 4; |
|
scene.add(indicator); |
|
}); |
|
|
|
scene.add(island); |
|
|
|
|
|
for (let i = 0; i < 3; i++) { |
|
const treeGeometry = new THREE.ConeGeometry(1, 4, 8); |
|
const treeMaterial = new THREE.MeshLambertMaterial({ color: 0x2d4d1d }); |
|
const tree = new THREE.Mesh(treeGeometry, treeMaterial); |
|
tree.position.set( |
|
x + (Math.random() - 0.5) * size * 0.8, |
|
2, |
|
z + (Math.random() - 0.5) * size * 0.8 |
|
); |
|
scene.add(tree); |
|
} |
|
|
|
return island; |
|
} |
|
|
|
|
|
function createCargoTexture(emoji) { |
|
const canvas = document.createElement('canvas'); |
|
canvas.width = 64; |
|
canvas.height = 64; |
|
const context = canvas.getContext('2d'); |
|
context.font = '48px Arial'; |
|
context.textAlign = 'center'; |
|
context.textBaseline = 'middle'; |
|
context.fillText(emoji, 32, 32); |
|
|
|
const texture = new THREE.Texture(canvas); |
|
texture.needsUpdate = true; |
|
return texture; |
|
} |
|
|
|
|
|
const islands = [ |
|
createIsland(-20, -30, 8, "Merchant's Harbor"), |
|
createIsland(30, -20, 6, "Fisher's Cove"), |
|
createIsland(-40, 20, 7, "Timber Point"), |
|
createIsland(25, 35, 5, "Spice Island"), |
|
createIsland(0, -50, 9, "Grand Bazaar"), |
|
createIsland(-60, -10, 6, "Gold Coast"), |
|
createIsland(45, 10, 7, "Pottery Bay"), |
|
createIsland(-70, -50, 5, "Tool Town"), |
|
createIsland(55, -40, 6, "Wine Harbor"), |
|
createIsland(-35, 65, 7, "Gem Isle"), |
|
createIsland(40, 60, 6, "Cloth Port") |
|
]; |
|
|
|
|
|
function createPlayerShip() { |
|
const shipGroup = new THREE.Group(); |
|
|
|
|
|
const hullGeometry = new THREE.BoxGeometry(2.5, 0.6, 8); |
|
const hullMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); |
|
const hull = new THREE.Mesh(hullGeometry, hullMaterial); |
|
hull.position.y = 0.3; |
|
shipGroup.add(hull); |
|
|
|
|
|
const deckGeometry = new THREE.BoxGeometry(2, 0.1, 7); |
|
const deckMaterial = new THREE.MeshLambertMaterial({ color: 0xDEB887 }); |
|
const deck = new THREE.Mesh(deckGeometry, deckMaterial); |
|
deck.position.y = 0.65; |
|
shipGroup.add(deck); |
|
|
|
|
|
const mastGeometry = new THREE.CylinderGeometry(0.15, 0.15, 10); |
|
const mastMaterial = new THREE.MeshLambertMaterial({ color: 0x654321 }); |
|
const mast = new THREE.Mesh(mastGeometry, mastMaterial); |
|
mast.position.y = 5.6; |
|
shipGroup.add(mast); |
|
|
|
|
|
const sailGeometry = new THREE.PlaneGeometry(4, 6); |
|
const sailMaterial = new THREE.MeshLambertMaterial({ |
|
color: 0xffffff, |
|
transparent: true, |
|
opacity: 0.9, |
|
side: THREE.DoubleSide |
|
}); |
|
const sail = new THREE.Mesh(sailGeometry, sailMaterial); |
|
sail.position.set(2, 5, 0); |
|
sail.userData = { type: 'sail' }; |
|
shipGroup.add(sail); |
|
|
|
|
|
const bowGeometry = new THREE.ConeGeometry(0.3, 1.5, 8); |
|
const bowMaterial = new THREE.MeshLambertMaterial({ color: 0xFFD700 }); |
|
const bow = new THREE.Mesh(bowGeometry, bowMaterial); |
|
bow.rotation.x = Math.PI / 2; |
|
bow.position.set(0, 1, 4); |
|
shipGroup.add(bow); |
|
|
|
shipGroup.position.set(0, 1, 0); |
|
return shipGroup; |
|
} |
|
|
|
playerShip = createPlayerShip(); |
|
scene.add(playerShip); |
|
|
|
|
|
function createEnemyShip() { |
|
const shipGroup = new THREE.Group(); |
|
|
|
const hullGeometry = new THREE.BoxGeometry(1.5, 0.4, 4); |
|
const hullMaterial = new THREE.MeshLambertMaterial({ color: 0x2c1810 }); |
|
const hull = new THREE.Mesh(hullGeometry, hullMaterial); |
|
hull.position.y = 0.2; |
|
shipGroup.add(hull); |
|
|
|
const flagGeometry = new THREE.PlaneGeometry(1, 1); |
|
const flagMaterial = new THREE.MeshLambertMaterial({ |
|
color: 0x000000, |
|
transparent: true, |
|
opacity: 0.8 |
|
}); |
|
const flag = new THREE.Mesh(flagGeometry, flagMaterial); |
|
flag.position.set(0, 3, 0); |
|
shipGroup.add(flag); |
|
|
|
return shipGroup; |
|
} |
|
|
|
|
|
function createCannonTower(position) { |
|
const towerGroup = new THREE.Group(); |
|
|
|
const baseGeometry = new THREE.CylinderGeometry(1.5, 2, 2); |
|
const baseMaterial = new THREE.MeshLambertMaterial({ color: 0x654321 }); |
|
const base = new THREE.Mesh(baseGeometry, baseMaterial); |
|
towerGroup.add(base); |
|
|
|
const cannonGeometry = new THREE.CylinderGeometry(0.2, 0.3, 2); |
|
const cannonMaterial = new THREE.MeshLambertMaterial({ color: 0x2c2c2c }); |
|
const cannon = new THREE.Mesh(cannonGeometry, cannonMaterial); |
|
cannon.rotation.z = Math.PI / 2; |
|
cannon.position.y = 1.5; |
|
towerGroup.add(cannon); |
|
|
|
towerGroup.position.copy(position); |
|
towerGroup.userData = { |
|
type: 'cannon', |
|
range: 30, |
|
damage: 25, |
|
fireRate: 1000, |
|
lastFire: 0, |
|
cost: 100 |
|
}; |
|
|
|
return towerGroup; |
|
} |
|
|
|
function createHarpoonTower(position) { |
|
const towerGroup = new THREE.Group(); |
|
|
|
const baseGeometry = new THREE.BoxGeometry(2, 1, 2); |
|
const baseMaterial = new THREE.MeshLambertMaterial({ color: 0x8B7355 }); |
|
const base = new THREE.Mesh(baseGeometry, baseMaterial); |
|
towerGroup.add(base); |
|
|
|
const launcherGeometry = new THREE.BoxGeometry(0.5, 0.5, 3); |
|
const launcherMaterial = new THREE.MeshLambertMaterial({ color: 0x2c2c2c }); |
|
const launcher = new THREE.Mesh(launcherGeometry, launcherMaterial); |
|
launcher.position.y = 1; |
|
towerGroup.add(launcher); |
|
|
|
towerGroup.position.copy(position); |
|
towerGroup.userData = { |
|
type: 'harpoon', |
|
range: 40, |
|
damage: 40, |
|
fireRate: 1500, |
|
lastFire: 0, |
|
cost: 150 |
|
}; |
|
|
|
return towerGroup; |
|
} |
|
|
|
function createNetTower(position) { |
|
const towerGroup = new THREE.Group(); |
|
|
|
const baseGeometry = new THREE.CylinderGeometry(1, 1.5, 1.5); |
|
const baseMaterial = new THREE.MeshLambertMaterial({ color: 0x4d5d3d }); |
|
const base = new THREE.Mesh(baseGeometry, baseMaterial); |
|
towerGroup.add(base); |
|
|
|
const netGeometry = new THREE.TorusGeometry(1, 0.1, 8, 16); |
|
const netMaterial = new THREE.MeshLambertMaterial({ color: 0x8B8B00 }); |
|
const net = new THREE.Mesh(netGeometry, netMaterial); |
|
net.position.y = 2; |
|
towerGroup.add(net); |
|
|
|
towerGroup.position.copy(position); |
|
towerGroup.userData = { |
|
type: 'net', |
|
range: 25, |
|
damage: 15, |
|
fireRate: 2000, |
|
lastFire: 0, |
|
slow: 0.5, |
|
cost: 200 |
|
}; |
|
|
|
return towerGroup; |
|
} |
|
|
|
function createLighthouseTower(position) { |
|
const towerGroup = new THREE.Group(); |
|
|
|
const baseGeometry = new THREE.CylinderGeometry(1.5, 2, 8); |
|
const baseMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); |
|
const base = new THREE.Mesh(baseGeometry, baseMaterial); |
|
base.position.y = 4; |
|
towerGroup.add(base); |
|
|
|
const lightGeometry = new THREE.SphereGeometry(0.5); |
|
const lightMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFF00 }); |
|
const light = new THREE.Mesh(lightGeometry, lightMaterial); |
|
light.position.y = 8.5; |
|
towerGroup.add(light); |
|
|
|
const areaLight = new THREE.PointLight(0xFFFF00, 1, 60); |
|
areaLight.position.y = 8.5; |
|
towerGroup.add(areaLight); |
|
|
|
towerGroup.position.copy(position); |
|
towerGroup.userData = { |
|
type: 'lighthouse', |
|
range: 50, |
|
damage: 10, |
|
fireRate: 500, |
|
lastFire: 0, |
|
areaEffect: true, |
|
cost: 300 |
|
}; |
|
|
|
return towerGroup; |
|
} |
|
|
|
|
|
function updateShipMovement() { |
|
const currentArea = getCurrentArea(); |
|
const areaData = lakeAreas[currentArea]; |
|
|
|
|
|
let moveSpeed = baseSpeed; |
|
|
|
|
|
if (areaData) { |
|
|
|
const windEffect = areaData.windMultiplier; |
|
moveSpeed *= windEffect; |
|
|
|
|
|
const tideBoost = areaData.tideEffect; |
|
moveSpeed += tideBoost * 0.2; |
|
} |
|
|
|
|
|
if (keys['ShiftLeft'] || keys['ShiftRight']) { |
|
moveSpeed *= 1.5; |
|
} |
|
|
|
|
|
let movement = new THREE.Vector3(0, 0, 0); |
|
|
|
if (keys['KeyW']) movement.z -= moveSpeed; |
|
if (keys['KeyS']) movement.z += moveSpeed; |
|
if (keys['KeyA']) movement.x -= moveSpeed; |
|
if (keys['KeyD']) movement.x += moveSpeed; |
|
|
|
|
|
if (movement.length() > 0) { |
|
movement.normalize().multiplyScalar(moveSpeed); |
|
playerShip.position.add(movement); |
|
|
|
|
|
if (movement.length() > 0) { |
|
shipRotation = Math.atan2(movement.x, movement.z); |
|
playerShip.rotation.y = shipRotation; |
|
} |
|
} |
|
|
|
|
|
playerShip.position.x = Math.max(-95, Math.min(95, playerShip.position.x)); |
|
playerShip.position.z = Math.max(-95, Math.min(95, playerShip.position.z)); |
|
|
|
|
|
gameState.shipSpeed = movement.length() * 10; |
|
|
|
|
|
lakeMaterial.uniforms.playerPos.value.set(playerShip.position.x, playerShip.position.z); |
|
} |
|
|
|
|
|
function getCurrentArea() { |
|
const playerPos = playerShip.position; |
|
|
|
for (const [name, area] of Object.entries(lakeAreas)) { |
|
const distance = Math.sqrt( |
|
Math.pow(playerPos.x - area.center.x, 2) + |
|
Math.pow(playerPos.z - area.center.z, 2) |
|
); |
|
|
|
if (distance <= area.radius) { |
|
return name; |
|
} |
|
} |
|
|
|
return "Open Water"; |
|
} |
|
|
|
|
|
function updateEnvironment() { |
|
const currentArea = getCurrentArea(); |
|
const areaData = lakeAreas[currentArea] || { windMultiplier: 1, tideEffect: 0, description: "Open Waters" }; |
|
|
|
|
|
if (gameState.currentArea !== currentArea) { |
|
gameState.currentArea = currentArea; |
|
|
|
|
|
gameState.windSpeed = 3 + Math.random() * 4 + (areaData.windMultiplier - 1) * 2; |
|
gameState.windDirection += (Math.random() - 0.5) * 0.3; |
|
} |
|
|
|
|
|
if (Math.random() < 0.002) { |
|
gameState.windDirection += (Math.random() - 0.5) * 0.5; |
|
gameState.windSpeed = Math.max(1, Math.min(10, gameState.windSpeed + (Math.random() - 0.5) * 2)); |
|
} |
|
|
|
|
|
const sail = playerShip.children.find(child => child.userData.type === 'sail'); |
|
if (sail) { |
|
const windIntensity = areaData.windMultiplier; |
|
sail.rotation.y = Math.sin(Date.now() * 0.003) * 0.15 * windIntensity; |
|
sail.position.x = 2 + Math.sin(Date.now() * 0.002) * 0.2 * windIntensity; |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('keydown', (event) => { |
|
keys[event.code] = true; |
|
|
|
if (event.code === 'Tab') { |
|
event.preventDefault(); |
|
cycleTowerSelection(); |
|
} |
|
}); |
|
|
|
document.addEventListener('keyup', (event) => { |
|
keys[event.code] = false; |
|
}); |
|
|
|
|
|
function updateCamera() { |
|
const targetPosition = playerShip.position.clone().add(cameraOffset); |
|
camera.position.lerp(targetPosition, 0.08); |
|
camera.lookAt(playerShip.position); |
|
} |
|
|
|
|
|
function updateUI() { |
|
document.getElementById('gold').textContent = gameState.gold; |
|
document.getElementById('lives').textContent = gameState.lives; |
|
document.getElementById('wave').textContent = gameState.wave; |
|
document.getElementById('score').textContent = gameState.score; |
|
document.getElementById('enemies-left').textContent = gameState.enemies.length; |
|
document.getElementById('health-fill').style.width = `${gameState.health}%`; |
|
document.getElementById('wind-speed').textContent = Math.round(gameState.windSpeed); |
|
document.getElementById('ship-speed').textContent = gameState.shipSpeed.toFixed(1); |
|
|
|
|
|
const currentArea = getCurrentArea(); |
|
const areaData = lakeAreas[currentArea] || { description: "Open Waters", windMultiplier: 1, tideEffect: 0 }; |
|
|
|
document.getElementById('current-area').textContent = currentArea; |
|
document.getElementById('area-effect').textContent = areaData.description; |
|
|
|
|
|
const windEffect = ((areaData.windMultiplier - 1) * 100).toFixed(0); |
|
document.getElementById('wind-effect').textContent = `${windEffect >= 0 ? '+' : ''}${windEffect}%`; |
|
|
|
const tideText = areaData.tideEffect > 0.1 ? "Outgoing" : |
|
areaData.tideEffect < -0.1 ? "Incoming" : "Normal"; |
|
document.getElementById('tide-info').textContent = tideText; |
|
|
|
|
|
const windArrow = document.getElementById('wind-arrow'); |
|
windArrow.style.transform = `rotate(${gameState.windDirection * 180 / Math.PI}deg)`; |
|
|
|
|
|
document.querySelectorAll('.tower-button').forEach(button => { |
|
if (button.dataset.cost) { |
|
const cost = parseInt(button.dataset.cost); |
|
button.classList.toggle('disabled', gameState.gold < cost); |
|
} |
|
}); |
|
} |
|
|
|
|
|
function createProjectile(start, target, type) { |
|
let projectileGeometry, projectileMaterial; |
|
|
|
switch(type) { |
|
case 'cannonball': |
|
projectileGeometry = new THREE.SphereGeometry(0.2); |
|
projectileMaterial = new THREE.MeshLambertMaterial({ color: 0x2c2c2c }); |
|
break; |
|
case 'harpoon': |
|
projectileGeometry = new THREE.CylinderGeometry(0.05, 0.05, 1); |
|
projectileMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 }); |
|
break; |
|
case 'net': |
|
projectileGeometry = new THREE.PlaneGeometry(1, 1); |
|
projectileMaterial = new THREE.MeshLambertMaterial({ |
|
color: 0x8B8B00, |
|
transparent: true, |
|
opacity: 0.7 |
|
}); |
|
break; |
|
case 'lighthouse': |
|
projectileGeometry = new THREE.SphereGeometry(0.1); |
|
projectileMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFF00 }); |
|
break; |
|
} |
|
|
|
const projectile = new THREE.Mesh(projectileGeometry, projectileMaterial); |
|
projectile.position.copy(start); |
|
|
|
const direction = new THREE.Vector3().subVectors(target, start).normalize(); |
|
const distance = start.distanceTo(target); |
|
|
|
projectile.userData = { |
|
type: type, |
|
target: target.clone(), |
|
direction: direction, |
|
speed: 0.5, |
|
life: distance / 0.5 |
|
}; |
|
|
|
return projectile; |
|
} |
|
|
|
function spawnEnemyWave() { |
|
const waveSize = gameState.wave * 3 + 2; |
|
const spawnPoints = [ |
|
new THREE.Vector3(-100, 1, -100), |
|
new THREE.Vector3(100, 1, -100), |
|
new THREE.Vector3(-100, 1, 100), |
|
new THREE.Vector3(100, 1, 100) |
|
]; |
|
|
|
for (let i = 0; i < waveSize; i++) { |
|
setTimeout(() => { |
|
const spawnPoint = spawnPoints[i % spawnPoints.length]; |
|
const enemy = createEnemyShip(); |
|
enemy.position.copy(spawnPoint); |
|
|
|
const health = 50 + gameState.wave * 10; |
|
const speed = 0.1 + gameState.wave * 0.02; |
|
|
|
enemy.userData = { |
|
type: 'enemy', |
|
health: health, |
|
maxHealth: health, |
|
speed: speed, |
|
value: 25 + gameState.wave * 5, |
|
slowFactor: 1, |
|
slowTime: 0 |
|
}; |
|
|
|
gameState.enemies.push(enemy); |
|
scene.add(enemy); |
|
}, i * 1000); |
|
} |
|
} |
|
|
|
function updateTowers() { |
|
const currentTime = Date.now(); |
|
|
|
gameState.towers.forEach(tower => { |
|
if (currentTime - tower.userData.lastFire < tower.userData.fireRate) return; |
|
|
|
let nearestEnemy = null; |
|
let nearestDistance = Infinity; |
|
|
|
gameState.enemies.forEach(enemy => { |
|
const distance = tower.position.distanceTo(enemy.position); |
|
if (distance <= tower.userData.range && distance < nearestDistance) { |
|
nearestEnemy = enemy; |
|
nearestDistance = distance; |
|
} |
|
}); |
|
|
|
if (nearestEnemy) { |
|
tower.userData.lastFire = currentTime; |
|
|
|
const projectileType = { |
|
'cannon': 'cannonball', |
|
'harpoon': 'harpoon', |
|
'net': 'net', |
|
'lighthouse': 'lighthouse' |
|
}[tower.userData.type]; |
|
|
|
const startPos = tower.position.clone(); |
|
startPos.y += 2; |
|
|
|
const projectile = createProjectile(startPos, nearestEnemy.position, projectileType); |
|
gameState.projectiles.push(projectile); |
|
scene.add(projectile); |
|
|
|
tower.lookAt(nearestEnemy.position); |
|
} |
|
}); |
|
} |
|
|
|
function updateProjectiles() { |
|
gameState.projectiles.forEach((projectile, index) => { |
|
projectile.userData.life -= 1; |
|
|
|
if (projectile.userData.life <= 0) { |
|
scene.remove(projectile); |
|
gameState.projectiles.splice(index, 1); |
|
return; |
|
} |
|
|
|
const movement = projectile.userData.direction.clone().multiplyScalar(projectile.userData.speed); |
|
projectile.position.add(movement); |
|
|
|
gameState.enemies.forEach((enemy, enemyIndex) => { |
|
if (projectile.position.distanceTo(enemy.position) < 2) { |
|
const tower = gameState.towers.find(t => |
|
t.userData.type === projectile.userData.type.replace('ball', '').replace('lighthouse', 'lighthouse') |
|
); |
|
|
|
if (tower) { |
|
enemy.userData.health -= tower.userData.damage; |
|
|
|
if (tower.userData.type === 'net') { |
|
enemy.userData.slowFactor = tower.userData.slow; |
|
enemy.userData.slowTime = 3000; |
|
} |
|
|
|
createHitEffect(enemy.position); |
|
} |
|
|
|
scene.remove(projectile); |
|
gameState.projectiles.splice(index, 1); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
function updateEnemies() { |
|
gameState.enemies.forEach((enemy, index) => { |
|
if (enemy.userData.health <= 0) { |
|
gameState.gold += enemy.userData.value; |
|
gameState.score += enemy.userData.value; |
|
scene.remove(enemy); |
|
gameState.enemies.splice(index, 1); |
|
return; |
|
} |
|
|
|
if (enemy.userData.slowTime > 0) { |
|
enemy.userData.slowTime -= 16; |
|
if (enemy.userData.slowTime <= 0) { |
|
enemy.userData.slowFactor = 1; |
|
} |
|
} |
|
|
|
const direction = new THREE.Vector3().subVectors(playerShip.position, enemy.position).normalize(); |
|
const speed = enemy.userData.speed * enemy.userData.slowFactor; |
|
enemy.position.add(direction.multiplyScalar(speed)); |
|
enemy.lookAt(playerShip.position); |
|
|
|
if (enemy.position.distanceTo(playerShip.position) < 3) { |
|
gameState.health -= 20; |
|
gameState.lives--; |
|
scene.remove(enemy); |
|
gameState.enemies.splice(index, 1); |
|
|
|
if (gameState.health <= 0 || gameState.lives <= 0) { |
|
endGame(); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
function createHitEffect(position) { |
|
for (let i = 0; i < 10; i++) { |
|
const particleGeometry = new THREE.SphereGeometry(0.1); |
|
const particleMaterial = new THREE.MeshBasicMaterial({ |
|
color: Math.random() < 0.5 ? 0xFF6B35 : 0xFFFF00 |
|
}); |
|
const particle = new THREE.Mesh(particleGeometry, particleMaterial); |
|
particle.position.copy(position); |
|
|
|
particle.userData = { |
|
velocity: new THREE.Vector3( |
|
(Math.random() - 0.5) * 0.2, |
|
Math.random() * 0.2, |
|
(Math.random() - 0.5) * 0.2 |
|
), |
|
life: 60 |
|
}; |
|
|
|
gameState.particles.push(particle); |
|
scene.add(particle); |
|
} |
|
} |
|
|
|
function updateParticles() { |
|
gameState.particles.forEach((particle, index) => { |
|
particle.userData.life--; |
|
|
|
if (particle.userData.life <= 0) { |
|
scene.remove(particle); |
|
gameState.particles.splice(index, 1); |
|
return; |
|
} |
|
|
|
particle.position.add(particle.userData.velocity); |
|
particle.userData.velocity.y -= 0.005; |
|
particle.material.opacity = particle.userData.life / 60; |
|
}); |
|
} |
|
|
|
|
|
let raycaster = new THREE.Raycaster(); |
|
let mouse = new THREE.Vector2(); |
|
|
|
renderer.domElement.addEventListener('click', (event) => { |
|
if (!gameState.selectedTower) return; |
|
|
|
mouse.x = (event.clientX / window.innerWidth) * 2 - 1; |
|
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; |
|
|
|
raycaster.setFromCamera(mouse, camera); |
|
const intersects = raycaster.intersectObjects(islands); |
|
|
|
if (intersects.length > 0) { |
|
const position = intersects[0].point; |
|
position.y = 2; |
|
|
|
const cost = parseInt(document.querySelector(`[data-tower="${gameState.selectedTower}"]`).dataset.cost); |
|
|
|
if (gameState.gold >= cost) { |
|
let tower; |
|
switch(gameState.selectedTower) { |
|
case 'cannon': |
|
tower = createCannonTower(position); |
|
break; |
|
case 'harpoon': |
|
tower = createHarpoonTower(position); |
|
break; |
|
case 'net': |
|
tower = createNetTower(position); |
|
break; |
|
case 'lighthouse': |
|
tower = createLighthouseTower(position); |
|
break; |
|
} |
|
|
|
if (tower) { |
|
gameState.towers.push(tower); |
|
scene.add(tower); |
|
gameState.gold -= cost; |
|
updateUI(); |
|
} |
|
} |
|
} |
|
}); |
|
|
|
|
|
document.querySelectorAll('.tower-button').forEach(button => { |
|
if (button.dataset.tower) { |
|
button.addEventListener('click', (e) => { |
|
e.stopPropagation(); |
|
const towerType = button.dataset.tower; |
|
const cost = parseInt(button.dataset.cost); |
|
|
|
if (gameState.gold >= cost) { |
|
gameState.selectedTower = towerType; |
|
document.querySelectorAll('.tower-button').forEach(b => b.classList.remove('selected')); |
|
button.classList.add('selected'); |
|
} |
|
}); |
|
} |
|
}); |
|
|
|
document.getElementById('start-wave').addEventListener('click', () => { |
|
if (!gameState.waveActive) { |
|
startWave(); |
|
} |
|
}); |
|
|
|
function startWave() { |
|
gameState.waveActive = true; |
|
spawnEnemyWave(); |
|
document.getElementById('start-wave').style.display = 'none'; |
|
} |
|
|
|
function checkWaveComplete() { |
|
if (gameState.waveActive && gameState.enemies.length === 0) { |
|
gameState.waveActive = false; |
|
gameState.wave++; |
|
gameState.gold += 100; |
|
document.getElementById('start-wave').style.display = 'block'; |
|
updateUI(); |
|
} |
|
} |
|
|
|
function cycleTowerSelection() { |
|
const towers = ['cannon', 'harpoon', 'net', 'lighthouse']; |
|
const currentIndex = towers.indexOf(gameState.selectedTower); |
|
const nextIndex = (currentIndex + 1) % towers.length; |
|
|
|
const button = document.querySelector(`[data-tower="${towers[nextIndex]}"]`); |
|
if (button && !button.classList.contains('disabled')) { |
|
button.click(); |
|
} |
|
} |
|
|
|
function endGame() { |
|
gameState.gameRunning = false; |
|
document.getElementById('final-score').textContent = gameState.score; |
|
document.getElementById('game-over').style.display = 'block'; |
|
} |
|
|
|
function restartGame() { |
|
gameState = { |
|
gold: 500, |
|
lives: 3, |
|
wave: 1, |
|
score: 0, |
|
health: 100, |
|
selectedTower: null, |
|
gameRunning: true, |
|
waveActive: false, |
|
enemies: [], |
|
towers: [], |
|
projectiles: [], |
|
particles: [], |
|
windDirection: 0, |
|
windSpeed: 5, |
|
currentArea: "Upper Lake", |
|
shipSpeed: 0 |
|
}; |
|
|
|
[...gameState.enemies, ...gameState.towers, ...gameState.projectiles, ...gameState.particles] |
|
.forEach(obj => scene.remove(obj)); |
|
|
|
playerShip.position.set(0, 1, 0); |
|
shipRotation = 0; |
|
|
|
document.getElementById('game-over').style.display = 'none'; |
|
document.getElementById('start-wave').style.display = 'block'; |
|
|
|
updateUI(); |
|
} |
|
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404070, 0.4); |
|
scene.add(ambientLight); |
|
|
|
const sunLight = new THREE.DirectionalLight(0xFFE4B5, 0.8); |
|
sunLight.position.set(100, 100, 50); |
|
sunLight.castShadow = true; |
|
sunLight.shadow.mapSize.width = 2048; |
|
sunLight.shadow.mapSize.height = 2048; |
|
scene.add(sunLight); |
|
|
|
|
|
let time = 0; |
|
function animate() { |
|
requestAnimationFrame(animate); |
|
time += 0.01; |
|
|
|
if (gameState.gameRunning) { |
|
updateShipMovement(); |
|
updateCamera(); |
|
updateTowers(); |
|
updateProjectiles(); |
|
updateEnemies(); |
|
updateParticles(); |
|
updateEnvironment(); |
|
checkWaveComplete(); |
|
updateUI(); |
|
} |
|
|
|
lakeMaterial.uniforms.time.value = time; |
|
skyMaterial.uniforms.time.value = time; |
|
|
|
renderer.render(scene, camera); |
|
} |
|
|
|
window.addEventListener('resize', () => { |
|
camera.aspect = window.innerWidth / window.innerHeight; |
|
camera.updateProjectionMatrix(); |
|
renderer.setSize(window.innerWidth, window.innerHeight); |
|
}); |
|
|
|
updateUI(); |
|
animate(); |
|
</script> |
|
</body> |
|
</html> |