1518 lines
52 KiB
JavaScript
1518 lines
52 KiB
JavaScript
(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();
|
||
}
|
||
})();
|