SailingSimulator / index.html
awacke1's picture
Update index.html
6e7a654 verified
raw
history blame
43.1 kB
<!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;
}
.sailing-controls {
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: 60px;
height: 60px;
text-align: center;
font-size: 24px;
}
.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;
}
</style>
</head>
<body>
<div class="ui-container">
<div class="controls ui-panel">
<h3>🏴‍☠️ Captain's Orders</h3>
<p>⚓ WASD: Sail your ship</p>
<p>🌊 Space: Adjust sails</p>
<p>🎯 Click: Place towers</p>
<p>🔄 R: Rotate ship</p>
<p>⚡ Tab: Quick towers</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>
</div>
<div class="wind-indicator ui-panel">
<div id="wind-arrow">💨</div>
</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="sailing-controls ui-panel">
<h4>⛵ Sailing</h4>
<p>Wind Speed: <span id="wind-speed">5</span> kts</p>
<p>Ship Speed: <span id="ship-speed">0</span> kts</p>
<p>Heading: <span id="heading">N</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="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>
// Game state
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
};
// Scene setup
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);
// Camera controls
let cameraOffset = new THREE.Vector3(0, 15, 20);
// Player ship
let playerShip;
let shipRotation = 0;
let shipSpeed = 0;
let sailsOut = 0.5;
const keys = {};
// Atmospheric fog
scene.fog = new THREE.Fog(0x2d1810, 50, 1000);
// Create atmospheric sky
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);
// Create Lake Minnetonka water system
const lakeGeometry = new THREE.PlaneGeometry(2000, 2000, 200, 200);
const lakeMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 }
},
vertexShader: `
uniform float time;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vUv = uv;
vec3 pos = position;
// Multiple wave patterns for realistic lake movement
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;
pos.z = wave1 + wave2 + wave3;
vPosition = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
uniform float time;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
vec3 deepWater = vec3(0.1, 0.3, 0.5);
vec3 shallowWater = vec3(0.3, 0.5, 0.7);
float wave = sin(vPosition.x * 0.1 + time) * sin(vPosition.y * 0.08 + time);
vec3 waterColor = mix(deepWater, shallowWater, wave * 0.5 + 0.5);
gl_FragColor = vec4(waterColor, 0.8);
}
`,
transparent: true
});
lakeGeometry.rotateX(-Math.PI / 2);
const lake = new THREE.Mesh(lakeGeometry, lakeMaterial);
scene.add(lake);
// Create multiple lake sections representing different areas of Minnetonka
function createLakeSection(x, z, size, name) {
const sectionGeometry = new THREE.RingGeometry(size * 0.8, size, 32);
const sectionMaterial = new THREE.MeshBasicMaterial({
color: 0x87CEEB,
transparent: true,
opacity: 0.1,
side: THREE.DoubleSide
});
const section = new THREE.Mesh(sectionGeometry, sectionMaterial);
section.rotation.x = -Math.PI / 2;
section.position.set(x, 0.1, z);
section.userData = { name: name };
scene.add(section);
}
// Create the different bays of Lake Minnetonka
createLakeSection(0, 0, 50, "Upper Lake");
createLakeSection(-80, -60, 40, "Wayzata Bay");
createLakeSection(70, -50, 35, "Crystal Bay");
createLakeSection(-50, 80, 30, "Carmans Bay");
createLakeSection(60, 70, 32, "Smithtown Bay");
// Create islands and shoreline features
function createIsland(x, z, size) {
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;
island.userData = { type: 'buildable' };
scene.add(island);
// Add some trees
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;
}
// Create strategic islands for tower placement
const islands = [
createIsland(-20, -30, 8),
createIsland(30, -20, 6),
createIsland(-40, 20, 7),
createIsland(25, 35, 5),
createIsland(0, -50, 9),
createIsland(-60, -10, 6),
createIsland(45, 10, 7)
];
// Create player ship
function createPlayerShip() {
const shipGroup = new THREE.Group();
// Hull
const hullGeometry = new THREE.BoxGeometry(2, 0.5, 6);
const hullMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
const hull = new THREE.Mesh(hullGeometry, hullMaterial);
hull.position.y = 0.25;
shipGroup.add(hull);
// Mast
const mastGeometry = new THREE.CylinderGeometry(0.1, 0.1, 8);
const mastMaterial = new THREE.MeshLambertMaterial({ color: 0x654321 });
const mast = new THREE.Mesh(mastGeometry, mastMaterial);
mast.position.y = 4;
shipGroup.add(mast);
// Sail
const sailGeometry = new THREE.PlaneGeometry(3, 5);
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(1.5, 4, 0);
sail.userData = { type: 'sail' };
shipGroup.add(sail);
shipGroup.position.set(0, 1, 0);
return shipGroup;
}
playerShip = createPlayerShip();
scene.add(playerShip);
// Enemy ship creation
function createEnemyShip() {
const shipGroup = new THREE.Group();
// Hull (smaller and darker)
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);
// Pirate flag
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;
}
// Tower creation functions
function createCannonTower(position) {
const towerGroup = new THREE.Group();
// Base
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);
// Cannon
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();
// Base
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);
// Harpoon launcher
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();
// Base
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);
// Net mechanism
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();
// Base
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);
// Light
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);
// Area light effect
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;
}
// Projectile creation
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;
}
// Enemy spawning
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);
}
}
// Combat system
function updateTowers() {
const currentTime = Date.now();
gameState.towers.forEach(tower => {
if (currentTime - tower.userData.lastFire < tower.userData.fireRate) return;
// Find nearest enemy in range
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;
// Create projectile
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);
// Point tower at target
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;
}
// Move projectile
const movement = projectile.userData.direction.clone().multiplyScalar(projectile.userData.speed);
projectile.position.add(movement);
// Check collision with enemies
gameState.enemies.forEach((enemy, enemyIndex) => {
if (projectile.position.distanceTo(enemy.position) < 2) {
// Hit!
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;
// Special effects
if (tower.userData.type === 'net') {
enemy.userData.slowFactor = tower.userData.slow;
enemy.userData.slowTime = 3000;
}
// Create hit particle effect
createHitEffect(enemy.position);
}
scene.remove(projectile);
gameState.projectiles.splice(index, 1);
}
});
});
}
function updateEnemies() {
gameState.enemies.forEach((enemy, index) => {
// Remove dead enemies
if (enemy.userData.health <= 0) {
gameState.gold += enemy.userData.value;
gameState.score += enemy.userData.value;
scene.remove(enemy);
gameState.enemies.splice(index, 1);
return;
}
// Update slow effect
if (enemy.userData.slowTime > 0) {
enemy.userData.slowTime -= 16;
if (enemy.userData.slowTime <= 0) {
enemy.userData.slowFactor = 1;
}
}
// Move towards player ship
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);
// Check if reached player
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();
}
}
});
}
// Visual effects
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; // Gravity
particle.material.opacity = particle.userData.life / 60;
});
}
// Sailing physics
function updateShipPhysics() {
// Wind effect on sailing
const windAngle = gameState.windDirection;
const windEffect = Math.cos(shipRotation - windAngle) * gameState.windSpeed * sailsOut;
// Calculate ship speed based on wind
shipSpeed = Math.max(0, windEffect * 0.1);
// Move ship
const movement = new THREE.Vector3(
Math.sin(shipRotation) * shipSpeed,
0,
Math.cos(shipRotation) * shipSpeed
);
playerShip.position.add(movement);
// Constrain to lake bounds
playerShip.position.x = Math.max(-90, Math.min(90, playerShip.position.x));
playerShip.position.z = Math.max(-90, Math.min(90, playerShip.position.z));
// Update sail animation based on wind
const sail = playerShip.children.find(child => child.userData.type === 'sail');
if (sail) {
sail.rotation.y = Math.sin(Date.now() * 0.003) * 0.1 * sailsOut;
}
}
// Input handling
document.addEventListener('keydown', (event) => {
keys[event.code] = true;
if (event.code === 'Space') {
event.preventDefault();
sailsOut = sailsOut === 1 ? 0.3 : 1;
}
if (event.code === 'KeyR') {
shipRotation += Math.PI / 4;
}
// Quick tower selection
if (event.code === 'Tab') {
event.preventDefault();
cycleTowerSelection();
}
});
document.addEventListener('keyup', (event) => {
keys[event.code] = false;
});
// Ship movement
function updateShipMovement() {
const turnSpeed = 0.05;
const acceleration = 0.01;
if (keys['KeyA']) shipRotation -= turnSpeed;
if (keys['KeyD']) shipRotation += turnSpeed;
if (keys['KeyW']) sailsOut = Math.min(1, sailsOut + acceleration);
if (keys['KeyS']) sailsOut = Math.max(0, sailsOut - acceleration);
playerShip.rotation.y = shipRotation;
}
// Camera following
function updateCamera() {
const targetPosition = playerShip.position.clone().add(cameraOffset);
camera.position.lerp(targetPosition, 0.05);
camera.lookAt(playerShip.position);
}
// Tower placement
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();
}
}
}
});
// UI event handlers
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();
}
});
// Game management
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 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 = gameState.windSpeed;
document.getElementById('ship-speed').textContent = shipSpeed.toFixed(1);
const headings = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
const headingIndex = Math.round((shipRotation + Math.PI) / (Math.PI / 4)) % 8;
document.getElementById('heading').textContent = headings[headingIndex];
// Update wind indicator
const windArrow = document.getElementById('wind-arrow');
windArrow.style.transform = `rotate(${gameState.windDirection * 180 / Math.PI}deg)`;
// Update tower button states
document.querySelectorAll('.tower-button').forEach(button => {
if (button.dataset.cost) {
const cost = parseInt(button.dataset.cost);
button.classList.toggle('disabled', gameState.gold < cost);
}
});
}
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() {
// Reset game state
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
};
// Clear scene
[...gameState.enemies, ...gameState.towers, ...gameState.projectiles, ...gameState.particles]
.forEach(obj => scene.remove(obj));
// Reset ship position
playerShip.position.set(0, 1, 0);
shipRotation = 0;
shipSpeed = 0;
sailsOut = 0.5;
document.getElementById('game-over').style.display = 'none';
document.getElementById('start-wave').style.display = 'block';
updateUI();
}
// Environmental effects
function updateEnvironment() {
// Change wind periodically
if (Math.random() < 0.001) {
gameState.windDirection += (Math.random() - 0.5) * 0.5;
gameState.windSpeed = 3 + Math.random() * 7;
}
}
// Lighting
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);
// Animation loop
let time = 0;
function animate() {
requestAnimationFrame(animate);
time += 0.01;
if (gameState.gameRunning) {
updateShipMovement();
updateShipPhysics();
updateCamera();
updateTowers();
updateProjectiles();
updateEnemies();
updateParticles();
updateEnvironment();
checkWaveComplete();
updateUI();
}
// Update water animation
lakeMaterial.uniforms.time.value = time;
skyMaterial.uniforms.time.value = time;
renderer.render(scene, camera);
}
// Window resize handler
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Initialize
updateUI();
animate();
</script>
</body>
</html>