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

3927 lines
148 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="rockGame.pageTitle">Rock Paper Scissors</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-topbar-offset: 0px;
}
* {
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;
}
.rps-frame {
--game-screen-scale: var(--game-scale);
}
.game-stage {
width: 100%;
max-width: 768px;
margin: 0 auto;
}
.rps-bg {
position: absolute;
top: -145px;
left: -53px;
width: 471px;
height: 836px;
object-fit: cover;
pointer-events: none;
user-select: none;
}
.rps-bg-overlay {
mix-blend-mode: multiply;
opacity: 0.9;
}
.rps-home-indicator {
position: absolute;
left: 120px;
bottom: 9px;
z-index: 50;
width: 135px;
height: 5px;
border-radius: 3px;
background: #fff;
}
.top-bar {
position: absolute;
top: 60px;
left: 16px;
z-index: 30;
display: grid;
grid-template-columns: 46px minmax(0, 1fr) max-content;
align-items: center;
justify-content: space-between;
width: 343px;
height: 36px;
transform: translateY(var(--dice-topbar-offset));
}
.top-back {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 46px;
height: 36px;
flex: 0 0 46px;
}
.top-back .layer-img {
position: static;
width: 46px;
height: 36px;
object-fit: fill;
}
.top-game-icon {
position: relative;
justify-self: center;
width: 36px;
height: 36px;
overflow: hidden;
border-radius: 6px;
}
.top-coin {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
justify-self: end;
width: max-content;
min-width: 102px;
max-width: 176px;
height: 36px;
flex: 0 1 auto;
gap: 4px;
overflow: visible;
padding: 0 7px;
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: flex;
align-items: center;
min-width: max-content;
max-width: none;
flex: 0 0 auto;
height: 100%;
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 {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #ffd15a;
font-size: 21px;
font-weight: 900;
line-height: 1;
text-shadow:
0 1px 1px rgba(65, 23, 0, 0.86),
0 0 8px rgba(255, 178, 47, 0.74);
}
.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: 800;
line-height: 14px;
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[hidden] {
display: none;
}
.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;
}
.rps-screen {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
transition: opacity 180ms ease;
}
.rps-frame[data-state='lobby'] .rps-lobby,
.rps-frame[data-state='searching'] .rps-matching,
.rps-frame[data-state='vs'] .rps-vs-screen,
.rps-frame[data-state='battle'] .rps-battle,
.rps-frame[data-state='victory'] .rps-victory {
opacity: 1;
pointer-events: auto;
}
.rps-profile {
position: absolute;
top: 116px;
left: 102px;
z-index: 6;
width: 170px;
text-align: center;
}
.rps-avatar-ring {
position: relative;
width: 96px;
height: 96px;
margin: 0 auto;
border: 3px solid #f8bc39;
border-radius: 50%;
background:
radial-gradient(circle, #74421f 0%, #2c1235 66%), #35153d;
box-shadow:
0 0 0 4px rgba(76, 35, 4, 0.9),
0 0 0 7px rgba(255, 205, 61, 0.45),
0 0 22px rgba(255, 175, 40, 0.76);
}
.rps-avatar-ring::before {
position: absolute;
top: -18px;
left: 27px;
width: 42px;
height: 26px;
content: '';
background: linear-gradient(180deg, #ffe777 0%, #c06d12 100%);
clip-path: polygon(
0 100%,
14% 38%,
29% 72%,
50% 0,
71% 72%,
86% 38%,
100% 100%
);
filter: drop-shadow(0 2px 0 rgba(92, 31, 0, 0.72));
}
.rps-profile .rps-avatar-ring {
width: 90px;
height: 98px;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.rps-profile .rps-avatar-ring::before {
display: none;
}
.rps-lobby-avatar-fallback {
position: absolute;
left: 7px;
top: 15px;
width: 75px;
height: 75px;
border-radius: 50%;
background:
radial-gradient(circle, #74421f 0%, #2c1235 66%), #35153d;
}
.rps-lobby-avatar-frame {
position: absolute;
inset: 0;
width: 90px;
height: 98px;
object-fit: fill;
pointer-events: none;
z-index: 2;
}
.rps-avatar-img {
position: absolute;
inset: 5px;
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
.rps-profile .rps-avatar-img {
left: 7px;
top: 15px;
width: 75px;
height: 75px;
z-index: 1;
}
.rps-avatar-img.is-empty {
display: none;
}
.rps-profile-name,
.rps-victory-name {
color: #ffe189;
font-weight: 700;
text-shadow:
0 1px 0 #7a2e09,
0 0 4px #692808;
white-space: nowrap;
}
.rps-profile-name {
margin-top: 13px;
font-size: 16px;
line-height: 16px;
}
.rps-showcase {
position: absolute;
top: 195px;
left: 0;
width: 375px;
height: 236px;
}
.rps-hand-zone {
position: absolute;
width: 258px;
height: 258px;
}
.rps-hand-zone.is-left {
left: -52px;
top: 0;
}
.rps-hand-zone.is-right {
left: 169px;
top: 0;
}
.rps-ring {
position: absolute;
top: 23px;
width: 258px;
height: 258px;
opacity: 0.88;
animation: matching-center-spin 10s linear infinite;
}
.rps-hand-zone.is-left .rps-ring {
left: -7px;
}
.rps-hand-zone.is-right .rps-ring {
right: -7px;
}
.rps-flare {
position: absolute;
top: 0;
width: 244px;
height: 236px;
opacity: 0.95;
}
.rps-hand-zone.is-left .rps-flare {
left: 0;
}
.rps-hand-zone.is-right .rps-flare {
right: 0;
}
.rps-hand {
position: absolute;
object-fit: contain;
filter: drop-shadow(0 16px 11px rgba(0, 0, 0, 0.42));
}
.rps-hand-zone.is-left .rps-hand {
left: 89px;
top: 90px;
width: 78px;
height: 101px;
}
.rps-hand-zone.is-right .rps-hand {
left: 68px;
top: 72px;
width: 121px;
height: 131px;
}
.rps-hand[data-gesture='paper'] {
width: 118px;
height: 132px;
}
.rps-hand[data-gesture='scissors'] {
width: 92px;
height: 132px;
}
.rps-lobby-panel {
position: absolute;
left: 12px;
top: 488px;
width: 350px;
height: 292px;
background: url('assets/figma/lobby-panel.svg') center/100% 100%
no-repeat;
}
.rps-panel-title,
.rps-stake-title {
position: absolute;
left: 0;
width: 100%;
color: #f8edc7;
font-size: 14px;
line-height: 14px;
text-align: center;
text-shadow: 0 0 6px rgba(255, 217, 115, 0.38);
}
.rps-panel-title {
top: 12px;
}
.rps-stake-title {
top: 155px;
}
.rps-help {
position: absolute;
top: 9px;
right: 8px;
width: 20px;
height: 20px;
overflow: hidden;
color: transparent;
background: url('assets/figma/help-badge.png') center/100% 100%
no-repeat;
}
.rps-gesture-row {
position: absolute;
top: 38px;
left: 8px;
display: grid;
grid-template-columns: repeat(3, 105px);
gap: 10px;
}
.rps-gesture {
position: relative;
width: 105px;
height: 103px;
background: url('assets/figma/gesture-card.png') center/100%
100% no-repeat;
}
.rps-gesture[aria-pressed='true'] {
background-image: url('assets/figma/gesture-selected-rock.png');
filter: drop-shadow(0 0 8px rgba(22, 102, 255, 0.95));
}
.rps-gesture img {
position: absolute;
left: 50%;
top: 20px;
width: 40px;
height: 48px;
object-fit: contain;
transform: translateX(-50%);
}
.rps-gesture[data-gesture='paper'] img {
top: 18px;
width: 42px;
height: 50px;
}
.rps-gesture[data-gesture='scissors'] img {
top: 17px;
width: 46px;
height: 52px;
}
.rps-gesture span {
position: absolute;
left: 0;
bottom: 10px;
width: 100%;
color: #ffd982;
font-size: 14px;
font-weight: 800;
line-height: 14px;
text-align: center;
text-shadow:
0 1px 0 #532203,
0 0 5px rgba(255, 216, 105, 0.58);
}
.rps-stake-row {
position: absolute;
top: 181px;
left: 8px;
display: grid;
grid-template-columns: repeat(4, 77px);
gap: 9px;
overflow: visible;
}
.rps-stake {
position: relative;
width: 77px;
height: 28px;
display: flex;
align-items: center;
gap: 2px;
color: #ffe189;
font-size: 16px;
font-weight: 800;
line-height: 16px;
text-shadow: 0 0 4px rgba(213, 148, 43, 0.82);
overflow: hidden;
padding-left: 8px;
white-space: nowrap;
z-index: 1;
}
.rps-stake-bg {
position: absolute;
top: 0;
left: 0;
width: 77px;
height: 28px;
pointer-events: none;
}
.rps-stake[aria-pressed='true'] .rps-stake-bg {
display: none;
}
.rps-stake[aria-pressed='true'] {
filter: drop-shadow(0 0 5px rgba(255, 197, 47, 0.4));
}
.rps-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-selected.png');
background-repeat: no-repeat;
background-size: 105px 28px;
}
.rps-stake[aria-pressed='true'] .rps-stake-selected-slice {
display: block;
}
.rps-stake-selected-left {
left: 0;
background-position: left top;
}
.rps-stake-selected-right {
right: 0;
background-position: right top;
}
.rps-stake-coin {
position: relative;
width: 14px;
height: 14px;
flex: 0 0 14px;
z-index: 1;
}
.rps-stake-value {
position: relative;
font-size: 12px;
line-height: 12px;
z-index: 1;
}
.rps-start {
position: absolute;
left: 73px;
top: 225px;
width: 201px;
height: 40px;
background: url('assets/figma/start-button.png') center/100%
100% no-repeat;
color: #fcf7cb;
filter: brightness(1.35) saturate(1.35)
drop-shadow(0 0 18px rgba(0, 136, 255, 0.96));
font-size: 18px;
font-weight: 800;
line-height: 40px;
text-align: center;
text-shadow:
0 1px 1px rgba(52, 18, 0, 0.9),
0 0 8px rgba(255, 231, 121, 0.76);
}
.rps-start::before {
content: none;
}
.rps-refund-note {
position: absolute;
left: 49px;
bottom: 11px;
width: 252px;
overflow: hidden;
color: rgba(255, 255, 255, 0.4);
font-size: 12px;
line-height: 12px;
text-align: center;
white-space: nowrap;
}
.rps-duel-card {
position: absolute;
top: 126px;
width: 160px;
height: 80px;
}
.rps-duel-card.is-self {
left: 12px;
}
.rps-duel-card.is-opponent {
left: 203px;
}
.rps-duel-card > img {
position: absolute;
inset: 0;
width: 160px;
height: 80px;
object-fit: fill;
}
.rps-duel-avatar {
position: absolute;
top: 10px;
width: 62px;
height: 62px;
border: 2px solid #f6bc43;
border-radius: 50%;
background: radial-gradient(circle, #5c2d48 0%, #180b32 76%);
box-shadow: 0 0 12px rgba(255, 192, 58, 0.68);
}
.rps-duel-card.is-self .rps-duel-avatar {
left: 6px;
}
.rps-duel-card.is-opponent .rps-duel-avatar {
right: 7px;
border-color: #54bcff;
box-shadow: 0 0 17px rgba(72, 142, 255, 0.92);
}
.rps-duel-avatar img {
position: absolute;
inset: 3px;
width: 52px;
height: 52px;
border-radius: 50%;
object-fit: cover;
}
.rps-duel-avatar img.is-empty {
display: none;
}
.rps-duel-card.is-opponent.has-opponent-avatar .rps-question {
display: none;
}
.rps-duel-card.is-opponent.has-opponent .rps-duel-status {
display: none;
}
.rps-question {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: #8fd5ff;
font-size: 28px;
font-weight: 900;
text-shadow: 0 0 12px rgba(52, 134, 255, 1);
}
.rps-duel-name {
position: absolute;
top: 29px;
width: 58px;
color: #fff;
font-size: 10px;
line-height: 14px;
}
.rps-duel-card.is-self .rps-duel-name {
left: 79px;
}
.rps-duel-card.is-opponent .rps-duel-name {
left: 21px;
text-align: left;
}
.rps-duel-status {
position: absolute;
top: 45px;
left: 37px;
display: flex;
align-items: center;
gap: 3px;
color: #b9defb;
font-size: 8px;
line-height: 14px;
}
.rps-duel-status img {
width: 12px;
height: 12px;
}
.rps-search-hud {
position: absolute;
top: 134px;
left: 152px;
width: 72px;
height: 72px;
}
.rps-search-hud img:first-child {
width: 72px;
height: 72px;
animation: matching-center-spin 3000ms linear infinite;
}
.rps-search-hud img:last-child {
position: absolute;
top: 29px;
left: 29px;
width: 15px;
height: 15px;
}
.rps-frame.has-opponent .rps-search-hud {
display: none;
}
.rps-bottom-status {
position: absolute;
left: 74px;
top: 676px;
width: 228px;
color: #c9e8ff;
font-size: 24px;
font-weight: 900;
line-height: 24px;
text-align: center;
text-shadow:
0 0 6px #0039bc,
0 0 16px rgba(70, 157, 255, 0.78);
}
.rps-bottom-status::before,
.rps-bottom-status::after {
position: absolute;
top: 11px;
width: 36px;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
rgba(180, 224, 255, 0.95)
);
}
.rps-bottom-status::before {
left: 0;
}
.rps-bottom-status::after {
right: 0;
transform: scaleX(-1);
}
.rps-vs-badge {
position: absolute;
top: 112px;
left: 156px;
width: 68px;
height: 112px;
z-index: 12;
}
.rps-battle .rps-showcase {
top: 244px;
}
.rps-battle .rps-hand-zone.is-left .rps-hand {
animation: rps-left-clash 900ms
cubic-bezier(0.21, 0.92, 0.22, 1) 260ms both;
}
.rps-battle .rps-hand-zone.is-right .rps-hand {
animation: rps-right-clash 900ms
cubic-bezier(0.21, 0.92, 0.22, 1) 260ms both;
}
.rps-frame.is-self-win-compare {
animation: you-win-screen-smash 780ms
cubic-bezier(0.14, 0.84, 0.18, 1) 430ms both;
}
.countdown-layer {
position: absolute;
top: 268px;
left: 0;
z-index: 28;
display: flex;
align-items: center;
justify-content: center;
width: 375px;
height: 158px;
visibility: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.rps-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: 31;
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%;
}
.rps-frame.is-self-win-compare .you-win-smash-layer {
visibility: visible;
opacity: 1;
}
.rps-frame.is-self-win-compare .you-win-smash-layer::before {
animation: you-win-screen-flash 760ms ease-out 430ms both;
}
.rps-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: 32;
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;
}
.rps-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;
}
.rps-victory-title {
position: absolute;
top: 90px;
left: 29px;
width: 316px;
height: 124px;
object-fit: contain;
z-index: 1;
}
.rps-victory .rps-avatar-ring {
position: absolute;
top: 246px;
left: 102px;
width: 170px;
height: 186px;
border: 0;
background: transparent;
box-shadow: none;
}
.rps-victory .rps-avatar-ring::before {
display: none;
}
.rps-victory-avatar-bg {
position: absolute;
left: 15px;
top: 28px;
width: 144px;
height: 144px;
border-radius: 50%;
object-fit: cover;
}
.rps-victory-avatar-frame {
position: absolute;
inset: 0;
width: 170px;
height: 186px;
object-fit: fill;
pointer-events: none;
}
.rps-victory .rps-avatar-img {
left: 15px;
top: 28px;
width: 144px;
height: 144px;
z-index: 1;
}
.rps-victory-name {
position: absolute;
top: 448px;
left: 72px;
width: 232px;
overflow: hidden;
font-size: 24px;
line-height: 26px;
text-align: center;
text-overflow: ellipsis;
}
.rps-reward-panel {
position: absolute;
top: 514px;
left: 38px;
width: 300px;
height: 103px;
background: url('assets/figma/victory-reward-frame.png')
center/100% 100% no-repeat;
}
.rps-reward-panel img {
position: absolute;
left: 38px;
top: 16px;
width: 75px;
height: 70px;
object-fit: contain;
}
.rps-reward-copy {
position: absolute;
top: 13px;
left: 145px;
width: 170px;
color: #ffe189;
text-align: center;
text-transform: uppercase;
}
.rps-reward-copy strong {
display: block;
font-size: 18px;
line-height: 20px;
}
.rps-reward-copy b {
display: block;
margin-top: 4px;
font-size: 24px;
line-height: 24px;
}
.rps-reward-copy span {
display: block;
margin-top: 4px;
font-size: 18px;
line-height: 20px;
}
.rps-victory-actions {
position: absolute;
top: 690px;
left: 12px;
width: 351px;
height: 40px;
}
.rps-victory-button {
position: absolute;
left: 0;
top: 0;
width: 168px;
height: 40px;
color: #fcf7cb;
font-size: 18px;
font-weight: 800;
line-height: 40px;
text-align: center;
text-shadow: 0 0 2px #d5942b;
}
.rps-victory-button::before {
position: absolute;
inset: 0;
z-index: -1;
content: '';
background: url('assets/figma/victory-action-blue.png')
center/100% 100% no-repeat;
filter: brightness(1.22) saturate(1.14)
drop-shadow(0 0 10px rgba(0, 111, 255, 0.9));
}
.rps-victory-button.is-secondary {
left: 183px;
}
.rps-victory-button.is-secondary::before {
background-image: url('assets/figma/victory-action-secondary.png');
filter: drop-shadow(0 0 5px rgba(255, 227, 142, 0.34));
}
.rps-message {
position: absolute;
left: 36px;
top: 636px;
z-index: 35;
width: 303px;
color: #f4f8ff;
font-size: 13px;
line-height: 16px;
text-align: center;
text-shadow: 0 0 8px rgba(31, 130, 255, 0.95);
}
@media (prefers-reduced-motion: reduce) {
.rps-ring,
.rps-search-hud img:first-child,
.rps-battle .rps-hand-zone .rps-hand,
.coin-credit-flyer,
.rps-frame.is-self-win-compare,
.rps-frame.is-self-win-compare .you-win-smash-layer::before,
.rps-frame.is-self-win-compare .you-win-smash-floor,
.rps-frame.is-self-win-compare .you-win-impact,
.top-coin.is-crediting {
animation: none !important;
}
}
@keyframes rps-left-clash {
0% {
transform: translateX(-34px) scale(0.94);
}
46% {
transform: translateX(42px) scale(1.08) rotate(4deg);
}
62% {
transform: translateX(26px) scale(0.98) rotate(-2deg);
}
100% {
transform: translateX(0) scale(1) rotate(0deg);
}
}
@keyframes rps-right-clash {
0% {
transform: translateX(34px) scale(0.94);
}
46% {
transform: translateX(-42px) scale(1.08) rotate(-4deg);
}
62% {
transform: translateX(-26px) scale(0.98) rotate(2deg);
}
100% {
transform: translateX(0) scale(1) rotate(0deg);
}
}
@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(--game-screen-scale));
filter: none;
}
12% {
transform: translate3d(0, 14px, 0)
scale(var(--game-screen-scale));
filter: brightness(1.22) contrast(1.08);
}
24% {
transform: translate3d(-12px, -8px, 0)
scale(var(--game-screen-scale));
}
36% {
transform: translate3d(10px, 7px, 0)
scale(var(--game-screen-scale));
}
50% {
transform: translate3d(-7px, 4px, 0)
scale(var(--game-screen-scale));
}
66% {
transform: translate3d(5px, -3px, 0)
scale(var(--game-screen-scale));
filter: brightness(1.08) contrast(1.03);
}
82% {
transform: translate3d(-2px, 1px, 0)
scale(var(--game-screen-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 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="game-stage"
role="application"
aria-label="Rock paper scissors"
data-i18n-aria="rockGame.pageLabel"
>
<main
class="game-frame rps-frame is-preloading"
data-state="lobby"
data-node-id="491:135"
data-name="rock-paper-scissors"
>
<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>
<img class="rps-bg" src="assets/figma/bg-arena.jpg" alt="" />
<img
class="rps-bg rps-bg-overlay"
src="assets/figma/bg-arena.jpg"
alt=""
/>
<div class="rps-home-indicator" aria-hidden="true"></div>
<div class="top-bar" data-name="top bar">
<button
class="top-back"
type="button"
aria-label="Back"
data-action="back"
>
<img
class="layer-img cover"
src="assets/figma/back-button.png"
alt=""
/>
</button>
<div class="top-game-icon">
<img
class="layer-img cover"
src="assets/figma/game-icon.png"
alt=""
/>
</div>
<button
class="top-coin"
type="button"
aria-label="Recharge"
data-action="recharge"
>
<img src="assets/figma/coin.png" alt="" />
<span>159</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="rockGame.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>
<section class="rps-screen rps-lobby">
<div class="rps-profile">
<div class="rps-avatar-ring">
<div
class="rps-lobby-avatar-fallback"
aria-hidden="true"
></div>
<img
class="rps-avatar-img is-empty"
data-role="self-avatar"
alt=""
/>
<img
class="rps-lobby-avatar-frame"
src="assets/figma/lobby-avatar-frame.png"
alt=""
/>
</div>
<div class="rps-profile-name" data-role="self-name">
NameNameName...
</div>
</div>
<div class="rps-showcase">
<div class="rps-hand-zone is-left">
<img
class="rps-ring"
src="assets/figma/red-ring.png"
alt=""
/>
<img
class="rps-flare"
src="assets/figma/red-flare.png"
alt=""
/>
<img
class="rps-hand"
data-role="self-lobby-hand"
data-gesture="rock"
src="assets/figma/rock.png"
alt=""
/>
</div>
<div class="rps-hand-zone is-right">
<img
class="rps-ring"
src="assets/figma/blue-ring.png"
alt=""
/>
<img
class="rps-flare"
src="assets/figma/blue-flare.png"
alt=""
/>
<img
class="rps-hand"
data-role="preview-opponent-hand"
data-gesture="paper"
src="assets/figma/paper.png"
alt=""
/>
</div>
</div>
<div class="rps-lobby-panel">
<div
class="rps-panel-title"
data-i18n="rockGame.chooseGesture"
>
Choose your gesture
</div>
<div class="rps-help">?</div>
<div class="rps-gesture-row">
<button
class="rps-gesture"
type="button"
data-action="gesture"
data-gesture="rock"
aria-pressed="true"
>
<img src="assets/figma/rock.png" alt="" />
<span data-i18n="rockGame.rock">Rock</span>
</button>
<button
class="rps-gesture"
type="button"
data-action="gesture"
data-gesture="paper"
aria-pressed="false"
>
<img src="assets/figma/paper.png" alt="" />
<span data-i18n="rockGame.paper">Paper</span>
</button>
<button
class="rps-gesture"
type="button"
data-action="gesture"
data-gesture="scissors"
aria-pressed="false"
>
<img src="assets/figma/scissors.png" alt="" />
<span data-i18n="rockGame.scissors"
>Scissors</span
>
</button>
</div>
<div
class="rps-stake-title"
data-i18n="rockGame.winnerReceives"
>
The winner receives
</div>
<div class="rps-stake-row">
<button
class="rps-stake"
type="button"
data-action="stake"
data-stake="8000"
aria-pressed="false"
>
<img
class="rps-stake-bg"
src="assets/figma/stake-outline.svg"
alt=""
/>
<span
class="rps-stake-selected-slice rps-stake-selected-left"
></span>
<span
class="rps-stake-selected-slice rps-stake-selected-right"
></span>
<img
class="rps-stake-coin"
src="assets/figma/coin.png"
alt=""
/>
<span class="rps-stake-value">8,000</span>
</button>
<button
class="rps-stake"
type="button"
data-action="stake"
data-stake="8000"
aria-pressed="true"
>
<img
class="rps-stake-bg"
src="assets/figma/stake-outline.svg"
alt=""
/>
<span
class="rps-stake-selected-slice rps-stake-selected-left"
></span>
<span
class="rps-stake-selected-slice rps-stake-selected-right"
></span>
<img
class="rps-stake-coin"
src="assets/figma/coin.png"
alt=""
/>
<span class="rps-stake-value">8,000</span>
</button>
<button
class="rps-stake"
type="button"
data-action="stake"
data-stake="8000"
aria-pressed="false"
>
<img
class="rps-stake-bg"
src="assets/figma/stake-outline.svg"
alt=""
/>
<span
class="rps-stake-selected-slice rps-stake-selected-left"
></span>
<span
class="rps-stake-selected-slice rps-stake-selected-right"
></span>
<img
class="rps-stake-coin"
src="assets/figma/coin.png"
alt=""
/>
<span class="rps-stake-value">8,000</span>
</button>
<button
class="rps-stake"
type="button"
data-action="stake"
data-stake="8000"
aria-pressed="false"
>
<img
class="rps-stake-bg"
src="assets/figma/stake-outline.svg"
alt=""
/>
<span
class="rps-stake-selected-slice rps-stake-selected-left"
></span>
<span
class="rps-stake-selected-slice rps-stake-selected-right"
></span>
<img
class="rps-stake-coin"
src="assets/figma/coin.png"
alt=""
/>
<span class="rps-stake-value">8,000</span>
</button>
</div>
<button
class="rps-start"
type="button"
data-action="start"
data-i18n="rockGame.start"
>
START
</button>
<div
class="rps-refund-note"
style="display: none"
></div>
</div>
</section>
<section class="rps-screen rps-matching">
<div class="rps-duel-card is-self">
<img src="assets/figma/player-card.png" alt="" />
<div class="rps-duel-avatar">
<img
class="is-empty"
data-role="self-card-avatar"
alt=""
/>
</div>
<div class="rps-duel-name" data-role="self-card-name">
<div>NameName</div>
<div>Name...</div>
</div>
</div>
<div class="rps-duel-card is-opponent">
<img src="assets/figma/opponent-card.png" alt="" />
<div class="rps-duel-avatar">
<img
class="is-empty"
data-role="opponent-card-avatar"
alt=""
/>
<div class="rps-question">?</div>
</div>
<div
class="rps-duel-name"
data-role="opponent-card-name"
>
<div>?</div>
<div></div>
</div>
<div class="rps-duel-status">
<img src="assets/figma/search-icon.svg" alt="" />
<span data-i18n="rockGame.matching"
>Matching...</span
>
</div>
</div>
<div class="rps-search-hud">
<img src="assets/figma/search-hud.png" alt="" />
<img src="assets/figma/search-icon.svg" alt="" />
</div>
<div
class="rps-bottom-status"
data-role="bottom-status"
data-i18n="rockGame.matching"
>
Matching...
</div>
</section>
<section class="rps-screen rps-vs-screen">
<div class="rps-duel-card is-self">
<img src="assets/figma/player-card.png" alt="" />
<div class="rps-duel-avatar">
<img
class="is-empty"
data-role="self-vs-avatar"
alt=""
/>
</div>
<div class="rps-duel-name" data-role="self-vs-name">
<div>NameName</div>
<div>Name...</div>
</div>
</div>
<div class="rps-duel-card is-opponent">
<img src="assets/figma/opponent-card.png" alt="" />
<div class="rps-duel-avatar">
<img
class="is-empty"
data-role="opponent-vs-avatar"
alt=""
/>
<div class="rps-question">?</div>
</div>
<div class="rps-duel-name" data-role="opponent-vs-name">
<div>?</div>
<div></div>
</div>
<div class="rps-duel-status">
<img src="assets/figma/search-icon.svg" alt="" />
<span data-i18n="rockGame.matched">Matched</span>
</div>
</div>
<div class="game-vs-badge rps-vs-badge">
<img
class="game-layer-img game-contain"
src="assets/figma/vs.png"
alt=""
/>
</div>
<div class="rps-bottom-status" data-i18n="rockGame.matched">
Matched
</div>
</section>
<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"
data-i18n="rockGame.youWin"
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>
<section class="rps-screen rps-battle">
<div class="rps-duel-card is-self">
<img src="assets/figma/player-card.png" alt="" />
<div class="rps-duel-avatar">
<img
class="is-empty"
data-role="self-battle-avatar"
alt=""
/>
</div>
<div class="rps-duel-name" data-role="self-battle-name">
<div>NameName</div>
<div>Name...</div>
</div>
</div>
<div class="rps-duel-card is-opponent">
<img src="assets/figma/opponent-card.png" alt="" />
<div class="rps-duel-avatar">
<img
class="is-empty"
data-role="opponent-battle-avatar"
alt=""
/>
<div class="rps-question">?</div>
</div>
<div
class="rps-duel-name"
data-role="opponent-battle-name"
>
<div>NameName</div>
<div>Name...</div>
</div>
</div>
<div class="game-vs-badge rps-vs-badge">
<img
class="game-layer-img game-contain"
src="assets/figma/vs.png"
alt=""
/>
</div>
<div class="rps-showcase">
<div class="rps-hand-zone is-left">
<img
class="rps-ring"
src="assets/figma/red-ring.png"
alt=""
/>
<img
class="rps-flare"
src="assets/figma/red-flare.png"
alt=""
/>
<img
class="rps-hand"
data-role="self-battle-hand"
data-gesture="rock"
src="assets/figma/rock.png"
alt=""
/>
</div>
<div class="rps-hand-zone is-right">
<img
class="rps-ring"
src="assets/figma/blue-ring.png"
alt=""
/>
<img
class="rps-flare"
src="assets/figma/blue-flare.png"
alt=""
/>
<img
class="rps-hand"
data-role="opponent-battle-hand"
data-gesture="paper"
src="assets/figma/paper.png"
alt=""
/>
</div>
</div>
</section>
<section class="rps-screen rps-victory">
<img
class="rps-victory-title"
src="assets/figma/victory-title.png"
alt=""
/>
<div class="rps-avatar-ring">
<img
class="rps-victory-avatar-bg"
src="assets/figma/victory-avatar-bg.png"
alt=""
/>
<img
class="rps-avatar-img is-empty"
data-role="winner-avatar"
alt=""
/>
<img
class="rps-victory-avatar-frame"
src="assets/figma/victory-avatar-frame.png"
alt=""
/>
</div>
<div class="rps-victory-name" data-role="winner-name">
NameNameName...
</div>
<div class="rps-reward-panel">
<img src="assets/figma/win-coins.png" alt="" />
<div class="rps-reward-copy">
<strong data-role="result-label">Win</strong>
<b data-role="win-amount">1,900,000</b>
<span data-i18n="rockGame.coins">Coins</span>
</div>
</div>
<div class="rps-victory-actions">
<button
class="rps-victory-button"
type="button"
data-action="rematch"
data-i18n="rockGame.rematch"
>
Re-match
</button>
<button
class="rps-victory-button is-secondary"
type="button"
data-action="play-again"
data-i18n="rockGame.playAgain"
>
Play again
</button>
</div>
</section>
<div class="rps-message" data-role="message"></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=rock-game-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 GAME_ID = 'rock';
var GAME_CODE = 'game_rock';
var RPS_PRELOAD_ASSET_URLS = [
'assets/figma/loading-bg.jpg',
'assets/figma/back-button.png',
'assets/figma/bg-arena.jpg',
'assets/figma/blue-flare.png',
'assets/figma/blue-ring.png',
'assets/figma/coin.png',
'assets/figma/game-icon.png',
'assets/figma/gesture-card.png',
'assets/figma/gesture-selected-rock.png',
'assets/figma/help-badge.png',
'assets/figma/lobby-avatar-frame.png',
'assets/figma/lobby-panel.svg',
'assets/figma/opponent-card.png',
'assets/figma/opponent-question.png',
'assets/figma/paper.png',
'assets/figma/player-card.png',
'assets/figma/red-flare.png',
'assets/figma/red-ring.png',
'assets/figma/rock.png',
'assets/figma/scissors.png',
'assets/figma/search-hud.png',
'assets/figma/search-icon.svg',
'assets/figma/stake-outline.svg',
'assets/figma/stake-selected.png',
'assets/figma/start-button.png',
'assets/figma/victory-action-blue.png',
'assets/figma/victory-action-secondary.png',
'assets/figma/victory-action-secondary.svg',
'assets/figma/victory-avatar-bg.png',
'assets/figma/victory-avatar-frame.png',
'assets/figma/victory-coin-stack.png',
'assets/figma/victory-reward-frame.png',
'assets/figma/victory-title.png',
'assets/figma/victory-title.svg',
'assets/figma/victory.png',
'assets/figma/vs.png',
'assets/figma/win-coins.png',
];
var root = document.documentElement;
var frame = document.querySelector('.rps-frame');
var timers = [];
var pollTimer = 0;
var pageStartedAt = performance.now();
var coinCreditFrame = 0;
var coinCreditLayer = null;
var transparentPixel =
'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%221%22 height=%221%22%2F%3E';
var gestures = {
rock: {
labelKey: 'rockGame.rock',
label: 'Rock',
image: 'assets/figma/rock.png',
beats: 'scissors',
},
paper: {
labelKey: 'rockGame.paper',
label: 'Paper',
image: 'assets/figma/paper.png',
beats: 'rock',
},
scissors: {
labelKey: 'rockGame.scissors',
label: 'Scissors',
image: 'assets/figma/scissors.png',
beats: 'paper',
},
};
var state = {
preloading: true,
profile: {},
balanceCoin: 159,
selectedGesture: 'rock',
selectedStake: 8000,
opponentGesture: 'paper',
opponent: {},
config: null,
match: null,
busy: false,
roundID: 0,
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: GAME_ID,
page: 'game/rock',
h5_version: 'rock-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(
'--game-scale',
String(Math.max(scale, 0.1))
);
}
function postBridge(action, payload) {
if (window.HyAppBridge && window.HyAppBridge.post) {
window.HyAppBridge.post(action, payload || {});
}
}
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 clearTimers() {
clearPollTimer();
timers.forEach(function (timer) {
window.clearTimeout(timer);
});
timers = [];
}
function clearPollTimer() {
if (!pollTimer) return;
window.clearTimeout(pollTimer);
pollTimer = 0;
}
function api() {
return window.HyAppAPI || {};
}
function rpsAPI() {
return api().game && api().game.rps;
}
function role(name) {
return document.querySelector('[data-role="' + name + '"]');
}
function each(nodes, iteratee) {
Array.prototype.forEach.call(nodes, iteratee);
}
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 formatCoin(value) {
var number = Number(value || 0);
if (!Number.isFinite(number)) number = 0;
return Math.trunc(Math.max(number, 0)).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 currentRoomID() {
var raw = currentParams();
return raw && raw.get
? raw.get('room_id') || raw.get('roomId') || ''
: '';
}
function entryMatchID() {
var raw = currentParams();
return raw && raw.get
? raw.get('match_id') || raw.get('matchId') || ''
: '';
}
function matchRequestMode() {
var raw = currentParams();
var mode =
raw && raw.get
? String(
raw.get('rps_mode') ||
raw.get('match_mode') ||
raw.get('mode') ||
''
)
.trim()
.toLowerCase()
: '';
if (entryMatchID()) return 'join';
if (mode === 'create' || mode === 'waiting') {
return 'create';
}
return 'match';
}
function currentParams() {
var params =
(window.HyAppParams && window.HyAppParams.current) ||
{};
if (params.raw && params.raw.get) return params.raw;
var raw = new URLSearchParams(window.location.search);
var hash = window.location.hash || '';
if (hash.indexOf('?') >= 0) {
new URLSearchParams(
hash.split('?').slice(1).join('?')
).forEach(function (value, key) {
if (!raw.has(key)) raw.set(key, value);
});
}
return raw;
}
function shouldUseMockFlow() {
var params = currentParams();
return (
params.get('mock') === '1' ||
params.get('demo') === '1' ||
params.get('rps_mock') === '1'
);
}
function displayName(user) {
user = user || {};
return (
user.nickname ||
user.username ||
user.name ||
user.display_user_id ||
user.user_id ||
'NameNameName...'
);
}
function displaySubName(user) {
user = user || {};
return user.display_user_id || user.user_id || '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
) {
return '';
}
return url;
}
function setText(node, value) {
if (node) node.textContent = value == null ? '' : value;
}
function setImage(node, url) {
if (!node) return;
var next = String(url || '').trim();
if (!next) {
node.removeAttribute('src');
node.classList.add('is-empty');
return;
}
node.onerror = function () {
node.onerror = null;
node.src = transparentPixel;
node.classList.add('is-empty');
};
node.src = next;
node.classList.remove('is-empty');
}
function setNameLines(selector, user) {
var nodes = document.querySelectorAll(selector + ' div');
if (nodes[0]) nodes[0].textContent = displayName(user);
if (nodes[1]) nodes[1].textContent = displaySubName(user);
}
function setOpponentNameLines(selector, user, hasDetails) {
var nodes = document.querySelectorAll(selector + ' div');
if (!hasDetails) {
if (nodes[0]) nodes[0].textContent = '?';
if (nodes[1]) nodes[1].textContent = '';
return;
}
if (nodes[0]) nodes[0].textContent = displayName(user);
if (nodes[1]) nodes[1].textContent = displaySubName(user);
}
function setHand(node, gesture) {
var meta = gestures[gesture] || gestures.rock;
if (!node) return;
node.src = meta.image;
node.setAttribute('data-gesture', gesture);
node.alt = i18nText(meta.labelKey, meta.label);
}
function showMessage(message) {
setText(role('message'), message || '');
if (!message) return;
schedule(function () {
setText(role('message'), '');
}, 2200);
}
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 setSelfWinCompare(visible) {
frame.classList.remove('is-self-win-compare');
if (!visible) return;
// 自己赢时复用 dice 的“YOU WIN 从天而降”节奏;先强制提交移除状态,下一局才能再次播放。
void frame.offsetWidth;
frame.classList.add('is-self-win-compare');
}
function setScreen(nextState) {
frame.setAttribute('data-state', nextState);
}
function renderCoin(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);
renderCoin(start + (target - start) * eased);
if (progress < 1) {
coinCreditFrame =
window.requestAnimationFrame(tick);
return;
}
coinCreditFrame = 0;
renderCoin(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) {
renderCoin(targetValue);
return false;
}
var start = { x: 114, y: 566 };
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 renderProfile() {
var profile = state.profile || {};
var avatar = avatarURL(profile);
setText(role('self-name'), displayName(profile));
setNameLines('[data-role="self-card-name"]', profile);
setNameLines('[data-role="self-vs-name"]', profile);
setNameLines('[data-role="self-battle-name"]', profile);
setImage(role('self-avatar'), avatar);
setImage(role('self-card-avatar'), avatar);
setImage(role('self-vs-avatar'), avatar);
setImage(role('self-battle-avatar'), avatar);
}
function renderSelection() {
each(
document.querySelectorAll('[data-action="gesture"]'),
function (button) {
var selected =
button.getAttribute('data-gesture') ===
state.selectedGesture;
button.setAttribute(
'aria-pressed',
selected ? 'true' : 'false'
);
}
);
var matchedStake = false;
each(
document.querySelectorAll('[data-action="stake"]'),
function (button) {
var selected =
!matchedStake &&
Number(
button.getAttribute('data-stake') || 0
) === state.selectedStake;
if (selected) matchedStake = true;
button.setAttribute(
'aria-pressed',
selected ? 'true' : 'false'
);
}
);
setHand(role('self-lobby-hand'), state.selectedGesture);
setHand(role('self-battle-hand'), state.selectedGesture);
setHand(
role('preview-opponent-hand'),
counterGestureFor(state.selectedGesture)
);
}
function hasOpponentDetails(user) {
user = user || {};
return Boolean(
avatarURL(user) ||
user.nickname ||
user.username ||
user.name ||
user.display_user_id ||
user.user_id
);
}
function renderOpponent() {
var opponent = state.opponent || {};
var avatar = avatarURL(opponent);
var hasDetails = hasOpponentDetails(opponent);
frame.classList.toggle('has-opponent', hasDetails);
// 匹配成功后同一份对手资料要同步到等待卡、VS 卡和对战卡;否则某个屏幕仍停留在搜索态,会出现头像未替换或搜索图标残留。
setOpponentNameLines(
'[data-role="opponent-card-name"]',
opponent,
hasDetails
);
setOpponentNameLines(
'[data-role="opponent-vs-name"]',
opponent,
hasDetails
);
setOpponentNameLines(
'[data-role="opponent-battle-name"]',
opponent,
hasDetails
);
setImage(role('opponent-card-avatar'), avatar);
setImage(role('opponent-vs-avatar'), avatar);
setImage(role('opponent-battle-avatar'), avatar);
each(
document.querySelectorAll('.rps-duel-card.is-opponent'),
function (card) {
// 有对手资料就隐藏 Matching/Search 状态;只有头像真实可用时才隐藏问号兜底,避免无头像用户显示成空圆。
card.classList.toggle('has-opponent', hasDetails);
card.classList.toggle(
'has-opponent-avatar',
Boolean(avatar)
);
}
);
setHand(
role('opponent-battle-hand'),
state.opponentGesture
);
}
function renderVictory(result, match) {
var self = findSelfParticipant(match);
var winner = findWinnerParticipant(match);
var winnerIsSelf =
result === 'draw' ||
!winner ||
sameParticipant(winner, self);
var winnerUser = winnerIsSelf
? state.profile
: Object.assign(
{},
participantUser(winner),
state.opponent
);
var amount = payoutFromMatch(match, result);
var label =
result === 'draw'
? i18nText('rockGame.draw', 'Draw')
: result === 'lose'
? i18nText('rockGame.lose', 'Lose')
: i18nText('rockGame.win', 'Win');
setImage(role('winner-avatar'), avatarURL(winnerUser));
setText(role('winner-name'), displayName(winnerUser));
setText(role('result-label'), label);
setText(role('win-amount'), formatCoin(amount));
if (self && self.balance_after > 0) {
var balanceAfter = Math.trunc(
Number(self.balance_after) || 0
);
if (
result === 'win' &&
winnerIsSelf &&
balanceAfter > state.balanceCoin
) {
return playVictoryCoinCredit(
state.balanceCoin,
balanceAfter
);
}
renderCoin(balanceAfter);
} else if (result === 'win' && !match) {
return playVictoryCoinCredit(
state.balanceCoin,
state.balanceCoin + state.selectedStake
);
}
return false;
}
function losingGestureFor(gesture) {
return (
(gestures[gesture] && gestures[gesture].beats) ||
'scissors'
);
}
function counterGestureFor(gesture) {
if (gesture === 'rock') return 'paper';
if (gesture === 'paper') return 'scissors';
return 'rock';
}
function resultFor(selfGesture, opponentGesture) {
if (selfGesture === opponentGesture) return 'draw';
return losingGestureFor(selfGesture) === opponentGesture
? 'win'
: 'lose';
}
function chooseOpponentGesture() {
return losingGestureFor(state.selectedGesture);
}
function matchFromData(data) {
return (
(data && (data.match || data.rps_match)) || data || null
);
}
function participantUser(participant) {
participant = participant || {};
return Object.assign(
{},
participant.user || {},
participant.profile || {}
);
}
function readGesture(participant) {
participant = participant || {};
var gesture = String(
participant.gesture ||
participant.rps_gesture ||
participant.choice ||
participant.selected_gesture ||
''
)
.trim()
.toLowerCase();
return gestures[gesture] ? gesture : '';
}
function readResult(participant) {
participant = participant || {};
return String(participant.result || '')
.trim()
.toLowerCase();
}
function selfUserID() {
var profile = state.profile || {};
return String(
profile.user_id ||
profile.userId ||
profile.id ||
profile.display_user_id ||
''
);
}
function sameParticipant(left, right) {
if (!left || !right) return false;
var leftID = String(
left.user_id ||
left.userId ||
left.display_user_id ||
''
);
var rightID = String(
right.user_id ||
right.userId ||
right.display_user_id ||
''
);
if (leftID && rightID) return leftID === rightID;
return left === right;
}
function findSelfParticipant(match) {
var participants = (match && match.participants) || [];
var id = selfUserID();
for (var i = 0; i < participants.length; i += 1) {
var candidateID = String(
participants[i].user_id ||
participants[i].userId ||
participants[i].display_user_id ||
''
);
if (id && candidateID === id) return participants[i];
}
return participants[0] || null;
}
function findOpponentParticipant(match, self) {
var participants = (match && match.participants) || [];
for (var i = 0; i < participants.length; i += 1) {
if (!sameParticipant(participants[i], self)) {
return participants[i];
}
}
return null;
}
function findWinnerParticipant(match) {
var participants = (match && match.participants) || [];
var winnerID = String(
(match &&
(match.winner_user_id || match.winnerUserId)) ||
''
);
for (var i = 0; i < participants.length; i += 1) {
if (
winnerID &&
String(participants[i].user_id || '') === winnerID
) {
return participants[i];
}
if (readResult(participants[i]) === 'win') {
return participants[i];
}
}
return null;
}
function isWaitingMatch(match) {
var status = String(
(match && match.status) || ''
).toLowerCase();
var phase = String(
(match && match.phase) || ''
).toLowerCase();
return (
status === 'waiting' ||
status === 'pending' ||
phase === 'waiting' ||
phase === 'matching'
);
}
function isSettledMatch(match) {
var status = String(
(match && match.status) || ''
).toLowerCase();
var phase = String(
(match && match.phase) || ''
).toLowerCase();
return (
status === 'settled' ||
status === 'finished' ||
phase === 'settled' ||
phase === 'finished'
);
}
function renderMatch(match) {
if (!match) return;
var self = findSelfParticipant(match);
var opponent = findOpponentParticipant(match, self);
if (match.stake_coin > 0) {
state.selectedStake = Number(match.stake_coin);
}
if (readGesture(self)) {
state.selectedGesture = readGesture(self);
}
if (opponent) {
state.opponent = Object.assign(
{},
participantUser(opponent),
{
user_id: opponent.user_id,
display_user_id:
participantUser(opponent).display_user_id ||
opponent.display_user_id ||
opponent.user_id,
}
);
if (readGesture(opponent)) {
state.opponentGesture = readGesture(opponent);
}
} else {
// 等待匹配时必须清空上一局对手资料,否则搜索页会复用旧头像并提前隐藏搜索状态。
state.opponent = {};
}
renderSelection();
renderOpponent();
}
function resultFromMatch(match) {
var self = findSelfParticipant(match);
var explicit = readResult(self);
if (
explicit === 'win' ||
explicit === 'lose' ||
explicit === 'draw'
) {
return explicit;
}
var opponent = findOpponentParticipant(match, self);
var selfGesture =
readGesture(self) || state.selectedGesture;
var opponentGesture =
readGesture(opponent) || state.opponentGesture;
return resultFor(selfGesture, opponentGesture);
}
function numberFromFields(source, fields) {
if (!source) return null;
for (var i = 0; i < fields.length; i += 1) {
if (
!Object.prototype.hasOwnProperty.call(
source,
fields[i]
)
) {
continue;
}
var raw = source[fields[i]];
if (raw === null || raw === undefined || raw === '') {
continue;
}
var value = Number(raw);
if (Number.isFinite(value)) return value;
}
return null;
}
function totalFeeBPSForMatch(match) {
var config = state.config || {};
var feeBPS = numberFromFields(match, ['fee_bps', 'feeBps']);
var poolBPS = numberFromFields(match, [
'pool_bps',
'poolBps',
]);
if (feeBPS === null) {
feeBPS = numberFromFields(config, [
'fee_bps',
'feeBps',
]);
}
if (poolBPS === null) {
poolBPS = numberFromFields(config, [
'pool_bps',
'poolBps',
]);
}
if (feeBPS === null && poolBPS === null) return null;
return Math.max(
0,
Math.min(10000, (feeBPS || 0) + (poolBPS || 0))
);
}
function estimatedPayoutFromFee(match) {
var stakeCoin =
numberFromFields(match, ['stake_coin', 'stakeCoin']) ||
state.selectedStake ||
0;
var totalFeeBPS = totalFeeBPSForMatch(match);
if (!stakeCoin || totalFeeBPS === null) return 0;
return Math.max(
0,
Math.trunc(
(stakeCoin * 2 * (10000 - totalFeeBPS)) / 10000
)
);
}
function payoutFromMatch(match, result) {
var self = findSelfParticipant(match);
var payoutCoin = numberFromFields(self, [
'payout_coin',
'payoutCoin',
]);
if (payoutCoin !== null) {
return Math.max(0, Math.trunc(payoutCoin));
}
if (result !== 'win') return 0;
// 真实局缺少 payout_coin 时不能再展示未扣点的 2 倍押注。
// 兜底只能按本局或配置里的 fee_bps + pool_bps 估算。
if (match) return estimatedPayoutFromFee(match);
return state.selectedStake * 2;
}
function enabledStakeOptions(config) {
var options = (config && config.stake_options) || [];
return options
.filter(function (item) {
return (
item &&
item.enabled !== false &&
Number(item.stake_coin || 0) > 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 renderStakeOptions(config) {
var options = enabledStakeOptions(config);
var buttons = document.querySelectorAll(
'[data-action="stake"]'
);
renderFeeNote(config);
if (!options.length) {
renderSelection();
return;
}
if (!state.selectedStake) {
state.selectedStake = Number(
options[0].stake_coin || 0
);
}
var matchedSelected = false;
each(buttons, function (button, index) {
var option = options[index];
if (!option) {
button.style.display = 'none';
return;
}
button.style.display = '';
button.setAttribute('data-stake', option.stake_coin);
button.setAttribute(
'aria-label',
formatCoin(option.stake_coin)
);
setText(
button.querySelector('.rps-stake-value'),
formatCoin(option.stake_coin)
);
var selected =
!matchedSelected &&
Number(option.stake_coin) === state.selectedStake;
if (selected) matchedSelected = true;
button.setAttribute(
'aria-pressed',
selected ? 'true' : 'false'
);
});
}
function renderFeeNote(config) {
var note = document.querySelector('.rps-refund-note');
if (!note) return;
var feeBPS = numberFromFields(config, [
'fee_bps',
'feeBps',
]);
var poolBPS = numberFromFields(config, [
'pool_bps',
'poolBps',
]);
// 费率必须来自后端配置;没有明确字段时隐藏,避免把历史兜底 5% 当成真实规则展示。
if (feeBPS === null && poolBPS === null) {
setText(note, '');
note.style.display = 'none';
return;
}
var totalFee =
Math.max(
0,
Math.min(10000, (feeBPS || 0) + (poolBPS || 0))
) / 100;
setText(
note,
i18nFormat('rockGame.configFeeNote', 'Tickets {fee}%', {
fee: totalFee,
})
);
note.style.display = '';
}
function pollMatch(matchID) {
clearPollTimer();
pollTimer = window.setTimeout(function () {
pollTimer = 0;
if (!state.busy || !matchID || !rpsAPI()) return;
rpsAPI()
.getMatch(matchID)
.then(handleMatchData)
.catch(function (error) {
showMessage(error.message);
pollMatch(matchID);
});
}, 1000);
}
function enterWaiting(match) {
state.match = match;
state.busy = true;
renderMatch(match);
setScreen('searching');
pollMatch(match.match_id || match.matchId);
}
function phaseDeadlineLeft(match) {
var deadline = Number(
(match && match.phase_deadline_ms) ||
(match && match.phaseDeadlineMs) ||
0
);
var serverTime = state.serverTimeMS || Date.now();
if (!deadline) return 3000;
return Math.max(deadline - serverTime, 0);
}
function runCountdown(roundID, match) {
var duration = Math.min(phaseDeadlineLeft(match), 3000);
var endAt = Date.now() + Math.max(duration, 0);
frame.classList.add('is-countdown');
function tick() {
if (roundID !== state.roundID) return;
var left = Math.max(endAt - Date.now(), 0);
if (left <= 0) {
frame.classList.remove('is-countdown');
setCountdownNumber('');
if (match && !isSettledMatch(match)) {
rollAndAnimate(roundID, match);
return;
}
showBattle(roundID, match || null);
return;
}
setCountdownNumber(Math.ceil(left / 1000));
schedule(tick, 160);
}
tick();
}
function beginRound(match) {
clearPollTimer();
state.match = match;
state.busy = true;
state.roundID += 1;
var roundID = state.roundID;
renderMatch(match);
showVS(roundID, match);
}
function rollAndAnimate(roundID, match) {
var matchID = match && (match.match_id || match.matchId);
if (!rpsAPI() || !matchID) {
showMessage(
i18nText(
'rockGame.apiUnavailable',
'RPS API unavailable'
)
);
resetToLobby();
return;
}
trackSelfGameEvent('reveal_enter', {
match_id: matchID,
gesture: state.selectedGesture,
});
var rollStartedAt = performance.now();
rpsAPI()
.roll(matchID, {
game_id: GAME_ID,
gesture: state.selectedGesture,
})
.then(function (data) {
trackSelfGameEvent('roll_api', {
match_id: matchID,
gesture: state.selectedGesture,
duration_ms: Math.round(
performance.now() - rollStartedAt
),
});
var nextMatch = matchFromData(data);
if (roundID !== state.roundID) return;
state.match = nextMatch || match;
renderMatch(state.match);
showBattle(roundID, state.match);
})
.catch(function (error) {
trackSelfGameEvent('roll_api', {
match_id: matchID,
gesture: state.selectedGesture,
duration_ms: Math.round(
performance.now() - rollStartedAt
),
success: false,
error_code: error && error.message,
});
if (isInsufficientBalanceError(error)) {
resetToLobby();
handleInsufficientBalance(error);
return;
}
showMessage(error.message);
resetToLobby();
});
}
function handleMatchData(data) {
var match = matchFromData(data);
if (!match || !(match.match_id || match.matchId)) return;
state.serverTimeMS =
(data && data.server_time_ms) || Date.now();
state.match = match;
renderMatch(match);
if (isSettledMatch(match)) {
beginRound(match);
return;
}
if (isWaitingMatch(match)) {
enterWaiting(match);
return;
}
beginRound(match);
}
function startMatch() {
if (state.preloading) return;
if (state.busy) return;
if (shouldUseMockFlow()) {
startMockMatch();
return;
}
if (!rpsAPI()) {
showMessage(
i18nText(
'rockGame.apiUnavailable',
'RPS API unavailable'
)
);
return;
}
clearTimers();
state.busy = true;
state.roundID += 1;
state.match = null;
state.opponent = {};
renderOpponent();
setScreen('searching');
postBridge('gameRPSMatchRequested', {
game_code: GAME_CODE,
game_id: GAME_ID,
room_id: currentRoomID(),
match_id: entryMatchID(),
mode: matchRequestMode(),
stake_coin: state.selectedStake,
gesture: state.selectedGesture,
});
trackSelfGameEvent('match_click', {
stake_coin: state.selectedStake,
gesture: state.selectedGesture,
mode: matchRequestMode(),
});
var payload = {
game_id: GAME_ID,
room_id: currentRoomID(),
stake_coin: state.selectedStake,
gesture: state.selectedGesture,
};
var mode = matchRequestMode();
var request;
// H5 默认走快速匹配;分享或指定局入口带 match_id 时只加入该局,后台仍会校验 rock 边界和预选手势。
if (mode === 'join') {
request = rpsAPI().join(entryMatchID(), {
game_id: GAME_ID,
gesture: state.selectedGesture,
});
} else if (mode === 'create') {
request = rpsAPI().create(payload);
} else {
request = rpsAPI().match(payload);
}
var matchStartedAt = performance.now();
request
.then(function (data) {
var match = matchFromData(data);
trackSelfGameEvent('match_success', {
match_id:
match && (match.match_id || match.matchId),
stake_coin: state.selectedStake,
gesture: state.selectedGesture,
duration_ms: Math.round(
performance.now() - matchStartedAt
),
});
handleMatchData(data);
})
.catch(function (error) {
trackSelfGameEvent('match_api', {
stake_coin: state.selectedStake,
gesture: state.selectedGesture,
duration_ms: Math.round(
performance.now() - matchStartedAt
),
success: false,
error_code: error && error.message,
});
state.busy = false;
if (isInsufficientBalanceError(error)) {
setScreen('lobby');
handleInsufficientBalance(error);
return;
}
showMessage(error.message);
setScreen('lobby');
});
}
function startMockMatch() {
clearTimers();
state.busy = true;
state.roundID += 1;
var roundID = state.roundID;
state.opponent = {};
state.opponentGesture = chooseOpponentGesture();
state.opponent = {
nickname: 'NameName',
display_user_id: 'Name...',
avatar: 'assets/figma/game-icon.png',
};
renderOpponent();
setScreen('searching');
postBridge('gameRPSMatchRequested', {
game_code: GAME_CODE,
game_id: GAME_ID,
room_id: currentRoomID(),
stake_coin: state.selectedStake,
gesture: state.selectedGesture,
mock: true,
});
schedule(function () {
if (roundID !== state.roundID) return;
showVS(roundID, null);
}, 1200);
}
function showVS(roundID, match) {
renderOpponent();
frame.classList.remove('is-vs-impact', 'is-screen-impact');
setScreen('vs');
void frame.offsetWidth;
frame.classList.add('is-vs-impact', 'is-screen-impact');
postBridge('gameRPSMatched', {
game_code: GAME_CODE,
game_id: GAME_ID,
room_id: currentRoomID(),
});
schedule(function () {
frame.classList.remove(
'is-vs-impact',
'is-screen-impact'
);
if (roundID !== state.roundID) return;
runCountdown(roundID, match || null);
}, 1050);
}
function showBattle(roundID, match) {
if (match) renderMatch(match);
else {
renderSelection();
renderOpponent();
}
var result = match
? resultFromMatch(match)
: resultFor(
state.selectedGesture,
state.opponentGesture
);
setScreen('battle');
setSelfWinCompare(result === 'win');
postBridge('gameRPSRevealing', {
game_code: GAME_CODE,
game_id: GAME_ID,
self_gesture: state.selectedGesture,
opponent_gesture: state.opponentGesture,
});
schedule(function () {
if (roundID !== state.roundID) return;
finishRound(result, match || null);
}, 1800);
}
function finishRound(result, match) {
state.busy = false;
if (match) renderMatch(match);
frame.classList.remove(
'is-countdown',
'is-self-win-compare'
);
setCountdownNumber('');
setScreen('victory');
renderVictory(result, match);
trackSelfGameEvent('settlement_show', {
match_id: match && (match.match_id || match.matchId),
result: result,
gesture: state.selectedGesture,
stake_coin: state.selectedStake,
});
postBridge('gameRPSSettled', {
game_code: GAME_CODE,
game_id: GAME_ID,
match_id: match && (match.match_id || match.matchId),
result: result,
self_gesture: state.selectedGesture,
opponent_gesture: state.opponentGesture,
stake_coin: state.selectedStake,
});
}
function resetToLobby() {
clearTimers();
state.busy = false;
state.roundID += 1;
state.match = null;
state.opponent = {};
cancelCoinCreditAnimation();
frame.classList.remove(
'is-vs-impact',
'is-screen-impact',
'is-countdown',
'is-self-win-compare'
);
setCountdownNumber('');
setScreen('lobby');
renderSelection();
renderProfile();
renderOpponent();
}
function cancelMatch() {
var match = state.match;
var matchID = match && (match.match_id || match.matchId);
if (!rpsAPI() || !matchID || isSettledMatch(match)) {
return Promise.resolve();
}
return rpsAPI()
.cancel(matchID)
.then(function () {
trackSelfGameEvent('match_cancel', {
match_id: matchID,
});
})
.catch(function () {
trackSelfGameEvent('cancel_api', {
match_id: matchID,
success: false,
error_code: 'cancel_failed',
});
});
}
function openRecharge() {
var payload = {
source: 'rock',
game_code: GAME_CODE,
game_id: GAME_ID,
};
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;
window.HyAppGamePreloader.complete({
root: root,
frame: frame,
loadingScreen: role('loading-screen'),
progressNode: role('loading-progress'),
});
}
function collectRPSPreloadURLs() {
return window.HyAppGamePreloader.collectResourceURLs(
RPS_PRELOAD_ASSET_URLS
);
}
function preloadResourceOnce(url) {
return window.HyAppGamePreloader.preloadResource(url);
}
function preloadProfileImages() {
var urls = [];
var seen = {};
window.HyAppGamePreloader.addURL(
urls,
seen,
avatarURL(state.profile)
);
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 loadProfile() {
if (shouldUseMockFlow()) {
renderProfile();
return Promise.resolve();
}
if (!api().user || !api().user.me) {
renderProfile();
return Promise.resolve();
}
return api()
.user.me()
.then(function (profile) {
state.profile = profile || {};
renderProfile();
})
.catch(function () {
renderProfile();
});
}
function loadBalance() {
if (shouldUseMockFlow()) {
renderCoin(state.balanceCoin);
return Promise.resolve();
}
if (!api().wallet || !api().wallet.balances) {
renderCoin(state.balanceCoin);
return Promise.resolve();
}
return api()
.wallet.balances('COIN')
.then(function (data) {
var balances = (data && data.balances) || [];
for (var i = 0; i < balances.length; i += 1) {
if (balances[i].asset_type === 'COIN') {
renderCoin(
balances[i].available_amount || 0
);
return;
}
}
renderCoin(state.balanceCoin);
})
.catch(function () {
renderCoin(state.balanceCoin);
});
}
function loadConfig() {
if (!rpsAPI() || shouldUseMockFlow()) {
renderStakeOptions(state.config);
return Promise.resolve();
}
var configStartedAt = performance.now();
return rpsAPI()
.config(GAME_ID)
.then(function (data) {
trackSelfGameEvent('config_load_success', {
duration_ms: Math.round(
performance.now() - configStartedAt
),
});
state.config =
(data && (data.config || data.rps_config)) ||
data ||
null;
renderStakeOptions(state.config);
})
.catch(function () {
trackSelfGameEvent('config_load_fail', {
duration_ms: Math.round(
performance.now() - configStartedAt
),
success: false,
error_code: 'config_failed',
});
renderStakeOptions(state.config);
});
}
function loadStartup() {
var profileTask =
window.HyAppGamePreloader.once(loadProfile);
var tasks = [
profileTask,
window.HyAppGamePreloader.once(loadBalance),
window.HyAppGamePreloader.once(loadConfig),
];
collectRPSPreloadURLs().forEach(function (url) {
tasks.push(function () {
return preloadResourceOnce(url);
});
});
tasks.push(function () {
return profileTask().then(preloadProfileImages);
});
// 启动期统一等用户、余额、配置和当前页面图片都进入缓存,再放开大厅点击,避免刚进页面时边操作边抖动加载。
return runPreloadTasks(tasks)
.then(function () {
return window.HyAppGamePreloader.delay(180);
})
.then(function () {
completeLoadingScreen();
trackSelfGameEvent('preload_complete', {
duration_ms: Math.round(
performance.now() - pageStartedAt
),
asset_count: tasks.length,
});
postBridge('gameRPSReady', {
game_code: GAME_CODE,
game_id: GAME_ID,
room_id: currentRoomID(),
});
});
}
function refreshLocalizedRuntimeText() {
renderSelection();
renderStakeOptions(state.config);
if (
!state.busy &&
frame.getAttribute('data-state') === 'lobby'
) {
setText(role('message'), '');
}
}
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') {
if (state.busy) {
cancelMatch().then(resetToLobby);
return;
}
postBridge('back');
return;
}
if (action === 'recharge') {
openRecharge();
return;
}
if (state.preloading) return;
if (action === 'gesture') {
if (state.busy) return;
state.selectedGesture =
target.getAttribute('data-gesture') || 'rock';
renderSelection();
trackSelfGameEvent('gesture_select', {
gesture: state.selectedGesture,
});
return;
}
if (action === 'stake') {
if (state.busy) return;
state.selectedStake = Number(
target.getAttribute('data-stake') || 0
);
each(
document.querySelectorAll(
'[data-action="stake"]'
),
function (button) {
button.setAttribute(
'aria-pressed',
button === target ? 'true' : 'false'
);
}
);
trackSelfGameEvent('stake_select', {
stake_coin: state.selectedStake,
});
return;
}
if (action === 'start') {
if (!state.selectedStake) {
showMessage(
i18nText(
'rockGame.selectStake',
'Select a stake'
)
);
return;
}
startMatch();
return;
}
if (action === 'rematch') {
cancelMatch().then(function () {
resetToLobby();
trackSelfGameEvent('play_again', {
source: 'rematch',
stake_coin: state.selectedStake,
gesture: state.selectedGesture,
});
startMatch();
});
return;
}
if (action === 'play-again') {
trackSelfGameEvent('play_again', {
source: 'settlement',
stake_coin: state.selectedStake,
gesture: state.selectedGesture,
});
resetToLobby();
}
});
}
window.HyAppRPSGame = {
reset: resetToLobby,
start: startMatch,
finish: function (payload) {
payload = payload || {};
if (
payload.self_gesture &&
gestures[payload.self_gesture]
) {
state.selectedGesture = payload.self_gesture;
}
if (
payload.opponent_gesture &&
gestures[payload.opponent_gesture]
) {
state.opponentGesture = payload.opponent_gesture;
}
if (payload.opponent) state.opponent = payload.opponent;
renderSelection();
renderOpponent();
finishRound(
payload.result ||
resultFor(
state.selectedGesture,
state.opponentGesture
)
);
},
};
updateScale();
initActions();
trackSelfGameEvent('page_open', {
duration_ms: Math.round(performance.now() - pageStartedAt),
});
window.addEventListener('beforeunload', function () {
cancelMatch();
});
renderCoin(state.balanceCoin);
renderSelection();
renderOpponent();
loadStartup().catch(function (error) {
showMessage(error && error.message);
completeLoadingScreen();
postBridge('gameRPSReady', {
game_code: GAME_CODE,
game_id: GAME_ID,
room_id: currentRoomID(),
});
});
window.addEventListener(
'hyapp:i18n-ready',
refreshLocalizedRuntimeText
);
window.addEventListener('resize', updateScale);
window.addEventListener('orientationchange', updateScale);
if (window.HyAppBridge && window.HyAppBridge.ready) {
window.HyAppBridge.ready({
page: 'game/rock',
figma_node_id: '491:135',
});
}
})();
</script>
</body>
</html>