SailingSimulator / index.html
awacke1's picture
Update index.html
09eca26 verified
<!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>
// 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,
currentArea: "Upper Lake",
shipSpeed: 0,
cargo: [],
cargoCapacity: 6,
nearIsland: null
};
// Cargo types with their emoji representations and base values
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 }
};
// Lake areas with different environmental effects
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
}
};
// 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, 25, 30);
// Player ship
let playerShip;
let shipRotation = 0;
const keys = {};
const baseSpeed = 0.4;
// 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 },
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);
// Create visible lake sections with different colors
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);
});
// Create islands and shoreline features
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;
// Generate random cargo offerings and demands for this island
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()
};
// Add visual cargo indicators on the island
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);
// 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 emoji texture for cargo indicators
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;
}
// Create strategic islands for tower placement and trading
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")
];
// Create player ship
function createPlayerShip() {
const shipGroup = new THREE.Group();
// Hull
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);
// Deck
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);
// Mast
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);
// Main sail
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);
// Bow decoration
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);
// Enemy ship creation (same as before)
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;
}
// Tower creation functions (same as before but keeping them)
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;
}
// Improved ship movement with area effects
function updateShipMovement() {
const currentArea = getCurrentArea();
const areaData = lakeAreas[currentArea];
// Base movement speed
let moveSpeed = baseSpeed;
// Apply area effects
if (areaData) {
// Wind effect
const windEffect = areaData.windMultiplier;
moveSpeed *= windEffect;
// Tide effect (affects movement in certain directions)
const tideBoost = areaData.tideEffect;
moveSpeed += tideBoost * 0.2;
}
// Speed boost with Shift
if (keys['ShiftLeft'] || keys['ShiftRight']) {
moveSpeed *= 1.5;
}
// Direct WASD movement
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;
// Apply movement
if (movement.length() > 0) {
movement.normalize().multiplyScalar(moveSpeed);
playerShip.position.add(movement);
// Rotate ship to face movement direction
if (movement.length() > 0) {
shipRotation = Math.atan2(movement.x, movement.z);
playerShip.rotation.y = shipRotation;
}
}
// Constrain to lake bounds
playerShip.position.x = Math.max(-95, Math.min(95, playerShip.position.x));
playerShip.position.z = Math.max(-95, Math.min(95, playerShip.position.z));
// Calculate actual speed for display
gameState.shipSpeed = movement.length() * 10;
// Update water shader with player position
lakeMaterial.uniforms.playerPos.value.set(playerShip.position.x, playerShip.position.z);
}
// Area detection
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";
}
// Environment updates
function updateEnvironment() {
const currentArea = getCurrentArea();
const areaData = lakeAreas[currentArea] || { windMultiplier: 1, tideEffect: 0, description: "Open Waters" };
// Update current area if changed
if (gameState.currentArea !== currentArea) {
gameState.currentArea = currentArea;
// Change wind based on area
gameState.windSpeed = 3 + Math.random() * 4 + (areaData.windMultiplier - 1) * 2;
gameState.windDirection += (Math.random() - 0.5) * 0.3;
}
// Random wind changes
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));
}
// Update sail animation based on area wind
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;
}
}
// Input handling
document.addEventListener('keydown', (event) => {
keys[event.code] = true;
if (event.code === 'Tab') {
event.preventDefault();
cycleTowerSelection();
}
});
document.addEventListener('keyup', (event) => {
keys[event.code] = false;
});
// Camera following
function updateCamera() {
const targetPosition = playerShip.position.clone().add(cameraOffset);
camera.position.lerp(targetPosition, 0.08);
camera.lookAt(playerShip.position);
}
// UI Updates
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);
// Update area info
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;
// Environmental effects
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;
// 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);
}
});
}
// All combat and game logic functions remain the same...
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;
});
}
// 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();
}
});
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();
}
// 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();
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>