This commit is contained in:
zhx 2026-06-11 16:10:32 +08:00
parent b55c3b1415
commit 1a1bb33b48
50 changed files with 4018 additions and 190 deletions

View File

@ -83,6 +83,6 @@
</main>
<script src="../../common/i18n.js?v=20260611-1"></script>
<script src="./script.js?v=20260611-1"></script>
<script src="./script.js?v=20260611-invite-referrer"></script>
</body>
</html>

View File

@ -59,6 +59,61 @@
return androidURL;
}
function inviteCode() {
return firstParam([
'invite_code',
'inviteCode',
'code',
'invitationCode',
]);
}
function setInviteCodeParams(searchParams, code) {
searchParams.set('invite_code', code);
searchParams.set('inviteCode', code);
searchParams.set('code', code);
}
function setPlayInstallReferrer(searchParams, code) {
searchParams.set('invite_code', code);
searchParams.set('source', 'invite_h5');
}
function removeTopLevelInviteCodeParams(searchParams) {
['invite_code', 'inviteCode', 'code', 'invitationCode'].forEach(
function (name) {
searchParams.delete(name);
}
);
}
function buildDownloadURL(downloadURL, code) {
var rawURL = String(downloadURL || '').trim();
var normalizedCode = String(code || '').trim();
if (!rawURL || !normalizedCode) return rawURL;
try {
var url = new URL(rawURL, window.location.href);
if (url.hostname.indexOf('play.google.com') >= 0) {
var referrer = new URLSearchParams(
url.searchParams.get('referrer') || ''
);
setPlayInstallReferrer(referrer, normalizedCode);
removeTopLevelInviteCodeParams(url.searchParams);
url.searchParams.set('referrer', referrer.toString());
return url.toString();
}
setInviteCodeParams(url.searchParams, normalizedCode);
return url.toString();
} catch (error) {
var separator = rawURL.indexOf('?') >= 0 ? '&' : '?';
var params = new URLSearchParams();
setInviteCodeParams(params, normalizedCode);
return rawURL + separator + params.toString();
}
}
function renderProfile() {
var name = firstParam(['name', 'nickname', 'inviterName', 'userName']);
var avatar = firstParam([
@ -113,7 +168,7 @@
}
function openDownload() {
var url = resolveDownloadURL();
var url = buildDownloadURL(resolveDownloadURL(), inviteCode());
if (!url) {
showToast(
t(
@ -137,6 +192,10 @@
bindEvents();
}
window.HyInviteLanding = {
buildDownloadURL: buildDownloadURL,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {

View File

@ -145,25 +145,25 @@
<section class="ornate-panel stats-panel" aria-label="Invite stats">
<div class="stat">
<strong>168</strong>
<strong id="statInvitees">0</strong>
<span data-i18n="invite.totalInvitees">
Total number of invitees
</span>
</div>
<div class="stat">
<strong>01</strong>
<strong id="statEligible">0</strong>
<span data-i18n="invite.eligibleVoters">
Number of eligible voters
</span>
</div>
<div class="stat">
<strong>01</strong>
<strong id="statTotalRecharged">0</strong>
<span data-i18n="invite.totalRecharged">
Total amount recharged by invitees
</span>
</div>
<div class="stat">
<strong>12354</strong>
<strong id="statMyRewards">0</strong>
<span data-i18n="invite.myRewards">My Rewards</span>
</div>
</section>
@ -207,56 +207,9 @@
<div class="task-pane is-active" id="transportPane">
<div
class="timeline"
id="transportTimeline"
aria-label="Transportation milestones"
>
<article class="milestone is-done">
<h3 data-i18n="invite.milestoneTitle">
Milestone1:balabalabal
</h3>
<p data-i18n="invite.totalSupply">
Total supply: 49999 coins
</p>
</article>
<article class="milestone is-current">
<h3 data-i18n="invite.milestoneTitle">
Milestone1:balabalabal
</h3>
<p data-i18n="invite.totalSupply">
Total supply: 49999 coins
</p>
</article>
<article class="milestone">
<h3 data-i18n="invite.milestoneTitle">
Milestone1:balabalabal
</h3>
<p data-i18n="invite.totalSupply">
Total supply: 49999 coins
</p>
</article>
<article class="milestone">
<h3 data-i18n="invite.milestoneTitle">
Milestone1:balabalabal
</h3>
<p data-i18n="invite.totalSupply">
Total supply: 49999 coins
</p>
</article>
<article class="milestone">
<h3 data-i18n="invite.milestoneTitle">
Milestone1:balabalabal
</h3>
<p data-i18n="invite.totalSupply">
Total supply: 49999 coins
</p>
</article>
</div>
<button
class="ornate-button copy-task-button"
type="button"
data-i18n="invite.copy"
>
Copy
</button>
></div>
<p
class="subtle footnote"
data-i18n="invite.transportFootnote"
@ -266,62 +219,11 @@
</div>
<div class="task-pane" id="quotaPane">
<div class="quota-list" aria-label="Invitation quota tasks">
<article class="quota-card">
<div>
<h3 data-i18n="invite.milestoneTitle">
Milestone1:balabalabal
</h3>
<p data-i18n="invite.invitePaidFriends">
Invite 10 friends who have already made a
payment
</p>
</div>
<button
class="claim-button is-ready"
type="button"
data-i18n="invite.claim"
>
Claim
</button>
</article>
<article class="quota-card">
<div>
<h3 data-i18n="invite.milestoneTitle">
Milestone1:balabalabal
</h3>
<p data-i18n="invite.invitePaidFriends">
Invite 10 friends who have already made a
payment
</p>
</div>
<button
class="claim-button"
type="button"
data-i18n="invite.claim"
>
Claim
</button>
</article>
<article class="quota-card">
<div>
<h3 data-i18n="invite.milestoneTitle">
Milestone1:balabalabal
</h3>
<p data-i18n="invite.invitePaidFriends">
Invite 10 friends who have already made a
payment
</p>
</div>
<button
class="claim-button"
type="button"
data-i18n="invite.claim"
>
Claim
</button>
</article>
</div>
<div
class="quota-list"
id="quotaList"
aria-label="Invitation quota tasks"
></div>
<p class="subtle footnote" data-i18n="invite.quotaFootnote">
(You'll receive a reward upon reaching each level)
</p>
@ -351,7 +253,9 @@
})();
</script>
<script src="../../common/i18n.js?v=20260606-1"></script>
<script src="../../common/api.js?v=20260611-invite-reward"></script>
<script src="../../common/params.js?v=20260611-invite-referrer"></script>
<script src="../../common/toast.js"></script>
<script src="./script.js?v=20260606-6"></script>
<script src="./script.js?v=20260611-invite-reward"></script>
</body>
</html>

View File

@ -1,16 +1,209 @@
(function () {
var INVITE_CODE_KEYS = [
'invite_code',
'inviteCode',
'code',
'invitationCode',
];
var SHARE_NAME_KEYS = ['name', 'nickname', 'inviterName', 'userName'];
var SHARE_AVATAR_KEYS = ['avatar', 'avatarUrl', 'avatar_url', 'headUrl'];
var PASS_THROUGH_KEYS = [
'env',
'app_code',
'appCode',
'lang',
'downloadUrl',
'download_url',
'androidUrl',
'android_url',
'iosUrl',
'ios_url',
'url',
];
var rankRows = Array.from({ length: 30 }, function (_, index) {
return {
rank: index + 1,
name: "hanneem",
score: "123",
name: 'hanneem',
score: '123',
};
});
var params = new URLSearchParams(window.location.search);
var state = {
inviteCode: firstParam(INVITE_CODE_KEYS),
inviterName: firstParam(SHARE_NAME_KEYS),
inviterAvatar: firstParam(SHARE_AVATAR_KEYS),
inviteLink: '',
inviteRewardStatus: null,
claiming: {},
};
function $(selector) {
return document.querySelector(selector);
}
function firstParam(keys) {
for (var i = 0; i < keys.length; i += 1) {
var value = String(params.get(keys[i]) || '').trim();
if (value) return value;
}
return '';
}
function textValue(value) {
return String(value || '').trim();
}
function readPath(source, path) {
var value = source;
for (var i = 0; i < path.length; i += 1) {
if (!value || typeof value !== 'object') return '';
value = value[path[i]];
}
return textValue(value);
}
function firstPath(source, paths) {
for (var i = 0; i < paths.length; i += 1) {
var value = readPath(source, paths[i]);
if (value) return value;
}
return '';
}
function currentLandingURL() {
return new URL('../invite-landing/', window.location.href);
}
function setInviteCodeParams(searchParams, inviteCode) {
searchParams.set('invite_code', inviteCode);
searchParams.set('inviteCode', inviteCode);
searchParams.set('code', inviteCode);
}
function buildInviteLink() {
var code = textValue(state.inviteCode);
if (!code) return '';
var url = currentLandingURL();
PASS_THROUGH_KEYS.forEach(function (key) {
var value = textValue(params.get(key));
if (value) url.searchParams.set(key, value);
});
setInviteCodeParams(url.searchParams, code);
if (state.inviterName) {
url.searchParams.set('name', state.inviterName);
url.searchParams.set('nickname', state.inviterName);
}
if (state.inviterAvatar) {
url.searchParams.set('avatar', state.inviterAvatar);
url.searchParams.set('avatar_url', state.inviterAvatar);
}
return url.toString();
}
function renderInviteShare() {
state.inviteLink = buildInviteLink();
var link = $('#inviteLink');
var code = $('#inviteCode');
if (link) link.textContent = state.inviteLink || '--';
if (code) code.textContent = state.inviteCode || '--';
}
function readOverviewPayload(payload) {
return payload && payload.user ? payload.user : payload;
}
function applyCurrentUser(payload) {
var data = readOverviewPayload(payload || {});
var profile = data.profile || data;
var inviteCode = firstPath(data, [
['invite', 'my_invite_code'],
['invite', 'myInviteCode'],
['profile', 'invite', 'my_invite_code'],
['profile', 'invite', 'myInviteCode'],
['my_invite_code'],
['myInviteCode'],
['invite_code'],
['inviteCode'],
]);
var name = firstPath(data, [
['profile', 'username'],
['profile', 'nickname'],
['username'],
['nickname'],
]);
var avatar = firstPath(data, [
['profile', 'avatar'],
['profile', 'avatar_url'],
['avatar'],
['avatar_url'],
]);
if (inviteCode) state.inviteCode = inviteCode;
if (name) state.inviterName = name;
if (avatar) state.inviterAvatar = avatar;
var inviteOverview = data.invite || profile.invite || {};
var inviteCount = inviteOverview.invite_count;
var validInviteCount = inviteOverview.valid_invite_count;
if (
inviteCount !== undefined &&
inviteCount !== null &&
$('#statInvitees')
) {
$('#statInvitees').textContent = inviteCount;
}
if (
validInviteCount !== undefined &&
validInviteCount !== null &&
$('#statEligible')
) {
$('#statEligible').textContent = validInviteCount;
}
renderInviteShare();
}
function loadCurrentUser() {
var api = window.HyAppAPI || {};
var userAPI = api.user || api.users;
var hasToken = api.getAccessToken && api.getAccessToken();
var userPromise = Promise.resolve();
if (hasToken && api.get) {
userPromise = window.HyAppAPI.get(
'/api/v1/users/me/invite-overview'
)
.then(applyCurrentUser)
.catch(function () {});
}
if (window.HyAppParams && window.HyAppParams.loadUser) {
return userPromise
.then(function () {
return window.HyAppParams.loadUser();
})
.then(function (current) {
if (current && current.user) applyCurrentUser(current.user);
});
}
if (userAPI) {
if (userAPI.overview) {
return userPromise
.then(function () {
return userAPI.overview();
})
.then(applyCurrentUser);
}
if (userAPI.me) {
return userPromise
.then(function () {
return userAPI.me();
})
.then(applyCurrentUser);
}
}
return userPromise;
}
function toast(message) {
if (window.HyToast && window.HyToast.show) {
window.HyToast.show(message);
@ -27,14 +220,46 @@
}
function formatPeople(value) {
return t("invite.peopleSuffix", "{value} people").replace(
"{value}",
return t('invite.peopleSuffix', '{value} people').replace(
'{value}',
value
);
}
function formatNumber(value) {
return new Intl.NumberFormat(
window.HyAppI18n && window.HyAppI18n.lang
? window.HyAppI18n.lang()
: 'en'
).format(Number(value || 0));
}
function numberValue(value) {
var number = Number(value || 0);
return Number.isFinite(number) ? number : 0;
}
function field(source, snakeKey, camelKey) {
if (!source || typeof source !== 'object') return undefined;
if (Object.prototype.hasOwnProperty.call(source, snakeKey)) {
return source[snakeKey];
}
if (
camelKey &&
Object.prototype.hasOwnProperty.call(source, camelKey)
) {
return source[camelKey];
}
return undefined;
}
function setText(selector, value) {
var node = $(selector);
if (node) node.textContent = value;
}
function pad(value) {
return String(Math.max(0, value)).padStart(2, "0");
return String(Math.max(0, value)).padStart(2, '0');
}
function updateCountdown() {
@ -48,46 +273,48 @@
var hours = Math.floor((totalSeconds % 86400) / 3600);
var minutes = Math.floor((totalSeconds % 3600) / 60);
var seconds = totalSeconds % 60;
$("#days").textContent = pad(days);
$("#hours").textContent = pad(hours);
$("#minutes").textContent = pad(minutes);
$("#seconds").textContent = pad(seconds);
$('#days').textContent = pad(days);
$('#hours').textContent = pad(hours);
$('#minutes').textContent = pad(minutes);
$('#seconds').textContent = pad(seconds);
}
function renderRank() {
var list = $("#rankList");
list.textContent = "";
var list = $('#rankList');
list.textContent = '';
rankRows.forEach(function (item) {
var row = document.createElement("article");
var rank = document.createElement("span");
var medal = document.createElement("img");
var avatar = document.createElement("img");
var name = document.createElement("strong");
var score = document.createElement("span");
var row = document.createElement('article');
var rank = document.createElement('span');
var medal = document.createElement('img');
var avatar = document.createElement('img');
var name = document.createElement('strong');
var score = document.createElement('span');
row.className = "rank-row";
rank.className = "rank-no";
medal.className = "rank-medal";
avatar.className = "rank-avatar";
name.className = "rank-name";
score.className = "rank-score";
row.className = 'rank-row';
rank.className = 'rank-no';
medal.className = 'rank-medal';
avatar.className = 'rank-avatar';
name.className = 'rank-name';
score.className = 'rank-score';
if (item.rank <= 3) {
medal.src = "./assets/medal-" + item.rank + ".png";
medal.alt = "Rank " + item.rank;
rank.textContent = "";
medal.src = './assets/medal-' + item.rank + '.png';
medal.alt = 'Rank ' + item.rank;
rank.textContent = '';
} else {
medal.alt = "";
medal.alt = '';
rank.textContent = item.rank;
}
avatar.src = "./assets/rank-avatar.png";
avatar.alt = "";
avatar.src = './assets/rank-avatar.png';
avatar.alt = '';
name.textContent = item.name;
score.textContent = formatPeople(item.score);
row.appendChild(rank);
row.appendChild(item.rank <= 3 ? medal : document.createElement("span"));
row.appendChild(
item.rank <= 3 ? medal : document.createElement('span')
);
row.appendChild(avatar);
row.appendChild(name);
row.appendChild(score);
@ -95,56 +322,359 @@
});
}
function setTab(tabName) {
var monthly = $("#monthlyTask");
var isQuota = tabName === "quota";
monthly.classList.toggle("is-quota", isQuota);
function normalizeInviteRewardStatus(payload) {
if (!payload || typeof payload !== 'object') return null;
return payload.status && typeof payload.status === 'object'
? payload.status
: payload;
}
document.querySelectorAll(".task-tab").forEach(function (button) {
var active = button.getAttribute("data-tab") === tabName;
button.classList.toggle("is-active", active);
button.setAttribute("aria-selected", String(active));
function claimKey(rewardType, tierID) {
return rewardType + ':' + tierID;
}
function tierID(tier) {
return numberValue(field(tier, 'tier_id', 'tierId'));
}
function tierRewardType(tier) {
return String(field(tier, 'reward_type', 'rewardType') || '');
}
function tierName(tier, index, rewardType) {
return (
textValue(field(tier, 'tier_name', 'tierName')) ||
textValue(field(tier, 'tier_code', 'tierCode')) ||
(rewardType === 'recharge'
? t('invite.rechargeMilestone', 'Recharge milestone {index}')
: t(
'invite.validInviteMilestone',
'Invitation milestone {index}'
)
).replace('{index}', index + 1)
);
}
function thresholdValue(tier, rewardType) {
return rewardType === 'valid_invite'
? numberValue(
field(
tier,
'threshold_valid_invite_count',
'thresholdValidInviteCount'
)
)
: numberValue(
field(tier, 'threshold_coin_amount', 'thresholdCoinAmount')
);
}
function rewardCoinAmount(tier) {
return numberValue(
field(tier, 'reward_coin_amount', 'rewardCoinAmount')
);
}
function tierClaimed(tier) {
return Boolean(field(tier, 'claimed', 'claimed'));
}
function tierReached(tier) {
return Boolean(field(tier, 'reached', 'reached'));
}
function tierClaimable(tier, status) {
var rewardType = tierRewardType(tier);
var progress = (status && field(status, 'progress', 'progress')) || {};
if (
!Boolean(field(status || {}, 'enabled', 'enabled')) ||
tierClaimed(tier)
) {
return false;
}
if (rewardType === 'recharge') {
return (
tierID(tier) ===
numberValue(
field(
progress,
'recharge_claimable_tier_id',
'rechargeClaimableTierId'
)
)
);
}
return Boolean(field(tier, 'claimable', 'claimable'));
}
function taskDescription(tier, rewardType) {
var threshold = formatNumber(thresholdValue(tier, rewardType));
var reward = formatNumber(rewardCoinAmount(tier));
if (rewardType === 'valid_invite') {
return t(
'invite.validInviteTaskDescription',
'Invite {threshold} valid friends, reward {reward} coins'
)
.replace('{threshold}', threshold)
.replace('{reward}', reward);
}
return t(
'invite.rechargeTaskDescription',
'Invitees recharge {threshold} coins, reward {reward} coins'
)
.replace('{threshold}', threshold)
.replace('{reward}', reward);
}
function emptyTaskNode(message) {
var node = document.createElement('p');
node.className = 'task-empty';
node.textContent = message;
return node;
}
function buildClaimButton(tier, status) {
var rewardType = tierRewardType(tier);
var id = tierID(tier);
var key = claimKey(rewardType, id);
var claimed = tierClaimed(tier);
var ready = tierClaimable(tier, status);
var button = document.createElement('button');
button.className = 'claim-button';
if (ready) button.classList.add('is-ready');
button.type = 'button';
button.disabled = !ready || Boolean(state.claiming[key]);
button.textContent = state.claiming[key]
? t('invite.claiming', 'Claiming')
: claimed
? t('invite.claimed', 'Claimed')
: t('invite.claim', 'Claim');
button.addEventListener('click', function () {
claimInviteReward(tier);
});
return button;
}
function renderRewardTier(container, tier, index, status) {
var rewardType = tierRewardType(tier);
var article = document.createElement('article');
var content = document.createElement('div');
var title = document.createElement('h3');
var desc = document.createElement('p');
article.className =
rewardType === 'recharge' ? 'milestone' : 'quota-card';
article.classList.toggle('is-done', tierClaimed(tier));
article.classList.toggle(
'is-current',
tierReached(tier) && !tierClaimed(tier)
);
title.textContent = tierName(tier, index, rewardType);
desc.textContent = taskDescription(tier, rewardType);
content.appendChild(title);
content.appendChild(desc);
article.appendChild(content);
article.appendChild(buildClaimButton(tier, status));
container.appendChild(article);
}
function renderTaskList(selector, status, rewardType) {
var container = $(selector);
if (!container) return;
container.textContent = '';
var tiers = ((status && field(status, 'tiers', 'tiers')) || [])
.filter(function (tier) {
return (
tierRewardType(tier) === rewardType &&
String(field(tier, 'status', 'status') || 'active') !==
'inactive'
);
})
.sort(function (left, right) {
var sortLeft = numberValue(
field(left, 'sort_order', 'sortOrder')
);
var sortRight = numberValue(
field(right, 'sort_order', 'sortOrder')
);
if (sortLeft === sortRight) {
return (
thresholdValue(left, rewardType) -
thresholdValue(right, rewardType)
);
}
return sortLeft - sortRight;
});
if (!tiers.length) {
container.appendChild(
emptyTaskNode(t('invite.noRewardTasks', 'No reward tasks'))
);
return;
}
tiers.forEach(function (tier, index) {
renderRewardTier(container, tier, index, status);
});
}
function grantedRewardTotal(status) {
return ((status && field(status, 'claims', 'claims')) || []).reduce(
function (total, claim) {
if (
String(field(claim, 'status', 'status') || '') !== 'granted'
) {
return total;
}
return (
total +
numberValue(
field(claim, 'reward_coin_amount', 'rewardCoinAmount')
)
);
},
0
);
}
function renderInviteActivityReward(status) {
state.inviteRewardStatus = normalizeInviteRewardStatus(status);
var current = state.inviteRewardStatus || {};
var progress = field(current, 'progress', 'progress') || {};
if (state.inviteRewardStatus) {
setText(
'#statTotalRecharged',
formatNumber(
field(
progress,
'total_recharge_coin_amount',
'totalRechargeCoinAmount'
)
)
);
setText(
'#statEligible',
formatNumber(
field(progress, 'valid_invite_count', 'validInviteCount')
)
);
setText(
'#statMyRewards',
formatNumber(grantedRewardTotal(current))
);
}
renderTaskList('#transportTimeline', current, 'recharge');
renderTaskList('#quotaList', current, 'valid_invite');
}
function loadInviteActivityReward() {
var api = window.HyAppAPI && window.HyAppAPI.inviteActivityReward;
if (!api || !api.status || !window.HyAppAPI.getAccessToken()) {
renderInviteActivityReward(null);
return Promise.resolve();
}
return api
.status()
.then(function (status) {
renderInviteActivityReward(status);
})
.catch(function () {
renderInviteActivityReward(null);
});
}
function claimInviteReward(tier) {
var api = window.HyAppAPI && window.HyAppAPI.inviteActivityReward;
var rewardType = tierRewardType(tier);
var id = tierID(tier);
var key = claimKey(rewardType, id);
if (!api || !api.claim || state.claiming[key]) return;
if (!tierClaimable(tier, state.inviteRewardStatus)) {
toast(t('invite.notAvailable', 'Not available yet'));
return;
}
state.claiming[key] = true;
renderInviteActivityReward(state.inviteRewardStatus);
api.claim({
reward_type: rewardType,
tier_id: id,
command_id:
'invite_reward_' +
rewardType +
'_' +
id +
'_' +
Date.now() +
'_' +
Math.random().toString(36).slice(2),
})
.then(function (payload) {
delete state.claiming[key];
renderInviteActivityReward(
normalizeInviteRewardStatus(payload && payload.status)
);
toast(t('invite.claimSuccess', 'Claimed'));
})
.catch(function (error) {
delete state.claiming[key];
renderInviteActivityReward(state.inviteRewardStatus);
toast(
(error && error.message) ||
t('invite.claimFailed', 'Claim failed')
);
});
}
function setTab(tabName) {
var monthly = $('#monthlyTask');
var isQuota = tabName === 'quota';
monthly.classList.toggle('is-quota', isQuota);
document.querySelectorAll('.task-tab').forEach(function (button) {
var active = button.getAttribute('data-tab') === tabName;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', String(active));
});
$("#transportPane").classList.toggle("is-active", !isQuota);
$("#quotaPane").classList.toggle("is-active", isQuota);
$("#rankPanel").hidden = !isQuota;
$('#transportPane').classList.toggle('is-active', !isQuota);
$('#quotaPane').classList.toggle('is-active', isQuota);
$('#rankPanel').hidden = !isQuota;
}
function copyTextFrom(selector) {
var node = $(selector);
var text = node ? node.textContent.trim() : "";
if (!text) {
toast(t("invite.copyFailed", "Copy failed"));
var text = node ? node.textContent.trim() : '';
if (!text || text === '--') {
toast(t('invite.copyFailed', 'Copy failed'));
return;
}
function fallbackCopy() {
var input = document.createElement("textarea");
var input = document.createElement('textarea');
input.value = text;
input.setAttribute("readonly", "readonly");
input.style.position = "fixed";
input.style.left = "-9999px";
input.style.top = "0";
input.setAttribute('readonly', 'readonly');
input.style.position = 'fixed';
input.style.left = '-9999px';
input.style.top = '0';
document.body.appendChild(input);
input.select();
var ok = false;
try {
ok = document.execCommand("copy");
ok = document.execCommand('copy');
} catch (error) {
ok = false;
}
document.body.removeChild(input);
toast(
ok
? t("invite.copied", "Copied")
: t("invite.copyFailed", "Copy failed")
? t('invite.copied', 'Copied')
: t('invite.copyFailed', 'Copy failed')
);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(text)
.then(function () {
toast(t("invite.copied", "Copied"));
toast(t('invite.copied', 'Copied'));
})
.catch(function () {
fallbackCopy();
@ -155,51 +685,50 @@
}
function copyInviteLink() {
copyTextFrom("#inviteLink");
copyTextFrom('#inviteLink');
}
function copyInviteCode() {
copyTextFrom("#inviteCode");
copyTextFrom('#inviteCode');
}
function bindEvents() {
document.querySelectorAll(".task-tab").forEach(function (button) {
button.addEventListener("click", function () {
setTab(button.getAttribute("data-tab") || "transport");
document.querySelectorAll('.task-tab').forEach(function (button) {
button.addEventListener('click', function () {
setTab(button.getAttribute('data-tab') || 'transport');
});
});
$("#copyLinkButton").addEventListener("click", copyInviteLink);
$("#copyCodeButton").addEventListener("click", copyInviteCode);
$('#copyLinkButton').addEventListener('click', copyInviteLink);
$('#copyCodeButton').addEventListener('click', copyInviteCode);
document
.querySelectorAll(".copy-task-button")
.querySelectorAll('.copy-task-button')
.forEach(function (button) {
button.addEventListener("click", copyInviteLink);
button.addEventListener('click', copyInviteLink);
});
document.querySelectorAll(".claim-button").forEach(function (button) {
button.addEventListener("click", function () {
if (!button.classList.contains("is-ready")) {
toast(t("invite.notAvailable", "Not available yet"));
return;
}
toast(t("invite.claimed", "Claimed"));
button.classList.remove("is-ready");
});
});
}
function init() {
renderInviteShare();
renderRank();
updateCountdown();
setInterval(updateCountdown, 1000);
bindEvents();
renderInviteActivityReward(null);
Promise.all([
loadCurrentUser().catch(function () {
renderInviteShare();
}),
loadInviteActivityReward(),
]);
}
window.addEventListener("hyapp:i18n-ready", function () {
window.addEventListener('hyapp:i18n-ready', function () {
renderRank();
renderInviteActivityReward(state.inviteRewardStatus);
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}

View File

@ -505,10 +505,11 @@ p {
.milestone {
position: relative;
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) calc(220 * var(--u));
align-items: center;
min-height: calc(139 * var(--u));
flex-direction: column;
justify-content: center;
column-gap: calc(20 * var(--u));
margin-bottom: calc(30 * var(--u));
padding: calc(18 * var(--u)) calc(34 * var(--u));
background: url('./assets/milestone-row.svg') center / 100% 100% no-repeat;
@ -529,6 +530,11 @@ p {
background-image: url('./assets/timeline-node-active.png');
}
.milestone > div,
.quota-card > div {
min-width: 0;
}
.milestone h3,
.quota-card h3 {
color: var(--white);
@ -587,6 +593,18 @@ p {
background-image: url('./assets/claim-active.png');
}
.claim-button:disabled {
cursor: default;
}
.task-empty {
margin: calc(76 * var(--u)) auto calc(50 * var(--u));
color: var(--gold);
font-size: calc(32 * var(--u));
line-height: 1.3;
text-align: center;
}
.rank-panel {
min-height: calc(5124 * var(--u));
margin-top: calc(42 * var(--u));

View File

@ -673,6 +673,22 @@ var default_api = 'https://api.global-interaction.com/';
},
};
// 邀请活动 H5 只关心当前用户本月状态和领取动作;有效人数、累计充值和最高档差额都以后端聚合事实为准。
// 领取时页面必须传入本次点击生成的 command_id服务端按用户、周期、档位和 command_id 做幂等发奖。
var inviteActivityRewardAPI = {
status: function () {
return request('/api/v1/activities/invite-reward', {
method: 'GET',
});
},
claim: function (payload) {
return request('/api/v1/activities/invite-reward/claim', {
method: 'POST',
body: payload || {},
});
},
};
var diceGameAPI = {
config: function (gameID) {
return request('/api/v1/games/dice/config', {
@ -711,8 +727,47 @@ var default_api = 'https://api.global-interaction.com/';
},
};
var rpsGameAPI = {
config: function (gameID) {
return request('/api/v1/games/rps/config', {
method: 'GET',
query: { game_id: gameID || 'rock' },
});
},
match: function (payload) {
return request('/api/v1/games/rps/match', {
method: 'POST',
body: payload || {},
});
},
getMatch: function (matchID) {
return request(
'/api/v1/games/rps/matches/' +
encodeURIComponent(matchID || ''),
{ method: 'GET' }
);
},
roll: function (matchID, payload) {
return request(
'/api/v1/games/rps/matches/' +
encodeURIComponent(matchID || '') +
'/roll',
{ method: 'POST', body: payload || {} }
);
},
cancel: function (matchID) {
return request(
'/api/v1/games/rps/matches/' +
encodeURIComponent(matchID || '') +
'/cancel',
{ method: 'POST', body: {} }
);
},
};
var gameAPI = {
dice: diceGameAPI,
rps: rpsGameAPI,
};
window.HyAppAPI = {
@ -753,6 +808,7 @@ var default_api = 'https://api.global-interaction.com/';
level: levelAPI,
task: taskAPI,
weeklyStar: weeklyStarAPI,
inviteActivityReward: inviteActivityRewardAPI,
game: gameAPI,
};
clearLocalDevCache();

View File

@ -19,6 +19,25 @@
"diceMatch.selectStake": "اختر قيمة الرهان",
"diceMatch.requestFailed": "فشل الطلب",
"diceMatch.configFeeNote": "الرسوم {fee}%",
"rockGame.pageTitle": "حجر ورقة مقص",
"rockGame.pageLabel": "لعبة حجر ورقة مقص",
"rockGame.start": "ابدأ",
"rockGame.matching": "جارٍ البحث...",
"rockGame.matched": "تم العثور على خصم",
"rockGame.winnerReceives": "الفائز يحصل على",
"rockGame.rock": "حجر",
"rockGame.paper": "ورقة",
"rockGame.scissors": "مقص",
"rockGame.win": "فوز",
"rockGame.lose": "خسارة",
"rockGame.draw": "تعادل",
"rockGame.coins": "عملات",
"rockGame.rematch": "إعادة",
"rockGame.playAgain": "العب مرة أخرى",
"rockGame.changeLanguage": "تغيير اللغة",
"rockGame.selectStake": "اختر قيمة الرهان",
"rockGame.apiUnavailable": "واجهة RPS غير متاحة",
"rockGame.refundNote": "إذا لم ينضم أحد إلى لعبة حجر ورقة مقص، سيتم رد العملات خلال 10 دقائق.",
"roomReward.pageTitle": "فعالية مكافآت الغرفة",
"roomReward.pageLabel": "فعالية مكافآت الغرفة",
"roomReward.currentTarget": "هدف المكافأة الحالي",
@ -73,6 +92,16 @@
"invite.claim": "استلام",
"invite.transportFootnote": "(تُمنح مكافآت أعلى مستوى فقط)",
"invite.quotaFootnote": "(ستتلقى مكافأة عند الوصول إلى كل مستوى)",
"invite.claiming": "جار الاستلام...",
"invite.claimed": "تم الاستلام",
"invite.claimSuccess": "تم الاستلام",
"invite.claimFailed": "فشل الاستلام",
"invite.notAvailable": "غير متاح بعد",
"invite.noRewardTasks": "لا توجد مهام مكافآت",
"invite.rechargeMilestone": "مرحلة الشحن {index}",
"invite.validInviteMilestone": "مرحلة الدعوة {index}",
"invite.rechargeTaskDescription": "يشحن المدعوون {threshold} عملة، المكافأة {reward} عملة",
"invite.validInviteTaskDescription": "ادع {threshold} أصدقاء مؤهلين، المكافأة {reward} عملة",
"invite.rank": "الترتيب",
"invite.peopleSuffix": "{value} أشخاص",
"invite.copied": "تم النسخ",

View File

@ -19,6 +19,25 @@
"diceMatch.selectStake": "Select a stake",
"diceMatch.requestFailed": "Request failed",
"diceMatch.configFeeNote": "Fee {fee}%",
"rockGame.pageTitle": "Rock Paper Scissors",
"rockGame.pageLabel": "Rock paper scissors",
"rockGame.start": "START",
"rockGame.matching": "Matching...",
"rockGame.matched": "Matched",
"rockGame.winnerReceives": "The winner receives",
"rockGame.rock": "Rock",
"rockGame.paper": "Paper",
"rockGame.scissors": "Scissors",
"rockGame.win": "Win",
"rockGame.lose": "Lose",
"rockGame.draw": "Draw",
"rockGame.coins": "Coins",
"rockGame.rematch": "Re-match",
"rockGame.playAgain": "Play again",
"rockGame.changeLanguage": "Change language",
"rockGame.selectStake": "Select a stake",
"rockGame.apiUnavailable": "RPS API unavailable",
"rockGame.refundNote": "If no one joins the rock-paper-scissors game, the coins will be refunded in 10 minutes.",
"roomReward.pageTitle": "Room Reward Event",
"roomReward.pageLabel": "Room Reward Event",
"roomReward.currentTarget": "Current reward target",
@ -73,6 +92,16 @@
"invite.claim": "Claim",
"invite.transportFootnote": "(Only the highest-level bonuses may be awarded)",
"invite.quotaFootnote": "(You'll receive a reward upon reaching each level)",
"invite.claiming": "Claiming...",
"invite.claimed": "Claimed",
"invite.claimSuccess": "Claimed",
"invite.claimFailed": "Claim failed",
"invite.notAvailable": "Not available yet",
"invite.noRewardTasks": "No reward tasks",
"invite.rechargeMilestone": "Recharge milestone {index}",
"invite.validInviteMilestone": "Invitation milestone {index}",
"invite.rechargeTaskDescription": "Invitees recharge {threshold} coins, reward {reward} coins",
"invite.validInviteTaskDescription": "Invite {threshold} valid friends, reward {reward} coins",
"invite.rank": "Rank",
"invite.peopleSuffix": "{value} people",
"invite.copied": "Copied",

View File

@ -19,6 +19,25 @@
"diceMatch.selectStake": "Selecciona una apuesta",
"diceMatch.requestFailed": "Error de solicitud",
"diceMatch.configFeeNote": "Comisión {fee}%",
"rockGame.pageTitle": "Piedra, papel o tijeras",
"rockGame.pageLabel": "Piedra, papel o tijeras",
"rockGame.start": "INICIAR",
"rockGame.matching": "Emparejando...",
"rockGame.matched": "Emparejado",
"rockGame.winnerReceives": "El ganador recibe",
"rockGame.rock": "Piedra",
"rockGame.paper": "Papel",
"rockGame.scissors": "Tijeras",
"rockGame.win": "Victoria",
"rockGame.lose": "Derrota",
"rockGame.draw": "Empate",
"rockGame.coins": "Monedas",
"rockGame.rematch": "Revancha",
"rockGame.playAgain": "Jugar otra vez",
"rockGame.changeLanguage": "Cambiar idioma",
"rockGame.selectStake": "Selecciona una apuesta",
"rockGame.apiUnavailable": "API de RPS no disponible",
"rockGame.refundNote": "Si nadie se une al juego de piedra, papel o tijeras, las monedas se reembolsarán en 10 minutos.",
"roomReward.pageTitle": "Evento de recompensas de sala",
"roomReward.pageLabel": "Evento de recompensas de sala",
"roomReward.currentTarget": "Objetivo de recompensa actual",
@ -73,6 +92,16 @@
"invite.claim": "Reclamar",
"invite.transportFootnote": "(Solo se otorgan los bonos del nivel más alto)",
"invite.quotaFootnote": "(Recibirás una recompensa al alcanzar cada nivel)",
"invite.claiming": "Reclamando...",
"invite.claimed": "Reclamado",
"invite.claimSuccess": "Reclamado",
"invite.claimFailed": "Error al reclamar",
"invite.notAvailable": "Aún no disponible",
"invite.noRewardTasks": "No hay tareas de recompensa",
"invite.rechargeMilestone": "Hito de recarga {index}",
"invite.validInviteMilestone": "Hito de invitación {index}",
"invite.rechargeTaskDescription": "Los invitados recargan {threshold} monedas, recompensa {reward} monedas",
"invite.validInviteTaskDescription": "Invita a {threshold} amigos válidos, recompensa {reward} monedas",
"invite.rank": "Ranking",
"invite.peopleSuffix": "{value} personas",
"invite.copied": "Copiado",

View File

@ -62,6 +62,16 @@
"invite.claim": "Klaim",
"invite.transportFootnote": "(Hanya bonus level tertinggi yang diberikan)",
"invite.quotaFootnote": "(Kamu akan menerima hadiah saat mencapai setiap level)",
"invite.claiming": "Mengklaim...",
"invite.claimed": "Diklaim",
"invite.claimSuccess": "Diklaim",
"invite.claimFailed": "Klaim gagal",
"invite.notAvailable": "Belum tersedia",
"invite.noRewardTasks": "Belum ada tugas hadiah",
"invite.rechargeMilestone": "Milestone top up {index}",
"invite.validInviteMilestone": "Milestone undangan {index}",
"invite.rechargeTaskDescription": "Undangan top up {threshold} koin, hadiah {reward} koin",
"invite.validInviteTaskDescription": "Undang {threshold} teman valid, hadiah {reward} koin",
"invite.rank": "Peringkat",
"invite.peopleSuffix": "{value} orang",
"invite.copied": "Disalin",

View File

@ -62,6 +62,16 @@
"invite.claim": "Al",
"invite.transportFootnote": "(Yalnızca en yüksek seviye bonusları verilir)",
"invite.quotaFootnote": "(Her seviyeye ulaştığında ödül alırsın)",
"invite.claiming": "Alınıyor...",
"invite.claimed": "Alındı",
"invite.claimSuccess": "Alındı",
"invite.claimFailed": "Alma başarısız",
"invite.notAvailable": "Henüz mevcut değil",
"invite.noRewardTasks": "Ödül görevi yok",
"invite.rechargeMilestone": "Yükleme kilometre taşı {index}",
"invite.validInviteMilestone": "Davet kilometre taşı {index}",
"invite.rechargeTaskDescription": "Davetliler {threshold} coin yükler, ödül {reward} coin",
"invite.validInviteTaskDescription": "{threshold} geçerli arkadaş davet et, ödül {reward} coin",
"invite.rank": "Sıralama",
"invite.peopleSuffix": "{value} kişi",
"invite.copied": "Kopyalandı",

View File

@ -62,6 +62,16 @@
"invite.claim": "领取",
"invite.transportFootnote": "(只发放最高档位奖励)",
"invite.quotaFootnote": "(每达到一个等级即可获得奖励)",
"invite.claiming": "领取中...",
"invite.claimed": "已领取",
"invite.claimSuccess": "领取成功",
"invite.claimFailed": "领取失败",
"invite.notAvailable": "暂未达成",
"invite.noRewardTasks": "暂无奖励任务",
"invite.rechargeMilestone": "充值里程碑 {index}",
"invite.validInviteMilestone": "邀请里程碑 {index}",
"invite.rechargeTaskDescription": "邀请用户累计充值 {threshold} 金币,奖励 {reward} 金币",
"invite.validInviteTaskDescription": "邀请 {threshold} 位有效用户,奖励 {reward} 金币",
"invite.rank": "排行榜",
"invite.peopleSuffix": "{value}人",
"invite.copied": "已复制",

394
game/common/game-match.css Normal file
View File

@ -0,0 +1,394 @@
:root {
--game-frame-width: 375px;
--game-frame-height: 812px;
--game-scale: 1;
--game-screen-scale: var(--game-scale, var(--dice-scale, 1));
}
.game-stage {
position: relative;
width: calc(var(--game-frame-width) * var(--game-screen-scale));
height: calc(var(--game-frame-height) * var(--game-screen-scale));
overflow: hidden;
background: #030623;
}
.game-frame {
position: absolute;
top: 0;
left: 0;
width: var(--game-frame-width);
height: var(--game-frame-height);
overflow: hidden;
background: #030623;
transform: scale(var(--game-screen-scale));
transform-origin: top left;
}
.game-layer,
.layer {
position: absolute;
}
.game-layer-img,
.layer-img {
position: absolute;
display: block;
width: 100%;
height: 100%;
max-width: none;
pointer-events: none;
user-select: none;
}
.game-cover,
.cover {
object-fit: cover;
}
.game-contain,
.contain {
object-fit: contain;
}
.game-fill,
.fill {
object-fit: fill;
}
.game-top-bar {
position: absolute;
top: 36px;
left: 16px;
z-index: 30;
display: flex;
align-items: center;
justify-content: space-between;
width: 343px;
height: 36px;
}
.game-top-back {
position: relative;
width: 46px;
height: 36px;
flex: 0 0 46px;
}
.game-top-icon {
position: absolute;
top: 0;
left: 154px;
width: 36px;
height: 36px;
overflow: hidden;
border-radius: 6px;
}
.game-coin-pill {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 90px;
height: 29px;
gap: 4px;
padding: 0 7px 0 6px;
border: 1px solid rgba(255, 255, 255, 0.24);
border-radius: 15px;
background: rgba(0, 15, 63, 0.7);
color: #fff;
font-size: 16px;
line-height: 15px;
white-space: nowrap;
}
.game-coin-pill img {
width: 21px;
height: 21px;
flex: 0 0 21px;
filter: drop-shadow(0 0 5px rgba(255, 197, 49, 0.45));
}
.game-coin-plus {
color: #ffd15a;
font-size: 22px;
font-weight: 900;
line-height: 18px;
text-shadow: 0 0 8px rgba(255, 178, 47, 0.72);
}
.game-vs-badge {
transform-origin: 50% 50%;
will-change: transform, filter;
}
.game-vs-badge::after {
position: absolute;
inset: 14px 10px;
content: '';
border: 2px solid rgba(255, 213, 87, 0.68);
border-radius: 50%;
opacity: 0;
pointer-events: none;
transform: scale(0.62);
filter: drop-shadow(0 0 10px rgba(255, 96, 243, 0.68));
}
.is-vs-impact .game-vs-badge,
.is-matching .vs-badge {
animation: vs-drop-impact 900ms cubic-bezier(0.13, 0.92, 0.18, 1) both;
}
.is-vs-impact .game-vs-badge::after,
.is-matching .vs-badge::after {
animation: vs-impact-ring 580ms ease-out 430ms both;
}
.is-screen-impact {
animation: screen-impact-shake 520ms ease-out 420ms both;
}
.is-self-win-impact {
animation: you-win-screen-smash 780ms cubic-bezier(0.14, 0.84, 0.18, 1)
430ms both;
}
.game-countdown-number {
transform-origin: center;
}
.game-countdown-number.is-ticking {
animation: countdown-number-pop 980ms cubic-bezier(0.16, 0.95, 0.2, 1) both;
}
@media (prefers-reduced-motion: reduce) {
.is-screen-impact,
.is-self-win-impact,
.is-vs-impact .game-vs-badge,
.is-vs-impact .game-vs-badge::after,
.is-matching .vs-badge,
.is-matching .vs-badge::after {
animation: none !important;
}
}
@keyframes screen-impact-shake {
0%,
100% {
transform: translate3d(0, 0, 0) scale(var(--game-screen-scale));
}
12% {
transform: translate3d(0, 9px, 0) scale(var(--game-screen-scale));
}
24% {
transform: translate3d(-7px, -5px, 0) scale(var(--game-screen-scale));
}
36% {
transform: translate3d(6px, 4px, 0) scale(var(--game-screen-scale));
}
50% {
transform: translate3d(-4px, 2px, 0) scale(var(--game-screen-scale));
}
66% {
transform: translate3d(3px, -2px, 0) scale(var(--game-screen-scale));
}
82% {
transform: translate3d(-1px, 1px, 0) scale(var(--game-screen-scale));
}
}
@keyframes vs-drop-impact {
0% {
opacity: 0;
filter: brightness(1.9) drop-shadow(0 0 34px #ff79ff);
transform: translateY(-640px) scale(4.8) rotate(-12deg);
}
34% {
opacity: 1;
filter: brightness(1.75) drop-shadow(0 0 32px #4cc9ff);
transform: translateY(-120px) scale(2.35) rotate(5deg);
}
58% {
opacity: 1;
filter: brightness(1.62) drop-shadow(0 0 28px #ffd557);
transform: translateY(22px) scale(0.78) rotate(2deg);
}
72% {
filter: brightness(1.48) drop-shadow(0 0 24px #ff79ff);
transform: translateY(-12px) scale(1.18) rotate(-1deg);
}
86% {
filter: brightness(1.18) drop-shadow(0 0 14px #4cc9ff);
transform: translateY(4px) scale(0.96) rotate(0.5deg);
}
100% {
opacity: 1;
filter: brightness(1);
transform: translateY(0) scale(1) rotate(0);
}
}
@keyframes vs-impact-ring {
0% {
opacity: 0.95;
transform: scale(0.36);
}
100% {
opacity: 0;
transform: scale(1.92);
}
}
@keyframes you-win-drop-impact {
0% {
opacity: 0;
filter: brightness(1.9) drop-shadow(0 0 36px #ff79ff);
transform: translateY(-650px) scale(3.9) rotate(-7deg);
}
34% {
opacity: 1;
filter: brightness(1.75) drop-shadow(0 0 32px #4cc9ff);
transform: translateY(-122px) scale(2.05) rotate(4deg);
}
58% {
opacity: 1;
filter: brightness(1.55) drop-shadow(0 0 30px #ffd557);
transform: translateY(28px) scale(0.78) rotate(1deg);
}
72% {
filter: brightness(1.42) drop-shadow(0 0 24px #ff79ff);
transform: translateY(-16px) scale(1.16) rotate(-0.8deg);
}
86% {
filter: brightness(1.18) drop-shadow(0 0 16px #4cc9ff);
transform: translateY(4px) scale(0.96) rotate(0.3deg);
}
100% {
opacity: 1;
filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9))
drop-shadow(0 0 22px rgba(255, 205, 46, 0.9))
drop-shadow(0 0 36px rgba(255, 77, 0, 0.78));
transform: translateY(0) scale(1) rotate(0);
}
}
@keyframes you-win-screen-smash {
0%,
100% {
transform: translate3d(0, 0, 0) scale(var(--game-screen-scale));
filter: none;
}
12% {
transform: translate3d(0, 14px, 0) scale(var(--game-screen-scale));
filter: brightness(1.22) contrast(1.08);
}
24% {
transform: translate3d(-12px, -8px, 0) scale(var(--game-screen-scale));
}
36% {
transform: translate3d(10px, 7px, 0) scale(var(--game-screen-scale));
}
50% {
transform: translate3d(-7px, 4px, 0) scale(var(--game-screen-scale));
}
66% {
transform: translate3d(5px, -3px, 0) scale(var(--game-screen-scale));
filter: brightness(1.08) contrast(1.03);
}
82% {
transform: translate3d(-2px, 1px, 0) scale(var(--game-screen-scale));
}
}
@keyframes you-win-screen-flash {
0% {
opacity: 0;
}
8% {
opacity: 1;
}
22% {
opacity: 0.46;
}
100% {
opacity: 0;
}
}
@keyframes you-win-floor-impact {
0% {
opacity: 0;
transform: translateX(-50%) scale(0.38, 0.2);
}
16% {
opacity: 1;
transform: translateX(-50%) scale(1.05, 0.52);
}
58% {
opacity: 0.62;
}
100% {
opacity: 0;
transform: translateX(-50%) scale(1.32, 0.72);
}
}
@keyframes search-orbit {
0% {
transform: rotate(0deg) translateX(var(--search-orbit-radius, 12px))
rotate(0deg);
}
100% {
transform: rotate(360deg) translateX(var(--search-orbit-radius, 12px))
rotate(-360deg);
}
}
@keyframes matching-center-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes dice-roll-fallback {
0% {
transform: scale(0.88) rotate(0deg);
opacity: 0.78;
}
50% {
transform: scale(1.08) rotate(180deg);
opacity: 1;
}
100% {
transform: scale(0.88) rotate(360deg);
opacity: 0.78;
}
}
@keyframes countdown-number-pop {
0% {
opacity: 0;
transform: scale(0.32);
filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9))
drop-shadow(0 0 8px rgba(255, 205, 46, 0.55));
}
24% {
opacity: 1;
transform: scale(1.18);
filter: drop-shadow(0 5px 0 rgba(94, 17, 0, 0.9))
drop-shadow(0 0 24px rgba(255, 229, 86, 1))
drop-shadow(0 0 42px rgba(255, 77, 0, 0.86));
}
70% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.86);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

View File

@ -0,0 +1,17 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 351 293" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector 2584">
<path d="M0.5 8L8 0.5H343L350.5 8V285L343 292.5H8L0.5 285V8Z" fill="var(--fill-0, #080C28)" fill-opacity="0.59"/>
<path d="M0.5 8L8 0.5H343L350.5 8V285L343 292.5H8L0.5 285V8Z" stroke="url(#paint0_linear_0_37)"/>
<path d="M0.5 8L8 0.5H343L350.5 8V285L343 292.5H8L0.5 285V8Z" stroke="url(#paint1_linear_0_37)" stroke-opacity="0.28"/>
</g>
<defs>
<linearGradient id="paint0_linear_0_37" x1="175" y1="-15.5" x2="175" y2="55.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#8A829E" stop-opacity="0.45"/>
<stop offset="1" stop-color="#8A829E" stop-opacity="0.16"/>
</linearGradient>
<linearGradient id="paint1_linear_0_37" x1="44.5" y1="-15.5" x2="45.5" y2="-11.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#CEBAFF"/>
<stop offset="1" stop-color="#CEBAFF" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 MiB

View File

@ -0,0 +1,22 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="&#230;&#144;&#156;&#231;&#180;&#162; 3" clip-path="url(#clip0_0_6)">
<g id="Vector" filter="url(#filter0_d_0_6)">
<path d="M13.1847 12.2741L11.3971 10.4826C12.0454 9.63864 12.4343 8.58478 12.4343 7.4402C12.4343 4.68007 10.1936 2.43576 7.44098 2.43576C4.68832 2.43576 2.44764 4.68133 2.44764 7.44156C2.44764 10.2017 4.68705 12.446 7.44098 12.446C8.59603 12.446 9.65808 12.047 10.5053 11.3842L12.2889 13.1718C12.412 13.2952 12.5744 13.3582 12.7368 13.3582C12.8992 13.3582 13.0615 13.2965 13.1847 13.1718C13.4322 12.9238 13.4322 12.5222 13.1847 12.2741ZM3.71393 7.44156C3.71393 5.38229 5.38635 3.70623 7.44098 3.70623C9.4956 3.70623 11.1679 5.38229 11.1679 7.44156C11.1679 9.50083 9.4956 11.1769 7.44098 11.1769C5.38635 11.1769 3.71393 9.50081 3.71393 7.44156Z" fill="var(--fill-0, #7EDCFF)"/>
</g>
</g>
<defs>
<filter id="filter0_d_0_6" x="-1.55236" y="-1.56424" width="18.9227" height="18.9225" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.286776 0 0 0 0 0.614521 0 0 0 1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_0_6"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_0_6" result="shape"/>
</filter>
<clipPath id="clip0_0_6">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,21 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 78 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Vector 2584">
<path d="M0.5 5.97887L5.93571 0.5H72.0643L77.5 5.97887V23.0211L72.0643 28.5H5.93571L0.5 23.0211V5.97887Z" fill="url(#paint0_linear_0_9)"/>
<path d="M0.5 5.97887L5.93571 0.5H72.0643L77.5 5.97887V23.0211L72.0643 28.5H5.93571L0.5 23.0211V5.97887Z" stroke="url(#paint1_linear_0_9)"/>
<path d="M0.5 5.97887L5.93571 0.5H72.0643L77.5 5.97887V23.0211L72.0643 28.5H5.93571L0.5 23.0211V5.97887Z" stroke="url(#paint2_linear_0_9)" stroke-opacity="0.28"/>
</g>
<defs>
<linearGradient id="paint0_linear_0_9" x1="33.9043" y1="-1.5" x2="33.9043" y2="12.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#080C28"/>
<stop offset="1" stop-color="#0E102E"/>
</linearGradient>
<linearGradient id="paint1_linear_0_9" x1="33.9043" y1="-1.5" x2="33.9043" y2="12.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#8A829E" stop-opacity="0.45"/>
<stop offset="1" stop-color="#8A829E" stop-opacity="0.16"/>
</linearGradient>
<linearGradient id="paint2_linear_0_9" x1="8.92286" y1="-1.5" x2="9.12524" y2="-0.714091" gradientUnits="userSpaceOnUse">
<stop stop-color="#CEBAFF"/>
<stop offset="1" stop-color="#CEBAFF" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,9 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 155.5 35.5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector 2588" d="M17.5 0H138.5C140 0 141 2 142 3C142.8 3.8 144 4.33333 144.5 4.5L155.5 18.5C152.167 22.5 145.3 30.7 144.5 31.5C143 31 142.5 31.5 142 32C141.2 33.6 139.333 35 138.5 35.5H17.5C16.1667 34.3333 13.4 31.8 13 31C12.2 31.8 11.3333 31.6667 11 31.5L0 18L11.5 4.5C11.6667 4.33333 12.3 3.8 13.5 3C14.7 2.2 16.6667 0.666667 17.5 0Z" fill="url(#paint0_radial_0_5)"/>
<defs>
<radialGradient id="paint0_radial_0_5" cx="0" cy="0" r="1" gradientTransform="matrix(-1.06506 10.6986 -81.7502 -0.424161 78.8151 24.8014)" gradientUnits="userSpaceOnUse">
<stop stop-color="#051B6E"/>
<stop offset="1" stop-color="#000630"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 316 124" width="316" height="124">
<defs>
<linearGradient id="victoryFill" x1="0" y1="20" x2="0" y2="98" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#fff6af"/>
<stop offset="0.48" stop-color="#ffd96c"/>
<stop offset="1" stop-color="#d46a15"/>
</linearGradient>
<filter id="victoryGlow" x="-20%" y="-35%" width="140%" height="180%">
<feDropShadow dx="0" dy="5" stdDeviation="0" flood-color="#8f2d00" flood-opacity="0.9"/>
<feDropShadow dx="0" dy="0" stdDeviation="7" flood-color="#ff9c25" flood-opacity="0.9"/>
<feDropShadow dx="0" dy="0" stdDeviation="15" flood-color="#ffd15d" flood-opacity="0.58"/>
</filter>
</defs>
<g opacity="0.96">
<path d="M23 62H85" stroke="#c95b18" stroke-width="7" stroke-linecap="round"/>
<path d="M231 62H293" stroke="#c95b18" stroke-width="7" stroke-linecap="round"/>
<path d="M48 52L82 62L48 72Z" fill="#f8b13c"/>
<path d="M268 52L234 62L268 72Z" fill="#f8b13c"/>
</g>
<text x="158" y="82" text-anchor="middle" font-family="Arial Black, Impact, Arial, sans-serif" font-size="58" font-weight="900" letter-spacing="0" fill="url(#victoryFill)" stroke="#7d2800" stroke-width="2" paint-order="stroke fill" filter="url(#victoryGlow)">VICTORY</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

2660
game/rock/index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
/>
<title data-i18n="diceMatch.pageTitle">Dice Match</title>
<link rel="stylesheet" href="../../common/theme.css" />
<link rel="stylesheet" href="../common/game-match.css" />
<style>
:root {
color-scheme: dark;