feat: reroll 一键重刷
This commit is contained in:
1 parent
3eced7bdba
commit
816d9dffcc
21 files changed
+709
-27
No files matched your search
@@ -34,7 +34,8 @@
|
||||
|
||||
/* ---- briefing layout ---- */
|
||||
.brief__main { position: relative; z-index: 1; flex: 1; min-height: 0; display: grid; grid-template-columns: 6.2rem 1fr; gap: .36rem; padding: .3rem .5rem .1rem; }
|
||||
.brief__left { display: flex; flex-direction: column; gap: .2rem; min-height: 0; }
|
||||
.brief__left { display: flex; flex-direction: column; gap: .2rem; min-height: 0; overflow: auto; padding-right: .06rem; }
|
||||
.brief__left > * { flex-shrink: 0; }
|
||||
.brief__right { display: flex; flex-direction: column; gap: .2rem; min-height: 0; overflow: auto; padding-right: .06rem; }
|
||||
|
||||
.brief-boss {
|
||||
@@ -104,3 +105,22 @@
|
||||
.brief-ready__pips i .icon { width: 100%; height: 100%; }
|
||||
.brief-ready__pips i.on { color: var(--mint-400); filter: drop-shadow(0 0 .06rem rgba(23, 249, 183, .6)); }
|
||||
.brief-ready__pips i.me { border-bottom: 2px solid var(--mint-700); }
|
||||
|
||||
/* Full setup reroll: consent stays visible alongside the information being confirmed. */
|
||||
.brief-reroll {
|
||||
position: relative; z-index: 2; flex: none;
|
||||
display: flex; align-items: center; justify-content: space-between; gap: .2rem;
|
||||
margin: .1rem .5rem 0; padding: .14rem .2rem;
|
||||
background: rgba(12, 15, 14, .9); border: 1px solid var(--mint-700);
|
||||
}
|
||||
.brief-reroll__text { display: flex; flex-direction: column; gap: .05rem; min-width: 0; font-size: .16rem; }
|
||||
.brief-reroll__text strong { font-size: .2rem; color: var(--mint-400); }
|
||||
.brief-reroll__text > span { color: var(--text-lo); }
|
||||
.brief-reroll__players { display: flex; flex-wrap: wrap; gap: .08rem .2rem; color: var(--text-dim); }
|
||||
.brief-reroll__players .is-agreed { color: var(--mint-400); }
|
||||
.brief-reroll__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .1rem; flex-shrink: 0; }
|
||||
@media (max-width: 900px) {
|
||||
.brief-reroll { margin-inline: .3rem; gap: .1rem; padding: .1rem .14rem; }
|
||||
.brief-reroll__actions { flex-shrink: 1; }
|
||||
.brief-reroll__actions .btn { min-height: .44rem; padding-inline: .12rem; }
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
.result__grid { position: absolute; inset: 0; z-index: 0; pointer-events: none;
|
||||
background: linear-gradient(rgba(78, 216, 175, .04) 1px, transparent 1px) 0 0 / .6rem .6rem, linear-gradient(90deg, rgba(78, 216, 175, .04) 1px, transparent 1px) 0 0 / .6rem .6rem;
|
||||
-webkit-mask-image: radial-gradient(70% 70% at 25% 50%, #000, transparent); mask-image: radial-gradient(70% 70% at 25% 50%, #000, transparent); }
|
||||
/* The settlement can scroll in portrait; the game-wide rotate overlay must not cover it. */
|
||||
.sp-rotatable.sp-result .rotate-hint { display: none; }
|
||||
.result__main { position: relative; z-index: 1; flex: 1; min-height: 0; display: grid; grid-template-columns: 5.6rem 1fr; gap: .5rem; padding: .36rem .5rem .3rem .6rem; }
|
||||
.result__hero { display: flex; flex-direction: column; align-items: flex-start; gap: .16rem; padding-top: .3rem; min-height: 0; }
|
||||
.result__logoimg { height: 1.1rem; width: auto; opacity: .92; }
|
||||
@@ -42,29 +44,57 @@
|
||||
@keyframes card-in { from { opacity: 0; transform: translateX(.3rem); } }
|
||||
.rcard.is-self { border-left-color: var(--mint-500); background: linear-gradient(90deg, rgba(78, 216, 175, .08), rgba(11, 15, 13, .9) 40%); }
|
||||
.rcard.is-dead { opacity: .8; }
|
||||
.rcard__head { display: grid; grid-template-columns: auto minmax(1.5rem, 2.1rem) minmax(0, 1fr) auto 2.3rem; gap: .18rem; align-items: center; }
|
||||
.rcard__head .pavatar { --pa: .6rem; }
|
||||
.rcard__who { display: flex; flex-direction: column; gap: .03rem; min-width: 0; }
|
||||
.rcard__head {
|
||||
display: grid; grid-template-columns: auto minmax(0, 1fr) auto 2.3rem;
|
||||
grid-template-areas: 'avatar who rounds title' 'lineup lineup lineup lineup'; gap: .12rem .18rem; align-items: center;
|
||||
}
|
||||
.rcard__head .pavatar { --pa: .6rem; grid-area: avatar; }
|
||||
.rcard__who { display: flex; flex-direction: column; gap: .03rem; min-width: 0; grid-area: who; }
|
||||
.rcard__name { font-size: .22rem; display: flex; align-items: center; gap: .08rem; }
|
||||
.rcard__ai, .rcard__you { font-size: .11rem; padding: 0 .05rem; font-weight: 700; }
|
||||
.rcard__ai { background: #a9c3d8; color: #0b1520; }
|
||||
.rcard__you { background: var(--mint-500); color: var(--text-on-mint); }
|
||||
.rcard__band { display: inline-flex; align-items: center; gap: .06rem; font-size: .15rem; color: var(--text-lo); }
|
||||
.rcard__round { display: flex; flex-direction: column; align-items: center; }
|
||||
.rcard__round { display: flex; flex-direction: column; align-items: center; grid-area: rounds; }
|
||||
.rcard__round b { font-size: .34rem; line-height: 1; color: #fff; }
|
||||
.rcard__gain { display: flex; gap: .08rem; margin-top: .03rem; font-size: .13rem; color: var(--gold); }
|
||||
.rcard__gain span { display: inline-flex; align-items: center; gap: .02rem; }
|
||||
.rcard__gain .icon { width: .14rem; height: .14rem; }
|
||||
.rcard__gain b { font-size: .14rem; color: var(--gold); }
|
||||
.rcard__title { display: flex; align-items: center; gap: .08rem; padding: .06rem .14rem .06rem .06rem; border: 1px solid rgba(255, 198, 0, .4); background: rgba(255, 198, 0, .06); min-width: 0; }
|
||||
.rcard__title { display: flex; align-items: center; gap: .08rem; padding: .06rem .14rem .06rem .06rem; border: 1px solid rgba(255, 198, 0, .4); background: rgba(255, 198, 0, .06); min-width: 0; grid-area: title; }
|
||||
.rcard__title--none { border-style: dashed; border-color: var(--line); background: none; height: .64rem; }
|
||||
.rcard__title > span { display: flex; flex-direction: column; }
|
||||
.rcard__title b { font-size: .19rem; color: var(--gold); }
|
||||
.rcard__ticon { width: .5rem; height: .5rem; object-fit: contain; }
|
||||
.rcard__title .icon { width: .3rem; height: .3rem; color: var(--gold); }
|
||||
.rcard__mid { display: flex; flex-direction: column; justify-content: center; gap: .06rem; min-width: 0; }
|
||||
.rcard__lineup .uthumb { --ut: .46rem; }
|
||||
.rcard__mid { display: flex; flex-direction: column; justify-content: center; gap: .08rem; min-width: 0; grid-area: lineup; }
|
||||
.rcard__lineup { display: flex; flex-wrap: wrap; gap: .05rem; }
|
||||
.runit { width: max(.9rem, 56px); min-width: 0; }
|
||||
.runit__open {
|
||||
display: flex; flex-direction: column; align-items: center; gap: .04rem; width: 100%; padding: .04rem;
|
||||
color: var(--text-md); background: #101613; border: 1px solid var(--line-2); cursor: pointer;
|
||||
}
|
||||
.runit__open:hover, .runit__open:focus-visible { border-color: var(--mint-400); background: #17231d; }
|
||||
.runit__open:focus-visible { outline: 2px solid var(--mint-400); outline-offset: 2px; }
|
||||
.runit__open.is-golden { border-color: var(--gold); }
|
||||
.runit__art { position: relative; display: grid; place-items: center; width: 100%; height: max(1.4rem, 80px); overflow: hidden; background: radial-gradient(ellipse at bottom, #2a3d32, #111713); }
|
||||
.runit__portrait { width: 100%; height: 100%; object-fit: contain; object-position: bottom; }
|
||||
.runit__art > .uthumb { --ut: .56rem; }
|
||||
.runit__tier { position: absolute; left: .02rem; top: .02rem; }
|
||||
.runit__standin { position: absolute; bottom: 0; right: 0; padding: .02rem .04rem; background: #101613; font-size: max(.1rem, 10px); }
|
||||
.runit__items { display: flex; gap: .04rem; }
|
||||
.runit__items .uthumb, .runit__empty { --ut: max(.28rem, 22px); width: max(.28rem, 22px); height: max(.28rem, 22px); }
|
||||
.runit__empty { display: grid; place-items: center; border: 1px dashed var(--line-2); font-size: .11rem; color: var(--text-dim); }
|
||||
.result-unit__body { display: grid; grid-template-columns: 1.5rem minmax(0, 1fr); gap: .24rem; }
|
||||
.result-unit__portrait { display: flex; flex-direction: column; align-items: center; gap: .1rem; }
|
||||
.result-unit__portrait > img { width: 100%; max-height: 3rem; object-fit: contain; }
|
||||
.result-unit__elite { display: inline-block; margin-left: .06rem; padding: .02rem .06rem; font-size: .12rem; color: var(--gold); border: 1px solid var(--gold); }
|
||||
.result-unit__equipment h3 { display: flex; justify-content: space-between; gap: .1rem; margin-bottom: .12rem; font-size: max(.18rem, 14px); }
|
||||
.result-unit__item { display: flex; gap: .12rem; padding: .14rem 0; border-top: 1px solid var(--line-2); }
|
||||
.result-unit__item > .uthumb { flex-shrink: 0; }
|
||||
.result-unit__item > div { min-width: 0; }
|
||||
.result-unit__item h4 { margin-bottom: .06rem; color: var(--text-hi); font-size: max(.16rem, 13px); }
|
||||
.result-unit__item p { font-size: max(.14rem, 12px); line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.rcard__noinfo { font-family: var(--font-display); font-size: .14rem; letter-spacing: .3em; color: var(--text-dim); padding: .14rem .2rem; border: 1px dashed var(--line-2); }
|
||||
.rcard__bonds { display: flex; gap: .05rem; flex-wrap: wrap; }
|
||||
.rbond { display: inline-flex; align-items: center; gap: .04rem; padding: .01rem .07rem .01rem .03rem; border: 1px solid var(--line-2); font-size: .13rem; color: var(--text-lo); }
|
||||
@@ -76,3 +106,35 @@
|
||||
.rstat.is-best b { color: var(--gold); }
|
||||
.result__foot { margin-top: auto; padding-top: .2rem; display: flex; }
|
||||
.result__foot .btn { min-width: 3.4rem; }
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.result__main { grid-template-columns: 3.4rem minmax(0, 1fr); gap: .2rem; padding: .2rem; }
|
||||
.result__hero { padding-top: 0; }
|
||||
.result__headline { font-size: .6rem; }
|
||||
.result__sub { font-size: max(.16rem, 10px); letter-spacing: 0; }
|
||||
.result__logoimg { height: .65rem; }
|
||||
.result__rnum { font-size: 1rem; }
|
||||
.result__foot .btn { min-width: 0; }
|
||||
.rcard__name { font-size: max(.22rem, 13px); }
|
||||
.rcard__band, .rcard__title b { font-size: max(.15rem, 11px); }
|
||||
.rcard__stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.rstat { font-size: max(.13rem, 10px); }
|
||||
.rstat b { font-size: max(.18rem, 12px); }
|
||||
.result-unit { width: min(6.8rem, 94vw); }
|
||||
.result-unit .modal__title { font-size: 18px; }
|
||||
.result-unit .modal__actions .btn { min-width: 64px; min-height: 44px; font-size: 14px; }
|
||||
.result-unit__elite { font-size: max(.12rem, 10px); }
|
||||
.result-unit__item > .uthumb { --ut: max(.5rem, 36px); }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.result__main { display: flex; flex-direction: column; overflow: auto; }
|
||||
.result__hero { flex-shrink: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .12rem; align-items: center; }
|
||||
.result__logo, .result__sub, .result__medals, .result__time { display: none; }
|
||||
.result__rounds { grid-column: 1; }
|
||||
.result__foot { grid-column: 2; grid-row: 2; margin: 0; padding: 0; }
|
||||
.result__players { flex-shrink: 0; overflow: visible; }
|
||||
.rcard__head { grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: 'avatar who rounds' 'title title title' 'lineup lineup lineup'; }
|
||||
.rcard__title--none { display: none; }
|
||||
.result-unit { width: 94vw !important; }
|
||||
.result-unit__body { grid-template-columns: 1rem minmax(0, 1fr); gap: .12rem; }
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
|overtime|drain (boss rounds: level time over / overtime drain) |paused (solo battles)
|
||||
|funny (标准: 本局禁用 bonds) |morph (变形同构体 in the hand / shop, a wearer on the board) |harmony (缪尔赛思 on the board)
|
||||
&shot=1 (hide the switcher) &render=engine|fallback (default: the Pixi engine, DOM fallback if it fails)
|
||||
&variant=equipped (RESULT: zero, one or two carried equipment items, including upgraded ones)
|
||||
Keyboard: ` toggles the switcher.
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
|
||||
@@ -353,7 +353,9 @@ function buildResult(victory) {
|
||||
players: S.pub.players.map((p, i) => ({
|
||||
playerId: p.playerId, seat: p.seat, name: p.name, isBot: p.isBot, left: false, alive: i !== 3, victory, lp: [12, 12, 12, 0][i], bandId: p.bandId,
|
||||
roundsPassed: i === 3 ? 9 : victory ? 14 : 11, eliminatedRound: i === 3 ? 10 : null, title: pickTitle(i),
|
||||
lineup: shuffle(S.pool).slice(0, 8 + (i % 2)).map((c, k) => ({ id: k < 3 ? c.goldenId : c.chessId, golden: k < 3, tier: c.tier, row: 9 + (k % 4), col: 2 + k, items: [] })),
|
||||
lineup: shuffle(S.pool).slice(0, 8 + (i % 2)).map((c, k) => ({ id: k < 3 ? c.goldenId : c.chessId, golden: k < 3, tier: c.tier, row: 9 + (k % 4), col: 2 + k,
|
||||
items: VARIANTS.has('equipped') ? data.list('items').filter((it) => it.itemType === 'EQUIP' && !!it.isGolden === (k % 2 === 0)).slice(0, k % 3).map((it) => ({ id: it.id })) : [],
|
||||
})),
|
||||
bonds: shuffle(data.list('bonds')).slice(0, 4).map((b, k) => ({ bondId: b.bondId, count: 3 - (k % 3), layers: [359, 136, 34, 12][k], active: k < 3, tier: k < 3 ? 1 : 0 })),
|
||||
stats: {
|
||||
dmgDealt: [2310000, 1720000, 980000, 402000][i], kills: [412, 388, 301, 150][i], leaks: [3, 6, 9, 31][i], gold: [188, 164, 231, 90][i],
|
||||
|
||||
+12
-1
@@ -1055,5 +1055,16 @@
|
||||
"选择「{name}」的朝向": "Choose {name}'s facing",
|
||||
"选择朝向": "Choose a facing",
|
||||
"朝向:{dir}": "Facing: {dir}",
|
||||
"开发版 · 不稳定,请勿用于公开服务器": "Development build · unstable, not for public servers"
|
||||
"开发版 · 不稳定,请勿用于公开服务器": "Development build · unstable, not for public servers",
|
||||
"重刷本局": "Reroll Setup",
|
||||
"重刷本局投票:{agreed}/{total}": "Setup Reroll Vote: {agreed}/{total}",
|
||||
"全员同意后重新随机战场、领袖、特训敌人和禁用;确认倒计时已暂停。": "Unanimous consent rerolls the battlefield, leader, special enemies and bans. The briefing countdown is paused.",
|
||||
"保留房间和个人配置,重新随机全部开局信息。": "Reroll all starting information, keeping the room and personal settings.",
|
||||
"已同意": "Agreed",
|
||||
"待同意": "Awaiting Consent",
|
||||
"全体真人玩家一致同意后刷新,随机结果可能重复。": "Reroll after all human players agree. Random results may repeat.",
|
||||
"请等待所有玩家连接后再发起投票": "Wait for all players to connect before starting a vote",
|
||||
"同意重刷": "Agree to Reroll",
|
||||
"拒绝重刷": "Reject Reroll",
|
||||
"取消投票": "Cancel Vote"
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { useGameData, Img, RichText } from '../ui/gameComponents.js';
|
||||
import { StepHeader, ExitModal } from '../ui/matchChrome.js';
|
||||
import { MatchInfo, matchInfoModel } from '../ui/matchInfo.js';
|
||||
import { LoadoutButton } from './loadout.js';
|
||||
import { SetupReroll } from '../ui/setupReroll.js';
|
||||
import { actions } from '../ui/gameActions.js';
|
||||
import { factionTypes, sortedPlayers, phaseTotalSeconds } from '../ui/gameLogic.js';
|
||||
import { enemyIconUrl, factionIconUrl } from '../ui/assetUrls.js';
|
||||
@@ -50,7 +51,7 @@ export function BriefingScreen() {
|
||||
const ready = async () => {
|
||||
if (busy || me?.ready) return;
|
||||
setBusy(true);
|
||||
await actions.infoReady();
|
||||
await actions.infoReady(pub.setupRevision ?? 0);
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
@@ -91,6 +92,7 @@ export function BriefingScreen() {
|
||||
<${MatchInfo} model=${info} />
|
||||
</section>
|
||||
</main>
|
||||
<${SetupReroll} pub=${pub} />
|
||||
<footer class="brief__foot">
|
||||
<${LoadoutButton} from="briefing" size="lg" class="brief-loadout" />
|
||||
<div class="brief-ready">
|
||||
@@ -98,7 +100,7 @@ export function BriefingScreen() {
|
||||
<span class="brief-ready__pips">${players.map((p) => html`<i key=${p.playerId} class=${cx(p.ready && 'on', p.playerId === myId && 'me')} title=${p.name}><${Icon} name="user" /></i>`)}</span>
|
||||
</div>
|
||||
<${Button} variant="primary" size="xl" icon=${me?.ready ? 'check' : me ? 'play' : 'eye'} active=${!!me?.ready} loading=${busy}
|
||||
disabled=${!!me?.ready || !me} onClick=${ready}>${me?.ready ? t('已就绪') : me ? t('准备就绪') : t('观战中')}<//>
|
||||
disabled=${!!me?.ready || !me || !!pub.rerollVote} onClick=${ready}>${me?.ready ? t('已就绪') : me ? t('准备就绪') : t('观战中')}<//>
|
||||
</footer>
|
||||
<${ExitModal} open=${exit} onClose=${() => setExit(false)} solo=${solo} />
|
||||
</div>`;
|
||||
|
||||
+50
-10
@@ -15,15 +15,16 @@
|
||||
// stats: { dmgDealt, kills, leaks, gold /* funds SPENT */, refreshes, merges, bossDamage?, itemsEquipped?,
|
||||
// activatedLayers?, lpLost?, perfectRounds? } }] }
|
||||
|
||||
import { useEffect } from '../../vendor/hooks.module.js';
|
||||
import { html, Button, Icon, MicroLabel, DifficultyTag } from '../ui/components.js';
|
||||
import { useGameData, Img, UnitThumb, BandIcon, PlayerAvatar, BondGlyph, LpTower, Sprite } from '../ui/gameComponents.js';
|
||||
import { useEffect, useState } from '../../vendor/hooks.module.js';
|
||||
import { html, Button, Icon, MicroLabel, DifficultyTag, Modal, TierChip } from '../ui/components.js';
|
||||
import { useGameData, Img, UnitThumb, BandIcon, PlayerAvatar, BondGlyph, LpTower, Sprite, RichText } from '../ui/gameComponents.js';
|
||||
import { normalizeResult, fmtNum, diyRecordFor, cardStandIn, standInForText } from '../ui/gameLogic.js';
|
||||
import { data } from '../data.js';
|
||||
import { enemyIconUrl, titleIconUrl, uiUrl } from '../ui/assetUrls.js';
|
||||
import { enemyIconUrl, titleIconUrl, uiUrl, chessPortraitUrl } from '../ui/assetUrls.js';
|
||||
import { store, useStore, emptyMatch } from '../store.js';
|
||||
import { audio } from '../audio.js';
|
||||
import { sentText } from '../ui/lang.js';
|
||||
import { useDocClass } from '../ui/device.js';
|
||||
import { t, tParts, N_ } from '../../../shared/i18n.js';
|
||||
|
||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||
@@ -56,11 +57,50 @@ export function resultBonds(bonds, bondRec = () => null) {
|
||||
* 「替补」 mark — the title names the chess it fielded for.
|
||||
*/
|
||||
export function LineupThumb({ u, gd }) {
|
||||
const chess = u.kind === 'token' ? null : gd.chess(u.id);
|
||||
const dr = u.diy && chess ? diyRecordFor(chess, u.diy, { chess: data.get('chess'), backups: data.get('backups') }) : null;
|
||||
const si = !dr && chess ? cardStandIn(chess, { unit: u, backups: gd.backups }) : null;
|
||||
return html`<${UnitThumb} kind=${u.kind === 'token' ? 'token' : 'chess'} id=${u.id} golden=${!!u.golden} tier=${u.tier} size="sm" rec=${dr || si}
|
||||
title=${si ? t('{name}({note})', { name: si.name, note: standInForText(chess.name) }) : undefined} />`;
|
||||
const [open, setOpen] = useState(false);
|
||||
const token = u.kind === 'token';
|
||||
const chess = token ? null : gd.chess(u.id);
|
||||
const dr = u.diy && chess ? diyRecordFor(chess, u.diy, { chess: data.get('chess'), backups: data.get('backups') }) : null;
|
||||
const si = !dr && chess ? cardStandIn(chess, { unit: u, backups: gd.backups }) : null;
|
||||
const rec = dr || si || chess;
|
||||
const name = rec?.name || (token ? gd.token(u.id)?.name : null) || u.id;
|
||||
const title = si ? t('{name}({note})', { name: si.name, note: standInForText(chess.name) }) : name;
|
||||
const items = (Array.isArray(u.items) ? u.items : []).map((it) => typeof it === 'string' ? it : it?.id)
|
||||
.filter((id) => typeof id === 'string' && id).slice(0, 2);
|
||||
const slots = [0, 1];
|
||||
return html`<div class="runit">
|
||||
<button type="button" class=${cx('runit__open', u.golden && 'is-golden')} title=${title} aria-label=${`${title} · ${t('装备')} ${items.length}/2`}
|
||||
aria-haspopup="dialog" aria-expanded=${open} onClick=${() => setOpen(true)}>
|
||||
<span class="runit__art"><${Img} src=${chessPortraitUrl(gd.m, rec)} alt=${name} class="runit__portrait"
|
||||
fallback=${html`<${UnitThumb} kind=${token ? 'token' : 'chess'} id=${u.id} golden=${!!u.golden} tier=${u.tier} size="sm" showTier=${false} rec=${dr || si} title=${title} />`} />
|
||||
${!token && (u.tier || rec?.tier) ? html`<${TierChip} tier=${u.tier || rec.tier} golden=${!!u.golden} size="sm" class="runit__tier" />` : null}
|
||||
${si ? html`<span class="runit__standin">${t('替补')}</span>` : null}
|
||||
</span>
|
||||
${!token ? html`<span class="runit__items">${slots.map((slot) => items[slot]
|
||||
? html`<${UnitThumb} key=${slot} kind="item" id=${items[slot]} size="xs" showTier=${false} title=${gd.item(items[slot])?.name || items[slot]} />`
|
||||
: html`<span key=${slot} class="runit__empty" aria-hidden="true">—</span>`)}</span>` : null}
|
||||
</button>
|
||||
${open ? html`<${Modal} open=${true} title=${title} micro=${t('装备')} class="result-unit" width="min(6.8rem, 94vw)"
|
||||
onClose=${() => setOpen(false)} actions=${html`<${Button} onClick=${() => setOpen(false)}>${t('关闭')}<//>`}>
|
||||
<div class="result-unit__body">
|
||||
<div class="result-unit__portrait"><${Img} src=${chessPortraitUrl(gd.m, rec)}
|
||||
fallback=${html`<${UnitThumb} kind=${token ? 'token' : 'chess'} id=${u.id} golden=${!!u.golden} rec=${dr || si} size="lg" />`} />
|
||||
${u.golden ? html`<span class="result-unit__elite">${t('精锐')}</span>` : null}</div>
|
||||
<section class="result-unit__equipment" aria-label=${t('装备')}>
|
||||
<h3>${t('装备')} <span class="num">${items.length}/2</span></h3>
|
||||
${items.length ? items.map((id, i) => {
|
||||
const item = gd.item(id);
|
||||
return html`<div key=${i} class="result-unit__item">
|
||||
<${UnitThumb} kind="item" id=${id} size="sm" />
|
||||
<div><h4>${item?.name || id}${item?.isGolden ? html`<span class="result-unit__elite">${t('进阶')}</span>` : null}</h4>
|
||||
<${RichText} as="p" text=${item?.descRaw || item?.desc || ''} />
|
||||
</div>
|
||||
</div>`;
|
||||
}) : html`<p class="t-dim">—</p>`}
|
||||
</section>
|
||||
</div>
|
||||
<//>` : null}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function PlayerCard({ p, myId, titles, best, solo = false }) {
|
||||
@@ -105,6 +145,7 @@ function PlayerCard({ p, myId, titles, best, solo = false }) {
|
||||
|
||||
/** RESULT screen. */
|
||||
export function ResultScreen() {
|
||||
useDocClass('sp-result');
|
||||
const res = useStore((s) => s.match.result);
|
||||
const pub = useStore((s) => s.match.public);
|
||||
const myId = useStore((s) => s.me.playerId);
|
||||
@@ -161,4 +202,3 @@ export function ResultScreen() {
|
||||
</main>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ export async function act(t, fields = {}, opts = {}) {
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
infoReady: () => act('g.infoReady'),
|
||||
infoReady: (setupRevision = 0) => act('g.infoReady', { setupRevision }),
|
||||
rerollSetup: (setupRevision) => act('room.rerollSetup', { setupRevision }, { sfx: 'confirm' }),
|
||||
rerollVote: (voteId, agree) => act('g.rerollVote', { voteId, agree }, { sfx: agree ? 'confirm' : 'back' }),
|
||||
cancelReroll: (voteId) => act('room.cancelReroll', { voteId }, { sfx: 'back' }),
|
||||
band: (bandId) => act('g.band', { bandId }),
|
||||
bandSkip: () => act('g.bandSkip'),
|
||||
buy: (slot) => act('g.buy', { slot }),
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// A server-owned unanimous vote. Reconnects render the current vote from m.public.
|
||||
import { useRef, useState } from '../../vendor/hooks.module.js';
|
||||
import { html, Button } from './components.js';
|
||||
import { actions } from './gameActions.js';
|
||||
import { useStore } from '../store.js';
|
||||
import { t } from '../../../shared/i18n.js';
|
||||
|
||||
export function SetupReroll({ pub }) {
|
||||
const myId = useStore((s) => s.me.playerId);
|
||||
const hostId = useStore((s) => s.room?.hostId);
|
||||
const online = useStore((s) => s.connection.status === 'online');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const pending = useRef(false);
|
||||
const vote = pub.rerollVote;
|
||||
const host = myId === hostId;
|
||||
const players = pub.players || [];
|
||||
const me = players.find((p) => p.playerId === myId && p.status !== 'left');
|
||||
const allConnected = players.every((p) => p.isBot || p.status === 'left' || p.connected);
|
||||
const voter = !!vote?.voters.includes(myId);
|
||||
const agreed = !!vote?.agreed.includes(myId);
|
||||
const send = async (fn) => {
|
||||
if (pending.current) return;
|
||||
pending.current = true;
|
||||
setBusy(true);
|
||||
try { await fn(); } finally { pending.current = false; setBusy(false); }
|
||||
};
|
||||
if (!vote && (!host || !me)) return null;
|
||||
return html`<section class="brief-reroll" aria-label=${t('重刷本局')} aria-live="polite">
|
||||
<div class="brief-reroll__text">
|
||||
<strong>${vote ? t('重刷本局投票:{agreed}/{total}', { agreed: vote.agreed.length, total: vote.voters.length }) : t('重刷本局')}</strong>
|
||||
<span>${vote ? t('全员同意后重新随机战场、领袖、特训敌人和禁用;确认倒计时已暂停。') : t('保留房间和个人配置,重新随机全部开局信息。')}</span>
|
||||
${vote ? html`<div class="brief-reroll__players">${vote.voters.map((id) => {
|
||||
const p = players.find((p) => p.playerId === id);
|
||||
return html`<span key=${id} class=${vote.agreed.includes(id) ? 'is-agreed' : ''}>${p?.name || id} · ${vote.agreed.includes(id) ? t('已同意') : t('待同意')}</span>`;
|
||||
})}</div>` : null}
|
||||
</div>
|
||||
<div class="brief-reroll__actions">
|
||||
${!vote ? html`<${Button} icon="refresh" size="lg" loading=${busy} disabled=${!online || !allConnected}
|
||||
title=${allConnected ? t('全体真人玩家一致同意后刷新,随机结果可能重复。') : t('请等待所有玩家连接后再发起投票')}
|
||||
onClick=${() => send(() => actions.rerollSetup(pub.setupRevision ?? 0))}>${t('重刷本局')}<//>` : null}
|
||||
${vote && voter ? html`<${Button} variant="primary" loading=${busy} disabled=${!online || agreed}
|
||||
onClick=${() => send(() => actions.rerollVote(vote.id, true))}>${agreed ? t('已同意') : t('同意重刷')}<//>
|
||||
<${Button} variant="danger" disabled=${busy || !online}
|
||||
onClick=${() => send(() => actions.rerollVote(vote.id, false))}>${t('拒绝重刷')}<//>` : null}
|
||||
${vote && host && me ? html`<${Button} disabled=${busy || !online}
|
||||
onClick=${() => send(() => actions.cancelReroll(vote.id))}>${t('取消投票')}<//>` : null}
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
Reference in new issue
Block a user