Spaces:
Running
Running
<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> |