- server.py: zero-dependency stdlib backend (HTTP + WebSocket /ws), 大话骰 rule engine, server-driven bots - live.html: real MVP frontend wired to the backend over WebSocket - index.html/app.js/style.css: older static prototype - deploy/: systemd unit + Nginx reverse proxy + Let's Encrypt setup.sh - test_*.py: standalone HTTP / in-process test scripts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
292 lines
44 KiB
HTML
292 lines
44 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||
<title>酒桌派对 Live MVP</title>
|
||
<link rel="stylesheet" href="style.css?v=4" />
|
||
<style>
|
||
html,body{min-height:100%;} body.in-room{overflow:hidden}.live-note{margin:10px 0 14px;padding:12px 14px;border:1px solid rgba(67,233,123,.25);border-radius:18px;background:rgba(67,233,123,.1);color:#c9ffe2;font-size:13px;line-height:1.5}.live-tabs{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:12px 0}.live-tabs button{padding:10px 8px;font-size:12px}.api-error{background:rgba(255,77,109,.14);border-color:rgba(255,77,109,.3);color:#ffd1d9}.hidden{display:none!important}.disabled{opacity:.45;pointer-events:none}.soft-disabled{opacity:.55}.muted-btn{background:rgba(255,255,255,.1);color:#fff;box-shadow:none;border:1px solid var(--line)}.danger{background:linear-gradient(135deg,#ff8a8a,#ff4d6d);color:#fff}.mini{font-size:12px;color:var(--muted);line-height:1.45}.state-pill{display:inline-block;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.1);font-size:12px}.thinking{background:linear-gradient(90deg,rgba(255,209,102,.22),rgba(255,138,42,.42),rgba(255,209,102,.22));background-size:200% 100%;animation:thinkingMove 1s linear infinite}.thinking:after{content:" · 思考中"}@keyframes thinkingMove{to{background-position:-200% 0}}
|
||
|
||
/* fixed game room */
|
||
.game-screen{position:fixed;inset:0;z-index:40;height:100dvh;padding:calc(env(safe-area-inset-top) + 8px) 12px calc(env(safe-area-inset-bottom) + 8px);background:radial-gradient(circle at 20% 0%,rgba(255,138,42,.24),transparent 34%),radial-gradient(circle at 100% 14%,rgba(255,77,109,.22),transparent 34%),linear-gradient(180deg,#160d22 0%,#080610 62%,#040308 100%);display:grid;grid-template-rows:48px 92px minmax(190px,1fr) 238px 60px;gap:8px;overflow:hidden}.game-topbar{display:grid;grid-template-columns:auto 1fr auto;gap:8px;align-items:center}.round-btn{width:42px;height:42px;padding:0;display:grid;place-items:center;border-radius:16px}.room-title strong{display:block;font-size:18px;letter-spacing:.04em}.room-title small{display:block;color:var(--muted);font-weight:800}.top-actions{display:flex;gap:6px}.top-actions button{padding:9px 10px;font-size:12px}.players-rail{display:flex;gap:8px;overflow-x:auto;overflow-y:hidden;padding:2px 0 8px;scrollbar-width:none}.players-rail::-webkit-scrollbar{display:none}.seat{min-width:92px;height:82px;padding:10px;border:1px solid var(--line);border-radius:20px;background:rgba(255,255,255,.075);position:relative;overflow:hidden}.seat strong{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.seat span{display:block;margin-top:4px;color:var(--muted);font-size:11px}.seat.turn{border-color:rgba(255,209,102,.78);box-shadow:0 0 0 2px rgba(255,209,102,.14),0 0 26px rgba(255,209,102,.25);animation:currentPulse 1.2s ease-in-out infinite}.seat.me{border-color:rgba(67,233,123,.48)}.seat .avatar{font-size:23px;margin-bottom:4px}@keyframes currentPulse{0%,100%{transform:scale(.99)}50%{transform:scale(1.02)}}
|
||
.table-panel{min-height:0;border:1px solid var(--line);border-radius:28px;background:linear-gradient(145deg,rgba(255,255,255,.13),rgba(255,255,255,.045));box-shadow:0 24px 70px rgba(0,0,0,.35);padding:14px;display:grid;grid-template-rows:auto 1fr auto;gap:10px}.call-board{text-align:center;border-radius:22px;background:rgba(0,0,0,.24);padding:14px 12px}.call-board p{margin:0 0 4px;color:var(--muted);font-weight:800}.call-board h1{font-size:38px;margin:0;line-height:1}.turn-line{margin-top:8px;color:var(--muted);font-weight:900}.my-dice{display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;min-height:68px}.rule-mini{padding:10px 12px;border-radius:18px;background:rgba(255,255,255,.07);border:1px solid var(--line)}.rule-mini p{margin:0;color:var(--muted);font-size:12px;line-height:1.45}
|
||
.picker-panel{min-height:0;border:1px solid var(--line);border-radius:28px;background:rgba(255,255,255,.075);padding:12px;display:grid;grid-template-rows:48px 50px 44px 1fr;gap:8px}.count-stepper{display:grid;grid-template-columns:44px 1fr 44px 44px 44px;gap:7px;align-items:center}.count-stepper button{height:46px;padding:0}.count-display{position:relative;height:46px;display:grid;place-items:center;border-radius:16px;background:rgba(255,255,255,.09);border:1px solid var(--line);font-size:28px;font-weight:900;line-height:1}.count-display:before{content:'数量';font-size:10px;color:var(--muted);font-weight:800;position:absolute;transform:translateY(-22px)}.mode-btn{height:46px!important;border-radius:15px!important;padding:0!important;background:rgba(255,255,255,.1)!important;color:#fff!important;box-shadow:none!important;border:1px solid var(--line)!important;font-size:15px!important}.mode-btn.active{background:linear-gradient(135deg,var(--gold),var(--orange))!important;color:#1b1005!important;border-color:rgba(255,209,102,.85)!important}.mode-btn.locked{opacity:.45}.point-grid{display:grid;grid-template-columns:repeat(6,1fr);gap:6px}.point-chip{height:46px;padding:0;border-radius:14px;background:rgba(255,255,255,.1);box-shadow:none;border:1px solid var(--line);display:grid;place-items:center}.point-chip.active{background:linear-gradient(135deg,var(--gold),var(--orange));border-color:rgba(255,209,102,.75)}.call-preview{display:grid;place-items:center;padding:6px 10px;border-radius:16px;background:rgba(255,138,42,.13);border:1px solid rgba(255,209,102,.22);font-weight:900;text-align:center;gap:4px}.mode-toggle{display:flex;gap:6px;justify-content:center}.mode-toggle button{padding:4px 10px;font-size:11px;border-radius:999px;background:rgba(255,255,255,.1);color:#fff;box-shadow:none;border:1px solid var(--line)}.mode-toggle button.active{background:linear-gradient(135deg,var(--gold),var(--orange));color:#1b1005}.action-bar{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}.action-bar button{height:52px;padding:0;min-width:0;font-size:15px}.action-bar .adv-btn{background:rgba(255,255,255,.1);color:#fff;box-shadow:none;border:1px solid var(--line);font-size:13px}.action-bar .soft-disabled{font-size:14px}.room-dock{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}.room-dock button{height:52px;padding:0;background:rgba(255,255,255,.1);color:#fff;box-shadow:none;border:1px solid var(--line);font-size:13px}.room-dock button.active{background:linear-gradient(135deg,var(--gold),var(--orange));color:#1b1005;border-color:rgba(255,209,102,.85)}.top-actions button.hidden-action{display:none}
|
||
|
||
/* dice */
|
||
.die.face,.mini-die.face{background:#fff!important;border:1px solid rgba(0,0,0,.14)!important;box-shadow:0 10px 22px rgba(0,0,0,.18);position:relative;color:#111}.die.face{width:54px;height:54px;border-radius:15px}.mini-die.face{width:30px;height:30px;border-radius:9px}.point-chip .mini-die.face{box-shadow:none}.pip{position:absolute;width:9px;height:9px;border-radius:50%;background:#111}.mini-die.face .pip{width:5px;height:5px}.pip.red{background:#e11d48}.p1{left:50%;top:50%;transform:translate(-50%,-50%)}.p2{left:24%;top:24%}.p3{right:24%;bottom:24%}.p4{right:24%;top:24%}.p5{left:24%;bottom:24%}.p6{left:24%;top:50%;transform:translateY(-50%)}.p7{right:24%;top:50%;transform:translateY(-50%)}body.opening-dice .die{animation:diceShake .12s linear infinite}@keyframes diceShake{0%{transform:rotate(-8deg) translateX(-2px)}50%{transform:rotate(8deg) translateX(2px)}100%{transform:rotate(-8deg) translateX(-2px)}}
|
||
|
||
/* sheets and result */
|
||
.sheet-mask{position:fixed;inset:0;display:none;background:rgba(0,0,0,.42);z-index:70;align-items:end}.sheet-mask.show{display:flex}.sheet{width:100%;max-height:72dvh;padding:10px 14px calc(env(safe-area-inset-bottom) + 14px);border-radius:28px 28px 0 0;background:#151020;border:1px solid var(--line);box-shadow:0 -24px 80px rgba(0,0,0,.65);overflow:auto}.grabber{width:46px;height:5px;border-radius:999px;background:rgba(255,255,255,.22);margin:0 auto 12px}.sheet-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.sheet-head h2{font-size:24px}.history{display:grid;gap:8px}.history-item{position:relative;padding:10px 12px 10px 28px;border-radius:14px;background:rgba(255,255,255,.07);font-size:13px;color:var(--muted)}.history-item:before{content:"";position:absolute;left:10px;top:15px;width:8px;height:8px;border-radius:50%;background:#ffd166;box-shadow:0 0 12px rgba(255,209,102,.8)}.history-item.open:before{background:#ff4d6d}.history-item.call:before{background:#38bdf8}.history-item.system:before{background:#43e97b}.history-item.split:before{background:#f97316}.history-item.counter:before{background:#fb7185}.history-item.challenge:before{background:#f59e0b}.history-item.decline:before{background:#a78bfa}.sheet-list{display:grid;gap:10px}.sheet-card{padding:12px;border:1px solid var(--line);border-radius:18px;background:rgba(255,255,255,.06)}.sheet-card h3{margin:0 0 6px}.sheet-card p{margin:0;color:var(--muted)}.sheet-actions{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}.copy-row{display:grid;grid-template-columns:1fr auto;gap:8px;margin-top:10px}.copy-row input{height:42px;font-size:12px}.settings-grid{display:grid;gap:12px}.settings-grid label{display:grid;gap:8px;color:var(--muted);font-size:13px}.settings-grid select{height:46px}.challenge-mask{position:fixed;inset:0;display:none;place-items:center;background:rgba(4,4,10,.82);backdrop-filter:blur(12px);z-index:75;padding:18px}.challenge-mask.show{display:grid}.challenge-card{width:min(100%,420px);border:1px solid rgba(255,77,109,.38);border-radius:30px;background:linear-gradient(180deg,rgba(34,16,28,.98),rgba(18,10,18,.98));box-shadow:0 24px 80px rgba(0,0,0,.82),0 0 0 1px rgba(255,77,109,.18) inset;padding:18px;animation:challengePop .22s ease-out}.challenge-card.warn{animation:challengeShake .45s ease-out}.challenge-card h2{margin:4px 0 10px;font-size:28px}.challenge-call{padding:14px;border-radius:20px;background:rgba(255,255,255,.08);text-align:center;font-weight:900}.challenge-actions{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:14px}.challenge-actions.two{grid-template-columns:1fr 1fr}.challenge-sub{color:rgba(255,247,237,.86);font-size:13px;line-height:1.5}.notice-mask{position:fixed;inset:0;display:none;place-items:center;background:rgba(4,4,10,.72);backdrop-filter:blur(10px);z-index:95;padding:18px}.notice-mask.show{display:grid}.notice-card{width:min(100%,380px);border:1px solid var(--line);border-radius:26px;background:linear-gradient(180deg,rgba(28,18,34,.98),rgba(14,10,18,.98));box-shadow:0 24px 80px rgba(0,0,0,.8);padding:18px}.notice-card h3{margin:0 0 8px;font-size:22px}.notice-card p{margin:0;color:rgba(255,247,237,.88);line-height:1.6}.notice-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}.notice-actions.one{grid-template-columns:1fr}.result-mask{position:fixed;inset:0;display:none;place-items:end center;background:rgba(0,0,0,.55);z-index:80;padding:14px}.result-mask.show{display:grid}.result-dialog{width:min(100%,432px);max-height:86vh;overflow:auto;border:1px solid var(--line);border-radius:30px;background:#151020;padding:20px;box-shadow:0 24px 80px rgba(0,0,0,.7)}.result-mask.show .result-dialog{animation:resultPop .28s cubic-bezier(.2,1.4,.4,1)}.result-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}.dice-summary{display:grid;gap:10px;margin-top:14px}.dice-user{padding:12px;border-radius:18px;border:1px solid var(--line);background:rgba(255,255,255,.06)}.dice-user h4{margin:0 0 8px}.mini-dice{display:flex;gap:6px;flex-wrap:wrap}.punish{animation:drinkGlow 1.1s ease-in-out infinite alternate}.result-winners{margin:12px 0 8px;padding:11px 14px;border-radius:16px;background:rgba(67,233,123,.12);border:1px solid rgba(67,233,123,.32);color:#c9ffe2;font-weight:900;text-align:center;font-size:15px}.drink-list{display:grid;gap:8px;margin:6px 0 4px}.drink-row{display:flex;align-items:center;justify-content:space-between;padding:11px 14px;border-radius:14px;background:rgba(255,138,42,.14);border:1px solid rgba(255,209,102,.3);font-weight:800;font-size:16px}.drink-row strong{color:#ffd166}.drink-row span:last-child{color:#ffd166;font-weight:900}.call-list{display:grid;gap:7px;margin:4px 0 8px}.call-row{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:13px;background:rgba(255,255,255,.06);border:1px solid var(--line);font-size:14px}.call-who{font-weight:900;min-width:60px}.call-what{flex:1;color:var(--muted);font-weight:800;display:flex;align-items:center;gap:5px}.call-act{color:#fff;font-weight:800;font-size:13px;white-space:nowrap}.call-tag{padding:2px 8px;border-radius:999px;font-size:11px;font-weight:900;background:rgba(255,255,255,.12);white-space:nowrap}.call-tag.ok{background:rgba(67,233,123,.2);color:#9affc4}.call-tag.bad{background:rgba(255,77,109,.2);color:#ffb3c1}.drink-fx{position:fixed;inset:0;pointer-events:none;z-index:90;overflow:hidden}.drink-emoji{position:absolute;left:50%;top:62%;font-size:34px;animation:drinkFly 1.2s ease-out forwards}@keyframes resultPop{from{transform:translateY(40px) scale(.94);opacity:0}to{transform:none;opacity:1}}@keyframes challengePop{from{transform:scale(.95);opacity:0}to{transform:none;opacity:1}}@keyframes challengeShake{0%,100%{transform:translateX(0)}20%{transform:translateX(-7px)}40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(3px)}}@keyframes drinkFly{0%{transform:translate(-50%,0) scale(.7);opacity:0}12%{opacity:1}100%{transform:translate(var(--dx),-360px) rotate(var(--rot)) scale(1.35);opacity:0}}@keyframes drinkGlow{from{box-shadow:0 0 0 rgba(255,138,42,0)}to{box-shadow:0 0 28px rgba(255,138,42,.42)}}
|
||
@media(max-height:760px){.game-screen{grid-template-rows:44px 78px minmax(160px,1fr) 210px 54px;gap:6px}.seat{height:70px;min-width:84px;padding:8px}.call-board h1{font-size:32px}.die.face{width:46px;height:46px}.picker-panel{grid-template-rows:42px 44px 38px 1fr}.action-bar button,.room-dock button{height:48px}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="app-shell" id="lobbyShell">
|
||
<header class="app-top"><div class="brand"><span class="logo">🍻</span><div><strong>酒桌派对</strong><small>Live MVP · 固定游戏桌版</small></div></div><button class="ghost-btn small" onclick="refreshRooms()">刷新</button></header>
|
||
<section id="lobby">
|
||
<div class="hero-card"><p class="eyebrow">Real MVP · Fixed Game Table</p><h1>大话骰一屏游戏桌</h1><p class="lead">房间页固定一屏,核心操作不用滚动;历史、规则、玩家和设置放到底部抽屉。</p><div class="join-panel"><label>你的昵称</label><div class="input-row"><input id="name" value="小鱼" /><button onclick="createRoom('dice')">开房</button></div></div></div>
|
||
<div id="msg" class="live-note">提示:N 人局最低从 N 个 1 开始;斋局 1 最大;破斋必须翻倍。</div>
|
||
<div class="live-tabs"><button onclick="createRoom('dice')">大话骰</button><button onclick="createRoom('poker')">德州扑克</button><button onclick="createRoom('six')">六张牌</button><button onclick="createRoom('catch')">抓二游</button></div>
|
||
<section class="room-section"><div class="section-head compact"><div><p class="eyebrow">Open Rooms</p><h2>当前房间</h2></div></div><div class="room-list" id="rooms"></div></section>
|
||
</section>
|
||
</main>
|
||
|
||
<section id="roomView" class="game-screen hidden">
|
||
<header class="game-topbar">
|
||
<button class="round-btn ghost-btn" onclick="backLobby()">‹</button>
|
||
<div class="room-title"><strong id="topRoomCode">----</strong><small id="topRoomState">等待中</small></div>
|
||
<div class="top-actions"><button id="topReadyBtn" class="ghost-btn" onclick="toggleReady()">准备</button><button id="topStartBtn" onclick="startGame()">开始</button></div>
|
||
</header>
|
||
<div class="players-rail" id="playersRail"></div>
|
||
<section class="table-panel">
|
||
<div class="call-board"><p>当前叫点</p><h1 id="callText">未开始</h1><div class="turn-line" id="turnText">等待开始</div></div>
|
||
<div class="my-dice" id="myDice"></div>
|
||
<div class="rule-mini"><p id="callTip">第一手最低 N 个 1。</p></div>
|
||
</section>
|
||
<section class="picker-panel" id="dicePanel">
|
||
<div class="count-stepper"><button class="muted-btn" onclick="adjustCallCount(-1)">−</button><div class="count-display"><span id="callCountText">1</span></div><button class="muted-btn" onclick="adjustCallCount(1)">+</button><button id="zhaiBtn" class="mode-btn" onclick="setCallMode('斋')">斋</button><button id="feiBtn" class="mode-btn" onclick="setCallMode('飞')">飞</button></div>
|
||
<div class="point-grid" id="pointGrid"></div>
|
||
<div class="call-preview" id="callPreview">准备叫骰</div>
|
||
<div class="action-bar" id="actionBar"></div>
|
||
<input id="callCount" type="hidden" value="1" /><input id="callPoint" type="hidden" value="1" /><input id="callMode" type="hidden" value="" />
|
||
</section>
|
||
<nav class="room-dock"><button onclick="openSheet('history')">历史</button><button onclick="openSheet('rules')">规则</button><button onclick="openSheet('players')">玩家</button><button onclick="openSheet('settings')">设置</button></nav>
|
||
</section>
|
||
|
||
<div class="sheet-mask" id="sheetMask" onclick="maskClose(event)"><div class="sheet"><div class="grabber"></div><div class="sheet-head"><h2 id="sheetTitle">详情</h2><button class="ghost-btn small" onclick="closeSheet()">关闭</button></div><div id="sheetContent"></div></div></div>
|
||
<div class="notice-mask" id="noticeMask"><div class="notice-card"><h3 id="noticeTitle">提示</h3><p id="noticeBody"></p><div class="notice-actions one" id="noticeActions"></div></div></div>
|
||
<div class="challenge-mask" id="challengeMask"><div class="challenge-card" id="challengeCard"><div class="eyebrow">Challenge</div><h2 id="challengeTitle">等待应战</h2><p class="challenge-sub" id="challengeDesc"></p><div class="challenge-call" id="challengeCall"></div><div class="challenge-actions" id="challengeActions"></div></div></div>
|
||
<div class="result-mask" id="resultMask"><div class="result-dialog"><div id="resultContent"></div><div class="result-actions"><button onclick="startGame();closeResult()">再来一局</button><button class="ghost-btn" onclick="closeResult()">关闭</button></div></div></div>
|
||
|
||
<script>
|
||
let currentCode=localStorage.getItem('dg_current_code')||'';
|
||
let currentPlayer=null,lastResultKey='',lastRoom=null,lastSuggestKey='',activeSheet='';
|
||
let isOpeningDice=false;
|
||
const clientId=localStorage.getItem('dg_client_id')||('p'+Math.random().toString(36).slice(2,12)); localStorage.setItem('dg_client_id',clientId);
|
||
const $=id=>document.getElementById(id); const roomPlayerKey=code=>'dg_player_'+code;
|
||
const pipMap={1:['p1'],2:['p2','p3'],3:['p2','p1','p3'],4:['p2','p4','p5','p3'],5:['p2','p4','p1','p5','p3'],6:['p2','p6','p5','p4','p7','p3']};
|
||
function dieHtml(n,mini=false){const red=(Number(n)===1||Number(n)===4);return `<div class="${mini?'mini-die':'die'} face">${(pipMap[n]||[]).map(pos=>`<span class="pip ${pos} ${red?'red':''}"></span>`).join('')}</div>`}
|
||
const savePlayer=(code,p)=>{currentPlayer=p;localStorage.setItem(roomPlayerKey(code),JSON.stringify(p));localStorage.setItem('dg_current_code',code)};
|
||
const loadPlayer=code=>{try{return JSON.parse(localStorage.getItem(roomPlayerKey(code))||'null')}catch{return null}};
|
||
function syncCurrentPlayer(room){if(!currentPlayer)currentPlayer=loadPlayer(room.code);if(currentPlayer){const fresh=room.players.find(p=>p.id===currentPlayer.id);if(fresh){currentPlayer={...currentPlayer,...fresh};localStorage.setItem(roomPlayerKey(room.code),JSON.stringify(currentPlayer))}}}
|
||
let noticeResolver=null;
|
||
function showNotice(msg,title='提示',buttons=[{label:'知道了',value:true,cls:''}]){ $('noticeTitle').textContent=title; $('noticeBody').textContent=String(msg||''); const box=$('noticeActions'); box.className='notice-actions'+(buttons.length===1?' one':''); box.innerHTML=buttons.map((b,i)=>`<button class="${b.cls||''}" onclick="resolveNotice(${i})">${b.label}</button>`).join(''); $('noticeMask').classList.add('show'); return new Promise(r=>{ noticeResolver=(idx)=>{ const btn=buttons[idx]; $('noticeMask').classList.remove('show'); r(btn.value); }; }); }
|
||
function resolveNotice(i){ if(noticeResolver) noticeResolver(i); }
|
||
const alert=(msg)=>showNotice(msg,'提示');
|
||
const appConfirm=(msg)=>showNotice(msg,'请确认',[{label:'取消',value:false,cls:'ghost-btn'},{label:'确定',value:true,cls:''}]);
|
||
function setMsg(t,err=false){$('msg').textContent=t;$('msg').className=err?'live-note api-error':'live-note'}
|
||
let ws=null, wsReady=false, wsQueue=[];
|
||
function connectWS(){
|
||
const proto=location.protocol==='https:'?'wss':'ws';
|
||
ws=new WebSocket(`${proto}://${location.host}/ws`);
|
||
ws.onopen=()=>{wsReady=true;const q=wsQueue;wsQueue=[];q.forEach(m=>ws.send(m));send('rooms',{});const known=currentCode&&loadPlayer(currentCode);const urlRoom=new URLSearchParams(location.search).get('room');if(known)send('join',{code:currentCode,name:known.name,playerId:known.id});else if(urlRoom)joinRoom(urlRoom.toUpperCase());};
|
||
ws.onmessage=e=>{try{handleWS(JSON.parse(e.data))}catch(err){}};
|
||
ws.onclose=()=>{wsReady=false;setTimeout(connectWS,1200)};
|
||
ws.onerror=()=>{try{ws.close()}catch(e){}};
|
||
}
|
||
function send(type,payload){const m=JSON.stringify(Object.assign({type},payload||{}));if(wsReady&&ws&&ws.readyState===1)ws.send(m);else wsQueue.push(m)}
|
||
function handleWS(msg){
|
||
if(msg.type==='room'){showRoom(msg.room)}
|
||
else if(msg.type==='rooms'){renderRoomList(msg.rooms)}
|
||
else if(msg.type==='joined'){currentCode=msg.room.code;savePlayer(currentCode,msg.player);showRoom(msg.room);history.replaceState(null,'','?room='+currentCode)}
|
||
else if(msg.type==='error'){if(currentCode)alert(msg.error);else setMsg(msg.error,true)}
|
||
else if(msg.type==='left'){backLobby()}
|
||
else if(msg.type==='closed'){if(currentCode)backLobby()}
|
||
}
|
||
function renderRoomList(list){$('rooms').innerHTML=(list||[]).map(r=>`<article class="room-card"><div><h3>${r.gameName} · ${r.code}</h3><p>${r.status} · ${r.players.length}/${r.maxPlayers} 人 ${r.locked?'· 已锁定':''}</p></div><button onclick="joinRoom('${r.code}')">加入</button></article>`).join('')||'<p class="lead">暂无房间</p>'}
|
||
function refreshRooms(){send('rooms',{})}
|
||
function createRoom(game){send('create',{game,name:$('name').value,playerId:clientId})}
|
||
function joinRoom(code){const known=loadPlayer(code);send('join',{code,name:$('name').value,playerId:known?.id||clientId})}
|
||
function isHost(){return !!currentPlayer?.host} function isMyTurn(room){return room.status==='playing'&¤tPlayer&&room.turnPlayerId===currentPlayer.id}
|
||
function nextLegalCall(room){const call=room.currentCall;if(!call)return{count:room.diceRule.minCount,point:1};if(call.mode==='斋'){const order=[2,3,4,5,6,1],idx=order.indexOf(Number(call.point));if(idx>=0&&idx<order.length-1)return{count:call.count,point:order[idx+1]};return{count:call.count+1,point:2}}if(call.point<6)return{count:call.count,point:call.point+1};return{count:call.count+1,point:1}}
|
||
function showRoom(room){lastRoom=room;syncCurrentPlayer(room);document.body.classList.add('in-room');$('lobbyShell').classList.add('hidden');$('roomView').classList.remove('hidden');currentCode=room.code;renderTop(room);renderPlayers(room);renderTable(room);renderPicker(room);renderChallengeOverlay(room);renderResult(room);if(activeSheet)renderSheet(activeSheet)}
|
||
function renderTop(room){
|
||
$('topRoomCode').textContent=room.code+' · 第 '+room.round+' 局';
|
||
const statusText=room.status==='waiting'?'等待中':room.status==='playing'?'游戏中':'已结算';
|
||
$('topRoomState').textContent=`${room.gameName} · ${statusText} · ${room.players.length}/${room.maxPlayers}人`;
|
||
const startBtn=$('topStartBtn'), readyBtn=$('topReadyBtn');
|
||
if(room.status==='playing'){
|
||
startBtn.textContent='进行中'; startBtn.className='disabled';
|
||
readyBtn.textContent='回合'; readyBtn.className='ghost-btn disabled';
|
||
}else{
|
||
startBtn.textContent=room.status==='result'?'再来':'开始'; startBtn.className=isHost()?'':'disabled';
|
||
readyBtn.textContent=currentPlayer?.ready?'取消':'准备'; readyBtn.className='ghost-btn';
|
||
}
|
||
}
|
||
function renderPlayers(room){
|
||
$('playersRail').innerHTML=room.players.map(p=>`<div data-player-id="${p.id}" class="seat ${p.id===room.turnPlayerId?'turn':''} ${currentPlayer&&p.id===currentPlayer.id?'me':''}"><div class="avatar">${p.host?'👑':p.bot?'🤖':'🙂'}</div><strong>${p.name}${p.id===room.turnPlayerId?' · 轮到':''}</strong><span>${p.ready?'已准备':'未准备'} · ${p.cups}杯</span></div>`).join('');
|
||
requestAnimationFrame(()=>{const el=$('playersRail').querySelector(`[data-player-id="${room.turnPlayerId}"]`); if(el) el.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'});});
|
||
}
|
||
function renderTable(room){
|
||
const call=room.currentCall;$('callText').textContent=call?`${call.count} 个 ${call.point} · ${call.mode}`:'未开始';
|
||
const p=room.pendingAction;
|
||
if(p){
|
||
if(p.stage==='wait_caller_counter') $('turnText').innerHTML=`${p.initiatorName} 要开骰,等待 ${p.targetName} 是否反劈`;
|
||
else if(p.stage==='wait_caller_accept') $('turnText').innerHTML=`${p.initiatorName} 劈 ${p.targetName},等待受/不受/反劈`;
|
||
else $('turnText').innerHTML=`${p.targetName} 反劈,等待 ${p.initiatorName} 受/不受`;
|
||
}else{
|
||
$('turnText').innerHTML=room.status==='playing'?`轮到 ${room.turnPlayerName}${room.turnPlayerBot?' <span class="state-pill thinking">机器人</span>':''}`:room.status==='result'?'本局已结算':'等待房主开始';
|
||
}
|
||
const me=room.players.find(p=>currentPlayer&&p.id===currentPlayer.id);$('myDice').innerHTML=me&&me.dice.length?me.dice.map(d=>dieHtml(d)).join(''):'<p class="mini">游戏中只显示自己的骰子;结算后公开全部。</p>';const n=nextLegalCall(room);$('callTip').textContent=p?'当前处于开骰/劈应战阶段,请等待相关玩家选择。':(call?`上一手:${call.by} 叫 ${call.count} 个 ${call.point}(${call.mode})。推荐:${n.count} 个 ${n.point}${call.mode==='斋'&&n.count>=call.count*2?'(破斋飞)':''}`:`${room.players.length}人局:最低 ${room.diceRule.minCount} 个1;斋局 2<3<4<5<6<1;破斋翻倍。`)
|
||
}
|
||
function setCallValues(c,p){$('callCount').value=String(c);$('callPoint').value=String(p);$('callCountText').textContent=String(c)}
|
||
function setCallMode(mode){ if(!lastRoom) return; if(Number($('callPoint').value)===1) mode='斋'; $('callMode').value=mode; renderPointChips(lastRoom)}
|
||
function selectedMode(room){const c=Number($('callCount').value);const pt=Number($('callPoint').value);const manual=$('callMode')?.value;if(pt===1)return '斋';if(manual==='斋'||manual==='飞')return manual;const old=room.currentCall;if(!old)return c>=room.diceRule.flyFrom?'飞':'斋';if(old.mode==='飞')return '飞';return c>=old.count*2?'飞':'斋'}
|
||
function callValidation(room){
|
||
const c=Number($('callCount').value), pt=Number($('callPoint').value), mode=selectedMode(room), old=room.currentCall;
|
||
if(pt===1 && $('callMode')?.value==='飞') return {ok:false,msg:'叫 1 只能是斋,不能飞'};
|
||
if(!old && c===room.diceRule.minCount && pt!==1) return {ok:false,msg:`首叫最低数量只能叫 ${room.diceRule.minCount} 个 1`};
|
||
if(!old && mode==='飞' && c<=room.diceRule.minCount+1) return {ok:false,msg:`首轮低数量阶段只能叫斋,${room.diceRule.minCount} 或 ${room.diceRule.minCount+1} 个不能直接飞`};
|
||
if(old){
|
||
if(c<old.count) return {ok:false,msg:`不能降数量,至少 ${old.count} 个`};
|
||
if(old.mode==='斋' && mode==='飞' && c<old.count*2) return {ok:false,msg:`破斋至少 ${old.count*2} 个,当前 ${c} 个不够`};
|
||
if(old.mode==='飞' && mode==='飞' && c===old.count && pt<=old.point) return {ok:false,msg:`继续飞同数量需叫 ${old.point+1} 点以上,或切到斋`};
|
||
if(old.mode==='飞' && mode==='斋' && c<old.count) return {ok:false,msg:`平斋/切斋不能降数量,至少 ${old.count} 个`};
|
||
}
|
||
return {ok:true,msg:''};
|
||
}
|
||
function renderPicker(room){
|
||
const key=JSON.stringify(room.currentCall||{})+'-'+room.turnPlayerId+'-'+room.round+'-'+JSON.stringify(room.pendingAction||{});
|
||
if(key!==lastSuggestKey){lastSuggestKey=key;$('callMode').value='';const n=nextLegalCall(room);setCallValues(n.count,n.point)}
|
||
renderPointChips(room);
|
||
renderActions(room);
|
||
}
|
||
function recentCallerCount(room){const s=new Set();(room.history||[]).forEach(h=>{if(h.type==='call'&&h.player)s.add(h.player)});return s.size}
|
||
function renderActions(room){
|
||
const bar=$('actionBar'); if(!bar) return;
|
||
const set=(cols,html)=>{bar.style.gridTemplateColumns=`repeat(${cols},1fr)`;bar.innerHTML=html};
|
||
// 应答阶段由全屏挑战弹层接管,动作行让位
|
||
if(room.pendingAction){ set(1,'<button class="soft-disabled">应答进行中…</button>'); return; }
|
||
if(room.status!=='playing'){ set(1,`<button class="soft-disabled">${room.status==='result'?'本局已结算':'等待房主开始'}</button>`); return; }
|
||
const me=currentPlayer?.id, hasCall=!!room.currentCall, btns=[];
|
||
if(isMyTurn(room)){
|
||
const v=callValidation(room);
|
||
btns.push(v.ok?'<button onclick="callDice()">叫点</button>':'<button class="soft-disabled">叫点</button>');
|
||
if(hasCall){
|
||
btns.push('<button id="openBtn" class="danger" onclick="confirmOpenDice()">开骰</button>');
|
||
btns.push('<button class="muted-btn" onclick="splitDice()">劈</button>');
|
||
const callers=recentCallerCount(room);
|
||
if(room.players.length>=4 && callers>=3){
|
||
btns.push('<button class="adv-btn" onclick="multiAction(\'open3\')">开三家</button>');
|
||
btns.push('<button class="adv-btn" onclick="multiAction(\'split3\')">劈三家</button>');
|
||
}else if(room.players.length>=3 && callers>=2){
|
||
btns.push('<button class="adv-btn" onclick="multiAction(\'open2\')">开两家</button>');
|
||
btns.push('<button class="adv-btn" onclick="multiAction(\'split2\')">劈两家</button>');
|
||
}
|
||
}
|
||
}else{
|
||
// 没轮到我:满足越位条件可抢开
|
||
if(hasCall && me && me!==room.currentCall.byId && me!==room.turnPlayerId)
|
||
btns.push('<button class="adv-btn" onclick="stealSplit()">抢开</button>');
|
||
btns.push(`<button class="soft-disabled">${hasCall?`等待 ${room.turnPlayerName}`:'等待叫点'}</button>`);
|
||
}
|
||
set(btns.length, btns.join(''));
|
||
}
|
||
function updateModeButtons(room, canPingZhai, finalMode, point){
|
||
const z=$('zhaiBtn'), f=$('feiBtn'); if(!z||!f) return;
|
||
z.className='mode-btn'; f.className='mode-btn'; z.disabled=false; f.disabled=false;
|
||
z.classList.toggle('active', finalMode==='斋');
|
||
f.classList.toggle('active', finalMode==='飞');
|
||
if(Number(point)===1){ f.classList.add('locked'); f.disabled=true; }
|
||
}
|
||
function renderPointChips(room){
|
||
let point=Number($('callPoint').value||1), mode=selectedMode(room), order=mode==='斋'?[2,3,4,5,6,1]:[1,2,3,4,5,6];
|
||
const c=Number($('callCount').value);
|
||
const firstMin=!room.currentCall && c===room.diceRule.minCount;
|
||
if(firstMin && point!==1){ point=1; $('callPoint').value='1'; }
|
||
$('pointGrid').innerHTML=order.map(p=>{const locked=firstMin&&p!==1; return `<button class="point-chip ${p===point?'active':''} ${locked?'disabled':''}" ${locked?'disabled':''} onclick="selectPoint(${p})">${dieHtml(p,true)}</button>`}).join('');
|
||
const pt=Number($('callPoint').value), finalMode=selectedMode(room), validation=callValidation(room);
|
||
const canPingZhai=room.currentCall&&room.currentCall.mode==='飞'&&c>=room.currentCall.count;
|
||
updateModeButtons(room, canPingZhai, finalMode, pt);
|
||
$('callPreview').innerHTML=validation.ok?`<div>将叫:${c} 个 ${pt} · ${finalMode}${firstMin?'(首叫最低只能叫1)':finalMode==='飞'?'(1万能)':'(1最大,不万能)'}</div>`:`<div>不能叫:${validation.msg}</div>`;
|
||
}
|
||
function adjustCallCount(delta){if(!lastRoom)return;let c=Number($('callCount').value||lastRoom.diceRule.minCount)+delta;c=Math.max(lastRoom.diceRule.minCount,Math.min(lastRoom.diceRule.maxCount,c));setCallValues(c,Number($('callPoint').value||1));renderPointChips(lastRoom)}
|
||
function selectPoint(p){if(!lastRoom)return;setCallValues(Number($('callCount').value||lastRoom.diceRule.minCount),p);renderPointChips(lastRoom)}
|
||
function callDice(){if(lastRoom){const v=callValidation(lastRoom);if(!v.ok)return alert(v.msg);}send('call',{count:Number($('callCount').value),point:Number($('callPoint').value),mode:selectedMode(lastRoom)})}
|
||
function splitDice(){send('split',{})}
|
||
function stealSplit(){send('steal',{})}
|
||
function multiAction(kind){closeSheet();send('multi',{kind})}
|
||
function respondChallenge(response){send('respond',{response})}
|
||
function respondMulti(response){send('respondMulti',{response})}
|
||
function confirmOpenDice(){if(!lastRoom?.currentCall)return alert('还没有人叫点,不能开骰。');openDice()}
|
||
function openDice(){if(isOpeningDice)return;isOpeningDice=true;document.body.classList.add('opening-dice');const ob=$('openBtn');if(ob)ob.textContent='开骰中...';setTimeout(()=>{document.body.classList.remove('opening-dice');isOpeningDice=false;send('open',{})},650)}
|
||
function startGame(){closeResult();closeChallenge();send('start',{})}
|
||
function toggleReady(){send('ready',{})}
|
||
function addBot(){send('addBot',{})}
|
||
function toggleLock(){send('lock',{})}
|
||
function saveSettings(){send('settings',{cupsPerLoss:$('cupsSelect').value})}
|
||
function kickPlayer(id){send('kick',{targetId:id})}
|
||
async function leaveRoom(){if(!await appConfirm('确定退出房间?'))return;localStorage.removeItem(roomPlayerKey(currentCode));send('leave',{})}
|
||
function renderHistory(room){return (room.history||[]).length?`<div class="history">${room.history.slice().reverse().map(h=>`<div class="history-item ${h.type||'system'}">${h.text}</div>`).join('')}</div>`:'<p class="mini">暂无记录</p>'}
|
||
function diceHtml(players){return `<div class="dice-summary">${players.map(p=>`<div class="dice-user"><h4>${p.host?'👑':p.bot?'🤖':'🙂'} ${p.name} · ${p.cups}杯</h4><div class="mini-dice">${(p.dice||[]).length?p.dice.map(d=>dieHtml(d,true)).join(''):'<span class="mini">未公开</span>'}</div></div>`).join('')}</div>`}
|
||
function closeChallenge(){ $('challengeMask').classList.remove('show'); }
|
||
function openResult(room){
|
||
closeChallenge(); const lr=room.lastResult;
|
||
const diceSection=lr.revealDice!==false?`<h3>所有人的骰子</h3>${diceHtml(room.players)}`:'<p class="mini">本局因不受结束,不公开骰子。</p>';
|
||
let body, totalCups;
|
||
if(lr.multi){
|
||
const winners=lr.winners||[], drinks=lr.drinks||[], calls=lr.calls||[];
|
||
totalCups=drinks.reduce((s,d)=>s+Number(d.cups||0),0);
|
||
const winHtml=winners.length?`<div class="result-winners">🏆 赢家:${winners.join('、')}</div>`:'';
|
||
const callHtml=calls.length?`<h3>本局叫点</h3><div class="call-list">${calls.map(c=>{const tag=c.win===true?'<span class="call-tag ok">成立</span>':c.win===false?'<span class="call-tag bad">不成立</span>':(c.actual==null?'<span class="call-tag">不受</span>':'');const act=c.actual!=null?`<span class="call-act">实际 ${c.actual}</span>`:'';return `<div class="call-row"><span class="call-who">${c.name}</span><span class="call-what">叫 ${c.count} 个 ${dieHtml(c.point,true)} · ${c.mode||''}</span>${act}${tag}</div>`}).join('')}</div>`:'';
|
||
const drinkHtml=drinks.length?drinks.map(d=>`<div class="drink-row"><span>🍺 <strong>${d.name}</strong></span><span>${d.cups} 杯</span></div>`).join(''):'<p class="mini">本局无人喝酒</p>';
|
||
body=`<p class="eyebrow">Open Dice</p><h2>多家结算结果</h2>${winHtml}${callHtml}<div class="drink-list">${drinkHtml}</div>${diceSection}`;
|
||
}else{
|
||
totalCups=lr.cups||room.settings?.cupsPerLoss||1;
|
||
const detail=lr.revealDice!==false?`<span class="call-who">${lr.call.by||'叫骰'}</span> 叫 ${lr.call.count} 个 ${dieHtml(lr.call.point,true)} · ${lr.call.mode} 实际 ${lr.actual} 个<br>`:'';
|
||
body=`<p class="eyebrow">Open Dice</p><h2>开骰结果</h2><p class="lead">${lr.text}</p><div class="punish">${detail}🍺 ${lr.loser} 喝 ${totalCups} 杯</div>${diceSection}`;
|
||
}
|
||
$('resultContent').innerHTML=body; $('resultMask').classList.add('show'); createDrinkEffect(totalCups);
|
||
}
|
||
function renderChallengeOverlay(room){
|
||
const p=room.pendingAction, mask=$('challengeMask'); if(!p){mask.classList.remove('show'); return;}
|
||
const me=currentPlayer?.id; let title='等待应战', desc='', actions='', two=false;
|
||
const currentTarget=(p.targets&&p.targets[p.currentIndex||0])||null;
|
||
const call=p.call||currentTarget?.call;
|
||
if(call) $('challengeCall').textContent=`当前叫点:${call.count} 个 ${call.point} · ${call.mode}`;
|
||
if(p.type==='open2' || p.type==='split2'){
|
||
if(p.stage==='wait_target' && currentTarget){
|
||
title=p.type==='open2'?'开两家应答':'劈两家应答';
|
||
desc=`${p.initiatorName} 正在挑战 ${currentTarget.playerName},请完成该家应答。`;
|
||
if(me===currentTarget.playerId){
|
||
if(p.type==='open2'){ actions=`<button onclick="respondMulti('no_counter')">不反</button><button class="danger" onclick="respondMulti('counter')">反劈</button>`; two=true; }
|
||
else { actions=`<button onclick="respondMulti('decline')">不受</button><button class="danger" onclick="respondMulti('accept')">受</button><button class="muted-btn" onclick="respondMulti('counter')">反劈</button>`; }
|
||
}
|
||
} else if(p.stage==='wait_initiator' && currentTarget){
|
||
title='两家反劈应答'; desc=`${currentTarget.playerName} 已反劈,${p.initiatorName} 请选择 受 / 不受。`;
|
||
if(me===p.initiatorId){ actions=`<button onclick="respondMulti('decline')">不受</button><button class="danger" onclick="respondMulti('accept')">受</button>`; two=true; }
|
||
}
|
||
} else if(p.stage==='wait_caller_counter'){
|
||
title='开骰应答'; desc=`${p.initiatorName} 要开骰,${p.targetName} 请选择是否反劈。`;
|
||
if(me===p.targetId){ actions=`<button onclick="respondChallenge('no_counter')">不反</button><button class="danger" onclick="respondChallenge('counter')">反劈</button>`; two=true; }
|
||
}else if(p.stage==='wait_caller_accept'){
|
||
title=(p.type==='steal_split'?'抢开应答':'劈应答'); desc=`${p.initiatorName} ${p.type==='steal_split'?'抢开':'劈'} ${p.targetName},请选择 受 / 不受 / 反劈。`;
|
||
if(me===p.targetId){ actions=`<button onclick="respondChallenge('decline')">不受</button><button class="danger" onclick="respondChallenge('accept')">受</button><button class="muted-btn" onclick="respondChallenge('counter')">反劈</button>`; }
|
||
}else if(p.stage==='wait_opener_accept'){
|
||
title='反劈应答'; desc=`${p.targetName} 已反劈,${p.initiatorName} 请选择 受 / 不受。`;
|
||
if(me===p.initiatorId){ actions=`<button onclick="respondChallenge('decline')">不受</button><button class="danger" onclick="respondChallenge('accept')">受</button>`; two=true; }
|
||
}
|
||
$('challengeTitle').textContent=title; $('challengeDesc').textContent=desc; $('challengeActions').className='challenge-actions'+(two?' two':''); $('challengeActions').innerHTML=actions || '<button class="muted-btn">等待对方选择</button>'; $('challengeCard').classList.add('warn'); setTimeout(()=>$('challengeCard').classList.remove('warn'),450); mask.classList.add('show');
|
||
}
|
||
function renderResult(room){if(room.lastResult){const key=room.code+'-'+room.round+'-'+room.updatedAt;if(lastResultKey!==key){lastResultKey=key;openResult(room)}}}
|
||
function closeResult(){$('resultMask').classList.remove('show')}
|
||
function createDrinkEffect(cups=1){const box=document.createElement('div');box.className='drink-fx';const total=Math.min(18,6+Number(cups)*4);for(let i=0;i<total;i++){const e=document.createElement('span');e.className='drink-emoji';e.textContent=i%3===0?'🍻':'🍺';e.style.setProperty('--dx',(Math.random()*320-160)+'px');e.style.setProperty('--rot',(Math.random()*480-240)+'deg');e.style.left=(45+Math.random()*10)+'%';e.style.animationDelay=(Math.random()*.22)+'s';box.appendChild(e)}document.body.appendChild(box);setTimeout(()=>box.remove(),1500)}
|
||
function setActiveDock(type){document.querySelectorAll('.room-dock button').forEach(btn=>btn.classList.toggle('active', btn.textContent.trim()===({history:'历史',rules:'规则',players:'玩家',settings:'设置'}[type]||'')));}
|
||
function openSheet(type){activeSheet=type;renderSheet(type);setActiveDock(type);$('sheetMask').classList.add('show')}
|
||
function closeSheet(){activeSheet='';setActiveDock('');$('sheetMask').classList.remove('show')} function maskClose(e){if(e.target.id==='sheetMask')closeSheet()}
|
||
function renderSheet(type){
|
||
if(!lastRoom) return; const room=lastRoom; let title='', html='';
|
||
if(type==='history'){ title='叫骰历史'; html=renderHistory(room); }
|
||
if(type==='rules'){ title='大话骰规则'; html=`<div class="sheet-card"><h3>斋 / 飞</h3><p>${room.players.length}人局最低 ${room.diceRule.minCount} 个1。斋局点数:2<3<4<5<6<1。破斋必须翻倍,飞后1为万能。</p></div><div class="sheet-card"><h3>进阶规则</h3><p>抢开=越位发起的劈;开/劈两家取最近两位叫骰人;开/劈三家取最近三位叫骰人,三家仅限4人局以上。</p></div><div class="sheet-card"><h3>当前惩罚</h3><p>输家喝 ${room.settings?.cupsPerLoss||1} 杯。</p></div>`; }
|
||
if(type==='players'){ title='玩家管理'; const host=isHost(); html=`<div class="sheet-list">${room.players.map(p=>{const canKick=host&¤tPlayer&&p.id!==currentPlayer.id&&room.status!=='playing'; const st=p.stats||{}; return `<div class="sheet-card"><h3>${p.host?'👑':p.bot?'🤖':'🙂'} ${p.name}${p.host?' · 房主':''}</h3><p>${p.ready?'已准备':'未准备'} · ${p.cups}杯${p.bot?' · 机器人':''}</p><p class="mini">劈 ${st.splits||0} 次 · 反劈 ${st.counters||0} 次 · 不受 ${st.declines||0} 次 · 总喝 ${st.cupsDrunk||0} 杯</p>${host&¤tPlayer&&p.id!==currentPlayer.id?`<div class="sheet-actions"><button class="danger ${canKick?'':'disabled'}" onclick="kickPlayer('${p.id}')">移出</button><span class="mini">${room.status==='playing'?'本局中不可移出':'可移出'}</span></div>`:''}</div>`}).join('')}</div><div class="sheet-actions"><button onclick="addBot()" class="muted-btn ${host&&room.status!=='playing'?'':'disabled'}">加机器人</button><button onclick="leaveRoom()" class="ghost-btn">退出房间</button></div>`; }
|
||
if(type==='settings'){ title='房间设置'; const invite=location.origin+location.pathname+'?room='+room.code; html=`<div class="settings-grid"><label>输家惩罚<select id="cupsSelect" onchange="saveSettings()" ${!isHost()||room.status==='playing'?'disabled':''}><option value="1">1杯</option><option value="2">2杯</option><option value="3">3杯</option><option value="4">4杯</option><option value="5">5杯</option></select></label><div class="copy-row"><input id="inviteLink" readonly value="${invite}"/><button onclick="copyInvite()">复制</button></div><div class="sheet-actions"><button onclick="toggleLock()" class="muted-btn ${isHost()?'':'disabled'}">${room.locked?'开放加入':'暂停加入'}</button><button onclick="backLobby()" class="ghost-btn">返回大厅</button></div></div>`; }
|
||
$('sheetTitle').textContent=title; $('sheetContent').innerHTML=html; if(type==='settings'&&$('cupsSelect')) $('cupsSelect').value=String(room.settings?.cupsPerLoss||1)
|
||
}
|
||
function copyInvite(){const input=$('inviteLink');input.select();document.execCommand('copy');alert('邀请链接已复制')}
|
||
function backLobby(){currentCode='';localStorage.removeItem('dg_current_code');closeResult();closeChallenge();closeSheet();document.body.classList.remove('in-room');$('roomView').classList.add('hidden');$('lobbyShell').classList.remove('hidden');history.replaceState(null,'',location.pathname);refreshRooms()}
|
||
connectWS();
|
||
</script>
|
||
</body>
|
||
</html>
|