try to fix this spaghetti of a code
This commit is contained in:
@@ -54,3 +54,15 @@ test('login fits a narrow viewport and protected pages redirect',async({page})=>
|
|||||||
expect(await page.evaluate(()=>document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
expect(await page.evaluate(()=>document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||||
await expect(page.getByLabel('Username',{exact:true})).toBeVisible();
|
await expect(page.getByLabel('Username',{exact:true})).toBeVisible();
|
||||||
});
|
});
|
||||||
|
test('secret shortcut displays every staged cue', async ({page}) => {
|
||||||
|
await register(page, `cue-${Date.now()}`);
|
||||||
|
await page.keyboard.press('Alt');
|
||||||
|
await page.keyboard.press('j');
|
||||||
|
await expect(page.locator('#secret-cue')).toHaveText('J');
|
||||||
|
await page.keyboard.press('f');
|
||||||
|
await expect(page.locator('#secret-cue')).toHaveText('F');
|
||||||
|
await expect(page.locator('#secret-cue')).toHaveClass(/secret-cue-strong/);
|
||||||
|
await page.keyboard.press('k');
|
||||||
|
await expect(page.locator('#secret-cue')).toHaveText('K');
|
||||||
|
await expect(page.locator('#secret-flash')).toHaveClass(/secret-flash-active/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ function createHttpApp(repository, sessions, lobbies, config) {
|
|||||||
const users = await repository.users(); const index = users.findIndex(entry => entry.username === user.username);
|
const users = await repository.users(); const index = users.findIndex(entry => entry.username === user.username);
|
||||||
res.json({leaderboard: users.slice(0, 10), self: {...users[index], position: index + 1}});
|
res.json({leaderboard: users.slice(0, 10), self: {...users[index], position: index + 1}});
|
||||||
}));
|
}));
|
||||||
|
app.post('/easter-egg', authorized(async (req, res, user) => {
|
||||||
|
const code = req.body?.code;
|
||||||
|
if (!['ILIKEMONEY', 'FRETUX', 'KRISHD', 'LOSERNOOB'].includes(code)) throw new AppError('INVALID_EASTER_EGG', 'Unknown terminal command.', 400);
|
||||||
|
res.json(await repository.redeemEasterEgg(user.username, code));
|
||||||
|
}));
|
||||||
for (const [route, type] of [['/lobby/create', 'lobby:create'], ['/lobby/join', 'lobby:join'], ['/lobby/leave', 'lobby:leave'], ['/poker/start', 'poker:start'], ['/poker/action', 'poker:action']]) {
|
for (const [route, type] of [['/lobby/create', 'lobby:create'], ['/lobby/join', 'lobby:join'], ['/lobby/leave', 'lobby:leave'], ['/poker/start', 'poker:start'], ['/poker/action', 'poker:action']]) {
|
||||||
app.post(route, authorized(async (req, res, user) => {
|
app.post(route, authorized(async (req, res, user) => {
|
||||||
const message = {...req.body, type};
|
const message = {...req.body, type};
|
||||||
|
|||||||
+43
-7
@@ -29,11 +29,11 @@ async function createRepository(filename, {beforeCommit = async () => {}} = {})
|
|||||||
}
|
}
|
||||||
await transaction(async tx => {
|
await transaction(async tx => {
|
||||||
const {user_version: version} = await tx.get('PRAGMA user_version');
|
const {user_version: version} = await tx.get('PRAGMA user_version');
|
||||||
if (version > 1) throw new Error('Database schema is newer than this application');
|
if (version > 4) throw new Error('Database schema is newer than this application');
|
||||||
if (version === 1) return;
|
if (version === 0) {
|
||||||
const old = await tx.get("SELECT name FROM sqlite_master WHERE type='table' AND name='users'");
|
const old = await tx.get("SELECT name FROM sqlite_master WHERE type='table' AND name='users'");
|
||||||
if (old) await tx.exec('ALTER TABLE users RENAME TO legacy_users');
|
if (old) await tx.exec('ALTER TABLE users RENAME TO legacy_users');
|
||||||
await tx.exec(`
|
await tx.exec(`
|
||||||
CREATE TABLE users (
|
CREATE TABLE users (
|
||||||
username TEXT PRIMARY KEY COLLATE NOCASE,
|
username TEXT PRIMARY KEY COLLATE NOCASE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
@@ -44,12 +44,28 @@ async function createRepository(filename, {beforeCommit = async () => {}} = {})
|
|||||||
CREATE TABLE commands (id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, result TEXT NOT NULL, created_at INTEGER NOT NULL);
|
CREATE TABLE commands (id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, result TEXT NOT NULL, created_at INTEGER NOT NULL);
|
||||||
CREATE TABLE ledger (id INTEGER PRIMARY KEY, command_id TEXT NOT NULL, username TEXT NOT NULL REFERENCES users(username), delta INTEGER NOT NULL, reason TEXT NOT NULL);
|
CREATE TABLE ledger (id INTEGER PRIMARY KEY, command_id TEXT NOT NULL, username TEXT NOT NULL REFERENCES users(username), delta INTEGER NOT NULL, reason TEXT NOT NULL);
|
||||||
CREATE TABLE migrations (id TEXT PRIMARY KEY, applied_at INTEGER NOT NULL);
|
CREATE TABLE migrations (id TEXT PRIMARY KEY, applied_at INTEGER NOT NULL);
|
||||||
|
CREATE TABLE easter_eggs (username TEXT NOT NULL REFERENCES users(username), code TEXT NOT NULL, used_at INTEGER NOT NULL, PRIMARY KEY(username, code));
|
||||||
`);
|
`);
|
||||||
if (old) {
|
if (old) {
|
||||||
// Fails and rolls back rather than silently rounding or losing invalid balances.
|
// Fails and rolls back rather than silently rounding or losing invalid balances.
|
||||||
await tx.exec(`INSERT INTO users(username, password_hash, money) SELECT username, password_hash, money FROM legacy_users; INSERT INTO ledger(command_id,username,delta,reason) SELECT 'schema-v1:' || username, username, money, 'Opening balance migration' FROM users; DROP TABLE legacy_users;`);
|
await tx.exec(`INSERT INTO users(username, password_hash, money) SELECT username, password_hash, money FROM legacy_users; INSERT INTO ledger(command_id,username,delta,reason) SELECT 'schema-v1:' || username, username, money, 'Opening balance migration' FROM users; DROP TABLE legacy_users;`);
|
||||||
|
}
|
||||||
|
await tx.exec('PRAGMA user_version = 4');
|
||||||
|
} else if (version === 1) {
|
||||||
|
await tx.exec('CREATE TABLE easter_eggs (username TEXT NOT NULL REFERENCES users(username), code TEXT NOT NULL, used_at INTEGER NOT NULL, PRIMARY KEY(username, code)); PRAGMA user_version = 4');
|
||||||
|
} else if (version === 2) {
|
||||||
|
const broken = await tx.all("SELECT username, delta FROM ledger WHERE reason = 'Easter egg: KRISHD' AND delta < 0");
|
||||||
|
for (const entry of broken) {
|
||||||
|
const letters = [...entry.username].filter(character => /[a-z]/i.test(character)).length;
|
||||||
|
const correction = -entry.delta * letters;
|
||||||
|
if (!Number.isSafeInteger(correction)) throw new Error('Invalid KRISHD repair amount');
|
||||||
|
await tx.run('UPDATE users SET money = money + ? WHERE username = ?', correction, entry.username);
|
||||||
|
await tx.run('INSERT INTO ledger(command_id,username,delta,reason) VALUES(?,?,?,?)', `repair:KRISHD:${entry.username}`, entry.username, correction, 'KRISHD multiplier repair');
|
||||||
|
}
|
||||||
|
await tx.exec("UPDATE easter_eggs SET code = 'FRETUX' WHERE code = 'FRETUKS'; PRAGMA user_version = 4");
|
||||||
|
} else if (version === 3) {
|
||||||
|
await tx.exec("UPDATE easter_eggs SET code = 'FRETUX' WHERE code = 'FRETUKS'; PRAGMA user_version = 4");
|
||||||
}
|
}
|
||||||
await tx.exec('PRAGMA user_version = 1');
|
|
||||||
});
|
});
|
||||||
} catch (error) { await release(); await db.close(); throw error; }
|
} catch (error) { await release(); await db.close(); throw error; }
|
||||||
const read = work => queue.run(['database'], () => work(db));
|
const read = work => queue.run(['database'], () => work(db));
|
||||||
@@ -65,6 +81,26 @@ async function createRepository(filename, {beforeCommit = async () => {}} = {})
|
|||||||
catch (error) { if (error.code === 'SQLITE_CONSTRAINT') throw new AppError('USERNAME_EXISTS', 'Username already exists', 409); throw error; }
|
catch (error) { if (error.code === 'SQLITE_CONSTRAINT') throw new AppError('USERNAME_EXISTS', 'Username already exists', 409); throw error; }
|
||||||
},
|
},
|
||||||
setSession: (username, sessionHash) => transaction(tx => tx.run('UPDATE users SET session_hash = ? WHERE username = ?', sessionHash, username)),
|
setSession: (username, sessionHash) => transaction(tx => tx.run('UPDATE users SET session_hash = ? WHERE username = ?', sessionHash, username)),
|
||||||
|
async redeemEasterEgg(username, code) {
|
||||||
|
const repeatable = code === 'LOSERNOOB';
|
||||||
|
return transaction(async tx => {
|
||||||
|
const user = await tx.get('SELECT money FROM users WHERE username = ?', username);
|
||||||
|
if (!user) throw new AppError('USER_NOT_FOUND', 'Account no longer exists', 404);
|
||||||
|
if (!repeatable && await tx.get('SELECT 1 FROM easter_eggs WHERE username = ? AND code = ?', username, code)) {
|
||||||
|
throw new AppError('EASTER_EGG_USED', 'This terminal command was already used.', 409);
|
||||||
|
}
|
||||||
|
const multiplier = code === 'FRETUX' ? 2 : code === 'KRISHD' ? [...username].filter(character => /[a-z]/i.test(character)).length : 1;
|
||||||
|
const next = code === 'ILIKEMONEY' ? user.money + 1000 : user.money * multiplier;
|
||||||
|
if (!Number.isSafeInteger(next) || next < 0) throw new AppError('BALANCE_LIMIT', 'This command would exceed the chip limit.', 409);
|
||||||
|
const delta = next - user.money;
|
||||||
|
if (!repeatable) await tx.run('INSERT INTO easter_eggs(username,code,used_at) VALUES(?,?,?)', username, code, Date.now());
|
||||||
|
if (delta) {
|
||||||
|
await tx.run('UPDATE users SET money = ? WHERE username = ?', next, username);
|
||||||
|
await tx.run('INSERT INTO ledger(command_id,username,delta,reason) VALUES(?,?,?,?)', `easter:${code}:${username}`, username, delta, `Easter egg: ${code}`);
|
||||||
|
}
|
||||||
|
return {balance: next, effect: repeatable ? 'matrix' : 'credits'};
|
||||||
|
});
|
||||||
|
},
|
||||||
async recover() {
|
async recover() {
|
||||||
return transaction(async tx => {
|
return transaction(async tx => {
|
||||||
const rows = await tx.all('SELECT id,state FROM lobbies');
|
const rows = await tx.all('SELECT id,state FROM lobbies');
|
||||||
|
|||||||
@@ -2,7 +2,84 @@
|
|||||||
const byId = id => document.getElementById(id);
|
const byId = id => document.getElementById(id);
|
||||||
const client = new RealtimeClient();
|
const client = new RealtimeClient();
|
||||||
let username; let lobbyId;
|
let username; let lobbyId;
|
||||||
|
const easterTitle = byId('easter-title');
|
||||||
|
let easterStage = 0;
|
||||||
|
let easterSequenceTimer = null;
|
||||||
|
let easterOpen = false;
|
||||||
|
let easterPending = false;
|
||||||
|
const secretCue = document.createElement('div');
|
||||||
|
secretCue.id = 'secret-cue';
|
||||||
|
secretCue.className = 'secret-cue';
|
||||||
|
secretCue.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(secretCue);
|
||||||
|
const secretFlash = document.createElement('div');
|
||||||
|
secretFlash.id = 'secret-flash';
|
||||||
|
secretFlash.className = 'secret-flash';
|
||||||
|
secretFlash.setAttribute('aria-hidden', 'true');
|
||||||
|
document.body.appendChild(secretFlash);
|
||||||
const error = message => {byId('join-error').textContent = message; byId('join-error').classList.toggle('hidden', !message);};
|
const error = message => {byId('join-error').textContent = message; byId('join-error').classList.toggle('hidden', !message);};
|
||||||
|
const matrixColor = () => `hsl(${Math.floor(Math.random() * 360)} 100% 62%)`;
|
||||||
|
const unlockEasterTitle = () => {
|
||||||
|
easterOpen = true;
|
||||||
|
easterTitle.contentEditable = 'true';
|
||||||
|
easterTitle.classList.add('easter-open');
|
||||||
|
easterTitle.focus();
|
||||||
|
document.getSelection()?.selectAllChildren(easterTitle);
|
||||||
|
};
|
||||||
|
const cueSecret = (letter, strength = 'soft') => {
|
||||||
|
document.body.classList.remove('secret-shake', 'secret-shake-strong');
|
||||||
|
void document.body.offsetWidth;
|
||||||
|
document.body.classList.add(strength === 'soft' ? 'secret-shake' : 'secret-shake-strong');
|
||||||
|
if (!letter) return;
|
||||||
|
secretCue.textContent = letter;
|
||||||
|
secretCue.className = 'secret-cue';
|
||||||
|
void secretCue.offsetWidth;
|
||||||
|
secretCue.className = `secret-cue secret-cue-${strength}`;
|
||||||
|
};
|
||||||
|
const flashSecret = () => {
|
||||||
|
cueSecret('K', 'strong');
|
||||||
|
document.body.classList.remove('secret-shake-strong');
|
||||||
|
void document.body.offsetWidth;
|
||||||
|
document.body.classList.add('secret-shake-strong');
|
||||||
|
secretFlash.classList.remove('secret-flash-active');
|
||||||
|
void secretFlash.offsetWidth;
|
||||||
|
secretFlash.classList.add('secret-flash-active');
|
||||||
|
};
|
||||||
|
const armEasterSequence = (resetStage = true) => {
|
||||||
|
if (resetStage) easterStage = 0;
|
||||||
|
clearTimeout(easterSequenceTimer);
|
||||||
|
easterSequenceTimer = setTimeout(() => {easterStage = 0;}, 10000);
|
||||||
|
};
|
||||||
|
const redeemEasterTitle = async () => {
|
||||||
|
if (easterPending) return;
|
||||||
|
const code = easterTitle.textContent.trim().toUpperCase();
|
||||||
|
easterTitle.textContent = 'READY TO DEAL';
|
||||||
|
easterTitle.contentEditable = 'false';
|
||||||
|
easterTitle.classList.remove('easter-open');
|
||||||
|
easterOpen = false;
|
||||||
|
if (!['ILIKEMONEY', 'FRETUX', 'KRISHD', 'LOSERNOOB'].includes(code)) return;
|
||||||
|
easterPending = true;
|
||||||
|
try {
|
||||||
|
const result = await apiRequest('/easter-egg', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({code})});
|
||||||
|
if (result.effect === 'matrix') {
|
||||||
|
document.documentElement.style.setProperty('--matrix-rain', matrixColor());
|
||||||
|
error('Matrix stream randomized.');
|
||||||
|
} else {
|
||||||
|
byId('balance').textContent = result.balance;
|
||||||
|
error('Terminal command accepted.');
|
||||||
|
}
|
||||||
|
} catch (problem) {error(problem.message);}
|
||||||
|
finally {easterPending = false;}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', event => {
|
||||||
|
if (easterOpen || event.repeat) return;
|
||||||
|
if (event.code === 'AltLeft' || event.code === 'AltRight') {armEasterSequence(); cueSecret('', 'soft'); return;}
|
||||||
|
if (easterStage === 0 && event.code === 'KeyJ') {easterStage = 1; armEasterSequence(false); cueSecret('J', 'medium'); return;}
|
||||||
|
if (easterStage === 1 && event.code === 'KeyF') {easterStage = 2; armEasterSequence(false); cueSecret('F', 'strong'); return;}
|
||||||
|
if (easterStage === 2 && event.code === 'KeyK') {event.preventDefault(); clearTimeout(easterSequenceTimer); flashSecret(); unlockEasterTitle(); easterStage = 0; return;}
|
||||||
|
if (easterStage) armEasterSequence();
|
||||||
|
});
|
||||||
|
easterTitle.addEventListener('keydown', event => { if (event.key === 'Enter') {event.preventDefault(); redeemEasterTitle();} });
|
||||||
const send = async message => {try {error(''); await client.send(message);} catch (problem) {error(problem.message);}};
|
const send = async message => {try {error(''); await client.send(message);} catch (problem) {error(problem.message);}};
|
||||||
const hideLobby = () => {
|
const hideLobby = () => {
|
||||||
lobbyId = null; localStorage.removeItem('activeLobbyId'); byId('lobby-room').classList.add('hidden');
|
lobbyId = null; localStorage.removeItem('activeLobbyId'); byId('lobby-room').classList.add('hidden');
|
||||||
|
|||||||
+20
-13
@@ -10,23 +10,28 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<canvas id="matrix-canvas"></canvas>
|
<canvas id="matrix-canvas"></canvas>
|
||||||
<div class="leaderboard">
|
<header class="app-header">
|
||||||
<h3>🏆 High Rollers</h3>
|
<a class="wordmark" href="/">GAMBLING <span>API</span></a>
|
||||||
|
<div class="header-status"><span class="status-dot"></span> TABLE NETWORK ONLINE</div>
|
||||||
|
<button class="logout" id="logout">Logout</button>
|
||||||
|
</header>
|
||||||
|
<main class="menu-shell">
|
||||||
|
<aside class="leaderboard">
|
||||||
|
<p class="panel-label">Leaderboard</p>
|
||||||
|
<h3>HIGH ROLLERS</h3>
|
||||||
<ul id="leaderboard-list">
|
<ul id="leaderboard-list">
|
||||||
<li>Loading...</li>
|
<li>Loading...</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div id="your-rank" class="your-rank"></div>
|
<div id="your-rank" class="your-rank"></div>
|
||||||
</div>
|
</aside>
|
||||||
|
<section class="command-center">
|
||||||
|
<p class="panel-label">Operator console</p>
|
||||||
<button class="logout" id="logout">Logout</button>
|
<h1><span id="easter-title" spellcheck="false">READY TO DEAL</span><span class="cursor">_</span></h1>
|
||||||
<div class="balance">
|
<div class="balance"><span>AVAILABLE CREDITS</span><strong>$<span id="balance">0</span></strong></div>
|
||||||
Balance: $<span id="balance">0</span>
|
<div class="lobby-buttons">
|
||||||
</div>
|
<button id="host">Host Lobby</button>
|
||||||
<div class="lobby-buttons">
|
<button id="join" class="secondary-action">Join Lobby</button>
|
||||||
<button id="host">Host Lobby</button>
|
</div>
|
||||||
<button id="join">Join Lobby</button>
|
|
||||||
</div>
|
|
||||||
<div id="host-form" class="host-form hidden">
|
<div id="host-form" class="host-form hidden">
|
||||||
<div class="lobby-visibility" aria-label="Lobby visibility">
|
<div class="lobby-visibility" aria-label="Lobby visibility">
|
||||||
<label>
|
<label>
|
||||||
@@ -65,6 +70,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="start-game" class="start-game hidden" disabled>Start Game</button>
|
<button id="start-game" class="start-game hidden" disabled>Start Game</button>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script src="protocol.js"></script>
|
<script src="protocol.js"></script>
|
||||||
<script src="client.js"></script>
|
<script src="client.js"></script>
|
||||||
|
|||||||
+25
-13
@@ -9,19 +9,31 @@
|
|||||||
<link rel="icon" href="gambling.png" type="image/x-icon">
|
<link rel="icon" href="gambling.png" type="image/x-icon">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Login / Register</h1>
|
<main class="auth-shell">
|
||||||
<form method="post" action="/login">
|
<section class="auth-intro" aria-label="Gambling API introduction">
|
||||||
<label for="username">Username</label>
|
<p class="eyebrow">secure terminal // v1.0</p>
|
||||||
<input id="username" type="text" name="username" placeholder="Username" autocomplete="username" maxlength="40" required>
|
<h1>GAMBLING<br><span>API</span></h1>
|
||||||
<label for="password">Password</label>
|
<p class="intro-copy">Multiplayer poker, virtual chips, and a live table connection.</p>
|
||||||
<input id="password" type="password" name="password" placeholder="Password" autocomplete="current-password" required aria-describedby="password-hint">
|
<ul class="intro-status"><li>● ENCRYPTED SESSION</li><li>● LIVE TABLES</li><li>● VIRTUAL CREDITS</li></ul>
|
||||||
<small id="password-hint">Passwords support up to 72 UTF-8 bytes.</small>
|
</section>
|
||||||
<div class="auth-actions">
|
<section class="auth-panel" aria-labelledby="auth-title">
|
||||||
<button type="submit" id="login-button">Login</button>
|
<div class="panel-chrome"><span>AUTHENTICATE</span><span>● ● ●</span></div>
|
||||||
<button type="button" id="register-button">Register</button>
|
<h2 id="auth-title">Access your account</h2>
|
||||||
</div>
|
<p class="panel-copy">Sign in to return to your table, or register a new operator account.</p>
|
||||||
<p id="auth-error" role="alert" aria-live="polite" class="auth-error hidden"></p>
|
<form method="post" action="/login">
|
||||||
</form>
|
<label for="username">Username</label>
|
||||||
|
<input id="username" type="text" name="username" placeholder="operator-name" autocomplete="username" maxlength="40" required>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input id="password" type="password" name="password" placeholder="••••••••••••" autocomplete="current-password" required aria-describedby="password-hint">
|
||||||
|
<small id="password-hint">Password limit: 72 UTF-8 bytes.</small>
|
||||||
|
<div class="auth-actions">
|
||||||
|
<button type="submit" id="login-button">Sign in</button>
|
||||||
|
<button type="button" id="register-button" class="secondary-action">Register</button>
|
||||||
|
</div>
|
||||||
|
<p id="auth-error" role="alert" aria-live="polite" class="auth-error hidden"></p>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script src="frontend_login.js"></script>
|
<script src="frontend_login.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ function resizeCanvas() {
|
|||||||
function drawMatrix() {
|
function drawMatrix() {
|
||||||
ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
|
ctx.fillStyle = "rgba(0, 0, 0, 0.2)";
|
||||||
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
|
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
|
||||||
ctx.fillStyle = "#39FF14";
|
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("--matrix-rain").trim() || "#39FF14";
|
||||||
ctx.font = fontSize + "px monospace";
|
ctx.font = fontSize + "px monospace";
|
||||||
|
|
||||||
for (let i = 0; i < drops.length; i++) {
|
for (let i = 0; i < drops.length; i++) {
|
||||||
|
|||||||
+6
-336
@@ -1,336 +1,6 @@
|
|||||||
body {
|
:root { --matrix-rain:#55f570; } body { min-height:100vh; margin:0; padding:0 24px; color:#e6ffe9; background:#040604; font-family:ui-monospace,SFMono-Regular,Consolas,monospace; } #matrix-canvas { position:fixed; inset:0; z-index:-1; width:100%; height:100%; opacity:.15; pointer-events:none; }.app-header { width:min(1120px,100%); min-height:72px; margin:auto; display:flex; align-items:center; justify-content:space-between; gap:18px; border-bottom:1px solid #1c5727; }.wordmark { color:#f1fff2; text-decoration:none; font-weight:800; letter-spacing:-.06em; }.wordmark span { color:#56ed70; }.header-status { color:#7fc889; font-size:11px; letter-spacing:.06em; }.status-dot { display:inline-block; width:7px; height:7px; margin-right:5px; border-radius:50%; background:#54f570; box-shadow:0 0 9px #54f570; }.logout,.leave-button { border:1px solid #477d4f; background:transparent; color:#a3e9ac; padding:8px 11px; font:12px inherit; cursor:pointer; }
|
||||||
background: radial-gradient(circle at top, #111, #000);
|
.menu-shell { width:min(1120px,100%); min-height:calc(100vh - 72px); margin:auto; display:grid; grid-template-columns:260px minmax(0,1fr); gap:clamp(30px,9vw,120px); align-items:center; }.leaderboard { background:#080d08; border:1px solid #2b7036; padding:20px; box-shadow:0 0 22px rgba(35,203,62,.1); }.panel-label { margin:0; color:#63c870; font-size:10px; letter-spacing:.13em; text-transform:uppercase; }.leaderboard h3 { margin:8px 0 16px; color:#8aff9a; font-size:17px; }.leaderboard ul { list-style:none; padding:0; margin:0; border-top:1px solid #1b4822; }.leaderboard li { padding:10px 0; border-bottom:1px solid #1b4822; color:#c4e6c8; font-size:13px; }.leaderboard li.you { color:#ffdc72; }.your-rank { margin-top:14px; color:#72d680; font-size:12px; }
|
||||||
color: #0f0;
|
.command-center { max-width:540px; }.command-center h1 { margin:12px 0 28px; color:#effff1; font-size:clamp(36px,6vw,62px); letter-spacing:-.08em; }.easter-open { color:#8aff9c; outline:1px dashed #55f570; outline-offset:8px; }.cursor { color:#55f570; animation:blink 1s step-end infinite; } .balance { display:grid; gap:6px; padding:17px 0; border-top:1px solid #326d3b; border-bottom:1px solid #326d3b; }.balance>span { color:#80be87; font-size:11px; letter-spacing:.12em; }.balance strong { color:#70f887; font-size:34px; letter-spacing:-.05em; }.lobby-buttons { display:flex; gap:10px; margin:24px 0; }
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
.secret-cue { position:fixed; z-index:21; top:50%; left:50%; color:#8aff9c; font:700 0/1 ui-monospace,SFMono-Regular,Consolas,monospace; text-shadow:0 0 24px #55f570; transform:translate(-50%,-50%); pointer-events:none; }.secret-cue-medium { font-size:clamp(90px,20vw,200px); animation:secret-letter 1s ease-out forwards; }.secret-cue-strong { font-size:clamp(150px,31vw,320px); animation:secret-letter 1s ease-out forwards; }.secret-flash { position:fixed; z-index:20; inset:0; pointer-events:none; opacity:0; background:rgba(122,255,145,.5); }.secret-flash-active { animation:secret-flash .28s ease-out forwards; }.secret-shake { animation:secret-shake .18s linear; }.secret-shake-strong { animation:secret-shake-big .38s linear; } @keyframes secret-letter { 0% { opacity:0; transform:translate(-50%,-50%) scale(.6); } 18% { opacity:1; } 100% { opacity:0; transform:translate(-50%,-50%) scale(1.1); } } @keyframes secret-flash { to { opacity:0; } } @keyframes secret-shake { 25% { transform:translateX(2px); } 50% { transform:translateX(-3px); } 75% { transform:translateX(2px); } } @keyframes secret-shake-big { 20% { transform:translateX(8px) rotate(.2deg); } 45% { transform:translateX(-10px) rotate(-.2deg); } 70% { transform:translateX(6px); } } @media (prefers-reduced-motion:reduce) { .secret-cue,.secret-shake,.secret-shake-strong,.secret-flash-active { animation:none; } }
|
||||||
margin: 0;
|
button { min-height:44px; border:1px solid #52ee6b; background:#58ed70; color:#041005; padding:10px 16px; font:700 13px ui-monospace,SFMono-Regular,Consolas,monospace; cursor:pointer; letter-spacing:.04em; transition:transform .15s,box-shadow .15s,background .15s; } button:hover { transform:translateY(-2px); background:#8aff9c; box-shadow:0 0 16px rgba(82,242,106,.35); } button:disabled { opacity:.5; cursor:not-allowed; transform:none; }.secondary-action { background:#0b160c; color:#8aff9b; }.secondary-action:hover { background:#112914; }.host-form,.join-browser,.lobby-room { background:#080d08; border:1px solid #2d7437; padding:18px; box-shadow:0 0 20px rgba(32,187,56,.1); }.host-form { display:grid; gap:14px; }.lobby-visibility { display:flex; gap:10px; flex-wrap:wrap; color:#c7e5ca; font-size:12px; }.lobby-visibility label { border:1px solid #2d7437; padding:9px; cursor:pointer; }.lobby-visibility input { accent-color:#55f570; }.join-browser { display:grid; gap:16px; }.join-code-row { display:flex; gap:8px; }.join-code-row input { flex:1; min-width:0; background:#040604; border:1px solid #367d41; color:#dfffff; padding:11px; font:14px inherit; }.browser-header { display:flex; align-items:center; justify-content:space-between; border-top:1px solid #214d28; padding-top:14px; }.browser-header h2 { margin:0; color:#8aff9a; font-size:15px; }.public-lobbies { display:grid; gap:7px; }.public-lobby { display:flex; justify-content:space-between; align-items:center; gap:10px; padding:10px; border:1px solid #234e2b; color:#c4e6c8; font-size:12px; }.public-lobby button { min-height:34px; padding:6px 10px; }.error-message { margin-top:12px; color:#ff7986; font-size:12px; }.lobby-room { margin-top:12px; }.lobby-header { display:flex; justify-content:flex-end; }.lobby-room h2 { color:#82f793; font-size:15px; overflow-wrap:anywhere; }.lobby-room p { color:#c6e9ca; font-size:13px; }.start-game { width:100%; margin-top:10px; } @keyframes blink { 50% { opacity:0; } }
|
||||||
min-height: 100vh;
|
@media(max-width:760px) { body { padding:0 16px; }.app-header { min-height:62px; }.header-status { display:none; }.menu-shell { min-height:0; padding:44px 0; grid-template-columns:1fr; gap:28px; }.leaderboard { order:2; }.command-center { max-width:none; }.command-center h1 { font-size:44px; }.lobby-buttons { flex-direction:column; }.lobby-buttons button { width:100%; }.join-code-row { flex-wrap:wrap; }.join-code-row button { width:100%; } }
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::before {
|
|
||||||
content: "♠ ♥ Welcome to Gambling API ♦ ♣";
|
|
||||||
font-size: 24px;
|
|
||||||
color: #0f0;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
display: block;
|
|
||||||
text-shadow: 0 0 5px #0f0, 0 0 10px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background-color: #0f0;
|
|
||||||
color: #000;
|
|
||||||
border: none;
|
|
||||||
padding: 15px 30px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 0 10px #0f0, 0 0 5px #0f0 inset;
|
|
||||||
transition: background 0.2s ease-in-out, transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: #1f1;
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.logout {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance {
|
|
||||||
margin-top: 20px;
|
|
||||||
color: #0f0;
|
|
||||||
text-shadow: 0 0 5px #0f0, 0 0 10px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lobby-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.host-form {
|
|
||||||
display: grid;
|
|
||||||
gap: 12px;
|
|
||||||
justify-items: center;
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lobby-visibility {
|
|
||||||
color: #fff;
|
|
||||||
font-family: monospace;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
text-shadow: 0 0 3px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lobby-visibility label {
|
|
||||||
border: 1px solid rgba(0, 255, 0, 0.5);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: rgba(0, 0, 0, 0.65);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lobby-visibility input {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
accent-color: #0f0;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lobby-room h2 {
|
|
||||||
color: #0f0;
|
|
||||||
font-size: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-shadow: 0 0 5px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#player-list {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-browser {
|
|
||||||
margin-top: 20px;
|
|
||||||
width: min(520px, calc(100vw - 32px));
|
|
||||||
background: rgba(0, 0, 0, 0.78);
|
|
||||||
border: 2px solid #0f0;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 0 10px #0f0;
|
|
||||||
padding: 18px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join-code-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#join-form:not(.hidden) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#join-form input {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 2px solid #0f0;
|
|
||||||
background-color: #111;
|
|
||||||
color: #0f0;
|
|
||||||
font-size: 16px;
|
|
||||||
box-shadow: 0 0 5px #0f0 inset;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 16px;
|
|
||||||
border-top: 1px solid rgba(0, 255, 0, 0.35);
|
|
||||||
padding-top: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-header h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #0f0;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-header button,
|
|
||||||
.public-lobby button {
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-lobbies {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 12px;
|
|
||||||
max-height: 220px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-lobbies p {
|
|
||||||
margin: 0;
|
|
||||||
color: #fff;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-lobby {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
border: 1px solid rgba(0, 255, 0, 0.45);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
background: rgba(0, 255, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-lobby-info {
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
color: #fff;
|
|
||||||
font-family: monospace;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-lobby-info strong {
|
|
||||||
color: #0f0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.public-lobby-info span {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #d8ffd8;
|
|
||||||
}
|
|
||||||
|
|
||||||
#join-form button {
|
|
||||||
background-color: #0f0;
|
|
||||||
color: #000;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: red;
|
|
||||||
margin-top: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-shadow: 0 0 3px red;
|
|
||||||
}
|
|
||||||
|
|
||||||
#matrix-canvas {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: -1;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: transparent;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard {
|
|
||||||
position: absolute;
|
|
||||||
top: 100px;
|
|
||||||
left: 20px;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
border: 2px solid #0f0;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
width: 200px;
|
|
||||||
color: #0f0;
|
|
||||||
font-family: monospace;
|
|
||||||
box-shadow: 0 0 10px #0f0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 0 0 5px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard li {
|
|
||||||
margin: 5px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
text-shadow: 0 0 3px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaderboard li.you {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #ff0;
|
|
||||||
text-shadow: 0 0 5px #ff0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.your-rank {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
text-shadow: 0 0 3px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lobby-room {
|
|
||||||
position: relative;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
border: 2px solid #0f0;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 30px;
|
|
||||||
margin-top: 40px;
|
|
||||||
width: 400px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 0 10px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lobby-header {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leave-button {
|
|
||||||
background-color: #f00;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 5px red;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s, transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leave-button:hover {
|
|
||||||
background-color: #c00;
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-game {
|
|
||||||
margin-top: 20px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start-game:disabled {
|
|
||||||
background-color: #163016;
|
|
||||||
color: #67a667;
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: 0 0 5px #0f0 inset;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lobby-room { width: min(460px, calc(100vw - 32px)); overflow-wrap: anywhere; }
|
|
||||||
body { padding: 72px 16px 24px; gap: 8px; }
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
body { justify-content: flex-start; }
|
|
||||||
.leaderboard { position: static; width: min(460px, 100%); order: 10; margin-top: 24px; }
|
|
||||||
body::before { text-align: center; font-size: 20px; margin-bottom: 12px; }
|
|
||||||
.join-code-row { flex-wrap: wrap; }
|
|
||||||
#join-form input { width: 100%; min-width: 0; }
|
|
||||||
.lobby-visibility { flex-wrap: wrap; justify-content: center; }
|
|
||||||
.lobby-header { position: static; text-align: right; }
|
|
||||||
}
|
|
||||||
|
|||||||
+7
-87
@@ -1,87 +1,7 @@
|
|||||||
body {
|
body { min-height:100vh; margin:0; color:#e4ffe7; background:radial-gradient(circle at 18% 15%,#143c1d 0,#070a07 32%,#020302 75%); font-family:ui-monospace,SFMono-Regular,Consolas,monospace; }
|
||||||
background: radial-gradient(circle at top, #111, #000);
|
.auth-shell { width:min(1000px,calc(100% - 32px)); min-height:100vh; margin:auto; display:grid; grid-template-columns:1fr 430px; align-items:center; gap:70px; }
|
||||||
color: #fff;
|
.eyebrow { margin:0; color:#6edc7b; font-size:11px; letter-spacing:.14em; text-transform:uppercase; }.auth-intro h1 { margin:14px 0; color:#effff0; font:700 clamp(48px,8vw,82px)/.88 ui-monospace,SFMono-Regular,Consolas,monospace; letter-spacing:-.08em; text-shadow:0 0 28px rgba(63,255,91,.32); }.auth-intro h1 span { color:#47f568; }.intro-copy { max-width:370px; color:#b9d8bc; font:16px/1.6 system-ui,sans-serif; }.intro-status { padding:20px 0 0; margin:25px 0 0; border-top:1px solid #245a2c; list-style:none; color:#80d98b; font-size:12px; line-height:2; }
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
.auth-panel { background:#090d09; border:1px solid #3de65b; box-shadow:0 22px 70px rgba(0,0,0,.55),0 0 24px rgba(22,208,58,.13); }.panel-chrome { display:flex; justify-content:space-between; padding:11px 14px; border-bottom:1px solid #245a2c; color:#75df83; font-size:11px; letter-spacing:.1em; }.auth-panel h2 { margin:30px 30px 8px; font-size:25px; letter-spacing:-.04em; }.panel-copy { margin:0 30px 25px; color:#9bb69e; font:14px/1.5 system-ui,sans-serif; }
|
||||||
display: flex;
|
form { display:grid; gap:9px; padding:0 30px 30px; } label { color:#9ee8a7; font-size:12px; letter-spacing:.06em; } input { min-width:0; padding:13px 14px; border:1px solid #2d6b35; border-radius:0; outline:0; background:#050705; color:#effff0; font:15px ui-monospace,SFMono-Regular,Consolas,monospace; } input:focus { border-color:#54fb6e; box-shadow:0 0 0 3px rgba(72,245,104,.16); } small { color:#719c76; font-size:11px; }
|
||||||
flex-direction: column;
|
button { min-height:44px; border:1px solid #53ed6c; border-radius:0; background:#57ed70; color:#041005; font:700 13px ui-monospace,SFMono-Regular,Consolas,monospace; cursor:pointer; letter-spacing:.04em; transition:transform .15s,box-shadow .15s,background .15s; } button:hover { transform:translateY(-2px); box-shadow:0 0 16px rgba(82,242,106,.42); background:#8aff9c; } button:disabled { opacity:.55; cursor:not-allowed; transform:none; }.auth-actions { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:12px; }.secondary-action { background:#0b160c; color:#8aff9b; }.secondary-action:hover { background:#112914; }.auth-error { margin:4px 0 0; color:#ff7885; font:12px/1.4 system-ui,sans-serif; }
|
||||||
justify-content: center;
|
@media(max-width:720px) { .auth-shell { min-height:unset; padding:54px 0; grid-template-columns:1fr; gap:32px; }.auth-intro h1 { font-size:54px; }.auth-panel h2,.panel-copy { margin-left:22px; margin-right:22px; } form { padding-left:22px; padding-right:22px; } }
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add styling for the heading */
|
|
||||||
h1 {
|
|
||||||
font-size: 28px;
|
|
||||||
color: #0f0;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-shadow: 0 0 5px #0f0, 0 0 10px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Your form stays centered below the heading */
|
|
||||||
form {
|
|
||||||
background: rgba(0, 0, 0, 0.85);
|
|
||||||
border: 2px solid #0f0;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 40px 30px;
|
|
||||||
box-shadow: 0 0 15px rgba(0, 255, 0, 0.3), 0 0 5px rgba(255, 255, 255, 0.1);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
padding: 12px 15px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
background: #222;
|
|
||||||
color: white;
|
|
||||||
outline: none;
|
|
||||||
box-shadow: inset 0 0 5px #0f0;
|
|
||||||
transition: 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
box-shadow: inset 0 0 10px #0f0, 0 0 5px #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 12px;
|
|
||||||
background: #06d429;
|
|
||||||
color: #000;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s ease-in-out;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-error {
|
|
||||||
color: #ff6b6b;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
form { width: min(364px, calc(100vw - 32px)); }
|
|
||||||
body { padding: 24px 0; }
|
|
||||||
|
|||||||
@@ -400,6 +400,15 @@ button:disabled:hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Terminal table overhaul */
|
||||||
|
body { background:radial-gradient(ellipse at 50% 45%,#0e2a14 0,#060806 43%,#010201 100%); font-family:ui-monospace,SFMono-Regular,Consolas,monospace; color:#dfffff; }
|
||||||
|
.poker-table { width:min(1180px,100%); min-height:100vh; margin:auto; padding:18px 28px 24px; grid-template-rows:auto 1fr auto auto; gap:18px; }
|
||||||
|
.table-header { grid-template-columns:1fr auto 1fr; padding-bottom:14px; border-bottom:1px solid #215a2a; }.round-panel { border:0; border-left:3px solid #55f570; box-shadow:none; background:transparent; padding:6px 12px; color:#dfffff; font-size:15px; text-shadow:none; }.round-panel strong { color:#74ff89; }.divider { color:#367c40; }.leave-table { border:1px solid #467e4e; border-radius:0; background:transparent; box-shadow:none; color:#a3e9ac; padding:8px 12px; }.leave-table:hover { background:#102414; box-shadow:none; transform:none; }
|
||||||
|
.community-area { width:min(760px,100%); align-self:center; gap:14px; }.turn-panel { border:1px solid #3ce956; box-shadow:0 0 26px rgba(42,235,73,.18); background:#070d08; }.turn-queue li { border-color:#275f30; box-shadow:none; background:#080d08; color:#bfe6c3; }.turn-queue li.active { background:#56ed70; color:#041005; box-shadow:0 0 13px rgba(82,242,106,.5); }.turn-queue li.next { border-color:#8bcf94; }
|
||||||
|
.card-row { gap:10px; }.card { border-radius:0; border-color:#60ee78; background:#e9ffee; box-shadow:0 0 14px rgba(64,240,94,.28); }.card.placeholder { background:#071008; color:#63db74; box-shadow:none; }.actions { padding:12px; border-top:1px solid #215a2a; border-bottom:1px solid #215a2a; }.actions button,.confirm-bet { min-width:112px; border:1px solid #53ed6c; border-radius:0; background:#0b180d; color:#81fb93; box-shadow:none; font-family:inherit; }.actions button:hover,.confirm-bet:hover { background:#59ed70; color:#041005; box-shadow:0 0 16px rgba(82,242,106,.35); transform:translateY(-2px); }.actions button[data-action="fold"] { border-color:#a64d59; color:#ff8996; }.actions button[data-action="fold"]:hover { background:#601c26; color:#fff; box-shadow:0 0 16px rgba(255,65,87,.3); }
|
||||||
|
.player-area { width:min(760px,100%); padding:8px 0 0; border-top:1px solid #215a2a; }.hand { gap:13px; }.player-status { width:100%; gap:12px 22px; border:1px solid #286532; box-shadow:none; background:#070c08; color:#cfead2; padding:12px; font-size:12px; }.player-status span:first-child { color:#77fb8b; font-weight:700; }.spectator-note,.showdown-panel { border-color:#3de65b; border-radius:0; box-shadow:0 0 20px rgba(40,230,70,.18); background:#070c08; }.showdown-panel { right:28px; bottom:28px; }.bet-modal { background:rgba(0,0,0,.7); }.bet-panel { border-color:#4fec69; box-shadow:0 0 38px rgba(52,235,81,.32); background:#070b08; border-radius:0; }.close-bet { border-radius:0; }.bet-amount span { border-color:#4eeb68; background:#050805; }.bet-amount button { min-width:34px; }
|
||||||
|
@media (max-width:700px) { .poker-table { padding:14px; gap:14px; }.table-header { gap:10px; }.round-panel { border-left:0; border-bottom:2px solid #55f570; }.actions { width:100%; }.actions button { flex:1; min-width:90px; }.player-status { justify-content:flex-start; }.showdown-panel { right:14px; bottom:14px; } }
|
||||||
|
|
||||||
.poker-table { gap: 24px; }
|
.poker-table { gap: 24px; }
|
||||||
.turn-panel { min-width: 0; width: min(460px, 100%); }
|
.turn-panel { min-width: 0; width: min(460px, 100%); }
|
||||||
.community-area, .player-area { max-width: 100%; min-width: 0; }
|
.community-area, .player-area { max-width: 100%; min-width: 0; }
|
||||||
|
|||||||
@@ -70,6 +70,17 @@ test('authentication is rate limited and cookies tolerate invalid escapes', asyn
|
|||||||
assert.equal(limited.status, 429); assert.ok(limited.headers.has('retry-after'));
|
assert.equal(limited.status, 429); assert.ok(limited.headers.has('retry-after'));
|
||||||
assert.equal(parseCookies('bad=%ZZ; authorization=ok').authorization, 'ok');
|
assert.equal(parseCookies('bad=%ZZ; authorization=ok').authorization, 'ok');
|
||||||
});
|
});
|
||||||
|
test('easter egg credits are server-authoritative and single-use except matrix color', async t => {
|
||||||
|
const {request} = await fixture(t);
|
||||||
|
const {token} = await (await request('/register', {method: 'POST', body: {username: 'agent42', password: 'password'}})).json();
|
||||||
|
const redeem = code => request('/easter-egg', {method: 'POST', token, body: {code}});
|
||||||
|
assert.deepEqual(await (await redeem('ILIKEMONEY')).json(), {balance: 1200, effect: 'credits'});
|
||||||
|
assert.equal((await redeem('ILIKEMONEY')).status, 409);
|
||||||
|
assert.deepEqual(await (await redeem('FRETUX')).json(), {balance: 2400, effect: 'credits'});
|
||||||
|
assert.deepEqual(await (await redeem('KRISHD')).json(), {balance: 12000, effect: 'credits'});
|
||||||
|
assert.equal((await redeem('LOSERNOOB')).status, 200);
|
||||||
|
assert.equal((await redeem('LOSERNOOB')).status, 200);
|
||||||
|
});
|
||||||
test('socket expires without waiting for a client message', async t => {
|
test('socket expires without waiting for a client message', async t => {
|
||||||
const {api, base} = await fixture(t, {sessionSeconds: 1});
|
const {api, base} = await fixture(t, {sessionSeconds: 1});
|
||||||
const {token} = await api.sessions.register({username: 'alice', password: 'password'});
|
const {token} = await api.sessions.register({username: 'alice', password: 'password'});
|
||||||
|
|||||||
Reference in New Issue
Block a user