diff --git a/game/rock/assets/figma/start-button.png b/game/rock/assets/figma/start-button.png index d4c6887..1f8ff9d 100644 Binary files a/game/rock/assets/figma/start-button.png and b/game/rock/assets/figma/start-button.png differ diff --git a/game/rock/assets/figma/victory-action-blue.png b/game/rock/assets/figma/victory-action-blue.png index d4c6887..67c2204 100644 Binary files a/game/rock/assets/figma/victory-action-blue.png and b/game/rock/assets/figma/victory-action-blue.png differ diff --git a/game/rock/assets/figma/victory-action-secondary.png b/game/rock/assets/figma/victory-action-secondary.png new file mode 100644 index 0000000..141b4f0 Binary files /dev/null and b/game/rock/assets/figma/victory-action-secondary.png differ diff --git a/game/rock/assets/figma/victory-title.png b/game/rock/assets/figma/victory-title.png index 70cb79e..4686ca8 100644 Binary files a/game/rock/assets/figma/victory-title.png and b/game/rock/assets/figma/victory-title.png differ diff --git a/game/rock/index.html b/game/rock/index.html index 676bacf..e3351ab 100644 --- a/game/rock/index.html +++ b/game/rock/index.html @@ -99,7 +99,8 @@ top: 60px; left: 16px; z-index: 30; - display: flex; + display: grid; + grid-template-columns: 46px minmax(0, 1fr) max-content; align-items: center; justify-content: space-between; width: 343px; @@ -109,20 +110,28 @@ .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: absolute; - top: 50%; - left: 50%; + position: relative; + justify-self: center; width: 36px; height: 36px; overflow: hidden; border-radius: 6px; - transform: translate(-50%, -50%); } .top-coin { @@ -130,10 +139,11 @@ display: flex; align-items: center; justify-content: flex-end; + justify-self: end; width: max-content; min-width: 102px; max-width: 176px; - height: 32px; + height: 36px; flex: 0 1 auto; gap: 4px; overflow: visible; @@ -156,10 +166,12 @@ } .top-coin span { - display: block; + display: flex; + align-items: center; min-width: max-content; max-width: none; flex: 0 0 auto; + height: 100%; overflow: visible; line-height: 20px; } @@ -181,10 +193,11 @@ display: flex; align-items: center; justify-content: center; - margin-bottom: 5px; + 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); @@ -732,6 +745,14 @@ 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; @@ -800,6 +821,10 @@ height: 15px; } + .rps-frame.has-opponent .rps-search-hud { + display: none; + } + .rps-bottom-status { position: absolute; left: 74px; @@ -1012,7 +1037,7 @@ } .rps-victory-button.is-secondary::before { - background-image: url('assets/figma/victory-action-secondary.svg'); + background-image: url('assets/figma/victory-action-secondary.png'); filter: drop-shadow(0 0 5px rgba(255, 227, 142, 0.34)); } @@ -1337,9 +1362,20 @@
+
?
-
?
+
+
?
+
+
+
?
@@ -1420,9 +1461,11 @@
+
?
@@ -1585,10 +1628,7 @@ selectedGesture: 'rock', selectedStake: 8000, opponentGesture: 'paper', - opponent: { - nickname: 'NameName', - display_user_id: 'Name...', - }, + opponent: {}, config: null, match: null, busy: false, @@ -1818,6 +1858,17 @@ 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; @@ -1912,12 +1963,52 @@ ); } + 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 || {}; - setNameLines('[data-role="opponent-vs-name"]', opponent); - setNameLines( + 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 + 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'), @@ -2147,6 +2238,9 @@ if (readGesture(opponent)) { state.opponentGesture = readGesture(opponent); } + } else { + // 等待匹配时必须清空上一局对手资料,否则搜索页会复用旧头像并提前隐藏搜索状态。 + state.opponent = {}; } renderSelection(); renderOpponent(); @@ -2361,6 +2455,8 @@ state.busy = true; state.roundID += 1; state.match = null; + state.opponent = {}; + renderOpponent(); setScreen('searching'); postBridge('gameRPSMatchRequested', { game_code: GAME_CODE, @@ -2400,7 +2496,8 @@ .then(function (data) { var match = matchFromData(data); trackSelfGameEvent('match_success', { - match_id: match && (match.match_id || match.matchId), + match_id: + match && (match.match_id || match.matchId), stake_coin: state.selectedStake, gesture: state.selectedGesture, duration_ms: Math.round( @@ -2410,19 +2507,19 @@ 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, + 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; + showMessage(error.message); + setScreen('lobby'); }); - state.busy = false; - showMessage(error.message); - setScreen('lobby'); - }); } function startMockMatch() { @@ -2430,10 +2527,12 @@ 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'); @@ -2532,6 +2631,7 @@ state.busy = false; state.roundID += 1; state.match = null; + state.opponent = {}; frame.classList.remove('is-vs-impact', 'is-screen-impact'); setScreen('lobby'); renderSelection(); diff --git a/game/touzi/assets/figma/dice-blue-1.png b/game/touzi/assets/figma/dice-blue-1.png new file mode 100644 index 0000000..d3321ee Binary files /dev/null and b/game/touzi/assets/figma/dice-blue-1.png differ diff --git a/game/touzi/assets/figma/dice-blue-2.png b/game/touzi/assets/figma/dice-blue-2.png new file mode 100644 index 0000000..a397f7a Binary files /dev/null and b/game/touzi/assets/figma/dice-blue-2.png differ diff --git a/game/touzi/assets/figma/dice-blue-3.png b/game/touzi/assets/figma/dice-blue-3.png new file mode 100644 index 0000000..514456c Binary files /dev/null and b/game/touzi/assets/figma/dice-blue-3.png differ diff --git a/game/touzi/assets/figma/dice-blue-4.png b/game/touzi/assets/figma/dice-blue-4.png new file mode 100644 index 0000000..18233a9 Binary files /dev/null and b/game/touzi/assets/figma/dice-blue-4.png differ diff --git a/game/touzi/assets/figma/dice-blue-5.png b/game/touzi/assets/figma/dice-blue-5.png new file mode 100644 index 0000000..e74133a Binary files /dev/null and b/game/touzi/assets/figma/dice-blue-5.png differ diff --git a/game/touzi/assets/figma/dice-blue-6.png b/game/touzi/assets/figma/dice-blue-6.png new file mode 100644 index 0000000..2871495 Binary files /dev/null and b/game/touzi/assets/figma/dice-blue-6.png differ diff --git a/game/touzi/assets/figma/dice-red-1.png b/game/touzi/assets/figma/dice-red-1.png new file mode 100644 index 0000000..af462cb Binary files /dev/null and b/game/touzi/assets/figma/dice-red-1.png differ diff --git a/game/touzi/assets/figma/dice-red-2.png b/game/touzi/assets/figma/dice-red-2.png new file mode 100644 index 0000000..454ff65 Binary files /dev/null and b/game/touzi/assets/figma/dice-red-2.png differ diff --git a/game/touzi/assets/figma/dice-red-3.png b/game/touzi/assets/figma/dice-red-3.png new file mode 100644 index 0000000..7ad530e Binary files /dev/null and b/game/touzi/assets/figma/dice-red-3.png differ diff --git a/game/touzi/assets/figma/dice-red-4.png b/game/touzi/assets/figma/dice-red-4.png new file mode 100644 index 0000000..c50f6db Binary files /dev/null and b/game/touzi/assets/figma/dice-red-4.png differ diff --git a/game/touzi/assets/figma/dice-red-5.png b/game/touzi/assets/figma/dice-red-5.png new file mode 100644 index 0000000..4cb7c50 Binary files /dev/null and b/game/touzi/assets/figma/dice-red-5.png differ diff --git a/game/touzi/assets/figma/dice-red-6.png b/game/touzi/assets/figma/dice-red-6.png new file mode 100644 index 0000000..e9660ac Binary files /dev/null and b/game/touzi/assets/figma/dice-red-6.png differ diff --git a/game/touzi/index.html b/game/touzi/index.html index 074c69b..80d5aa5 100644 --- a/game/touzi/index.html +++ b/game/touzi/index.html @@ -263,6 +263,32 @@ 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; @@ -1074,27 +1100,25 @@ .dice-score { position: absolute; - top: 302px; + top: 379px; z-index: 12; - width: 88px; - min-height: 34px; + width: 132px; + height: 132px; visibility: hidden; opacity: 0; - color: #ffffff; - font-size: 18px; - font-weight: 900; - line-height: 22px; - text-align: center; - text-shadow: 0 2px 5px rgba(0, 0, 0, 0.82); + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; transition: opacity 180ms ease; } .dice-score-self { - left: 34px; + left: 8px; } .dice-score-opponent { - left: 255px; + left: 230px; } .dice-frame.has-scores .dice-score { @@ -1102,6 +1126,14 @@ 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; @@ -1367,6 +1399,7 @@ .match-battle-only, .opponent-battle-only, .dice-roll-effect, + .coin-credit-flyer, .versus-only, .victory-only { transition: none; @@ -1381,7 +1414,9 @@ .dice-frame.is-matching, .dice-frame.is-searching .center-search-bg, .dice-frame.is-searching .search-indicator img, - .dice-roll-effect.is-fallback::before { + .dice-roll-effect.is-fallback::before, + .coin-credit-flyer, + .top-coin.is-crediting { animation: none; } } @@ -1593,6 +1628,49 @@ } } + @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; @@ -2435,6 +2513,8 @@ var pollTimer = 0; var roundSequence = 0; var pageStartedAt = performance.now(); + var coinCreditFrame = 0; + var coinCreditLayer = null; var rollEffectPlayers = { self: null, opponent: null, @@ -2739,6 +2819,147 @@ ); } + 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 ( @@ -2960,6 +3181,7 @@ function resetFrame() { stopRollEffects(); + cancelCoinCreditAnimation(); frame.classList.remove( 'is-searching', 'is-matching', @@ -3085,10 +3307,39 @@ return null; } - function scoreText(participant) { + function dicePointValue(participant) { var points = (participant && participant.dice_points) || []; if (!points.length) return ''; - return String(Number(points[0] || 0)); + 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) { @@ -3162,10 +3413,14 @@ setImage(role('opponent-avatar'), '', ''); setFrame(role('opponent-avatar-frame'), {}); } - var selfScore = scoreText(self); - var opponentScore = scoreText(opponent); - setText(role('self-score'), selfScore); - setText(role('opponent-score'), opponentScore); + 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 { @@ -3381,8 +3636,18 @@ formatCoin((winner && winner.payout_coin) || 0) ); if (self && self.balance_after > 0) { - setCoin(self.balance_after); + 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() { @@ -3401,8 +3666,7 @@ renderMatchParticipants(match); resetFrame(); frame.classList.add('is-victory'); - renderVictory(match); - refreshBalance(); + if (!renderVictory(match)) refreshBalance(); trackSelfGameEvent('settlement_show', { match_id: match.match_id, result: match.result, @@ -3557,8 +3821,8 @@ resetFrame(); setPhaseText(''); setActionText(matchingText()); - setText(role('self-score'), ''); - setText(role('opponent-score'), ''); + setDiceScore(role('self-score'), '', 'self'); + setDiceScore(role('opponent-score'), '', 'opponent'); setText(document.querySelector('.opponent-mark'), '?'); setText( document.querySelector('.matching-label'),