@@ -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'),