228 lines
7.2 KiB
HTML
228 lines
7.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<meta property="og:title" content="Y.A.R.N HTML5" />
|
|
<meta property="og:description" content="A collaborative story-writing game. Remake of the 2000s GameSpy Arcade classic." />
|
|
<title>Y.A.R.N. 2026</title>
|
|
|
|
<!-- re-use the same CSS-variables and base look -->
|
|
<style>
|
|
:root {
|
|
--bg-main: #4b0082;
|
|
--text-beige: #FFFFE4;
|
|
--box-bg: #0a0a0a;
|
|
--accent-purple: #6a0dad;
|
|
--player-green: #00ff88;
|
|
}
|
|
* { margin:0; padding:0; box-sizing:border-box; font-family:system-ui, sans-serif; }
|
|
body {
|
|
height:100vh;
|
|
background:var(--bg-main);
|
|
color:var(--text-beige);
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content:center;
|
|
}
|
|
#splash {
|
|
background:var(--box-bg);
|
|
border:2px solid var(--accent-purple);
|
|
border-radius:8px;
|
|
padding:2rem 3rem;
|
|
min-width:320px;
|
|
text-align:center;
|
|
}
|
|
h1 { margin-bottom:1.5rem; letter-spacing:1px; }
|
|
.section { margin:1.2rem 0; }
|
|
input[type=text], button {
|
|
width:100%;
|
|
padding:.6rem .8rem;
|
|
font-size:1rem;
|
|
border-radius:4px;
|
|
border:none;
|
|
}
|
|
input[type=text] {
|
|
background:#1a1a1a;
|
|
border:1px solid var(--accent-purple);
|
|
color:var(--text-beige);
|
|
margin-bottom:.6rem;
|
|
}
|
|
button {
|
|
background:var(--accent-purple);
|
|
color:#fff;
|
|
font-weight:bold;
|
|
cursor:pointer;
|
|
transition:background .2s;
|
|
}
|
|
button:hover:not(:disabled) { background:#8a1dbd; }
|
|
button:disabled { background:#444; cursor:not-allowed; }
|
|
|
|
#room-list {
|
|
max-height:220px;
|
|
overflow-y:auto;
|
|
text-align:left;
|
|
margin-top:.6rem;
|
|
}
|
|
.room-row {
|
|
display:flex;
|
|
justify-content:space-between;
|
|
padding:.4rem .6rem;
|
|
cursor:pointer;
|
|
border-radius:3px;
|
|
}
|
|
.room-row:hover { background:#222; }
|
|
.room-name { flex:1; }
|
|
.room-count { color:var(--player-green); }
|
|
.no-rooms { color:#888; font-style:italic; }
|
|
|
|
/* About overlay */
|
|
#about-overlay {
|
|
position:fixed; inset:0;
|
|
background:rgba(0,0,0,.75);
|
|
display:flex; align-items:center; justify-content:center;
|
|
padding:1rem;
|
|
}
|
|
#about-box {
|
|
background:var(--box-bg);
|
|
border:2px solid var(--accent-purple);
|
|
border-radius:8px;
|
|
padding:2rem 2.5rem;
|
|
max-width:640px;
|
|
color:var(--text-beige);
|
|
line-height:1.45;
|
|
}
|
|
#about-box h3 { margin-top:0; margin-bottom:1rem; }
|
|
#about-box p { margin:.6rem 0; }
|
|
#about-box ul { margin:.6rem 0 1rem 1.2rem; }
|
|
#about-box button {
|
|
margin-top:1rem; width:auto; padding:.6rem 1.2rem;
|
|
}
|
|
.hidden { display:none !important; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="splash">
|
|
<h2>Y.A.R.N.</h2>
|
|
<h4>HTML5 Edition</h4>
|
|
|
|
<!-- ENTER GAME -->
|
|
<div class="section">
|
|
<input id="room-input" type="text" placeholder="Room name..." maxlength="30"/>
|
|
<button id="enter-btn">Enter Game</button>
|
|
</div>
|
|
|
|
<!-- FIND GAME -->
|
|
<div class="section">
|
|
<button id="find-btn">Find Games</button>
|
|
<div id="room-list"></div>
|
|
</div>
|
|
|
|
<!-- ABOUT -->
|
|
<div class="section">
|
|
<button id="about-btn">About Y.A.R.N.</button>
|
|
</div>
|
|
|
|
<!-- ABOUT OVERLAY -->
|
|
<div id="about-overlay" class="hidden">
|
|
<div id="about-box">
|
|
<h3>What is Y.A.R.N HTML5?</h3>
|
|
<p>Y.A.R.N was a collaborative story-writing "parlor game" originally included in GameSpy Arcade in the early 2000s, but removed shortly thereafter due to security issues that were never resolved.</p>
|
|
<p><strong>Y.A.R.N. HTML5</strong> is a modern remake of (what I can remember from) the original, designed to run in-browser, and with extra features such as (optional) LLM-based bots.</p>
|
|
<p><strong>The rules are simple:</strong></p>
|
|
<ul>
|
|
<li>Players have a limited amount of time to write a sentence, selected by the host at game start.</li>
|
|
<li>Once all players have submitted their sentences, players have 30 seconds to vote for their favorite.</li>
|
|
<li>The player whose sentence is chosen receives 5 points, plus one point for each vote they received.</li>
|
|
<li>When a player reaches the point limit, they win the game.</li>
|
|
</ul>
|
|
<p>Enjoy the story-writing!<br><br>
|
|
Report any bugs/comments/questions/concerns in the <a href="https://discord.gg/nvP92Mc">Juke Gaming Network Discord</a> or message the author <strong>@juke420</strong>.</p>
|
|
<button id="close-about">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const roomInput = document.getElementById('room-input');
|
|
const enterBtn = document.getElementById('enter-btn');
|
|
const findBtn = document.getElementById('find-btn');
|
|
const roomListBox = document.getElementById('room-list');
|
|
const aboutBtn = document.getElementById('about-btn');
|
|
const aboutBox = document.getElementById('about-overlay');
|
|
const closeAbout = document.getElementById('close-about');
|
|
|
|
let findActive = false;
|
|
let pollId = null;
|
|
|
|
function toggleAbout(show){
|
|
aboutBox.classList.toggle('hidden',!show);
|
|
}
|
|
aboutBtn.addEventListener('click', () => toggleAbout(true));
|
|
closeAbout.addEventListener('click', () => toggleAbout(false));
|
|
aboutBox.addEventListener('click', e => { if (e.target===aboutBox) toggleAbout(false); });
|
|
|
|
/* sanitize helper */
|
|
const esc = str => {
|
|
const d = document.createElement('div');
|
|
d.textContent = str;
|
|
return d.innerHTML;
|
|
};
|
|
|
|
/* ENTER GAME */
|
|
function goToRoom() {
|
|
const name = roomInput.value.trim().toLowerCase().replace(/[^a-z0-9-_]/g,'');
|
|
if (!name) { alert('Please enter a room name.'); return; }
|
|
window.location.href = '/game/' + encodeURIComponent(name);
|
|
}
|
|
enterBtn.addEventListener('click', goToRoom);
|
|
roomInput.addEventListener('keydown', e => { if (e.key==='Enter') goToRoom(); });
|
|
|
|
/* FIND GAME */
|
|
async function fetchRooms() {
|
|
try {
|
|
const r = await fetch('/api/rooms');
|
|
if (!r.ok) throw new Error();
|
|
const data = await r.json(); // {rooms:[{name,playerCount},...]}
|
|
renderRooms(data.rooms || []);
|
|
} catch {
|
|
renderRooms([]);
|
|
}
|
|
}
|
|
|
|
function renderRooms(list) {
|
|
if (!list.length) {
|
|
roomListBox.innerHTML = '<div class="no-rooms">No open rooms right now.</div>';
|
|
return;
|
|
}
|
|
roomListBox.innerHTML = list.map(r => `
|
|
<div class="room-row" data-room="${esc(r.name)}">
|
|
<span class="room-name">${esc(r.name)}</span>
|
|
<span class="room-count">${r.playerCount} player${r.playerCount===1?'':'s'}</span>
|
|
</div>`).join('');
|
|
|
|
roomListBox.querySelectorAll('.room-row').forEach(row => {
|
|
row.addEventListener('click', () => {
|
|
window.location.href = '/game/' + encodeURIComponent(row.dataset.room);
|
|
});
|
|
});
|
|
}
|
|
|
|
function toggleFind() {
|
|
findActive = !findActive;
|
|
if (findActive) {
|
|
findBtn.textContent = 'Stop Refresh';
|
|
fetchRooms();
|
|
pollId = setInterval(fetchRooms, 2000);
|
|
} else {
|
|
findBtn.textContent = 'Find Game';
|
|
clearInterval(pollId);
|
|
roomListBox.innerHTML = '';
|
|
}
|
|
}
|
|
findBtn.addEventListener('click', toggleFind);
|
|
</script>
|
|
|
|
</body>
|
|
</html> |