838 lines
21 KiB
HTML
838 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>YARN 2026 – Spin Your Story!</title>
|
||
<style>
|
||
:root {
|
||
--bg-main: #4b0082;
|
||
--text-beige: #FFFFE4;
|
||
--box-bg: #0a0a0a;
|
||
--player-green: #00ff88;
|
||
--player-yellow: #ffdd00;
|
||
--header-bg: #3a0066;
|
||
--accent-purple: #6a0dad;
|
||
--chat-text: #ffffcc;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
height: 100vh;
|
||
background: var(--bg-main);
|
||
font-family: system-ui, sans-serif;
|
||
color: var(--text-beige);
|
||
overflow: hidden;
|
||
}
|
||
|
||
#layout {
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#header {
|
||
flex: 0 0 5%;
|
||
background: var(--header-bg);
|
||
color: white;
|
||
font-size: 1.4rem;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-bottom: 2px solid var(--accent-purple);
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
#main-text {
|
||
flex: 0 0 50%;
|
||
background: var(--box-bg);
|
||
margin: 1.2rem 2rem 0 2rem;
|
||
border-radius: 8px;
|
||
padding: 1.6rem 2rem;
|
||
overflow-y: auto;
|
||
font-family: "Times New Roman", Times, serif;
|
||
font-size: 1.25rem;
|
||
line-height: 1.45;
|
||
color: var(--text-beige);
|
||
border: 2px solid var(--accent-purple);
|
||
}
|
||
|
||
.story-segment {
|
||
display: inline;
|
||
}
|
||
|
||
.story-segment[data-player]::before {
|
||
content: attr(data-player) ": ";
|
||
font-weight: bold;
|
||
color: var(--accent-purple);
|
||
display: none;
|
||
}
|
||
|
||
#game-bar {
|
||
flex: 0 0 10%;
|
||
background: var(--bg-main);
|
||
padding: 0.8rem 2rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 1.4rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
#game-bar > * {
|
||
font-size: 1.1rem;
|
||
font-weight: bold;
|
||
color: white;
|
||
}
|
||
|
||
#game-bar input[type="text"],
|
||
#game-bar input[type="number"] {
|
||
background: #1a1a1a;
|
||
border: 2px solid var(--accent-purple);
|
||
border-radius: 4px;
|
||
padding: 0.5rem 1rem;
|
||
color: var(--text-beige);
|
||
font-size: 1rem;
|
||
}
|
||
|
||
#game-bar button {
|
||
background: var(--accent-purple);
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 0.6rem 1.2rem;
|
||
color: white;
|
||
font-size: 1rem;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
#game-bar button:hover {
|
||
background: #8a1dbd;
|
||
}
|
||
|
||
#game-bar button:disabled {
|
||
background: #444;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.setting-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.setting-group label {
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.setting-group input[type="number"] {
|
||
width: 80px;
|
||
}
|
||
|
||
#timer {
|
||
font-size: 2rem;
|
||
color: #ff6666;
|
||
min-width: 60px;
|
||
text-align: center;
|
||
}
|
||
|
||
#timer.warning {
|
||
color: #ff3333;
|
||
animation: pulse 0.5s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
#entry-input {
|
||
flex: 1;
|
||
max-width: 500px;
|
||
min-width: 200px;
|
||
}
|
||
|
||
#voting-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
max-height: 100%;
|
||
overflow-y: auto;
|
||
width: 100%;
|
||
max-width: 800px;
|
||
}
|
||
|
||
.vote-entry {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
background: #1a1a1a;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 4px;
|
||
border: 1px solid var(--accent-purple);
|
||
}
|
||
|
||
.vote-entry-text {
|
||
flex: 1;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.vote-btn {
|
||
background: var(--player-green);
|
||
color: #000;
|
||
border: none;
|
||
padding: 0.4rem 1rem;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.vote-btn:disabled {
|
||
background: #444;
|
||
color: #888;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
#chat-area {
|
||
flex: 1;
|
||
display: flex;
|
||
margin: 0 2rem 1.2rem 2rem;
|
||
background: var(--box-bg);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
color: var(--chat-text);
|
||
border: 2px solid var(--accent-purple);
|
||
}
|
||
|
||
#players {
|
||
width: 260px;
|
||
background: #111;
|
||
padding: 1rem 0.8rem;
|
||
overflow-y: auto;
|
||
border-right: 1px solid #333;
|
||
}
|
||
|
||
#players-header {
|
||
font-weight: bold;
|
||
padding: 0.3rem 1rem 0.8rem;
|
||
border-bottom: 1px solid #333;
|
||
margin-bottom: 0.5rem;
|
||
color: white;
|
||
}
|
||
|
||
.player-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 0.15rem 1rem;
|
||
font-family: Arial, Helvetica, sans-serif;
|
||
font-size: 1.05rem;
|
||
line-height: 1.2;
|
||
transition: color 0.3s;
|
||
}
|
||
|
||
.player-row.submitted {
|
||
color: var(--player-green);
|
||
}
|
||
|
||
.player-row.pending {
|
||
color: var(--player-yellow);
|
||
}
|
||
|
||
.player-row.host::after {
|
||
content: " ★";
|
||
color: gold;
|
||
}
|
||
|
||
.player-name {
|
||
text-align: left;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.player-score {
|
||
text-align: right;
|
||
min-width: 70px;
|
||
padding-left: 1rem;
|
||
}
|
||
|
||
#chat-and-input {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#chat-messages {
|
||
flex: 1;
|
||
padding: 1.3rem 1.6rem;
|
||
overflow-y: auto;
|
||
font-size: 1.1rem;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.chat-message {
|
||
margin-bottom: 0.3rem;
|
||
}
|
||
|
||
.chat-message strong {
|
||
color: var(--player-green);
|
||
}
|
||
|
||
.system-message {
|
||
color: #888;
|
||
font-style: italic;
|
||
}
|
||
|
||
#chat-input {
|
||
height: 52px;
|
||
background: #1a1a1a;
|
||
border: none;
|
||
border-top: 1px solid #444;
|
||
color: var(--text-beige);
|
||
font-size: 1.15rem;
|
||
padding: 0 1.6rem;
|
||
outline: none;
|
||
font-family: Arial, Helvetica, sans-serif;
|
||
}
|
||
|
||
#chat-input:focus {
|
||
background: #222;
|
||
}
|
||
|
||
.waiting-message {
|
||
color: #aaa;
|
||
font-style: italic;
|
||
}
|
||
|
||
.game-over-message {
|
||
font-size: 1.5rem;
|
||
color: gold;
|
||
text-align: center;
|
||
}
|
||
|
||
.results-display {
|
||
text-align: center;
|
||
}
|
||
|
||
.round-winner {
|
||
color: var(--player-green);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="layout">
|
||
|
||
<div id="header">
|
||
Y.A.R.N. 2026 - Spin a story! - <span id="room-display"></span>
|
||
</div>
|
||
|
||
<div id="main-text">
|
||
<em>Waiting for the game to begin...</em>
|
||
</div>
|
||
|
||
<div id="game-bar">
|
||
<!-- Dynamic content will be inserted here -->
|
||
</div>
|
||
|
||
<div id="chat-area">
|
||
|
||
<div id="players">
|
||
<div id="players-header">Players</div>
|
||
<div id="players-list">
|
||
<!-- Players will be listed here -->
|
||
</div>
|
||
</div>
|
||
|
||
<div id="chat-and-input">
|
||
<div id="chat-messages">
|
||
<!-- Chat messages will appear here -->
|
||
</div>
|
||
|
||
<input type="text" id="chat-input" placeholder="Type message… (Enter to send)" autocomplete="off">
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script src="/socket.io/socket.io.js"></script>
|
||
<script>
|
||
(function() {
|
||
// Get room name from URL
|
||
const pathParts = window.location.pathname.split('/').filter(p => p);
|
||
const roomName = pathParts[0] || 'default';
|
||
document.getElementById('room-display').textContent = `Room: ${roomName}`;
|
||
|
||
// Get or create player UUID
|
||
let playerUUID = getCookie('yarn_player_uuid');
|
||
if (!playerUUID) {
|
||
playerUUID = generateUUID();
|
||
setCookie('yarn_player_uuid', playerUUID, 365);
|
||
}
|
||
|
||
// Get stored player name
|
||
let playerName = getCookie('yarn_player_name') || '';
|
||
|
||
// Game state
|
||
let gameState = null;
|
||
let hasVoted = false;
|
||
let hasSubmitted = false;
|
||
|
||
// Connect to Socket.IO
|
||
const socket = io();
|
||
|
||
// Join the room
|
||
socket.emit('joinRoom', {
|
||
roomName: roomName,
|
||
playerUUID: playerUUID,
|
||
playerName: playerName
|
||
});
|
||
|
||
// === Socket Event Handlers ===
|
||
|
||
socket.on('gameState', (state) => {
|
||
gameState = state;
|
||
updateUI();
|
||
});
|
||
|
||
socket.on('chatHistory', (chat) => {
|
||
const chatMessages = document.getElementById('chat-messages');
|
||
chatMessages.innerHTML = '';
|
||
chat.forEach(msg => appendChatMessage(msg));
|
||
});
|
||
|
||
socket.on('playersUpdate', (players) => {
|
||
if (gameState) {
|
||
gameState.players = players;
|
||
updatePlayersList();
|
||
}
|
||
});
|
||
|
||
socket.on('settingsUpdate', (settings) => {
|
||
if (gameState) {
|
||
gameState.gameSettings = settings;
|
||
}
|
||
});
|
||
|
||
socket.on('nameSet', (data) => {
|
||
if (data.success) {
|
||
playerName = data.name;
|
||
setCookie('yarn_player_name', playerName, 365);
|
||
document.getElementById('chat-input').disabled = false;
|
||
}
|
||
});
|
||
|
||
socket.on('playerJoined', (data) => {
|
||
appendSystemMessage(`${data.name} joined the game`);
|
||
});
|
||
|
||
socket.on('playerLeft', (data) => {
|
||
appendSystemMessage(`${data.name} left the game`);
|
||
});
|
||
|
||
socket.on('gameStarted', (state) => {
|
||
gameState = state;
|
||
hasSubmitted = false;
|
||
hasVoted = false;
|
||
updateUI();
|
||
appendSystemMessage('The game has started!');
|
||
});
|
||
|
||
socket.on('timerUpdate', (time) => {
|
||
if (gameState) {
|
||
gameState.timeRemaining = time;
|
||
updateTimer();
|
||
}
|
||
});
|
||
|
||
socket.on('votingPhase', (data) => {
|
||
if (gameState) {
|
||
gameState.currentPhase = 'voting';
|
||
gameState.round_data = data.entries;
|
||
gameState.timeRemaining = data.timeRemaining;
|
||
hasVoted = false;
|
||
updateUI();
|
||
}
|
||
});
|
||
|
||
socket.on('roundResults', (data) => {
|
||
if (gameState) {
|
||
gameState.players = data.players;
|
||
gameState.yarnStory = data.story;
|
||
showRoundResults(data.winner);
|
||
}
|
||
});
|
||
|
||
socket.on('newRound', (state) => {
|
||
gameState = state;
|
||
hasSubmitted = false;
|
||
hasVoted = false;
|
||
updateUI();
|
||
appendSystemMessage('New round started!');
|
||
});
|
||
|
||
socket.on('gameOver', (data) => {
|
||
if (gameState) {
|
||
gameState.currentPhase = 'gameOver';
|
||
gameState.players = data.players;
|
||
gameState.yarnStory = data.story;
|
||
updateUI();
|
||
showGameOver(data.winner);
|
||
}
|
||
});
|
||
|
||
socket.on('gameReset', (state) => {
|
||
gameState = state;
|
||
hasSubmitted = false;
|
||
hasVoted = false;
|
||
updateUI();
|
||
appendSystemMessage('Game has been reset. Waiting for host to start...');
|
||
});
|
||
|
||
socket.on('newChatMessage', (msg) => {
|
||
appendChatMessage(msg);
|
||
});
|
||
|
||
socket.on('error', (data) => {
|
||
alert(data.message);
|
||
});
|
||
|
||
// === UI Functions ===
|
||
|
||
function updateUI() {
|
||
updateStory();
|
||
updateGameBar();
|
||
updatePlayersList();
|
||
}
|
||
|
||
function updateStory() {
|
||
const mainText = document.getElementById('main-text');
|
||
|
||
if (!gameState || !gameState.inProgress && gameState.yarnStory.length === 0) {
|
||
mainText.innerHTML = '<em>Waiting for the game to begin...</em>';
|
||
return;
|
||
}
|
||
|
||
mainText.innerHTML = gameState.yarnStory.map(segment =>
|
||
`<span class="story-segment" data-player="${escapeHtml(segment.player)}">${escapeHtml(segment.str)} </span>`
|
||
).join('');
|
||
}
|
||
|
||
function updateGameBar() {
|
||
const gameBar = document.getElementById('game-bar');
|
||
|
||
// If player hasn't set name yet
|
||
if (!playerName) {
|
||
gameBar.innerHTML = `
|
||
<span>Enter your name to join:</span>
|
||
<input type="text" id="name-input" placeholder="Your name..." maxlength="20">
|
||
<button id="set-name-btn">Join Game</button>
|
||
`;
|
||
|
||
document.getElementById('set-name-btn').onclick = setPlayerName;
|
||
document.getElementById('name-input').addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') setPlayerName();
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!gameState) return;
|
||
|
||
const isHost = gameState.gameHost === playerUUID;
|
||
|
||
switch (gameState.currentPhase) {
|
||
case 'lobby':
|
||
if (isHost) {
|
||
gameBar.innerHTML = `
|
||
<div class="setting-group">
|
||
<label>Score Limit:</label>
|
||
<input type="number" id="score-limit" value="${gameState.gameSettings.scoreLimit}" min="10" max="100">
|
||
</div>
|
||
<div class="setting-group">
|
||
<label>Time per Round (sec):</label>
|
||
<input type="number" id="time-limit" value="${gameState.gameSettings.timeLimit}" min="15" max="180">
|
||
</div>
|
||
<button id="start-game-btn">Start Game</button>
|
||
`;
|
||
|
||
document.getElementById('start-game-btn').onclick = startGame;
|
||
} else {
|
||
gameBar.innerHTML = `
|
||
<span class="waiting-message">Waiting for host to start the game...</span>
|
||
<span>Score Limit: ${gameState.gameSettings.scoreLimit}</span>
|
||
<span>Time Limit: ${gameState.gameSettings.timeLimit}s</span>
|
||
`;
|
||
}
|
||
break;
|
||
|
||
case 'writing':
|
||
if (hasSubmitted) {
|
||
gameBar.innerHTML = `
|
||
<span id="timer">${gameState.timeRemaining}</span>
|
||
<span class="waiting-message">Entry submitted! Waiting for other players...</span>
|
||
`;
|
||
} else {
|
||
gameBar.innerHTML = `
|
||
<span id="timer">${gameState.timeRemaining}</span>
|
||
<input type="text" id="entry-input" placeholder="Continue the story..." maxlength="200">
|
||
<button id="submit-entry-btn">Submit</button>
|
||
`;
|
||
|
||
document.getElementById('submit-entry-btn').onclick = submitEntry;
|
||
document.getElementById('entry-input').addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') submitEntry();
|
||
});
|
||
document.getElementById('entry-input').focus();
|
||
}
|
||
updateTimer();
|
||
break;
|
||
|
||
case 'voting':
|
||
if (hasVoted) {
|
||
gameBar.innerHTML = `
|
||
<span id="timer">${gameState.timeRemaining}</span>
|
||
<span class="waiting-message">Vote cast! Waiting for other players...</span>
|
||
`;
|
||
} else {
|
||
const entries = gameState.round_data.filter(e => e.entry_text);
|
||
|
||
if (entries.length === 0) {
|
||
gameBar.innerHTML = `
|
||
<span class="waiting-message">No entries to vote on...</span>
|
||
`;
|
||
} else {
|
||
gameBar.innerHTML = `
|
||
<span id="timer">${gameState.timeRemaining}</span>
|
||
<div id="voting-container">
|
||
${entries.map(e => `
|
||
<div class="vote-entry">
|
||
<span class="vote-entry-text">"${escapeHtml(e.entry_text)}"</span>
|
||
<button class="vote-btn" data-index="${e.index}">Vote</button>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
|
||
document.querySelectorAll('.vote-btn').forEach(btn => {
|
||
btn.onclick = () => submitVote(parseInt(btn.dataset.index));
|
||
});
|
||
}
|
||
}
|
||
updateTimer();
|
||
break;
|
||
|
||
case 'gameOver':
|
||
if (isHost) {
|
||
gameBar.innerHTML = `
|
||
<span class="game-over-message">Game Over!</span>
|
||
<button id="play-again-btn">Play Again</button>
|
||
`;
|
||
|
||
document.getElementById('play-again-btn').onclick = () => {
|
||
socket.emit('playAgain');
|
||
};
|
||
} else {
|
||
gameBar.innerHTML = `
|
||
<span class="game-over-message">Game Over! Waiting for host...</span>
|
||
`;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
function updateTimer() {
|
||
const timer = document.getElementById('timer');
|
||
if (timer && gameState) {
|
||
timer.textContent = gameState.timeRemaining;
|
||
if (gameState.timeRemaining <= 10) {
|
||
timer.classList.add('warning');
|
||
} else {
|
||
timer.classList.remove('warning');
|
||
}
|
||
}
|
||
}
|
||
|
||
function updatePlayersList() {
|
||
const playersList = document.getElementById('players-list');
|
||
if (!gameState) {
|
||
playersList.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
playersList.innerHTML = gameState.players.map(p => {
|
||
let statusClass = '';
|
||
if (gameState.currentPhase === 'writing') {
|
||
statusClass = p.hasSubmitted ? 'submitted' : 'pending';
|
||
} else if (gameState.currentPhase === 'voting') {
|
||
statusClass = p.hasVoted ? 'submitted' : 'pending';
|
||
} else {
|
||
statusClass = 'submitted';
|
||
}
|
||
|
||
const hostClass = p.player_id === gameState.gameHost ? 'host' : '';
|
||
|
||
return `
|
||
<div class="player-row ${statusClass} ${hostClass}">
|
||
<span class="player-name">${escapeHtml(p.name)}${p.player_id === playerUUID ? ' (You)' : ''}</span>
|
||
<span class="player-score">${p.score}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function showRoundResults(winner) {
|
||
const gameBar = document.getElementById('game-bar');
|
||
if (winner && winner.entry_text) {
|
||
const winnerPlayer = gameState.players.find(p => p.player_id === winner.player_id);
|
||
const winnerName = winnerPlayer ? winnerPlayer.name : 'Unknown';
|
||
|
||
gameBar.innerHTML = `
|
||
<div class="results-display">
|
||
<span class="round-winner">🏆 "${escapeHtml(winner.entry_text)}" by ${escapeHtml(winnerName)} wins!</span>
|
||
<br><span class="waiting-message">Next round starting soon...</span>
|
||
</div>
|
||
`;
|
||
} else {
|
||
gameBar.innerHTML = `
|
||
<div class="results-display">
|
||
<span class="waiting-message">No winner this round. Next round starting soon...</span>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
updateStory();
|
||
updatePlayersList();
|
||
}
|
||
|
||
function showGameOver(winner) {
|
||
const mainText = document.getElementById('main-text');
|
||
const storyHtml = gameState.yarnStory.map(segment =>
|
||
`<span class="story-segment" data-player="${escapeHtml(segment.player)}">${escapeHtml(segment.str)} </span>`
|
||
).join('');
|
||
|
||
mainText.innerHTML = `
|
||
<div style="text-align: center; margin-bottom: 1rem;">
|
||
<span class="game-over-message">🎉 ${escapeHtml(winner.name)} is victorious with ${winner.score} points! 🎉</span>
|
||
</div>
|
||
<hr style="border-color: var(--accent-purple); margin: 1rem 0;">
|
||
<strong>The Final Story:</strong><br><br>
|
||
${storyHtml}
|
||
`;
|
||
|
||
appendSystemMessage(`${winner.name} wins the game with ${winner.score} points!`);
|
||
}
|
||
|
||
// === Action Functions ===
|
||
|
||
function setPlayerName() {
|
||
const input = document.getElementById('name-input');
|
||
const name = input.value.trim();
|
||
|
||
if (name.length < 1 || name.length > 20) {
|
||
alert('Please enter a name (1-20 characters)');
|
||
return;
|
||
}
|
||
|
||
socket.emit('setName', { name });
|
||
}
|
||
|
||
function startGame() {
|
||
const scoreLimit = parseInt(document.getElementById('score-limit').value) || 100;
|
||
const timeLimit = parseInt(document.getElementById('time-limit').value) || 60;
|
||
|
||
socket.emit('startGame', {
|
||
scoreLimit: Math.max(10, Math.min(9999, scoreLimit)),
|
||
timeLimit: Math.max(15, Math.min(600, timeLimit))
|
||
});
|
||
}
|
||
|
||
function submitEntry() {
|
||
const input = document.getElementById('entry-input');
|
||
const entry = input.value.trim();
|
||
|
||
socket.emit('submitEntry', { entry });
|
||
hasSubmitted = true;
|
||
updateGameBar();
|
||
}
|
||
|
||
function submitVote(entryIndex) {
|
||
socket.emit('submitVote', { entryIndex });
|
||
hasVoted = true;
|
||
updateGameBar();
|
||
}
|
||
|
||
// === Chat Functions ===
|
||
|
||
document.getElementById('chat-input').addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const msg = this.value.trim();
|
||
if (msg && playerName) {
|
||
socket.emit('chatMessage', { msg });
|
||
this.value = '';
|
||
}
|
||
}
|
||
});
|
||
|
||
function appendChatMessage(msg) {
|
||
const chatMessages = document.getElementById('chat-messages');
|
||
const div = document.createElement('div');
|
||
div.className = 'chat-message';
|
||
div.innerHTML = `<strong>${escapeHtml(msg.name)}></strong> ${escapeHtml(msg.msg)}`;
|
||
chatMessages.appendChild(div);
|
||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||
}
|
||
|
||
function appendSystemMessage(msg) {
|
||
const chatMessages = document.getElementById('chat-messages');
|
||
const div = document.createElement('div');
|
||
div.className = 'chat-message system-message';
|
||
div.textContent = `* ${msg}`;
|
||
chatMessages.appendChild(div);
|
||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||
}
|
||
|
||
// === Utility Functions ===
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function generateUUID() {
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
const r = Math.random() * 16 | 0;
|
||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||
return v.toString(16);
|
||
});
|
||
}
|
||
|
||
function setCookie(name, value, days) {
|
||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
||
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/`;
|
||
}
|
||
|
||
function getCookie(name) {
|
||
const value = `; ${document.cookie}`;
|
||
const parts = value.split(`; ${name}=`);
|
||
if (parts.length === 2) {
|
||
return decodeURIComponent(parts.pop().split(';').shift());
|
||
}
|
||
return null;
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|