Add js-dos powered arcade page with 6 classic DOS games

This commit is contained in:
Roberth Rajala 2026-02-02 15:42:15 +01:00
parent f5cf0a3fd5
commit 4e98955c92
1 changed files with 668 additions and 0 deletions

668
arcade.html Normal file
View File

@ -0,0 +1,668 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🕹️ LemonLink Arcade | Powered by js-dos</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
<!-- js-dos CSS -->
<link rel="stylesheet" href="https://v8.js-dos.com/latest/js-dos.css">
<style>
:root {
--arcade-bg: #0a0a0f;
--arcade-purple: #8b5cf6;
--arcade-pink: #ec4899;
--arcade-cyan: #22d3ee;
--arcade-yellow: #facc15;
--arcade-green: #22c55e;
--arcade-red: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
font-family: 'Space Grotesk', sans-serif;
background: var(--arcade-bg);
color: #fff;
min-height: 100vh;
overflow-x: hidden;
}
/* CRT Scanline Effect */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
rgba(18, 16, 16, 0) 50%,
rgba(0, 0, 0, 0.25) 50%
);
background-size: 100% 4px;
pointer-events: none;
z-index: 9999;
animation: scanline 8s linear infinite;
}
@keyframes scanline {
0% { transform: translateY(0); }
100% { transform: translateY(10px); }
}
/* Header */
.arcade-header {
text-align: center;
padding: 3rem 1rem;
background: linear-gradient(180deg, rgba(139, 92, 246, 0.2) 0%, transparent 100%);
position: relative;
}
.back-link {
position: absolute;
top: 1.5rem;
left: 1.5rem;
color: var(--arcade-cyan);
text-decoration: none;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s;
}
.back-link:hover {
color: var(--arcade-yellow);
transform: translateX(-5px);
}
.arcade-title {
font-family: 'Press Start 2P', cursive;
font-size: clamp(1.5rem, 5vw, 3rem);
background: linear-gradient(90deg, var(--arcade-purple), var(--arcade-pink), var(--arcade-cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(139, 92, 246, 0.5);
animation: glow 2s ease-in-out infinite alternate;
margin-bottom: 1rem;
}
@keyframes glow {
from { filter: drop-shadow(0 0 10px rgba(139, 92, 246, 0.5)); }
to { filter: drop-shadow(0 0 20px rgba(236, 72, 153, 0.8)); }
}
.arcade-subtitle {
color: #94a3b8;
font-size: 1.1rem;
max-width: 600px;
margin: 0 auto 1rem;
}
.legal-note {
padding: 0.75rem 1.5rem;
background: rgba(34, 211, 238, 0.1);
border: 1px solid rgba(34, 211, 238, 0.3);
border-radius: 100px;
display: inline-block;
font-size: 0.8rem;
color: var(--arcade-cyan);
}
/* Game Container */
.game-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Game Selection Grid */
.games-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.game-card {
background: rgba(20, 20, 30, 0.8);
border: 2px solid rgba(139, 92, 246, 0.3);
border-radius: 16px;
overflow: hidden;
transition: all 0.3s;
position: relative;
cursor: pointer;
}
.game-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
opacity: 0;
transition: opacity 0.3s;
}
.game-card:hover {
transform: translateY(-5px);
border-color: var(--arcade-purple);
box-shadow: 0 10px 40px rgba(139, 92, 246, 0.3);
}
.game-card:hover::before {
opacity: 1;
}
.game-card.active {
border-color: var(--arcade-green);
box-shadow: 0 0 20px rgba(34, 197, 94, 0.4);
}
.game-preview {
height: 160px;
background: linear-gradient(135deg, #1a1a2e 0%, #0f0f1a 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 4rem;
position: relative;
overflow: hidden;
}
.game-preview::after {
content: '▶';
position: absolute;
font-size: 2rem;
opacity: 0;
transition: all 0.3s;
color: var(--arcade-yellow);
text-shadow: 0 0 20px rgba(250, 204, 21, 0.8);
}
.game-card:hover .game-preview::after {
opacity: 1;
transform: scale(1.2);
}
.game-card.active .game-preview::after {
content: '●';
color: var(--arcade-green);
opacity: 1;
}
.game-info {
padding: 1.25rem;
}
.game-title {
font-family: 'Press Start 2P', cursive;
font-size: 0.75rem;
color: var(--arcade-yellow);
margin-bottom: 0.5rem;
line-height: 1.4;
}
.game-desc {
color: #94a3b8;
font-size: 0.85rem;
margin-bottom: 0.75rem;
}
.game-tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.game-tag {
padding: 0.2rem 0.6rem;
background: rgba(139, 92, 246, 0.2);
border-radius: 100px;
font-size: 0.7rem;
color: var(--arcade-purple);
}
/* Active Game Area */
.active-game-section {
display: none;
background: rgba(20, 20, 30, 0.9);
border: 2px solid rgba(139, 92, 246, 0.4);
border-radius: 16px;
overflow: hidden;
}
.active-game-section.visible {
display: block;
}
.game-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: rgba(139, 92, 246, 0.1);
border-bottom: 1px solid rgba(139, 92, 246, 0.3);
}
.game-header-title {
font-family: 'Press Start 2P', cursive;
font-size: 0.8rem;
color: var(--arcade-yellow);
}
.game-controls {
display: flex;
gap: 0.75rem;
}
.control-btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 8px;
font-family: 'Space Grotesk', sans-serif;
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.3s;
}
.control-btn.fullscreen {
background: linear-gradient(90deg, var(--arcade-purple), var(--arcade-pink));
color: #fff;
}
.control-btn.close {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.5);
color: var(--arcade-red);
}
.control-btn:hover {
transform: scale(1.05);
}
/* js-dos Container */
#dos-container {
width: 100%;
aspect-ratio: 4/3;
max-height: 70vh;
background: #000;
}
/* Loading State */
.loading-overlay {
display: none;
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.9);
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1rem;
}
.loading-overlay.visible {
display: flex;
}
.loading-text {
font-family: 'Press Start 2P', cursive;
font-size: 0.8rem;
color: var(--arcade-yellow);
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading-bar {
width: 200px;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
}
.loading-progress {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--arcade-purple), var(--arcade-pink));
animation: progress 2s infinite;
}
@keyframes progress {
0% { width: 0%; }
50% { width: 100%; }
100% { width: 0%; }
}
/* Instructions */
.instructions {
padding: 1.5rem;
background: rgba(0, 0, 0, 0.3);
border-top: 1px solid rgba(139, 92, 246, 0.2);
}
.instructions-title {
font-family: 'Press Start 2P', cursive;
font-size: 0.65rem;
color: var(--arcade-cyan);
margin-bottom: 1rem;
}
.key-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
list-style: none;
}
.key-item {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.85rem;
color: #94a3b8;
}
.key-badge {
padding: 0.25rem 0.5rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
font-family: monospace;
font-size: 0.75rem;
color: #fff;
min-width: 60px;
text-align: center;
}
/* Footer */
.arcade-footer {
text-align: center;
padding: 2rem;
color: #64748b;
font-size: 0.8rem;
}
.arcade-footer a {
color: var(--arcade-purple);
text-decoration: none;
}
/* Responsive */
@media (max-width: 768px) {
.games-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.game-preview {
height: 120px;
font-size: 3rem;
}
.game-header {
flex-direction: column;
gap: 0.75rem;
text-align: center;
}
#dos-container {
aspect-ratio: 4/3;
}
}
</style>
</head>
<body>
<!-- Header -->
<header class="arcade-header">
<a href="index.html" class="back-link">
← Back to LemonLink
</a>
<h1 class="arcade-title">🕹️ LEMONLINK ARCADE</h1>
<p class="arcade-subtitle">Play classic DOS games directly in your browser powered by js-dos</p>
<div class="legal-note">⚖️ All games are abandonware or shareware - legally distributable</div>
</header>
<!-- Game Container -->
<main class="game-container">
<!-- Game Selection Grid -->
<div class="games-grid" id="gamesGrid">
<!-- Games will be loaded by JavaScript -->
</div>
<!-- Active Game Area -->
<div class="active-game-section" id="activeGameSection">
<div class="game-header">
<span class="game-header-title" id="activeGameTitle">Select a Game</span>
<div class="game-controls">
<button class="control-btn fullscreen" id="fullscreenBtn">⛶ Fullscreen</button>
<button class="control-btn close" id="closeGameBtn">✕ Close</button>
</div>
</div>
<div id="dos-container"></div>
<div class="instructions" id="gameInstructions">
<div class="instructions-title">🎮 CONTROLS</div>
<ul class="key-list" id="keyList">
<!-- Dynamic key list -->
</ul>
</div>
</div>
</main>
<!-- Footer -->
<footer class="arcade-footer">
<p>Powered by <a href="https://js-dos.com/" target="_blank">js-dos</a> | Built with 💜 by LemonLink</p>
</footer>
<!-- js-dos Script -->
<script src="https://v8.js-dos.com/latest/js-dos.js"></script>
<script>
// Game Library - Using publicly available bundles
const games = [
{
id: 'digger',
title: 'Digger',
emoji: '🟡',
description: 'Classic digging arcade game from 1983',
url: 'https://v8.js-dos.com/bundles/digger.jsdos',
tags: ['Arcade', '1983'],
controls: [
{ key: '←↑↓→', action: 'Move digger' },
{ key: 'F1', action: 'Fire/Shoot' },
{ key: 'ESC', action: 'Pause' }
]
},
{
id: 'doom',
title: 'DOOM',
emoji: '🔫',
description: 'The legendary FPS that defined a genre',
url: 'https://v8.js-dos.com/bundles/doom.jsdos',
tags: ['FPS', '1993'],
controls: [
{ key: '←↑↓→', action: 'Move/Strafe' },
{ key: 'CTRL', action: 'Fire' },
{ key: 'SPACE', action: 'Use/Open' },
{ key: '1-7', action: 'Change weapon' },
{ key: 'ESC', action: 'Menu' }
]
},
{
id: 'wolf3d',
title: 'Wolfenstein 3D',
emoji: '🪖',
description: 'The original first-person shooter',
url: 'https://v8.js-dos.com/bundles/wolf3d.jsdos',
tags: ['FPS', '1992'],
controls: [
{ key: '←↑↓→', action: 'Move/Turn' },
{ key: 'CTRL', action: 'Fire' },
{ key: 'SPACE', action: 'Open doors' },
{ key: '1-4', action: 'Change weapon' },
{ key: 'ESC', action: 'Menu' }
]
},
{
id: 'prince',
title: 'Prince of Persia',
emoji: '🤴',
description: 'Revolutionary cinematic platformer',
url: 'https://v8.js-dos.com/bundles/prince.jsdos',
tags: ['Platformer', '1989'],
controls: [
{ key: '←→', action: 'Walk/Run' },
{ key: 'SHIFT', action: 'Grab ledge' },
{ key: '↑', action: 'Jump up' },
{ key: '↓', action: 'Crouch' },
{ key: 'SHIFT+←→', action: 'Step carefully' }
]
},
{
id: 'duke3d',
title: 'Duke Nukem 3D',
emoji: '😎',
description: 'The king of action with attitude',
url: 'https://v8.js-dos.com/bundles/duke3d.jsdos',
tags: ['FPS', '1996'],
controls: [
{ key: '←↑↓→', action: 'Move' },
{ key: 'CTRL', action: 'Fire' },
{ key: 'SPACE', action: 'Jump' },
{ key: 'SHIFT', action: 'Run' },
{ key: '1-9', action: 'Weapons' }
]
},
{
id: 'quake',
title: 'Quake',
emoji: '⚡',
description: 'Revolutionary 3D shooter with true 3D engine',
url: 'https://v8.js-dos.com/bundles/quake.jsdos',
tags: ['FPS', '1996'],
controls: [
{ key: 'WASD', action: 'Move' },
{ key: '←→', action: 'Look' },
{ key: 'CTRL', action: 'Fire' },
{ key: 'SPACE', action: 'Jump' },
{ key: '1-8', action: 'Weapons' }
]
}
];
// State
let currentGame = null;
let dosInstance = null;
// Initialize game grid
function initGameGrid() {
const grid = document.getElementById('gamesGrid');
games.forEach(game => {
const card = document.createElement('div');
card.className = 'game-card';
card.dataset.gameId = game.id;
card.innerHTML = `
<div class="game-preview">${game.emoji}</div>
<div class="game-info">
<div class="game-title">${game.title}</div>
<div class="game-desc">${game.description}</div>
<div class="game-tags">
${game.tags.map(tag => `<span class="game-tag">${tag}</span>`).join('')}
</div>
</div>
`;
card.addEventListener('click', () => loadGame(game));
grid.appendChild(card);
});
}
// Load a game
async function loadGame(game) {
// Update UI
document.querySelectorAll('.game-card').forEach(c => c.classList.remove('active'));
document.querySelector(`[data-game-id="${game.id}"]`).classList.add('active');
// Show game section
document.getElementById('activeGameSection').classList.add('visible');
document.getElementById('activeGameTitle').textContent = `NOW PLAYING: ${game.title.toUpperCase()}`;
// Update controls
const keyList = document.getElementById('keyList');
keyList.innerHTML = game.controls.map(c => `
<li class="key-item">
<span class="key-badge">${c.key}</span>
<span>${c.action}</span>
</li>
`).join('');
// Scroll to game
document.getElementById('activeGameSection').scrollIntoView({ behavior: 'smooth' });
// Cleanup previous instance
if (dosInstance) {
dosInstance.stop();
dosInstance = null;
}
// Clear container
const container = document.getElementById('dos-container');
container.innerHTML = '';
currentGame = game;
// Load js-dos
try {
dosInstance = await Dos(container, {
url: game.url,
theme: 'dark'
});
} catch (err) {
console.error('Failed to load game:', err);
container.innerHTML = `
<div style="display:flex;align-items:center;justify-content:center;height:100%;flex-direction:column;gap:1rem;color:#ef4444;">
<div style="font-size:3rem;">⚠️</div>
<div>Failed to load game. Please try again.</div>
<div style="font-size:0.8rem;color:#64748b;">${err.message}</div>
</div>
`;
}
}
// Close game
document.getElementById('closeGameBtn').addEventListener('click', () => {
if (dosInstance) {
dosInstance.stop();
dosInstance = null;
}
currentGame = null;
document.getElementById('activeGameSection').classList.remove('visible');
document.getElementById('dos-container').innerHTML = '';
document.querySelectorAll('.game-card').forEach(c => c.classList.remove('active'));
});
// Fullscreen
document.getElementById('fullscreenBtn').addEventListener('click', () => {
const container = document.getElementById('dos-container');
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.webkitRequestFullscreen) {
container.webkitRequestFullscreen();
}
});
// Initialize
document.addEventListener('DOMContentLoaded', initGameGrid);
</script>
</body>
</html>