aslan两个活动
405
activity/game-king/aslan.html
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-hy-app-code="aslan">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#140e64" />
|
||||||
|
<link rel="icon" href="data:," />
|
||||||
|
<title>Game King</title>
|
||||||
|
<link rel="stylesheet" href="../../common/theme.css?v=20260717" />
|
||||||
|
<link rel="stylesheet" href="./aslan/aslan.css?v=20260717" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="game-king-app" id="gameKingApp" data-node-id="918:4398">
|
||||||
|
<header class="hero" data-node-id="918:4400">
|
||||||
|
<!-- Approved standalone Figma node 918:32244: title/crown/gamepad
|
||||||
|
artwork only. Activity controls and countdown remain live DOM. -->
|
||||||
|
<img
|
||||||
|
class="hero-art"
|
||||||
|
src="./aslan/assets/layers/hero-game-king.png"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
data-node-id="918:32244"
|
||||||
|
/>
|
||||||
|
<h1 class="visually-hidden" data-i18n="gameKing.title">
|
||||||
|
Game King
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="side-actions" aria-label="Activity information">
|
||||||
|
<button
|
||||||
|
class="side-action"
|
||||||
|
id="ruleButton"
|
||||||
|
type="button"
|
||||||
|
data-node-id="918:4542"
|
||||||
|
data-i18n-aria="gameKing.rule"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.rule">Rule</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="side-action"
|
||||||
|
id="awardButton"
|
||||||
|
type="button"
|
||||||
|
data-node-id="918:4545"
|
||||||
|
data-i18n-aria="gameKing.award"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.award">Award</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="countdown"
|
||||||
|
id="countdown"
|
||||||
|
aria-label="Activity countdown"
|
||||||
|
data-i18n-aria="gameKing.countdown"
|
||||||
|
>
|
||||||
|
<span class="countdown-cell">
|
||||||
|
<strong data-countdown="days">--</strong>
|
||||||
|
<small data-i18n="gameKing.days">D</small>
|
||||||
|
</span>
|
||||||
|
<span class="countdown-cell">
|
||||||
|
<strong data-countdown="hours">--</strong>
|
||||||
|
</span>
|
||||||
|
<i aria-hidden="true">:</i>
|
||||||
|
<span class="countdown-cell">
|
||||||
|
<strong data-countdown="minutes">--</strong>
|
||||||
|
</span>
|
||||||
|
<i aria-hidden="true">:</i>
|
||||||
|
<span class="countdown-cell">
|
||||||
|
<strong data-countdown="seconds">--</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="event-stage">
|
||||||
|
<nav
|
||||||
|
class="primary-tabs"
|
||||||
|
id="primaryTabs"
|
||||||
|
aria-label="Game King sections"
|
||||||
|
data-i18n-aria="gameKing.sections"
|
||||||
|
data-node-id="918:4555"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="primary-tab is-active"
|
||||||
|
type="button"
|
||||||
|
data-view="lottery"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.tabLottery">Lottery</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="primary-tab"
|
||||||
|
type="button"
|
||||||
|
data-view="tycoon"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.tabTycoon">Tycoon King</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="primary-tab"
|
||||||
|
type="button"
|
||||||
|
data-view="victorious"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.tabVictorious"
|
||||||
|
>Victorious King</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="view-panel is-active"
|
||||||
|
id="lotteryView"
|
||||||
|
data-view-panel="lottery"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="section-state loading-state"
|
||||||
|
id="pageLoading"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span class="loading-spinner" aria-hidden="true"></span>
|
||||||
|
<span data-i18n="gameKing.loading">Loading...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="section-state error-state"
|
||||||
|
id="pageError"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<p data-i18n="gameKing.loadFailed">
|
||||||
|
Unable to load this activity.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="retry-button"
|
||||||
|
id="retryButton"
|
||||||
|
type="button"
|
||||||
|
data-i18n="gameKing.retry"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lottery-content" id="lotteryContent" hidden>
|
||||||
|
<div class="wheel" data-node-id="918:4571">
|
||||||
|
<img
|
||||||
|
class="wheel-frame"
|
||||||
|
id="wheelFrame"
|
||||||
|
src="./aslan/assets/layers/wheel-frame.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<ol
|
||||||
|
class="wheel-prizes"
|
||||||
|
id="wheelPrizes"
|
||||||
|
aria-label="Lottery prizes"
|
||||||
|
></ol>
|
||||||
|
<button
|
||||||
|
class="spin-button"
|
||||||
|
id="spinButton"
|
||||||
|
type="button"
|
||||||
|
data-node-id="918:4572"
|
||||||
|
data-i18n-aria="gameKing.draw"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./aslan/assets/layers/spin-center.png"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<strong data-i18n="gameKing.spin">Spin</strong>
|
||||||
|
<small id="spinChanceBadge">×0</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lottery-progress" aria-live="polite">
|
||||||
|
<div>
|
||||||
|
<span data-i18n="gameKing.myChances"
|
||||||
|
>My chances</span
|
||||||
|
>
|
||||||
|
<strong id="chanceCount">0</strong>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="records-link"
|
||||||
|
id="recordsButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.myRewards"
|
||||||
|
>My rewards</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<div class="progress-track" aria-hidden="true">
|
||||||
|
<span id="consumptionProgress"></span>
|
||||||
|
</div>
|
||||||
|
<p id="progressCopy"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="draw-button"
|
||||||
|
id="drawButton"
|
||||||
|
type="button"
|
||||||
|
data-node-id="918:4599"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.startDraw"
|
||||||
|
>Start the lucky draw</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<section class="rule-card" data-node-id="918:4601">
|
||||||
|
<h2 data-i18n="gameKing.rule">Rule</h2>
|
||||||
|
<ol id="ruleSummary"></ol>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="view-panel ranking-panel"
|
||||||
|
id="rankingView"
|
||||||
|
data-view-panel="ranking"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="period-tabs"
|
||||||
|
id="periodTabs"
|
||||||
|
data-node-id="918:4661"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="period-tab is-active"
|
||||||
|
type="button"
|
||||||
|
data-period="DAILY"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.dailyRankings"
|
||||||
|
>Daily Rankings</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="period-tab"
|
||||||
|
type="button"
|
||||||
|
data-period="OVERALL"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.overallRankings"
|
||||||
|
>Overall Rankings</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rank-state section-state"
|
||||||
|
id="rankLoading"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<span class="loading-spinner" aria-hidden="true"></span>
|
||||||
|
<span data-i18n="gameKing.loadingRankings"
|
||||||
|
>Loading rankings...</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="rank-state section-state error-state"
|
||||||
|
id="rankError"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<p data-i18n="gameKing.rankFailed">
|
||||||
|
Unable to load rankings.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="retry-button"
|
||||||
|
id="rankRetryButton"
|
||||||
|
type="button"
|
||||||
|
data-i18n="gameKing.retry"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="rank-empty" id="rankEmpty" hidden>
|
||||||
|
<span aria-hidden="true">✦</span>
|
||||||
|
<p data-i18n="gameKing.noRankings">
|
||||||
|
No ranking data yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="rankingContent" hidden>
|
||||||
|
<ol class="rank-podium" id="rankPodium"></ol>
|
||||||
|
<ol class="rank-list" id="rankList" start="4"></ol>
|
||||||
|
<div class="my-rank" id="myRank"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="view-panel rewards-panel"
|
||||||
|
id="rewardsView"
|
||||||
|
data-view-panel="rewards"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="rewards-back"
|
||||||
|
id="rewardsBackButton"
|
||||||
|
type="button"
|
||||||
|
data-i18n-aria="gameKing.back"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">↩</span>
|
||||||
|
</button>
|
||||||
|
<div class="rewards-heading">
|
||||||
|
<span data-i18n="gameKing.rankingRewards"
|
||||||
|
>Ranking rewards</span
|
||||||
|
>
|
||||||
|
<small id="rewardBoardName"></small>
|
||||||
|
</div>
|
||||||
|
<div class="reward-empty" id="rewardEmpty" hidden>
|
||||||
|
<p data-i18n="gameKing.noRewards">
|
||||||
|
No reward configuration yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="reward-tier-list" id="rewardTierList"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="toast"
|
||||||
|
id="toast"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
hidden
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" id="ruleModal" hidden>
|
||||||
|
<section
|
||||||
|
class="info-modal rule-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="ruleTitle"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="modal-close"
|
||||||
|
type="button"
|
||||||
|
data-close-modal="ruleModal"
|
||||||
|
aria-label="Close"
|
||||||
|
data-i18n-aria="gameKing.close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<h2 id="ruleTitle" data-i18n="gameKing.rule">Rule</h2>
|
||||||
|
<ol id="ruleList"></ol>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" id="resultModal" hidden>
|
||||||
|
<section
|
||||||
|
class="result-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="resultTitle"
|
||||||
|
>
|
||||||
|
<h2 id="resultTitle" data-i18n="gameKing.congratulations">
|
||||||
|
Congratulations
|
||||||
|
</h2>
|
||||||
|
<div class="result-rewards" id="resultRewards"></div>
|
||||||
|
<p id="resultCopy" data-i18n="gameKing.rewardIssued">
|
||||||
|
The reward has been automatically issued to your
|
||||||
|
account.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="result-confirm"
|
||||||
|
type="button"
|
||||||
|
data-close-modal="resultModal"
|
||||||
|
>
|
||||||
|
<span data-i18n="gameKing.confirm">Confirm</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-backdrop" id="recordsModal" hidden>
|
||||||
|
<section
|
||||||
|
class="info-modal records-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="recordsTitle"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="modal-close"
|
||||||
|
type="button"
|
||||||
|
data-close-modal="recordsModal"
|
||||||
|
aria-label="Close"
|
||||||
|
data-i18n-aria="gameKing.close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<h2 id="recordsTitle" data-i18n="gameKing.myRewards">
|
||||||
|
My rewards
|
||||||
|
</h2>
|
||||||
|
<div class="records-loading" id="recordsLoading" hidden>
|
||||||
|
<span class="loading-spinner" aria-hidden="true"></span>
|
||||||
|
<span data-i18n="gameKing.loading">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="records-empty" id="recordsEmpty" hidden>
|
||||||
|
<p data-i18n="gameKing.noDrawRecords">
|
||||||
|
No draw records yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul class="records-list" id="recordsList"></ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.HyAppI18nSupported = ['en', 'ar', 'tr', 'es'];
|
||||||
|
</script>
|
||||||
|
<script src="../../common/i18n.js?v=20260717"></script>
|
||||||
|
<script src="../../common_aslan/aslan-common.js?v=20260717-game-king"></script>
|
||||||
|
<script src="./aslan/aslan.js?v=20260717"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1704
activity/game-king/aslan/aslan.css
Normal file
1517
activity/game-king/aslan/aslan.js
Normal file
BIN
activity/game-king/aslan/assets/layers/countdown-cell.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
activity/game-king/aslan/assets/layers/draw-button.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
activity/game-king/aslan/assets/layers/hero-game-king.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
465
activity/game-king/aslan/assets/layers/manifest.json
Normal file
@ -0,0 +1,465 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"figma": {
|
||||||
|
"fileKey": "OTOg5cwbhVNWxkImGmzTJl",
|
||||||
|
"canvasNode": "646:1997",
|
||||||
|
"referenceFrames": [
|
||||||
|
"918:4398",
|
||||||
|
"918:4508",
|
||||||
|
"918:4607",
|
||||||
|
"918:5732",
|
||||||
|
"918:12834"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"description": "Each runtime bitmap is one decorative layer only. Text, controls, dynamic reward media and user media remain DOM/API content.",
|
||||||
|
"forbiddenSourceNodes": [
|
||||||
|
"646:1997",
|
||||||
|
"918:4398",
|
||||||
|
"918:4607",
|
||||||
|
"918:5732"
|
||||||
|
],
|
||||||
|
"approvedStandaloneCompositeNodes": [
|
||||||
|
"918:32244",
|
||||||
|
"918:4693",
|
||||||
|
"918:4681",
|
||||||
|
"918:4669"
|
||||||
|
],
|
||||||
|
"forbiddenRuntimeTerms": [
|
||||||
|
"whole-page",
|
||||||
|
"full-page",
|
||||||
|
"page-screenshot",
|
||||||
|
"root-frame",
|
||||||
|
"hero-composite"
|
||||||
|
],
|
||||||
|
"maxDecorativeLayerPixels": 1600000
|
||||||
|
},
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"filename": "countdown-cell.png",
|
||||||
|
"sourceNode": "918:4415",
|
||||||
|
"sourceName": "图片 24 5",
|
||||||
|
"use": "Countdown cell decoration; time remains live DOM text.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 53, "height": 63 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 105,
|
||||||
|
"pixelHeight": 126,
|
||||||
|
"bytes": 23166
|
||||||
|
},
|
||||||
|
"sha256": "181c560ed0cffc4d4b73aaf241267284742ec935fa1d843d8e0f56ee1b8ea7a0",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "draw-button.png",
|
||||||
|
"sourceNode": "918:4489",
|
||||||
|
"sourceName": "图层 0 315",
|
||||||
|
"use": "Lucky draw button border; label stays in the button DOM.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 269, "height": 48 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 530,
|
||||||
|
"pixelHeight": 96,
|
||||||
|
"bytes": 67399
|
||||||
|
},
|
||||||
|
"sha256": "82404a50e6bd352c7b5b86a60c86ca98ca1ed601b49a59ef97804efa6a566090",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "hero-game-king.png",
|
||||||
|
"sourceNode": "918:32244",
|
||||||
|
"sourceName": "image 5068",
|
||||||
|
"use": "Approved standalone Figma header fill. It contains only the 375x501 crown, Game King title, decorative gamepad and city/crowd scene; countdown and actions remain DOM overlays.",
|
||||||
|
"kind": "approved-static-header-composite",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 375, "height": 501 },
|
||||||
|
"naturalSize": { "width": 1085, "height": 1450 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 1085,
|
||||||
|
"pixelHeight": 1450,
|
||||||
|
"bytes": 2866290
|
||||||
|
},
|
||||||
|
"sha256": "e430b3407f9d7f10215f3af2c113aaac9e325c6ff6409db79b5f2d3c926fa732",
|
||||||
|
"containsText": true,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "prize-fallback.png",
|
||||||
|
"sourceNode": "918:4464",
|
||||||
|
"sourceName": "缩略图 (1) 3",
|
||||||
|
"use": "Mock prize thumbnail only when ?mock=1; production prizes are API media.",
|
||||||
|
"kind": "single-fixture-media",
|
||||||
|
"scope": "fixture",
|
||||||
|
"renderedDesignSize": { "width": 50, "height": 50 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 100,
|
||||||
|
"pixelHeight": 100,
|
||||||
|
"bytes": 26391
|
||||||
|
},
|
||||||
|
"sha256": "11a65b70468495eb9ba1434f0829ff3af911694e0e27764ab5d10e398d4d29fa",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": true,
|
||||||
|
"mockOnly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-period-active.png",
|
||||||
|
"sourceNode": "918:4663",
|
||||||
|
"sourceName": "啊啊撒啊大撒撒撒 1",
|
||||||
|
"use": "Selected ranking-period decoration; tab label remains DOM text.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 157, "height": 47 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 314,
|
||||||
|
"pixelHeight": 94,
|
||||||
|
"bytes": 49444
|
||||||
|
},
|
||||||
|
"sha256": "efa26c376d2acc9bab308af2a953e6f0056bfa7baec3a0120165454772704f19",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-period-frame.png",
|
||||||
|
"sourceNode": "918:4661",
|
||||||
|
"sourceName": "sadasdsad 1",
|
||||||
|
"use": "Ranking-period switcher decoration; buttons remain DOM controls.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 351, "height": 87 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 702,
|
||||||
|
"pixelHeight": 174,
|
||||||
|
"bytes": 146732
|
||||||
|
},
|
||||||
|
"sha256": "9d7055b4d21bf55b54cb04574b55c89fcf54e63587c6278e415d8bfc667c0e80",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-top1-frame.png",
|
||||||
|
"sourceNode": "918:4693",
|
||||||
|
"sourceName": "Top 1 frame",
|
||||||
|
"use": "Approved static Top 1 podium frame; avatar, account name and score are live DOM/API content within its transparent openings.",
|
||||||
|
"kind": "approved-static-podium-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 274.219, "height": 234.375 },
|
||||||
|
"naturalSize": { "width": 854, "height": 730 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 854,
|
||||||
|
"pixelHeight": 730,
|
||||||
|
"bytes": 564570
|
||||||
|
},
|
||||||
|
"sha256": "a9f2fc2aaf6818bbbfb86e64bbb40e53a47b642e677bc5543bcd0836f43051cc",
|
||||||
|
"containsText": true,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-top2-frame.png",
|
||||||
|
"sourceNode": "918:4681",
|
||||||
|
"sourceName": "Top 2 frame",
|
||||||
|
"use": "Approved static Top 2 podium frame; avatar, account name and score are live DOM/API content within its transparent openings.",
|
||||||
|
"kind": "approved-static-podium-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 117.188, "height": 181.641 },
|
||||||
|
"naturalSize": { "width": 546, "height": 847 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 546,
|
||||||
|
"pixelHeight": 847,
|
||||||
|
"bytes": 411924
|
||||||
|
},
|
||||||
|
"sha256": "bf0831484639a218404a03a2e6ec07839960a2c91d6ea54313049fd1c093c47f",
|
||||||
|
"containsText": true,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-top3-frame.png",
|
||||||
|
"sourceNode": "918:4669",
|
||||||
|
"sourceName": "Top 3 frame",
|
||||||
|
"use": "Approved static Top 3 podium frame; avatar, account name and score are live DOM/API content within its transparent openings.",
|
||||||
|
"kind": "approved-static-podium-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 117.188, "height": 181.641 },
|
||||||
|
"naturalSize": { "width": 546, "height": 847 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 546,
|
||||||
|
"pixelHeight": 847,
|
||||||
|
"bytes": 381162
|
||||||
|
},
|
||||||
|
"sha256": "2d57e948ecb742fea3d177d4d2d4f4dd5a341be9cfa94a5e84ac63bf57f5e754",
|
||||||
|
"containsText": true,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-row.png",
|
||||||
|
"sourceNode": "918:4706",
|
||||||
|
"sourceName": "萨达大厦 1",
|
||||||
|
"use": "Leaderboard row border; rank, avatar, user identity and score are live DOM/API content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 375, "height": 84 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 750,
|
||||||
|
"pixelHeight": 168,
|
||||||
|
"bytes": 192238
|
||||||
|
},
|
||||||
|
"sha256": "f130b00221f2a5a7c98fc63841ea06103a3c618e252a7a25624eb2bb597a8d98",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-self-row.png",
|
||||||
|
"sourceNode": "918:4739",
|
||||||
|
"sourceName": "撒大撒撒撒 1",
|
||||||
|
"use": "Current-user ranking row border; user data remains live DOM/API content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 375, "height": 84 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 750,
|
||||||
|
"pixelHeight": 167,
|
||||||
|
"bytes": 199098
|
||||||
|
},
|
||||||
|
"sha256": "1d047edab697448aafa8cb49d893f6e452bdaa22d2bc57dbe3c0b8c50d17221b",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "reward-count-frame.png",
|
||||||
|
"sourceNode": "918:5944",
|
||||||
|
"sourceName": "dsfsd 2",
|
||||||
|
"use": "Reward quantity label decoration; reward name and quantity remain DOM/API content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 100, "height": 17 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 200,
|
||||||
|
"pixelHeight": 34,
|
||||||
|
"bytes": 13545
|
||||||
|
},
|
||||||
|
"sha256": "6e4780ce10c58f3e2ee775bb20b9fe6f0f99205fe8e5b2b1fe7d2a21ecd77bc1",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "reward-panel.png",
|
||||||
|
"sourceNode": "918:5775",
|
||||||
|
"sourceName": "picsart-687df19b-6e40-4c2c-a38b-270043a2c8eb 1",
|
||||||
|
"use": "Ranking reward-card background slices only; Figma composes the card from this shared image-fill in a 92px top, stretchable middle and 67px bottom. Tier title, reward media and settlement state remain DOM/API content.",
|
||||||
|
"kind": "single-decorative-panel",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 351, "height": 317 },
|
||||||
|
"naturalSize": { "width": 702, "height": 318 },
|
||||||
|
"designUseSize": { "width": 351, "height": 317 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 702,
|
||||||
|
"pixelHeight": 318,
|
||||||
|
"bytes": 311440
|
||||||
|
},
|
||||||
|
"sha256": "fb8fc0bc0d0e7cef7833fc12608380d6853483e541fcd813b1ab56e6681379c6",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "reward-ring.png",
|
||||||
|
"sourceNode": "918:5940",
|
||||||
|
"sourceName": "图片 107 1",
|
||||||
|
"use": "Reward and podium icon surround; item images remain API media.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 100, "height": 99 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 200,
|
||||||
|
"pixelHeight": 197,
|
||||||
|
"bytes": 73676
|
||||||
|
},
|
||||||
|
"sha256": "0153c3b90ef9879ac45457f198a0ab580fc3e2f5f4618e2f867043b662a426f8",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "reward-status-frame.png",
|
||||||
|
"sourceNode": "918:5960",
|
||||||
|
"sourceName": "asdsad 1",
|
||||||
|
"use": "Reward settlement-status decoration; status stays live DOM text.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 200, "height": 53 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 400,
|
||||||
|
"pixelHeight": 106,
|
||||||
|
"bytes": 76836
|
||||||
|
},
|
||||||
|
"sha256": "813f96c2b8f500b64b7fb6a2a8c03ffe54d310431f3c0e67e435d6a8fc29c7c7",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "side-action-frame.png",
|
||||||
|
"sourceNode": "918:4432",
|
||||||
|
"sourceName": "图片 92 1",
|
||||||
|
"use": "Rule and awards side-button decoration; button text remains DOM/i18n content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 136, "height": 43 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 421,
|
||||||
|
"pixelHeight": 86,
|
||||||
|
"bytes": 63072
|
||||||
|
},
|
||||||
|
"sha256": "24a168359e5238a67a2077be7fab3cea9f2bca4c708ff3e88a76337e1e1fae28",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "spin-center.png",
|
||||||
|
"sourceNode": "918:4462",
|
||||||
|
"sourceName": "图层 0 43",
|
||||||
|
"use": "Lottery spin-center decoration; button action, chance count and label remain DOM content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 102, "height": 112 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 201,
|
||||||
|
"pixelHeight": 224,
|
||||||
|
"bytes": 64266
|
||||||
|
},
|
||||||
|
"sha256": "d38866cf0d5de9cd16f8e656944389c4b06dc0a33f910b7b2c5e392ebd5bf117",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "stage-background.jpg",
|
||||||
|
"sourceNode": "918:4554",
|
||||||
|
"sourceName": "8d9724ebcfedc7fe266dce4b22c1acd0c36d3eec106fb4-z5Hap7 2",
|
||||||
|
"use": "Pure decorative space-stage background only; all activity content stays DOM/API driven.",
|
||||||
|
"kind": "single-decorative-background",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 591, "height": 1280 },
|
||||||
|
"file": {
|
||||||
|
"format": "jpeg",
|
||||||
|
"pixelWidth": 591,
|
||||||
|
"pixelHeight": 1280,
|
||||||
|
"bytes": 68918
|
||||||
|
},
|
||||||
|
"sha256": "156036a55c0aaaa6e65cb17cd65842f343ba8b4c20d78f5a753ce25568fca766",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "tab-active.png",
|
||||||
|
"sourceNode": "918:4448",
|
||||||
|
"sourceName": "sdasdasd 4",
|
||||||
|
"use": "Active primary-tab decoration; tab labels remain DOM/i18n content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 112, "height": 34 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 224,
|
||||||
|
"pixelHeight": 68,
|
||||||
|
"bytes": 29835
|
||||||
|
},
|
||||||
|
"sha256": "82ad16cf0dea642cd96fd196caba44add87a7a08a3670f9967507fa081f0c5f1",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "tab-idle.png",
|
||||||
|
"sourceNode": "918:4452",
|
||||||
|
"sourceName": "sdasdasd 4",
|
||||||
|
"use": "Inactive primary-tab decoration; tab labels remain DOM/i18n content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 112, "height": 34 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 224,
|
||||||
|
"pixelHeight": 68,
|
||||||
|
"bytes": 17086
|
||||||
|
},
|
||||||
|
"sha256": "accbdd006dd7adfbe8334ae1d2bd6c0596f4f8f7d3cbd71293e9b2c0299d5d8a",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "wheel-frame.png",
|
||||||
|
"sourceNode": "918:4461",
|
||||||
|
"sourceName": "大撒上的 1",
|
||||||
|
"use": "Lottery wheel decoration; seven prize cells and button stay DOM/API content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 355, "height": 417 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 709,
|
||||||
|
"pixelHeight": 834,
|
||||||
|
"bytes": 941508
|
||||||
|
},
|
||||||
|
"sha256": "5ba68ed72edddeaa3287537c4698e7a5b2c2a7a45f1378a8f81a97dba096b3b9",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false,
|
||||||
|
"mockOnly": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
activity/game-king/aslan/assets/layers/prize-fallback.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
activity/game-king/aslan/assets/layers/rank-period-active.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
activity/game-king/aslan/assets/layers/rank-period-frame.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
activity/game-king/aslan/assets/layers/rank-row.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
activity/game-king/aslan/assets/layers/rank-self-row.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
activity/game-king/aslan/assets/layers/rank-top1-frame.png
Normal file
|
After Width: | Height: | Size: 551 KiB |
BIN
activity/game-king/aslan/assets/layers/rank-top2-frame.png
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
activity/game-king/aslan/assets/layers/rank-top3-frame.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
activity/game-king/aslan/assets/layers/reward-count-frame.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
activity/game-king/aslan/assets/layers/reward-panel.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
activity/game-king/aslan/assets/layers/reward-ring.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
activity/game-king/aslan/assets/layers/reward-status-frame.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
activity/game-king/aslan/assets/layers/side-action-frame.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
activity/game-king/aslan/assets/layers/spin-center.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
activity/game-king/aslan/assets/layers/stage-background.jpg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
activity/game-king/aslan/assets/layers/tab-active.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
activity/game-king/aslan/assets/layers/tab-idle.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
activity/game-king/aslan/assets/layers/wheel-frame.png
Normal file
|
After Width: | Height: | Size: 919 KiB |
340
activity/gift-challenge/aslan.html
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-hy-app-code="aslan">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
<meta name="theme-color" content="#5b0903" />
|
||||||
|
<link rel="icon" href="data:," />
|
||||||
|
<title>Gift Challenge</title>
|
||||||
|
<link rel="stylesheet" href="../../common/theme.css?v=20260718" />
|
||||||
|
<link rel="stylesheet" href="./aslan/aslan.css?v=20260718" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div
|
||||||
|
class="gift-challenge-app"
|
||||||
|
id="giftChallengeApp"
|
||||||
|
data-node-id="918:7827"
|
||||||
|
>
|
||||||
|
<header class="hero" data-node-id="918:7882">
|
||||||
|
<!-- Figma 原节点 918:7884 把标题烘焙进了图片。运行时使用去字派生背景,
|
||||||
|
标题仍是可翻译 DOM,避免以复合截图替代文字和交互。 -->
|
||||||
|
<img
|
||||||
|
class="hero-art"
|
||||||
|
src="./aslan/assets/layers/hero-art.png"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<h1 class="hero-title" data-i18n="giftChallenge.title">
|
||||||
|
Gift Challenge
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="side-actions" aria-label="Activity actions">
|
||||||
|
<button
|
||||||
|
class="side-action"
|
||||||
|
id="rewardButton"
|
||||||
|
type="button"
|
||||||
|
data-i18n-aria="giftChallenge.rewards"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="./aslan/assets/layers/side-rewards.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="side-action"
|
||||||
|
id="ruleButton"
|
||||||
|
type="button"
|
||||||
|
data-i18n-aria="giftChallenge.rules"
|
||||||
|
>
|
||||||
|
<img src="./aslan/assets/layers/side-info.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="event-stage">
|
||||||
|
<div
|
||||||
|
class="countdown"
|
||||||
|
id="countdown"
|
||||||
|
data-i18n-aria="giftChallenge.countdown"
|
||||||
|
data-node-id="918:7886"
|
||||||
|
>
|
||||||
|
<span class="countdown-cell">
|
||||||
|
<strong data-countdown="days">--</strong>
|
||||||
|
<small data-i18n="giftChallenge.days">D</small>
|
||||||
|
</span>
|
||||||
|
<span class="countdown-cell">
|
||||||
|
<strong data-countdown="hours">--</strong>
|
||||||
|
</span>
|
||||||
|
<i aria-hidden="true">:</i>
|
||||||
|
<span class="countdown-cell">
|
||||||
|
<strong data-countdown="minutes">--</strong>
|
||||||
|
</span>
|
||||||
|
<i aria-hidden="true">:</i>
|
||||||
|
<span class="countdown-cell">
|
||||||
|
<strong data-countdown="seconds">--</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
class="primary-tabs"
|
||||||
|
id="primaryTabs"
|
||||||
|
data-i18n-aria="giftChallenge.sections"
|
||||||
|
data-node-id="918:19521"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="primary-tab is-active"
|
||||||
|
type="button"
|
||||||
|
data-view="tasks"
|
||||||
|
>
|
||||||
|
<span data-i18n="giftChallenge.tasks">Tasks</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="primary-tab"
|
||||||
|
type="button"
|
||||||
|
data-view="ranking"
|
||||||
|
>
|
||||||
|
<span data-i18n="giftChallenge.ranking">Ranking</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="primary-tab"
|
||||||
|
type="button"
|
||||||
|
data-view="rewards"
|
||||||
|
>
|
||||||
|
<span data-i18n="giftChallenge.rewards">Rewards</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="view-panel is-active"
|
||||||
|
id="tasksView"
|
||||||
|
data-view-panel="tasks"
|
||||||
|
>
|
||||||
|
<div class="section-state loading-state" id="pageLoading">
|
||||||
|
<span class="loading-spinner" aria-hidden="true"></span>
|
||||||
|
<span data-i18n="giftChallenge.loading"
|
||||||
|
>Loading...</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="section-state error-state"
|
||||||
|
id="pageError"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<p data-i18n="giftChallenge.loadFailed">
|
||||||
|
Unable to load the activity.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="retry-button"
|
||||||
|
id="retryButton"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span data-i18n="giftChallenge.retry">Retry</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="ornate-panel tasks-panel"
|
||||||
|
id="tasksContent"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="panel-decoration"
|
||||||
|
src="./aslan/assets/layers/task-panel.png"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
data-node-id="918:7908"
|
||||||
|
/>
|
||||||
|
<div class="task-list" id="taskList"></div>
|
||||||
|
<p
|
||||||
|
class="empty-copy"
|
||||||
|
id="taskEmpty"
|
||||||
|
hidden
|
||||||
|
data-i18n="giftChallenge.noTasks"
|
||||||
|
>
|
||||||
|
No tasks are available.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="view-panel ranking-view"
|
||||||
|
id="rankingView"
|
||||||
|
data-view-panel="ranking"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<div class="rank-period-tabs" id="rankPeriodTabs">
|
||||||
|
<button
|
||||||
|
class="rank-period is-active"
|
||||||
|
type="button"
|
||||||
|
data-period="DAILY"
|
||||||
|
>
|
||||||
|
<span data-i18n="giftChallenge.dailyRankings"
|
||||||
|
>Daily Rankings</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rank-period"
|
||||||
|
type="button"
|
||||||
|
data-period="OVERALL"
|
||||||
|
>
|
||||||
|
<span data-i18n="giftChallenge.overallRankings"
|
||||||
|
>Overall Rankings</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ranking-shell" data-node-id="918:9024">
|
||||||
|
<img
|
||||||
|
class="ranking-palace"
|
||||||
|
src="./aslan/assets/layers/ranking-palace.jpg"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
class="ranking-frame"
|
||||||
|
src="./aslan/assets/layers/ranking-title-frame.png"
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<h2 data-i18n="giftChallenge.ranking">Ranking</h2>
|
||||||
|
<div class="ranking-state" id="rankingLoading" hidden>
|
||||||
|
<span
|
||||||
|
class="loading-spinner"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
<span data-i18n="giftChallenge.loadingRankings"
|
||||||
|
>Loading rankings...</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="ranking-state" id="rankingError" hidden>
|
||||||
|
<span data-i18n="giftChallenge.rankFailed"
|
||||||
|
>Unable to load rankings.</span
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="rankingRetry"
|
||||||
|
data-i18n="giftChallenge.retry"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="ranking-content" id="rankingContent" hidden>
|
||||||
|
<ol class="podium" id="podium"></ol>
|
||||||
|
<ol class="rank-list" id="rankList"></ol>
|
||||||
|
<p
|
||||||
|
class="empty-copy"
|
||||||
|
id="rankEmpty"
|
||||||
|
hidden
|
||||||
|
data-i18n="giftChallenge.noRankings"
|
||||||
|
>
|
||||||
|
No ranking data yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="self-rank" id="selfRank" hidden></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="view-panel rewards-view"
|
||||||
|
id="rewardsView"
|
||||||
|
data-view-panel="rewards"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="reward-back"
|
||||||
|
id="rewardBack"
|
||||||
|
type="button"
|
||||||
|
data-i18n-aria="giftChallenge.back"
|
||||||
|
>
|
||||||
|
<img src="./aslan/assets/layers/back-icon.svg" alt="" />
|
||||||
|
</button>
|
||||||
|
<h2
|
||||||
|
class="view-title"
|
||||||
|
data-i18n="giftChallenge.rankingRewards"
|
||||||
|
>
|
||||||
|
Ranking Rewards
|
||||||
|
</h2>
|
||||||
|
<div class="reward-tier-list" id="rewardTierList"></div>
|
||||||
|
<p
|
||||||
|
class="empty-copy reward-empty"
|
||||||
|
id="rewardEmpty"
|
||||||
|
hidden
|
||||||
|
data-i18n="giftChallenge.noRewards"
|
||||||
|
>
|
||||||
|
No ranking rewards are configured.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="ruleModal" hidden>
|
||||||
|
<div class="modal-backdrop" data-close-modal="ruleModal"></div>
|
||||||
|
<section
|
||||||
|
class="modal-card rules-card"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="ruleTitle"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="modal-close"
|
||||||
|
type="button"
|
||||||
|
data-close-modal="ruleModal"
|
||||||
|
data-i18n-aria="giftChallenge.close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<h2 id="ruleTitle" data-i18n="giftChallenge.rules">Rules</h2>
|
||||||
|
<div class="rule-copy">
|
||||||
|
<h3 data-i18n="giftChallenge.taskRules">Tasks</h3>
|
||||||
|
<p data-i18n="giftChallenge.ruleTasks">
|
||||||
|
Daily progress resets at 00:00 in the activity time
|
||||||
|
zone. Claim completed task rewards before the next
|
||||||
|
reset.
|
||||||
|
</p>
|
||||||
|
<h3 data-i18n="giftChallenge.rankingRules">Leaderboard</h3>
|
||||||
|
<p data-i18n="giftChallenge.ruleRanking">
|
||||||
|
Gold gifts sent during the activity count toward the
|
||||||
|
daily and overall rankings. Lucky Gifts and Super Lucky
|
||||||
|
Gifts do not count.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal" id="resultModal" hidden>
|
||||||
|
<div class="modal-backdrop" data-close-modal="resultModal"></div>
|
||||||
|
<section
|
||||||
|
class="modal-card result-card"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="resultTitle"
|
||||||
|
>
|
||||||
|
<h2 id="resultTitle" data-i18n="giftChallenge.congratulations">
|
||||||
|
Congratulations
|
||||||
|
</h2>
|
||||||
|
<div class="result-rewards" id="resultRewards"></div>
|
||||||
|
<p id="resultMessage" data-i18n="giftChallenge.rewardIssued">
|
||||||
|
The reward has been sent to your inventory.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="confirm-button"
|
||||||
|
type="button"
|
||||||
|
data-close-modal="resultModal"
|
||||||
|
>
|
||||||
|
<span data-i18n="giftChallenge.confirm">Confirm</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toast" id="toast" role="status" hidden></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.HyAppI18nSupported = ['en', 'ar', 'tr', 'es'];
|
||||||
|
</script>
|
||||||
|
<script src="../../common/i18n.js?v=20260718"></script>
|
||||||
|
<script src="../../common_aslan/aslan-common.js?v=20260718"></script>
|
||||||
|
<script src="./aslan/aslan.js?v=20260718"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
984
activity/gift-challenge/aslan/aslan.css
Normal file
@ -0,0 +1,984 @@
|
|||||||
|
:root {
|
||||||
|
--gift-red-950: #380400;
|
||||||
|
--gift-red-900: #590600;
|
||||||
|
--gift-red-800: #790d03;
|
||||||
|
--gift-red-700: #a71b06;
|
||||||
|
--gift-gold-100: #fff8c9;
|
||||||
|
--gift-gold-300: #ffe16b;
|
||||||
|
--gift-gold-500: #f6ad19;
|
||||||
|
--gift-gold-700: #a95a00;
|
||||||
|
--gift-panel: rgba(86, 0, 0, 0.94);
|
||||||
|
--gift-text: #fff8df;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
min-height: 100%;
|
||||||
|
background: #280200;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
color: var(--gift-text);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 0, #a52d08 0, transparent 34rem), #280200;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid #fff4a4;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gift-challenge-app {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, 375px);
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--gift-red-900);
|
||||||
|
box-shadow: 0 0 38px rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #6a1600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
height: 34px;
|
||||||
|
background: linear-gradient(transparent, #7c1102);
|
||||||
|
content: '';
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-art {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
right: 34px;
|
||||||
|
bottom: 23px;
|
||||||
|
left: 34px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #ffeaa1;
|
||||||
|
font-family: Georgia, 'Times New Roman', serif;
|
||||||
|
font-size: clamp(20px, 6.8vw, 31px);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-shadow:
|
||||||
|
0 2px 0 #6c0900,
|
||||||
|
0 0 5px #ffea91;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-actions {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 126px;
|
||||||
|
right: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-action {
|
||||||
|
display: grid;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 5px;
|
||||||
|
place-items: center;
|
||||||
|
border: 2px solid #ffe76d;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(#aa240e, #6c0800);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2px #9b3500,
|
||||||
|
0 2px 8px rgba(48, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-action img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-stage {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
min-height: calc(100vh - 375px);
|
||||||
|
padding: 6px 0 90px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(123, 17, 1, 0.08), rgba(79, 5, 0, 0.32)),
|
||||||
|
url('./assets/layers/stage-background.jpg');
|
||||||
|
background-position: center top;
|
||||||
|
background-size: 100% auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown {
|
||||||
|
--countdown-cell-size: clamp(60px, 19.2vw, 72px);
|
||||||
|
--countdown-cell-height: clamp(64px, 20.54vw, 77px);
|
||||||
|
--countdown-gap: clamp(3px, 1.4vw, 6px);
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns:
|
||||||
|
var(--countdown-cell-size) var(--countdown-cell-size) 4px
|
||||||
|
var(--countdown-cell-size) 4px var(--countdown-cell-size);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--countdown-cell-height);
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
column-gap: var(--countdown-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-cell {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: var(--countdown-cell-size);
|
||||||
|
height: var(--countdown-cell-height);
|
||||||
|
place-items: center;
|
||||||
|
background: url('./assets/layers/countdown-cell.png') center / 100% 100%
|
||||||
|
no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-cell strong {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #fff8cf;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: clamp(23px, 7.2vw, 27px);
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow:
|
||||||
|
0 2px 0 #600000,
|
||||||
|
0 0 6px #ff9d00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-cell small {
|
||||||
|
position: absolute;
|
||||||
|
right: 9px;
|
||||||
|
bottom: 15px;
|
||||||
|
color: #fff2bd;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown > i {
|
||||||
|
color: #ffe976;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 23px;
|
||||||
|
font-style: normal;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 1px 3px #6c0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-tab {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 12px 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
background: url('./assets/layers/section-tab-idle.png') center / 113px 42px
|
||||||
|
no-repeat;
|
||||||
|
color: #e5e5e5;
|
||||||
|
font-size: clamp(10px, 3vw, 12px);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.05;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 1px 2px #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-tab.is-active {
|
||||||
|
background-image: url('./assets/layers/section-tab-active.png');
|
||||||
|
color: #fff3b0;
|
||||||
|
text-shadow:
|
||||||
|
0 1px 1px #780000,
|
||||||
|
0 0 4px #ffb800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-tab span {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-panel {
|
||||||
|
position: relative;
|
||||||
|
min-height: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-state,
|
||||||
|
.ranking-state {
|
||||||
|
display: grid;
|
||||||
|
min-height: 260px;
|
||||||
|
padding: 30px;
|
||||||
|
place-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 14px;
|
||||||
|
color: #fff4cb;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 3px solid rgba(255, 224, 104, 0.28);
|
||||||
|
border-top-color: #ffe571;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: gift-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gift-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-button,
|
||||||
|
.ranking-state button {
|
||||||
|
min-width: 112px;
|
||||||
|
min-height: 38px;
|
||||||
|
border: 1px solid #ffe571;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(#d94011, #8c0800);
|
||||||
|
box-shadow: inset 0 0 7px #ffb936;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ornate-panel {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 720px;
|
||||||
|
padding: 42px 21px 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-decoration {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: fill;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card {
|
||||||
|
min-height: 180px;
|
||||||
|
padding: 0 0 12px;
|
||||||
|
border-bottom: 1px solid rgba(255, 203, 45, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-heading {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: start;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 48px;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title-wrap {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-heading h3 {
|
||||||
|
display: -webkit-box;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #fff7e7;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-progress {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #ffd66b;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action,
|
||||||
|
.confirm-button {
|
||||||
|
position: relative;
|
||||||
|
min-width: 72px;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 2px 12px 4px;
|
||||||
|
border: 0;
|
||||||
|
background: url('./assets/layers/action-button.png') center / 100% 100%
|
||||||
|
no-repeat;
|
||||||
|
color: #fffbe2;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-shadow: 0 1px 2px #680000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action:disabled,
|
||||||
|
.task-action.is-claimed {
|
||||||
|
filter: grayscale(1);
|
||||||
|
opacity: 0.78;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action.is-loading {
|
||||||
|
color: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action.is-loading::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
margin: -7px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: gift-spin 0.7s linear infinite;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 79px));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-slot {
|
||||||
|
position: relative;
|
||||||
|
width: 79px;
|
||||||
|
height: 92px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: url('./assets/layers/task-reward-frame.png') center top / 79px
|
||||||
|
80px no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-slot img {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
width: 59px;
|
||||||
|
height: 56px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
top: 13px;
|
||||||
|
left: 13px;
|
||||||
|
display: grid;
|
||||||
|
width: 53px;
|
||||||
|
height: 51px;
|
||||||
|
place-items: center;
|
||||||
|
color: #ffdc66;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-count {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
display: grid;
|
||||||
|
height: 30px;
|
||||||
|
place-items: center;
|
||||||
|
background: url('./assets/layers/reward-count-frame.png') center / 79px 30px
|
||||||
|
no-repeat;
|
||||||
|
color: #fff4a7;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-shadow: 0 1px 2px #5f0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-period-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-period {
|
||||||
|
height: 60px;
|
||||||
|
padding: 2px 18px 5px;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
background-image: url('./assets/layers/rank-period-sprite.png');
|
||||||
|
background-position: right center;
|
||||||
|
background-size: 200% 100%;
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-shadow: 0 1px 2px #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-period.is-active {
|
||||||
|
background-position: left center;
|
||||||
|
color: #fff0a0;
|
||||||
|
text-shadow: 0 1px 3px #7b0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-shell {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 980px;
|
||||||
|
padding: 85px 12px 34px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-palace {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
top: 54px;
|
||||||
|
right: 10px;
|
||||||
|
left: 10px;
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
height: 620px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 12px;
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-frame {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 326px;
|
||||||
|
object-fit: fill;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-shell > h2,
|
||||||
|
.view-title {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 25px;
|
||||||
|
right: 56px;
|
||||||
|
left: 56px;
|
||||||
|
margin: 0;
|
||||||
|
color: #fff7d4;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 28px;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow:
|
||||||
|
0 2px #860000,
|
||||||
|
0 0 6px #ffe389;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-state,
|
||||||
|
.ranking-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
align-items: end;
|
||||||
|
min-height: 215px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 3px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-entry {
|
||||||
|
display: grid;
|
||||||
|
justify-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
color: #fff8d7;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-entry:nth-child(1) {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-entry:nth-child(2) {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-entry:nth-child(3) {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-avatar-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: 82px;
|
||||||
|
height: 82px;
|
||||||
|
place-items: center;
|
||||||
|
border: 5px solid #ffc83b;
|
||||||
|
border-radius: 50% 50% 45% 45%;
|
||||||
|
background: radial-gradient(#ffdd64, #7e1000 67%);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 3px #a93d00,
|
||||||
|
0 0 20px #ffbd1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-entry:nth-child(1) .podium-avatar-wrap {
|
||||||
|
width: 105px;
|
||||||
|
height: 105px;
|
||||||
|
border-color: #fff29d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-avatar-wrap::before {
|
||||||
|
position: absolute;
|
||||||
|
top: -25px;
|
||||||
|
color: #ffdf5e;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-shadow: 0 2px #7b0000;
|
||||||
|
content: attr(data-rank-label);
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-avatar,
|
||||||
|
.rank-avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-fallback {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #ffce50, #9b1800);
|
||||||
|
color: #fff6d1;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-name {
|
||||||
|
width: 105px;
|
||||||
|
margin-top: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-score,
|
||||||
|
.rank-score {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
color: #fff0a2;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.podium-score img,
|
||||||
|
.rank-score img {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 12px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-entry,
|
||||||
|
.self-rank {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 28px 52px minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 88px;
|
||||||
|
padding: 12px 26px 12px 20px;
|
||||||
|
background: url('./assets/layers/rank-row.png') center / 100% 100% no-repeat;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-number {
|
||||||
|
color: #ffe968;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-avatar-wrap {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid #ffe078;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #701000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-user {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-user strong,
|
||||||
|
.rank-user small {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-user strong {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rank-user small {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #fff0c5;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-rank {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 4;
|
||||||
|
right: 0;
|
||||||
|
bottom: calc(8px + env(safe-area-inset-bottom));
|
||||||
|
left: 0;
|
||||||
|
margin: 8px 10px 0;
|
||||||
|
filter: drop-shadow(0 5px 8px rgba(61, 0, 0, 0.55));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewards-view {
|
||||||
|
min-height: 940px;
|
||||||
|
padding: 58px 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-back {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 2px;
|
||||||
|
right: 12px;
|
||||||
|
display: grid;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 7px;
|
||||||
|
place-items: center;
|
||||||
|
border: 2px solid #ffe46b;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #8f1605;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-back img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewards-view > .view-title {
|
||||||
|
top: 7px;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-tier-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-tier {
|
||||||
|
position: relative;
|
||||||
|
min-height: 305px;
|
||||||
|
padding: 70px 24px 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 3px solid #e79a15;
|
||||||
|
border-radius: 16px;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(87, 0, 0, 0.44), rgba(87, 0, 0, 0.82)),
|
||||||
|
url('./assets/layers/ranking-palace.jpg') center / cover;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2px #ffe36a,
|
||||||
|
0 8px 16px rgba(54, 0, 0, 0.38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-tier h3 {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 24px;
|
||||||
|
left: 24px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 9px 20px;
|
||||||
|
border: 2px solid #ffc83e;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(#c91d10, #710000);
|
||||||
|
color: #fff7d1;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 25px;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 2px #730000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-tier .reward-strip {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
justify-items: center;
|
||||||
|
gap: 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reward-tier .reward-slot {
|
||||||
|
transform: scale(1.15);
|
||||||
|
transform-origin: center top;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-copy {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
margin: 36px auto;
|
||||||
|
padding: 20px;
|
||||||
|
color: #ffecc1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
padding: 18px;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(24, 0, 0, 0.76);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, 355px);
|
||||||
|
max-height: min(82vh, 620px);
|
||||||
|
padding: 58px 28px 34px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 4px solid #d88c11;
|
||||||
|
border-radius: 18px;
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(112, 0, 0, 0.86), rgba(55, 0, 0, 0.95)),
|
||||||
|
url('./assets/layers/ranking-palace.jpg') center / cover;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2px #ffe06a,
|
||||||
|
0 18px 40px rgba(0, 0, 0, 0.52);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card::before {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 36px;
|
||||||
|
left: 36px;
|
||||||
|
height: 42px;
|
||||||
|
border: 2px solid #ffc341;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: linear-gradient(#c42314, #6d0000);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card h2 {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 19px;
|
||||||
|
right: 55px;
|
||||||
|
left: 55px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #fff5c4;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 28px;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-shadow: 0 1px #790000;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
top: 7px;
|
||||||
|
right: 7px;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border: 1px solid #ffe36b;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #8c0c02;
|
||||||
|
color: #fff0a5;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-copy h3 {
|
||||||
|
margin: 18px 0 6px;
|
||||||
|
color: #ffe078;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-copy p {
|
||||||
|
margin: 0;
|
||||||
|
color: #fff3dd;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-rewards {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 100px;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card p {
|
||||||
|
margin: 14px 0 18px;
|
||||||
|
color: #fff4de;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-button {
|
||||||
|
width: 216px;
|
||||||
|
height: 58px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 140;
|
||||||
|
bottom: calc(24px + env(safe-area-inset-bottom));
|
||||||
|
left: 50%;
|
||||||
|
width: max-content;
|
||||||
|
max-width: calc(100vw - 36px);
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: 1px solid rgba(255, 223, 93, 0.58);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(58, 0, 0, 0.92);
|
||||||
|
color: #fff4d0;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] .side-actions {
|
||||||
|
right: auto;
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] .reward-back {
|
||||||
|
right: auto;
|
||||||
|
left: 12px;
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] .rank-entry,
|
||||||
|
[dir='rtl'] .self-rank {
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-left: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 340px) {
|
||||||
|
.task-heading {
|
||||||
|
min-height: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-heading h3 {
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.gift-challenge-app {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
1274
activity/gift-challenge/aslan/aslan.js
Normal file
BIN
activity/gift-challenge/aslan/assets/layers/action-button.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
14
activity/gift-challenge/aslan/assets/layers/back-icon.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="返回 1">
|
||||||
|
<g id="Vector">
|
||||||
|
<path d="M22.737 21.2414C22.737 21.2414 22.515 21.8129 22.2195 21.2414C22.2195 21.2414 18.9165 11.2724 10.8645 13.7834V16.8779C10.8645 16.8779 10.7355 18.6974 9.14848 17.5124L1.23148 10.6784C1.23148 10.6784 -0.447024 9.76492 1.33348 8.51542L9.33448 1.64092C9.33448 1.64092 10.5375 0.782922 10.827 2.18992L10.836 5.52892C10.8375 5.53042 25.8435 6.24892 22.737 21.2414Z" fill="var(--fill-0, black)"/>
|
||||||
|
<path d="M22.737 21.2414C22.737 21.2414 22.515 21.8129 22.2195 21.2414C22.2195 21.2414 18.9165 11.2724 10.8645 13.7834V16.8779C10.8645 16.8779 10.7355 18.6974 9.14848 17.5124L1.23148 10.6784C1.23148 10.6784 -0.447024 9.76492 1.33348 8.51542L9.33448 1.64092C9.33448 1.64092 10.5375 0.782922 10.827 2.18992L10.836 5.52892C10.8375 5.53042 25.8435 6.24892 22.737 21.2414Z" fill="url(#paint0_linear_0_4)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_0_4" x1="11.8376" y1="1.38188" x2="11.8376" y2="21.4954" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFFEE7"/>
|
||||||
|
<stop offset="1" stop-color="#FEE65C"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
activity/gift-challenge/aslan/assets/layers/coin.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
activity/gift-challenge/aslan/assets/layers/countdown-cell.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
activity/gift-challenge/aslan/assets/layers/hero-art.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
377
activity/gift-challenge/aslan/assets/layers/manifest.json
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"figma": {
|
||||||
|
"fileKey": "OTOg5cwbhVNWxkImGmzTJl",
|
||||||
|
"canvasNode": "646:1997",
|
||||||
|
"referenceFrames": [
|
||||||
|
"918:6917",
|
||||||
|
"918:7827",
|
||||||
|
"918:8941",
|
||||||
|
"918:9851",
|
||||||
|
"918:10385"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"description": "Runtime assets are traceable decorative layers only. All copy, controls, user data, ranking data, task state and reward media remain DOM/API content.",
|
||||||
|
"forbiddenSourceNodes": [
|
||||||
|
"646:1997",
|
||||||
|
"918:6917",
|
||||||
|
"918:7827",
|
||||||
|
"918:8941",
|
||||||
|
"918:9851",
|
||||||
|
"918:10385"
|
||||||
|
],
|
||||||
|
"forbiddenRuntimeTerms": [
|
||||||
|
"whole-page",
|
||||||
|
"full-page",
|
||||||
|
"page-screenshot",
|
||||||
|
"root-frame"
|
||||||
|
],
|
||||||
|
"maxDecorativeLayerPixels": 1600000
|
||||||
|
},
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"filename": "action-button.png",
|
||||||
|
"sourceNode": "918:8734",
|
||||||
|
"use": "Task action-button chrome; the label, state and click target remain a DOM button.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 72, "height": 26 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 288,
|
||||||
|
"pixelHeight": 50,
|
||||||
|
"bytes": 27630
|
||||||
|
},
|
||||||
|
"sha256": "63372b8ba1ddc800de3467c86baa29de055ba46012d097830770dfac5eae5850",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "back-icon.svg",
|
||||||
|
"sourceNode": "918:10383",
|
||||||
|
"use": "Back-arrow decoration inside the DOM navigation button.",
|
||||||
|
"kind": "single-decorative-icon",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 24, "height": 24 },
|
||||||
|
"file": {
|
||||||
|
"format": "svg",
|
||||||
|
"pixelWidth": 24,
|
||||||
|
"pixelHeight": 24,
|
||||||
|
"bytes": 1277
|
||||||
|
},
|
||||||
|
"exportViewBox": {
|
||||||
|
"minX": 0,
|
||||||
|
"minY": 0,
|
||||||
|
"width": 24,
|
||||||
|
"height": 24
|
||||||
|
},
|
||||||
|
"sha256": "4bf8ae2499b884a4b7e069d4ddad2dd06d56230313b596c53a06c6f75cc535bc",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "coin.png",
|
||||||
|
"sourceNode": "918:9739",
|
||||||
|
"use": "Gold-score icon; leaderboard values remain live API text.",
|
||||||
|
"kind": "single-decorative-icon",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 32, "height": 32 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 64,
|
||||||
|
"pixelHeight": 64,
|
||||||
|
"bytes": 6506
|
||||||
|
},
|
||||||
|
"sha256": "7f7fa561003ed8baf1e7a747637bbe6591bdb1543d7cb39ebe23cd6b322ce8da",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "countdown-cell.png",
|
||||||
|
"sourceNode": "918:7887",
|
||||||
|
"use": "One countdown-number cell; days, hours, minutes and seconds remain live DOM text.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 72, "height": 77 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 144,
|
||||||
|
"pixelHeight": 154,
|
||||||
|
"bytes": 42992
|
||||||
|
},
|
||||||
|
"sha256": "3a11b703471aff658bf499496f59e45e186c390e492545e8f191b7abe840d148",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "hero-art.png",
|
||||||
|
"sourceNode": "918:7884",
|
||||||
|
"use": "Trophy, lions and blank title plaque. The original embedded Gift Challenge title was removed so the localized title remains DOM text.",
|
||||||
|
"kind": "derived-decorative-hero",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 375, "height": 375 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 1000,
|
||||||
|
"pixelHeight": 1000,
|
||||||
|
"bytes": 2039585
|
||||||
|
},
|
||||||
|
"sha256": "36babcc651785d107f310010f255c4c8513976ee9d93e9cbe66e3fbfcd9f582a",
|
||||||
|
"derived": true,
|
||||||
|
"derivation": "Precise object edit removed only the embedded title from Figma node 918:7884 and reconstructed the blank plaque.",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-period-sprite.png",
|
||||||
|
"sourceNode": "918:9020",
|
||||||
|
"use": "Selected and idle ranking-period chrome in one CSS sprite; labels and tab controls remain DOM elements.",
|
||||||
|
"kind": "decorative-state-sprite",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 169, "height": 60 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 676,
|
||||||
|
"pixelHeight": 116,
|
||||||
|
"bytes": 131835
|
||||||
|
},
|
||||||
|
"sha256": "48094db756f6909bed483acc4bcf09d57bdbd8f7536ddc309e276f8c785b2bce",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "rank-row.png",
|
||||||
|
"sourceNode": "918:9728",
|
||||||
|
"use": "Leaderboard-row border; rank, avatar, user identity and score remain live DOM/API content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 351, "height": 88 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 702,
|
||||||
|
"pixelHeight": 176,
|
||||||
|
"bytes": 198958
|
||||||
|
},
|
||||||
|
"sha256": "665dcdacad7bddeea9da79456088123cc50e1376be9fb329989f1265d981d174",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "ranking-palace.jpg",
|
||||||
|
"sourceNode": "918:9029",
|
||||||
|
"use": "Palace, red-carpet and floor background behind the live leaderboard. Source watermarks were removed without adding content.",
|
||||||
|
"kind": "derived-decorative-background",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 359, "height": 637 },
|
||||||
|
"file": {
|
||||||
|
"format": "jpeg",
|
||||||
|
"pixelWidth": 718,
|
||||||
|
"pixelHeight": 1276,
|
||||||
|
"bytes": 317466
|
||||||
|
},
|
||||||
|
"sha256": "5723b0f00739092da4bb58f29a7c491ce7547f18c08caeb839b50e40175221be",
|
||||||
|
"derived": true,
|
||||||
|
"derivation": "Precise object edit removed only the source watermarks from Figma node 918:9029 and reconstructed the palace background beneath them.",
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "ranking-title-frame.png",
|
||||||
|
"sourceNode": "918:9031",
|
||||||
|
"use": "Leaderboard title and podium surround; heading, avatars, names and scores remain live DOM/API content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 375, "height": 326 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 750,
|
||||||
|
"pixelHeight": 652,
|
||||||
|
"bytes": 265575
|
||||||
|
},
|
||||||
|
"sha256": "c26a575e22569903ec0ff0d7c74aa31e30cf3a82446b0c263e63deed39666679",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "reward-count-frame.png",
|
||||||
|
"sourceNode": "918:8710",
|
||||||
|
"use": "Task reward-quantity frame; quantity remains live DOM/API text.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 79, "height": 30 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 158,
|
||||||
|
"pixelHeight": 60,
|
||||||
|
"bytes": 16175
|
||||||
|
},
|
||||||
|
"sha256": "89a2cbbb8eaea727123c417caf5fa3bff3c77c3cbfa8b57333b8495706110c26",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "section-tab-active.png",
|
||||||
|
"sourceNode": "918:19524",
|
||||||
|
"use": "Selected primary-tab chrome; label and click target remain DOM content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 113, "height": 42 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 452,
|
||||||
|
"pixelHeight": 168,
|
||||||
|
"bytes": 123940
|
||||||
|
},
|
||||||
|
"sha256": "f6df12b7c290706f83e009f5b4eeca8a51904d9f2d8992f8dbfdb931a3061b71",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "section-tab-idle.png",
|
||||||
|
"sourceNode": "918:19527",
|
||||||
|
"use": "Idle primary-tab chrome; label and click target remain DOM content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 113, "height": 42 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 452,
|
||||||
|
"pixelHeight": 168,
|
||||||
|
"bytes": 70971
|
||||||
|
},
|
||||||
|
"sha256": "0cb9954872b49c785df55fd59041f614731f362e9c7a02b4459bf1eea6ae9c85",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "side-info.svg",
|
||||||
|
"sourceNode": "918:8934",
|
||||||
|
"use": "Rules icon inside the live DOM action button.",
|
||||||
|
"kind": "single-decorative-icon",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 24, "height": 24 },
|
||||||
|
"file": {
|
||||||
|
"format": "svg",
|
||||||
|
"pixelWidth": 24,
|
||||||
|
"pixelHeight": 24,
|
||||||
|
"bytes": 1896
|
||||||
|
},
|
||||||
|
"exportViewBox": {
|
||||||
|
"minX": 0,
|
||||||
|
"minY": 0,
|
||||||
|
"width": 24,
|
||||||
|
"height": 24
|
||||||
|
},
|
||||||
|
"sha256": "2100697fb9232b99c3ff687e0b8b8231869f8cbe0516ad848939823b0fcfd27c",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "side-rewards.svg",
|
||||||
|
"sourceNode": "918:8939",
|
||||||
|
"use": "Ranking-rewards icon inside the live DOM action button.",
|
||||||
|
"kind": "single-decorative-icon",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 24, "height": 24 },
|
||||||
|
"file": {
|
||||||
|
"format": "svg",
|
||||||
|
"pixelWidth": 24,
|
||||||
|
"pixelHeight": 24,
|
||||||
|
"bytes": 6265
|
||||||
|
},
|
||||||
|
"exportViewBox": {
|
||||||
|
"minX": 0,
|
||||||
|
"minY": 0,
|
||||||
|
"width": 24,
|
||||||
|
"height": 24
|
||||||
|
},
|
||||||
|
"sha256": "734c29bf9b58ff7d6d400191e10b77f30b6578c7f3519ac47dbca08f84343dd8",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "stage-background.jpg",
|
||||||
|
"sourceNode": "918:7830",
|
||||||
|
"use": "Pure red-and-gold event-stage background beneath all live sections.",
|
||||||
|
"kind": "single-decorative-background",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 375, "height": 667 },
|
||||||
|
"file": {
|
||||||
|
"format": "jpeg",
|
||||||
|
"pixelWidth": 750,
|
||||||
|
"pixelHeight": 1333,
|
||||||
|
"bytes": 223745
|
||||||
|
},
|
||||||
|
"sha256": "c70f7d4b377894e84781dc4dff7520c7dd21aa6acf28c53f506d05029bcaff29",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "task-panel.png",
|
||||||
|
"sourceNode": "918:7908",
|
||||||
|
"use": "Pure task-panel background behind three separately rendered DOM task cards.",
|
||||||
|
"kind": "single-decorative-background",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 375, "height": 996 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 375,
|
||||||
|
"pixelHeight": 996,
|
||||||
|
"bytes": 191882
|
||||||
|
},
|
||||||
|
"sha256": "4d6331d2450be1e917c143c84e429aa51e75c7dd8ec4b3cfe1421d22c9413b1c",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "task-reward-frame.png",
|
||||||
|
"sourceNode": "918:8707",
|
||||||
|
"use": "Task reward-item surround; icon, reward name and quantity remain live API content.",
|
||||||
|
"kind": "single-decorative-frame",
|
||||||
|
"scope": "runtime",
|
||||||
|
"renderedDesignSize": { "width": 79, "height": 80 },
|
||||||
|
"file": {
|
||||||
|
"format": "png",
|
||||||
|
"pixelWidth": 158,
|
||||||
|
"pixelHeight": 160,
|
||||||
|
"bytes": 29047
|
||||||
|
},
|
||||||
|
"sha256": "7e7c828e2eef258b6762fa5e0876a8a35731213de976909e29dc729ad856c232",
|
||||||
|
"derived": false,
|
||||||
|
"containsText": false,
|
||||||
|
"containsControls": false,
|
||||||
|
"containsDynamicBusinessMedia": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 129 KiB |
BIN
activity/gift-challenge/aslan/assets/layers/rank-row.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
activity/gift-challenge/aslan/assets/layers/ranking-palace.jpg
Normal file
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 121 KiB |
BIN
activity/gift-challenge/aslan/assets/layers/section-tab-idle.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
11
activity/gift-challenge/aslan/assets/layers/side-info.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="Frame 2085664025">
|
||||||
|
<path id="Exclude" fill-rule="evenodd" clip-rule="evenodd" d="M9.76814 19.9293C9.76814 21.1499 10.7553 22.1396 11.9776 22.1396C13.1998 22.1396 14.187 21.1499 14.187 19.9293C14.187 19.0932 13.7236 18.3663 13.042 17.9902C12.7263 17.8173 12.3637 17.7191 11.9776 17.7191C10.7553 17.7191 9.76814 18.7088 9.76814 19.9293ZM15.1607 12.3483C16.2386 11.5256 17.1989 10.5586 18.0115 9.47405C18.4211 8.80333 18.6293 8.02768 18.6092 7.24112C18.6159 6.52676 18.4648 5.81911 18.1659 5.17022C17.8671 4.52049 17.4306 3.94463 16.8833 3.48377C15.5066 2.3522 13.7572 1.77634 11.9776 1.87036C10.2718 1.84434 8.61978 2.45042 7.33711 3.57191C6.76964 4.05376 6.30291 4.64388 5.97049 5.30872C5.63472 5.97356 5.43996 6.698 5.3896 7.44007C5.36945 8.78906 8.57277 8.50113 8.68358 7.83797C8.79774 7.2705 9.016 6.73158 9.32491 6.24722C9.50287 5.96937 9.7077 5.70998 9.94274 5.47326C10.2819 5.19624 10.668 4.9889 11.0777 4.85878C11.4235 4.74965 11.7862 4.69509 12.1555 4.69929C12.958 4.66991 13.7437 4.93769 14.3649 5.45143C14.6268 5.66968 14.8384 5.94334 14.9861 6.25226C15.1305 6.56201 15.2044 6.89947 15.2044 7.24196C15.2077 7.73807 15.0432 8.22076 14.741 8.61194C14.1601 9.21382 13.5322 9.76785 12.8606 10.2698C12.4309 10.5788 12.0346 10.9288 11.6754 11.3141C11.3429 11.6734 11.0441 12.0637 10.7822 12.4801C10.4061 13.2885 10.2248 14.1749 10.255 15.0664V15.9285H13.502C13.4415 15.2343 13.5255 14.5342 13.7471 13.8736C14.1131 13.2776 14.5933 12.7596 15.1607 12.3483Z" fill="url(#paint0_linear_0_4)"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_0_4" x1="12" y1="1.86042" x2="12" y2="22.1396" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFFEE7"/>
|
||||||
|
<stop offset="1" stop-color="#FEE65C"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
14
activity/gift-challenge/aslan/assets/layers/side-rewards.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="礼物-F 1">
|
||||||
|
<g id="Vector">
|
||||||
|
<path d="M4.9994 4.0355C4.9994 2.498 6.2474 1.25 7.7849 1.25H8.1434C9.7154 1.25 11.1134 1.997 11.9999 3.155C12.4529 2.56222 13.0365 2.08195 13.7054 1.75154C14.3743 1.42113 15.1104 1.2495 15.8564 1.25H16.2149C17.7524 1.25 19.0004 2.498 19.0004 4.0355C19.0004 4.8485 18.7694 5.6075 18.3719 6.251H18.5264C18.9704 6.251 19.3439 6.251 19.6484 6.272C19.9649 6.296 20.2724 6.3455 20.5634 6.4805C20.8964 6.6335 21.1919 6.863 21.4079 7.1615C21.6104 7.4435 21.6869 7.748 21.7199 8.0405C21.7499 8.3045 21.7499 8.6195 21.7499 8.9615V9.0395C21.7499 9.3815 21.7499 9.698 21.7199 9.9605C21.6946 10.276 21.5865 10.5792 21.4064 10.8395C21.1919 11.1395 20.8964 11.3675 20.5649 11.5205C20.2724 11.6555 19.9649 11.705 19.6499 11.729C19.3439 11.75 18.9719 11.75 18.5264 11.75H5.4749C5.0294 11.75 4.6574 11.75 4.3529 11.729C4.03773 11.716 3.72761 11.6453 3.4379 11.5205C3.1031 11.3696 2.81278 11.1351 2.5949 10.8395C2.41422 10.5794 2.30554 10.2762 2.2799 9.9605C2.2541 9.65322 2.24459 9.34479 2.2514 9.0365V8.9645C2.2514 8.6225 2.2514 8.3045 2.2814 8.0405C2.3144 7.748 2.3909 7.4435 2.5949 7.1615C2.8094 6.863 3.1049 6.6335 3.4364 6.4805C3.7289 6.3455 4.0364 6.296 4.3514 6.272C4.6574 6.251 5.0294 6.251 5.4749 6.251H5.6294C5.21728 5.58561 4.99957 4.81818 5.0009 4.0355H4.9994ZM8.1434 3.251H7.7849C7.57696 3.2514 7.37765 3.33418 7.23061 3.48121C7.08357 3.62825 7.00079 3.82756 7.0004 4.0355C7.0004 5.2595 7.9904 6.251 9.2144 6.251H10.9994V6.1055C10.9986 5.34868 10.6976 4.62309 10.1625 4.08794C9.62731 3.55279 8.90171 3.2518 8.1449 3.251H8.1434ZM14.7854 6.251C16.0079 6.251 16.9994 5.2595 16.9994 4.0355C16.999 3.8273 16.916 3.62776 16.7687 3.48068C16.6213 3.33361 16.4216 3.251 16.2134 3.251H15.8564C15.0993 3.2514 14.3733 3.55222 13.8379 4.08741C13.3024 4.62261 13.0012 5.34842 13.0004 6.1055V6.2495H14.7854V6.251ZM3.7874 12.92C3.9554 12.9485 4.1144 12.965 4.2599 12.9755C4.6109 13.001 5.0219 13.001 5.4389 13.001H10.5989C10.7879 13.001 10.8839 13.001 10.9409 13.0595C11.0009 13.118 11.0009 13.2125 11.0009 13.4V22.694C11.0009 22.7255 10.9754 22.751 10.9439 22.751C9.3419 22.751 8.0639 22.751 7.0589 22.616C6.0239 22.4765 5.1689 22.181 4.4939 21.506C3.8189 20.831 3.5234 19.976 3.3839 18.941C3.2489 17.9375 3.2489 16.658 3.2489 15.056V13.3415C3.2489 13.0955 3.2489 12.974 3.3209 12.914C3.3914 12.854 3.5234 12.875 3.7859 12.92H3.7874ZM13.3994 13.001C13.2119 13.001 13.1174 13.001 13.0589 13.0595C12.9989 13.118 12.9989 13.2125 12.9989 13.4V22.694C12.9989 22.7255 13.0244 22.751 13.0559 22.751C14.6579 22.751 15.9359 22.751 16.9409 22.616C17.9759 22.4765 18.8309 22.181 19.5059 21.506C20.1809 20.831 20.4749 19.976 20.6159 18.941C20.7509 17.9375 20.7509 16.658 20.7509 15.056V13.3415C20.7509 13.0955 20.7509 12.974 20.6789 12.914C20.6084 12.854 20.4764 12.875 20.2139 12.92C20.0444 12.9485 19.8839 12.965 19.7399 12.9755C19.3889 13.001 18.9779 13.001 18.5609 13.001H13.4009H13.3994Z" fill="var(--fill-0, #333333)"/>
|
||||||
|
<path d="M4.9994 4.0355C4.9994 2.498 6.2474 1.25 7.7849 1.25H8.1434C9.7154 1.25 11.1134 1.997 11.9999 3.155C12.4529 2.56222 13.0365 2.08195 13.7054 1.75154C14.3743 1.42113 15.1104 1.2495 15.8564 1.25H16.2149C17.7524 1.25 19.0004 2.498 19.0004 4.0355C19.0004 4.8485 18.7694 5.6075 18.3719 6.251H18.5264C18.9704 6.251 19.3439 6.251 19.6484 6.272C19.9649 6.296 20.2724 6.3455 20.5634 6.4805C20.8964 6.6335 21.1919 6.863 21.4079 7.1615C21.6104 7.4435 21.6869 7.748 21.7199 8.0405C21.7499 8.3045 21.7499 8.6195 21.7499 8.9615V9.0395C21.7499 9.3815 21.7499 9.698 21.7199 9.9605C21.6946 10.276 21.5865 10.5792 21.4064 10.8395C21.1919 11.1395 20.8964 11.3675 20.5649 11.5205C20.2724 11.6555 19.9649 11.705 19.6499 11.729C19.3439 11.75 18.9719 11.75 18.5264 11.75H5.4749C5.0294 11.75 4.6574 11.75 4.3529 11.729C4.03773 11.716 3.72761 11.6453 3.4379 11.5205C3.1031 11.3696 2.81278 11.1351 2.5949 10.8395C2.41422 10.5794 2.30554 10.2762 2.2799 9.9605C2.2541 9.65322 2.24459 9.34479 2.2514 9.0365V8.9645C2.2514 8.6225 2.2514 8.3045 2.2814 8.0405C2.3144 7.748 2.3909 7.4435 2.5949 7.1615C2.8094 6.863 3.1049 6.6335 3.4364 6.4805C3.7289 6.3455 4.0364 6.296 4.3514 6.272C4.6574 6.251 5.0294 6.251 5.4749 6.251H5.6294C5.21728 5.58561 4.99957 4.81818 5.0009 4.0355H4.9994ZM8.1434 3.251H7.7849C7.57696 3.2514 7.37765 3.33418 7.23061 3.48121C7.08357 3.62825 7.00079 3.82756 7.0004 4.0355C7.0004 5.2595 7.9904 6.251 9.2144 6.251H10.9994V6.1055C10.9986 5.34868 10.6976 4.62309 10.1625 4.08794C9.62731 3.55279 8.90171 3.2518 8.1449 3.251H8.1434ZM14.7854 6.251C16.0079 6.251 16.9994 5.2595 16.9994 4.0355C16.999 3.8273 16.916 3.62776 16.7687 3.48068C16.6213 3.33361 16.4216 3.251 16.2134 3.251H15.8564C15.0993 3.2514 14.3733 3.55222 13.8379 4.08741C13.3024 4.62261 13.0012 5.34842 13.0004 6.1055V6.2495H14.7854V6.251ZM3.7874 12.92C3.9554 12.9485 4.1144 12.965 4.2599 12.9755C4.6109 13.001 5.0219 13.001 5.4389 13.001H10.5989C10.7879 13.001 10.8839 13.001 10.9409 13.0595C11.0009 13.118 11.0009 13.2125 11.0009 13.4V22.694C11.0009 22.7255 10.9754 22.751 10.9439 22.751C9.3419 22.751 8.0639 22.751 7.0589 22.616C6.0239 22.4765 5.1689 22.181 4.4939 21.506C3.8189 20.831 3.5234 19.976 3.3839 18.941C3.2489 17.9375 3.2489 16.658 3.2489 15.056V13.3415C3.2489 13.0955 3.2489 12.974 3.3209 12.914C3.3914 12.854 3.5234 12.875 3.7859 12.92H3.7874ZM13.3994 13.001C13.2119 13.001 13.1174 13.001 13.0589 13.0595C12.9989 13.118 12.9989 13.2125 12.9989 13.4V22.694C12.9989 22.7255 13.0244 22.751 13.0559 22.751C14.6579 22.751 15.9359 22.751 16.9409 22.616C17.9759 22.4765 18.8309 22.181 19.5059 21.506C20.1809 20.831 20.4749 19.976 20.6159 18.941C20.7509 17.9375 20.7509 16.658 20.7509 15.056V13.3415C20.7509 13.0955 20.7509 12.974 20.6789 12.914C20.6084 12.854 20.4764 12.875 20.2139 12.92C20.0444 12.9485 19.8839 12.965 19.7399 12.9755C19.3889 13.001 18.9779 13.001 18.5609 13.001H13.4009H13.3994Z" fill="url(#paint0_linear_0_4)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_0_4" x1="11.9996" y1="1.25" x2="11.9996" y2="22.751" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#FFFEE7"/>
|
||||||
|
<stop offset="1" stop-color="#FEE65C"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
BIN
activity/gift-challenge/aslan/assets/layers/stage-background.jpg
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
activity/gift-challenge/aslan/assets/layers/task-panel.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 28 KiB |
@ -965,5 +965,116 @@
|
|||||||
"cp.rewardMicSeatIcon": "أيقونة مقعد الميكروفون",
|
"cp.rewardMicSeatIcon": "أيقونة مقعد الميكروفون",
|
||||||
"cp.rewardMicSeatAnimation": "حركة مقعد الميكروفون",
|
"cp.rewardMicSeatAnimation": "حركة مقعد الميكروفون",
|
||||||
"cp.rewardEmojiPack": "حزمة رموز تعبيرية",
|
"cp.rewardEmojiPack": "حزمة رموز تعبيرية",
|
||||||
"cp.rewardVipTrialCard": "بطاقة VIP تجريبية"
|
"cp.rewardVipTrialCard": "بطاقة VIP تجريبية",
|
||||||
|
"gameKing.rule": "القواعد",
|
||||||
|
"gameKing.awards": "الجوائز",
|
||||||
|
"gameKing.award": "الجائزة",
|
||||||
|
"gameKing.countdown": "العد التنازلي للفعالية",
|
||||||
|
"gameKing.days": "ي",
|
||||||
|
"gameKing.sections": "أقسام ملك الألعاب",
|
||||||
|
"gameKing.tabLottery": "السحب",
|
||||||
|
"gameKing.tabTycoon": "ملك الثراء",
|
||||||
|
"gameKing.tabVictorious": "ملك الانتصار",
|
||||||
|
"gameKing.loading": "جارٍ التحميل...",
|
||||||
|
"gameKing.loadFailed": "تعذر تحميل هذه الفعالية.",
|
||||||
|
"gameKing.retry": "إعادة المحاولة",
|
||||||
|
"gameKing.draw": "اسحب جائزة",
|
||||||
|
"gameKing.spin": "تدوير",
|
||||||
|
"gameKing.myChances": "فرصي",
|
||||||
|
"gameKing.myRewards": "جوائزي",
|
||||||
|
"gameKing.startDraw": "ابدأ السحب المحظوظ",
|
||||||
|
"gameKing.dailyRankings": "الترتيب اليومي",
|
||||||
|
"gameKing.overallRankings": "الترتيب العام",
|
||||||
|
"gameKing.loadingRankings": "جارٍ تحميل الترتيب...",
|
||||||
|
"gameKing.rankFailed": "تعذر تحميل الترتيب.",
|
||||||
|
"gameKing.noRankings": "لا توجد بيانات ترتيب بعد.",
|
||||||
|
"gameKing.back": "رجوع",
|
||||||
|
"gameKing.rankingRewards": "جوائز الترتيب",
|
||||||
|
"gameKing.tycoonOverallRewards": "ملك الثراء · الإجمالي",
|
||||||
|
"gameKing.noRewards": "لم يتم إعداد الجوائز بعد.",
|
||||||
|
"gameKing.congratulations": "تهانينا",
|
||||||
|
"gameKing.rewardIssued": "تم إرسال الجائزة تلقائيًا إلى حسابك.",
|
||||||
|
"gameKing.rewardProcessingTitle": "الجائزة قيد المعالجة",
|
||||||
|
"gameKing.rewardProcessing": "يجري إرسال جائزتك.",
|
||||||
|
"gameKing.rewardVerificationTitle": "يجري التحقق من حالة الجائزة",
|
||||||
|
"gameKing.rewardVerification": "يجري التحقق من حالة الجائزة. يرجى التواصل مع الدعم.",
|
||||||
|
"gameKing.rewardDeliveryFailedTitle": "تعذر إرسال الجائزة",
|
||||||
|
"gameKing.rewardDeliveryFailed": "تعذر إرسال الجائزة. يرجى التواصل مع الدعم.",
|
||||||
|
"gameKing.confirm": "تأكيد",
|
||||||
|
"gameKing.receive": "استلام",
|
||||||
|
"gameKing.noDrawRecords": "لا توجد سجلات سحب بعد.",
|
||||||
|
"gameKing.close": "إغلاق",
|
||||||
|
"gameKing.title": "ملك الألعاب",
|
||||||
|
"gameKing.titleGame": "الألعاب",
|
||||||
|
"gameKing.titleKing": "الملك",
|
||||||
|
"gameKing.idPrefix": "المعرف:",
|
||||||
|
"gameKing.season": "الموسم 2",
|
||||||
|
"gameKing.subtitle": "العب. اربح. احكم الساحة.",
|
||||||
|
"gameKing.prizePending": "الجائزة قيد الإعداد",
|
||||||
|
"gameKing.reward": "جائزة",
|
||||||
|
"gameKing.anonymous": "مجهول",
|
||||||
|
"gameKing.ruleEvent": "العب الألعاب المؤهلة خلال الفعالية لتحصل على فرصة للفوز بالجوائز.",
|
||||||
|
"gameKing.ruleBoards": "تضم الفعالية ترتيبي ملك الثراء وملك الانتصار.",
|
||||||
|
"gameKing.ruleTycoon": "يعتمد ترتيب ملك الثراء على الذهب المنفق في الألعاب المؤهلة.",
|
||||||
|
"gameKing.ruleVictorious": "يعتمد ترتيب ملك الانتصار على الذهب الذي تم ربحه في الألعاب المؤهلة.",
|
||||||
|
"gameKing.ruleSettlement": "تتم تسوية جوائز الترتيب بعد انتهاء الفعالية.",
|
||||||
|
"gameKing.ruleChance": "احصل على فرصة سحب واحدة مقابل كل {coins} ذهب مستهلك في الألعاب المؤهلة.",
|
||||||
|
"gameKing.ruleDailyLimit": "يتوفر بحد أقصى {count} سحوبات يوميًا.",
|
||||||
|
"gameKing.ruleNoDailyLimit": "تبقى الفرص غير المستخدمة متاحة طوال الفعالية.",
|
||||||
|
"gameKing.activityEnded": "انتهت الفعالية.",
|
||||||
|
"gameKing.progressNext": "استهلك {coins} ذهب إضافي لتحصل على فرصة أخرى.",
|
||||||
|
"gameKing.goldScore": "{score} ذهب",
|
||||||
|
"gameKing.topSingle": "المركز {rank}",
|
||||||
|
"gameKing.topRange": "المراكز {start}-{end}",
|
||||||
|
"gameKing.rewardsIssued": "تم إرسال الجوائز",
|
||||||
|
"gameKing.settling": "جارٍ تسوية الجوائز",
|
||||||
|
"gameKing.settleAfterEnd": "تتم التسوية بعد انتهاء الفعالية",
|
||||||
|
"gameKing.noChances": "لا توجد فرص سحب متاحة.",
|
||||||
|
"gameKing.drawFailed": "فشل السحب. اضغط مجددًا لإعادة المحاولة بأمان.",
|
||||||
|
"gameKing.recordsFailed": "تعذر تحميل سجلات السحب.",
|
||||||
|
"giftChallenge.title": "تحدي الهدايا",
|
||||||
|
"giftChallenge.rewards": "المكافآت",
|
||||||
|
"giftChallenge.rules": "القواعد",
|
||||||
|
"giftChallenge.countdown": "العد التنازلي للفعالية",
|
||||||
|
"giftChallenge.days": "ي",
|
||||||
|
"giftChallenge.sections": "أقسام تحدي الهدايا",
|
||||||
|
"giftChallenge.tasks": "المهام",
|
||||||
|
"giftChallenge.ranking": "الترتيب",
|
||||||
|
"giftChallenge.loading": "جارٍ التحميل...",
|
||||||
|
"giftChallenge.loadFailed": "تعذر تحميل الفعالية.",
|
||||||
|
"giftChallenge.retry": "إعادة المحاولة",
|
||||||
|
"giftChallenge.noTasks": "لا توجد مهام متاحة.",
|
||||||
|
"giftChallenge.dailyRankings": "الترتيب اليومي",
|
||||||
|
"giftChallenge.overallRankings": "الترتيب العام",
|
||||||
|
"giftChallenge.loadingRankings": "جارٍ تحميل الترتيب...",
|
||||||
|
"giftChallenge.rankFailed": "تعذر تحميل الترتيب.",
|
||||||
|
"giftChallenge.noRankings": "لا توجد بيانات ترتيب بعد.",
|
||||||
|
"giftChallenge.back": "رجوع",
|
||||||
|
"giftChallenge.rankingRewards": "مكافآت الترتيب",
|
||||||
|
"giftChallenge.noRewards": "لم يتم إعداد مكافآت للترتيب.",
|
||||||
|
"giftChallenge.taskRules": "المهام",
|
||||||
|
"giftChallenge.ruleTasks": "تُعاد تهيئة المهام اليومية عند الساعة 00:00 حسب المنطقة الزمنية للفعالية. استلم مكافآت المهام المكتملة قبل التهيئة التالية.",
|
||||||
|
"giftChallenge.rankingRules": "لوحة المتصدرين",
|
||||||
|
"giftChallenge.ruleRanking": "تُحتسب الهدايا الذهبية المرسلة أثناء الفعالية في الترتيب اليومي والعام. لا تُحتسب هدايا الحظ وهدايا الحظ الفائق.",
|
||||||
|
"giftChallenge.close": "إغلاق",
|
||||||
|
"giftChallenge.congratulations": "تهانينا",
|
||||||
|
"giftChallenge.rewardIssued": "تم إرسال المكافأة إلى مخزونك.",
|
||||||
|
"giftChallenge.confirm": "تأكيد",
|
||||||
|
"giftChallenge.claiming": "جارٍ الاستلام",
|
||||||
|
"giftChallenge.claimed": "تم الاستلام",
|
||||||
|
"giftChallenge.processing": "جارٍ الإرسال",
|
||||||
|
"giftChallenge.underReview": "قيد المراجعة",
|
||||||
|
"giftChallenge.receive": "استلام",
|
||||||
|
"giftChallenge.go": "اذهب",
|
||||||
|
"giftChallenge.reward": "مكافأة",
|
||||||
|
"giftChallenge.taskEnter": "ادخل صفحة الفعالية يوميًا.",
|
||||||
|
"giftChallenge.taskSendGold": "أرسل يوميًا هدايا مؤهلة بقيمة {amount} من الذهب.",
|
||||||
|
"giftChallenge.untitledTask": "مهمة يومية",
|
||||||
|
"giftChallenge.anonymous": "مجهول",
|
||||||
|
"giftChallenge.idPrefix": "المعرّف:",
|
||||||
|
"giftChallenge.topSingle": "المركز {rank}",
|
||||||
|
"giftChallenge.topRange": "المراكز {start}-{end}",
|
||||||
|
"giftChallenge.claimFailed": "تعذر استلام المكافأة. حاول مرة أخرى.",
|
||||||
|
"giftChallenge.rewardPending": "جارٍ التحقق من إرسال المكافأة.",
|
||||||
|
"giftChallenge.completeInApp": "أكمل هذه المهمة داخل التطبيق."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -971,5 +971,116 @@
|
|||||||
"cp.rewardMicSeatIcon": "Mic seat icon",
|
"cp.rewardMicSeatIcon": "Mic seat icon",
|
||||||
"cp.rewardMicSeatAnimation": "Mic seat animation",
|
"cp.rewardMicSeatAnimation": "Mic seat animation",
|
||||||
"cp.rewardEmojiPack": "Emoji pack",
|
"cp.rewardEmojiPack": "Emoji pack",
|
||||||
"cp.rewardVipTrialCard": "VIP trial card"
|
"cp.rewardVipTrialCard": "VIP trial card",
|
||||||
|
"gameKing.rule": "Rule",
|
||||||
|
"gameKing.awards": "Awards",
|
||||||
|
"gameKing.award": "Award",
|
||||||
|
"gameKing.countdown": "Activity countdown",
|
||||||
|
"gameKing.days": "D",
|
||||||
|
"gameKing.sections": "Game King sections",
|
||||||
|
"gameKing.tabLottery": "Lottery",
|
||||||
|
"gameKing.tabTycoon": "Tycoon King",
|
||||||
|
"gameKing.tabVictorious": "Victorious King",
|
||||||
|
"gameKing.loading": "Loading...",
|
||||||
|
"gameKing.loadFailed": "Unable to load this activity.",
|
||||||
|
"gameKing.retry": "Retry",
|
||||||
|
"gameKing.draw": "Draw a prize",
|
||||||
|
"gameKing.spin": "Spin",
|
||||||
|
"gameKing.myChances": "My chances",
|
||||||
|
"gameKing.myRewards": "My rewards",
|
||||||
|
"gameKing.startDraw": "Start the lucky draw",
|
||||||
|
"gameKing.dailyRankings": "Daily Rankings",
|
||||||
|
"gameKing.overallRankings": "Overall Rankings",
|
||||||
|
"gameKing.loadingRankings": "Loading rankings...",
|
||||||
|
"gameKing.rankFailed": "Unable to load rankings.",
|
||||||
|
"gameKing.noRankings": "No ranking data yet.",
|
||||||
|
"gameKing.back": "Back",
|
||||||
|
"gameKing.rankingRewards": "Ranking rewards",
|
||||||
|
"gameKing.tycoonOverallRewards": "Tycoon King · Overall",
|
||||||
|
"gameKing.noRewards": "No reward configuration yet.",
|
||||||
|
"gameKing.congratulations": "Congratulations",
|
||||||
|
"gameKing.rewardIssued": "The reward has been automatically issued to your account.",
|
||||||
|
"gameKing.rewardProcessingTitle": "Reward is processing",
|
||||||
|
"gameKing.rewardProcessing": "Your reward is being delivered.",
|
||||||
|
"gameKing.rewardVerificationTitle": "Reward status is being verified",
|
||||||
|
"gameKing.rewardVerification": "Reward status is being verified. Please contact support.",
|
||||||
|
"gameKing.rewardDeliveryFailedTitle": "Reward delivery failed",
|
||||||
|
"gameKing.rewardDeliveryFailed": "Reward delivery failed. Please contact support.",
|
||||||
|
"gameKing.confirm": "Confirm",
|
||||||
|
"gameKing.receive": "Receive",
|
||||||
|
"gameKing.noDrawRecords": "No draw records yet.",
|
||||||
|
"gameKing.close": "Close",
|
||||||
|
"gameKing.title": "Game King",
|
||||||
|
"gameKing.titleGame": "Game",
|
||||||
|
"gameKing.titleKing": "King",
|
||||||
|
"gameKing.idPrefix": "ID:",
|
||||||
|
"gameKing.season": "Season 2",
|
||||||
|
"gameKing.subtitle": "Play. Win. Rule the arena.",
|
||||||
|
"gameKing.prizePending": "Prize pending",
|
||||||
|
"gameKing.reward": "Reward",
|
||||||
|
"gameKing.anonymous": "Anonymous",
|
||||||
|
"gameKing.ruleEvent": "Play eligible games during the event for a chance to win rewards.",
|
||||||
|
"gameKing.ruleBoards": "The event has Tycoon King and Victorious King leaderboards.",
|
||||||
|
"gameKing.ruleTycoon": "Tycoon King is ranked by gold spent in eligible games.",
|
||||||
|
"gameKing.ruleVictorious": "Victorious King is ranked by gold won in eligible games.",
|
||||||
|
"gameKing.ruleSettlement": "Ranking rewards are settled after the activity ends.",
|
||||||
|
"gameKing.ruleChance": "Automatically Receive 1 Lucky Draw Chance For Every {coins} Gold Coins Wagered In The Game.",
|
||||||
|
"gameKing.ruleDailyLimit": "A Maximum Of {count} Draws Are Available Per Day.",
|
||||||
|
"gameKing.ruleNoDailyLimit": "Unused chances remain available during the activity.",
|
||||||
|
"gameKing.activityEnded": "The activity has ended.",
|
||||||
|
"gameKing.progressNext": "Consume {coins} more gold to receive another chance.",
|
||||||
|
"gameKing.goldScore": "{score} gold",
|
||||||
|
"gameKing.topSingle": "TOP {rank}",
|
||||||
|
"gameKing.topRange": "TOP {start}-{end}",
|
||||||
|
"gameKing.rewardsIssued": "Rewards issued",
|
||||||
|
"gameKing.settling": "Settling rewards",
|
||||||
|
"gameKing.settleAfterEnd": "Settled after the activity ends",
|
||||||
|
"gameKing.noChances": "No draw chances available.",
|
||||||
|
"gameKing.drawFailed": "Draw failed. Tap again to retry safely.",
|
||||||
|
"gameKing.recordsFailed": "Unable to load draw records.",
|
||||||
|
"giftChallenge.title": "Gift Challenge",
|
||||||
|
"giftChallenge.rewards": "Rewards",
|
||||||
|
"giftChallenge.rules": "Rules",
|
||||||
|
"giftChallenge.countdown": "Activity countdown",
|
||||||
|
"giftChallenge.days": "D",
|
||||||
|
"giftChallenge.sections": "Gift Challenge sections",
|
||||||
|
"giftChallenge.tasks": "Tasks",
|
||||||
|
"giftChallenge.ranking": "Ranking",
|
||||||
|
"giftChallenge.loading": "Loading...",
|
||||||
|
"giftChallenge.loadFailed": "Unable to load the activity.",
|
||||||
|
"giftChallenge.retry": "Retry",
|
||||||
|
"giftChallenge.noTasks": "No tasks are available.",
|
||||||
|
"giftChallenge.dailyRankings": "Daily Rankings",
|
||||||
|
"giftChallenge.overallRankings": "Overall Rankings",
|
||||||
|
"giftChallenge.loadingRankings": "Loading rankings...",
|
||||||
|
"giftChallenge.rankFailed": "Unable to load rankings.",
|
||||||
|
"giftChallenge.noRankings": "No ranking data yet.",
|
||||||
|
"giftChallenge.back": "Back",
|
||||||
|
"giftChallenge.rankingRewards": "Ranking Rewards",
|
||||||
|
"giftChallenge.noRewards": "No ranking rewards are configured.",
|
||||||
|
"giftChallenge.taskRules": "Tasks",
|
||||||
|
"giftChallenge.ruleTasks": "Daily progress resets at 00:00 in the activity time zone. Claim completed task rewards before the next reset.",
|
||||||
|
"giftChallenge.rankingRules": "Leaderboard",
|
||||||
|
"giftChallenge.ruleRanking": "Gold gifts sent during the activity count toward the daily and overall rankings. Lucky Gifts and Super Lucky Gifts do not count.",
|
||||||
|
"giftChallenge.close": "Close",
|
||||||
|
"giftChallenge.congratulations": "Congratulations",
|
||||||
|
"giftChallenge.rewardIssued": "The reward has been sent to your inventory.",
|
||||||
|
"giftChallenge.confirm": "Confirm",
|
||||||
|
"giftChallenge.claiming": "Claiming",
|
||||||
|
"giftChallenge.claimed": "Claimed",
|
||||||
|
"giftChallenge.processing": "Processing",
|
||||||
|
"giftChallenge.underReview": "Under review",
|
||||||
|
"giftChallenge.receive": "Receive",
|
||||||
|
"giftChallenge.go": "Go",
|
||||||
|
"giftChallenge.reward": "Reward",
|
||||||
|
"giftChallenge.taskEnter": "Visit the event page daily.",
|
||||||
|
"giftChallenge.taskSendGold": "Send {amount} Gold worth of eligible gifts daily.",
|
||||||
|
"giftChallenge.untitledTask": "Daily task",
|
||||||
|
"giftChallenge.anonymous": "Anonymous",
|
||||||
|
"giftChallenge.idPrefix": "ID:",
|
||||||
|
"giftChallenge.topSingle": "Top {rank}",
|
||||||
|
"giftChallenge.topRange": "Top {start}-{end}",
|
||||||
|
"giftChallenge.claimFailed": "Unable to claim the reward. Please try again.",
|
||||||
|
"giftChallenge.rewardPending": "Reward delivery is being verified.",
|
||||||
|
"giftChallenge.completeInApp": "Complete this task in the app."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -965,5 +965,116 @@
|
|||||||
"cp.rewardMicSeatIcon": "Icono de asiento de micrófono",
|
"cp.rewardMicSeatIcon": "Icono de asiento de micrófono",
|
||||||
"cp.rewardMicSeatAnimation": "Animación de asiento de micrófono",
|
"cp.rewardMicSeatAnimation": "Animación de asiento de micrófono",
|
||||||
"cp.rewardEmojiPack": "Paquete de emojis",
|
"cp.rewardEmojiPack": "Paquete de emojis",
|
||||||
"cp.rewardVipTrialCard": "Tarjeta de prueba VIP"
|
"cp.rewardVipTrialCard": "Tarjeta de prueba VIP",
|
||||||
|
"gameKing.rule": "Reglas",
|
||||||
|
"gameKing.awards": "Premios",
|
||||||
|
"gameKing.award": "Premio",
|
||||||
|
"gameKing.countdown": "Cuenta atrás del evento",
|
||||||
|
"gameKing.days": "D",
|
||||||
|
"gameKing.sections": "Secciones de Game King",
|
||||||
|
"gameKing.tabLottery": "Sorteo",
|
||||||
|
"gameKing.tabTycoon": "Rey Magnate",
|
||||||
|
"gameKing.tabVictorious": "Rey Victorioso",
|
||||||
|
"gameKing.loading": "Cargando...",
|
||||||
|
"gameKing.loadFailed": "No se pudo cargar este evento.",
|
||||||
|
"gameKing.retry": "Reintentar",
|
||||||
|
"gameKing.draw": "Sortear un premio",
|
||||||
|
"gameKing.spin": "Girar",
|
||||||
|
"gameKing.myChances": "Mis oportunidades",
|
||||||
|
"gameKing.myRewards": "Mis premios",
|
||||||
|
"gameKing.startDraw": "Iniciar el sorteo",
|
||||||
|
"gameKing.dailyRankings": "Clasificación diaria",
|
||||||
|
"gameKing.overallRankings": "Clasificación general",
|
||||||
|
"gameKing.loadingRankings": "Cargando clasificación...",
|
||||||
|
"gameKing.rankFailed": "No se pudo cargar la clasificación.",
|
||||||
|
"gameKing.noRankings": "Aún no hay datos de clasificación.",
|
||||||
|
"gameKing.back": "Volver",
|
||||||
|
"gameKing.rankingRewards": "Premios de clasificación",
|
||||||
|
"gameKing.tycoonOverallRewards": "Rey Magnate · General",
|
||||||
|
"gameKing.noRewards": "Aún no hay premios configurados.",
|
||||||
|
"gameKing.congratulations": "Felicidades",
|
||||||
|
"gameKing.rewardIssued": "El premio se envió automáticamente a tu cuenta.",
|
||||||
|
"gameKing.rewardProcessingTitle": "El premio se está procesando",
|
||||||
|
"gameKing.rewardProcessing": "Tu premio se está enviando.",
|
||||||
|
"gameKing.rewardVerificationTitle": "Se está verificando el estado del premio",
|
||||||
|
"gameKing.rewardVerification": "Se está verificando el estado del premio. Contacta con soporte.",
|
||||||
|
"gameKing.rewardDeliveryFailedTitle": "No se pudo enviar el premio",
|
||||||
|
"gameKing.rewardDeliveryFailed": "No se pudo enviar el premio. Contacta con soporte.",
|
||||||
|
"gameKing.confirm": "Confirmar",
|
||||||
|
"gameKing.receive": "Recibir",
|
||||||
|
"gameKing.noDrawRecords": "Aún no hay registros de sorteos.",
|
||||||
|
"gameKing.close": "Cerrar",
|
||||||
|
"gameKing.title": "Rey de los Juegos",
|
||||||
|
"gameKing.titleGame": "Juego",
|
||||||
|
"gameKing.titleKing": "Rey",
|
||||||
|
"gameKing.idPrefix": "ID:",
|
||||||
|
"gameKing.season": "Temporada 2",
|
||||||
|
"gameKing.subtitle": "Juega. Gana. Domina la arena.",
|
||||||
|
"gameKing.prizePending": "Premio pendiente",
|
||||||
|
"gameKing.reward": "Premio",
|
||||||
|
"gameKing.anonymous": "Anónimo",
|
||||||
|
"gameKing.ruleEvent": "Juega a los juegos válidos durante el evento para ganar premios.",
|
||||||
|
"gameKing.ruleBoards": "El evento incluye las clasificaciones Rey Magnate y Rey Victorioso.",
|
||||||
|
"gameKing.ruleTycoon": "Rey Magnate se ordena por el oro gastado en juegos válidos.",
|
||||||
|
"gameKing.ruleVictorious": "Rey Victorioso se ordena por el oro ganado en juegos válidos.",
|
||||||
|
"gameKing.ruleSettlement": "Los premios de clasificación se liquidan al finalizar el evento.",
|
||||||
|
"gameKing.ruleChance": "Recibe 1 oportunidad por cada {coins} de oro consumido en juegos válidos.",
|
||||||
|
"gameKing.ruleDailyLimit": "Hay un máximo de {count} sorteos al día.",
|
||||||
|
"gameKing.ruleNoDailyLimit": "Las oportunidades no usadas siguen disponibles durante el evento.",
|
||||||
|
"gameKing.activityEnded": "El evento ha finalizado.",
|
||||||
|
"gameKing.progressNext": "Consume {coins} de oro más para recibir otra oportunidad.",
|
||||||
|
"gameKing.goldScore": "{score} de oro",
|
||||||
|
"gameKing.topSingle": "TOP {rank}",
|
||||||
|
"gameKing.topRange": "TOP {start}-{end}",
|
||||||
|
"gameKing.rewardsIssued": "Premios enviados",
|
||||||
|
"gameKing.settling": "Liquidando premios",
|
||||||
|
"gameKing.settleAfterEnd": "Se liquida al finalizar el evento",
|
||||||
|
"gameKing.noChances": "No hay oportunidades de sorteo disponibles.",
|
||||||
|
"gameKing.drawFailed": "El sorteo falló. Toca de nuevo para reintentar de forma segura.",
|
||||||
|
"gameKing.recordsFailed": "No se pudieron cargar los registros de sorteos.",
|
||||||
|
"giftChallenge.title": "Desafío de Regalos",
|
||||||
|
"giftChallenge.rewards": "Premios",
|
||||||
|
"giftChallenge.rules": "Reglas",
|
||||||
|
"giftChallenge.countdown": "Cuenta atrás del evento",
|
||||||
|
"giftChallenge.days": "D",
|
||||||
|
"giftChallenge.sections": "Secciones del Desafío de Regalos",
|
||||||
|
"giftChallenge.tasks": "Tareas",
|
||||||
|
"giftChallenge.ranking": "Clasificación",
|
||||||
|
"giftChallenge.loading": "Cargando...",
|
||||||
|
"giftChallenge.loadFailed": "No se pudo cargar el evento.",
|
||||||
|
"giftChallenge.retry": "Reintentar",
|
||||||
|
"giftChallenge.noTasks": "No hay tareas disponibles.",
|
||||||
|
"giftChallenge.dailyRankings": "Clasificación Diaria",
|
||||||
|
"giftChallenge.overallRankings": "Clasificación General",
|
||||||
|
"giftChallenge.loadingRankings": "Cargando clasificación...",
|
||||||
|
"giftChallenge.rankFailed": "No se pudo cargar la clasificación.",
|
||||||
|
"giftChallenge.noRankings": "Aún no hay datos de clasificación.",
|
||||||
|
"giftChallenge.back": "Volver",
|
||||||
|
"giftChallenge.rankingRewards": "Premios de Clasificación",
|
||||||
|
"giftChallenge.noRewards": "No hay premios de clasificación configurados.",
|
||||||
|
"giftChallenge.taskRules": "Tareas",
|
||||||
|
"giftChallenge.ruleTasks": "El progreso diario se reinicia a las 00:00 en la zona horaria del evento. Recoge los premios completados antes del siguiente reinicio.",
|
||||||
|
"giftChallenge.rankingRules": "Clasificación",
|
||||||
|
"giftChallenge.ruleRanking": "Los regalos de Oro enviados durante el evento cuentan para las clasificaciones diaria y general. Los Regalos de la Suerte y Súper Suerte no cuentan.",
|
||||||
|
"giftChallenge.close": "Cerrar",
|
||||||
|
"giftChallenge.congratulations": "Enhorabuena",
|
||||||
|
"giftChallenge.rewardIssued": "El premio se ha enviado a tu inventario.",
|
||||||
|
"giftChallenge.confirm": "Confirmar",
|
||||||
|
"giftChallenge.claiming": "Recogiendo",
|
||||||
|
"giftChallenge.claimed": "Recogido",
|
||||||
|
"giftChallenge.processing": "Enviando",
|
||||||
|
"giftChallenge.underReview": "En revisión",
|
||||||
|
"giftChallenge.receive": "Recibir",
|
||||||
|
"giftChallenge.go": "Ir",
|
||||||
|
"giftChallenge.reward": "Premio",
|
||||||
|
"giftChallenge.taskEnter": "Visita la página del evento cada día.",
|
||||||
|
"giftChallenge.taskSendGold": "Envía cada día regalos válidos por valor de {amount} de Oro.",
|
||||||
|
"giftChallenge.untitledTask": "Tarea diaria",
|
||||||
|
"giftChallenge.anonymous": "Anónimo",
|
||||||
|
"giftChallenge.idPrefix": "ID:",
|
||||||
|
"giftChallenge.topSingle": "Top {rank}",
|
||||||
|
"giftChallenge.topRange": "Top {start}-{end}",
|
||||||
|
"giftChallenge.claimFailed": "No se pudo recoger el premio. Inténtalo de nuevo.",
|
||||||
|
"giftChallenge.rewardPending": "Se está verificando la entrega del premio.",
|
||||||
|
"giftChallenge.completeInApp": "Completa esta tarea en la aplicación."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -932,5 +932,116 @@
|
|||||||
"cp.rewardMicSeatIcon": "Mikrofon koltuğu simgesi",
|
"cp.rewardMicSeatIcon": "Mikrofon koltuğu simgesi",
|
||||||
"cp.rewardMicSeatAnimation": "Mikrofon koltuğu animasyonu",
|
"cp.rewardMicSeatAnimation": "Mikrofon koltuğu animasyonu",
|
||||||
"cp.rewardEmojiPack": "Emoji paketi",
|
"cp.rewardEmojiPack": "Emoji paketi",
|
||||||
"cp.rewardVipTrialCard": "VIP deneme kartı"
|
"cp.rewardVipTrialCard": "VIP deneme kartı",
|
||||||
|
"gameKing.rule": "Kurallar",
|
||||||
|
"gameKing.awards": "Ödüller",
|
||||||
|
"gameKing.award": "Ödül",
|
||||||
|
"gameKing.countdown": "Etkinlik geri sayımı",
|
||||||
|
"gameKing.days": "G",
|
||||||
|
"gameKing.sections": "Game King bölümleri",
|
||||||
|
"gameKing.tabLottery": "Çekiliş",
|
||||||
|
"gameKing.tabTycoon": "Zenginlik Kralı",
|
||||||
|
"gameKing.tabVictorious": "Zafer Kralı",
|
||||||
|
"gameKing.loading": "Yükleniyor...",
|
||||||
|
"gameKing.loadFailed": "Bu etkinlik yüklenemedi.",
|
||||||
|
"gameKing.retry": "Tekrar dene",
|
||||||
|
"gameKing.draw": "Ödül çek",
|
||||||
|
"gameKing.spin": "Çevir",
|
||||||
|
"gameKing.myChances": "Haklarım",
|
||||||
|
"gameKing.myRewards": "Ödüllerim",
|
||||||
|
"gameKing.startDraw": "Şanslı çekilişi başlat",
|
||||||
|
"gameKing.dailyRankings": "Günlük Sıralama",
|
||||||
|
"gameKing.overallRankings": "Genel Sıralama",
|
||||||
|
"gameKing.loadingRankings": "Sıralama yükleniyor...",
|
||||||
|
"gameKing.rankFailed": "Sıralama yüklenemedi.",
|
||||||
|
"gameKing.noRankings": "Henüz sıralama verisi yok.",
|
||||||
|
"gameKing.back": "Geri",
|
||||||
|
"gameKing.rankingRewards": "Sıralama ödülleri",
|
||||||
|
"gameKing.tycoonOverallRewards": "Zenginlik Kralı · Genel",
|
||||||
|
"gameKing.noRewards": "Henüz ödül yapılandırılmadı.",
|
||||||
|
"gameKing.congratulations": "Tebrikler",
|
||||||
|
"gameKing.rewardIssued": "Ödül otomatik olarak hesabınıza gönderildi.",
|
||||||
|
"gameKing.rewardProcessingTitle": "Ödül işleniyor",
|
||||||
|
"gameKing.rewardProcessing": "Ödülünüz gönderiliyor.",
|
||||||
|
"gameKing.rewardVerificationTitle": "Ödül durumu doğrulanıyor",
|
||||||
|
"gameKing.rewardVerification": "Ödül durumu doğrulanıyor. Lütfen destekle iletişime geçin.",
|
||||||
|
"gameKing.rewardDeliveryFailedTitle": "Ödül gönderilemedi",
|
||||||
|
"gameKing.rewardDeliveryFailed": "Ödül gönderilemedi. Lütfen destekle iletişime geçin.",
|
||||||
|
"gameKing.confirm": "Onayla",
|
||||||
|
"gameKing.receive": "Al",
|
||||||
|
"gameKing.noDrawRecords": "Henüz çekiliş kaydı yok.",
|
||||||
|
"gameKing.close": "Kapat",
|
||||||
|
"gameKing.title": "Oyun Kralı",
|
||||||
|
"gameKing.titleGame": "Oyun",
|
||||||
|
"gameKing.titleKing": "Kralı",
|
||||||
|
"gameKing.idPrefix": "ID:",
|
||||||
|
"gameKing.season": "Sezon 2",
|
||||||
|
"gameKing.subtitle": "Oyna. Kazan. Arenaya hükmet.",
|
||||||
|
"gameKing.prizePending": "Ödül bekleniyor",
|
||||||
|
"gameKing.reward": "Ödül",
|
||||||
|
"gameKing.anonymous": "Anonim",
|
||||||
|
"gameKing.ruleEvent": "Ödül kazanma şansı için etkinlik sırasında uygun oyunları oynayın.",
|
||||||
|
"gameKing.ruleBoards": "Etkinlikte Zenginlik Kralı ve Zafer Kralı sıralamaları bulunur.",
|
||||||
|
"gameKing.ruleTycoon": "Zenginlik Kralı, uygun oyunlarda harcanan altına göre sıralanır.",
|
||||||
|
"gameKing.ruleVictorious": "Zafer Kralı, uygun oyunlarda kazanılan altına göre sıralanır.",
|
||||||
|
"gameKing.ruleSettlement": "Sıralama ödülleri etkinlik bittikten sonra hesaplanır.",
|
||||||
|
"gameKing.ruleChance": "Uygun oyunlarda harcanan her {coins} altın için 1 çekiliş hakkı kazanın.",
|
||||||
|
"gameKing.ruleDailyLimit": "Günde en fazla {count} çekiliş yapılabilir.",
|
||||||
|
"gameKing.ruleNoDailyLimit": "Kullanılmayan haklar etkinlik boyunca kullanılabilir.",
|
||||||
|
"gameKing.activityEnded": "Etkinlik sona erdi.",
|
||||||
|
"gameKing.progressNext": "Yeni bir hak için {coins} altın daha harcayın.",
|
||||||
|
"gameKing.goldScore": "{score} altın",
|
||||||
|
"gameKing.topSingle": "İLK {rank}",
|
||||||
|
"gameKing.topRange": "İLK {start}-{end}",
|
||||||
|
"gameKing.rewardsIssued": "Ödüller gönderildi",
|
||||||
|
"gameKing.settling": "Ödüller hesaplanıyor",
|
||||||
|
"gameKing.settleAfterEnd": "Etkinlik sonunda hesaplanır",
|
||||||
|
"gameKing.noChances": "Kullanılabilir çekiliş hakkınız yok.",
|
||||||
|
"gameKing.drawFailed": "Çekiliş başarısız. Güvenle yeniden denemek için dokunun.",
|
||||||
|
"gameKing.recordsFailed": "Çekiliş kayıtları yüklenemedi.",
|
||||||
|
"giftChallenge.title": "Hediye Mücadelesi",
|
||||||
|
"giftChallenge.rewards": "Ödüller",
|
||||||
|
"giftChallenge.rules": "Kurallar",
|
||||||
|
"giftChallenge.countdown": "Etkinlik geri sayımı",
|
||||||
|
"giftChallenge.days": "G",
|
||||||
|
"giftChallenge.sections": "Hediye Mücadelesi bölümleri",
|
||||||
|
"giftChallenge.tasks": "Görevler",
|
||||||
|
"giftChallenge.ranking": "Sıralama",
|
||||||
|
"giftChallenge.loading": "Yükleniyor...",
|
||||||
|
"giftChallenge.loadFailed": "Etkinlik yüklenemedi.",
|
||||||
|
"giftChallenge.retry": "Yeniden dene",
|
||||||
|
"giftChallenge.noTasks": "Kullanılabilir görev yok.",
|
||||||
|
"giftChallenge.dailyRankings": "Günlük Sıralama",
|
||||||
|
"giftChallenge.overallRankings": "Genel Sıralama",
|
||||||
|
"giftChallenge.loadingRankings": "Sıralama yükleniyor...",
|
||||||
|
"giftChallenge.rankFailed": "Sıralama yüklenemedi.",
|
||||||
|
"giftChallenge.noRankings": "Henüz sıralama verisi yok.",
|
||||||
|
"giftChallenge.back": "Geri",
|
||||||
|
"giftChallenge.rankingRewards": "Sıralama Ödülleri",
|
||||||
|
"giftChallenge.noRewards": "Sıralama ödülü yapılandırılmadı.",
|
||||||
|
"giftChallenge.taskRules": "Görevler",
|
||||||
|
"giftChallenge.ruleTasks": "Günlük ilerleme, etkinlik saat diliminde 00:00'da sıfırlanır. Tamamlanan görev ödüllerini sonraki sıfırlamadan önce alın.",
|
||||||
|
"giftChallenge.rankingRules": "Liderlik Tablosu",
|
||||||
|
"giftChallenge.ruleRanking": "Etkinlik sırasında gönderilen Altın hediyeler günlük ve genel sıralamaya sayılır. Şanslı Hediyeler ve Süper Şanslı Hediyeler sayılmaz.",
|
||||||
|
"giftChallenge.close": "Kapat",
|
||||||
|
"giftChallenge.congratulations": "Tebrikler",
|
||||||
|
"giftChallenge.rewardIssued": "Ödül envanterinize gönderildi.",
|
||||||
|
"giftChallenge.confirm": "Onayla",
|
||||||
|
"giftChallenge.claiming": "Alınıyor",
|
||||||
|
"giftChallenge.claimed": "Alındı",
|
||||||
|
"giftChallenge.processing": "Gönderiliyor",
|
||||||
|
"giftChallenge.underReview": "İnceleniyor",
|
||||||
|
"giftChallenge.receive": "Al",
|
||||||
|
"giftChallenge.go": "Git",
|
||||||
|
"giftChallenge.reward": "Ödül",
|
||||||
|
"giftChallenge.taskEnter": "Etkinlik sayfasını her gün ziyaret et.",
|
||||||
|
"giftChallenge.taskSendGold": "Her gün {amount} Altın değerinde uygun hediye gönder.",
|
||||||
|
"giftChallenge.untitledTask": "Günlük görev",
|
||||||
|
"giftChallenge.anonymous": "Anonim",
|
||||||
|
"giftChallenge.idPrefix": "ID:",
|
||||||
|
"giftChallenge.topSingle": "İlk {rank}",
|
||||||
|
"giftChallenge.topRange": "İlk {start}-{end}",
|
||||||
|
"giftChallenge.claimFailed": "Ödül alınamadı. Lütfen tekrar deneyin.",
|
||||||
|
"giftChallenge.rewardPending": "Ödül teslimatı doğrulanıyor.",
|
||||||
|
"giftChallenge.completeInApp": "Bu görevi uygulamada tamamlayın."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
var LOCAL_API_BASE = 'http://127.0.0.1:1100/';
|
var LOCAL_API_BASE = 'http://127.0.0.1:1100/';
|
||||||
var APP_CODE = 'aslan';
|
var APP_CODE = 'aslan';
|
||||||
var DEFAULT_ORIGIN = 'ATYOU';
|
var DEFAULT_ORIGIN = 'ATYOU';
|
||||||
|
var LOOPBACK_API_HOSTS = ['127.0.0.1', 'localhost', '[::1]'];
|
||||||
var readyPromise = null;
|
var readyPromise = null;
|
||||||
var headers = {
|
var headers = {
|
||||||
authorization: '',
|
authorization: '',
|
||||||
@ -46,17 +47,51 @@
|
|||||||
return env === 'local' || env === 'test' || env === 'dev' ? env : '';
|
return env === 'local' || env === 'test' || env === 'dev' ? env : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nonProductionBaseURL(value) {
|
||||||
|
var config = window.AslanCommonConfig || {};
|
||||||
|
var trustedHosts = Array.isArray(config.trustedApiHosts)
|
||||||
|
? config.trustedApiHosts.map(function (host) {
|
||||||
|
return String(host || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
var parsed;
|
||||||
|
try {
|
||||||
|
parsed = new URL(String(value || ''));
|
||||||
|
} catch (_) {
|
||||||
|
throw new Error('aslan_test_api_invalid');
|
||||||
|
}
|
||||||
|
var host = String(parsed.hostname || '').toLowerCase();
|
||||||
|
var isLoopback = LOOPBACK_API_HOSTS.indexOf(host) >= 0;
|
||||||
|
var isTrusted = trustedHosts.indexOf(host) >= 0;
|
||||||
|
// App authorization is attached to every request. Test/dev may use only a
|
||||||
|
// build-time configured HTTP(S) origin whose hostname is explicitly
|
||||||
|
// allowlisted; URL parameters are intentionally never an API override.
|
||||||
|
if (
|
||||||
|
!/^https?:$/.test(parsed.protocol) ||
|
||||||
|
!(isLoopback || (parsed.protocol === 'https:' && isTrusted))
|
||||||
|
) {
|
||||||
|
throw new Error('aslan_test_api_untrusted');
|
||||||
|
}
|
||||||
|
return normalizeBaseURL(parsed.toString());
|
||||||
|
}
|
||||||
|
|
||||||
function baseURL() {
|
function baseURL() {
|
||||||
var override =
|
var env = resolveEnv();
|
||||||
query('apiBase') ||
|
// Production must always use the compiled service origin: accepting an
|
||||||
query('api_base') ||
|
// apiBase query here would forward the App bearer token to an attacker URL.
|
||||||
query('aslan_api_base') ||
|
if (!env) return normalizeBaseURL(PROD_API_BASE);
|
||||||
window.ASLAN_API_BASE_URL ||
|
if (env === 'local') return normalizeBaseURL(LOCAL_API_BASE);
|
||||||
(window.AslanCommonConfig && window.AslanCommonConfig.apiBase);
|
if (env === 'test' || env === 'dev') {
|
||||||
if (override) return normalizeBaseURL(override);
|
var testBase =
|
||||||
return resolveEnv() === 'local'
|
window.ASLAN_TEST_API_BASE_URL ||
|
||||||
? normalizeBaseURL(LOCAL_API_BASE)
|
(window.AslanCommonConfig && window.AslanCommonConfig.testApi);
|
||||||
: normalizeBaseURL(PROD_API_BASE);
|
// Test/dev 请求携带真实 App 登录态,未注入独立测试地址时必须失败,
|
||||||
|
// 不能把 `?env=test` 静默回落到生产域名并产生抽奖或领奖写入。
|
||||||
|
if (!testBase) throw new Error('aslan_test_api_not_configured');
|
||||||
|
return nonProductionBaseURL(testBase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildURL(path, queryData) {
|
function buildURL(path, queryData) {
|
||||||
@ -519,5 +554,116 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
gameKing: {
|
||||||
|
current: function () {
|
||||||
|
return request('/activity/game/king/aslan/detail', {
|
||||||
|
method: 'GET',
|
||||||
|
}).then(function (detail) {
|
||||||
|
var activity =
|
||||||
|
detail && detail.activity
|
||||||
|
? detail.activity
|
||||||
|
: detail || {};
|
||||||
|
var activityId = String(
|
||||||
|
activity.activityId ||
|
||||||
|
activity.id ||
|
||||||
|
detail.activityId ||
|
||||||
|
detail.id ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
if (!activityId) return activity;
|
||||||
|
return request('/activity/game/king/aslan/me', {
|
||||||
|
method: 'GET',
|
||||||
|
query: { activityId: activityId },
|
||||||
|
}).then(function (me) {
|
||||||
|
// detail 的 prizes/rankRewards/activityStatus 位于外层,而
|
||||||
|
// activity 只携带基础配置;保留两个层级再叠加 me,避免页面
|
||||||
|
// 因合并顺序丢掉七个奖项或榜单奖励配置。
|
||||||
|
return Object.assign(
|
||||||
|
{},
|
||||||
|
detail || {},
|
||||||
|
activity,
|
||||||
|
me || {},
|
||||||
|
{
|
||||||
|
activityId: activityId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
draw: function (activityId, requestId) {
|
||||||
|
// requestId 由页面为“一次用户意图”稳定生成;网络超时后重试仍携带同一值,避免重复扣减抽奖次数。
|
||||||
|
return request('/activity/game/king/aslan/draw', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
activityId: String(activityId || ''),
|
||||||
|
requestId: String(requestId || ''),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ranking: function (activityId, rankingType, period, limit) {
|
||||||
|
return request('/activity/game/king/aslan/ranking', {
|
||||||
|
method: 'GET',
|
||||||
|
query: {
|
||||||
|
activityId: String(activityId || ''),
|
||||||
|
rankingType: String(rankingType || 'TYCOON'),
|
||||||
|
period: String(period || 'DAILY'),
|
||||||
|
limit: Number(limit) || 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
records: function (activityId, limit) {
|
||||||
|
return request('/activity/game/king/aslan/draw-records', {
|
||||||
|
method: 'GET',
|
||||||
|
query: {
|
||||||
|
activityId: String(activityId || ''),
|
||||||
|
limit: Number(limit) || 50,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
giftChallenge: {
|
||||||
|
current: function () {
|
||||||
|
return request('/activity/gift-challenge/aslan/detail', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enter: function (activityId) {
|
||||||
|
// 进入任务按“活动 + 活动时区自然日 + 用户”由服务端幂等;请求体只传
|
||||||
|
// 后端契约声明的 activityId,用户和统计日始终从网关上下文推导。
|
||||||
|
return request('/activity/gift-challenge/aslan/enter', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
activityId: String(activityId || ''),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ranking: function (activityId, period, date, limit) {
|
||||||
|
return request('/activity/gift-challenge/aslan/ranking', {
|
||||||
|
method: 'GET',
|
||||||
|
query: {
|
||||||
|
activityId: String(activityId || ''),
|
||||||
|
period: String(period || 'DAILY'),
|
||||||
|
statDate: date ? String(date) : '',
|
||||||
|
limit: Number(limit) || 30,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
claimTask: function (activityId, taskCode, requestId) {
|
||||||
|
// taskCode 进入 URL 前编码,避免后台配置值意外改变请求路径;用户 ID
|
||||||
|
// 从 App Bearer 登录态读取,页面从不接受或上送可伪造的 userId。
|
||||||
|
return request(
|
||||||
|
'/activity/gift-challenge/aslan/tasks/' +
|
||||||
|
encodeURIComponent(String(taskCode || '')) +
|
||||||
|
'/claim',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
activityId: String(activityId || ''),
|
||||||
|
requestId: String(requestId || ''),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -20,7 +20,9 @@
|
|||||||
"audit:huwaa-cp-layers": "node scripts/check-activity-cp-huwaa-layer-assets.mjs",
|
"audit:huwaa-cp-layers": "node scripts/check-activity-cp-huwaa-layer-assets.mjs",
|
||||||
"audit:fami-room-reward-layers": "node scripts/check-activity-room-reward-fami-layer-assets.mjs",
|
"audit:fami-room-reward-layers": "node scripts/check-activity-room-reward-fami-layer-assets.mjs",
|
||||||
"audit:recharge-reward-fami-layers": "node scripts/check-activity-recharge-reward-fami-layer-assets.mjs",
|
"audit:recharge-reward-fami-layers": "node scripts/check-activity-recharge-reward-fami-layer-assets.mjs",
|
||||||
"audit:coin-seller-fami-layers": "node scripts/check-coin-seller-fami-layer-assets.mjs"
|
"audit:coin-seller-fami-layers": "node scripts/check-coin-seller-fami-layer-assets.mjs",
|
||||||
|
"audit:game-king-aslan-layers": "node scripts/check-activity-game-king-aslan-assets.mjs",
|
||||||
|
"audit:gift-challenge-aslan-layers": "node scripts/check-activity-gift-challenge-aslan-assets.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"playwright-core": "^1.61.1",
|
"playwright-core": "^1.61.1",
|
||||||
|
|||||||
338
scripts/check-activity-game-king-aslan-assets.mjs
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDirectory, '..');
|
||||||
|
const pageDirectory = path.join(repoRoot, 'activity', 'game-king');
|
||||||
|
const aslanDirectory = path.join(pageDirectory, 'aslan');
|
||||||
|
const layerDirectory = path.join(aslanDirectory, 'assets', 'layers');
|
||||||
|
const manifestPath = path.join(layerDirectory, 'manifest.json');
|
||||||
|
|
||||||
|
function invariant(condition, message) {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(`[game-king-aslan-asset-audit] ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pngDimensions(bytes, filename) {
|
||||||
|
invariant(
|
||||||
|
bytes.length >= 24 && bytes.toString('ascii', 1, 4) === 'PNG',
|
||||||
|
`${filename}: expected PNG signature`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
width: bytes.readUInt32BE(16),
|
||||||
|
height: bytes.readUInt32BE(20),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function jpegDimensions(bytes, filename) {
|
||||||
|
invariant(
|
||||||
|
bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8,
|
||||||
|
`${filename}: expected JPEG signature`
|
||||||
|
);
|
||||||
|
for (let offset = 2; offset + 9 < bytes.length; ) {
|
||||||
|
if (bytes[offset] !== 0xff) {
|
||||||
|
offset += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const marker = bytes[offset + 1];
|
||||||
|
const length = bytes.readUInt16BE(offset + 2);
|
||||||
|
if (
|
||||||
|
marker >= 0xc0 &&
|
||||||
|
marker <= 0xc3 &&
|
||||||
|
length >= 7 &&
|
||||||
|
offset + length + 2 <= bytes.length
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
height: bytes.readUInt16BE(offset + 5),
|
||||||
|
width: bytes.readUInt16BE(offset + 7),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
invariant(length >= 2, `${filename}: invalid JPEG segment length`);
|
||||||
|
offset += length + 2;
|
||||||
|
}
|
||||||
|
throw new Error(`[game-king-aslan-asset-audit] ${filename}: no JPEG size`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bitmapDimensions(bytes, asset) {
|
||||||
|
if (asset.file.format === 'png')
|
||||||
|
return pngDimensions(bytes, asset.filename);
|
||||||
|
if (asset.file.format === 'jpeg')
|
||||||
|
return jpegDimensions(bytes, asset.filename);
|
||||||
|
throw new Error(
|
||||||
|
`[game-king-aslan-asset-audit] ${asset.filename}: unsupported format`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||||
|
invariant(manifest.schemaVersion === 1, 'unsupported manifest schema');
|
||||||
|
invariant(
|
||||||
|
manifest.figma?.fileKey === 'OTOg5cwbhVNWxkImGmzTJl',
|
||||||
|
'unexpected Figma file key'
|
||||||
|
);
|
||||||
|
const forbiddenNodes = new Set(manifest.policy?.forbiddenSourceNodes || []);
|
||||||
|
const approvedCompositeNodes = new Set(
|
||||||
|
manifest.policy?.approvedStandaloneCompositeNodes || []
|
||||||
|
);
|
||||||
|
const assets = manifest.assets || [];
|
||||||
|
invariant(assets.length > 0, 'manifest must list the exported layers');
|
||||||
|
|
||||||
|
const byFile = new Map();
|
||||||
|
for (const asset of assets) {
|
||||||
|
const isApprovedStaticComposite =
|
||||||
|
approvedCompositeNodes.has(asset.sourceNode) &&
|
||||||
|
/^approved-static-(header-composite|podium-frame)$/.test(
|
||||||
|
asset.kind || ''
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
typeof asset.filename === 'string' &&
|
||||||
|
path.basename(asset.filename) === asset.filename,
|
||||||
|
'layer filename must be a basename'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!byFile.has(asset.filename),
|
||||||
|
`${asset.filename}: duplicate asset`
|
||||||
|
);
|
||||||
|
byFile.set(asset.filename, asset);
|
||||||
|
invariant(
|
||||||
|
/^\d+:\d+$/.test(asset.sourceNode || ''),
|
||||||
|
`${asset.filename}: missing source node`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!forbiddenNodes.has(asset.sourceNode),
|
||||||
|
`${asset.filename}: root frame/canvas exports are forbidden`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
asset.renderedDesignSize?.width > 0 &&
|
||||||
|
asset.renderedDesignSize?.height > 0,
|
||||||
|
`${asset.filename}: design size is required`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
/^[a-f0-9]{64}$/.test(asset.sha256 || ''),
|
||||||
|
`${asset.filename}: SHA-256 is required`
|
||||||
|
);
|
||||||
|
for (const field of [
|
||||||
|
'containsText',
|
||||||
|
'containsControls',
|
||||||
|
'containsDynamicBusinessMedia',
|
||||||
|
'mockOnly',
|
||||||
|
]) {
|
||||||
|
invariant(
|
||||||
|
typeof asset[field] === 'boolean',
|
||||||
|
`${asset.filename}: ${field} must be boolean`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
isApprovedStaticComposite || !asset.containsText,
|
||||||
|
`${asset.filename}: text must be live DOM/i18n content`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!asset.containsControls,
|
||||||
|
`${asset.filename}: controls must be DOM elements`
|
||||||
|
);
|
||||||
|
if (asset.scope === 'runtime') {
|
||||||
|
invariant(
|
||||||
|
!asset.mockOnly,
|
||||||
|
`${asset.filename}: runtime layer cannot be mock-only`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!asset.containsDynamicBusinessMedia,
|
||||||
|
`${asset.filename}: runtime layer cannot contain dynamic business media`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
invariant(
|
||||||
|
asset.scope === 'fixture' &&
|
||||||
|
asset.mockOnly &&
|
||||||
|
asset.containsDynamicBusinessMedia,
|
||||||
|
`${asset.filename}: only an explicit mock fixture may contain business media`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetPath = path.join(layerDirectory, asset.filename);
|
||||||
|
const [bytes, info] = await Promise.all([
|
||||||
|
readFile(assetPath),
|
||||||
|
stat(assetPath),
|
||||||
|
]);
|
||||||
|
const dimensions = bitmapDimensions(bytes, asset);
|
||||||
|
invariant(
|
||||||
|
info.size === asset.file.bytes,
|
||||||
|
`${asset.filename}: byte size mismatch`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
dimensions.width === asset.file.pixelWidth &&
|
||||||
|
dimensions.height === asset.file.pixelHeight,
|
||||||
|
`${asset.filename}: pixel dimensions mismatch`
|
||||||
|
);
|
||||||
|
if (asset.naturalSize) {
|
||||||
|
invariant(
|
||||||
|
dimensions.width === asset.naturalSize.width &&
|
||||||
|
dimensions.height === asset.naturalSize.height,
|
||||||
|
`${asset.filename}: natural source dimensions mismatch`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
dimensions.width * dimensions.height <=
|
||||||
|
manifest.policy.maxDecorativeLayerPixels,
|
||||||
|
`${asset.filename}: bitmap is too large for an atomic layer`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
createHash('sha256').update(bytes).digest('hex') === asset.sha256,
|
||||||
|
`${asset.filename}: SHA-256 mismatch`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diskFiles = new Set(
|
||||||
|
(await readdir(layerDirectory)).filter(
|
||||||
|
(filename) => filename !== 'manifest.json'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
diskFiles.size === byFile.size &&
|
||||||
|
[...diskFiles].every((filename) => byFile.has(filename)),
|
||||||
|
'every layer file must have exactly one traceable manifest record'
|
||||||
|
);
|
||||||
|
|
||||||
|
const [html, css, js, common, ...locales] = await Promise.all([
|
||||||
|
readFile(path.join(pageDirectory, 'aslan.html'), 'utf8'),
|
||||||
|
readFile(path.join(aslanDirectory, 'aslan.css'), 'utf8'),
|
||||||
|
readFile(path.join(aslanDirectory, 'aslan.js'), 'utf8'),
|
||||||
|
readFile(path.join(repoRoot, 'common_aslan', 'aslan-common.js'), 'utf8'),
|
||||||
|
...['en', 'ar', 'tr', 'es'].map((language) =>
|
||||||
|
readFile(
|
||||||
|
path.join(repoRoot, 'common', 'locales', `${language}.json`),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
const runtimeSource = `${html}\n${css}\n${js}`;
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
invariant(
|
||||||
|
runtimeSource.includes(asset.filename),
|
||||||
|
`${asset.filename}: layer is not referenced`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const term of manifest.policy.forbiddenRuntimeTerms || []) {
|
||||||
|
invariant(
|
||||||
|
!runtimeSource.toLowerCase().includes(String(term).toLowerCase()),
|
||||||
|
`runtime source includes forbidden aggregate-image term: ${term}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const match of runtimeSource.matchAll(
|
||||||
|
/(?:\.\/assets\/layers\/)([^"')\s]+)/g
|
||||||
|
)) {
|
||||||
|
invariant(
|
||||||
|
byFile.has(match[1]),
|
||||||
|
`${match[1]}: untraced local bitmap reference`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
html.includes('../../common/theme.css') &&
|
||||||
|
html.includes('../../common/i18n.js'),
|
||||||
|
'shared theme and i18n runtime are required'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
html.includes('data-hy-app-code="aslan"'),
|
||||||
|
'Aslan app identity is required'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
/function isMockMode\(\)[\s\S]*query\('mock'\)/.test(js) &&
|
||||||
|
/hostname === 'localhost'/.test(js) &&
|
||||||
|
/hostname === '127\.0\.0\.1'/.test(js) &&
|
||||||
|
/env === 'local'/.test(js) &&
|
||||||
|
/env === 'test'/.test(js) &&
|
||||||
|
/function buildMockStore\(\)/.test(js),
|
||||||
|
'mock data must be query-gated and restricted to local/test environments'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
js.indexOf('var icon = PRIZE_FALLBACK;') >
|
||||||
|
js.indexOf('function buildMockStore()'),
|
||||||
|
'fixture prize art must be confined to the mock builder'
|
||||||
|
);
|
||||||
|
for (const endpoint of [
|
||||||
|
'/activity/game/king/aslan/detail',
|
||||||
|
'/activity/game/king/aslan/me',
|
||||||
|
'/activity/game/king/aslan/draw',
|
||||||
|
'/activity/game/king/aslan/ranking',
|
||||||
|
'/activity/game/king/aslan/draw-records',
|
||||||
|
]) {
|
||||||
|
invariant(
|
||||||
|
common.includes(endpoint),
|
||||||
|
`missing common_aslan endpoint: ${endpoint}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
common.includes("rankingType: String(rankingType || 'TYCOON')") &&
|
||||||
|
common.includes("period: String(period || 'DAILY')"),
|
||||||
|
'ranking contract must preserve TYCOON/VICTORIOUS and DAILY/OVERALL'
|
||||||
|
);
|
||||||
|
for (const dtoField of [
|
||||||
|
"'prizeImage'",
|
||||||
|
"'coinPerDraw'",
|
||||||
|
"'totalConsumed'",
|
||||||
|
"'entries'",
|
||||||
|
"'my'",
|
||||||
|
"'account'",
|
||||||
|
"'cover'",
|
||||||
|
"'sourceUrl'",
|
||||||
|
"'deliveryStatus'",
|
||||||
|
"'failureReason'",
|
||||||
|
]) {
|
||||||
|
invariant(
|
||||||
|
js.includes(dtoField),
|
||||||
|
`missing Game King backend DTO mapping: ${dtoField}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
/Object\.assign\(\s*\{\},\s*detail \|\| \{\},\s*activity,\s*me \|\| \{\}/.test(
|
||||||
|
common
|
||||||
|
),
|
||||||
|
'detail config must be preserved when current-user fields are merged'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
js.includes("status === 'FAILED'") &&
|
||||||
|
js.includes("status === 'PROCESSING'") &&
|
||||||
|
js.includes("status === 'UNKNOWN'") &&
|
||||||
|
js.includes('function deliveryMessage') &&
|
||||||
|
js.includes('source.record || source.drawRecord || source'),
|
||||||
|
'draw result and record UI must surface delivery success, processing and failure states'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
common.includes('function nonProductionBaseURL') &&
|
||||||
|
common.includes('trustedApiHosts') &&
|
||||||
|
common.includes('if (!env) return normalizeBaseURL(PROD_API_BASE)') &&
|
||||||
|
!common.includes("query('apiBase')") &&
|
||||||
|
!common.includes("query('api_base')") &&
|
||||||
|
!common.includes("query('aslan_api_base')") &&
|
||||||
|
common.includes("throw new Error('aslan_test_api_not_configured')"),
|
||||||
|
'production API must be fixed; test/dev needs an explicit trusted build-time host'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
js.includes("var SETTLEMENT_BOARD_TYPE = 'TYCOON'") &&
|
||||||
|
js.includes('rankingRewards[SETTLEMENT_BOARD_TYPE]'),
|
||||||
|
'Award entry must show the single Tycoon overall settlement board'
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseLocale = JSON.parse(locales[0]);
|
||||||
|
const gameKingKeys = Object.keys(baseLocale).filter((key) =>
|
||||||
|
key.startsWith('gameKing.')
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
gameKingKeys.length >= 40,
|
||||||
|
'English game-king i18n keys are incomplete'
|
||||||
|
);
|
||||||
|
for (const localeText of locales.slice(1)) {
|
||||||
|
const locale = JSON.parse(localeText);
|
||||||
|
for (const key of gameKingKeys) {
|
||||||
|
invariant(
|
||||||
|
typeof locale[key] === 'string' && locale[key],
|
||||||
|
`${key}: locale translation missing`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[game-king-aslan-asset-audit] passed: ${assets.length} atomic layers, 4 locales, common_aslan contract`
|
||||||
|
);
|
||||||
397
scripts/check-activity-gift-challenge-aslan-assets.mjs
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const auditLabel = 'gift-challenge-aslan-asset-audit';
|
||||||
|
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDirectory, '..');
|
||||||
|
const pageDirectory = path.join(repoRoot, 'activity', 'gift-challenge');
|
||||||
|
const aslanDirectory = path.join(pageDirectory, 'aslan');
|
||||||
|
const layerDirectory = path.join(aslanDirectory, 'assets', 'layers');
|
||||||
|
const manifestPath = path.join(layerDirectory, 'manifest.json');
|
||||||
|
const localeCodes = ['en', 'ar', 'tr', 'es'];
|
||||||
|
|
||||||
|
// Keep the permanent red-line list in code as well as the manifest. Otherwise a
|
||||||
|
// later manifest edit could silently remove a root frame from its own denylist.
|
||||||
|
const canonicalForbiddenNodes = new Set([
|
||||||
|
'646:1997',
|
||||||
|
'918:6917',
|
||||||
|
'918:7827',
|
||||||
|
'918:8941',
|
||||||
|
'918:9851',
|
||||||
|
'918:10385',
|
||||||
|
]);
|
||||||
|
const forbiddenAssetName =
|
||||||
|
/(?:whole[-_ ]?page|full[-_ ]?page|page[-_ ]?shot|page[-_ ]?screenshot|root[-_ ]?frame|screenshot)/i;
|
||||||
|
|
||||||
|
function invariant(condition, message) {
|
||||||
|
if (!condition) throw new Error(`[${auditLabel}] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pngDimensions(bytes, filename) {
|
||||||
|
invariant(
|
||||||
|
bytes.length >= 24 &&
|
||||||
|
bytes[0] === 0x89 &&
|
||||||
|
bytes.toString('ascii', 1, 4) === 'PNG',
|
||||||
|
`${filename}: expected PNG signature`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
width: bytes.readUInt32BE(16),
|
||||||
|
height: bytes.readUInt32BE(20),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function jpegDimensions(bytes, filename) {
|
||||||
|
invariant(
|
||||||
|
bytes.length >= 4 && bytes[0] === 0xff && bytes[1] === 0xd8,
|
||||||
|
`${filename}: expected JPEG signature`
|
||||||
|
);
|
||||||
|
const sizeMarkers = new Set([
|
||||||
|
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce,
|
||||||
|
0xcf,
|
||||||
|
]);
|
||||||
|
for (let offset = 2; offset + 8 < bytes.length; ) {
|
||||||
|
if (bytes[offset] !== 0xff) {
|
||||||
|
offset += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
while (offset < bytes.length && bytes[offset] === 0xff) offset += 1;
|
||||||
|
const marker = bytes[offset];
|
||||||
|
offset += 1;
|
||||||
|
if (
|
||||||
|
marker === 0xd8 ||
|
||||||
|
marker === 0xd9 ||
|
||||||
|
marker === 0x01 ||
|
||||||
|
(marker >= 0xd0 && marker <= 0xd7)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
invariant(offset + 2 <= bytes.length, `${filename}: truncated JPEG`);
|
||||||
|
const segmentLength = bytes.readUInt16BE(offset);
|
||||||
|
invariant(segmentLength >= 2, `${filename}: invalid JPEG segment`);
|
||||||
|
if (sizeMarkers.has(marker)) {
|
||||||
|
invariant(
|
||||||
|
segmentLength >= 7 && offset + segmentLength <= bytes.length,
|
||||||
|
`${filename}: invalid JPEG size segment`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
height: bytes.readUInt16BE(offset + 3),
|
||||||
|
width: bytes.readUInt16BE(offset + 5),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
offset += segmentLength;
|
||||||
|
}
|
||||||
|
throw new Error(`[${auditLabel}] ${filename}: JPEG dimensions not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function svgDimensions(bytes, asset) {
|
||||||
|
const source = bytes.toString('utf8');
|
||||||
|
invariant(/<svg\b/i.test(source), `${asset.filename}: expected SVG root`);
|
||||||
|
const match = source.match(/viewBox=["']([^"']+)["']/i);
|
||||||
|
invariant(match, `${asset.filename}: SVG viewBox is required`);
|
||||||
|
const values = match[1]
|
||||||
|
.trim()
|
||||||
|
.split(/[\s,]+/)
|
||||||
|
.map(Number);
|
||||||
|
invariant(
|
||||||
|
values.length === 4 && values.every(Number.isFinite),
|
||||||
|
`${asset.filename}: SVG viewBox must contain four finite numbers`
|
||||||
|
);
|
||||||
|
const expected = asset.exportViewBox;
|
||||||
|
invariant(
|
||||||
|
expected,
|
||||||
|
`${asset.filename}: manifest exportViewBox is required`
|
||||||
|
);
|
||||||
|
const expectedValues = [
|
||||||
|
expected.minX,
|
||||||
|
expected.minY,
|
||||||
|
expected.width,
|
||||||
|
expected.height,
|
||||||
|
].map(Number);
|
||||||
|
invariant(
|
||||||
|
expectedValues.every(Number.isFinite) &&
|
||||||
|
values.every(
|
||||||
|
(value, index) =>
|
||||||
|
Math.abs(value - expectedValues[index]) <= 0.001
|
||||||
|
),
|
||||||
|
`${asset.filename}: SVG viewBox does not match the manifest`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
values[2] > 0 && values[3] > 0,
|
||||||
|
`${asset.filename}: SVG viewBox dimensions must be positive`
|
||||||
|
);
|
||||||
|
return { width: values[2], height: values[3] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileDimensions(bytes, asset) {
|
||||||
|
if (asset.file.format === 'png') {
|
||||||
|
return pngDimensions(bytes, asset.filename);
|
||||||
|
}
|
||||||
|
if (asset.file.format === 'jpeg') {
|
||||||
|
return jpegDimensions(bytes, asset.filename);
|
||||||
|
}
|
||||||
|
if (asset.file.format === 'svg') {
|
||||||
|
return svgDimensions(bytes, asset);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`[${auditLabel}] ${asset.filename}: unsupported file format`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||||
|
invariant(manifest.schemaVersion === 1, 'unsupported manifest schema');
|
||||||
|
invariant(
|
||||||
|
manifest.figma?.fileKey === 'OTOg5cwbhVNWxkImGmzTJl',
|
||||||
|
'unexpected Figma file key'
|
||||||
|
);
|
||||||
|
const manifestForbiddenNodes = new Set(
|
||||||
|
manifest.policy?.forbiddenSourceNodes || []
|
||||||
|
);
|
||||||
|
for (const node of canonicalForbiddenNodes) {
|
||||||
|
invariant(
|
||||||
|
manifestForbiddenNodes.has(node),
|
||||||
|
`${node}: permanent forbidden node is missing from manifest policy`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const maxLayerPixels = Number(manifest.policy?.maxDecorativeLayerPixels);
|
||||||
|
invariant(
|
||||||
|
Number.isFinite(maxLayerPixels) && maxLayerPixels > 0,
|
||||||
|
'maxDecorativeLayerPixels must be a positive number'
|
||||||
|
);
|
||||||
|
|
||||||
|
const assets = manifest.assets || [];
|
||||||
|
invariant(assets.length > 0, 'manifest must list exported layers');
|
||||||
|
const assetsByFile = new Map();
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
// basename-only paths prevent a manifest entry from escaping the audited
|
||||||
|
// directory while still appearing to represent a local layer.
|
||||||
|
invariant(
|
||||||
|
typeof asset.filename === 'string' &&
|
||||||
|
path.basename(asset.filename) === asset.filename,
|
||||||
|
'every layer filename must be a basename'
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!assetsByFile.has(asset.filename),
|
||||||
|
`${asset.filename}: duplicate manifest entry`
|
||||||
|
);
|
||||||
|
assetsByFile.set(asset.filename, asset);
|
||||||
|
invariant(
|
||||||
|
!forbiddenAssetName.test(asset.filename),
|
||||||
|
`${asset.filename}: filename indicates a forbidden whole-page asset`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
/^\d+:\d+$/.test(asset.sourceNode || ''),
|
||||||
|
`${asset.filename}: source Figma node is required`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!canonicalForbiddenNodes.has(asset.sourceNode) &&
|
||||||
|
!manifestForbiddenNodes.has(asset.sourceNode),
|
||||||
|
`${asset.filename}: root canvas/frame exports are forbidden`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
typeof asset.use === 'string' && asset.use.trim(),
|
||||||
|
`${asset.filename}: usage description is required`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
asset.scope === 'runtime',
|
||||||
|
`${asset.filename}: Gift Challenge layers must be runtime decoration`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
typeof asset.derived === 'boolean',
|
||||||
|
`${asset.filename}: derived must be explicit`
|
||||||
|
);
|
||||||
|
if (asset.derived) {
|
||||||
|
invariant(
|
||||||
|
typeof asset.derivation === 'string' && asset.derivation.trim(),
|
||||||
|
`${asset.filename}: derived assets must document their derivation`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
Number(asset.renderedDesignSize?.width) > 0 &&
|
||||||
|
Number(asset.renderedDesignSize?.height) > 0,
|
||||||
|
`${asset.filename}: positive Figma design size is required`
|
||||||
|
);
|
||||||
|
for (const field of [
|
||||||
|
'containsText',
|
||||||
|
'containsControls',
|
||||||
|
'containsDynamicBusinessMedia',
|
||||||
|
]) {
|
||||||
|
invariant(
|
||||||
|
typeof asset[field] === 'boolean',
|
||||||
|
`${asset.filename}: ${field} must be boolean`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
asset[field] === false,
|
||||||
|
`${asset.filename}: text, controls and dynamic business media must remain DOM/API content`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
/^[a-f0-9]{64}$/.test(asset.sha256 || ''),
|
||||||
|
`${asset.filename}: SHA-256 is required`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
Number.isInteger(asset.file?.bytes) && asset.file.bytes > 0,
|
||||||
|
`${asset.filename}: byte size must be a positive integer`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
Number(asset.file?.pixelWidth) > 0 &&
|
||||||
|
Number(asset.file?.pixelHeight) > 0,
|
||||||
|
`${asset.filename}: positive exported dimensions are required`
|
||||||
|
);
|
||||||
|
const extension = path.extname(asset.filename).toLowerCase();
|
||||||
|
invariant(
|
||||||
|
(asset.file.format === 'png' && extension === '.png') ||
|
||||||
|
(asset.file.format === 'jpeg' &&
|
||||||
|
['.jpg', '.jpeg'].includes(extension)) ||
|
||||||
|
(asset.file.format === 'svg' && extension === '.svg'),
|
||||||
|
`${asset.filename}: extension does not match manifest format`
|
||||||
|
);
|
||||||
|
|
||||||
|
const assetPath = path.join(layerDirectory, asset.filename);
|
||||||
|
const [bytes, fileInfo] = await Promise.all([
|
||||||
|
readFile(assetPath),
|
||||||
|
stat(assetPath),
|
||||||
|
]);
|
||||||
|
const dimensions = fileDimensions(bytes, asset);
|
||||||
|
invariant(
|
||||||
|
fileInfo.isFile(),
|
||||||
|
`${asset.filename}: manifest entry must point to a file`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
fileInfo.size === asset.file.bytes,
|
||||||
|
`${asset.filename}: byte size mismatch`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
Math.abs(dimensions.width - Number(asset.file.pixelWidth)) <= 0.001 &&
|
||||||
|
Math.abs(dimensions.height - Number(asset.file.pixelHeight)) <=
|
||||||
|
0.001,
|
||||||
|
`${asset.filename}: exported dimensions mismatch`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
dimensions.width * dimensions.height <= maxLayerPixels,
|
||||||
|
`${asset.filename}: layer exceeds the decorative pixel limit`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
createHash('sha256').update(bytes).digest('hex') === asset.sha256,
|
||||||
|
`${asset.filename}: SHA-256 mismatch`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const diskFiles = new Set(
|
||||||
|
(await readdir(layerDirectory)).filter(
|
||||||
|
(filename) => filename !== 'manifest.json'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
diskFiles.size === assetsByFile.size &&
|
||||||
|
[...diskFiles].every((filename) => assetsByFile.has(filename)),
|
||||||
|
'every layer file must have exactly one manifest record'
|
||||||
|
);
|
||||||
|
|
||||||
|
const [html, css, js, ...localeTexts] = await Promise.all([
|
||||||
|
readFile(path.join(pageDirectory, 'aslan.html'), 'utf8'),
|
||||||
|
readFile(path.join(aslanDirectory, 'aslan.css'), 'utf8'),
|
||||||
|
readFile(path.join(aslanDirectory, 'aslan.js'), 'utf8'),
|
||||||
|
...localeCodes.map((locale) =>
|
||||||
|
readFile(
|
||||||
|
path.join(repoRoot, 'common', 'locales', `${locale}.json`),
|
||||||
|
'utf8'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
const runtimeSource = `${html}\n${css}\n${js}`;
|
||||||
|
|
||||||
|
// Every runtime image reference must resolve back to this manifest. This checks
|
||||||
|
// the actual page path, not merely whether an approved collection exists on disk.
|
||||||
|
const runtimeLayerReferences = [
|
||||||
|
...runtimeSource.matchAll(
|
||||||
|
/(?:\.\/)?(?:aslan\/)?assets\/layers\/([^"')\s?#]+)/g
|
||||||
|
),
|
||||||
|
].map((match) => match[1]);
|
||||||
|
invariant(
|
||||||
|
runtimeLayerReferences.length > 0,
|
||||||
|
'page must reference audited layers'
|
||||||
|
);
|
||||||
|
for (const filename of runtimeLayerReferences) {
|
||||||
|
invariant(
|
||||||
|
assetsByFile.has(filename),
|
||||||
|
`${filename}: runtime layer reference is not present in the manifest`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!forbiddenAssetName.test(filename),
|
||||||
|
`${filename}: aslan.html/runtime references a forbidden whole-page asset`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const asset of assets) {
|
||||||
|
invariant(
|
||||||
|
runtimeLayerReferences.includes(asset.filename),
|
||||||
|
`${asset.filename}: manifested layer is not used by the page`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlImageSources = [
|
||||||
|
...html.matchAll(/<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi),
|
||||||
|
].map((match) => match[1]);
|
||||||
|
for (const source of htmlImageSources) {
|
||||||
|
invariant(
|
||||||
|
!forbiddenAssetName.test(source),
|
||||||
|
`${source}: aslan.html references a forbidden whole-page image`
|
||||||
|
);
|
||||||
|
const layerMatch = source.match(/assets\/layers\/([^"')\s?#]+)/);
|
||||||
|
invariant(
|
||||||
|
layerMatch && assetsByFile.has(layerMatch[1]),
|
||||||
|
`${source}: every aslan.html image must be an audited layer`
|
||||||
|
);
|
||||||
|
invariant(
|
||||||
|
!canonicalForbiddenNodes.has(
|
||||||
|
assetsByFile.get(layerMatch[1]).sourceNode
|
||||||
|
),
|
||||||
|
`${source}: aslan.html image resolves to a forbidden root frame`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
html.includes('../../common/theme.css') &&
|
||||||
|
html.includes('../../common/i18n.js'),
|
||||||
|
'shared theme and i18n runtime are required'
|
||||||
|
);
|
||||||
|
|
||||||
|
const locales = localeTexts.map((source) => JSON.parse(source));
|
||||||
|
const baseKeys = Object.keys(locales[0])
|
||||||
|
.filter((key) => key.startsWith('giftChallenge.'))
|
||||||
|
.sort();
|
||||||
|
invariant(baseKeys.length >= 40, 'English Gift Challenge locale is incomplete');
|
||||||
|
const baseKeySet = new Set(baseKeys);
|
||||||
|
for (let index = 0; index < locales.length; index += 1) {
|
||||||
|
const localeCode = localeCodes[index];
|
||||||
|
const locale = locales[index];
|
||||||
|
const keys = Object.keys(locale)
|
||||||
|
.filter((key) => key.startsWith('giftChallenge.'))
|
||||||
|
.sort();
|
||||||
|
const missing = baseKeys.filter((key) => !(key in locale));
|
||||||
|
const extra = keys.filter((key) => !baseKeySet.has(key));
|
||||||
|
invariant(
|
||||||
|
missing.length === 0 && extra.length === 0,
|
||||||
|
`${localeCode}: Gift Challenge key set differs (missing: ${missing.join(', ') || 'none'}; extra: ${extra.join(', ') || 'none'})`
|
||||||
|
);
|
||||||
|
for (const key of keys) {
|
||||||
|
invariant(
|
||||||
|
typeof locale[key] === 'string' && locale[key].trim(),
|
||||||
|
`${localeCode}:${key}: translation must be a non-empty string`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeI18nKeys = new Set(
|
||||||
|
[...runtimeSource.matchAll(/giftChallenge\.[A-Za-z0-9_.-]+/g)].map(
|
||||||
|
(match) => match[0]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for (const key of runtimeI18nKeys) {
|
||||||
|
invariant(baseKeySet.has(key), `${key}: runtime i18n key is missing`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[${auditLabel}] passed: ${assets.length} traceable layers, ${baseKeys.length} aligned keys across ${localeCodes.length} locales`
|
||||||
|
);
|
||||||