diff --git a/activity/invite-landing/index.html b/activity/invite-landing/index.html index 1fa1ede..18cb895 100644 --- a/activity/invite-landing/index.html +++ b/activity/invite-landing/index.html @@ -83,6 +83,6 @@ - + diff --git a/activity/invite-landing/script.js b/activity/invite-landing/script.js index b3f34d2..3b77ac2 100644 --- a/activity/invite-landing/script.js +++ b/activity/invite-landing/script.js @@ -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 { diff --git a/activity/invite/index.html b/activity/invite/index.html index 807acd9..e3c48b5 100644 --- a/activity/invite/index.html +++ b/activity/invite/index.html @@ -145,25 +145,25 @@
- 168 + 0 Total number of invitees
- 01 + 0 Number of eligible voters
- 01 + 0 Total amount recharged by invitees
- 12354 + 0 My Rewards
@@ -207,56 +207,9 @@
-
-

- Milestone1:balabalabal -

-

- Total supply: 49999 coins -

-
-
-

- Milestone1:balabalabal -

-

- Total supply: 49999 coins -

-
-
-

- Milestone1:balabalabal -

-

- Total supply: 49999 coins -

-
-
-

- Milestone1:balabalabal -

-

- Total supply: 49999 coins -

-
-
-

- Milestone1:balabalabal -

-

- Total supply: 49999 coins -

-
-
- + >

-
-
-
-

- Milestone1:balabalabal -

-

- Invite 10 friends who have already made a - payment -

-
- -
-
-
-

- Milestone1:balabalabal -

-

- Invite 10 friends who have already made a - payment -

-
- -
-
-
-

- Milestone1:balabalabal -

-

- Invite 10 friends who have already made a - payment -

-
- -
-
+

(You'll receive a reward upon reaching each level)

@@ -351,7 +253,9 @@ })(); + + - + diff --git a/activity/invite/script.js b/activity/invite/script.js index 500529a..f4bb523 100644 --- a/activity/invite/script.js +++ b/activity/invite/script.js @@ -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(); } diff --git a/activity/invite/style.css b/activity/invite/style.css index 4ac2933..94dc5c1 100644 --- a/activity/invite/style.css +++ b/activity/invite/style.css @@ -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)); diff --git a/common/api.js b/common/api.js index bb402ed..337d812 100644 --- a/common/api.js +++ b/common/api.js @@ -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(); diff --git a/common/locales/ar.json b/common/locales/ar.json index 5f3befb..18f9064 100644 --- a/common/locales/ar.json +++ b/common/locales/ar.json @@ -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": "تم النسخ", diff --git a/common/locales/en.json b/common/locales/en.json index 5ea502d..b84948f 100644 --- a/common/locales/en.json +++ b/common/locales/en.json @@ -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", diff --git a/common/locales/es.json b/common/locales/es.json index 4b01621..5f2633a 100644 --- a/common/locales/es.json +++ b/common/locales/es.json @@ -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", diff --git a/common/locales/id.json b/common/locales/id.json index faae215..4b6cee9 100644 --- a/common/locales/id.json +++ b/common/locales/id.json @@ -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", diff --git a/common/locales/tr.json b/common/locales/tr.json index f2ba698..809a62c 100644 --- a/common/locales/tr.json +++ b/common/locales/tr.json @@ -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ı", diff --git a/common/locales/zh.json b/common/locales/zh.json index 81470f9..be62756 100644 --- a/common/locales/zh.json +++ b/common/locales/zh.json @@ -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": "已复制", diff --git a/game/common/game-match.css b/game/common/game-match.css new file mode 100644 index 0000000..78c7ce3 --- /dev/null +++ b/game/common/game-match.css @@ -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); + } +} diff --git a/game/rock/assets/figma/back-button.png b/game/rock/assets/figma/back-button.png new file mode 100644 index 0000000..d507fd7 Binary files /dev/null and b/game/rock/assets/figma/back-button.png differ diff --git a/game/rock/assets/figma/bg-arena.png b/game/rock/assets/figma/bg-arena.png new file mode 100644 index 0000000..8ae3f2f Binary files /dev/null and b/game/rock/assets/figma/bg-arena.png differ diff --git a/game/rock/assets/figma/blue-flare.png b/game/rock/assets/figma/blue-flare.png new file mode 100644 index 0000000..74c7f85 Binary files /dev/null and b/game/rock/assets/figma/blue-flare.png differ diff --git a/game/rock/assets/figma/blue-ring.png b/game/rock/assets/figma/blue-ring.png new file mode 100644 index 0000000..3100980 Binary files /dev/null and b/game/rock/assets/figma/blue-ring.png differ diff --git a/game/rock/assets/figma/coin.png b/game/rock/assets/figma/coin.png new file mode 100644 index 0000000..5ac0c56 Binary files /dev/null and b/game/rock/assets/figma/coin.png differ diff --git a/game/rock/assets/figma/game-icon.png b/game/rock/assets/figma/game-icon.png new file mode 100644 index 0000000..ed66edc Binary files /dev/null and b/game/rock/assets/figma/game-icon.png differ diff --git a/game/rock/assets/figma/gesture-card.png b/game/rock/assets/figma/gesture-card.png new file mode 100644 index 0000000..76538e6 Binary files /dev/null and b/game/rock/assets/figma/gesture-card.png differ diff --git a/game/rock/assets/figma/gesture-selected-rock.png b/game/rock/assets/figma/gesture-selected-rock.png new file mode 100644 index 0000000..f297e90 Binary files /dev/null and b/game/rock/assets/figma/gesture-selected-rock.png differ diff --git a/game/rock/assets/figma/help-badge.png b/game/rock/assets/figma/help-badge.png new file mode 100644 index 0000000..db80217 Binary files /dev/null and b/game/rock/assets/figma/help-badge.png differ diff --git a/game/rock/assets/figma/lobby-avatar-frame.png b/game/rock/assets/figma/lobby-avatar-frame.png new file mode 100644 index 0000000..1f8a944 Binary files /dev/null and b/game/rock/assets/figma/lobby-avatar-frame.png differ diff --git a/game/rock/assets/figma/lobby-panel.svg b/game/rock/assets/figma/lobby-panel.svg new file mode 100644 index 0000000..28c802d --- /dev/null +++ b/game/rock/assets/figma/lobby-panel.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/game/rock/assets/figma/opponent-card.png b/game/rock/assets/figma/opponent-card.png new file mode 100644 index 0000000..2b8a519 Binary files /dev/null and b/game/rock/assets/figma/opponent-card.png differ diff --git a/game/rock/assets/figma/opponent-question.png b/game/rock/assets/figma/opponent-question.png new file mode 100644 index 0000000..55f1ccd Binary files /dev/null and b/game/rock/assets/figma/opponent-question.png differ diff --git a/game/rock/assets/figma/paper.png b/game/rock/assets/figma/paper.png new file mode 100644 index 0000000..a8f0967 Binary files /dev/null and b/game/rock/assets/figma/paper.png differ diff --git a/game/rock/assets/figma/player-card.png b/game/rock/assets/figma/player-card.png new file mode 100644 index 0000000..35b8586 Binary files /dev/null and b/game/rock/assets/figma/player-card.png differ diff --git a/game/rock/assets/figma/red-flare.png b/game/rock/assets/figma/red-flare.png new file mode 100644 index 0000000..5d27634 Binary files /dev/null and b/game/rock/assets/figma/red-flare.png differ diff --git a/game/rock/assets/figma/red-ring.png b/game/rock/assets/figma/red-ring.png new file mode 100644 index 0000000..4f98260 Binary files /dev/null and b/game/rock/assets/figma/red-ring.png differ diff --git a/game/rock/assets/figma/rock.png b/game/rock/assets/figma/rock.png new file mode 100644 index 0000000..63a98ce Binary files /dev/null and b/game/rock/assets/figma/rock.png differ diff --git a/game/rock/assets/figma/scissors.png b/game/rock/assets/figma/scissors.png new file mode 100644 index 0000000..30659a7 Binary files /dev/null and b/game/rock/assets/figma/scissors.png differ diff --git a/game/rock/assets/figma/search-hud.png b/game/rock/assets/figma/search-hud.png new file mode 100644 index 0000000..3008c56 Binary files /dev/null and b/game/rock/assets/figma/search-hud.png differ diff --git a/game/rock/assets/figma/search-icon.svg b/game/rock/assets/figma/search-icon.svg new file mode 100644 index 0000000..8f0126f --- /dev/null +++ b/game/rock/assets/figma/search-icon.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/game/rock/assets/figma/stake-outline.svg b/game/rock/assets/figma/stake-outline.svg new file mode 100644 index 0000000..9671c99 --- /dev/null +++ b/game/rock/assets/figma/stake-outline.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/game/rock/assets/figma/stake-selected.png b/game/rock/assets/figma/stake-selected.png new file mode 100644 index 0000000..85545b9 Binary files /dev/null and b/game/rock/assets/figma/stake-selected.png differ diff --git a/game/rock/assets/figma/start-button.png b/game/rock/assets/figma/start-button.png new file mode 100644 index 0000000..d4c6887 Binary files /dev/null and b/game/rock/assets/figma/start-button.png differ diff --git a/game/rock/assets/figma/victory-action-blue.png b/game/rock/assets/figma/victory-action-blue.png new file mode 100644 index 0000000..d4c6887 Binary files /dev/null and b/game/rock/assets/figma/victory-action-blue.png differ diff --git a/game/rock/assets/figma/victory-action-secondary.svg b/game/rock/assets/figma/victory-action-secondary.svg new file mode 100644 index 0000000..eea0ac4 --- /dev/null +++ b/game/rock/assets/figma/victory-action-secondary.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/game/rock/assets/figma/victory-avatar-bg.png b/game/rock/assets/figma/victory-avatar-bg.png new file mode 100644 index 0000000..48ebfd7 Binary files /dev/null and b/game/rock/assets/figma/victory-avatar-bg.png differ diff --git a/game/rock/assets/figma/victory-avatar-frame.png b/game/rock/assets/figma/victory-avatar-frame.png new file mode 100644 index 0000000..1f8a944 Binary files /dev/null and b/game/rock/assets/figma/victory-avatar-frame.png differ diff --git a/game/rock/assets/figma/victory-coin-stack.png b/game/rock/assets/figma/victory-coin-stack.png new file mode 100644 index 0000000..49f73d1 Binary files /dev/null and b/game/rock/assets/figma/victory-coin-stack.png differ diff --git a/game/rock/assets/figma/victory-reward-frame.png b/game/rock/assets/figma/victory-reward-frame.png new file mode 100644 index 0000000..8890ec3 Binary files /dev/null and b/game/rock/assets/figma/victory-reward-frame.png differ diff --git a/game/rock/assets/figma/victory-title.png b/game/rock/assets/figma/victory-title.png new file mode 100644 index 0000000..70cb79e Binary files /dev/null and b/game/rock/assets/figma/victory-title.png differ diff --git a/game/rock/assets/figma/victory-title.svg b/game/rock/assets/figma/victory-title.svg new file mode 100644 index 0000000..7362749 --- /dev/null +++ b/game/rock/assets/figma/victory-title.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + VICTORY + diff --git a/game/rock/assets/figma/victory.png b/game/rock/assets/figma/victory.png new file mode 100644 index 0000000..70cb79e Binary files /dev/null and b/game/rock/assets/figma/victory.png differ diff --git a/game/rock/assets/figma/vs.png b/game/rock/assets/figma/vs.png new file mode 100644 index 0000000..d9beaa9 Binary files /dev/null and b/game/rock/assets/figma/vs.png differ diff --git a/game/rock/assets/figma/win-coins.png b/game/rock/assets/figma/win-coins.png new file mode 100644 index 0000000..49f73d1 Binary files /dev/null and b/game/rock/assets/figma/win-coins.png differ diff --git a/game/rock/index.html b/game/rock/index.html new file mode 100644 index 0000000..cd7c84f --- /dev/null +++ b/game/rock/index.html @@ -0,0 +1,2660 @@ + + + + + + Rock Paper Scissors + + + + + +
+
+ + +
9:41
+ + +
+ +
+ +
+ +
+ + +
+
+ +
+
+
+ + + +
+
+ NameNameName... +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ The winner receives +
+
?
+
+ + + +
+
+ The winner receives +
+
+ + + + +
+ +
+ If no one joins the rock-paper-scissors game, the + coins will be refunded in 10 minutes. +
+
+
+ +
+
+ +
+ +
+
+
NameName
+
Name...
+
+
+
+ +
+
?
+
+
?
+
+ + Matching... +
+
+
+ + +
+
+ Matching... +
+
+ +
+
+ +
+ +
+
+
NameName
+
Name...
+
+
+
+ +
+
?
+
+
+
?
+
+
+
+ + Matched +
+
+
+ +
+
+ Matched +
+
+ +
+
+ +
+ +
+
+
NameName
+
Name...
+
+
+
+ +
+ +
+
+
NameName
+
Name...
+
+
+
+ +
+
+
+ + + +
+
+ + + +
+
+
+ +
+ +
+ + + +
+
+ NameNameName... +
+
+ +
+ Win + 1,900,000 + Coins +
+
+
+ + +
+
+ +
+
+
+ + + + + + + + + diff --git a/game/touzi/index.html b/game/touzi/index.html index 118e76a..b41fde3 100644 --- a/game/touzi/index.html +++ b/game/touzi/index.html @@ -8,6 +8,7 @@ /> Dice Match +