docking-simulator / index.html
navalmen007's picture
Add 3 files
6881f7f verified
Raw History Blame Contribute Delete
23.6 kB
Here's the revised version with keyboard arrow controls and reduced vessel speed:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maritime Vessel Docking Simulator</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
}
#gameCanvas {
background-color: #0077be;
display: block;
}
#controls {
position: absolute;
bottom: 20px;
left: 20px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 5px;
}
#dashboard {
position: absolute;
top: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 5px;
}
#gameOverScreen {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
display: none;
}
#restartBtn {
margin-top: 20px;
padding: 10px 20px;
background-color: #4CAF50;
border: none;
color: white;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="controls">
<h3>Controls</h3>
<p>↑ - Thrust Forward</p>
<p>↓ - Thrust Reverse</p>
<p>← - Rotate Left</p>
<p>→ - Rotate Right</p>
<p>Q - Bow Thruster Left</p>
<p>E - Bow Thruster Right</p>
<p>Z - Stern Thruster Left</p>
<p>C - Stern Thruster Right</p>
</div>
<div id="dashboard">
<h3>Dashboard</h3>
<p>Speed: <span id="speedDisplay">0.0</span> knots</p>
<p>Heading: <span id="headingDisplay">0</span>°</p>
<p>Distance to Dock: <span id="distanceDisplay">0</span> meters</p>
<p>Time: <span id="timeDisplay">0</span> seconds</p>
</div>
<div id="gameOverScreen">
<h2 id="gameResult"></h2>
<p id="gameScore"></p>
<button id="restartBtn">Restart</button>
</div>
<script>
// Canvas setup
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Game variables
let gameActive = true;
let gameTime = 0;
let lastTimestamp = 0;
// Ship physics parameters
const ship = {
x: canvas.width * 0.2,
y: canvas.height * 0.5,
width: 150,
height: 30,
angle: 0,
velocity: { x: 0, y: 0 },
angularVelocity: 0,
mass: 100000, // tons
inertia: 10000000,
drag: 0.01, // Increased drag to reduce speed
angularDrag: 0.05, // Increased angular drag
mainThrust: 0,
bowThruster: 0,
sternThruster: 0,
maxSpeed: 1.5, // Reduced max speed (was 5)
maxAngularSpeed: 0.0005 // Reduced max angular speed
};
// Port and dock
const dock = {
x: canvas.width * 0.8,
y: canvas.height * 0.5,
width: 40,
height: 150
};
const port = {
x: canvas.width * 0.7,
y: 0,
width: canvas.width * 0.3,
height: canvas.height
};
// Success criteria
const successCriteria = {
maxDockingSpeed: 0.5, // knots
maxDockingAngle: 5 // degrees
};
// Control state
const controls = {
thrustForward: false,
thrustReverse: false,
rotateLeft: false,
rotateRight: false,
bowThrusterLeft: false,
bowThrusterRight: false,
sternThrusterLeft: false,
sternThrusterRight: false
};
// Keyboard controls
window.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowUp': controls.thrustForward = true; break;
case 'ArrowDown': controls.thrustReverse = true; break;
case 'ArrowLeft': controls.rotateLeft = true; break;
case 'ArrowRight': controls.rotateRight = true; break;
case 'q': case 'Q': controls.bowThrusterLeft = true; break;
case 'e': case 'E': controls.bowThrusterRight = true; break;
case 'z': case 'Z': controls.sternThrusterLeft = true; break;
case 'c': case 'C': controls.sternThrusterRight = true; break;
}
});
window.addEventListener('keyup', (e) => {
switch(e.key) {
case 'ArrowUp': controls.thrustForward = false; break;
case 'ArrowDown': controls.thrustReverse = false; break;
case 'ArrowLeft': controls.rotateLeft = false; break;
case 'ArrowRight': controls.rotateRight = false; break;
case 'q': case 'Q': controls.bowThrusterLeft = false; break;
case 'e': case 'E': controls.bowThrusterRight = false; break;
case 'z': case 'Z': controls.sternThrusterLeft = false; break;
case 'c': case 'C': controls.sternThrusterRight = false; break;
}
});
// Restart button
document.getElementById('restartBtn').addEventListener('click', () => {
resetGame();
document.getElementById('gameOverScreen').style.display = 'none';
gameActive = true;
lastTimestamp = 0;
requestAnimationFrame(gameLoop);
});
// Reset game state
function resetGame() {
ship.x = canvas.width * 0.2;
ship.y = canvas.height * 0.5;
ship.angle = 0;
ship.velocity = { x: 0, y: 0 };
ship.angularVelocity = 0;
ship.mainThrust = 0;
ship.bowThruster = 0;
ship.sternThruster = 0;
gameTime = 0;
}
// Update physics
function updatePhysics(deltaTime) {
// Apply controls
ship.mainThrust = 0;
if (controls.thrustForward) ship.mainThrust = 50; // Reduced thrust (was 50000)
if (controls.thrustReverse) ship.mainThrust = -50; // Reduced reverse thrust (was -30000)
ship.bowThruster = 0;
if (controls.bowThrusterLeft) ship.bowThruster = -500; // Reduced thruster power
if (controls.bowThrusterRight) ship.bowThruster = 500; // Reduced thruster power
ship.sternThruster = 0;
if (controls.sternThrusterLeft) ship.sternThruster = -500; // Reduced thruster power
if (controls.sternThrusterRight) ship.sternThruster = 500; // Reduced thruster power
// Apply rotation from main controls
if (controls.rotateLeft) ship.angularVelocity -= 0.000005 * deltaTime*Math.sqrt(ship.velocity.x * ship.velocity.x + ship.velocity.y * ship.velocity.y); // Reduced rotation rate
if (controls.rotateRight) ship.angularVelocity += 0.000005 * deltaTime*Math.sqrt(ship.velocity.x * ship.velocity.x + ship.velocity.y * ship.velocity.y); // Reduced rotation rate
// Calculate forces
const thrustForceX = Math.cos(ship.angle) * ship.mainThrust;
const thrustForceY = Math.sin(ship.angle) * ship.mainThrust;
// Apply forces to velocity
ship.velocity.x += (thrustForceX / ship.mass) * deltaTime;
ship.velocity.y += (thrustForceY / ship.mass) * deltaTime;
// Apply drag
ship.velocity.x *= (1 - ship.drag);
ship.velocity.y *= (1 - ship.drag);
// Apply thrusters to angular velocity
const thrusterTorque = ship.bowThruster * (ship.width / 2) + ship.sternThruster * (ship.width / 2);
ship.angularVelocity += (thrusterTorque / ship.inertia) * deltaTime;
// Apply angular drag
ship.angularVelocity *= (1 - ship.angularDrag);
// Limit speeds
const currentSpeed = Math.sqrt(ship.velocity.x * ship.velocity.x + ship.velocity.y * ship.velocity.y);
if (currentSpeed > ship.maxSpeed) {
ship.velocity.x = (ship.velocity.x / currentSpeed) * ship.maxSpeed;
ship.velocity.y = (ship.velocity.y / currentSpeed) * ship.maxSpeed;
}
if (Math.abs(ship.angularVelocity) > ship.maxAngularSpeed) {
ship.angularVelocity = Math.sign(ship.angularVelocity) * ship.maxAngularSpeed;
}
// Update position and angle
ship.x += ship.velocity.x * deltaTime/10;
ship.y += ship.velocity.y * deltaTime/10;
ship.angle += ship.angularVelocity * deltaTime;
// Boundary checks
if (ship.x < 0) ship.x = 0;
if (ship.y < 0) ship.y = 0;
if (ship.x > canvas.width) ship.x = canvas.width;
if (ship.y > canvas.height) ship.y = canvas.height;
// Check for collision with port walls
if (ship.x > port.x && ship.x < port.x + port.width &&
(ship.y < port.y + 100 || ship.y > port.y + port.height - 100)) {
gameOver(false, "You crashed into the port wall!");
}
// Check for docking
checkDocking();
// Update dashboard
updateDashboard();
}
function checkDocking() {
// Calculate ship corners based on rotation
const corners = getShipCorners();
// Check if all corners are inside the dock area
const dockLeft = dock.x - 20;
const dockRight = dock.x + dock.width + 20;
const dockTop = dock.y - 20;
const dockBottom = dock.y + dock.height + 20;
let allCornersNearDock = true;
for (const corner of corners) {
if (corner.x < dockLeft || corner.x > dockRight ||
corner.y < dockTop || corner.y > dockBottom) {
allCornersNearDock = false;
break;
}
}
if (allCornersNearDock) {
// Check docking criteria
const speed = Math.sqrt(ship.velocity.x * ship.velocity.x + ship.velocity.y * ship.velocity.y);
const speedInKnots = speed * 1.94384; // Convert to knots
const angleDiff = Math.abs((ship.angle * 180 / Math.PI) % 360);
const normalizedAngleDiff = Math.min(angleDiff, 360 - angleDiff);
if (speedInKnots <= successCriteria.maxDockingSpeed &&
normalizedAngleDiff <= successCriteria.maxDockingAngle) {
gameOver(true, "Perfect Docking!");
} else {
gameOver(false, "Docking failed! Speed or angle was not within limits.");
}
}
}
function getShipCorners() {
const cos = Math.cos(ship.angle);
const sin = Math.sin(ship.angle);
const w2 = ship.width / 2;
const h2 = ship.height / 2;
return [
{
x: ship.x + cos * w2 - sin * h2,
y: ship.y + sin * w2 + cos * h2
},
{
x: ship.x + cos * w2 + sin * h2,
y: ship.y + sin * w2 - cos * h2
},
{
x: ship.x - cos * w2 + sin * h2,
y: ship.y - sin * w2 - cos * h2
},
{
x: ship.x - cos * w2 - sin * h2,
y: ship.y - sin * w2 + cos * h2
}
];
}
function updateDashboard() {
const speed = Math.sqrt(ship.velocity.x * ship.velocity.x + ship.velocity.y * ship.velocity.y);
const speedInKnots = speed * 1.94384; // Convert to knots
document.getElementById('speedDisplay').textContent = speedInKnots.toFixed(1);
let heading = (ship.angle * 180 / Math.PI) % 360;
if (heading < 0) heading += 360;
document.getElementById('headingDisplay').textContent = heading.toFixed(0);
const distanceToDock = Math.sqrt(
Math.pow(ship.x - dock.x, 2) +
Math.pow(ship.y - dock.y, 2)
);
document.getElementById('distanceDisplay').textContent = distanceToDock.toFixed(0);
document.getElementById('timeDisplay').textContent = (gameTime / 1000).toFixed(0);
}
function gameOver(success, message) {
if (!gameActive) return;
gameActive = false;
const resultElement = document.getElementById('gameResult');
const scoreElement = document.getElementById('gameScore');
if (success) {
resultElement.textContent = message;
scoreElement.textContent = `Time: ${(gameTime / 1000).toFixed(0)} seconds`;
} else {
resultElement.textContent = message;
scoreElement.textContent = "Try again!";
}
document.getElementById('gameOverScreen').style.display = 'block';
}
// Render game
function render() {
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw water
ctx.fillStyle = '#0077be';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw port
ctx.fillStyle = '#555555';
ctx.fillRect(port.x, 0, port.width, 100);
ctx.fillRect(port.x, canvas.height - 100, port.width, 100);
// Draw dock
ctx.fillStyle = '#8B4513';
ctx.fillRect(dock.x, dock.y, dock.width, dock.height);
// Draw dock bumpers
ctx.fillStyle = '#333333';
ctx.fillRect(dock.x - 5, dock.y, 5, dock.height);
// Draw ship
ctx.save();
ctx.translate(ship.x, ship.y);
ctx.rotate(ship.angle);
// Ship hull
ctx.fillStyle = '#DDDDDD';
ctx.beginPath();
ctx.moveTo(ship.width / 2, 0);
ctx.lineTo(ship.width / 4, -ship.height / 2);
ctx.lineTo(-ship.width / 2, -ship.height / 2);
ctx.lineTo(-ship.width / 2, ship.height / 2);
ctx.lineTo(ship.width / 4, ship.height / 2);
ctx.closePath();
ctx.fill();
// Ship deck structures
ctx.fillStyle = '#AAAAAA';
ctx.fillRect(-ship.width / 4, -ship.height / 3, ship.width / 3, ship.height * 2/3);
// Bow thruster indicator
if (ship.bowThruster !== 0) {
ctx.fillStyle = ship.bowThruster < 0 ? 'rgba(255, 0, 0, 0.7)' : 'rgba(0, 255, 0, 0.7)';
ctx.beginPath();
if (ship.bowThruster < 0) {
ctx.moveTo(ship.width / 4, -ship.height / 2 - 10);
ctx.lineTo(ship.width / 4 - 20, -ship.height / 2);
ctx.lineTo(ship.width / 4, -ship.height / 2 + 10);
} else {
ctx.moveTo(ship.width / 4, -ship.height / 2 - 10);
ctx.lineTo(ship.width / 4 + 20, -ship.height / 2);
ctx.lineTo(ship.width / 4, -ship.height / 2 + 10);
}
ctx.closePath();
ctx.fill();
}
// Stern thruster indicator
if (ship.sternThruster !== 0) {
ctx.fillStyle = ship.sternThruster < 0 ? 'rgba(255, 0, 0, 0.7)' : 'rgba(0, 255, 0, 0.7)';
ctx.beginPath();
if (ship.sternThruster < 0) {
ctx.moveTo(-ship.width / 2, -ship.height / 2 - 10);
ctx.lineTo(-ship.width / 2 - 20, -ship.height / 2);
ctx.lineTo(-ship.width / 2, -ship.height / 2 + 10);
} else {
ctx.moveTo(-ship.width / 2, -ship.height / 2 - 10);
ctx.lineTo(-ship.width / 2 + 20, -ship.height / 2);
ctx.lineTo(-ship.width / 2, -ship.height / 2 + 10);
}
ctx.closePath();
ctx.fill();
}
// Main thrust indicator
if (ship.mainThrust !== 0) {
ctx.fillStyle = ship.mainThrust < 0 ? 'rgba(255, 0, 0, 0.7)' : 'rgba(0, 255, 0, 0.7)';
ctx.beginPath();
if (ship.mainThrust < 0) {
ctx.moveTo(ship.width / 2 + 10, -10);
ctx.lineTo(ship.width / 2 + 30, 0);
ctx.lineTo(ship.width / 2 + 10, 10);
} else {
ctx.moveTo(-ship.width / 2 - 10, -10);
ctx.lineTo(-ship.width / 2 - 30, 0);
ctx.lineTo(-ship.width / 2 - 10, 10);
}
ctx.closePath();
ctx.fill();
}
ctx.restore();
// Draw water ripples/wake effect
if (Math.abs(ship.velocity.x) > 0.05 || Math.abs(ship.velocity.y) > 0.05) {
const wakeLength = Math.sqrt(ship.velocity.x * ship.velocity.x + ship.velocity.y * ship.velocity.y) * 30;
const wakeAngle = Math.atan2(ship.velocity.y, ship.velocity.x) + Math.PI;
ctx.save();
ctx.translate(ship.x, ship.y);
ctx.rotate(wakeAngle);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 2;
// Draw wake lines
for (let i = -2; i <= 2; i += 2) {
ctx.beginPath();
ctx.moveTo(0, i * 5);
// Create wavy wake line
for (let j = 1; j < wakeLength; j += 5) {
const waveHeight = Math.sin(j * 0.1) * 3;
ctx.lineTo(j, i * 5 + waveHeight);
}
ctx.stroke();
}
ctx.restore();
}
// Draw wind and current indicators
drawEnvironmentalIndicators();
// Draw instructions
drawInstructions();
}
function drawEnvironmentalIndicators() {
// Draw wind indicator
ctx.save();
ctx.translate(100, 100);
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.beginPath();
ctx.arc(0, 0, 30, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText('WIND', 0, -40);
// Wind arrow
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 15);
ctx.lineTo(0, -15);
ctx.moveTo(-10, -5);
ctx.lineTo(0, -15);
ctx.lineTo(10, -5);
ctx.stroke();
ctx.restore();
// Draw current indicator
ctx.save();
ctx.translate(100, 180);
ctx.fillStyle = 'rgba(100, 200, 255, 0.7)';
ctx.beginPath();
ctx.arc(0, 0, 30, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText('CURRENT', 0, -40);
// Current arrow
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-15, 0);
ctx.lineTo(15, 0);
ctx.moveTo(5, -10);
ctx.lineTo(15, 0);
ctx.lineTo(5, 10);
ctx.stroke();
ctx.restore();
}
function drawInstructions() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(canvas.width/2 - 200, 20, 400, 60);
ctx.fillStyle = 'white';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('Dock the vessel at the brown dock. Approach slowly and carefully!', canvas.width/2, 45);
ctx.fillText('Speed must be under 0.5 knots and angle within 5° for successful docking.', canvas.width/2, 70);
}
// Game loop
function gameLoop(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
const deltaTime = timestamp - lastTimestamp;
lastTimestamp = timestamp;
if (gameActive) {
gameTime += deltaTime;
updatePhysics(deltaTime);
render();
requestAnimationFrame(gameLoop);
}
}
// Handle window resize
window.addEventListener('resize', () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Update dock and port positions
dock.x = canvas.width * 0.8;
dock.y = canvas.height * 0.5;
port.x = canvas.width * 0.7;
port.width = canvas.width * 0.3;
port.height = canvas.height;
});
// Start the game
requestAnimationFrame(gameLoop);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=navalmen007/docking-simulator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>