hyapp-h5/game/touzi/index.html
2026-06-24 10:31:15 +08:00

4361 lines
163 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<title data-i18n="diceMatch.pageTitle">Dice Match</title>
<link rel="stylesheet" href="../../common/theme.css" />
<link rel="stylesheet" href="../common/game-match.css" />
<link rel="stylesheet" href="../common/game-loading.css" />
<style>
:root {
color-scheme: dark;
--dice-frame-width: 375px;
--dice-frame-height: 812px;
--dice-scale: 1;
--dice-playfield-offset: 24px;
--dice-topbar-offset: -24px;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
min-width: 100%;
height: 100%;
min-height: 100%;
margin: 0;
overflow: hidden;
background: #030623;
font-family:
'Source Han Sans SC', 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: geometricprecision;
}
body {
display: flex;
align-items: flex-start;
justify-content: center;
}
button {
padding: 0;
border: 0;
color: inherit;
font: inherit;
background: transparent;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.dice-stage {
position: relative;
width: 100%;
max-width: 768px;
height: calc(var(--dice-frame-height) * var(--dice-scale));
margin: 0 auto;
overflow: hidden;
background: #030623;
}
.dice-frame {
position: absolute;
top: 0;
left: 0;
width: var(--dice-frame-width);
height: var(--dice-frame-height);
overflow: hidden;
background: #030623;
transform: scale(var(--dice-scale));
transform-origin: top left;
}
.dice-frame.is-matching {
animation: screen-impact-shake 520ms ease-out 420ms both;
}
.dice-frame.is-self-win-compare {
animation: you-win-screen-smash 780ms
cubic-bezier(0.14, 0.84, 0.18, 1) 430ms both;
}
.game-content {
position: absolute;
inset: 0;
transform: translateY(
calc(-44px + var(--dice-playfield-offset))
);
}
.layer {
position: absolute;
}
.vs-badge {
transform-origin: 50% 50%;
will-change: transform, filter;
}
.vs-badge::after {
position: absolute;
inset: 14px 10px;
content: '';
border: 2px solid rgba(255, 213, 87, 0.68);
border-radius: 50%;
opacity: 0;
pointer-events: none;
transform: scale(0.62);
filter: drop-shadow(0 0 10px rgba(255, 96, 243, 0.68));
}
.dice-frame.is-matching .vs-badge {
animation: vs-drop-impact 900ms
cubic-bezier(0.13, 0.92, 0.18, 1) both;
}
.dice-frame.is-matching .vs-badge::after {
animation: vs-impact-ring 580ms ease-out 430ms both;
}
.layer-img {
position: absolute;
display: block;
width: 100%;
height: 100%;
max-width: none;
pointer-events: none;
user-select: none;
}
.cover {
object-fit: cover;
}
.fill {
object-fit: fill;
}
.status-time {
position: absolute;
top: 13px;
left: 33.87px;
width: 54px;
height: 15px;
color: #ffffff;
font-family: Helvetica, Arial, sans-serif;
font-size: 15px;
line-height: 15px;
text-align: center;
}
.top-bar {
position: absolute;
top: 60px;
left: 16px;
z-index: 10;
width: 343px;
height: 36px;
display: flex;
align-items: center;
justify-content: space-between;
transform: translateY(var(--dice-topbar-offset));
}
.top-back {
position: relative;
width: 46px;
height: 36px;
flex: 0 0 46px;
transform: translateY(-17px);
}
.top-game-icon {
position: absolute;
top: 50%;
left: 50%;
width: 36px;
height: 36px;
overflow: hidden;
border-radius: 6px;
transform: translate(-50%, -50%);
}
.top-coin {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
width: max-content;
min-width: 102px;
max-width: 176px;
height: 32px;
flex: 0 1 auto;
gap: 4px;
overflow: visible;
padding: 0 7px;
/* border: 1px solid rgba(108, 113, 194, 0.96);
border-radius: 4px; */
/* background: linear-gradient(
180deg,
rgba(15, 18, 58, 0.92) 0%,
rgba(4, 6, 31, 0.86) 100%
);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.05),
0 0 11px rgba(79, 93, 202, 0.22); */
color: #ffe38e;
font-family: inherit;
font-size: 18px;
font-weight: 900;
line-height: 20px;
margin: 0;
text-align: center;
text-shadow:
0 1px 1px rgba(65, 23, 0, 0.86),
0 0 6px rgba(255, 204, 74, 0.34);
white-space: nowrap;
appearance: none;
-webkit-appearance: none;
border-image: none;
cursor: pointer;
}
.top-coin span {
display: block;
min-width: max-content;
max-width: none;
flex: 0 0 auto;
overflow: visible;
line-height: 20px;
}
.top-coin.is-compact span {
max-width: none;
font-size: 18px;
}
.top-coin img {
display: block;
width: 23px;
height: 23px;
flex: 0 0 23px;
filter: drop-shadow(0 0 5px rgba(255, 197, 49, 0.45));
}
.top-coin-plus {
color: #ffd15a;
font-size: 21px;
font-weight: 900;
/* line-height: 8px; */
text-shadow:
0 1px 1px rgba(65, 23, 0, 0.86),
0 0 8px rgba(255, 178, 47, 0.74);
align-items: center;
/* font-size: 0; */
display: inline-block;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 5px;
}
.top-coin.is-crediting {
animation: top-coin-credit-pulse 720ms ease-out both;
}
.coin-credit-layer {
position: absolute;
inset: 0;
z-index: 60;
pointer-events: none;
}
.coin-credit-flyer {
position: absolute;
left: var(--coin-credit-x);
top: var(--coin-credit-y);
width: 24px;
height: 24px;
opacity: 0;
filter: drop-shadow(0 0 6px rgba(255, 203, 67, 0.86));
transform: translate3d(0, 0, 0) scale(0.78);
animation: coin-credit-fly 920ms
cubic-bezier(0.2, 0.82, 0.16, 1) forwards;
animation-delay: var(--coin-credit-delay, 0ms);
will-change: transform, opacity;
}
.language-switcher {
position: absolute;
top: 41px;
right: 0;
z-index: 20;
width: 102px;
font-family: inherit;
}
.language-button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 22px;
border: 1px solid rgba(108, 113, 194, 0.68);
border-radius: 4px;
background: rgba(7, 10, 40, 0.72);
color: #ffe38e;
font-size: 12px;
font-weight: 900;
line-height: 14px;
letter-spacing: 0;
text-shadow: 0 0 5px rgba(255, 204, 74, 0.42);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.04);
}
.language-button::after {
margin-left: 4px;
color: #ffd15a;
content: 'v';
font-size: 10px;
line-height: 10px;
}
.language-menu {
position: absolute;
top: 26px;
right: 0;
width: 102px;
padding: 4px;
border: 1px solid rgba(108, 113, 194, 0.78);
border-radius: 6px;
background: rgba(5, 8, 35, 0.94);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.42);
}
.language-menu button {
display: block;
width: 100%;
min-height: 24px;
border-radius: 4px;
color: rgba(255, 227, 142, 0.78);
font-size: 12px;
font-weight: 800;
line-height: 14px;
text-align: center;
}
.language-menu button.is-active {
background: rgba(255, 209, 90, 0.16);
color: #ffd15a;
}
html[dir='rtl'] .language-button::after {
margin-right: 4px;
margin-left: 0;
}
.lobby-avatar-glow {
top: 163px;
left: 151px;
width: 73px;
height: 73px;
}
.lobby-player-card {
top: 146px;
left: 131px;
width: 113px;
height: 158px;
}
.dice-avatar,
.dice-avatar-frame {
position: absolute;
display: block;
pointer-events: none;
user-select: none;
}
.dice-avatar {
z-index: 4;
width: 71px;
height: 71px;
border-radius: 50%;
object-fit: cover;
clip-path: circle(50% at 50% 50%);
background: transparent;
}
.dice-avatar-frame {
z-index: 5;
width: 84px;
height: 84px;
object-fit: contain;
opacity: 0;
}
.dice-avatar-frame.has-frame {
opacity: 1;
}
.avatar-hole-mask {
position: absolute;
z-index: 3;
width: 74px;
height: 74px;
border-radius: 50%;
background: radial-gradient(
circle at 50% 44%,
rgba(27, 19, 36, 0.98) 0%,
rgba(13, 10, 22, 0.98) 68%,
rgba(6, 5, 15, 0.98) 100%
);
box-shadow:
inset 0 0 16px rgba(0, 0, 0, 0.72),
0 0 10px rgba(255, 174, 58, 0.12);
pointer-events: none;
}
.avatar-hole-mask-lobby {
top: 163px;
left: 150px;
}
.avatar-hole-mask-self {
top: 163px;
left: 40px;
}
.dice-avatar.is-empty,
.dice-avatar-frame.is-empty {
visibility: hidden;
opacity: 0;
}
.dice-avatar-lobby {
top: 163px;
left: 152px;
}
.dice-avatar-frame-lobby {
top: 158px;
left: 145px;
}
.dice-avatar-self {
top: 168px;
left: 45px;
}
.dice-avatar-frame-self {
top: 158px;
left: 35px;
}
.dice-avatar-opponent {
top: 168px;
left: 266px;
}
.dice-avatar-frame-opponent {
top: 158px;
left: 256px;
}
.lobby-player-name {
position: absolute;
top: 247px;
left: 147px;
width: 81px;
height: 32px;
overflow: hidden;
color: #ffffff;
font-size: 14px;
line-height: 16px;
text-align: center;
word-break: break-word;
}
.player-name {
position: absolute;
top: 247px;
left: 37px;
width: 81px;
height: 32px;
overflow: hidden;
color: #ffffff;
font-size: 14px;
line-height: 16px;
word-break: break-word;
}
.opponent-mark {
position: absolute;
top: 247px;
left: 258px;
width: 81px;
height: 16px;
color: #ffffff;
font-size: 14px;
line-height: 16px;
text-align: center;
}
.opponent-name {
position: absolute;
top: 247px;
left: 258px;
z-index: 6;
width: 81px;
height: 32px;
overflow: hidden;
color: #ffffff;
font-size: 14px;
line-height: 16px;
text-align: center;
word-break: break-word;
}
.matching-label {
position: absolute;
top: 270px;
left: 282px;
width: 54px;
height: 16px;
overflow: hidden;
color: #b9defb;
font-size: 10px;
line-height: 16px;
white-space: nowrap;
}
.winner-label {
position: absolute;
top: 608px;
left: 123px;
width: 130px;
height: 14px;
color: transparent;
font-size: 14px;
line-height: 14px;
text-align: center;
white-space: nowrap;
background-image: url('assets/figma/winner-text-fill.png');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
-webkit-background-clip: text;
background-clip: text;
}
.stake {
position: absolute;
top: 634px;
width: 77px;
height: 28px;
display: flex;
align-items: center;
gap: 2px;
overflow: hidden;
padding-left: 8px;
color: #ffe293;
font-size: 16px;
font-weight: 700;
line-height: 16px;
text-shadow: 0 0 1px #d5942b;
white-space: nowrap;
}
.stake:active {
filter: brightness(1.15);
}
.stake[data-selected='true'] {
filter: drop-shadow(0 0 5px rgba(255, 197, 47, 0.4));
}
.stake-bg {
position: absolute;
top: 0;
left: 0;
width: 77px;
height: 28px;
pointer-events: none;
}
.stake[data-selected='true'] .stake-bg {
display: none;
}
.stake-selected-slice {
position: absolute;
top: 0;
z-index: 0;
display: none;
width: 45px;
height: 28px;
pointer-events: none;
background-image: url('assets/figma/stake-fill.png');
background-repeat: no-repeat;
background-size: 105px 28px;
}
.stake[data-selected='true'] .stake-selected-slice {
display: block;
}
.stake-selected-left {
left: 0;
background-position: left top;
}
.stake-selected-right {
right: 0;
background-position: right top;
}
.stake-coin {
position: relative;
z-index: 1;
width: 14px;
height: 14px;
flex: 0 0 14px;
}
.stake-value {
position: relative;
z-index: 1;
font-size: 12px;
line-height: 12px;
}
.match-button {
position: absolute;
top: 690px;
left: 74px;
width: 227px;
height: 50px;
}
.match-button img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.match-button span {
position: absolute;
top: 13px;
left: 0;
width: 227px;
color: #3e1f04;
font-size: 24px;
font-weight: 900;
line-height: 24px;
text-align: center;
white-space: nowrap;
}
.match-button:active {
transform: translateY(1px);
filter: brightness(0.96);
}
.victory-button {
position: absolute;
}
.victory-button img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.victory-button:active {
filter: brightness(1.12);
transform: translateY(1px);
}
.fee-note {
position: absolute;
top: 752px;
left: 62px;
width: 252px;
height: 12px;
overflow: hidden;
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
line-height: 12px;
text-align: center;
white-space: nowrap;
}
.waiting-only {
opacity: 1;
transition:
opacity 260ms ease,
visibility 0s linear 0s;
will-change: opacity;
}
.dice-frame.is-matching .waiting-only {
visibility: hidden;
opacity: 0;
pointer-events: none;
transition:
opacity 260ms ease 560ms,
visibility 0s linear 830ms;
}
.dice-frame.is-searching .waiting-only {
visibility: hidden;
opacity: 0;
pointer-events: none;
transition:
opacity 240ms ease,
visibility 0s linear 260ms;
}
.lobby-only {
transition:
opacity 180ms ease,
visibility 0s linear 0s,
transform 820ms ease-in-out;
will-change: opacity, transform;
}
.dice-frame.is-searching .lobby-only,
.dice-frame.is-matching .lobby-only {
visibility: visible;
opacity: 1;
pointer-events: auto;
transition:
opacity 180ms ease,
visibility 0s linear 0s,
transform 820ms ease-in-out;
}
.dice-frame.is-searching .lobby-avatar-glow,
.dice-frame.is-matching .lobby-avatar-glow,
.dice-frame.is-searching .lobby-player-card,
.dice-frame.is-matching .lobby-player-card,
.dice-frame.is-searching .avatar-hole-mask-lobby,
.dice-frame.is-matching .avatar-hole-mask-lobby,
.dice-frame.is-searching .dice-avatar-lobby,
.dice-frame.is-matching .dice-avatar-lobby,
.dice-frame.is-searching .dice-avatar-frame-lobby,
.dice-frame.is-matching .dice-avatar-frame-lobby,
.dice-frame.is-searching .lobby-player-name,
.dice-frame.is-matching .lobby-player-name {
transform: translateX(-110px);
}
.matching-card-only {
visibility: hidden;
opacity: 0;
transition:
opacity 180ms ease,
visibility 0s linear 180ms;
}
.dice-frame.is-searching .matching-card-only {
visibility: visible;
opacity: 1;
transition:
opacity 220ms ease 760ms,
visibility 0s linear 760ms;
}
.search-indicator img {
transform-origin: 50% 50%;
}
.search-indicator img {
--search-orbit-radius: 5px;
}
.dice-frame.is-searching .search-indicator img {
animation: search-orbit 1100ms linear infinite;
}
.searching-only {
visibility: hidden;
opacity: 0;
transition:
opacity 180ms ease,
visibility 0s linear 180ms;
}
.dice-frame.is-searching .searching-only {
visibility: visible;
opacity: 1;
transition:
opacity 220ms ease 820ms,
visibility 0s linear 820ms;
}
.center-search {
position: absolute;
top: 157px;
left: 122px;
z-index: 8;
width: 132px;
height: 132px;
filter: drop-shadow(0 0 12px rgba(77, 224, 255, 0.72));
}
.center-search-bg,
.center-search-icon {
position: absolute;
display: block;
pointer-events: none;
user-select: none;
}
.center-search-bg {
inset: 0;
width: 100%;
height: 100%;
transform-origin: 50% 50%;
will-change: transform;
}
.dice-frame.is-searching .center-search-bg {
animation: matching-center-spin 3000ms linear infinite;
}
.center-search-icon {
top: 50%;
left: 50%;
width: 44px;
height: 44px;
filter: drop-shadow(0 0 8px rgba(126, 220, 255, 0.88));
transform: translate(-50%, -50%);
}
.matching-action {
position: absolute;
top: 676px;
left: 74px;
width: 264px;
height: 24px;
visibility: hidden;
opacity: 0;
transition:
opacity 260ms ease 180ms,
visibility 0s linear 440ms;
will-change: opacity;
}
.dice-frame.is-searching .matching-action {
visibility: visible;
opacity: 1;
transition:
opacity 260ms ease 860ms,
visibility 0s linear 860ms;
}
.matching-action img {
position: absolute;
top: 7px;
width: 36px;
height: 7px;
opacity: 0;
pointer-events: none;
transition:
opacity 180ms ease 260ms,
transform 260ms ease 240ms;
}
.matching-action-left {
left: 0;
transform: translateX(18px);
}
.matching-action-right {
left: 228px;
transform: translateX(-18px);
}
.dice-frame.is-searching .matching-action img {
opacity: 1;
transform: translateX(0);
transition:
opacity 180ms ease 940ms,
transform 260ms ease 920ms;
}
.matching-action-text {
position: absolute;
top: 0;
left: 44px;
width: 140px;
opacity: 0;
color: transparent;
font-size: 24px;
font-weight: 900;
line-height: 24px;
text-align: center;
white-space: nowrap;
background: linear-gradient(180deg, #fef8ca 27.76%, #ffb36c);
-webkit-background-clip: text;
background-clip: text;
text-shadow: 0 0 3px #bc0003;
transition: opacity 220ms ease 260ms;
}
.dice-frame.is-searching .matching-action-text {
opacity: 1;
transition: opacity 220ms ease 920ms;
}
.dice-phase-text {
position: absolute;
top: 318px;
left: 52px;
z-index: 12;
width: 271px;
min-height: 42px;
visibility: hidden;
opacity: 0;
color: #fff1a8;
font-size: 36px;
font-weight: 900;
line-height: 42px;
text-align: center;
text-shadow:
0 2px 0 rgba(96, 16, 0, 0.78),
0 0 14px rgba(255, 88, 0, 0.82);
transition: opacity 180ms ease;
}
.dice-frame.is-searching .dice-phase-text,
.dice-frame.is-matching .dice-phase-text {
visibility: visible;
opacity: 1;
}
.dice-frame.is-countdown .dice-phase-text {
visibility: hidden;
opacity: 0;
}
.countdown-layer {
position: absolute;
top: 268px;
left: 0;
z-index: 18;
display: flex;
align-items: center;
justify-content: center;
width: 375px;
height: 158px;
visibility: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.dice-frame.is-countdown .countdown-layer {
visibility: visible;
opacity: 1;
}
.you-win-smash-layer,
.you-win-impact {
position: absolute;
visibility: hidden;
opacity: 0;
pointer-events: none;
}
.you-win-smash-layer {
inset: 0;
z-index: 23;
overflow: hidden;
}
.you-win-smash-layer::before {
position: absolute;
inset: 0;
content: '';
background:
radial-gradient(
circle at 50% 39%,
rgba(255, 248, 179, 0.72) 0%,
rgba(255, 126, 20, 0.3) 18%,
rgba(255, 73, 0, 0.08) 35%,
rgba(255, 73, 0, 0) 58%
),
rgba(255, 255, 255, 0.2);
opacity: 0;
}
.you-win-smash-floor {
position: absolute;
top: 362px;
left: 50%;
width: 312px;
height: 92px;
border-radius: 50%;
background: radial-gradient(
ellipse at center,
rgba(255, 215, 75, 0.68) 0%,
rgba(255, 103, 15, 0.34) 30%,
rgba(120, 16, 0, 0.28) 54%,
rgba(0, 0, 0, 0) 76%
);
opacity: 0;
filter: blur(1px) drop-shadow(0 0 18px rgba(255, 118, 22, 0.76));
transform: translateX(-50%) scale(0.38, 0.2);
transform-origin: 50% 50%;
}
.dice-frame.is-self-win-compare .you-win-smash-layer {
visibility: visible;
opacity: 1;
}
.dice-frame.is-self-win-compare .you-win-smash-layer::before {
animation: you-win-screen-flash 760ms ease-out 430ms both;
}
.dice-frame.is-self-win-compare .you-win-smash-floor {
animation: you-win-floor-impact 900ms ease-out 430ms both;
}
.you-win-impact {
top: 300px;
left: 0;
z-index: 25;
display: flex;
align-items: center;
justify-content: center;
width: 375px;
height: 92px;
color: transparent;
font-size: 56px;
font-weight: 1000;
line-height: 62px;
text-align: center;
background: linear-gradient(
180deg,
#ffffff 0%,
#fff5b6 19%,
#ffcf44 48%,
#ff7a00 74%,
#9b2d00 100%
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-stroke: 2px #7d2700;
filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9))
drop-shadow(0 0 22px rgba(255, 205, 46, 0.9))
drop-shadow(0 0 36px rgba(255, 77, 0, 0.78));
transform-origin: 50% 78%;
will-change: opacity, transform, filter;
}
.dice-frame.is-self-win-compare .you-win-impact {
visibility: visible;
opacity: 1;
animation: you-win-drop-impact 980ms
cubic-bezier(0.13, 0.92, 0.18, 1) both;
}
.countdown-number {
min-width: 132px;
color: transparent;
font-size: 132px;
font-weight: 1000;
line-height: 132px;
text-align: center;
background: linear-gradient(
180deg,
#ffffff 0%,
#fff5b6 17%,
#ffcf44 43%,
#ff7a00 71%,
#9b2d00 100%
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-stroke: 2px #7d2700;
filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9))
drop-shadow(0 0 18px rgba(255, 205, 46, 0.92))
drop-shadow(0 0 30px rgba(255, 77, 0, 0.72));
opacity: 0;
transform: scale(0.72);
}
.countdown-number.is-ticking {
animation: countdown-number-pop 940ms ease-out both;
}
.dice-score {
position: absolute;
top: 379px;
z-index: 12;
width: 132px;
height: 132px;
visibility: hidden;
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
transition: opacity 180ms ease;
}
.dice-score-self {
left: 8px;
}
.dice-score-opponent {
left: 230px;
}
.dice-frame.has-scores .dice-score {
visibility: visible;
opacity: 1;
}
.dice-score img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
user-select: none;
}
.dice-roll-effect {
position: absolute;
z-index: 11;
width: 132px;
height: 132px;
visibility: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.dice-roll-effect canvas,
.dice-roll-effect video {
display: block;
width: 100%;
height: 100%;
}
.dice-roll-effect-self {
left: 8px;
top: 379px;
}
.dice-roll-effect-opponent {
left: 230px;
top: 379px;
}
.dice-roll-effect.is-playing,
.dice-roll-effect.is-fallback {
visibility: visible;
opacity: 1;
}
.dice-roll-effect.is-fallback::before {
position: absolute;
inset: 28px;
content: '';
border-radius: 50%;
background: radial-gradient(
circle at 45% 38%,
rgba(255, 255, 255, 0.95) 0%,
rgba(255, 230, 124, 0.72) 32%,
rgba(255, 108, 26, 0.38) 58%,
rgba(255, 108, 26, 0) 78%
);
filter: blur(1px) drop-shadow(0 0 14px rgba(255, 196, 69, 0.82));
animation: dice-roll-fallback 520ms linear infinite;
}
.battle-only {
opacity: 1;
transition:
opacity 240ms ease,
visibility 0s linear 0s;
}
.match-battle-only {
visibility: hidden;
opacity: 0;
pointer-events: none;
transition:
opacity 180ms ease,
visibility 0s linear 180ms;
}
.dice-frame.is-matching .match-battle-only {
visibility: visible;
opacity: 1;
pointer-events: auto;
transition:
opacity 220ms ease,
visibility 0s linear 0s;
}
.player-copy-only {
display: none;
}
.opponent-battle-only,
.versus-only {
visibility: hidden;
opacity: 0;
pointer-events: none;
transition:
opacity 180ms ease,
visibility 0s linear 180ms;
}
.dice-frame.is-searching .opponent-battle-only,
.dice-frame.is-matching .opponent-battle-only {
visibility: visible;
opacity: 1;
pointer-events: auto;
transition:
opacity 220ms ease 760ms,
visibility 0s linear 760ms;
}
.opponent-name {
display: none;
}
.dice-frame.is-matching .opponent-name {
display: block;
}
.dice-frame.has-opponent .opponent-default-avatar {
visibility: hidden;
opacity: 0;
pointer-events: none;
}
.dice-frame.is-matching .versus-only {
visibility: visible;
opacity: 1;
pointer-events: auto;
transition:
opacity 80ms ease,
visibility 0s linear 0s;
}
.dice-frame .dice-avatar.is-empty,
.dice-frame .dice-avatar-frame.is-empty,
.dice-frame.is-searching .dice-avatar.is-empty,
.dice-frame.is-searching .dice-avatar-frame.is-empty,
.dice-frame.is-matching .dice-avatar.is-empty,
.dice-frame.is-matching .dice-avatar-frame.is-empty {
visibility: hidden;
opacity: 0;
pointer-events: none;
}
.dice-frame.is-victory .battle-only,
.dice-frame.is-victory .waiting-only,
.dice-frame.is-victory .matching-card-only,
.dice-frame.is-victory .matching-action,
.dice-frame.is-victory .searching-only,
.dice-frame.is-victory .opponent-battle-only,
.dice-frame.is-victory .dice-roll-effect,
.dice-frame.is-victory .versus-only {
visibility: hidden;
opacity: 0;
pointer-events: none;
transition:
opacity 240ms ease,
visibility 0s linear 260ms;
}
.victory-only {
visibility: hidden;
opacity: 0;
transform: translateY(18px) scale(0.98);
transition:
opacity 320ms ease 120ms,
transform 420ms cubic-bezier(0.16, 1, 0.3, 1) 120ms,
visibility 0s linear 440ms;
will-change: opacity, transform;
}
.dice-frame.is-victory .victory-only {
visibility: visible;
opacity: 1;
transform: translateY(0) scale(1);
transition:
opacity 320ms ease 120ms,
transform 420ms cubic-bezier(0.16, 1, 0.3, 1) 120ms,
visibility 0s linear 120ms;
}
.winner-avatar-mask,
.winner-avatar {
position: absolute;
top: 268px;
left: 134px;
width: 108px;
height: 108px;
border-radius: 50%;
pointer-events: none;
}
.winner-avatar-mask {
z-index: 3;
background: radial-gradient(
circle at 50% 44%,
rgba(27, 19, 36, 0.98) 0%,
rgba(13, 10, 22, 0.98) 68%,
rgba(6, 5, 15, 0.98) 100%
);
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.72);
}
.winner-avatar {
z-index: 4;
object-fit: cover;
user-select: none;
}
.winner-avatar.is-empty {
opacity: 0;
}
.victory-name {
position: absolute;
top: 392px;
left: 121px;
width: 133px;
height: 52px;
overflow: hidden;
color: #ffffff;
font-size: 24px;
line-height: 26px;
text-align: center;
word-break: break-word;
}
.victory-panel-label,
.victory-panel-amount,
.victory-panel-currency {
position: absolute;
left: 201px;
width: 65px;
color: #f7cf58;
font-weight: 700;
text-align: center;
white-space: nowrap;
}
.victory-panel-label {
top: 540px;
height: 20px;
font-size: 18px;
line-height: 20px;
}
.victory-panel-amount {
top: 566px;
height: 24px;
font-size: 24px;
line-height: 24px;
}
.victory-panel-currency {
top: 597px;
height: 20px;
font-size: 18px;
line-height: 20px;
text-transform: uppercase;
background: linear-gradient(180deg, #f9d76f 0%, #f1bb3c 95%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
@media (prefers-reduced-motion: reduce) {
.waiting-only,
.lobby-only,
.searching-only,
.matching-action,
.matching-action img,
.matching-action-text,
.battle-only,
.match-battle-only,
.opponent-battle-only,
.dice-roll-effect,
.coin-credit-flyer,
.versus-only,
.victory-only {
transition: none;
}
.dice-frame.is-matching .vs-badge,
.dice-frame.is-matching .vs-badge::after,
.dice-frame.is-self-win-compare,
.dice-frame.is-self-win-compare .you-win-smash-layer::before,
.dice-frame.is-self-win-compare .you-win-smash-floor,
.dice-frame.is-self-win-compare .you-win-impact,
.dice-frame.is-matching,
.dice-frame.is-searching .center-search-bg,
.dice-frame.is-searching .search-indicator img,
.dice-roll-effect.is-fallback::before,
.coin-credit-flyer,
.top-coin.is-crediting {
animation: none;
}
}
@keyframes screen-impact-shake {
0%,
100% {
transform: translate3d(0, 0, 0) scale(var(--dice-scale));
}
12% {
transform: translate3d(0, 9px, 0) scale(var(--dice-scale));
}
24% {
transform: translate3d(-7px, -5px, 0)
scale(var(--dice-scale));
}
36% {
transform: translate3d(6px, 4px, 0) scale(var(--dice-scale));
}
50% {
transform: translate3d(-4px, 2px, 0)
scale(var(--dice-scale));
}
66% {
transform: translate3d(3px, -2px, 0)
scale(var(--dice-scale));
}
82% {
transform: translate3d(-1px, 1px, 0)
scale(var(--dice-scale));
}
}
@keyframes vs-drop-impact {
0% {
opacity: 0;
filter: brightness(1.9) drop-shadow(0 0 34px #ff79ff);
transform: translateY(-640px) scale(4.8) rotate(-12deg);
}
34% {
opacity: 1;
filter: brightness(1.75) drop-shadow(0 0 32px #4cc9ff);
transform: translateY(-120px) scale(2.35) rotate(5deg);
}
58% {
opacity: 1;
filter: brightness(1.62) drop-shadow(0 0 28px #ffd557);
transform: translateY(22px) scale(0.78) rotate(2deg);
}
72% {
filter: brightness(1.48) drop-shadow(0 0 24px #ff79ff);
transform: translateY(-12px) scale(1.18) rotate(-1deg);
}
86% {
filter: brightness(1.18) drop-shadow(0 0 14px #4cc9ff);
transform: translateY(4px) scale(0.96) rotate(0.5deg);
}
100% {
opacity: 1;
filter: brightness(1);
transform: translateY(0) scale(1) rotate(0);
}
}
@keyframes vs-impact-ring {
0% {
opacity: 0.95;
transform: scale(0.36);
}
100% {
opacity: 0;
transform: scale(1.92);
}
}
@keyframes you-win-drop-impact {
0% {
opacity: 0;
filter: brightness(1.9) drop-shadow(0 0 36px #ff79ff);
transform: translateY(-650px) scale(3.9) rotate(-7deg);
}
34% {
opacity: 1;
filter: brightness(1.75) drop-shadow(0 0 32px #4cc9ff);
transform: translateY(-122px) scale(2.05) rotate(4deg);
}
58% {
opacity: 1;
filter: brightness(1.55) drop-shadow(0 0 30px #ffd557);
transform: translateY(28px) scale(0.78) rotate(1deg);
}
72% {
filter: brightness(1.42) drop-shadow(0 0 24px #ff79ff);
transform: translateY(-16px) scale(1.16) rotate(-0.8deg);
}
86% {
filter: brightness(1.18) drop-shadow(0 0 16px #4cc9ff);
transform: translateY(4px) scale(0.96) rotate(0.3deg);
}
100% {
opacity: 1;
filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9))
drop-shadow(0 0 22px rgba(255, 205, 46, 0.9))
drop-shadow(0 0 36px rgba(255, 77, 0, 0.78));
transform: translateY(0) scale(1) rotate(0);
}
}
@keyframes you-win-screen-smash {
0%,
100% {
transform: translate3d(0, 0, 0) scale(var(--dice-scale));
filter: none;
}
12% {
transform: translate3d(0, 14px, 0) scale(var(--dice-scale));
filter: brightness(1.22) contrast(1.08);
}
24% {
transform: translate3d(-12px, -8px, 0)
scale(var(--dice-scale));
}
36% {
transform: translate3d(10px, 7px, 0)
scale(var(--dice-scale));
}
50% {
transform: translate3d(-7px, 4px, 0)
scale(var(--dice-scale));
}
66% {
transform: translate3d(5px, -3px, 0)
scale(var(--dice-scale));
filter: brightness(1.08) contrast(1.03);
}
82% {
transform: translate3d(-2px, 1px, 0)
scale(var(--dice-scale));
}
}
@keyframes you-win-screen-flash {
0% {
opacity: 0;
}
8% {
opacity: 1;
}
22% {
opacity: 0.46;
}
100% {
opacity: 0;
}
}
@keyframes you-win-floor-impact {
0% {
opacity: 0;
transform: translateX(-50%) scale(0.38, 0.2);
}
16% {
opacity: 1;
transform: translateX(-50%) scale(1.05, 0.52);
}
58% {
opacity: 0.62;
}
100% {
opacity: 0;
transform: translateX(-50%) scale(1.32, 0.72);
}
}
@keyframes search-orbit {
0% {
transform: rotate(0deg)
translateX(var(--search-orbit-radius, 12px))
rotate(0deg);
}
100% {
transform: rotate(360deg)
translateX(var(--search-orbit-radius, 12px))
rotate(-360deg);
}
}
@keyframes matching-center-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes dice-roll-fallback {
0% {
transform: scale(0.88) rotate(0deg);
opacity: 0.78;
}
50% {
transform: scale(1.08) rotate(180deg);
opacity: 1;
}
100% {
transform: scale(0.88) rotate(360deg);
opacity: 0.78;
}
}
@keyframes coin-credit-fly {
0% {
opacity: 0;
transform: translate3d(0, 0, 0) scale(0.78) rotate(0deg);
}
12% {
opacity: 1;
}
58% {
opacity: 1;
transform: translate3d(
calc(var(--coin-credit-dx) * 0.72),
calc(var(--coin-credit-dy) * 0.58 - 34px),
0
)
scale(1.05) rotate(220deg);
}
100% {
opacity: 0;
transform: translate3d(
var(--coin-credit-dx),
var(--coin-credit-dy),
0
)
scale(0.36) rotate(520deg);
}
}
@keyframes top-coin-credit-pulse {
0% {
transform: scale(1);
filter: drop-shadow(0 0 0 rgba(255, 211, 79, 0));
}
42% {
transform: scale(1.12);
filter: drop-shadow(0 0 13px rgba(255, 211, 79, 0.86));
}
100% {
transform: scale(1);
filter: drop-shadow(0 0 0 rgba(255, 211, 79, 0));
}
}
@keyframes countdown-number-pop {
0% {
opacity: 0;
transform: scale(0.32);
filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9))
drop-shadow(0 0 8px rgba(255, 205, 46, 0.55));
}
24% {
opacity: 1;
transform: scale(1.18);
filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9))
drop-shadow(0 0 24px rgba(255, 229, 86, 1))
drop-shadow(0 0 42px rgba(255, 77, 0, 0.86));
}
70% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.86);
}
}
</style>
</head>
<body>
<div
class="dice-stage"
role="application"
aria-label="Dice match lobby"
data-i18n-aria="diceMatch.pageLabel"
>
<main
class="dice-frame is-preloading"
data-node-id="466:61"
data-name="dice"
>
<div
class="loading-screen"
data-role="loading-screen"
aria-label="Loading"
role="status"
>
<img
class="loading-bg"
src="assets/figma/loading-bg.jpg"
alt=""
/>
<div
class="loading-progress"
data-role="loading-progress"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
>
<span
class="loading-progress-bar"
data-role="loading-progress-bar"
></span>
</div>
</div>
<div
class="layer"
style="left: -47px; top: 0; width: 470px; height: 834px"
data-node-id="466:64"
data-name="arena background"
>
<img
class="layer-img cover"
src="assets/figma/bg-arena.jpg"
alt=""
/>
</div>
<div class="game-content">
<div class="top-bar" data-name="top bar">
<button
class="top-back"
type="button"
aria-label="Back"
data-node-id="466:68"
data-action="back"
>
<img
class="layer-img cover"
src="assets/figma/back-button.png"
alt=""
/>
</button>
<div
class="top-game-icon"
data-node-id="466:87"
data-name="game icon"
>
<img
class="layer-img cover"
src="assets/figma/game-icon.png"
alt=""
/>
</div>
<button
class="top-coin"
type="button"
data-name="coin balance"
data-action="recharge"
aria-label="Recharge"
>
<img src="assets/figma/coin.png" alt="" />
<span>8,000</span>
<strong class="top-coin-plus" aria-hidden="true"
>+</strong
>
</button>
<div class="language-switcher">
<button
class="language-button"
type="button"
data-language-toggle
data-i18n-aria="diceMatch.changeLanguage"
aria-label="Change language"
aria-expanded="false"
>
<span data-current-lang>EN</span>
</button>
<div
class="language-menu"
data-language-menu
hidden
>
<button type="button" data-lang-option="en">
EN
</button>
<button type="button" data-lang-option="ar">
AR
</button>
<button type="button" data-lang-option="es">
ES
</button>
</div>
</div>
</div>
<div
class="layer waiting-only lobby-only lobby-avatar-glow"
data-name="lobby avatar glow"
>
<img
class="layer-img fill"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
</div>
<div
class="layer waiting-only lobby-only lobby-player-card"
data-name="lobby player card"
>
<img
class="layer-img cover"
src="assets/figma/player-card.png"
alt=""
/>
</div>
<div
class="avatar-hole-mask avatar-hole-mask-lobby waiting-only lobby-only"
data-name="lobby avatar empty mask"
></div>
<img
class="dice-avatar dice-avatar-lobby waiting-only lobby-only is-empty"
data-role="self-avatar"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
<img
class="dice-avatar-frame dice-avatar-frame-lobby waiting-only lobby-only is-empty"
data-role="self-avatar-frame"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
<div
class="lobby-player-name waiting-only lobby-only"
data-name="lobby player name"
>
<div>NameName</div>
<div>Name...</div>
</div>
<div
class="layer battle-only match-battle-only player-copy-only"
style="
left: 41px;
top: 163px;
width: 73px;
height: 73px;
"
data-node-id="466:89"
>
<img
class="layer-img fill"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
</div>
<div
class="layer battle-only match-battle-only player-copy-only"
style="
left: 21px;
top: 146px;
width: 113px;
height: 158px;
"
data-node-id="466:90"
>
<img
class="layer-img cover"
src="assets/figma/player-card.png"
alt=""
/>
</div>
<div
class="avatar-hole-mask avatar-hole-mask-self battle-only match-battle-only"
data-name="self avatar empty mask"
></div>
<img
class="dice-avatar dice-avatar-self battle-only match-battle-only is-empty"
data-role="self-battle-avatar"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
<img
class="dice-avatar-frame dice-avatar-frame-self battle-only match-battle-only is-empty"
data-role="self-battle-avatar-frame"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
<div
class="player-name battle-only match-battle-only player-copy-only"
data-node-id="466:91"
>
<div>NameName</div>
<div>Name...</div>
</div>
<div
class="layer vs-badge battle-only versus-only"
style="
left: 120px;
top: 159px;
width: 133px;
height: 128px;
"
data-node-id="466:122"
>
<img
class="layer-img cover"
src="assets/figma/vs.png"
alt=""
/>
</div>
<div
class="center-search searching-only"
aria-hidden="true"
>
<img
class="center-search-bg"
src="assets/figma/matching-center-bg.svg"
alt=""
/>
<img
class="center-search-icon"
src="assets/figma/search-icon.svg"
alt=""
/>
</div>
<div
class="layer waiting-only"
style="
left: -51px;
top: 311px;
width: 258px;
height: 258px;
"
data-node-id="466:124"
>
<img
class="layer-img cover"
src="assets/figma/red-ring.png"
alt=""
/>
</div>
<div
class="layer waiting-only"
style="
left: 32px;
top: 400px;
width: 84px;
height: 89px;
"
data-node-id="466:125"
>
<img
class="layer-img cover"
src="assets/figma/red-dice.png"
alt=""
/>
</div>
<div
class="layer waiting-only"
style="
left: -44px;
top: 288px;
width: 244px;
height: 236px;
"
data-node-id="466:126"
>
<img
class="layer-img cover"
src="assets/figma/red-flare.png"
alt=""
/>
</div>
<div
class="layer waiting-only"
style="
left: 170px;
top: 311px;
width: 258px;
height: 258px;
"
data-node-id="466:128"
>
<img
class="layer-img cover"
src="assets/figma/blue-ring.png"
alt=""
/>
</div>
<div
class="layer waiting-only"
style="
left: 254px;
top: 400px;
width: 83px;
height: 87px;
"
data-node-id="466:129"
>
<img
class="layer-img cover"
src="assets/figma/blue-dice.png"
alt=""
/>
</div>
<div
class="layer waiting-only"
style="
left: 177px;
top: 288px;
width: 244px;
height: 236px;
"
data-node-id="466:130"
>
<img
class="layer-img cover"
src="assets/figma/blue-flare.png"
alt=""
/>
</div>
<div
class="dice-roll-effect dice-roll-effect-self"
data-role="self-roll-effect"
aria-hidden="true"
></div>
<div
class="dice-roll-effect dice-roll-effect-opponent"
data-role="opponent-roll-effect"
aria-hidden="true"
></div>
<div
class="layer battle-only opponent-battle-only"
style="
left: 262px;
top: 163px;
width: 73px;
height: 73px;
"
data-node-id="466:132"
>
<img
class="layer-img fill"
src="assets/figma/opponent-avatar-glow.svg"
alt=""
/>
</div>
<div
class="layer battle-only opponent-battle-only opponent-default-avatar"
style="
left: 262px;
top: 163px;
width: 73px;
height: 73px;
"
data-node-id="466:133"
>
<img
class="layer-img cover"
src="assets/figma/opponent-question.png"
alt=""
/>
</div>
<div
class="layer battle-only opponent-battle-only"
style="
left: 242px;
top: 146px;
width: 113px;
height: 158px;
"
data-node-id="466:134"
>
<img
class="layer-img cover"
src="assets/figma/opponent-card.png"
alt=""
/>
</div>
<img
class="dice-avatar dice-avatar-opponent battle-only opponent-battle-only is-empty"
data-role="opponent-avatar"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
<img
class="dice-avatar-frame dice-avatar-frame-opponent battle-only opponent-battle-only is-empty"
data-role="opponent-avatar-frame"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
<div
class="opponent-name battle-only opponent-battle-only"
data-role="opponent-name"
>
<div>?</div>
<div></div>
</div>
<div
class="opponent-mark matching-card-only"
data-node-id="466:135"
data-i18n="diceMatch.unknownOpponent"
>
?
</div>
<div
class="layer matching-card-only search-indicator"
style="
left: 262px;
top: 269px;
width: 18px;
height: 18px;
"
data-node-id="466:137"
>
<img
class="layer-img fill"
src="assets/figma/search-icon.svg"
alt=""
/>
</div>
<div
class="matching-label matching-card-only"
data-node-id="466:136"
data-i18n="diceMatch.matching"
>
Matching...
</div>
<div
class="layer waiting-only"
style="
left: 13px;
top: 596px;
width: 350px;
height: 78px;
"
data-node-id="466:94"
>
<img
class="layer-img fill"
src="assets/figma/prize-panel.svg"
alt=""
/>
</div>
<div
class="winner-label waiting-only"
data-node-id="466:95"
data-i18n="diceMatch.winnerReceives"
>
The winner receives
</div>
<button
class="stake waiting-only"
type="button"
style="left: 21px"
data-node-id="466:96"
data-action="stake"
data-stake="8000"
aria-label="8,000"
aria-pressed="false"
>
<img
class="stake-bg"
src="assets/figma/stake-outline.svg"
alt=""
/>
<span
class="stake-selected-slice stake-selected-left"
></span>
<span
class="stake-selected-slice stake-selected-right"
></span>
<img
class="stake-coin"
src="assets/figma/coin.png"
alt=""
/>
<span class="stake-value">8,000</span>
</button>
<button
class="stake waiting-only"
type="button"
style="left: 107px"
data-node-id="466:99"
data-action="stake"
data-stake="8000"
data-selected="true"
aria-label="8,000"
aria-pressed="true"
>
<img
class="stake-bg"
src="assets/figma/stake-fill.png"
alt=""
/>
<span
class="stake-selected-slice stake-selected-left"
></span>
<span
class="stake-selected-slice stake-selected-right"
></span>
<img
class="stake-coin"
src="assets/figma/coin.png"
alt=""
/>
<span class="stake-value">8,000</span>
</button>
<button
class="stake waiting-only"
type="button"
style="left: 192px"
data-node-id="466:97"
data-action="stake"
data-stake="8000"
aria-label="8,000"
aria-pressed="false"
>
<img
class="stake-bg"
src="assets/figma/stake-outline.svg"
alt=""
/>
<span
class="stake-selected-slice stake-selected-left"
></span>
<span
class="stake-selected-slice stake-selected-right"
></span>
<img
class="stake-coin"
src="assets/figma/coin.png"
alt=""
/>
<span class="stake-value">8,000</span>
</button>
<button
class="stake waiting-only"
type="button"
style="left: 278px"
data-node-id="466:98"
data-action="stake"
data-stake="8000"
aria-label="8,000"
aria-pressed="false"
>
<img
class="stake-bg"
src="assets/figma/stake-outline.svg"
alt=""
/>
<span
class="stake-selected-slice stake-selected-left"
></span>
<span
class="stake-selected-slice stake-selected-right"
></span>
<img
class="stake-coin"
src="assets/figma/coin.png"
alt=""
/>
<span class="stake-value">8,000</span>
</button>
<div
class="layer waiting-only"
style="
left: 335px;
top: 602px;
width: 20px;
height: 20px;
"
data-node-id="466:118"
>
<img
class="layer-img cover"
src="assets/figma/badge-help.png"
alt=""
/>
</div>
<button
class="match-button waiting-only"
type="button"
data-node-id="466:119"
data-action="match"
aria-label="Match"
data-i18n-aria="diceMatch.match"
>
<img src="assets/figma/match-button.png" alt="" />
<span data-node-id="466:121" data-i18n="diceMatch.match"
>Match</span
>
</button>
<div
class="fee-note waiting-only"
data-node-id="466:92"
style="display: none"
></div>
<div
class="matching-action"
data-node-id="466:255"
aria-live="polite"
>
<img
class="matching-action-left"
src="assets/figma/matching-left.svg"
alt=""
/>
<span
class="matching-action-text"
data-node-id="466:262"
data-i18n="diceMatch.matching"
>Matching...</span
>
<img
class="matching-action-right"
src="assets/figma/matching-right.svg"
alt=""
/>
</div>
<div
class="dice-phase-text"
data-role="phase-text"
aria-live="polite"
></div>
<div
class="you-win-smash-layer"
data-role="you-win-smash"
aria-hidden="true"
>
<span class="you-win-smash-floor"></span>
</div>
<div
class="you-win-impact"
data-role="you-win-impact"
aria-hidden="true"
>
YOU WIN
</div>
<div
class="countdown-layer"
data-role="countdown-layer"
aria-live="polite"
aria-hidden="true"
>
<div
class="countdown-number"
data-role="countdown-number"
></div>
</div>
<div
class="dice-score dice-score-self"
data-role="self-score"
></div>
<div
class="dice-score dice-score-opponent"
data-role="opponent-score"
></div>
<div
class="layer victory-only"
style="
left: 29px;
top: 90px;
width: 316px;
height: 124px;
"
data-node-id="466:297"
data-name="victory light"
>
<img
class="layer-img cover"
src="assets/figma/win-victory-light.png"
alt=""
/>
</div>
<div
class="winner-avatar-mask victory-only"
data-name="winner avatar empty mask"
></div>
<img
class="winner-avatar victory-only is-empty"
data-role="winner-avatar"
src="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E"
alt=""
/>
<div
class="layer victory-only"
style="
left: 104px;
top: 243px;
width: 167px;
height: 233px;
"
data-node-id="466:293"
>
<img
class="layer-img cover"
data-role="winner-card"
src="assets/figma/win-player-card.png"
alt=""
/>
</div>
<div
class="victory-name victory-only"
data-node-id="466:294"
>
<div>NameName</div>
<div>Name...</div>
</div>
<div
class="layer victory-only"
style="
left: 38px;
top: 528px;
width: 300px;
height: 103px;
"
data-node-id="466:299"
>
<img
class="layer-img cover"
src="assets/figma/win-reward-panel.png"
alt=""
/>
</div>
<div
class="victory-panel-label victory-only"
data-node-id="466:301"
data-i18n="diceMatch.win"
>
Win
</div>
<div
class="victory-panel-amount victory-only"
data-node-id="466:300"
>
1,900
</div>
<div
class="victory-panel-currency victory-only"
data-node-id="466:302"
data-i18n="diceMatch.coins"
>
Coins
</div>
<button
class="victory-button victory-only"
type="button"
style="
left: 12px;
top: 692px;
width: 168px;
height: 38px;
"
data-node-id="466:295"
data-action="rematch"
aria-label="Re-match"
>
<img src="assets/figma/win-rematch-button.png" alt="" />
</button>
<button
class="victory-button victory-only"
type="button"
style="
left: 193px;
top: 692px;
width: 170px;
height: 38px;
"
data-node-id="466:296"
data-action="play-again"
aria-label="Play again"
>
<img
src="assets/figma/win-play-again-button.png"
alt=""
/>
</button>
</div>
</main>
</div>
<script src="../../common/api.js"></script>
<script src="../../common/params.js"></script>
<script src="../../common/jsbridge.js"></script>
<script>
window.HyAppI18nSupported = ['en', 'ar', 'es'];
</script>
<script src="../../common/i18n.js?v=dice-lang-20260611"></script>
<script src="../../common/effect-player.js?v=dice-preload-20260611"></script>
<script src="../../common/game-preloader.js?v=game-preloader-20260611"></script>
<script>
(function () {
var DESIGN_WIDTH = 375;
var DESIGN_HEIGHT = 812;
var MAX_STAGE_WIDTH = 768;
var SETTLEMENT_AUTO_CLOSE_MS = 30000;
var DICE_ROLL_EFFECT_URL = 'assets/figma/dice-roll.svga';
var DICE_PRELOAD_ASSET_URLS = [
'assets/figma/loading-bg.jpg',
'assets/figma/amount-fill.png',
'assets/figma/back-button.png',
'assets/figma/badge-help.png',
'assets/figma/bg-arena.jpg',
'assets/figma/blue-dice.png',
'assets/figma/blue-flare.png',
'assets/figma/blue-ring.png',
'assets/figma/coin.png',
'assets/figma/dice-blue-1.png',
'assets/figma/dice-blue-2.png',
'assets/figma/dice-blue-3.png',
'assets/figma/dice-blue-4.png',
'assets/figma/dice-blue-5.png',
'assets/figma/dice-blue-6.png',
'assets/figma/dice-red-1.png',
'assets/figma/dice-red-2.png',
'assets/figma/dice-red-3.png',
'assets/figma/dice-red-4.png',
'assets/figma/dice-red-5.png',
'assets/figma/dice-red-6.png',
DICE_ROLL_EFFECT_URL,
'assets/figma/game-icon.png',
'assets/figma/home-indicator.svg',
'assets/figma/mask-bg.svg',
'assets/figma/match-button.png',
'assets/figma/matching-center-bg.svg',
'assets/figma/matching-center.svg',
'assets/figma/matching-left.svg',
'assets/figma/matching-right.svg',
'assets/figma/opponent-avatar-glow.svg',
'assets/figma/opponent-card.png',
'assets/figma/opponent-question.png',
'assets/figma/player-avatar-glow.png',
'assets/figma/player-card.png',
'assets/figma/prize-panel.svg',
'assets/figma/red-dice.png',
'assets/figma/red-flare.png',
'assets/figma/red-ring.png',
'assets/figma/search-icon.svg',
'assets/figma/stake-fill.png',
'assets/figma/stake-mask-left.svg',
'assets/figma/stake-mask-right.svg',
'assets/figma/stake-outline.svg',
'assets/figma/status-battery.svg',
'assets/figma/status-cellular.svg',
'assets/figma/status-frame.svg',
'assets/figma/status-wifi.svg',
'assets/figma/vs.png',
'assets/figma/win-avatar-glow.png',
'assets/figma/win-play-again-button.png',
'assets/figma/win-player-card.png',
'assets/figma/win-rematch-button.png',
'assets/figma/win-reward-panel.png',
'assets/figma/win-victory-light.png',
'assets/figma/winner-text-fill.png',
];
var root = document.documentElement;
var transparentPixel =
'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E';
var frame = document.querySelector('.dice-frame');
var timers = [];
var pollTimer = 0;
var roundSequence = 0;
var pageStartedAt = performance.now();
var coinCreditFrame = 0;
var coinCreditLayer = null;
var rollEffectPlayers = {
self: null,
opponent: null,
};
var state = {
preloading: true,
profile: {},
appearance: {},
balanceCoin: 0,
config: null,
selectedStake: 0,
match: null,
busy: false,
roundStarted: false,
serverTimeMS: 0,
};
function trackSelfGameEvent(name, extra) {
var api =
window.HyAppAPI &&
window.HyAppAPI.game &&
window.HyAppAPI.game.reportEvent;
if (typeof api !== 'function') return;
var payload = Object.assign(
{
event_name: name,
game_id: 'dice',
page: 'game/touzi',
h5_version: 'dice-h5-20260611',
entry_source: currentRoomID() ? 'room' : 'direct',
success: true,
},
extra || {}
);
api(payload).catch(function () {});
}
function updateScale() {
// 游戏区域宽度始终吃满可用宽度,但最多放大到 768px缩放只跟最终宽度走避免大屏时被高度压窄后露出右侧空条。
var viewportWidth = Math.min(
window.innerWidth,
MAX_STAGE_WIDTH
);
var scale = viewportWidth / DESIGN_WIDTH;
root.style.setProperty(
'--dice-scale',
String(Math.max(scale, 0.1))
);
}
function postBridge(action, payload) {
if (window.HyAppBridge && window.HyAppBridge.post) {
window.HyAppBridge.post(action, payload || {});
}
}
function openRecharge() {
var payload = { source: 'dice', game_code: 'touzi' };
if (
window.HyAppBridge &&
typeof window.HyAppBridge.recharge === 'function'
) {
window.HyAppBridge.recharge(payload);
return;
}
postBridge('gameRecharge', payload);
postBridge('recharge', payload);
}
function isInsufficientBalanceError(error) {
var code = String(
(error && error.code) || ''
).toUpperCase();
var message = String(
(error && error.message) || ''
).toLowerCase();
return (
code === 'INSUFFICIENT_BALANCE' ||
message === 'insufficient balance'
);
}
function handleInsufficientBalance() {
// 余额不足需要马上让用户补币:先给固定英文提示,再拉起 Flutter 支付页桥接。
showMessage('insufficient balance');
openRecharge();
}
function setLoadingProgress(progress) {
window.HyAppGamePreloader.setProgress({
root: root,
progress: progress,
});
}
function completeLoadingScreen() {
state.preloading = false;
setMatchButtonBusy(false, matchText());
window.HyAppGamePreloader.complete({
root: root,
frame: frame,
loadingScreen: role('loading-screen'),
progressNode: role('loading-progress'),
});
}
function collectDicePreloadURLs() {
return window.HyAppGamePreloader.collectResourceURLs(
DICE_PRELOAD_ASSET_URLS
);
}
function preloadResourceOnce(url) {
return window.HyAppGamePreloader.preloadResource(url, {
svga: {
svgaScript: '../../common/vendor/svga.min.js',
waitForWindowLoad: false,
},
});
}
function preloadLobbyDataImages(lobbyData) {
var urls = [];
var seen = {};
var profile = (lobbyData && lobbyData.profile) || {};
var appearance = (lobbyData && lobbyData.appearance) || {};
window.HyAppGamePreloader.addURL(
urls,
seen,
avatarURL(profile)
);
window.HyAppGamePreloader.addURL(
urls,
seen,
frameURL(appearance.avatar_frame || {})
);
if (!urls.length) return Promise.resolve();
return Promise.all(
urls.map(function (url) {
return window.HyAppGamePreloader.preloadImage(url);
})
);
}
function runPreloadTasks(tasks) {
return window.HyAppGamePreloader.runTasks(
tasks,
setLoadingProgress
);
}
function each(nodes, iteratee) {
Array.prototype.forEach.call(nodes, iteratee);
}
function schedule(callback, ms) {
var timer = window.setTimeout(function () {
timers = timers.filter(function (item) {
return item !== timer;
});
callback();
}, ms);
timers.push(timer);
return timer;
}
function delay(ms) {
return new Promise(function (resolve) {
schedule(resolve, ms);
});
}
function clearPollTimer() {
if (!pollTimer) return;
window.clearTimeout(pollTimer);
pollTimer = 0;
}
function clearMatchTimers() {
clearPollTimer();
timers.forEach(function (timer) {
window.clearTimeout(timer);
});
timers = [];
}
function api() {
return window.HyAppAPI || {};
}
function diceAPI() {
return api().game && api().game.dice;
}
function role(name) {
return document.querySelector('[data-role="' + name + '"]');
}
function rollEffectNode(side) {
return role(
side === 'opponent'
? 'opponent-roll-effect'
: 'self-roll-effect'
);
}
function createRollEffectPlayer(side) {
var node = rollEffectNode(side);
if (!node || !window.HyAppEffectPlayer) return null;
if (!rollEffectPlayers[side]) {
rollEffectPlayers[side] =
window.HyAppEffectPlayer.create(node, {
activeClass: 'is-playing',
fit: 'contain',
loop: true,
svgaScript: '../../common/vendor/svga.min.js',
waitForWindowLoad: false,
});
}
return rollEffectPlayers[side];
}
function playSingleRollEffect(side) {
var node = rollEffectNode(side);
var player = createRollEffectPlayer(side);
if (!node) return Promise.resolve();
node.classList.remove('is-fallback');
if (!player) {
node.classList.add('is-fallback');
return Promise.resolve();
}
player.stop();
return player
.play(DICE_ROLL_EFFECT_URL, {
type: 'svga',
waitForWindowLoad: false,
})
.catch(function () {
node.innerHTML = '';
node.classList.remove('is-playing');
node.classList.add('is-fallback');
});
}
function playRollEffects() {
return Promise.all([
playSingleRollEffect('self'),
playSingleRollEffect('opponent'),
]);
}
function stopSingleRollEffect(side) {
var node = rollEffectNode(side);
var player = rollEffectPlayers[side];
if (player) player.stop();
if (node) {
node.innerHTML = '';
node.classList.remove('is-playing', 'is-fallback');
}
}
function stopRollEffects() {
stopSingleRollEffect('self');
stopSingleRollEffect('opponent');
}
function setText(node, value) {
if (node) node.textContent = value == null ? '' : value;
}
function i18nText(key, fallback) {
return window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback;
}
function i18nFormat(key, fallback, values) {
var text = i18nText(key, fallback);
return text.replace(
/\{([a-zA-Z0-9_]+)\}/g,
function (match, name) {
return Object.prototype.hasOwnProperty.call(
values || {},
name
)
? values[name]
: match;
}
);
}
function matchText() {
return i18nText('diceMatch.match', 'Match');
}
function matchingText() {
return i18nText('diceMatch.matching', 'Matching...');
}
function setPhaseText(value) {
setText(role('phase-text'), value || '');
}
function setCountdownNumber(value) {
var node = role('countdown-number');
var layer = role('countdown-layer');
var next = value == null ? '' : String(value);
if (layer) {
layer.setAttribute(
'aria-hidden',
next ? 'false' : 'true'
);
}
if (!node) return;
if (
(node.getAttribute('data-countdown-value') || '') ===
next
) {
return;
}
if (next) {
node.setAttribute('data-countdown-value', next);
} else {
node.removeAttribute('data-countdown-value');
}
node.textContent = next;
node.classList.remove('is-ticking');
if (!next) return;
// 重新触发每一跳的弹出动画,保证 3、2、1 都是独立的一次视觉反馈。
void node.offsetWidth;
node.classList.add('is-ticking');
}
function setActionText(value) {
setText(
document.querySelector('.matching-action-text'),
value || matchingText()
);
}
function showMessage(message) {
var text =
message ||
i18nText('diceMatch.requestFailed', 'Request failed');
if (window.HyAppToast && window.HyAppToast.show) {
window.HyAppToast.show(text);
return;
}
setPhaseText(text);
}
function formatCoin(value) {
var number = Number(value || 0);
if (!Number.isFinite(number)) number = 0;
return Math.trunc(number).toLocaleString('en-US');
}
function formatTopCoin(value) {
var number = Number(value || 0);
if (!Number.isFinite(number)) number = 0;
number = Math.max(0, Math.trunc(number));
if (number < 1000000) return formatCoin(number);
return Math.floor(number / 1000) + 'K';
}
function configBPSValue(config, fields) {
if (!config) return null;
for (var i = 0; i < fields.length; i += 1) {
if (
!Object.prototype.hasOwnProperty.call(
config,
fields[i]
)
) {
continue;
}
var raw = config[fields[i]];
if (raw === null || raw === undefined || raw === '') {
continue;
}
var value = Number(raw);
if (Number.isFinite(value)) return value;
}
return null;
}
function configTicketPercent(config) {
var feeBPS = configBPSValue(config, ['fee_bps', 'feeBps']);
var poolBPS = configBPSValue(config, [
'pool_bps',
'poolBps',
]);
if (feeBPS === null && poolBPS === null) return null;
return (
Math.max(
0,
Math.min(10000, (feeBPS || 0) + (poolBPS || 0))
) / 100
);
}
function renderFeeNote(config) {
var note = document.querySelector('.fee-note');
if (!note) return;
var ticketPercent = configTicketPercent(config);
// 费率必须来自后端配置;没有明确字段时隐藏,避免把历史兜底 5% 当成真实规则展示。
if (ticketPercent === null) {
setText(note, '');
note.style.display = 'none';
return;
}
setText(
note,
i18nFormat(
'diceMatch.configFeeNote',
'Tickets {fee}%',
{
fee: ticketPercent,
}
)
);
note.style.display = '';
}
function setCoin(value) {
var number = Number(value || 0);
if (!Number.isFinite(number)) number = 0;
number = Math.max(0, Math.trunc(number));
state.balanceCoin = number;
var fullLabel = formatCoin(number);
var topCoin = document.querySelector('.top-coin');
if (topCoin) {
topCoin.classList.toggle(
'is-compact',
number >= 1000000
);
topCoin.setAttribute('title', fullLabel);
topCoin.setAttribute(
'aria-label',
'Recharge ' + fullLabel
);
}
setText(
document.querySelector('.top-coin span'),
formatTopCoin(number)
);
}
function cancelCoinCreditAnimation() {
if (coinCreditFrame) {
window.cancelAnimationFrame(coinCreditFrame);
coinCreditFrame = 0;
}
if (coinCreditLayer && coinCreditLayer.parentNode) {
coinCreditLayer.parentNode.removeChild(coinCreditLayer);
}
coinCreditLayer = null;
var topCoin = document.querySelector('.top-coin');
if (topCoin) topCoin.classList.remove('is-crediting');
}
function framePointFromRect(rect, anchorX, anchorY) {
var frameRect = frame.getBoundingClientRect();
var scale = frameRect.width / DESIGN_WIDTH || 1;
return {
x:
(rect.left +
rect.width * anchorX -
frameRect.left) /
scale,
y:
(rect.top + rect.height * anchorY - frameRect.top) /
scale,
};
}
function createCoinCreditLayer() {
cancelCoinCreditAnimation();
coinCreditLayer = document.createElement('div');
coinCreditLayer.className = 'coin-credit-layer';
frame.appendChild(coinCreditLayer);
return coinCreditLayer;
}
function burstCreditCoins(start, end) {
var layer = createCoinCreditLayer();
var offsets = [
[-8, 6],
[12, -4],
[3, 16],
[20, 10],
[-16, -3],
[8, -17],
[-2, -12],
[17, 22],
];
offsets.forEach(function (offset, index) {
var coin = document.createElement('img');
coin.className = 'coin-credit-flyer';
coin.src = 'assets/figma/coin.png';
coin.alt = '';
coin.draggable = false;
coin.setAttribute('aria-hidden', 'true');
coin.style.setProperty(
'--coin-credit-x',
start.x + offset[0] + 'px'
);
coin.style.setProperty(
'--coin-credit-y',
start.y + offset[1] + 'px'
);
coin.style.setProperty(
'--coin-credit-dx',
end.x - start.x - offset[0] + 'px'
);
coin.style.setProperty(
'--coin-credit-dy',
end.y - start.y - offset[1] + 'px'
);
coin.style.setProperty(
'--coin-credit-delay',
index * 64 + 'ms'
);
layer.appendChild(coin);
});
schedule(function () {
if (coinCreditLayer === layer) {
layer.remove();
coinCreditLayer = null;
}
}, 1500);
}
function animateCoinNumber(startValue, targetValue) {
var start = Math.max(
0,
Math.trunc(Number(startValue) || 0)
);
var target = Math.max(
0,
Math.trunc(Number(targetValue) || 0)
);
var startedAt = performance.now();
var duration = 1180;
function tick(now) {
var progress = Math.min(
Math.max((now - startedAt) / duration, 0),
1
);
var eased = 1 - Math.pow(1 - progress, 3);
setCoin(start + (target - start) * eased);
if (progress < 1) {
coinCreditFrame =
window.requestAnimationFrame(tick);
return;
}
coinCreditFrame = 0;
setCoin(target);
}
if (coinCreditFrame) {
window.cancelAnimationFrame(coinCreditFrame);
}
coinCreditFrame = window.requestAnimationFrame(tick);
}
function playVictoryCoinCredit(startValue, targetValue) {
var topCoin = document.querySelector('.top-coin');
var topCoinIcon = topCoin && topCoin.querySelector('img');
if (!topCoinIcon || targetValue <= startValue) {
setCoin(targetValue);
return false;
}
var start = { x: 95, y: 584 };
var end = framePointFromRect(
topCoinIcon.getBoundingClientRect(),
0.5,
0.5
);
burstCreditCoins(start, end);
animateCoinNumber(startValue, targetValue);
topCoin.classList.remove('is-crediting');
void topCoin.offsetWidth;
topCoin.classList.add('is-crediting');
schedule(function () {
topCoin.classList.remove('is-crediting');
}, 780);
return true;
}
function displayName(user) {
user = user || {};
return (
user.nickname ||
user.username ||
user.name ||
user.display_user_id ||
user.user_id ||
'NameName'
);
}
function displayID(user) {
user = user || {};
return user.display_user_id || user.user_id || '';
}
function setNameGroup(selector, user) {
var nodes = document.querySelectorAll(selector + ' div');
var name = displayName(user);
var sub = displayID(user);
if (nodes[0]) nodes[0].textContent = name;
if (nodes[1]) nodes[1].textContent = sub || name;
}
function avatarURL(user) {
user = user || {};
var url =
user.avatar || user.avatar_url || user.avatarUrl || '';
url = String(url || '').trim();
if (!url) return '';
var normalized = url.toLowerCase();
if (
normalized.indexOf('/avatar/default') !== -1 ||
normalized.indexOf('avatar/default.') !== -1 ||
normalized.indexOf('/default-avatar') !== -1 ||
normalized.indexOf('default_avatar') !== -1 ||
normalized.indexOf('cdn.example.com/avatar/default') !==
-1
) {
return '';
}
return url;
}
function frameURL(frameData) {
frameData = frameData || {};
return (
frameData.asset_url ||
frameData.preview_url ||
frameData.animation_url ||
frameData.url ||
''
);
}
function setImage(node, url, fallbackURL) {
if (!node) return;
var nextURL = url || fallbackURL || '';
if (nextURL) {
node.onerror = function () {
node.onerror = null;
node.src = transparentPixel;
node.classList.add('is-empty');
};
node.src = nextURL;
node.classList.remove('is-empty');
} else {
node.onerror = null;
node.src = transparentPixel;
node.classList.add('is-empty');
}
}
function setFrame(node, frameData) {
var url = frameURL(frameData);
setImage(node, url, '');
if (url) {
node.classList.add('has-frame');
node.classList.remove('is-empty');
} else {
node.classList.remove('has-frame');
node.classList.add('is-empty');
}
}
function renderSelfProfile() {
var profile = state.profile || {};
var avatar = avatarURL(profile);
var frameData = state.appearance.avatar_frame || {};
setNameGroup('.lobby-player-name', profile);
setNameGroup('.player-name', profile);
setImage(role('self-avatar'), avatar, '');
setImage(role('self-battle-avatar'), avatar, '');
setFrame(role('self-avatar-frame'), frameData);
setFrame(role('self-battle-avatar-frame'), frameData);
}
function balanceFromResponse(data) {
var balances = (data && data.balances) || [];
for (var i = 0; i < balances.length; i += 1) {
if (balances[i].asset_type === 'COIN') {
return balances[i].available_amount || 0;
}
}
return 0;
}
function enabledStakeOptions(config) {
var options = (config && config.stake_options) || [];
return options
.filter(function (item) {
return (
item &&
item.enabled !== false &&
item.stake_coin > 0
);
})
.sort(function (left, right) {
return (
Number(left.sort_order || 0) -
Number(right.sort_order || 0) ||
Number(left.stake_coin || 0) -
Number(right.stake_coin || 0)
);
});
}
function selectStake(button) {
if (!button) return;
each(document.querySelectorAll('.stake'), function (stake) {
var selected = stake === button;
var img = stake.querySelector('img');
if (selected) {
stake.setAttribute('data-selected', 'true');
state.selectedStake = Number(
stake.getAttribute('data-stake') || 0
);
} else {
stake.removeAttribute('data-selected');
}
stake.setAttribute(
'aria-pressed',
selected ? 'true' : 'false'
);
if (img) {
img.src = selected
? 'assets/figma/stake-fill.png'
: 'assets/figma/stake-outline.svg';
}
});
}
function renderStakeOptions(config) {
var options = enabledStakeOptions(config);
if (!options.length) {
selectStake(
document.querySelector(
'.stake[data-selected="true"]'
) || document.querySelector('.stake')
);
return;
}
var selected = state.selectedStake;
var fallbackSelected = options[0] && options[0].stake_coin;
var selectedButton = null;
each(
document.querySelectorAll('.stake'),
function (button, index) {
var option = options[index];
if (!option) {
button.style.display = 'none';
return;
}
button.style.display = '';
button.setAttribute(
'data-stake',
String(option.stake_coin)
);
button.setAttribute(
'aria-label',
formatCoin(option.stake_coin)
);
setText(
button.querySelector('.stake-value'),
formatCoin(option.stake_coin)
);
if (option.stake_coin === selected) {
selectedButton = button;
}
if (
!selectedButton &&
option.stake_coin === fallbackSelected
) {
selectedButton = button;
}
}
);
selectStake(
selectedButton || document.querySelector('.stake')
);
renderFeeNote(config);
}
function resetFrame() {
stopRollEffects();
cancelCoinCreditAnimation();
frame.classList.remove(
'is-searching',
'is-matching',
'is-countdown',
'is-victory',
'is-self-win-compare',
'has-scores'
);
setCountdownNumber('');
}
function setMatchButtonBusy(busy, label) {
var button = document.querySelector('.match-button');
if (!button) return;
button.disabled = !!busy;
setText(button.querySelector('span'), label || matchText());
}
function currentRoomID() {
var params =
window.HyAppParams &&
window.HyAppParams.current &&
window.HyAppParams.current.raw;
if (params && params.get) {
return (
params.get('room_id') || params.get('roomId') || ''
);
}
return (
new URLSearchParams(window.location.search).get(
'room_id'
) || ''
);
}
function selfUserID() {
return String(
(state.profile && state.profile.user_id) || ''
);
}
function participantUser(participant) {
participant = participant || {};
var user = Object.assign(
{
user_id: participant.user_id || '',
display_user_id: participant.display_user_id || '',
nickname:
participant.nickname ||
participant.username ||
'',
username: participant.username || '',
avatar:
participant.avatar ||
participant.avatar_url ||
participant.avatarUrl ||
'',
},
participant.user || {}
);
if (!user.user_id) user.user_id = participant.user_id || '';
if (!user.display_user_id) {
user.display_user_id =
participant.display_user_id || '';
}
if (!user.nickname && participant.nickname) {
user.nickname = participant.nickname;
}
if (!user.username && participant.username) {
user.username = participant.username;
}
if (!user.avatar) {
user.avatar =
participant.avatar ||
participant.avatar_url ||
participant.avatarUrl ||
'';
}
return user;
}
function findSelfParticipant(match) {
var participants = (match && match.participants) || [];
var id = selfUserID();
for (var i = 0; i < participants.length; i += 1) {
if (String(participants[i].user_id || '') === id) {
return participants[i];
}
}
for (var j = 0; j < participants.length; j += 1) {
if (!participants[j].is_robot) return participants[j];
}
return participants[0] || null;
}
function findOpponentParticipant(match, selfParticipant) {
var participants = (match && match.participants) || [];
for (var i = 0; i < participants.length; i += 1) {
if (participants[i] !== selfParticipant) {
return participants[i];
}
}
return null;
}
function sameParticipant(left, right) {
if (!left || !right) return false;
var leftID = String(left.user_id || '');
var rightID = String(right.user_id || '');
return leftID !== '' && leftID === rightID;
}
function findWinnerParticipant(match) {
var participants = (match && match.participants) || [];
for (var i = 0; i < participants.length; i += 1) {
if (
participants[i] &&
participants[i].result === 'win'
) {
return participants[i];
}
}
return null;
}
function dicePointValue(participant) {
var points = (participant && participant.dice_points) || [];
if (!points.length) return '';
var point = Number(points[0] || 0);
if (!isFinite(point) || point < 1 || point > 6) return '';
return String(Math.floor(point));
}
function diceScoreImage(point, side) {
if (!point) return '';
return (
'assets/figma/dice-' +
(side === 'self' ? 'red' : 'blue') +
'-' +
point +
'.png'
);
}
function setDiceScore(node, point, side) {
if (!node) return;
node.textContent = '';
if (!point) {
node.removeAttribute('data-score-value');
return;
}
node.setAttribute('data-score-value', point);
var img = document.createElement('img');
img.src = diceScoreImage(point, side);
img.alt = '';
img.draggable = false;
img.setAttribute('aria-hidden', 'true');
node.appendChild(img);
}
function isDrawReroll(match) {
return (
match &&
match.result === 'draw' &&
match.status !== 'settled'
);
}
function isSelfWinner(match) {
var self = findSelfParticipant(match);
var winner = findWinnerParticipant(match);
if (!self) return false;
if (winner && sameParticipant(winner, self)) return true;
return self.result === 'win';
}
function setSelfWinCompare(visible) {
frame.classList.remove('is-self-win-compare');
if (!visible) return;
// 每一局 Compare 都要重新播放“砸下”效果,因此先读一次布局强制浏览器提交移除状态。
void frame.offsetWidth;
frame.classList.add('is-self-win-compare');
}
function renderMatchParticipants(match) {
var self = findSelfParticipant(match);
var opponent = findOpponentParticipant(match, self);
if (self && self.user) {
setNameGroup('.player-name', participantUser(self));
}
if (opponent) {
var opponentUser = participantUser(opponent);
var opponentAvatar = avatarURL(opponentUser);
var hasOpponentProfile =
displayName(opponentUser) !== '?' ||
!!opponentAvatar ||
!!displayID(opponentUser);
frame.classList.toggle(
'has-opponent',
hasOpponentProfile
);
setText(
document.querySelector('.opponent-mark'),
displayName(opponentUser)
);
setNameGroup(
'[data-role="opponent-name"]',
opponentUser
);
setText(
document.querySelector('.matching-label'),
i18nText('diceMatch.ready', 'Ready')
);
setImage(role('opponent-avatar'), opponentAvatar, '');
setFrame(
role('opponent-avatar-frame'),
opponent.avatar_frame || {}
);
} else {
setText(document.querySelector('.opponent-mark'), '?');
setText(
document.querySelector('.matching-label'),
matchingText()
);
setNameGroup('[data-role="opponent-name"]', {
nickname: '?',
});
frame.classList.remove('has-opponent');
setImage(role('opponent-avatar'), '', '');
setFrame(role('opponent-avatar-frame'), {});
}
var selfScore = dicePointValue(self);
var opponentScore = dicePointValue(opponent);
setDiceScore(role('self-score'), selfScore, 'self');
setDiceScore(
role('opponent-score'),
opponentScore,
'opponent'
);
if (selfScore && opponentScore) {
frame.classList.add('has-scores');
} else {
frame.classList.remove('has-scores');
}
}
function matchFromData(data) {
state.serverTimeMS = Number(
(data && data.server_time_ms) || 0
);
return (data && data.match) || data || {};
}
function isWaitingMatch(match) {
if (!match) return false;
return (
match.phase === 'waiting' ||
(match.status === 'waiting' &&
Number(match.current_players || 0) <
Number(match.min_players || 2))
);
}
function phaseDeadlineLeft(match) {
var deadline = Number(
(match && match.phase_deadline_ms) || 0
);
var serverTime = state.serverTimeMS || Date.now();
if (!deadline) return 3000;
return Math.max(deadline - serverTime, 0);
}
function pollMatch(matchID) {
clearPollTimer();
pollTimer = window.setTimeout(function () {
pollTimer = 0;
var game = diceAPI();
if (!game || !matchID || !state.busy) return;
game.getMatch(matchID)
.then(handleMatchData)
.catch(function (error) {
showMessage(error.message);
schedule(function () {
pollMatch(matchID);
}, 1000);
});
}, 1000);
}
function enterWaiting(match) {
state.match = match;
state.roundStarted = false;
resetFrame();
frame.classList.add('is-searching');
setActionText(matchingText());
setPhaseText(matchingText());
setMatchButtonBusy(true, matchingText());
renderMatchParticipants(match);
pollMatch(match.match_id);
}
function runCountdown(match, sequence) {
var duration = Math.min(phaseDeadlineLeft(match), 3000);
var endAt = Date.now() + Math.max(duration, 0);
function tick() {
if (sequence !== roundSequence) return;
var left = Math.max(endAt - Date.now(), 0);
if (left <= 0) {
frame.classList.remove('is-countdown');
setCountdownNumber('');
setPhaseText(i18nText('diceMatch.roll', 'Roll'));
rollAndAnimate(sequence);
return;
}
setPhaseText('');
setCountdownNumber(Math.ceil(left / 1000));
schedule(tick, 160);
}
tick();
}
function beginRound(match) {
if (state.roundStarted) {
renderMatchParticipants(match);
return;
}
clearPollTimer();
state.roundStarted = true;
state.busy = true;
state.match = match;
roundSequence += 1;
resetFrame();
frame.classList.add('is-matching', 'is-countdown');
setMatchButtonBusy(true, matchText());
renderMatchParticipants(match);
postBridge('gameMatchReady', {
game_code: 'touzi',
match_id: match.match_id,
});
runCountdown(match, roundSequence);
}
function rollAndAnimate(sequence) {
var game = diceAPI();
var matchID = state.match && state.match.match_id;
if (!game || !matchID) {
showMessage(
i18nText(
'diceMatch.apiUnavailable',
'Dice API unavailable'
)
);
resetToLobby(false);
return;
}
frame.classList.remove('is-countdown');
setCountdownNumber('');
setPhaseText(i18nText('diceMatch.rolling', 'Rolling...'));
frame.classList.remove('has-scores');
playRollEffects();
trackSelfGameEvent('reveal_enter', {
match_id: matchID,
});
postBridge('gameDiceRolling', {
game_code: 'touzi',
match_id: matchID,
});
var rollPromise = game
.roll(matchID)
.then(function (data) {
trackSelfGameEvent('roll_api', {
match_id: matchID,
success: true,
});
return matchFromData(data);
})
.catch(function () {
trackSelfGameEvent('roll_api', {
match_id: matchID,
success: false,
error_code: 'roll_failed',
});
return game.getMatch(matchID).then(function (data) {
return matchFromData(data);
});
});
Promise.all([rollPromise, delay(3000)])
.then(function (results) {
if (sequence !== roundSequence) {
stopRollEffects();
return;
}
var match = results[0];
state.match = match;
stopRollEffects();
renderMatchParticipants(match);
frame.classList.add('has-scores');
var drawReroll = isDrawReroll(match);
var selfWinCompare =
!drawReroll && isSelfWinner(match);
setPhaseText(
selfWinCompare
? ''
: drawReroll
? i18nText('diceMatch.draw', 'Draw')
: i18nText('diceMatch.compare', 'Compare')
);
setSelfWinCompare(selfWinCompare);
if (drawReroll) {
return delay(2000).then(function () {
if (sequence !== roundSequence) return;
rollAndAnimate(sequence);
});
}
return delay(3000).then(function () {
showSettlement(match, sequence);
});
})
.catch(function (error) {
stopRollEffects();
if (isInsufficientBalanceError(error)) {
resetToLobby(false);
handleInsufficientBalance(error);
return;
}
showMessage(error.message);
resetToLobby(false);
});
}
function renderVictory(match) {
var self = findSelfParticipant(match);
var winner = findWinnerParticipant(match) || self;
var winnerIsSelf = sameParticipant(winner, self);
var winnerUser = winnerIsSelf
? Object.assign(
{},
participantUser(winner),
state.profile || {}
)
: participantUser(winner);
var winnerCard = role('winner-card');
if (winnerCard) {
winnerCard.src = winnerIsSelf
? 'assets/figma/win-player-card.png'
: 'assets/figma/opponent-card.png';
}
setImage(role('winner-avatar'), avatarURL(winnerUser), '');
setNameGroup('.victory-name', winnerUser);
setText(
document.querySelector('.victory-panel-label'),
i18nText('diceMatch.win', 'Win')
);
setText(
document.querySelector('.victory-panel-amount'),
formatCoin((winner && winner.payout_coin) || 0)
);
if (self && self.balance_after > 0) {
var balanceAfter = Math.trunc(
Number(self.balance_after) || 0
);
if (winnerIsSelf && balanceAfter > state.balanceCoin) {
return playVictoryCoinCredit(
state.balanceCoin,
balanceAfter
);
}
setCoin(balanceAfter);
}
return false;
}
function refreshBalance() {
if (!api().wallet || !api().wallet.balances) return;
api()
.wallet.balances('COIN')
.then(function (data) {
setCoin(balanceFromResponse(data));
})
.catch(function () {});
}
function showSettlement(match, sequence) {
if (sequence !== roundSequence) return;
state.match = match;
renderMatchParticipants(match);
resetFrame();
frame.classList.add('is-victory');
if (!renderVictory(match)) refreshBalance();
trackSelfGameEvent('settlement_show', {
match_id: match.match_id,
result: match.result,
});
postBridge('gameDiceSettled', {
game_code: 'touzi',
match_id: match.match_id,
result: match.result,
});
delay(SETTLEMENT_AUTO_CLOSE_MS).then(function () {
if (sequence === roundSequence) resetToLobby(true);
});
}
function handleMatchData(data) {
var match = matchFromData(data);
if (!match || !match.match_id) return;
state.match = match;
renderMatchParticipants(match);
if (
match.status === 'settled' ||
match.phase === 'settled'
) {
roundSequence += 1;
showSettlement(match, roundSequence);
return;
}
if (
match.status === 'canceled' ||
match.phase === 'canceled'
) {
resetToLobby(true);
return;
}
if (isWaitingMatch(match)) {
enterWaiting(match);
return;
}
beginRound(match);
}
function selectedStakeCoin() {
var selected = document.querySelector(
'.stake[data-selected="true"]'
);
return Number(
(selected && selected.getAttribute('data-stake')) ||
state.selectedStake ||
0
);
}
function startMatch() {
var game = diceAPI();
var stake = selectedStakeCoin();
if (!game) {
showMessage(
i18nText(
'diceMatch.apiUnavailable',
'Dice API unavailable'
)
);
return;
}
if (!stake) {
showMessage(
i18nText('diceMatch.selectStake', 'Select stake')
);
return;
}
clearMatchTimers();
state.busy = true;
state.roundStarted = false;
state.match = null;
roundSequence += 1;
resetFrame();
frame.classList.add('is-searching');
setActionText(matchingText());
setPhaseText(matchingText());
setMatchButtonBusy(true, matchingText());
postBridge('gameMatchRequested', {
game_code: 'touzi',
stake: stake,
});
trackSelfGameEvent('match_click', {
stake_coin: stake,
});
var matchStartedAt = performance.now();
game.match({
game_id: 'dice',
room_id: currentRoomID(),
stake_coin: stake,
})
.then(function (data) {
var match = matchFromData(data);
trackSelfGameEvent('match_success', {
match_id: match && match.match_id,
stake_coin: stake,
duration_ms: Math.round(
performance.now() - matchStartedAt
),
});
handleMatchData(data);
})
.catch(function (error) {
trackSelfGameEvent('match_api', {
stake_coin: stake,
duration_ms: Math.round(
performance.now() - matchStartedAt
),
success: false,
error_code: error && error.message,
});
if (isInsufficientBalanceError(error)) {
resetToLobby(true);
handleInsufficientBalance(error);
return;
}
showMessage(error.message);
resetToLobby(true);
});
}
function cancelWaitingMatch() {
var game = diceAPI();
var match = state.match;
if (
!game ||
!match ||
!match.match_id ||
!isWaitingMatch(match)
) {
return Promise.resolve();
}
return game
.cancel(match.match_id)
.then(function () {
trackSelfGameEvent('match_cancel', {
match_id: match.match_id,
});
})
.catch(function () {
trackSelfGameEvent('cancel_api', {
match_id: match.match_id,
success: false,
error_code: 'cancel_failed',
});
});
}
function resetToLobby(keepStake) {
clearMatchTimers();
state.busy = false;
state.roundStarted = false;
state.match = null;
roundSequence += 1;
resetFrame();
setPhaseText('');
setActionText(matchingText());
setDiceScore(role('self-score'), '', 'self');
setDiceScore(role('opponent-score'), '', 'opponent');
setText(document.querySelector('.opponent-mark'), '?');
setText(
document.querySelector('.matching-label'),
matchingText()
);
setNameGroup('[data-role="opponent-name"]', {
nickname: '?',
});
frame.classList.remove('has-opponent');
setImage(role('opponent-avatar'), '', '');
setFrame(role('opponent-avatar-frame'), {});
setMatchButtonBusy(false, matchText());
if (!keepStake) state.selectedStake = 0;
renderStakeOptions(state.config);
}
function refreshLocalizedRuntimeText() {
renderStakeOptions(state.config);
if (state.match) {
renderMatchParticipants(state.match);
}
if (frame.classList.contains('is-victory')) {
renderVictory(state.match || {});
}
if (frame.classList.contains('is-searching')) {
setActionText(matchingText());
setPhaseText(matchingText());
setMatchButtonBusy(true, matchingText());
return;
}
if (state.preloading) {
setMatchButtonBusy(
true,
i18nText('diceMatch.loading', 'Loading')
);
return;
}
if (!state.busy) {
setActionText(matchingText());
setMatchButtonBusy(false, matchText());
}
}
function requestLobbyData() {
var lobbyData = {
profile: {},
appearance: {},
balance: null,
configResponse: null,
};
function onceTask(task) {
var promise = null;
return function () {
if (!promise) promise = task();
return promise;
};
}
if (!window.HyAppAPI || !diceAPI()) {
showMessage(
i18nText(
'diceMatch.apiUnavailable',
'Dice API unavailable'
)
);
return {
lobbyData: lobbyData,
tasks: [
function () {
return Promise.resolve();
},
],
};
}
setMatchButtonBusy(
true,
i18nText('diceMatch.loading', 'Loading')
);
var configStartedAt = performance.now();
var profileTask = function () {
return api()
.user.me()
.then(function (data) {
lobbyData.profile = data || {};
return data;
})
.catch(function () {
lobbyData.profile = {};
return null;
});
};
var appearanceTask = function () {
return api()
.user.appearance()
.then(function (data) {
lobbyData.appearance = data || {};
return data;
})
.catch(function () {
lobbyData.appearance = {};
return null;
});
};
var balanceTask = function () {
return api()
.wallet.balances('COIN')
.then(function (data) {
lobbyData.balance = data || null;
return data;
})
.catch(function () {
lobbyData.balance = null;
return null;
});
};
var configTask = function () {
return diceAPI()
.config('dice')
.then(function (data) {
lobbyData.configResponse = data || null;
trackSelfGameEvent('config_load_success', {
duration_ms: Math.round(
performance.now() - configStartedAt
),
});
return data;
})
.catch(function () {
lobbyData.configResponse = null;
trackSelfGameEvent('config_load_fail', {
duration_ms: Math.round(
performance.now() - configStartedAt
),
success: false,
error_code: 'config_failed',
});
return null;
});
};
return {
lobbyData: lobbyData,
tasks: [
onceTask(profileTask),
onceTask(appearanceTask),
onceTask(balanceTask),
onceTask(configTask),
],
};
}
function applyLobbyData(lobbyData) {
lobbyData = lobbyData || {};
state.profile = lobbyData.profile || {};
state.appearance = lobbyData.appearance || {};
state.config =
(lobbyData.configResponse &&
lobbyData.configResponse.config) ||
null;
if (lobbyData.balance) {
setCoin(balanceFromResponse(lobbyData.balance));
}
renderSelfProfile();
renderStakeOptions(state.config);
setMatchButtonBusy(
state.preloading,
state.preloading
? i18nText('diceMatch.loading', 'Loading')
: matchText()
);
}
function loadLobby() {
var lobby = requestLobbyData();
return runPreloadTasks(lobby.tasks).then(function () {
return preloadLobbyDataImages(lobby.lobbyData).then(
function () {
applyLobbyData(lobby.lobbyData);
return lobby.lobbyData;
}
);
});
}
function loadStartup() {
var lobby = requestLobbyData();
var tasks = lobby.tasks.slice();
collectDicePreloadURLs().forEach(function (url) {
tasks.push(function () {
return preloadResourceOnce(url);
});
});
tasks.push(function () {
return Promise.all([
lobby.tasks[0](),
lobby.tasks[1](),
]).then(function () {
return preloadLobbyDataImages(lobby.lobbyData);
});
});
// 启动期必须等用户、余额、配置、骰子 SVGA、六面骰子和页面图片都进入缓存后才允许用户进入大厅操作。
return runPreloadTasks(tasks)
.then(function () {
applyLobbyData(lobby.lobbyData);
return new Promise(function (resolve) {
window.setTimeout(resolve, 180);
});
})
.then(function () {
completeLoadingScreen();
trackSelfGameEvent('preload_complete', {
duration_ms: Math.round(
performance.now() - pageStartedAt
),
asset_count: tasks.length,
});
});
}
function initActions() {
document.addEventListener('click', function (event) {
var target = event.target.closest('[data-action]');
if (!target) return;
var action = target.getAttribute('data-action');
if (action === 'back') {
cancelWaitingMatch().then(function () {
postBridge('back');
});
return;
}
if (action === 'recharge') {
openRecharge();
return;
}
if (state.preloading) return;
if (action === 'stake') {
if (state.busy) return;
selectStake(target);
trackSelfGameEvent('stake_select', {
stake_coin: state.selectedStake,
});
return;
}
if (action === 'match') {
if (!state.busy) startMatch();
return;
}
if (action === 'rematch') {
cancelWaitingMatch().then(function () {
resetToLobby(true);
postBridge('gameRematch', {
game_code: 'touzi',
});
trackSelfGameEvent('play_again', {
source: 'rematch',
stake_coin: state.selectedStake,
});
startMatch();
});
return;
}
if (action === 'play-again') {
cancelWaitingMatch().then(function () {
resetToLobby(true);
postBridge('gamePlayAgain', {
game_code: 'touzi',
});
trackSelfGameEvent('play_again', {
source: 'settlement',
stake_coin: state.selectedStake,
});
});
}
});
window.addEventListener('beforeunload', function () {
cancelWaitingMatch();
});
}
updateScale();
initActions();
trackSelfGameEvent('page_open', {
duration_ms: Math.round(performance.now() - pageStartedAt),
});
window.addEventListener(
'hyapp:i18n-ready',
refreshLocalizedRuntimeText
);
loadStartup().catch(function (error) {
showMessage(error && error.message);
completeLoadingScreen();
setMatchButtonBusy(false, matchText());
});
window.addEventListener('resize', updateScale);
window.addEventListener('orientationchange', updateScale);
if (window.HyAppBridge && window.HyAppBridge.ready) {
window.HyAppBridge.ready({
page: 'game/touzi',
figma_node_id: '466:61',
});
}
})();
</script>
</body>
</html>