2026-07-18 11:13:12 +08:00

1518 lines
52 KiB
JavaScript
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.

(function () {
'use strict';
var REQUEST_ID_KEY = 'aslan_game_king_pending_draw_request_id';
var PRIZE_FALLBACK = './aslan/assets/layers/prize-fallback.png';
var BOARD_TYPES = { tycoon: 'TYCOON', victorious: 'VICTORIOUS' };
// The product settles exactly one six-tier reward board: total Tycoon
// consumption. Victorious remains a display leaderboard, never a second
// payout board, so the global Award entry must not follow the active tab.
var SETTLEMENT_BOARD_TYPE = 'TYCOON';
var refs = {};
var mockStore = null;
var toastTimer = null;
var state = {
overview: null,
view: 'lottery',
boardType: 'TYCOON',
period: 'DAILY',
rankings: {},
drawBusy: false,
countdownTimer: null,
returnView: 'lottery',
};
function query(name) {
var params = new URLSearchParams(window.location.search || '');
return params.get(name);
}
function isMockMode() {
if (!/^(1|true|yes)$/i.test(String(query('mock') || ''))) return false;
var hostname = String(window.location.hostname || '').toLowerCase();
var env = String(query('env') || '').toLowerCase();
// Fixture data must never be enabled merely by adding ?mock=1 to a
// production activity URL. Allow it only on a local host or an explicit
// local/test environment used by the development WebView.
return (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
env === 'local' ||
env === 'test'
);
}
function t(key, fallback) {
return window.HyAppI18n
? window.HyAppI18n.t(key, fallback || key)
: fallback || key;
}
function format(key, values, fallback) {
var text = t(key, fallback);
Object.keys(values || {}).forEach(function (name) {
text = text.replace(
new RegExp('\\{' + name + '\\}', 'g'),
String(values[name])
);
});
return text;
}
function toNumber(value, fallback) {
var parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback || 0;
}
function asArray(value) {
if (Array.isArray(value)) return value;
if (value && Array.isArray(value.items)) return value.items;
if (value && Array.isArray(value.list)) return value.list;
if (value && Array.isArray(value.records)) return value.records;
return [];
}
function firstValue(source, names, fallback) {
for (var index = 0; index < names.length; index += 1) {
var value = source && source[names[index]];
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return fallback;
}
function safeMediaURL(value) {
var source = String(value || '').trim();
// Real prizes always come from the activity API. A production response without
// media must stay visibly empty instead of showing the mock Figma car as if it
// were a real reward; the local bitmap is reserved for ?mock=1 fixtures below.
if (!source) return '';
if (isMockMode() && source === PRIZE_FALLBACK) return source;
if (/^(https?:)?\/\//i.test(source) || /^data:image\//i.test(source)) {
return source;
}
return '';
}
function prizeFrom(raw, index) {
var source = raw || {};
return {
id: String(
firstValue(
source,
['id', 'prizeId', 'rewardId'],
'slot-' + index
)
),
name: String(
firstValue(
source,
['name', 'prizeName', 'rewardName', 'resourceName'],
t('gameKing.prizePending', 'Prize pending')
)
),
imageUrl: safeMediaURL(
firstValue(
source,
[
'prizeImage',
'imageUrl',
'iconUrl',
'resourceUrl',
'coverUrl',
'cover',
'sourceUrl',
],
''
)
),
quantity: Math.max(
1,
toNumber(
firstValue(
source,
['quantity', 'count', 'amount', 'num'],
1
),
1
)
),
sort: toNumber(
firstValue(source, ['sort', 'position', 'index'], index + 1),
index + 1
),
};
}
function normalizePrizes(raw) {
var source = asArray(raw)
.map(prizeFrom)
.sort(function (left, right) {
return left.sort - right.sort;
})
.slice(0, 7);
var configuredCount = source.length;
while (source.length < 7) source.push(prizeFrom({}, source.length));
source.configuredCount = configuredCount;
return source;
}
function rewardFrom(raw, index) {
var source = raw || {};
return {
id: String(
firstValue(
source,
['id', 'rewardId', 'resourceId'],
'reward-' + index
)
),
name: String(
firstValue(
source,
['name', 'rewardName', 'resourceName'],
t('gameKing.reward', 'Reward')
)
),
imageUrl: safeMediaURL(
firstValue(
source,
[
'imageUrl',
'iconUrl',
'resourceUrl',
'coverUrl',
'cover',
'sourceUrl',
],
''
)
),
quantity: Math.max(
1,
toNumber(
firstValue(
source,
['quantity', 'count', 'amount', 'num'],
1
),
1
)
),
};
}
function rewardTierFrom(raw, index) {
var source = raw || {};
var defaults = [
[1, 1],
[2, 2],
[3, 3],
[4, 7],
[8, 10],
[11, 30],
];
var fallback = defaults[index] || [index + 1, index + 1];
var start = toNumber(
firstValue(
source,
['rankStart', 'startRank', 'minRank', 'from'],
fallback[0]
),
fallback[0]
);
var end = toNumber(
firstValue(
source,
['rankEnd', 'endRank', 'maxRank', 'to'],
fallback[1]
),
fallback[1]
);
return {
start: start,
end: end,
rewards: asArray(
firstValue(
source,
['rewards', 'resources', 'rewardItems', 'items'],
[]
)
).map(rewardFrom),
settlementStatus: String(
firstValue(source, ['settlementStatus', 'status'], 'PENDING')
).toUpperCase(),
};
}
function normalizeRewardBoards(raw) {
var result = { TYCOON: [], VICTORIOUS: [] };
var source = raw || {};
if (Array.isArray(source)) {
source.forEach(function (item) {
var board = String(
firstValue(
item,
['boardType', 'rankingType', 'type'],
'TYCOON'
)
).toUpperCase();
if (!result[board]) return;
result[board].push(rewardTierFrom(item, result[board].length));
});
} else {
Object.keys(result).forEach(function (board) {
var items = source[board] || source[board.toLowerCase()] || [];
result[board] = asArray(items).map(rewardTierFrom);
});
}
return result;
}
function normalizeOverview(raw) {
var envelope = raw || {};
var activity =
envelope.activity ||
envelope.overview ||
envelope.current ||
envelope;
// detail has public prize/rank configuration at its outer level, while
// activity/me carry different subsets. A flattened snapshot lets every
// renderer consume one server-authoritative object without dropping fields.
var source =
activity === envelope
? envelope
: Object.assign({}, envelope, activity);
var prizes = normalizePrizes(
firstValue(
source,
['prizes', 'lotteryPrizes', 'prizeList'],
envelope.prizes || []
)
);
var rewards = firstValue(
source,
['rankingRewards', 'rewardTiers', 'rankRewards'],
envelope.rankingRewards || envelope.rewardTiers || {}
);
return {
id: String(firstValue(source, ['id', 'activityId'], '')),
name: String(
firstValue(
source,
['name', 'activityName', 'title'],
'Game King'
)
),
status: String(
firstValue(source, ['status', 'activityStatus'], 'ACTIVE')
).toUpperCase(),
startTime: firstValue(
source,
['startTime', 'beginTime', 'startedAt'],
''
),
endTime: firstValue(
source,
['endTime', 'finishTime', 'endedAt'],
''
),
coinPerChance: Math.max(
1,
toNumber(
firstValue(
source,
[
'coinPerChance',
'coinPerDraw',
'coinsPerChance',
'consumeThreshold',
'drawThreshold',
],
80000
),
80000
)
),
consumedCoins: Math.max(
0,
toNumber(
firstValue(
source,
[
'consumedCoins',
'totalConsumed',
'totalConsumedCoins',
'gameGoldConsumed',
'totalSpent',
],
0
),
0
)
),
availableChances: Math.max(
0,
toNumber(
firstValue(
source,
[
'availableChances',
'availableDrawCount',
'drawChances',
'remainingChances',
],
0
),
0
)
),
dailyDrawLimit: Math.max(
0,
toNumber(
firstValue(source, ['dailyDrawLimit', 'maxDrawsPerDay'], 0),
0
)
),
todayDrawCount: Math.max(
0,
toNumber(
firstValue(source, ['todayDrawCount', 'drawnToday'], 0),
0
)
),
prizes: prizes,
configuredPrizeCount: prizes.configuredCount,
rules: asArray(firstValue(source, ['rules', 'ruleList'], [])),
rankingRewards: normalizeRewardBoards(rewards),
};
}
function normalizeRank(raw, index) {
var source = raw || {};
return {
rank: Math.max(
1,
toNumber(
firstValue(
source,
['rank', 'ranking', 'position'],
index + 1
),
index + 1
)
),
userId: String(
firstValue(source, ['userId', 'uid', 'id', 'account'], '')
),
displayId: String(
firstValue(
source,
['displayId', 'showId', 'shortId', 'account'],
''
)
),
nickname: String(
firstValue(
source,
['nickname', 'nickName', 'userName', 'name', 'account'],
t('gameKing.anonymous', 'Anonymous')
)
),
avatarUrl: String(
firstValue(source, ['avatarUrl', 'avatar', 'headUrl'], '')
),
score: Math.max(
0,
toNumber(
firstValue(
source,
['score', 'value', 'coins', 'amount'],
0
),
0
)
),
};
}
function normalizeRanking(raw) {
var source = raw || {};
var list = asArray(
firstValue(
source,
['entries', 'rankings', 'records', 'items', 'list'],
source
)
).map(normalizeRank);
var own = firstValue(
source,
['my', 'myRank', 'self', 'currentUser'],
null
);
return {
list: list,
myRank: own ? normalizeRank(own, list.length) : null,
};
}
function normalizeDraw(raw) {
var source = raw || {};
// draw returns the delivery lifecycle inside record; keep the top-level
// fallback for older deployments without exposing failure diagnostics to UI.
var delivery = source.record || source.drawRecord || source;
var rewardSource = firstValue(source, ['rewards', 'rewardItems'], null);
var rewards = rewardSource
? asArray(rewardSource).map(rewardFrom)
: [rewardFrom(firstValue(source, ['prize', 'reward'], source), 0)];
return {
prizeId: String(
firstValue(
source,
['prizeId', 'rewardId'],
rewards[0] ? rewards[0].id : ''
)
),
rewards: rewards,
remainingChances: firstValue(
source,
['remainingChances', 'availableChances', 'availableDrawCount'],
null
),
deliveryStatus: String(
firstValue(delivery, ['deliveryStatus', 'status'], 'SUCCESS')
).toUpperCase(),
failureReason: String(
firstValue(
delivery,
['failureReason', 'deliveryFailureReason'],
''
)
),
};
}
function normalizeRecords(raw) {
return asArray(raw).map(function (item, index) {
var source = item || {};
var prize = rewardFrom(
firstValue(source, ['prize', 'reward'], source),
index
);
return {
id: String(firstValue(source, ['id', 'recordId'], index)),
name: prize.name,
imageUrl: prize.imageUrl,
quantity: prize.quantity,
createdAt: firstValue(
source,
['createdAt', 'drawTime', 'createTime'],
''
),
deliveryStatus: String(
firstValue(source, ['deliveryStatus', 'status'], 'SUCCESS')
).toUpperCase(),
failureReason: String(
firstValue(
source,
['failureReason', 'deliveryFailureReason'],
''
)
),
};
});
}
function delay(value, milliseconds) {
return new Promise(function (resolve) {
window.setTimeout(function () {
resolve(value);
}, milliseconds || 180);
});
}
function buildMockStore() {
var icon = PRIZE_FALLBACK;
var prizes = [
['Nitro Rider', 1],
['Speed King', 1],
['Golden Car', 1],
['Victory Badge', 3],
['Lucky Coins', 1000],
['Royal Frame', 1],
['Arena Gift', 5],
].map(function (item, index) {
return {
id: String(index + 1),
name: item[0],
quantity: item[1],
prizeImage: icon,
sort: index + 1,
};
});
var rewardNames = ['Royal Car', 'King Frame', 'Gold Coins'];
var ranges = [
[1, 1],
[2, 2],
[3, 3],
[4, 7],
[8, 10],
[11, 30],
];
function tiers(multiplier) {
return ranges.map(function (range, tierIndex) {
return {
rankStart: range[0],
rankEnd: range[1],
rankingType: 'TYCOON',
resources: rewardNames.map(function (name, rewardIndex) {
return {
id:
String(multiplier) +
'-' +
tierIndex +
'-' +
rewardIndex,
name: name,
cover: icon,
quantity: Math.max(
1,
multiplier *
(6 - tierIndex) *
(rewardIndex === 2 ? 500 : 1)
),
};
}),
};
});
}
function ranks(boardType, period) {
var names = [
'Kevin',
'Dora',
'Mina',
'Alex',
'Luna',
'Ryan',
'Elif',
'Noah',
'Sara',
'Leo',
'Arda',
];
return names.map(function (name, index) {
return {
rank: index + 1,
account: name + String(1635468 + index),
nickname: name,
avatarUrl: '',
score:
(boardType === 'TYCOON' ? 980000 : 720000) -
index * (period === 'DAILY' ? 53121 : 23121),
};
});
}
return {
overview: {
id: '2026-game-king-s2',
name: 'Game King Season 2',
activityStatus: 'ACTIVE',
startTime: new Date(Date.now() - 86400000).toISOString(),
endTime: new Date(
Date.now() + 6 * 86400000 + 23 * 3600000
).toISOString(),
coinPerDraw: 80000,
totalConsumed: 135000,
availableChances: 5,
dailyDrawLimit: 10,
todayDrawCount: 2,
prizes: prizes,
rankRewards: tiers(2),
},
rankings: {
TYCOON_DAILY: ranks('TYCOON', 'DAILY'),
TYCOON_OVERALL: ranks('TYCOON', 'OVERALL'),
VICTORIOUS_DAILY: ranks('VICTORIOUS', 'DAILY'),
VICTORIOUS_OVERALL: ranks('VICTORIOUS', 'OVERALL'),
},
records: [],
drawCursor: 0,
};
}
function activityApi() {
if (!isMockMode()) return window.AslanCommon.gameKing;
if (!mockStore) mockStore = buildMockStore();
return {
current: function () {
return delay(mockStore.overview);
},
ranking: function (activityId, boardType, period) {
var key = boardType + '_' + period;
return delay({
entries: mockStore.rankings[key] || [],
my: {
rank: 18,
account: 'You2088666',
score: boardType === 'TYCOON' ? 118300 : 96400,
},
});
},
draw: function (activityId, requestId) {
if (!mockStore.overview.availableChances) {
return Promise.reject(new Error('no_draw_chance'));
}
var prize = mockStore.overview.prizes[mockStore.drawCursor % 7];
mockStore.drawCursor += 1;
mockStore.overview.availableChances -= 1;
mockStore.records.unshift({
id: requestId,
prize: prize,
createdAt: new Date().toISOString(),
deliveryStatus: 'SUCCESS',
});
return delay({
prize: prize,
prizeId: prize.id,
remainingChances: mockStore.overview.availableChances,
record: { deliveryStatus: 'SUCCESS' },
});
},
records: function () {
return delay({ records: mockStore.records });
},
};
}
function cacheRefs() {
[
'pageLoading',
'pageError',
'retryButton',
'lotteryContent',
'wheelPrizes',
'spinButton',
'drawButton',
'spinChanceBadge',
'chanceCount',
'consumptionProgress',
'progressCopy',
'ruleSummary',
'ruleList',
'ruleButton',
'awardButton',
'recordsButton',
'primaryTabs',
'lotteryView',
'rankingView',
'rewardsView',
'periodTabs',
'rankLoading',
'rankError',
'rankRetryButton',
'rankEmpty',
'rankingContent',
'rankPodium',
'rankList',
'myRank',
'rewardTierList',
'rewardEmpty',
'rewardBoardName',
'rewardsBackButton',
'resultTitle',
'resultRewards',
'resultCopy',
'recordsLoading',
'recordsEmpty',
'recordsList',
'recordsModal',
'toast',
].forEach(function (id) {
refs[id] = document.getElementById(id);
});
}
function setPageState(name) {
refs.pageLoading.hidden = name !== 'loading';
refs.pageError.hidden = name !== 'error';
refs.lotteryContent.hidden = name !== 'ready';
}
function defaultRules() {
return [
t(
'gameKing.ruleEvent',
'Play eligible games during the event for a chance to win rewards.'
),
t(
'gameKing.ruleBoards',
'The event has Tycoon King and Victorious King leaderboards.'
),
t(
'gameKing.ruleTycoon',
'Tycoon King is ranked by gold spent in eligible games.'
),
t(
'gameKing.ruleVictorious',
'Victorious King is ranked by gold won in eligible games.'
),
t(
'gameKing.ruleSettlement',
'Ranking rewards are settled after the activity ends.'
),
];
}
function drawRules() {
if (!state.overview) return [];
return [
format(
'gameKing.ruleChance',
{ coins: numberText(state.overview.coinPerChance) },
'Receive 1 draw chance for every {coins} gold consumed in eligible games.'
),
state.overview.dailyDrawLimit
? format(
'gameKing.ruleDailyLimit',
{ count: state.overview.dailyDrawLimit },
'A maximum of {count} draws is available per day.'
)
: t(
'gameKing.ruleNoDailyLimit',
'Unused chances remain available during the activity.'
),
];
}
function fillList(element, items) {
element.textContent = '';
items.forEach(function (copy) {
var item = document.createElement('li');
item.textContent = String(copy || '');
element.appendChild(item);
});
}
function renderRules() {
fillList(refs.ruleSummary, drawRules());
fillList(
refs.ruleList,
state.overview && state.overview.rules.length
? state.overview.rules
: defaultRules()
);
}
function makeImage(url, alt, className) {
if (!url) {
var placeholder = document.createElement('span');
placeholder.className =
(className ? className + ' ' : '') + 'media-placeholder';
placeholder.setAttribute('role', 'img');
placeholder.setAttribute('aria-label', alt || '');
return placeholder;
}
var image = document.createElement('img');
image.src = safeMediaURL(url);
image.alt = alt || '';
if (className) image.className = className;
image.addEventListener('error', function () {
// Do not silently replace an unavailable production reward with fixture
// artwork; preserving the empty state avoids a misleading prize display.
image.replaceWith(makeImage('', alt, className));
});
return image;
}
function renderWheel() {
refs.wheelPrizes.textContent = '';
state.overview.prizes.forEach(function (prize) {
var item = document.createElement('li');
item.className = 'wheel-prize';
item.dataset.prizeId = prize.id;
item.appendChild(makeImage(prize.imageUrl, prize.name));
var copy = document.createElement('span');
copy.textContent = prize.name;
item.appendChild(copy);
refs.wheelPrizes.appendChild(item);
});
}
function numberText(value) {
try {
return new Intl.NumberFormat(
window.HyAppI18n ? window.HyAppI18n.lang() : 'en',
{ maximumFractionDigits: 0 }
).format(toNumber(value, 0));
} catch (_) {
return String(toNumber(value, 0));
}
}
function updateLotteryStatus() {
if (!state.overview) return;
var overview = state.overview;
var remainder = overview.consumedCoins % overview.coinPerChance;
var ratio = Math.min(1, remainder / overview.coinPerChance);
refs.chanceCount.textContent = numberText(overview.availableChances);
refs.spinChanceBadge.textContent =
'×' + numberText(overview.availableChances);
refs.consumptionProgress.style.width = ratio * 100 + '%';
if (overview.status === 'ENDED') {
refs.progressCopy.textContent = t(
'gameKing.activityEnded',
'The activity has ended.'
);
} else {
refs.progressCopy.textContent = format(
'gameKing.progressNext',
{ coins: numberText(overview.coinPerChance - remainder) },
'Consume {coins} more gold to receive another chance.'
);
}
var disabled =
state.drawBusy ||
overview.status === 'ENDED' ||
overview.availableChances < 1 ||
overview.configuredPrizeCount !== 7;
refs.spinButton.disabled = disabled;
refs.drawButton.disabled = disabled;
}
function renderOverview() {
renderWheel();
renderRules();
updateLotteryStatus();
startCountdown();
}
function startCountdown() {
if (state.countdownTimer) window.clearInterval(state.countdownTimer);
function tick() {
var end = state.overview
? new Date(state.overview.endTime).getTime()
: 0;
var remaining = Number.isFinite(end)
? Math.max(0, end - Date.now())
: 0;
var days = Math.floor(remaining / 86400000);
var hours = Math.floor((remaining % 86400000) / 3600000);
var minutes = Math.floor((remaining % 3600000) / 60000);
var seconds = Math.floor((remaining % 60000) / 1000);
var values = {
days: days,
hours: hours,
minutes: minutes,
seconds: seconds,
};
Object.keys(values).forEach(function (unit) {
var node = document.querySelector(
'[data-countdown="' + unit + '"]'
);
if (!node) return;
node.textContent =
unit === 'days'
? values[unit]
: String(values[unit]).padStart(2, '0');
});
if (!remaining && state.overview) {
state.overview.status = 'ENDED';
updateLotteryStatus();
}
}
tick();
state.countdownTimer = window.setInterval(tick, 1000);
}
function loadOverview() {
setPageState('loading');
return activityApi()
.current()
.then(function (data) {
state.overview = normalizeOverview(data);
renderOverview();
setPageState('ready');
})
.catch(function () {
setPageState('error');
});
}
function showView(view, boardType) {
state.view = view;
if (boardType) state.boardType = boardType;
// Awards is a Hero action with its own Figma composition, so hide the
// primary navigation only while preserving the normal lottery/ranking flow.
document
.getElementById('gameKingApp')
.classList.toggle('is-rewards-view', view === 'rewards');
refs.lotteryView.hidden = view !== 'lottery';
refs.rankingView.hidden = view !== 'ranking';
refs.rewardsView.hidden = view !== 'rewards';
refs.lotteryView.classList.toggle('is-active', view === 'lottery');
refs.rankingView.classList.toggle('is-active', view === 'ranking');
refs.rewardsView.classList.toggle('is-active', view === 'rewards');
refs.primaryTabs
.querySelectorAll('[data-view]')
.forEach(function (button) {
var buttonView = button.dataset.view;
var active =
(view === 'lottery' && buttonView === 'lottery') ||
(view !== 'lottery' &&
BOARD_TYPES[buttonView] === state.boardType);
button.classList.toggle('is-active', active);
});
if (view === 'ranking') loadRanking();
if (view === 'rewards') renderRewards();
}
function rankingKey() {
return state.boardType + '_' + state.period;
}
function setRankState(name) {
refs.rankLoading.hidden = name !== 'loading';
refs.rankError.hidden = name !== 'error';
refs.rankEmpty.hidden = name !== 'empty';
refs.rankingContent.hidden = name !== 'ready';
}
function appendAvatar(parent, user, className) {
if (user.avatarUrl) {
parent.appendChild(
makeImage(user.avatarUrl, user.nickname, className)
);
return;
}
var fallback = document.createElement('span');
fallback.className = className + ' avatar-fallback';
fallback.textContent = user.nickname.slice(0, 1).toUpperCase();
fallback.setAttribute('aria-label', user.nickname);
parent.appendChild(fallback);
}
function scoreLabel(score) {
return format(
'gameKing.goldScore',
{ score: numberText(score) },
'{score} gold'
);
}
function renderPodium(users) {
refs.rankPodium.textContent = '';
users.slice(0, 3).forEach(function (user) {
var item = document.createElement('li');
item.className = 'podium-user';
item.dataset.rank = user.rank;
var ring = document.createElement('div');
ring.className = 'podium-ring';
appendAvatar(ring, user, 'podium-avatar');
var rank = document.createElement('span');
rank.className = 'podium-rank';
rank.textContent = format(
'gameKing.topSingle',
{ rank: user.rank },
'TOP {rank}'
);
ring.appendChild(rank);
item.appendChild(ring);
var name = document.createElement('strong');
name.className = 'podium-name';
name.textContent = user.nickname;
item.appendChild(name);
var score = document.createElement('small');
score.className = 'podium-score';
// The Figma podium plate is intentionally compact. Keep the unit in
// the accessible label while rendering only the live numeric score.
score.textContent = numberText(user.score);
score.setAttribute('aria-label', scoreLabel(user.score));
item.appendChild(score);
refs.rankPodium.appendChild(item);
});
}
function createRankRow(user) {
var item = document.createElement('li');
item.className = 'rank-row';
var rank = document.createElement('strong');
rank.className = 'rank-number';
rank.textContent = String(user.rank);
item.appendChild(rank);
appendAvatar(item, user, 'rank-avatar');
var copy = document.createElement('span');
copy.className = 'rank-user-copy';
var name = document.createElement('strong');
name.textContent = user.nickname;
var id = document.createElement('small');
id.textContent = user.displayId
? t('gameKing.idPrefix', 'ID:') + ' ' + user.displayId
: '';
copy.appendChild(name);
copy.appendChild(id);
item.appendChild(copy);
var score = document.createElement('strong');
score.className = 'rank-score';
score.textContent = numberText(user.score);
item.appendChild(score);
return item;
}
function renderRanking(data) {
renderPodium(data.list);
refs.rankList.textContent = '';
data.list.slice(3).forEach(function (user) {
refs.rankList.appendChild(createRankRow(user));
});
refs.myRank.textContent = '';
if (data.myRank) {
var own = createRankRow(data.myRank);
while (own.firstChild) refs.myRank.appendChild(own.firstChild);
refs.myRank.hidden = false;
} else {
refs.myRank.hidden = true;
}
}
function loadRanking(force) {
var key = rankingKey();
if (!force && state.rankings[key]) {
renderRanking(state.rankings[key]);
setRankState(state.rankings[key].list.length ? 'ready' : 'empty');
return Promise.resolve();
}
setRankState('loading');
return activityApi()
.ranking(state.overview.id, state.boardType, state.period, 30)
.then(function (raw) {
var data = normalizeRanking(raw);
state.rankings[key] = data;
renderRanking(data);
setRankState(data.list.length ? 'ready' : 'empty');
})
.catch(function () {
setRankState('error');
});
}
function tierLabel(tier) {
if (tier.start === tier.end) {
return format(
'gameKing.topSingle',
{ rank: tier.start },
'TOP {rank}'
);
}
return format(
'gameKing.topRange',
{ start: tier.start, end: tier.end },
'TOP {start}-{end}'
);
}
function settlementLabel(status) {
if (status === 'SETTLED' || status === 'ISSUED') {
return t('gameKing.rewardsIssued', 'Rewards issued');
}
if (status === 'PROCESSING')
return t('gameKing.settling', 'Settling rewards');
return t('gameKing.settleAfterEnd', 'Settled after the activity ends');
}
function createRewardMedia(reward) {
var item = document.createElement('div');
item.className = 'tier-reward';
var media = document.createElement('div');
media.className = 'tier-reward-media';
media.appendChild(makeImage(reward.imageUrl, reward.name));
item.appendChild(media);
var count = document.createElement('div');
count.className = 'tier-reward-count';
// The card shows the visual quantity only; the reward name remains on the
// image alt/accessible node so long API names cannot distort Figma spacing.
count.setAttribute('aria-label', reward.name);
count.textContent = '×' + numberText(reward.quantity);
item.appendChild(count);
return item;
}
function renderRewards() {
refs.rewardBoardName.textContent = t(
'gameKing.tycoonOverallRewards',
'Tycoon King · Overall'
);
refs.rewardTierList.textContent = '';
var tiers =
state.overview && state.overview.rankingRewards
? state.overview.rankingRewards[SETTLEMENT_BOARD_TYPE] || []
: [];
refs.rewardEmpty.hidden = tiers.length > 0;
tiers.forEach(function (tier) {
var card = document.createElement('section');
card.className = 'reward-tier';
// The Figma card uses one shared image-fill sliced into top/middle/bottom;
// three clipped DOM layers preserve its corners without scaling a 317px
// panel as one bitmap over dynamic reward content.
var panelLayer = document.createElement('div');
panelLayer.className = 'reward-panel-layer';
['top', 'middle', 'bottom'].forEach(function (part) {
var slice = document.createElement('i');
slice.className = 'reward-panel-slice is-' + part;
slice.setAttribute('aria-hidden', 'true');
panelLayer.appendChild(slice);
});
card.appendChild(panelLayer);
var title = document.createElement('h3');
title.textContent = tierLabel(tier);
card.appendChild(title);
var rewards = document.createElement('div');
rewards.className = 'tier-rewards';
tier.rewards.forEach(function (reward) {
rewards.appendChild(createRewardMedia(reward));
});
card.appendChild(rewards);
var status = document.createElement('div');
status.className = 'reward-status';
// Ranking prizes are settled automatically after the event. This is a
// static design label, never a button or a fabricated claim operation.
status.textContent = t('gameKing.receive', 'Receive');
card.appendChild(status);
refs.rewardTierList.appendChild(card);
});
}
function stableRequestId() {
try {
var existing = window.sessionStorage.getItem(REQUEST_ID_KEY);
if (existing) return existing;
var created =
window.crypto && typeof window.crypto.randomUUID === 'function'
? window.crypto.randomUUID()
: 'gk-' +
Date.now() +
'-' +
Math.random().toString(16).slice(2);
window.sessionStorage.setItem(REQUEST_ID_KEY, created);
return created;
} catch (_) {
return (
'gk-' + Date.now() + '-' + Math.random().toString(16).slice(2)
);
}
}
function clearStableRequestId() {
try {
window.sessionStorage.removeItem(REQUEST_ID_KEY);
} catch (_) {}
}
function wait(milliseconds) {
return new Promise(function (resolve) {
window.setTimeout(resolve, milliseconds);
});
}
function isDeliveryPending(status) {
return (
status === 'PROCESSING' ||
status === 'PENDING' ||
status === 'UNKNOWN'
);
}
function deliveryMessage(status, failureReason) {
if (status === 'FAILED') {
// failureReason is retained only for the record contract. It may carry
// internal operational diagnostics, so users receive safe localized copy.
return t(
'gameKing.rewardDeliveryFailed',
'Reward delivery failed. Please contact support.'
);
}
if (status === 'UNKNOWN') {
return t(
'gameKing.rewardVerification',
'Reward status is being verified. Please contact support.'
);
}
if (isDeliveryPending(status)) {
return t(
'gameKing.rewardProcessing',
'Your reward is being delivered.'
);
}
return t(
'gameKing.rewardIssued',
'The reward has been automatically issued to your account.'
);
}
function renderResult(rewards, deliveryStatus, failureReason) {
var status = String(deliveryStatus || 'SUCCESS').toUpperCase();
refs.resultTitle.textContent =
status === 'FAILED'
? t(
'gameKing.rewardDeliveryFailedTitle',
'Reward delivery failed'
)
: status === 'UNKNOWN'
? t(
'gameKing.rewardVerificationTitle',
'Reward status is being verified'
)
: isDeliveryPending(status)
? t(
'gameKing.rewardProcessingTitle',
'Reward is processing'
)
: t('gameKing.congratulations', 'Congratulations');
refs.resultCopy.textContent = deliveryMessage(status, failureReason);
refs.resultRewards.textContent = '';
rewards.forEach(function (reward) {
var item = document.createElement('div');
item.className = 'result-reward';
item.appendChild(createRewardMedia(reward).firstChild);
var name = document.createElement('strong');
name.textContent = reward.name + ' ×' + numberText(reward.quantity);
item.appendChild(name);
refs.resultRewards.appendChild(item);
});
openModal('resultModal');
}
function highlightPrize(prizeId) {
refs.wheelPrizes
.querySelectorAll('.wheel-prize')
.forEach(function (node) {
node.classList.toggle(
'is-winning',
node.dataset.prizeId === String(prizeId)
);
});
}
function performDraw() {
if (state.drawBusy || !state.overview) return;
if (state.overview.availableChances < 1) {
showToast(t('gameKing.noChances', 'No draw chances available.'));
return;
}
state.drawBusy = true;
updateLotteryStatus();
document.querySelector('.wheel').classList.add('is-spinning');
highlightPrize('');
var requestId = stableRequestId();
var animation = wait(1550);
activityApi()
.draw(state.overview.id, requestId)
.then(function (raw) {
var result = normalizeDraw(raw);
return animation.then(function () {
clearStableRequestId();
if (result.remainingChances !== null) {
state.overview.availableChances = Math.max(
0,
toNumber(result.remainingChances, 0)
);
} else {
state.overview.availableChances = Math.max(
0,
state.overview.availableChances - 1
);
}
highlightPrize(result.prizeId);
renderResult(
result.rewards,
result.deliveryStatus,
result.failureReason
);
updateLotteryStatus();
});
})
.catch(function (error) {
// 不清除 requestId超时并不代表服务端未成功用户重试必须复用同一幂等键。
showToast(
error && error.message === 'no_draw_chance'
? t('gameKing.noChances', 'No draw chances available.')
: t(
'gameKing.drawFailed',
'Draw failed. Tap again to retry safely.'
)
);
})
.finally(function () {
document
.querySelector('.wheel')
.classList.remove('is-spinning');
state.drawBusy = false;
updateLotteryStatus();
});
}
function openModal(id) {
var modal = document.getElementById(id);
if (!modal) return;
modal.hidden = false;
document.body.style.overflow = 'hidden';
var focusable = modal.querySelector('button');
if (focusable) focusable.focus();
}
function closeModal(id) {
var modal = document.getElementById(id);
if (modal) modal.hidden = true;
if (!document.querySelector('.modal-backdrop:not([hidden])')) {
document.body.style.overflow = '';
}
}
function formatRecordTime(value) {
if (!value) return '';
var date = new Date(value);
if (!Number.isFinite(date.getTime())) return String(value);
try {
return new Intl.DateTimeFormat(
window.HyAppI18n ? window.HyAppI18n.lang() : 'en',
{
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}
).format(date);
} catch (_) {
return date.toLocaleString();
}
}
function loadRecords() {
refs.recordsLoading.hidden = false;
refs.recordsEmpty.hidden = true;
refs.recordsList.textContent = '';
openModal('recordsModal');
activityApi()
.records(state.overview ? state.overview.id : '', 50)
.then(function (raw) {
var records = normalizeRecords(raw);
refs.recordsEmpty.hidden = records.length > 0;
records.forEach(function (record) {
var item = document.createElement('li');
item.className = 'record-item';
item.appendChild(makeImage(record.imageUrl, record.name));
var copy = document.createElement('span');
var name = document.createElement('strong');
name.textContent =
record.name + ' ×' + numberText(record.quantity);
var time = document.createElement('small');
time.textContent = [
formatRecordTime(record.createdAt),
deliveryMessage(
record.deliveryStatus,
record.failureReason
),
]
.filter(Boolean)
.join(' · ');
copy.appendChild(name);
copy.appendChild(time);
item.appendChild(copy);
refs.recordsList.appendChild(item);
});
})
.catch(function () {
refs.recordsEmpty.hidden = false;
showToast(
t('gameKing.recordsFailed', 'Unable to load draw records.')
);
})
.finally(function () {
refs.recordsLoading.hidden = true;
});
}
function showToast(message) {
if (toastTimer) window.clearTimeout(toastTimer);
refs.toast.textContent = message;
refs.toast.hidden = false;
toastTimer = window.setTimeout(function () {
refs.toast.hidden = true;
}, 2600);
}
function bindEvents() {
refs.retryButton.addEventListener('click', loadOverview);
refs.rankRetryButton.addEventListener('click', function () {
loadRanking(true);
});
refs.spinButton.addEventListener('click', performDraw);
refs.drawButton.addEventListener('click', performDraw);
refs.ruleButton.addEventListener('click', function () {
openModal('ruleModal');
});
refs.awardButton.addEventListener('click', function () {
state.returnView = state.view === 'ranking' ? 'ranking' : 'lottery';
showView('rewards');
});
refs.rewardsBackButton.addEventListener('click', function () {
showView(state.returnView, state.boardType);
});
refs.recordsButton.addEventListener('click', loadRecords);
refs.primaryTabs.addEventListener('click', function (event) {
var button = event.target.closest('[data-view]');
if (!button) return;
var view = button.dataset.view;
if (view === 'lottery') showView('lottery');
else showView('ranking', BOARD_TYPES[view]);
});
refs.periodTabs.addEventListener('click', function (event) {
var button = event.target.closest('[data-period]');
if (!button || button.dataset.period === state.period) return;
state.period = button.dataset.period;
refs.periodTabs
.querySelectorAll('[data-period]')
.forEach(function (item) {
item.classList.toggle('is-active', item === button);
});
loadRanking();
});
document.addEventListener('click', function (event) {
var close = event.target.closest('[data-close-modal]');
if (close) closeModal(close.dataset.closeModal);
if (event.target.classList.contains('modal-backdrop'))
closeModal(event.target.id);
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') return;
document
.querySelectorAll('.modal-backdrop:not([hidden])')
.forEach(function (modal) {
closeModal(modal.id);
});
});
window.addEventListener('hyapp:i18n-ready', function () {
if (!state.overview) return;
renderRules();
updateLotteryStatus();
if (state.view === 'ranking' && state.rankings[rankingKey()]) {
renderRanking(state.rankings[rankingKey()]);
}
if (state.view === 'rewards') renderRewards();
});
}
function init() {
cacheRefs();
bindEvents();
loadOverview();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();