245 lines
7.6 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0f1720" />
<title>Arrowstack Airship</title>
<style>
body {
touch-action: none;
overscroll-behavior: none;
}
#mobile-controls {
position: fixed;
inset: 0;
z-index: 100;
pointer-events: none;
user-select: none;
-webkit-user-select: none;
}
#drag-stick {
position: absolute;
left: max(18px, env(safe-area-inset-left));
bottom: max(22px, env(safe-area-inset-bottom));
width: 132px;
height: 132px;
border: 1px solid rgba(255, 255, 255, 0.28);
border-radius: 50%;
background: rgba(7, 16, 24, 0.42);
box-shadow: inset 0 0 0 1px rgba(140, 211, 255, 0.08);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
pointer-events: auto;
touch-action: none;
}
#drag-stick::before,
#drag-stick::after {
content: '';
position: absolute;
background: rgba(232, 242, 247, 0.18);
pointer-events: none;
}
#drag-stick::before {
left: 50%;
top: 12px;
bottom: 12px;
width: 1px;
}
#drag-stick::after {
top: 50%;
left: 12px;
right: 12px;
height: 1px;
}
#drag-thumb {
position: absolute;
left: 50%;
top: 50%;
width: 58px;
height: 58px;
margin: -29px 0 0 -29px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.5);
background: rgba(140, 211, 255, 0.28);
box-shadow: 0 3px 18px rgba(0, 0, 0, 0.28);
pointer-events: none;
will-change: transform;
}
.mobile-control {
position: absolute;
min-width: 72px;
height: 64px;
padding: 0 14px;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 18px;
background: rgba(7, 16, 24, 0.68);
color: #e8f2f7;
font: 700 13px/1 system-ui, sans-serif;
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
pointer-events: auto;
touch-action: none;
}
.mobile-control:active,
.mobile-control.active {
background: rgba(140, 211, 255, 0.34);
transform: scale(0.96);
}
.mobile-control.port {
right: max(18px, env(safe-area-inset-right));
bottom: max(100px, calc(env(safe-area-inset-bottom) + 82px));
}
.mobile-control.starboard {
right: max(18px, env(safe-area-inset-right));
bottom: max(26px, env(safe-area-inset-bottom));
}
.mobile-control.restart {
top: max(14px, env(safe-area-inset-top));
right: max(14px, env(safe-area-inset-right));
width: 48px;
min-width: 48px;
height: 48px;
padding: 0;
border-radius: 50%;
font-size: 20px;
}
@media (hover: hover) and (pointer: fine) {
#mobile-controls { display: none; }
}
</style>
</head>
<body>
<div id="app">
<div id="game"></div>
</div>
<div id="mobile-controls" aria-label="Touch controls">
<div id="drag-stick" aria-label="Drag to steer and throttle">
<div id="drag-thumb"></div>
</div>
<button class="mobile-control port" data-code="KeyQ" data-key="q" data-keycode="81" aria-label="Fire port broadside">PORT</button>
<button class="mobile-control starboard" data-code="KeyE" data-key="e" data-keycode="69" aria-label="Fire starboard broadside">STARBOARD</button>
<button class="mobile-control ship-action buy" data-action="open-shipyard" aria-label="Open shipyard">SHIPYARD</button>
<button class="mobile-control restart" data-code="KeyR" data-key="r" data-keycode="82" aria-label="Restart">↻</button>
</div>
<script type="module" src="/src/game-entry.js"></script>
<script type="module">
const dispatchKey = (type, key, code, keyCode) => {
const event = new KeyboardEvent(type, {
key,
code,
bubbles: true,
cancelable: true,
});
Object.defineProperty(event, 'keyCode', { get: () => keyCode });
Object.defineProperty(event, 'which', { get: () => keyCode });
window.dispatchEvent(event);
};
const emitHelmVector = (x, y) => {
window.dispatchEvent(new CustomEvent('airship:helm-vector', { detail: { x, y } }));
};
const releaseStick = () => {
document.querySelector('#drag-thumb').style.transform = 'translate(0px, 0px)';
emitHelmVector(0, 0);
};
const stick = document.querySelector('#drag-stick');
const thumb = document.querySelector('#drag-thumb');
const radius = 45;
let stickPointer = null;
const updateStick = (event) => {
const rect = stick.getBoundingClientRect();
let dx = event.clientX - (rect.left + rect.width / 2);
let dy = event.clientY - (rect.top + rect.height / 2);
const length = Math.hypot(dx, dy);
if (length > radius) {
dx = dx / length * radius;
dy = dy / length * radius;
}
thumb.style.transform = `translate(${dx}px, ${dy}px)`;
emitHelmVector(dx / radius, dy / radius);
};
stick.addEventListener('pointerdown', (event) => {
event.preventDefault();
stickPointer = event.pointerId;
stick.setPointerCapture?.(event.pointerId);
updateStick(event);
});
stick.addEventListener('pointermove', (event) => {
if (event.pointerId !== stickPointer) return;
event.preventDefault();
updateStick(event);
});
const endStick = (event) => {
if (stickPointer !== null && event.pointerId !== undefined && event.pointerId !== stickPointer) return;
event.preventDefault?.();
stickPointer = null;
releaseStick();
};
stick.addEventListener('pointerup', endStick);
stick.addEventListener('pointercancel', endStick);
stick.addEventListener('lostpointercapture', endStick);
stick.addEventListener('contextmenu', (event) => event.preventDefault());
for (const button of document.querySelectorAll('.mobile-control')) {
const isAction = Boolean(button.dataset.action);
const fire = (type) => {
if (isAction) {
if (type === 'keydown') {
window.dispatchEvent(new CustomEvent('airship:action', {
detail: { action: button.dataset.action },
}));
}
return;
}
dispatchKey(type, button.dataset.key, button.dataset.code, Number(button.dataset.keycode));
};
const release = (event) => {
event.preventDefault();
fire('keyup');
button.classList.remove('active');
};
button.addEventListener('pointerdown', (event) => {
event.preventDefault();
button.setPointerCapture?.(event.pointerId);
fire('keydown');
button.classList.add('active');
});
button.addEventListener('pointerup', release);
button.addEventListener('pointercancel', release);
button.addEventListener('lostpointercapture', () => {
fire('keyup');
button.classList.remove('active');
});
button.addEventListener('contextmenu', (event) => event.preventDefault());
}
window.addEventListener('blur', releaseStick);
</script>
</body>
</html>