Compare commits

..

23 Commits

Author SHA1 Message Date
richarjiang
3d246de24c feat: 完善关卡创作页面 2026-04-30 16:35:08 +08:00
richarjiang
f8198e0463 perf: 还原挑战页面 2026-04-30 15:22:31 +08:00
richarjiang
dd883f14b0 perf: 完善首页 icon 2026-04-30 12:10:41 +08:00
richarjiang
16efb1bb25 perf: 首页支持扣减体力动画 2026-04-27 10:11:36 +08:00
richarjiang
fbea31b9ea feat: 支持首页 UI 2026-04-27 09:29:50 +08:00
richarjiang
950572ad74 perf: 提交遗漏代码 2026-04-26 17:19:08 +08:00
richarjiang
9d60fa601d perf: 优化 UI 2026-04-26 17:18:25 +08:00
richarjiang
1e5017e28e feat: 对接最新的关卡工作流 2026-04-26 17:04:47 +08:00
richarjiang
5074706115 feat: 接入通关弹窗 2026-04-26 16:20:37 +08:00
richarjiang
f5732b46a5 perf: 优化关卡内 UI 2026-04-26 14:47:38 +08:00
richarjiang
ecc82ae9a7 feat: 完善新版 UI 2026-04-24 21:44:22 +08:00
richarjiang
4f93725779 feat: 支持动态创建输入框 2026-04-24 20:18:36 +08:00
richarjiang
8d54ffdbf8 perf: 优化倒计时格式 2026-04-24 08:46:58 +08:00
richarjiang
4d699c127f perf: 进一步完善首页 2026-04-24 08:41:58 +08:00
richarjiang
457dd07d80 feat: 还原关卡页面 2026-04-23 22:57:06 +08:00
richarjiang
5eef9d8528 feat: 支持新版的关卡页面 2026-04-19 14:19:13 +08:00
richarjiang
34e06480ce perf: 删除不必要的文件 2026-04-19 12:00:19 +08:00
richarjiang
5d472e1c30 feat: 优化关卡内 UI 2026-04-14 09:53:08 +08:00
richarjiang
ddf51919b0 feat: 接入我创建的挑战列表接口 2026-04-13 09:51:38 +08:00
richarjiang
2f74f260b7 feat: 优化关卡;支持加时 2026-04-10 23:08:28 +08:00
richarjiang
9cf499a5e1 fix: 修复关卡排序 2026-04-10 11:00:43 +08:00
richarjiang
69c0986996 feat: 进入关卡时 toast 提示体力消耗,修复 StorageManager 接口位置和 WxSDK 访问级别
- 进入关卡成功后显示 toast 提示消耗体力及剩余体力
- 将 StorageManager 中 UserInfo 接口移至模块顶层,修复嵌套接口语法问题
- WxSDK.getWx() 改为 static 公开方法,便于外部调用
2026-04-10 10:10:19 +08:00
richarjiang
447e7a944a feat: 支持分享关卡通关上报 2026-04-08 21:34:36 +08:00
177 changed files with 27319 additions and 8441 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

102
AGENTS.md Normal file
View File

@@ -0,0 +1,102 @@
# Repository Guidelines
## 项目结构与模块组织
本仓库是 `Cocos Creator 3.8.8` 小游戏项目。主入口在 `assets/main.ts`,主场景是 `assets/main.scene`。页面与弹窗组件集中在 `assets/prefabs/`,命名通常为 `PageXxx.ts``PassModal.ts`。公共逻辑位于 `assets/scripts/``core/` 放页面基类与视图管理,`utils/` 放 SDK、鉴权、存储、网络等工具`config/` 放接口配置,`types/` 放类型定义。静态资源在 `assets/resources/`,编辑器配置在 `settings/v2/packages/`,设计说明在 `docs/` 与根目录分析文档中。
## 构建、调试与开发命令
先运行 `npm install`,同步 `minigame-api-typings` 依赖。日常开发主要通过 Cocos Creator 编辑器完成:使用 3.8.8 打开仓库,点击 `Play` 预览,使用 `Project > Build``Cmd+B` 构建小游戏包。若编辑器已生成 `temp/tsconfig.cocos.json`,可执行 `npx tsc --noEmit` 做一次 TypeScript 静态检查。
## 代码风格与命名约定
项目使用 TypeScript当前代码统一为 4 空格缩进。组件类、页面类、管理器类使用 `PascalCase`,实例属性与私有方法使用 `camelCase` / `_camelCase`,管理器统一使用 `XxxManager` 后缀。新增页面、预制体、脚本请保持同名,例如 `PageLevel.prefab` 对应 `PageLevel.ts`。优先把复用逻辑放入 `assets/scripts/utils/``assets/scripts/core/`不要把业务代码散落到场景脚本中。Cocos 资源的 `.meta` 文件必须一并提交。
## 测试与验证
仓库当前未配置 Jest、Vitest 一类自动化测试。提交前至少完成三项验证1. 编辑器预览主流程可进入页面2. 目标平台构建成功3. 涉及微信能力时,在真机或开发者工具验证登录、分享、隐私授权等流程。若修改接口或体力/关卡逻辑,补充手动验证步骤到 PR 描述。
## 提交与 Pull Request 规范
Git 历史采用 Conventional Commits且摘要多为中文例如 `feat: 支持分享关卡通关上报``fix: 修复关卡排序``docs: 添加设计文档`。请继续使用 `feat:``fix:``perf:``docs:` 前缀首行聚焦单一变更。PR 需说明改动范围、影响页面或模块、验证方式;涉及 UI 请附截图或录屏,涉及微信环境差异请写明复现条件与平台。
<claude-mem-context>
# Memory Context
# [mp-xieyingeng] recent context, 2026-04-29 6:32pm GMT+8
Legend: 🎯session 🔴bugfix 🟣feature 🔄refactor ✅change 🔵discovery ⚖decision 🚨security_alert 🔐security_note
Format: ID TIME TYPE TITLE
Fetch details: get_observations([IDs]) | Search: mem-search skill
Stats: 50 obs (9,755t read) | 1,857,754t work | 99% savings
### Apr 26, 2026
1330 4:45p 🔄 API Protocol Upgrade - NextLevel-Driven Architecture
1331 " ✅ ApiTypes.ts type definitions updated
1332 " ✅ Task 4 completed: ApiTypes.ts type definitions updated
1333 4:46p ✅ Tasks 4 and 5 completed: type definitions migrated
1334 5:10p 🔵 进度条小百分比时圆角畸形问题
1335 " 🔵 进度条圆角畸形问题调查中
1336 " 🔵 通关弹窗进度条使用 Cocos ProgressBar 组件
1337 5:11p 🔵 进度条圆角畸形调查中 - Prefab 结构检查
1338 " 🔵 ProgressBar 系统架构已定位
1340 " 🔵 找到进度条圆角畸形根本原因
1341 5:12p 🔵 进度条小百分比时圆角变形问题定位
1339 " 🔵 发现 RoundedRectMask 工具组件
S1309 移除 PageLevel.ts 进入关卡时弹出体力扣减 toast (Apr 26 at 5:16 PM)
1342 5:16p 🔵 PassModal 进度值规范化逻辑
1343 5:17p 🔴 进度条圆角畸形问题已修复
1344 " ⚖️ 探索使用 Mask 裁剪替代最小进度值方案
1345 " ⚖️ 否决 Mask 方案,确认最小进度值方案最优
### Apr 27, 2026
1346 9:21a 🔴 移除进入游戏后的体力扣减 toast 提示
1347 " 🔴 移除进入游戏后的体力扣减 toast 提示
1348 " 🟣 首页添加体力值显示功能
S1308 移除进入游戏关卡时弹出体力扣减的 toast 提示 (Apr 27 at 9:21 AM)
S1310 移除进入游戏体力扣减 toast 并在首页添加体力显示 (Apr 27 at 9:22 AM)
S1311 移除体力扣减 toast 并完善首页体验 (Apr 27 at 9:23 AM)
1349 9:25a ✅ PageHome.ts 添加 ToastManager 导入
1350 " ✅ PK按钮点击改为提示"功能开发中"
S1313 Implement object-fit: cover image scaling in Cocos Creator to prevent downloaded images from being distorted (Apr 27 at 9:26 AM)
1351 9:32a 🟣 Stamina animation system for level entry
1352 " 🔵 Cocos Creator game project structure discovered
1354 " 🟣 StarGame 按钮点击动画需求
1356 " 🔵 体力系统架构发现
1353 9:33a 🔵 StaminaManager API confirmed for stamina operations
1355 9:37a 🔵 项目结构和现有代码发现
1357 9:38a 🔵 EnterLevelData 结构分析
1359 9:39a 🟣 Stamina animation system implemented for game entry flow
1358 9:41a ⚖️ 体力值飞行动画实现计划制定完成
1362 9:45a 🟣 Image Cover Mode Scaling in Cocos Creator
S1312 Fix image distortion in PageLevel.ts by implementing object-fit: cover behavior for downloaded images (Apr 27 at 9:45 AM)
1360 " ✅ Cocos Creator animation imports added to PageHome.ts
1361 " ✅ Animation constants added to PageHome class
1363 " ✅ Start game button integrated with stamina check and animation flow
1364 " 🟣 Stamina consumption animation fully implemented in PageHome
S1314 Implement stamina consumption animation in Cocos Creator game - when clicking StarGame button, IconLive node flies to button with floating "-1" text, then navigates to level (Apr 27 at 9:45 AM)
1365 9:46a 🔴 Animation completes but navigation to PageLevel fails
S1315 为 PageLevel.ts 的 mainImage 和 mainImage2 节点实现 CSS cover 模式图片缩放 (Apr 27 at 9:46 AM)
1366 9:50a 🔴 Cocos Creator tween chain broken by premature node destroy
1367 9:55a 🔴 Navigation still failing after first tween chain fix - further debugging needed
1368 " 🔴 Decoupled fly animation from float text timing using setTimeout
1369 9:56a 🔴 Cocos Creator tween chain broken by any node state change mid-animation
1370 9:59a 🟣 CSS cover-style image scaling for PageLevel images
1371 10:01a ✅ PageLevel.ts imports extended for cover-style image scaling
1372 " 🟣 CSS cover-mode image scaling implemented for PageLevel images
S1316 Debug cover mode image overflow in PageLevel.ts - image exceeds container bounds despite Mask applied (Apr 27 at 10:02 AM)
1373 10:02a 🔴 Cover mode image overflows container bounds
1374 10:06a 🔵 GRAPHICS_RECT vs GRAPHICS_STENCIL for Mask in Cocos Creator
1375 10:08a 🔴 Mask.Type.RECT does not exist in Cocos Creator API
1376 " 🔵 MaskType enum found in Cocos Creator 3.8 engine declarations
1378 " 🔵 MaskType enum values confirmed - GRAPHICS_RECT exists
1377 10:09a 🔵 MaskType enum definition found at line 46015
1379 10:10a 🔴 Reverted Mask.Type to GRAPHICS_RECT
S1317 Implement CSS cover-mode image scaling for PageLevel.ts mainImage nodes (Apr 27 at 10:10 AM)
**Investigated**: Cover mode implementation in _applyCoverSprite() using Mask + scaled child node approach; Cocos Creator 3.8 MaskType enum investigation; root cause analysis of image overflow
**Learned**: Cocos Creator Mask only clips child nodes, NOT the container's own Sprite component; GRAPHICS_RECT = 0 is valid enum value in Cocos 3.8; container Sprite renders outside Mask bounds; solution: disable containerSprite and use child node for image display
**Completed**: _applyCoverSprite() method implemented with: Mask(GRAPHICS_RECT) on container, child node '_coverImg' with Sprite.SizeMode.RAW, cover scale calculation, containerSprite.enabled = false to prevent overflow
**Next Steps**: User to test in Cocos Creator editor to verify image clipping now works correctly
Access 1858k tokens of past work via get_observations([IDs]) or mem-search skill.
</claude-mem-context>

View File

@@ -1,496 +0,0 @@
# Architecture Overview
## System Architecture Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ COCOS CREATOR GAME │
│ WeChat Mini-Game Version 3.8.8 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER (UI) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ PageLoading.ts ──────┐ │
│ (Loading Screen) │ │
│ │ │
│ ┌──────────┐ │ ┌──────────┐ ┌──────────┐ │
│ │PageHome │─────────┴──────│PageLevel │──────│PassModal │ │
│ │(Menu) │ │ (Play) │ │(Victory) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ▲ ▲ │ │
│ │ │ │ │
│ └────────────────┬───────────┘ │ │
│ │ (Back button) │ │
│ └─────────────────────────────┘ │
│ │
│ Toast.ts ────────────────────────────────────────────────────────────│
│ (Notifications) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ CORE LOGIC LAYER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ PageLevel.ts │ │ PassModal.ts │ │
│ ├──────────────────────┤ ├──────────────────────┤ │
│ │ • Answer validation │ │ • Next level │ │
│ │ • Hint unlock (-life)│ │ • Share button │ │
│ │ • Countdown (60s) │ │ • Callbacks │ │
│ │ • Sound/Vibration │ │ │ │
│ │ • Life display │ │ │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ └────────────────┬───────────────┘ │
│ │ │
│ ┌──────────────────────────┴──────────────────────────────────┐ │
│ │ ViewManager.ts (View Navigation) │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ • Page registration & caching │ │
│ │ • Page stack management (push/pop/replace) │ │
│ │ • Lifecycle: onViewLoad → onViewShow → onViewHide → destroy │ │
│ │ • Parameter passing between pages │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ BaseView.ts (Abstract base class for all pages) │
│ • Lifecycle hooks │
│ • Page state management │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ DATA & STATE MANAGEMENT LAYER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ StorageManager.ts (Single Source of Truth) │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ Lives Management: │ │
│ │ • getLives() / setLives() / consumeLife() / addLife() │ │
│ │ │ │
│ │ Progress Management: │ │
│ │ • getCurrentLevelIndex() / getMaxUnlockedLevelIndex() │ │
│ │ • onLevelCompleted(levelIndex) │ │
│ │ • isLevelUnlocked(levelIndex) │ │
│ │ │ │
│ │ Storage Backend: sys.localStorage │ │
│ │ └─ game_lives: string number (default: "10") │ │
│ │ └─ game_progress: JSON with currentIndex & maxUnlockedIdx │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ LevelDataManager.ts (Level Data & Assets) │ │
│ ├──────────────────────────────────────────────────────────────┤ │
│ │ • initialize(onProgress) - Fetch API & load first level │ │
│ │ • getLevelConfig(index) - Get level data + image │ │
│ │ • ensureLevelReady(index) - On-demand level loading │ │
│ │ • preloadNextLevel(currentIndex) - Async preload │ │
│ │ │ │
│ │ Memory Caches: │ │
│ │ • _apiData: All levels from server │ │
│ │ • _levelConfigs: Map of loaded level configs │ │
│ │ • _imageCache: Map of loaded images (SpriteFrames) │ │
│ │ • _loadingLevels: Set of levels being loaded │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ UTILITY & SDK LAYER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────┐ ┌────────────────────┐ ┌──────────────────┐ │
│ │ HttpUtil.ts │ │ WxSDK.ts │ │ToastManager.ts │ │
│ ├────────────────────┤ ├────────────────────┤ ├──────────────────┤ │
│ │ • get<T>() │ │ • isWechat() │ │ • init() │ │
│ │ • post<T>() │ │ • initShare() │ │ • show() │ │
│ │ │ │ • shareAppMessage()│ │ │ │
│ │ Timeout: 10s │ │ • onShareAppMsg() │ │ Display duration │ │
│ │ Error handling │ │ • vibrateShort() │ │ Fade out anim │ │
│ │ │ │ • vibrateLong() │ │ │ │
│ └────────────────────┘ └────────────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ EXTERNAL SYSTEMS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────┐ │
│ │ WeChat Mini-Game SDK (wx global) │ │
│ ├──────────────────────────────────────┤ │
│ │ • wx.shareAppMessage() │ │
│ │ • wx.onShareAppMessage() │ │
│ │ • wx.showShareMenu() │ │
│ │ • wx.vibrateShort() │ │
│ │ • wx.vibrateLong() │ │
│ └──────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ │ │
│ ┌──────────────────────────────────────┐ │
│ │ Backend API Server │ │
│ │ https://ilookai.cn │ │
│ ├──────────────────────────────────────┤ │
│ │ GET /api/v1/wechat-game/levels │ │
│ │ • Returns: {success, data, message} │ │
│ │ • Retry: 2x with 1s delay │ │
│ │ • Timeout: 8s │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Data Flow Diagram: Complete User Session
```
START
├─ [main.ts] onLoad()
│ └─ ViewManager.init(canvas)
│ └─ ToastManager.init(toastPrefab)
│ └─ Register PageHome & PageLevel
├─ [PageLoading.ts] start()
│ ├─ LevelDataManager.initialize()
│ │ ├─ HttpUtil.get() → API call
│ │ │ └─ https://ilookai.cn/api/v1/wechat-game/levels
│ │ ├─ Cache response → _apiData
│ │ ├─ Load first level image
│ │ │ └─ assetManager.loadRemote() → SpriteFrame
│ │ │ └─ Cache in _imageCache
│ │ └─ Return success
│ │
│ └─ ViewManager.preload('PageHome')
│ └─ ViewManager.open('PageHome')
│ └─ PageHome.onViewLoad()
│ └─ WxSDK.initShare()
├─ [USER CLICKS "START GAME"]
├─ ViewManager.open('PageLevel', {params: {levelIndex: 0}})
│ └─ PageLevel.onViewLoad()
│ ├─ StorageManager.getCurrentLevelIndex()
│ ├─ PageLevel.initLevel()
│ │ ├─ LevelDataManager.ensureLevelReady(levelIndex)
│ │ │ ├─ Check cache (_levelConfigs)
│ │ │ ├─ If not cached: load image + create config
│ │ │ └─ Store in cache
│ │ │
│ │ └─ PageLevel._applyLevelConfig()
│ │ ├─ Display main image (sprite)
│ │ ├─ Show Hint 1 (free)
│ │ ├─ Hide Hints 2, 3 (show unlock buttons)
│ │ ├─ Create input field based on answer.length
│ │ ├─ Display lives: StorageManager.getLives()
│ │ ├─ Start countdown (60s)
│ │ └─ LevelDataManager.preloadNextLevel() [async]
├─ [USER INTERACTS]
├─ Option A: UNLOCK HINT
│ ├─ PageLevel.onUnlockClue(2 or 3)
│ ├─ Check: StorageManager.hasLives()?
│ ├─ Yes → StorageManager.consumeLife() → display hint
│ └─ No → Show "生命值不足" message
├─ Option B: SUBMIT ANSWER
│ ├─ PageLevel.onSubmitAnswer()
│ ├─ Validate: userAnswer === correctAnswer?
│ │
│ ├─ YES (Correct)
│ │ ├─ PageLevel.showSuccess()
│ │ ├─ Stop countdown
│ │ ├─ Play successAudio
│ │ ├─ StorageManager.addLife() [+1]
│ │ ├─ PageLevel._showPassModal()
│ │ │ ├─ Instantiate PassModal prefab
│ │ │ ├─ PassModal.onViewLoad()
│ │ │ ├─ PassModal.onViewShow()
│ │ │ │ └─ Play success sound
│ │ │ │ └─ Adjust widget to full screen
│ │ │ │
│ │ │ └─ User clicks button:
│ │ │ ├─ "Next Level" → PageLevel.nextLevel()
│ │ │ │ ├─ StorageManager.onLevelCompleted(currentIndex)
│ │ │ │ │ ├─ currentLevelIndex++
│ │ │ │ │ ├─ maxUnlockedLevelIndex = max(prev, current)
│ │ │ │ │ └─ Save to localStorage
│ │ │ │ ├─ Reload PageLevel with new index
│ │ │ │ │ └─ Repeat from Level Setup
│ │ │ │ │
│ │ │ │ └─ If last level:
│ │ │ │ └─ ViewManager.back() → return to PageHome
│ │ │ │
│ │ │ └─ "Share" → WxSDK.shareAppMessage()
│ │ │ └─ query: `level=${currentIndex+1}`
│ │
│ ├─ NO (Wrong)
│ │ ├─ PageLevel.showError()
│ │ ├─ Play failAudio
│ │ ├─ WxSDK.vibrateLong() [device vibration]
│ │ ├─ ToastManager.show("答案错误,再试试吧!")
│ │ │ ├─ Create Toast node
│ │ │ ├─ Show for 2000ms
│ │ │ ├─ Fade out animation (300ms)
│ │ │ └─ Destroy node
│ │ │
│ │ └─ Continue playing (allow retry)
├─ Option C: TIMEOUT
│ └─ Countdown reaches 0
│ ├─ PageLevel.onTimeUp()
│ ├─ Play failAudio
│ └─ [Incomplete: can still submit after timeout]
├─ [USER CLICKS BACK BUTTON]
│ ├─ ViewManager.back()
│ │ ├─ PageLevel._doHide()
│ │ └─ PageHome._doShow()
│ │
│ └─ Return to home (progress saved in localStorage)
└─ END
```
---
## State Management Flow
```
USER PROGRESS STATE
├─ localStorage: game_progress
│ └─ {
│ "currentLevelIndex": 5,
│ "maxUnlockedLevelIndex": 5
│ }
├─ Memory Cache (LevelDataManager)
│ ├─ _apiData: ApiLevelData[] (all levels)
│ ├─ _levelConfigs: Map<levelIndex, RuntimeLevelConfig>
│ ├─ _imageCache: Map<url, SpriteFrame>
│ └─ _loadingLevels: Set<levelIndex> (currently loading)
└─ Session State (PageLevel component)
├─ currentLevelIndex: number
├─ _currentConfig: RuntimeLevelConfig
├─ _countdown: number (60 → 0)
├─ _isTimeUp: boolean
├─ _isTransitioning: boolean
└─ _passModalNode: Node | null
LIVES STATE
├─ localStorage: game_lives
│ └─ "10" (string representation of number)
├─ Cached value (StorageManager._progressCache)
│ └─ Read on first access, cached for performance
└─ Operations (immediate storage update)
├─ getLives() → read from storage
├─ setLives(n) → validate & write to storage
├─ consumeLife() → getLives() - 1, setLives()
├─ addLife() → getLives() + 1, setLives()
└─ hasLives() → getLives() > 0
API CACHE
├─ First Load (Startup)
│ ├─ HttpUtil.get(apiUrl, 8000ms timeout)
│ ├─ Retry: 2 times (1s delay between)
│ └─ Cache in LevelDataManager._apiData
└─ Per-Level Resources (On-Demand)
├─ Check _levelConfigs cache
├─ If missing: assetManager.loadRemote(imageUrl)
├─ Create SpriteFrame
└─ Cache in _imageCache
```
---
## View Stack & Navigation
```
VIEW STACK (LIFO - Last In First Out)
├─ Level 0 (Bottom)
│ └─ PageHome
├─ Level 1 (Middle)
│ └─ PageLevel
├─ Level 2 (Top - Current)
│ └─ PassModal (temporary, on PageLevel)
└─ Operations
├─ open(viewId) → push to stack + show
├─ back() → pop from stack + hide + show prev
├─ replace(viewId) → pop + push new view
└─ close() → pop + show previous
CACHING BEHAVIOR
├─ PageHome
│ ├─ cache: true
│ └─ Cached after first open, reused on back
├─ PageLevel
│ ├─ cache: true
│ └─ Cached per instance (but reinitializes on each open)
└─ PassModal
├─ Dynamically instantiated
├─ Not cached
└─ Destroyed after close
```
---
## Class Hierarchy
```
Component (Cocos)
├─ BaseView (Abstract base)
│ ├─ PageHome
│ ├─ PageLevel
│ └─ PassModal
└─ Toast
Managers (Singleton)
├─ ViewManager
│ └─ Manages page lifecycle & navigation
├─ LevelDataManager
│ └─ Manages API data & asset loading
├─ StorageManager
│ └─ Manages user data persistence
├─ ToastManager
│ └─ Manages toast notifications
└─ WxSDK
└─ Manages WeChat integration
Utilities
├─ HttpUtil
│ └─ Static HTTP methods (GET/POST)
└─ LevelTypes
└─ TypeScript interfaces for API data
```
---
## Dependency Graph
```
main.ts
├─ ViewManager
├─ ToastManager
└─ [Page Prefabs]
├─ PageHome
│ └─ WxSDK
├─ PageLevel
│ ├─ StorageManager
│ ├─ WxSDK
│ ├─ LevelDataManager
│ ├─ ToastManager
│ └─ PassModal
│ ├─ WxSDK
│ └─ BaseView
└─ PageLoading
├─ ViewManager
├─ LevelDataManager
│ ├─ HttpUtil
│ │ └─ XMLHttpRequest
│ ├─ LevelTypes
│ └─ Cocos assetManager
└─ ToastManager
EXTERNAL APIS
├─ https://ilookai.cn/api/v1/wechat-game/levels
│ └─ Called by LevelDataManager._fetchApiData()
└─ WeChat SDK (wx global)
├─ wx.shareAppMessage()
├─ wx.onShareAppMessage()
├─ wx.showShareMenu()
├─ wx.vibrateShort()
└─ wx.vibrateLong()
```
---
## Performance Considerations
### Memory Management
- **Image Loading**: Remote images loaded on-demand with local caching
- **Level Configs**: Loaded incrementally, current + next level only
- **View Caching**: Pages cached after first load, reused on navigation back
- **Message Queuing**: No event queue; direct method calls
### Network
- **Single API Call**: Startup only (all levels fetched at once)
- **Timeout**: 8 seconds for API requests
- **Retry Logic**: 2 attempts with 1-second delay
- **Cache Strategy**: In-memory cache, no expiration
### Storage
- **localStorage**: 2 keys (lives + progress)
- **No Batching**: Each update writes immediately
- **Synchronous**: No async operations needed
### UI/Graphics
- **Single Image**: Main puzzle image per level
- **Dynamic Input**: Input field size adjusts based on answer length
- **Audio**: One-shot playback, no loops
- **Animations**: Toast fade-out only
---
## Security Considerations
### Current Implementation
- ⚠️ All data stored locally (no encryption)
- ⚠️ No user authentication
- ⚠️ No server-side validation
- ⚠️ Progress can be manually edited via localStorage
### Vulnerabilities
1. localStorage can be inspected/modified via browser console
2. No server-side checks on progress/lives
3. Can modify localStorage to skip levels
4. Can modify lives directly
### Improvements Needed
- Server-side progress validation
- User authentication
- Encrypted storage
- Server-side truth for lives/progress

View File

@@ -1,288 +0,0 @@
# Documentation Index
This directory contains comprehensive analysis of the mp-xieyingeng (写英语) Cocos Creator game point/score system.
## 📚 Documentation Files
### 1. **GAME_ANALYSIS.md** (19 KB) - COMPREHENSIVE ANALYSIS
The complete technical analysis of the game's point and score system. **Start here for complete understanding.**
**Contents:**
- PART 1: Points/Asset System (Lives currency)
- PART 2: Level Completion & Rewards (+1 life per pass)
- PART 3: Hint/Clue System (Costs 1 life each)
- PART 4: Game Play Logic (Answer validation, timing)
- PART 5: Loading Page Logic (Initialization flow)
- PART 6: API & Network Requests (Backend endpoint)
- PART 7: User Data Storage (localStorage schema)
- PART 8: WeChat Mini-Game SDK Usage
- PART 9: Complete Game Flow Diagram
- PART 10: Complete Resource Flow
- PART 11: Key Observations & Implementation Gaps
- PART 12: Network Architecture
- Summary table of all features
### 2. **QUICK_REFERENCE.md** (8.6 KB) - QUICK LOOKUP GUIDE
Fast reference guide for developers. Use this for quick lookups.
**Contents:**
- Core Currency Overview (Lives system)
- Flow Charts (Life economics, answer submission, app startup)
- Data Structures (localStorage, API response)
- Key Functions & Methods
- Missing Features & Implementation Gaps
- Network Calls Summary
- Game Loop Per Level
- Data Integrity Notes
- WeChat Features Status
- Extension Ideas (Easy/Medium/Complex)
### 3. **ARCHITECTURE.md** (26 KB) - SYSTEM DESIGN
Detailed architecture diagrams and system design documentation.
**Contents:**
- System Architecture Diagram (layered view)
- Data Flow Diagram (complete user session)
- State Management Flow
- View Stack & Navigation
- Class Hierarchy
- Dependency Graph
- Performance Considerations
- Security Considerations & Vulnerabilities
## 🎯 Quick Navigation
### "I want to understand..."
**...how the point system works**
→ Read: QUICK_REFERENCE.md "THE COMPLETE PICTURE"
**...the complete game flow**
→ Read: GAME_ANALYSIS.md "PART 9: Complete Game Flow Diagram"
**...how lives are stored and managed**
→ Read: GAME_ANALYSIS.md "PART 1: Points/Asset System" + PART 7: User Data Storage"
**...what happens when a user completes a level**
→ Read: GAME_ANALYSIS.md "PART 2: Level Completion & Rewards"
**...how hints work**
→ Read: GAME_ANALYSIS.md "PART 3: Hint/Clue System"
**...the API integration**
→ Read: GAME_ANALYSIS.md "PART 6: API & Network Requests"
**...the code organization**
→ Read: ARCHITECTURE.md "System Architecture Diagram" + "Dependency Graph"
**...what's missing in the implementation**
→ Read: GAME_ANALYSIS.md "PART 11: Key Observations & Gaps"
**...where to add new features**
→ Read: QUICK_REFERENCE.md "EXTENSION IDEAS"
---
## 🔑 Key Findings Summary
### THE POINT SYSTEM
- **Currency**: Lives (生命值)
- **Default**: 10 lives
- **Earn**: +1 per level pass
- **Spend**: -1 per hint unlock (Hint 2 or 3 only)
- **Storage**: localStorage under key `game_lives`
- **No other currency**: No points, coins, or score counter
### LEVEL COMPLETION
- **Reward**: +1 life (only reward)
- **No time bonus**: Same reward regardless of speed
- **No partial credit**: Exact case-sensitive match required
- **Unlimited retries**: Can retry wrong answers indefinitely
- **Timeout incomplete**: 60s countdown exists but doesn't prevent submission
### HINT SYSTEM
- **Hint 1**: Free (always shown)
- **Hint 2**: Costs 1 life (unlock button)
- **Hint 3**: Costs 1 life (unlock button)
- **Max loss per level**: 2 lives (both hints)
- **Net per level**: -1 to +1 depending on hints used
### DATA STORAGE
- **Lives**: localStorage["game_lives"]
- **Progress**: localStorage["game_progress"] with currentLevelIndex & maxUnlockedLevelIndex
- **All local**: No server-side sync
- **No encryption**: Direct access via console
- **Immediate writes**: Each update written to storage immediately
### API INTEGRATION
- **Single endpoint**: GET https://ilookai.cn/api/v1/wechat-game/levels
- **Startup only**: Called once during initialization
- **Retry**: 2 attempts with 1s delay
- **Timeout**: 8 seconds
- **No other backend calls**: No score submission, no analytics, no leaderboard
### WECHAT FEATURES
- ✅ Sharing (with level parameter)
- ✅ Haptic feedback (vibration on errors)
- ❌ No authentication
- ❌ No cloud save
- ❌ No leaderboard
---
## 📁 Source Files Referenced
All 16 TypeScript files in the project are analyzed:
**Core Pages:**
- `PageLoading.ts` - Loading screen & initialization
- `PageHome.ts` - Home menu page
- `PageLevel.ts` - Main game level (where all game logic happens)
- `PassModal.ts` - Level completion modal
**Management Systems:**
- `ViewManager.ts` - Page navigation & lifecycle
- `StorageManager.ts` - Lives & progress persistence
- `LevelDataManager.ts` - API integration & asset loading
**Utilities:**
- `BaseView.ts` - Base class for pages
- `HttpUtil.ts` - HTTP request wrapper
- `WxSDK.ts` - WeChat SDK integration
- `ToastManager.ts` - Toast notifications
- `Toast.ts` - Toast component
- `LevelTypes.ts` - TypeScript interfaces
- `RoundedRectMask.ts` - UI utility
- `BackgroundScaler.ts` - UI utility
- `main.ts` - App entry point
---
## 🔬 Analysis Methodology
This documentation was created by:
1. Finding all 16 TypeScript files in the assets/ directory
2. Reading and analyzing each file for:
- Score/points logic
- Currency/asset management
- Level completion mechanics
- Hint/cost systems
- API calls
- Storage mechanisms
- WeChat SDK usage
- Data flows
3. Mapping dependencies between files
4. Creating flowcharts and diagrams
5. Documenting observations and gaps
---
## 🚀 Using This Documentation
### For Understanding the System
1. Start with QUICK_REFERENCE.md for overview
2. Read GAME_ANALYSIS.md for detailed understanding
3. Refer to ARCHITECTURE.md for system design
### For Making Changes
1. Check ARCHITECTURE.md "Dependency Graph"
2. Review relevant code sections in GAME_ANALYSIS.md
3. Use QUICK_REFERENCE.md to find specific methods
### For Adding Features
1. Review QUICK_REFERENCE.md "EXTENSION IDEAS"
2. Check ARCHITECTURE.md "Security Considerations"
3. Plan changes against current dependencies
### For Debugging
1. Review GAME_ANALYSIS.md "PART 9: Game Flow Diagram"
2. Check ARCHITECTURE.md "Data Flow Diagram"
3. Trace through StorageManager and LevelDataManager
---
## ⚠️ Important Notes
### Security Issues
- ⚠️ All data stored locally without encryption
- ⚠️ No server-side validation of progress
- ⚠️ Users can modify localStorage directly
- ⚠️ Can skip levels by editing progress
### Implementation Gaps
- ❌ No points/coins display
- ❌ No time-based bonuses
- ❌ Timeout doesn't prevent submission
- ❌ No server-side progress sync
- ❌ No analytics tracking
### To Improve
- Add server-side progress validation
- Implement user authentication
- Add score API endpoint
- Track time-to-completion
- Consider leaderboard system
---
## 📝 Document Versions
- Created: April 5, 2026
- Cocos Creator Version: 3.8.8
- Project: mp-xieyingeng (写英语)
- Platform: WeChat Mini-Game
- Analysis Coverage: 100% of TypeScript codebase (16 files)
---
## 📞 Questions Answered
This documentation answers:
- ✅ What is the points/score system?
- ✅ How do users earn points?
- ✅ How are points spent?
- ✅ What happens on level completion?
- ✅ How do hints work and cost lives?
- ✅ Where is data stored?
- ✅ What API calls are made?
- ✅ How does WeChat integration work?
- ✅ What's the complete game flow?
- ✅ What features are missing?
- ✅ What's the system architecture?
- ✅ Where are the security issues?
---
## 🔗 Cross-References
| Topic | Main Document | Quick Ref | Architecture |
|-------|---------------|-----------|--------------|
| Lives System | PART 1 | Overview | State Mgmt |
| Level Rewards | PART 2 | Economics | Data Flow |
| Hints & Costs | PART 3 | Game Loop | Dependencies |
| API | PART 6 | Network | External |
| Storage | PART 7 | Data Structures | State Mgmt |
| WeChat | PART 8 | Features | Dependencies |
| Game Flow | PART 9 | Game Loop | Data Flow |
| Features | PART 11 | Missing | Performance |
---
## 📊 Statistics
- **TypeScript Files Analyzed**: 16
- **Lines of Code Reviewed**: ~1,800
- **API Endpoints**: 1
- **Storage Keys**: 2
- **External SDKs**: 1 (WeChat)
- **Currency Types**: 1 (Lives)
- **Hint Levels**: 3
- **Levels Supported**: 100+ (from API)
- **Player Lives**: 10 (default)
- **Time Per Level**: 60 seconds
- **Max Hint Cost Per Level**: 2 lives
- **Max Life Gain Per Level**: 1 life
---
**For questions or clarifications, refer to the specific document sections listed above.**

View File

@@ -1,504 +0,0 @@
# Cocos Creator Game - Complete Point/Score System Analysis
## Project Overview
This is a WeChat Mini-Game built with Cocos Creator. It's a word puzzle game where players guess answers to images within a 60-second time limit. The game uses a "lives" system instead of traditional points/coins.
---
## 1. LIVES/RESOURCE SYSTEM (The Currency/Points Equivalent)
### Storage & Persistence
**File:** `StorageManager.ts`
- **Storage Key:** `game_lives`
- **Default Lives:** 10 (for new users)
- **Minimum Lives:** 0
- **Storage Method:** `sys.localStorage` (Cocos local storage)
### Lives Management Methods:
```typescript
// Core Methods:
- getLives(): number // Get current lives (default 10 if not set)
- setLives(lives: number) // Set lives value (min 0)
- consumeLife(): boolean // Deduct 1 life, returns success status
- addLife(): void // Add 1 life
- hasLives(): boolean // Check if lives > 0
- resetLives(): void // Reset to default 10
```
### Lives Usage:
1. **Unlocking Hints (Clues):** Each unlock costs 1 life
- Clue 1: Free (always unlocked)
- Clue 2: Costs 1 life to unlock
- Clue 3: Costs 1 life to unlock
---
## 2. LEVEL PROGRESSION SYSTEM
**File:** `StorageManager.ts` (User Progress section)
### Progress Data Structure:
```typescript
interface UserProgress {
currentLevelIndex: number; // Current level (0-based)
maxUnlockedLevelIndex: number; // Highest level player reached
}
```
### Progress Storage:
- **Storage Key:** `game_progress`
- **Default:** `{ currentLevelIndex: 0, maxUnlockedLevelIndex: 0 }`
- **Caching:** Cached in memory (`_progressCache`) to avoid repeated localStorage reads
### Progression Methods:
```typescript
- getCurrentLevelIndex(): number // Get current level
- setCurrentLevelIndex(index): void // Set current level
- getMaxUnlockedLevelIndex(): number // Get highest unlocked level
- isLevelUnlocked(levelIndex): boolean // Check if level is playable
- onLevelCompleted(completedLevelIndex) // Called when level is beaten
- resetProgress(): void // Reset to level 1
```
### Level Unlock Logic:
- **Level 1** is always unlocked
- When player completes level N:
- Current level → N+1
- Max unlocked → max(maxUnlocked, N)
- This allows replaying lower levels while progressing forward
---
## 3. GAME LEVEL DATA & API
**File:** `LevelDataManager.ts`
### API Configuration:
```typescript
API_URL = 'https://ilookai.cn/api/v1/wechat-game/levels'
REQUEST_TIMEOUT = 8000ms
API_RETRY_COUNT = 2 (retries on failure)
```
### Level Data Structure (from API):
```typescript
interface ApiLevelData {
id: string; // UUID
level: number; // Level number
imageUrl: string; // Main image URL
hint1: string; // Free clue
hint2: string; // Paid clue (costs 1 life)
hint3: string; // Paid clue (costs 1 life)
answer: string; // The correct answer
sortOrder: number; // Sorting order
}
```
### API Response:
```typescript
interface ApiResponse {
success: boolean;
message: string | null;
data: {
levels: ApiLevelData[];
total: number;
}
}
```
### Image Loading:
- Remote images loaded via `assetManager.loadRemote()`
- Cached in memory (`_imageCache: Map<URL, SpriteFrame>`)
- First level image preloaded during app initialization
- Next level image preloaded silently after entering current level
### Loading Strategy:
1. **On App Start:** Load all level metadata + first level image (80% of loading bar)
2. **On Level Enter:** Load current level image if needed
3. **After Level Completion:** Preload next level asynchronously (doesn't block gameplay)
---
## 4. GAMEPLAY LOOP
**File:** `PageLevel.ts`
### Game Sequence:
1. Player enters level
2. Main image displays with hint 1 visible
3. 60-second countdown starts
4. Player enters answer in single EditBox
5. Player can unlock hints 2 & 3 by spending lives
6. Player submits answer
### Time Limit:
- **Duration:** 60 seconds per level
- **Implementation:** `schedule(this.onCountdownTick, 1)` (1-second interval)
- **On Time Up:** Plays fail sound, but doesn't force level exit
### Input System:
- **Type:** Single EditBox (not multi-input per character)
- **Width:** Dynamic (based on answer length)
- Formula: `Math.min(600, Math.max(200, answerLength * 60 + 40))` pixels
- **Max Length:** Match answer length
- **Placeholder:** Shows answer length as hint
### Answer Processing:
```typescript
getAnswer(): string {
const editBox = this._inputNodes[0].getComponent(EditBox);
return (editBox?.string ?? '').trim(); // Trimmed
}
// Comparison is case-sensitive:
if (userAnswer === this._currentConfig.answer) {
// WIN
} else {
// LOSE
}
```
---
## 5. WINNING & REWARDS
**File:** `PageLevel.ts`
### On Correct Answer:
1. **Stop Timer:** Countdown stops
2. **Play Sound:** Success audio plays
3. **Award 1 Life:** `addLife()` called
4. **Show Modal:** PassModal displays with buttons
### Pass Modal (Victory Screen):
- Shows "Next Level" button
- Shows "Share with Friends" button
- On "Next Level": Progress to next level (calls `onLevelCompleted()`)
- On "Share": Triggers WeChat share with query param `?level=<levelIndex>`
### Progression on Pass:
```typescript
StorageManager.onLevelCompleted(currentLevelIndex);
// Sets:
// - currentLevelIndex → currentLevelIndex + 1
// - maxUnlockedLevelIndex → max(maxUnlocked, currentLevelIndex)
```
### Game End Condition:
- When `currentLevelIndex >= totalLevels`, player has beaten all levels
- Game returns to home page
---
## 6. LOSING & CONSEQUENCES
**File:** `PageLevel.ts`
### On Wrong Answer:
1. **Play Sound:** Fail audio plays
2. **Vibration:** `WxSDK.vibrateLong()` (400ms vibration on WeChat)
3. **Toast Message:** "答案错误,再试试吧!" (Answer wrong, try again!)
4. **No Penalty:** No life deducted, level doesn't change
### On Time Up:
1. **Play Sound:** Fail audio plays
2. **Countdown Stops:** `_isTimeUp = true`
3. **No Forced Exit:** Player can continue typing and submitting
4. **No Life Penalty:** Still can retry
---
## 7. HINT/CLUE SYSTEM
**File:** `PageLevel.ts`
### Clue Mechanics:
1. **Clue 1:** Always visible, FREE
2. **Clue 2:** Hidden by default, costs 1 life to unlock
3. **Clue 3:** Hidden by default, costs 1 life to unlock
### Unlocking Process:
```typescript
onUnlockClue(index: number) {
// 1. Check if lives available
if (!this.hasLives()) return;
// 2. Consume 1 life
if (!this.consumeLife()) return;
// 3. Play click sound
this.playClickSound();
// 4. Hide unlock button
this.hideUnlockButton(index);
// 5. Show clue content
this.showClue(index);
this.setClue(index, clueContent);
}
```
### Clue Cost Implications:
- Player starts with 10 lives
- Can unlock both clues 2 & 3 = 2 lives spent minimum
- But only 8 lives remain per level if both used
- Player can conserve lives by solving without clues
---
## 8. NETWORK & API COMMUNICATION
**File:** `HttpUtil.ts`
### HTTP Methods:
```typescript
// GET request
HttpUtil.get<T>(url: string, timeout: number = 10000): Promise<T>
// POST request
HttpUtil.post<T>(url: string, data: object, timeout: number = 10000): Promise<T>
```
### Implementation:
- Uses `XMLHttpRequest`
- Supports JSON responses
- Default timeout: 10 seconds
- Error handling: Rejects on HTTP errors, timeouts, or network failures
### Used By:
- `LevelDataManager` uses `HttpUtil.get()` to fetch level data from API
- No POST requests currently used
---
## 9. WECHAT SDK INTEGRATION
**File:** `WxSDK.ts`
### WeChat Features Used:
#### 1. Platform Detection:
```typescript
isWechat(): boolean
// Returns: sys.platform === sys.Platform.WECHAT_GAME
```
#### 2. Sharing:
- **Share Menu:** `showShareMenu()` - Enables share button in header
- **Friend Share:** `onShareAppMessage(config)` - Right-click "Share" message
- **Timeline Share:** `onShareTimeline(config)` - Moments sharing
- **Active Share:** `shareAppMessage(config)` - Trigger share dialog
#### 3. Vibration:
- **Short Vibrate:** `vibrateShort()` - 15ms, for button clicks
- **Long Vibrate:** `vibrateLong()` - 400ms, for errors
#### 4. Share Configuration:
```typescript
interface WxShareConfig {
title: string; // Share title: "写英语"
imageUrl?: string; // Share image (optional)
query?: string; // Query params (e.g., "level=5")
}
```
#### 5. Initialization:
```typescript
WxSDK.initShare(config) {
// Calls in sequence:
// 1. showShareMenu()
// 2. onShareAppMessage(config)
// 3. onShareTimeline(config)
}
```
**Called in:** PageHome on game start
---
## 10. GAME STATE MANAGEMENT
**File:** `ViewManager.ts` (Page Stack) + `StorageManager.ts` (Data)
### View Stack (Navigation):
- Maintains page stack for navigation
- `PageHome` (z-index 0) - Main menu
- `PageLevel` (z-index 1) - Game level
- `PassModal` (z-index 999) - Victory overlay
### Persistent State:
- Lives stored in localStorage with key `game_lives`
- Progress stored in localStorage with key `game_progress`
- Both persist across app sessions
- Data survives app closure and reopening
### Runtime State:
- Current countdown timer
- Current input box content
- Unlocked clues state (reset each level)
- Current level config (API data)
---
## 11. LOADING PAGE FLOW
**File:** `PageLoading.ts`
### Initialization Sequence:
1. **Stage 1 (0-30%):** Fetch all levels from API via `LevelDataManager.initialize()`
- API call with retry logic
- Parse level metadata
- NOT loading all images yet
2. **Stage 2 (30-80%):** Preload first level image
- Uses `LevelDataManager.ensureLevelReady(0)`
- Shows "正在加载游戏必备资源..." message
3. **Stage 3 (80-100%):** Preload PageHome view
- `ViewManager.preload('PageHome')`
- Shows "正在加载界面资源..." message
4. **Completion (100%):** Open PageHome and destroy loading page
---
## 12. COMPLETE POINTS FLOW DIAGRAM
```
START GAME
[10 Lives] (default)
LEVEL 1 STARTS
├─ View Clue 1 (FREE)
├─ Option: Unlock Clue 2 (-1 Life) → [9 Lives]
├─ Option: Unlock Clue 3 (-1 Life) → [8 Lives]
├─ Player submits answer
├─ IF CORRECT:
│ ├─ Add 1 Life → [9 or 10+ Lives]
│ ├─ Show PassModal
│ └─ Move to LEVEL 2
└─ IF WRONG:
├─ Play fail sound & vibrate
├─ Show toast message
├─ Lives unchanged
└─ Can retry (no level exit)
IF ALL LEVELS COMPLETE:
└─ Return to home
IF TIME UP:
├─ Play fail sound
├─ Can still submit (lives unchanged)
└─ No forced exit
```
---
## 13. KEY FILES SUMMARY
| File | Purpose | Key Components |
|------|---------|-----------------|
| StorageManager.ts | Data persistence | Lives + Progress storage |
| LevelDataManager.ts | Level data loading | API calls + Image caching |
| PageLevel.ts | Main game logic | Countdown, input, hints, validation |
| PageLoading.ts | App initialization | Loading bar + progress |
| PageHome.ts | Home screen | Start game button |
| PassModal.ts | Victory screen | Next/Share buttons |
| ViewManager.ts | Page navigation | View stack + caching |
| WxSDK.ts | WeChat API | Share + vibration |
| HttpUtil.ts | Network requests | GET/POST + error handling |
| ToastManager.ts | Notifications | Brief toast messages |
---
## 14. IMPORTANT CONSTANTS
### Game Constants:
- **Level Time Limit:** 60 seconds
- **Default Lives:** 10
- **Life Cost per Hint:** 1 life per hint (hints 2 & 3)
- **Reward per Level:** +1 life
### API Constants:
- **Endpoint:** `https://ilookai.cn/api/v1/wechat-game/levels`
- **Timeout:** 8000ms
- **Retry Count:** 2
### UI Constants:
- **PageHome z-index:** 0
- **PageLevel z-index:** 1
- **PassModal z-index:** 999
---
## 15. MISSING FEATURES (Observations)
1. **No User Authentication:** No wx.login call visible
2. **No Backend Sync:** No calls to save progress to server
3. **No Ads/IAP:** No monetization system
4. **No Leaderboards:** No score submission to WeChat
5. **No Analytics:** No tracking events beyond console logs
6. **No Life Refill:** No premium way to get more lives
7. **No Difficulty Levels:** All players see same levels
8. **No Sound Toggle:** Sound plays automatically
---
## 16. DATA FLOW SUMMARY
```
WeChat API: https://ilookai.cn/api/v1/wechat-game/levels
LevelDataManager (fetch + cache)
PageLevel (display + gameplay)
├─ InputBox (player answer)
├─ Clues (cost lives to unlock)
└─ Timer (60 second countdown)
StorageManager (save lives + progress)
localStorage
├─ game_lives: number
└─ game_progress: UserProgress (JSON)
```
---
## 17. CRITICAL BUSINESS LOGIC
### Win Condition:
```
userAnswer (trimmed) === correctAnswer (from API)
→ Award +1 life
→ Save progress
→ Move to next level
```
### Lose Condition:
```
userAnswer !== correctAnswer
→ No penalty
→ Can retry immediately
→ Timer continues (even after time up)
```
### Progression:
```
Beat Level N
→ currentLevel = N + 1
→ maxUnlocked = max(maxUnlocked, N)
→ Reward: +1 life (so levels can chain profitably)
```
### Economy Balance:
- Start: 10 lives
- Per level: Can spend 0-2 lives (hints) or 0 lives (no hints)
- Per level: Earn +1 life (net: -1 or +1 lives)
- Average player with no hints: +1 life/level → infinite scaling
- Average player with 1 hint: 0 lives/level → stable
- Hardcore with 2 hints: -1 life/level → finite runway

View File

@@ -1,494 +0,0 @@
# Complete Points/Lives Flow Diagram
## 🔴 INITIALIZATION PHASE
```
┌─────────────────────────────────────┐
│ GAME STARTS │
│ PageLoading initializes │
└──────────────┬──────────────────────┘
┌──────────────────────────┐
│ Check localStorage │
│ "game_lives" key │
└────┬──────────────┬──────┘
│ │
NOT SET EXISTS
(New User) (Returning)
│ │
↓ ↓
┌────────────┐ ┌────────────┐
│ 10 Lives │ │Parse Value │
│(DEFAULT) │ │from Storage│
└────┬───────┘ └────┬───────┘
│ │
└────────┬───────┘
┌────────────────────────┐
│ LOAD LEVEL DATA │
│ (API fetch) │
│ https://ilookai.cn ... │
└────────┬───────────────┘
┌────────────────────────┐
│ Display PageHome │
│ (with shared config) │
└────────┬───────────────┘
┌────────────────────────┐
│ READY TO PLAY │
│ Lives: X | Level: Y │
└────────────────────────┘
```
---
## 🎮 GAMEPLAY PHASE (Single Level)
```
┌────────────────────────────────────────┐
│ LEVEL STARTS │
│ Load level data from cache │
│ Display image + Clue 1 (FREE) │
│ Start 60-second countdown │
└──────────────┬─────────────────────────┘
├─────────────────────────────────────┐
│ │
↓ ↓
┌──────────────────┐ ┌──────────────────────┐
│ UNLOCK CLUE 2 │ │ SUBMIT ANSWER │
│ onUnlockClue(2) │ │ onSubmitAnswer() │
│ │ │ │
│ Check: hasLives()├──NO──→ │ Compare: │
│ │ │ │ input === │
│ YES │ │ correctAnswer │
│ │ │ │ │
│ ↓ │ │ ├──YES──→┐ │
│ consumeLife() │ │ │ │ │
│ Lives: -1 │ │ │ │ │
│ │ │ │ │ │ │
│ ↓ │ │ │ ┌┴─────────┐
│ Display Clue 2 │ │ │ │ │
│ Refresh Label │ │ │ ↓ │
│ │ │ │ │ SUCCESS! │
│ ↓ │ │ │ showSuccess() │
│ Continue Game │ │ │ │ │
└──────────────────┘ │ │ ↓ │
│ │ │ Play success │
│ │ │ sound │
↓ │ │ Stop timer │
┌──────────────────┐ │ │ addLife() │
│ UNLOCK CLUE 3 │ │ │ Lives: +1 │
│ onUnlockClue(3) │ │ │ │ │
│ │ │ │ ↓ │
│ Check: hasLives()├──NO──→ │ │ Show PassModal │
│ │ │ │ │ │ │
│ YES │ │ │ ├─►[NEXT] │
│ │ │ │ │ │ │
│ ↓ │ │ │ └─►[SHARE] │
│ consumeLife() │ │ │ │
│ Lives: -1 │ │ └──NO──────────────┘
│ │ │ │ │
│ ↓ │ │ ↓
│ Display Clue 3 │ │ ERROR!
│ Refresh Label │ │ showError()
│ │ │ │ │
│ ↓ │ │ ↓
│ Continue Game │ │ Play fail sound
│ │ │ Vibrate long
│ │ │ Show toast:
│ │ │ "答案错误,
└──────────────────┘ │ 再试试吧!"
│ │ Lives unchanged
│ │ │
│ │ ↓
│ │ Can retry
│ │ (same level)
│ │
└──────────────────────┘
┌──────────────────────┐
│ TIME UP? │
└──────────────────────┘
┌─────────────────────┐
│ Play fail sound │
│ Stop countdown │
│ Can still retry │
│ Lives unchanged │
└─────────────────────┘
```
---
## 📊 LIVES TRANSACTION FLOW
```
LEVEL START
┌────────┴─────────┐
│ │
OPTIONAL MANDATORY
(Hints) (Level)
│ │
┌───────┴────────┐ │
│ │ │
HINT 2 HINT 3 SUBMISSION
-1 Life -1 Life │
│ │ │
└────────┬───────┴─┬───────┤
│ │ │
(Spent) (Spent) ANSWER
│ │ │
└────┬────┴───┬───┘
│ │
0-2 LIVES SPENT
(Depends on choices)
│ │
┌─────────┼────────┼─────────┐
│ │ │ │
CORRECT WRONG TIMEOUT ...
│ │ │
│ │ │
+1 LIFE 0 LIVES 0 LIVES
│ │ │
↓ ↓ ↓
┌───────────────────────────────┐
│ NEW LIVES BALANCE │
│ │
│ Formula: │
│ newLives = │
│ oldLives │
│ - (hintsUnlocked × 1) │
│ + (if levelWon ? 1 : 0) │
│ │
│ Examples: │
│ 10 - 0 + 1 = 11 (no hints) │
│ 10 - 1 + 1 = 10 (1 hint) │
│ 10 - 2 + 1 = 9 (2 hints) │
│ 10 - 0 + 0 = 10 (retry) │
└───────────────────────────────┘
```
---
## 🏆 LEVEL PROGRESSION & PERSISTENCE
```
LEVEL WON
┌────────────────────────────┐
│ StorageManager. │
│ onLevelCompleted() │
│ │
│ Takes: levelIndex (0-based) │
└────────────┬────────────────┘
┌────────┴────────┐
│ │
↓ ↓
┌────────────┐ ┌──────────────────┐
│ currentLevel │ maxUnlocked │
│ = N + 1 │ = max(N, current)│
└────────────┘ └──────────────────┘
│ │
└────────┬────────┘
┌──────────────────────────────┐
│ Save to localStorage: │
│ "game_progress" = │
│ { │
│ currentLevelIndex: N+1, │
│ maxUnlockedLevelIndex: N │
│ } │
└────────┬─────────────────────┘
┌──────────────────────────────┐
│ NEXT GAME SESSION: │
│ Restore from localStorage │
│ Resume at Level N+1 │
│ Can replay Levels 0 to N │
└──────────────────────────────┘
```
---
## 💾 DATA PERSISTENCE
```
┌─────────────────────────────────────────┐
│ localStorage (Browser) │
├─────────────────────────────────────────┤
│ │
│ KEY: "game_lives" │
│ VALUE: "10" (string number) │
│ Type: String │
│ Managed by: StorageManager │
│ Accessed by: PageLevel, PassModal │
│ │
├─────────────────────────────────────────┤
│ │
│ KEY: "game_progress" │
│ VALUE: JSON string │
│ { │
│ "currentLevelIndex": 2, │
│ "maxUnlockedLevelIndex": 4 │
│ } │
│ Type: String (JSON) │
│ Managed by: StorageManager │
│ Accessed by: PageLevel, ViewManager │
│ │
└─────────────────────────────────────────┘
```
---
## 🌐 API & LEVEL DATA FLOW
```
┌──────────────────────────────────────────┐
│ Remote API Server │
│ https://ilookai.cn/api/v1/... │
│ │
│ Returns: Array<ApiLevelData> │
│ Fields: id, level, imageUrl, hint1-3, │
│ answer, sortOrder │
└────────────────┬──────────────────────────┘
↓ (1st app load only)
HttpUtil.get()
┌──────────────────────────┐
│ LevelDataManager │
│ _fetchApiData() │
│ │
│ Retry: 2 attempts │
│ Timeout: 8000ms │
└────────┬─────────────────┘
┌────┴────┐
│ │
FAIL SUCCESS
│ │
│ ↓
│ ┌──────────────────┐
│ │ Cache all level │
│ │ metadata in │
│ │ memory │
│ │ (_apiData) │
│ └────┬─────────────┘
│ │
│ ↓
│ ┌──────────────────┐
│ │ Preload Level 0 │
│ │ image │
│ │ (_levelConfigs) │
│ └────┬─────────────┘
│ │
└────┬────┘
┌──────────────────────────┐
│ PageLevel ready │
│ Display first level │
└──────────────────────────┘
```
---
## ⏰ COUNTDOWN TIMER FLOW
```
┌──────────────────┐
│ startCountdown() │
│ _countdown = 60 │
│ _isTimeUp = false│
└────────┬─────────┘
┌─────────────────────────┐
│ schedule( │
│ onCountdownTick, 1 │ ← Every 1 second
│ ) │
└────────┬────────────────┘
├─────────────────────────────┐
│ EVERY SECOND │
│ (if _isTimeUp = false) │
│ │
├─→ _countdown-- │
├─→ updateClockLabel() │
│ (display "59s", "58s"...) │
│ │
└────────┬────────────────────┘
├─────────────────────────┐
│ │
_countdown > 0 _countdown <= 0
│ │
│ ↓
│ _isTimeUp = true
│ stopCountdown()
│ onTimeUp()
│ playFailSound()
│ │
│ ↓
│ Can still submit!
│ Lives unchanged
│ │
└──────────┬──────────────┘
┌────────────────┐
│ PLAYER ACTION: │
│ Submit Answer │
└────────────────┘
```
---
## 📱 WECHAT INTEGRATION POINTS
```
┌──────────────────────────────────────┐
│ GAME START (PageHome) │
├──────────────────────────────────────┤
│ WxSDK.initShare({ │
│ title: "写英语", │
│ query: "" │
│ }) │
│ └─► Enable share menu in header │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ LEVEL WON (PassModal) │
├──────────────────────────────────────┤
│ User clicks "Share" │
│ WxSDK.shareAppMessage({ │
│ title: "快来一起玩...", │
│ query: "level=<levelIndex>" │
│ }) │
│ └─► Opens share dialog │
│ with level param │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ WRONG ANSWER (PageLevel) │
├──────────────────────────────────────┤
│ showError() │
│ WxSDK.vibrateLong() │
│ └─► 400ms vibration feedback │
└──────────────────────────────────────┘
```
---
## 🔄 Complete Player Journey
```
START
┌─────────────────────┐
│ Check localStorage │
│ game_lives: 10 │
│ game_progress: {0,0}│
└──────────┬──────────┘
┌─────────────────┐
│ PageHome │
│ Start Button │
└──────────┬──────┘
┌──────────────────────────┐
│ PageLevel: LEVEL 1 │
│ Lives: 10 │ Answer: __ │
│ Clue 1: ✓ (FREE) │
│ Clue 2: Unlock? (-1 life)│
│ Clue 3: Unlock? (-1 life)│
│ ⏱️ 60s countdown... │
└──────────┬───────────────┘
┌──────┴──────┬────────────────┐
│ │ │
UNLOCK SUBMIT TIMEOUT
CLUE 2 ANSWER (still ok)
-1 Life │
│ ├─ CORRECT: +1 Life
│ │ ↓
│ │ PassModal
│ │ ├─ NEXT → Level 2
│ │ └─ SHARE → wx.share
│ │
│ └─ WRONG: Lives stay
│ ↓
│ Retry same level
└─ UNLOCK
CLUE 3
-1 Life
(Try answer)
├─ CORRECT: +1 Life
│ ↓
│ PassModal
│ └─ NEXT → Level 2
└─ WRONG: Lives stay
Retry
PROGRESSION CONTINUES...
BEAT ALL LEVELS
Return to PageHome
(Save progress in localStorage)
COMPLETE
```
---
## 📈 Lives Over Time (Example Scenario)
```
Session 1: No hints used
━━━━━━━━━━━━━━━━━━━━━
Level 1: 10 Lives → Correct → 11 Lives
Level 2: 11 Lives → Correct → 12 Lives
Level 3: 12 Lives → Correct → 13 Lives
Session 1 ends: 13 Lives saved
Session 2: Some hints used
━━━━━━━━━━━━━━━━━━━━━
Start: 13 Lives
Level 4: 13 - 1 (hint) + 1 (win) = 13 Lives
Level 5: 13 - 2 (hints) + 1 (win) = 12 Lives
Level 6: 12 - 0 (no hints) + 1 (win) = 13 Lives
Level 7: 13 - 0 (no hints) + 1 (win) = 14 Lives
Session 2 ends: 14 Lives saved
Session 3: Struggling with hints
━━━━━━━━━━━━━━━━━━━━━
Start: 14 Lives
Level 8: 14 - 2 (hints) + 1 (win) = 13 Lives
Level 9: 13 - 2 (hints) + 1 (win) = 12 Lives
Level 10: 12 - 0 (retry, no hints) + 1 (eventually win) = 13 Lives
Session 3 ends: 13 Lives saved
PATTERN: Lives stay stable or grow over time
depending on hint usage
```

View File

@@ -1,336 +0,0 @@
# 📚 Game Points/Lives System - Documentation Index
> Complete analysis of the Cocos Creator WeChat Mini-Game points/score/lives system
**Generated:** April 5, 2026
**Scope:** All 21 TypeScript source files analyzed
**Total Documentation:** ~45KB
---
## 📄 Documentation Files
### 1. **SUMMARY.md** (11 KB) ⭐ **START HERE**
**Best for:** High-level overview and executive understanding
**Contains:**
- Executive summary of the lives-based economy
- 21 files analyzed (categorized)
- Complete points/lives flow table
- Data persistence details
- Level progression mechanics
- Hint system mechanics
- Gameplay loop description
- API integration overview
- WeChat integration features
- User journey example
- Testing scenarios
- Business logic highlights
**Read this if:** You want a quick understanding of how the entire system works
---
### 2. **GAME_ANALYSIS.md** (13 KB)
**Best for:** Deep technical analysis of every system component
**Contains 17 Sections:**
1. Project Overview
2. Lives/Resource System (complete)
3. Level Progression System
4. Game Level Data & API
5. Gameplay Loop
6. Winning & Rewards
7. Losing & Consequences
8. Hint/Clue System
9. Network & API Communication
10. WeChat SDK Integration
11. Game State Management
12. Loading Page Logic
13. Complete Points Flow Diagram
14. Key Files Summary (table)
15. Important Constants
16. Missing Features
17. Data Flow Summary & Business Logic
**Read this if:** You need comprehensive technical details on every aspect
---
### 3. **QUICK_REFERENCE.md** (5.6 KB)
**Best for:** Quick lookup while coding
**Contains:**
- What is the currency? (Lives)
- Lives management methods
- How lives are spent (table)
- How lives are earned (table)
- Level progression mechanics
- Level data structure (from API)
- Gameplay mechanics (time limit, input, conditions)
- Rewards & penalties (table)
- Economy balance formulas
- API integration details
- Storage schema
- WeChat integration points
- Key files list
- Important constants
- What's NOT implemented
**Read this if:** You need quick facts/numbers without reading full docs
---
### 4. **POINTS_FLOW_DIAGRAM.md** (22 KB)
**Best for:** Visual understanding of system flows
**Contains ASCII Diagrams For:**
1. Initialization Phase
2. Gameplay Phase (single level)
3. Lives Transaction Flow
4. Level Progression & Persistence
5. Data Persistence (localStorage)
6. API & Level Data Flow
7. Countdown Timer Flow
8. WeChat Integration Points
9. Complete Player Journey
10. Lives Over Time (example scenario)
**Read this if:** You prefer visual diagrams over text explanations
---
### 5. **POINTS_SYSTEM_INDEX.md** (This File!)
**Best for:** Navigation and understanding what exists where
**Contains:**
- Overview of all documentation
- Quick navigation by topic
- Search guide
- Related code file references
---
## 🎯 Quick Navigation by Topic
### I want to understand...
**The Lives Economy**
- → SUMMARY.md → "The Complete Points/Lives Flow"
- → QUICK_REFERENCE.md → "Economy Balance"
- → GAME_ANALYSIS.md → Section 1 & 2
**How to Earn Lives**
- → SUMMARY.md → "EARNING Lives table"
- → QUICK_REFERENCE.md → "How Lives Are Earned"
- → POINTS_FLOW_DIAGRAM.md → "Complete Player Journey"
**How to Spend Lives**
- → SUMMARY.md → "SPENDING Lives table"
- → QUICK_REFERENCE.md → "How Lives Are Spent"
- → GAME_ANALYSIS.md → Section 7 "Hint/Clue System"
**Level Progression**
- → SUMMARY.md → "Level Progression System"
- → QUICK_REFERENCE.md → "Level Progression"
- → GAME_ANALYSIS.md → Section 2
- → POINTS_FLOW_DIAGRAM.md → "Level Progression & Persistence"
**Data Storage**
- → SUMMARY.md → "Data Persistence"
- → QUICK_REFERENCE.md → "Storage Schema"
- → GAME_ANALYSIS.md → Section 2 "Storage & Persistence"
- → POINTS_FLOW_DIAGRAM.md → "Data Persistence"
**API Integration**
- → SUMMARY.md → "API Integration"
- → GAME_ANALYSIS.md → Section 3 & 4
- → POINTS_FLOW_DIAGRAM.md → "API & Level Data Flow"
**WeChat Integration**
- → SUMMARY.md → "WeChat Integration"
- → GAME_ANALYSIS.md → Section 9
- → POINTS_FLOW_DIAGRAM.md → "WeChat Integration Points"
**Complete Gameplay Flow**
- → SUMMARY.md → "Gameplay Loop"
- → GAME_ANALYSIS.md → Section 4 & 5 & 6
- → POINTS_FLOW_DIAGRAM.md → "Gameplay Phase"
**Testing/Validation**
- → SUMMARY.md → "Testing Scenarios"
- → QUICK_REFERENCE.md → Check "What's NOT implemented" section
---
## 📖 Code References
### Core Files & What They Do
**StorageManager.ts** (Lives + Progress)
- `getLives()` / `setLives()` / `consumeLife()` / `addLife()`
- `getCurrentLevelIndex()` / `onLevelCompleted()`
- Referenced in: GAME_ANALYSIS §2, SUMMARY "Data Persistence"
**PageLevel.ts** (Main Gameplay)
- `onUnlockClue()` / `onSubmitAnswer()` / `showSuccess()` / `showError()`
- `startCountdown()` / `onCountdownTick()`
- Referenced in: GAME_ANALYSIS §4-7, SUMMARY "Gameplay Loop"
**LevelDataManager.ts** (API + Caching)
- `initialize()` / `ensureLevelReady()` / `preloadNextLevel()`
- Referenced in: GAME_ANALYSIS §3, SUMMARY "API Integration"
**PassModal.ts** (Victory Screen)
- Victory UI and rewards display
- Referenced in: GAME_ANALYSIS §5
**WxSDK.ts** (WeChat Integration)
- `shareAppMessage()` / `vibrateLong()` / etc.
- Referenced in: GAME_ANALYSIS §9
**ViewManager.ts** (Page Navigation)
- Page stack management
- Referenced in: GAME_ANALYSIS §10
---
## 🔑 Key Numbers & Values
| Item | Value | Reference |
|------|-------|-----------|
| Default Lives | 10 | QUICK_REFERENCE, SUMMARY |
| Min Lives | 0 | QUICK_REFERENCE, SUMMARY |
| Hint Cost | 1 life each | QUICK_REFERENCE, SUMMARY |
| Win Reward | 1 life | QUICK_REFERENCE, SUMMARY |
| Level Time | 60 seconds | QUICK_REFERENCE, SUMMARY |
| API Timeout | 8000ms | QUICK_REFERENCE, GAME_ANALYSIS §3 |
| API Retries | 2 attempts | QUICK_REFERENCE, GAME_ANALYSIS §3 |
---
## 🧩 How Systems Interact
```
StorageManager (Lives + Progress)
PageLevel (Gameplay)
├─ Uses Lives for: Hint Unlocks
├─ Updates Lives on: Level Complete
├─ Uses Progress for: Level Selection
└─ Updates Progress on: Level Complete
LevelDataManager (API + Cache)
├─ Fetches: Level Data + Images
└─ Serves: Hints + Answers + Images to PageLevel
WxSDK (WeChat)
├─ Shares: Victory with Level Param
└─ Vibrates: On Error
ViewManager (Navigation)
└─ Manages: Page Stack + State
```
---
## ✅ Document Completeness Checklist
- [x] Lives earning mechanics
- [x] Lives spending mechanics
- [x] Level progression system
- [x] Hint/clue system
- [x] API integration
- [x] Data persistence
- [x] WeChat features
- [x] Gameplay loop
- [x] Win/lose conditions
- [x] User journey examples
- [x] Flow diagrams
- [x] Code references
- [x] Constants & values
- [x] Error handling
- [x] Performance optimizations
- [x] Missing features list
- [x] Testing scenarios
**Coverage:** 100% of points/lives/score system
---
## 🚀 Using This Documentation
### For Understanding
1. Start with SUMMARY.md (5 min read)
2. Read QUICK_REFERENCE.md (3 min read)
3. Review POINTS_FLOW_DIAGRAM.md (5 min read)
4. Deep dive into GAME_ANALYSIS.md (15 min read)
### For Maintenance
- QUICK_REFERENCE.md is your quick lookup
- GAME_ANALYSIS.md has implementation details
- Code references show where logic lives
### For Modifications
- "Data Persistence" section for storage changes
- "Lives Transaction Flow" for economy tweaks
- "Constants" section for balance adjustments
- Individual sections in GAME_ANALYSIS for specific systems
---
## ❓ FAQ
**Q: Where is the lives starting value defined?**
A: `StorageManager.ts` line 25, `DEFAULT_LIVES = 10`
**Q: How do players earn lives?**
A: By completing levels correctly. See SUMMARY.md "Earning Lives"
**Q: Can players lose lives?**
A: Only by unlocking hints (-1 each). Wrong answers don't penalize. See SUMMARY.md "Spending Lives"
**Q: Where is progress stored?**
A: Browser localStorage under keys "game_lives" and "game_progress". See POINTS_FLOW_DIAGRAM.md "Data Persistence"
**Q: What happens when time runs out?**
A: Game plays fail sound but doesn't end. Players can still submit. See GAME_ANALYSIS.md Section 6
**Q: How does the hint system work?**
A: Hint 1 is free, Hints 2 & 3 cost 1 life each. See GAME_ANALYSIS.md Section 7
**Q: Is there backend sync?**
A: No. Progress is local-only. See SUMMARY.md "Missing Features"
---
## 📞 Document Metadata
| Property | Value |
|----------|-------|
| Total Files Analyzed | 21 TypeScript files |
| Total Lines of Code | ~2,500 lines |
| Documentation Generated | 4 files, ~45KB |
| Coverage | Complete (100%) |
| Last Updated | April 5, 2026 |
| Scope | Points/Lives/Score System |
---
## 🎓 Summary of Key Takeaways
1. **Currency:** LIVES (renewable resource), not points
2. **Starting:** 10 lives for new players
3. **Economy:** +1 life per win, -1 per hint unlock
4. **Storage:** localStorage (persistent, survives app close)
5. **API:** Fetches level data on app start, cached in memory
6. **Progression:** Sequential level unlocking with level 1 always available
7. **Strategy:** No penalties for wrong answers, encourages replayability
8. **WeChat:** Sharing with level referral parameter
9. **Scope:** No backend sync, no authentication, local-only progress
10. **Design:** Simple, effective, encourages skill-based progression
---
**Questions?** All answers are in one of these 4 documentation files!

View File

@@ -1,196 +0,0 @@
# QUICK REFERENCE - Game Points/Score System
## 🎮 What is the "Currency"?
**LIVES** - Not traditional points/coins, but a renewable "health" resource.
## 📊 Lives Management
```
Storage Key: "game_lives" (localStorage)
Default: 10
Min Value: 0
Max Value: ∞ (no limit)
Methods:
├─ getLives() → Returns current lives
├─ setLives(n) → Set specific value
├─ consumeLife() → Deduct 1 life
├─ addLife() → Add 1 life
├─ hasLives() → Check if > 0
└─ resetLives() → Reset to 10
```
## 🎯 How Lives Are Spent
| Action | Cost | Where |
|--------|------|-------|
| Unlock Clue 2 | 1 Life | PageLevel → onUnlockClue(2) |
| Unlock Clue 3 | 1 Life | PageLevel → onUnlockClue(3) |
| **TOTAL PER LEVEL** | **0-2 Lives** | Depends on player choice |
## 🏆 How Lives Are Earned
| Action | Reward | Where |
|--------|--------|-------|
| Complete a Level | +1 Life | PageLevel → showSuccess() |
| Wrong Answer | 0 | No penalty |
| Time Up | 0 | No penalty |
## 📈 Level Progression
```
Storage Key: "game_progress" (localStorage)
Structure:
{
currentLevelIndex: number, // 0-based, current level
maxUnlockedLevelIndex: number // 0-based, highest reached
}
Default: { currentLevelIndex: 0, maxUnlockedLevelIndex: 0 }
```
### Progression Rules:
1. **Level 1 always unlocked** - Start here
2. **Beat Level N** → currentLevel becomes N+1
3. **Beat Level N** → maxUnlocked becomes max(maxUnlocked, N)
4. **Can replay earlier levels** - But always progress forward
### Methods:
```
getCurrentLevelIndex() → Get current (0-based)
setCurrentLevelIndex(n) → Jump to level
getMaxUnlockedLevelIndex() → Get highest reached
isLevelUnlocked(n) → Check if playable
onLevelCompleted(n) → Save win + progress
resetProgress() → Reset to level 1
```
## 🎨 Level Data (from API)
**Endpoint:** `https://ilookai.cn/api/v1/wechat-game/levels`
```typescript
ApiLevelData {
id: string, // UUID
level: number, // Level number (1-based display)
imageUrl: string, // Main puzzle image
hint1: string, // Free clue
hint2: string, // Costs 1 life
hint3: string, // Costs 1 life
answer: string, // The answer (case-sensitive, trimmed)
sortOrder: number // Sort order
}
```
## ⏱️ Gameplay Mechanics
### Time Limit
- **Duration:** 60 seconds per level
- **On Timeout:** Play fail sound, game doesn't end
- **After Timeout:** Can still submit answers
### Input System
- **Type:** Single text box (not per-character)
- **Processing:** Trimmed, case-sensitive comparison
- **Max Length:** Based on answer length
### Win Condition
```
input.trim() === answer
Play success sound → Stop timer → Award +1 life
→ Show PassModal → Save progress
```
### Lose Condition
```
input.trim() !== answer
Play fail sound → Vibrate → Show toast
→ Lives unchanged → Can retry
```
## 🎁 Rewards & Penalties
| Event | Lives Change | Other Effects |
|-------|--------------|---------------|
| Correct Answer | +1 | Play success sound, show modal |
| Wrong Answer | 0 | Play fail sound, vibrate, toast |
| Unlock Clue | -1 | Show clue content |
| Time Up | 0 | Play fail sound, countdown stops |
| Level Complete | Already +1ed | Save progress, move to next |
## 🔄 Economy Balance
```
Starting Inventory: 10 lives
Without Hints: +1 life/level → Infinite
With 1 Hint/Level: 0 lives/level → Stable
With 2 Hints/Level: -1 life/level → Finite (10-20 levels)
Net Formula: newLives = oldLives - hintsUsed + 1 (on win)
```
## 📡 API Integration
```
LevelDataManager {
API_URL: "https://ilookai.cn/api/v1/wechat-game/levels"
TIMEOUT: 8000ms
RETRY_COUNT: 2
Calls:
├─ initialize() → Load all level metadata + image for level 1
├─ ensureLevelReady(n) → Load specific level image
├─ preloadNextLevel(n) → Silently preload level n+1
└─ getLevelConfig(n) → Get cached level data
}
```
## 📁 Storage Schema
```
localStorage: {
"game_lives": "10",
"game_progress": "{\"currentLevelIndex\":0,\"maxUnlockedLevelIndex\":0}"
}
```
## 🌐 WeChat Integration
```
Features Used:
├─ WxSDK.initShare() → Enable sharing
├─ WxSDK.shareAppMessage() → Share to friend with level query param
├─ WxSDK.vibrateLong() → 400ms vibration on error
└─ WxSDK.vibrateShort() → 15ms vibration on click
```
## 🔑 Key Files
```
StorageManager.ts → Lives & progress persistence
LevelDataManager.ts → API & image loading
PageLevel.ts → Main game logic
PageLoading.ts → App initialization
PassModal.ts → Victory screen
ViewManager.ts → Page navigation
WxSDK.ts → WeChat APIs
```
## ⚙️ Constants
```
DEFAULT_LIVES 10
MIN_LIVES 0
LEVEL_TIME_LIMIT 60 seconds
LIFE_PER_HINT 1
LIFE_PER_WIN 1
API_TIMEOUT 8000ms
API_RETRY_COUNT 2
Game Title "写英语" (Write English)
Share Query Format "level=<levelIndex>"
```
## 🚨 No Implementation For:
- User Authentication (wx.login)
- Backend Progress Save
- Ads/Monetization
- Leaderboards
- Analytics
- Premium Life Refills
- Difficulty Levels
---
**In Summary:** Players earn/spend LIVES by unlocking clues (-1 each) or winning levels (+1 each). Progress is saved locally with streak tracking. The economy encourages players to solve without hints to maximize lives.

View File

@@ -1,416 +0,0 @@
# Game System Summary - Points/Score/Lives Analysis
**Project:** Cocos Creator WeChat Mini-Game ("写英语" - Write English)
**Date Analyzed:** April 5, 2026
**Analysis Scope:** Complete TypeScript codebase (21 source files)
---
## Executive Summary
This game uses a **LIVES-based economy** rather than traditional points/coins:
- **Currency:** Lives (renewable resource)
- **Starting Value:** 10 lives per new player
- **Earning Mechanic:** +1 life per completed level
- **Spending Mechanic:** -1 life per hint unlock (optional, 2 hints available)
- **Net Effect:** Players naturally gain lives if they solve without hints, stay stable with 1 hint/level, or lose lives if using both hints
The system is **fully persistent** (saved to localStorage) and **progression-aware** (can replay earlier levels while advancing).
---
## Files Analyzed (21 total)
### Core Game Logic (4 files)
1. **PageLevel.ts** (786 lines) - Main gameplay, hint system, answer validation
2. **LevelDataManager.ts** (312 lines) - API calls, level caching, image loading
3. **StorageManager.ts** (240 lines) - Lives & progress persistence
4. **PageLoading.ts** (88 lines) - App initialization, preloading
### UI/Navigation (3 files)
5. **ViewManager.ts** (320 lines) - Page stack management
6. **BaseView.ts** (132 lines) - View lifecycle
7. **PassModal.ts** (155 lines) - Victory screen with rewards
### Utilities (5 files)
8. **ToastManager.ts** (59 lines) - Toast notifications
9. **WxSDK.ts** (188 lines) - WeChat API wrapper
10. **HttpUtil.ts** (76 lines) - HTTP requests
11. **Toast.ts** (50 lines) - Toast display component
12. **LevelTypes.ts** (59 lines) - TypeScript interfaces
### Prefabs & Entry Points (3 files)
13. **PageHome.ts** (78 lines) - Home page
14. **main.ts** (59 lines) - App entry point
15. Plus UI utilities (BackgroundScaler, RoundedRectMask)
---
## The Complete Points/Lives Flow
### 🟢 EARNING Lives
| Event | Amount | Condition |
|-------|--------|-----------|
| Correct Answer | +1 | Submitted answer matches API answer exactly (case-sensitive, trimmed) |
| New Game | +10 | First time players only (default) |
| **Cannot Earn** | 0 | Wrong answers, timeouts, hint unlocks, returning players don't reset |
### 🔴 SPENDING Lives
| Event | Amount | Condition |
|--------|--------|-----------|
| Unlock Hint 2 | -1 | Player clicks unlock, must have lives > 0 |
| Unlock Hint 3 | -1 | Player clicks unlock, must have lives > 0 |
| No Other Costs | - | No penalties for wrong answers or timeouts |
### 📊 Net Economy Per Level
```
Best Case (No Hints): +1 life/level → Infinite scaling
Average Case (1 Hint): 0 lives/level → Stable indefinitely
Hard Case (2 Hints): -1 life/level → Runs out after 10-20 levels
```
---
## Data Persistence
### localStorage Keys
```javascript
{
"game_lives": "10", // Current lives count
"game_progress": "{ // Player progression
\"currentLevelIndex\": 0,
\"maxUnlockedLevelIndex\": 0
}"
}
```
### Lifespan
- **Initial:** Set when first loading game
- **Updates:** Every level completion or hint unlock
- **Persistence:** Survives app close/reopen (native browser storage)
- **Manual Reset:** Available via `StorageManager.resetAll()`
---
## Level Progression System
### Mechanics
1. **Level 1 Always Unlocked** - New players start here
2. **Sequential Unlocking** - Beat level N to unlock level N+1
3. **Non-Linear Progress** - Can replay earlier levels anytime
4. **Max Tracking** - Tracks highest level reached for achievements
### Data Structure
```typescript
{
currentLevelIndex: number, // Which level to play next (0-based)
maxUnlockedLevelIndex: number // Highest level player has beaten
}
```
### Example Progression
```
User beats Level 3 (index 2)
currentLevelIndex → 2+1 = 3 (level 4)
maxUnlockedLevelIndex → max(2, previous) = 2
Can now play levels 0-3
Must complete level 3 to unlock level 4
```
---
## Hint System
### Mechanics
- **Hint 1:** Always visible, completely free
- **Hint 2:** Hidden by default, costs 1 life to unlock
- **Hint 3:** Hidden by default, costs 1 life to unlock
### Implementation
```typescript
onUnlockClue(index: 2|3) {
if (!hasLives()) return; // Must have >= 1 life
consumeLife(); // Deduct 1 life
showClue(index); // Reveal the clue content
}
```
### Strategic Element
Players must decide if they want to spend lives for hints or solve blindly to accumulate more lives for future levels.
---
## Gameplay Loop
### Per-Level Sequence
1. Load level image from API cache
2. Display Hint 1 (free) + two "Unlock" buttons
3. Start 60-second countdown
4. Player can:
- Unlock hints by spending lives (optional, repeatable)
- Type their answer in single text box
- Submit answer
5. Outcome:
- **Correct:** Reward +1 life, show victory modal
- **Wrong:** No penalty, show error toast, can retry
- **Timeout:** Play fail sound, can still submit
### Victory Rewards
```
onSubmitAnswer(userAnswer) {
if (userAnswer.trim() === correctAnswer) {
playSuccessSound();
addLife(); // +1 life
StorageManager.onLevelCompleted(currentLevel);
showPassModal(); // Next/Share buttons
}
}
```
---
## API Integration
### Endpoint
```
GET https://ilookai.cn/api/v1/wechat-game/levels
```
### Response Format
```typescript
{
success: boolean,
message: string | null,
data: {
levels: [
{
id: "uuid",
level: 1,
imageUrl: "https://...",
hint1: "Free clue",
hint2: "Paid clue",
hint3: "Paid clue",
answer: "CORRECT_ANSWER",
sortOrder: 1
},
// ... more levels
],
total: number
}
}
```
### Reliability Features
- **Retry Logic:** 2 attempts on failure
- **Timeout:** 8 seconds per request
- **Fallback:** Shows error message if all retries fail
- **Caching:** All levels cached in memory after first load
---
## WeChat Integration
### Features Implemented
1. **Sharing** - Share game to friends with level parameter
2. **Vibration** - Haptic feedback on errors
3. **Platform Detection** - Gracefully degrade on non-WeChat platforms
### Share Implementation
```typescript
// On victory, user can click "Share"
WxSDK.shareAppMessage({
title: "快来一起玩这款游戏吧",
query: `level=${victoryLevelIndex}`
});
// Opens WeChat share dialog with referral link
```
### Not Implemented
- ❌ User authentication (wx.login)
- ❌ Backend progress sync
- ❌ Analytics
- ❌ Ads/Monetization
- ❌ Leaderboards
---
## User Journey Example
```
New User Opens Game
[Initialize: 10 lives, Level 1]
Attempt Level 1
├─ Unlocks Hint 2 (-1 life) → 9 lives
├─ Unlocks Hint 3 (-1 life) → 8 lives
├─ Submits Answer
├─ CORRECT
│ ├─ Awards +1 life → 9 lives
│ ├─ Saves progress (level → 2)
│ └─ Shows PassModal
│ ├─ Can click "Next Level" → Level 2
│ └─ Can click "Share" → wx.share
└─ WRONG
├─ No penalty → 9 lives unchanged
├─ Shows error toast
└─ Can retry immediately
```
---
## Loading & Initialization
### On App Start
1. **Detect New vs. Returning User**
- Check localStorage "game_lives" key
- New: Initialize to 10
- Returning: Restore value
2. **Load Level Metadata**
- API call to fetch all levels
- Retry up to 2 times on failure
- Cache all level data in memory
3. **Preload First Level**
- Download image for level 1
- Cache in memory for instant display
4. **Show Home Screen**
- Display "Start Game" button
- Initialize WeChat sharing
### On Subsequent Game Sessions
1. Restore lives from localStorage
2. Restore progress from localStorage
3. Resume at saved level
4. API already cached (no refetch unless app restarted)
---
## Key Constants
| Constant | Value | Usage |
|----------|-------|-------|
| DEFAULT_LIVES | 10 | New player starting amount |
| MIN_LIVES | 0 | Cannot go negative |
| LEVEL_TIME_LIMIT | 60 seconds | Countdown timer |
| API_TIMEOUT | 8000ms | HTTP request timeout |
| API_RETRY_COUNT | 2 | Attempts on failure |
| HINT_COST | 1 life each | Unlock clue 2 or 3 |
| WIN_REWARD | 1 life | Completion bonus |
| MODAL_Z_INDEX | 999 | Victory modal layer |
---
## Business Logic Highlights
### Win Condition
```typescript
userAnswer.trim() === correctAnswer
```
- Case-sensitive
- Whitespace trimmed
- Must match exactly
### No Lose Condition
- Wrong answers: No penalty
- Time up: No penalty
- Can retry infinitely on same level
### Safety Checks
- Can't unlock hints if lives ≤ 0
- Can't go below 0 lives
- Can't exceed total levels
- Invalid data resets to defaults
---
## Performance Optimizations
1. **Image Caching** - Downloaded images cached in memory
2. **Level Metadata Caching** - API data cached to avoid re-fetching
3. **Progress Caching** - localStorage data cached to reduce reads
4. **Async Preloading** - Next level preloaded silently
5. **Efficient UI** - Single EditBox (not per-character)
6. **Lazy Loading** - Pages cached after first load
---
## Missing Features / Gaps
| Feature | Status | Impact |
|---------|--------|--------|
| User Authentication | ❌ | No way to sync across devices |
| Backend Progress Save | ❌ | Progress lost if localStorage clears |
| Monetization | ❌ | No revenue stream |
| Ads | ❌ | No ad integration |
| Analytics | ❌ | Can't track player behavior |
| Leaderboards | ❌ | No competition mechanics |
| Sound Toggle | ❌ | Always plays sounds |
| Difficulty Levels | ❌ | All players see same levels |
---
## Testing Scenarios
### Test Case 1: Lives Economy
```
1. New game → 10 lives
2. Beat level without hints → 11 lives
3. Beat level with 1 hint → 11 lives (net 0)
4. Beat level with 2 hints → 10 lives (net -1)
→ Verify localStorage updated after each
```
### Test Case 2: Progression
```
1. Complete level 1
2. Verify currentLevel → 2
3. Verify maxUnlocked → 1
4. Restart app
5. Verify levels still saved
6. Replay level 1
7. Verify no duplicate progress
```
### Test Case 3: Hint Cost
```
1. Start with 10 lives
2. Unlock hint 2 → 9 lives displayed
3. Unlock hint 3 → 8 lives displayed
4. Can't unlock again (button inactive)
5. Wrong answer → 8 lives still
6. Correct answer → 9 lives (net -1)
```
---
## Conclusion
This is a **well-architected mini-game** with:
- ✅ Clear lives-based economy
- ✅ Persistent progress tracking
- ✅ Strategic hint system
- ✅ Reliable API integration
- ✅ Good error handling
- ✅ WeChat platform integration
The point system is **intentionally forgiving** - players who solve puzzles smartly gain lives indefinitely, while those using hints maintain stability. This encourages skill development without hard progress walls.
---
## Documentation Files Generated
1. **GAME_ANALYSIS.md** - Comprehensive 17-section analysis (13KB)
2. **QUICK_REFERENCE.md** - Quick lookup guide (5.6KB)
3. **POINTS_FLOW_DIAGRAM.md** - Visual flow diagrams (15KB)
4. **SUMMARY.md** - This executive summary (8KB)
**Total:** 42KB of detailed documentation covering every aspect of the points/score/lives system.

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -176,7 +176,7 @@
"a": 255
},
"_spriteFrame": {
"__uuid__": "d532045e-55f8-47c2-9493-b918e18364b0@f9941",
"__uuid__": "10047f2e-bf2c-46f0-ba98-6fc23cf7ea87@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
@@ -597,8 +597,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 80,
"height": 50.4
"width": 88,
"height": 58.4
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -646,19 +646,19 @@
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": false,
"_isBold": true,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_enableOutline": true,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"r": 72,
"g": 158,
"b": 35,
"a": 255
},
"_outlineWidth": 2,
"_outlineWidth": 4,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",

View File

@@ -1,8 +1,8 @@
import { _decorator, Component, ProgressBar, Label } from 'cc';
import { _decorator, Component, ProgressBar, Label, assetManager } from 'cc';
import type { AssetManager } from 'cc';
import { ViewManager } from './scripts/core/ViewManager';
import { LevelDataManager } from './scripts/utils/LevelDataManager';
import { AuthManager } from './scripts/utils/AuthManager';
import { StorageManager } from './scripts/utils/StorageManager';
import { ShareManager } from './scripts/utils/ShareManager';
import { WxSDK } from './scripts/utils/WxSDK';
const { ccclass, property } = _decorator;
@@ -10,10 +10,12 @@ const { ccclass, property } = _decorator;
/**
* 页面加载组件
* 负责用户登录、预加载资源并显示加载进度
* 登录与关卡数据加载并行执行以减少等待时间
* 流程:登录 + game-data → 拿到 nextLevel → 加载首关图片 → 进入首页
*/
@ccclass('PageLoading')
export class PageLoading extends Component {
private static readonly FONT_BUNDLE_NAME = 'fonts';
@property(ProgressBar)
progressBar: ProgressBar | null = null;
@@ -31,15 +33,11 @@ export class PageLoading extends Component {
this._updateStatusLabel('正在加载...');
// 登录和关卡数据并行加载
const [loginSuccess, levelSuccess] = await Promise.all([
AuthManager.instance.initialize(),
LevelDataManager.instance.initialize((progress, message) => {
// 关卡加载占 0-80% 进度
this._updateProgress(progress);
this._updateStatusLabel(message);
}),
]);
// 阶段1: 登录 + 获取 game-data含 nextLevel
this._updateProgress(0);
this._updateStatusLabel('正在连接服务器...');
const loginSuccess = await AuthManager.instance.initialize();
if (loginSuccess) {
console.log('[PageLoading] 用户登录成功');
@@ -47,14 +45,38 @@ export class PageLoading extends Component {
console.warn('[PageLoading] 登录失败,继续离线模式');
}
this._updateProgress(0.2);
// 阶段2: 加载首关图片(如果有 nextLevel
const nextLevel = AuthManager.instance.nextLevel;
let levelSuccess = false;
if (nextLevel) {
levelSuccess = await LevelDataManager.instance.initialize(nextLevel, (progress, message) => {
// 关卡图片加载占 20%-80% 进度
this._updateProgress(0.2 + progress * 0.6);
this._updateStatusLabel(message);
});
if (!levelSuccess) {
this._updateStatusLabel('资源加载失败,请重新打开游戏');
return;
}
} else if (loginSuccess) {
// nextLevel 为 null → 全部通关(或服务端无关卡)
console.log('[PageLoading] 全部通关或无可用关卡');
this._updateProgress(0.8);
} else {
// 登录失败且没有 nextLevel
this._updateStatusLabel('加载失败,请重新打开游戏');
return;
}
// 登录 + 关卡数据都就绪后,用服务端进度覆盖本地进度
if (loginSuccess) {
this._syncProgressFromServer();
// 阶段3: 加载字体分包
const fontSuccess = await this._loadFontBundle();
if (!fontSuccess) {
this._updateStatusLabel('字体资源加载失败,请重新打开游戏');
return;
}
// 检测分享码:从微信启动参数中获取
@@ -77,10 +99,10 @@ export class PageLoading extends Component {
console.warn('[PageLoading] 加入分享失败,进入正常模式');
}
// 正常流程:预加载 PageHome (80-100%)
// 正常流程:预加载 PageHome (82-100%)
ViewManager.instance.preload('PageHome',
(progress) => {
this._updateProgress(0.8 + progress * 0.2);
this._updateProgress(0.82 + progress * 0.18);
this._updateStatusLabel('正在加载界面资源...');
},
() => {
@@ -113,28 +135,32 @@ export class PageLoading extends Component {
}
/**
* 用服务端通关进度覆盖本地进度
* 将 completedLevelIds 转换为本地的 currentLevelIndex / maxUnlockedLevelIndex
* 加载字体分包,避免字体资源进入小游戏主包
*/
private _syncProgressFromServer(): void {
const completedIds = AuthManager.instance.completedLevelIds;
if (completedIds.length === 0) {
console.log('[PageLoading] 服务端无通关记录,使用本地进度');
private _loadFontBundle(): Promise<boolean> {
const bundleName = PageLoading.FONT_BUNDLE_NAME;
const cachedBundle = assetManager.getBundle(bundleName);
if (cachedBundle) {
console.log(`[PageLoading] 字体分包已加载: ${bundleName}`);
this._updateProgress(0.82);
return Promise.resolve(true);
}
this._updateStatusLabel('正在加载字体资源...');
this._updateProgress(0.8);
return new Promise((resolve) => {
assetManager.loadBundle(bundleName, (err: Error | null, bundle: AssetManager.Bundle | null) => {
if (err || !bundle) {
console.error(`[PageLoading] 字体分包加载失败: ${bundleName}`, err);
resolve(false);
return;
}
const maxCompletedIndex = LevelDataManager.instance.getMaxCompletedIndex(completedIds);
if (maxCompletedIndex < 0) {
return;
}
const localMax = StorageManager.getMaxUnlockedLevelIndex();
// 取服务端和本地的较大值,防止进度回退
if (maxCompletedIndex > localMax) {
// onLevelCompleted 会同时设置 currentLevelIndex = maxCompletedIndex + 1 和 maxUnlockedLevelIndex
StorageManager.onLevelCompleted(maxCompletedIndex);
console.log(`[PageLoading] 服务端进度同步:已通关到第 ${maxCompletedIndex + 1}`);
}
console.log(`[PageLoading] 字体分包加载完成: ${bundleName}`);
this._updateProgress(0.82);
resolve(true);
});
});
}
}

11
assets/fonts.meta Normal file
View File

@@ -0,0 +1,11 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "fc1f44af-699f-467c-ba2d-f54994551c4d",
"files": [],
"subMetas": {},
"userData": {
"isBundle": true
}
}

BIN
assets/fonts/Game011_3.ttf Normal file

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"ver": "1.0.1",
"importer": "ttf-font",
"imported": true,
"uuid": "fb4acba6-6bc7-4eb3-be34-8f2ac9823a80",
"files": [
".json",
"Game011_3.ttf"
],
"subMetas": {},
"userData": {}
}

9
assets/levels.meta Normal file
View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "0b928321-a809-4339-8af8-5a053aeda2d5",
"files": [],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,10 @@
import { _decorator, Node, Button } from 'cc';
import { _decorator, Node, Button, Label, tween, Vec3, UIOpacity, UITransform, Color, instantiate } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { WxSDK, checkPrivacySetting, requirePrivacyAuthorize } from 'db://assets/scripts/utils/WxSDK';
import { StaminaManager } from 'db://assets/scripts/utils/StaminaManager';
import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
import { StaminaInfo } from 'db://assets/scripts/types/ApiTypes';
const { ccclass, property } = _decorator;
/**
@@ -10,12 +13,34 @@ const { ccclass, property } = _decorator;
*/
@ccclass('PageHome')
export class PageHome extends BaseView {
/** 默认体力上限 */
private static readonly DEFAULT_STAMINA_MAX = 50;
@property({ type: Node, tooltip: '开始游戏按钮' })
startGameBtn: Node | null = null;
@property({ type: Node, tooltip: 'PK按钮' })
pkBtn: Node | null = null;
/** 体力值显示标签 */
@property(Label)
liveLabel: Label | null = null;
/** 飞行动画持续时间(秒) */
private static readonly FLY_DURATION = 0.5;
/** 到达后弹跳持续时间(秒) */
private static readonly BOUNCE_DURATION = 0.15;
/** 浮动文本动画持续时间(秒) */
private static readonly FLOAT_DURATION = 0.8;
/** 浮动文本上移距离 */
private static readonly FLOAT_OFFSET_Y = 120;
/** 是否正在播放体力消耗动画 */
private _isAnimating: boolean = false;
/**
* 页面首次加载时调用
*/
@@ -76,8 +101,29 @@ export class PageHome extends BaseView {
* 开始游戏按钮点击回调
*/
private _onStartGameClick(): void {
if (this._isAnimating) return;
console.log('[PageHome] 开始游戏按钮点击');
// 体力检查
if (!StaminaManager.instance.hasStamina()) {
ToastManager.show('体力不足,请等待恢复');
return;
}
this._isAnimating = true;
this._playStaminaCostAnimation()
.then(() => {
ViewManager.instance.open('PageLevel');
})
.catch(err => {
console.error('[PageHome] 体力消耗动画异常:', err);
// 异常兜底:直接进入关卡
ViewManager.instance.open('PageLevel');
})
.finally(() => {
this._isAnimating = false;
});
}
/**
@@ -88,11 +134,198 @@ export class PageHome extends BaseView {
ViewManager.instance.open('PageWriteLevels');
}
// ========== 体力消耗动画 ==========
/**
* 通过节点路径查找 IconLive
*/
private _findIconLive(): Node | null {
return this.node
.getChildByName('TopLayout')
?.getChildByName('Live')
?.getChildByName('IconLive') ?? null;
}
/**
* 将节点的世界坐标转换为目标父节点的本地坐标
*/
private _worldToLocal(worldPos: Vec3, parent: Node): Vec3 {
const parentTransform = parent.getComponent(UITransform);
if (!parentTransform) return worldPos;
return parentTransform.convertToNodeSpaceAR(worldPos);
}
/**
* 获取节点的世界坐标
*/
private _getWorldPos(node: Node): Vec3 {
const transform = node.getComponent(UITransform);
if (!transform) return node.worldPosition.clone();
return transform.convertToWorldSpaceAR(Vec3.ZERO);
}
/**
* 播放体力消耗动画
* 1. 克隆 IconLive 飞向 StarGame 按钮
* 2. 到达后弹跳
* 3. "体力值-1" 浮动文本上移渐隐
* 4. 同步更新体力数字
*/
private _playStaminaCostAnimation(): Promise<void> {
return new Promise<void>((resolve) => {
const iconLive = this._findIconLive();
const targetBtn = this.startGameBtn;
if (!iconLive || !targetBtn) {
console.warn('[PageHome] 动画节点未找到,跳过动画');
resolve();
return;
}
// --- 坐标计算 ---
const iconWorldPos = this._getWorldPos(iconLive);
const targetWorldPos = this._getWorldPos(targetBtn);
const rootNode = this.node;
// 起始和终点在 root 本地空间的坐标
const startLocal = this._worldToLocal(iconWorldPos, rootNode);
const endLocal = this._worldToLocal(targetWorldPos, rootNode);
// --- 克隆飞行节点 ---
const flyNode = instantiate(iconLive);
flyNode.name = '_flyIcon';
flyNode.setPosition(startLocal);
// 保持与原始 IconLive 相同的缩放
flyNode.setScale(iconLive.worldScale.clone());
rootNode.addChild(flyNode);
// 隐藏原始 IconLive
iconLive.active = false;
// --- 飞行路径(带弧度的抛物线效果) ---
// 中间控制点x 取中点y 取较高值 + 偏移形成弧线
const midX = (startLocal.x + endLocal.x) / 2;
const midY = Math.max(startLocal.y, endLocal.y) + 150;
const midPoint = new Vec3(midX, midY, 0);
// 阶段1飞到弧线顶点
const halfDuration = PageHome.FLY_DURATION / 2;
tween(flyNode)
.to(halfDuration, { position: midPoint }, { easing: 'quadOut' })
.to(halfDuration, { position: endLocal }, { easing: 'quadIn' })
// 阶段2到达弹跳
.to(PageHome.BOUNCE_DURATION / 2, { scale: new Vec3(0.4, 0.4, 1) }, { easing: 'quadOut' })
.to(PageHome.BOUNCE_DURATION / 2, { scale: new Vec3(0.3, 0.3, 1) }, { easing: 'quadIn' })
.call(() => {
// 飞行完成 — flyNode 使命结束,立即清理
flyNode.destroy();
iconLive.active = true;
// 创建浮动文本
this._showFloatText(targetBtn, rootNode);
// 乐观更新体力数字
this._optimisticUpdateStamina();
// 等浮动文本播完再 resolve用 setTimeout不依赖已销毁节点的 tween
setTimeout(() => resolve(), PageHome.FLOAT_DURATION * 1000);
})
.start();
});
}
/**
* 显示浮动提示文本 "体力值-1"
* 从按钮位置向上漂移并渐隐
*/
private _showFloatText(anchorNode: Node, parentNode: Node): void {
// 创建文本节点
const textNode = new Node('_floatText');
textNode.addComponent(UITransform);
const label = textNode.addComponent(Label);
label.string = '体力值-1';
label.fontSize = 36;
label.lineHeight = 40;
label.color = new Color(255, 80, 80, 255);
label.isBold = true;
// 复用 liveLabel 的字体(如果有)
if (this.liveLabel?.font) {
label.font = this.liveLabel.font;
}
// 添加 UIOpacity 用于渐隐
const opacity = textNode.addComponent(UIOpacity);
opacity.opacity = 255;
// 定位到按钮上方
const anchorWorldPos = this._getWorldPos(anchorNode);
const localPos = this._worldToLocal(anchorWorldPos, parentNode);
// 起始位置在按钮上方偏移
localPos.y += 120;
textNode.setPosition(localPos);
parentNode.addChild(textNode);
// 向上漂移 + 渐隐
const floatTarget = new Vec3(localPos.x, localPos.y + PageHome.FLOAT_OFFSET_Y, 0);
tween(textNode)
.to(PageHome.FLOAT_DURATION, { position: floatTarget }, { easing: 'quadOut' })
.call(() => {
textNode.destroy();
})
.start();
tween(opacity)
.delay(PageHome.FLOAT_DURATION * 0.3)
.to(PageHome.FLOAT_DURATION * 0.7, { opacity: 0 }, { easing: 'quadIn' })
.start();
}
/**
* 乐观更新体力标签(本地 -1 预扣显示)
*/
private _optimisticUpdateStamina(): void {
if (!this.liveLabel) return;
const stamina = StaminaManager.instance.getStamina();
const maxStamina = this._getStaminaMax(stamina);
const displayCurrent = Math.max(0, stamina.current - 1);
this.liveLabel.string = `${displayCurrent}/${maxStamina}`;
}
/**
* 页面每次显示时调用
*/
onViewShow(): void {
console.log('[PageHome] onViewShow');
this.updateStaminaLabel();
// 保险恢复:防止动画中途被打断导致 IconLive 隐藏残留
const iconLive = this._findIconLive();
if (iconLive && !iconLive.active) {
iconLive.active = true;
}
}
/**
* 获取体力上限
*/
private _getStaminaMax(stamina: StaminaInfo): number {
return typeof stamina.max === 'number' ? stamina.max : PageHome.DEFAULT_STAMINA_MAX;
}
/**
* 更新体力值显示
*/
private updateStaminaLabel(): void {
if (this.liveLabel) {
const stamina = StaminaManager.instance.getStamina();
const maxStamina = this._getStaminaMax(stamina);
this.liveLabel.string = `${stamina.current}/${maxStamina}`;
}
}
/**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,9 @@
import { _decorator, Node, Button } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { CreatedShareItem } from 'db://assets/scripts/types/ApiTypes';
import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
const { ccclass, property } = _decorator;
@ccclass('PagePKData')
@@ -8,6 +11,9 @@ export class PagePKData extends BaseView {
@property({ type: Node, tooltip: '返回按钮' })
backBtn: Node | null = null;
private _createdShares: CreatedShareItem[] = [];
private _isLoading: boolean = false;
onViewLoad(): void {
if (this.backBtn) {
this.backBtn.on(Button.EventType.CLICK, this._onBackClick, this);
@@ -15,6 +21,7 @@ export class PagePKData extends BaseView {
}
onViewShow(): void {
void this._loadCreatedShares();
}
onViewHide(): void {
@@ -24,6 +31,26 @@ export class PagePKData extends BaseView {
ViewManager.instance.back();
}
private async _loadCreatedShares(): Promise<void> {
if (this._isLoading) {
return;
}
this._isLoading = true;
try {
const items = await ShareManager.instance.fetchCreatedShares();
if (!items) {
ToastManager.instance.show('获取挑战列表失败,请稍后重试');
return;
}
this._createdShares = items;
console.log('[PagePKData] 我创建的挑战列表:', this._createdShares);
} finally {
this._isLoading = false;
}
}
onViewDestroy(): void {
if (this.backBtn) {
this.backBtn.off(Button.EventType.CLICK, this._onBackClick, this);

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,22 @@
import { _decorator, Node, Button, Sprite, Label, ScrollView, instantiate, UITransform, Vec2 } from 'cc';
import { _decorator, Node, Button, Label, ScrollView, instantiate, UITransform } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
import { CompletedLevelsManager } from 'db://assets/scripts/utils/CompletedLevelsManager';
import { PreviewLevelItem } from './PreviewLevelItem';
const { ccclass, property } = _decorator;
/**
* 预览试卷页面
* 垂直滚动展示用户在 PageWriteLevels 中选中的 6 个关卡
* 每个关卡展示:封面图、提示1、提示2、答案
* 每个关卡展示:封面图、线索1、线索2、线索3、答案
*
* prefab 节点结构(已在编辑器中搭建:
* 节点结构(仅 ScrollView 侧需要固定:
* PagePreviewLevels
* ├── Bg
* ── IconBack ← backBtn (返回按钮)
* ├── PKTitle ← 标题 "挑战"
* ├── ScrollView ← scrollView
* │ ├── scrollBar
* │ └── view
* │ └── content ← listContent
* ├── ListTpl ← listTemplate (关卡模板)
* │ ├── LevelCover ← Sprite 封面图
* │ ├── Tips1 ← Label 提示1
* │ ├── Tips2 ← Label 提示2
* │ └── Answer ← Label 答案
* └── BackButton ← backButton (底部返回按钮)
* ├── ScrollView / view / content ← listContent 容器
* ── ListTpl ← listTemplate 模板根节点
* (挂 PreviewLevelItem 组件,字段由编辑器拖拽绑定)
*
* item 内部节点层级/命名对本文件透明:所有引用都来自 PreviewLevelItem 的 @property。
*/
/** 布局配置 — 垂直列表 */
@@ -197,44 +190,26 @@ export class PagePreviewLevels extends BaseView {
* 异步加载关卡数据并填充到 item 节点
*/
private async _loadLevelData(item: Node, levelIndex: number, displayIndex: number): Promise<void> {
const config = await LevelDataManager.instance.ensureLevelReady(levelIndex);
if (!config || !item.isValid) return;
const level = CompletedLevelsManager.instance.getByIndex(levelIndex);
if (!level || !item.isValid) return;
// 填充封面图
const levelCover = item.getChildByName('LevelCover');
if (levelCover) {
const sprite = levelCover.getComponent(Sprite);
if (sprite && config.spriteFrame) {
sprite.spriteFrame = config.spriteFrame;
}
const view = item.getComponent(PreviewLevelItem);
if (!view) {
console.warn('[PagePreviewLevels] listTemplate 缺少 PreviewLevelItem 组件');
return;
}
// 填充提示1
const tips1 = item.getChildByName('Tips1');
if (tips1) {
const label = tips1.getComponent(Label);
if (label) {
label.string = `线索一:${config.clue1 || ''}`;
}
}
view.setTexts({
answer: level.answer || '',
hint1: level.hint1 || '',
hint2: level.hint2 || '',
hint3: level.hint3 || '',
});
// 填充提示2
const tips2 = item.getChildByName('Tips2');
if (tips2) {
const label = tips2.getComponent(Label);
if (label) {
label.string = `线索二:${config.clue2 || ''}`;
}
}
// 填充答案
const answer = item.getChildByName('Answer');
if (answer) {
const label = answer.getComponent(Label);
if (label) {
label.string = `答案:${config.answer || ''}`;
}
}
// 异步加载封面图(通常已由 WriteLevels 预热到缓存)
const spriteFrame = await CompletedLevelsManager.instance.loadImage(level.image1Url);
if (!spriteFrame || !item.isValid) return;
view.setCover(spriteFrame);
}
// ─── 事件处理 ───────────────────────────────────────

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { _decorator, Node, Button, Sprite, Label, Toggle, ScrollView, EditBox, instantiate, UITransform, Vec2, EventTouch } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { ViewManager } from 'db://assets/scripts/core/ViewManager';
import { LevelDataManager } from 'db://assets/scripts/utils/LevelDataManager';
import { CompletedLevelsManager } from 'db://assets/scripts/utils/CompletedLevelsManager';
import { ToastManager } from 'db://assets/scripts/utils/ToastManager';
import { ShareManager } from 'db://assets/scripts/utils/ShareManager';
import { StorageManager } from 'db://assets/scripts/utils/StorageManager';
@@ -9,28 +9,27 @@ import { WxSDK, getUserProfile } from 'db://assets/scripts/utils/WxSDK';
import { AuthManager } from 'db://assets/scripts/utils/AuthManager';
import { API_ENDPOINTS, API_TIMEOUT } from 'db://assets/scripts/config/ApiConfig';
import { HttpUtil } from 'db://assets/scripts/utils/HttpUtil';
import { ApiEnvelope } from 'db://assets/scripts/types/ApiTypes';
const { ccclass, property } = _decorator;
/**
* 布局配置
* 基于实际 prefab 尺寸计算:
* ScrollView / view 宽 900高 1300
* view (ScrollView 的可视窗口) 宽 900高 1000anchor (0.5, 1) 顶部中点
* ListTpl (item) 宽 300高 400
*
* 水平居中2 × 300 + 1 × 40 = 640, padding_left = (900 - 640) / 2 = 130
* 垂直居中3 × 400 + 2 × 25 = 1250, padding_top = (1300 - 1250) / 2 = 25
* 每页 2 行 × 2 列 = 4 个关卡
* PADDING 不再手写,由 VIEW / ITEM / SPACING 自动派生(见 _getHorizontalPadding / _getVerticalPadding
* 保证 item 网格在 view 内始终居中。改 SPACING 时不用再算 PADDING。
*/
const LAYOUT_CONFIG = {
COLS: 2,
ROWS: 3,
ROWS: 2,
ITEM_WIDTH: 300,
ITEM_HEIGHT: 400,
SPACING_X: 40,
SPACING_Y: 25,
PADDING_LEFT: 130,
PADDING_TOP: 25,
SPACING_X: 160,
SPACING_Y: 180,
VIEW_WIDTH: 900,
VIEW_HEIGHT: 1300,
VIEW_HEIGHT: 1000,
};
/** 必须选择的关卡数量 */
@@ -140,7 +139,7 @@ export class PageWriteLevels extends BaseView {
}
}
private _onTouchStart(event: EventTouch): void {
private _onTouchStart(_event: EventTouch): void {
if (!this._scrollViewComp) return;
this._touchStartOffsetX = this._scrollViewComp.getScrollOffset().x;
this._touchStartTime = Date.now();
@@ -149,7 +148,7 @@ export class PageWriteLevels extends BaseView {
/**
* 触摸结束:根据滑动距离和速度决定翻页方向
*/
private _onTouchEnd(event: EventTouch): void {
private _onTouchEnd(_event: EventTouch): void {
if (!this._scrollViewComp || this._totalPages <= 1) return;
const currentOffsetX = this._scrollViewComp.getScrollOffset().x;
@@ -176,18 +175,27 @@ export class PageWriteLevels extends BaseView {
console.log('[PageWriteLevels] onViewShow');
// 仅首次初始化列表,从预览页返回时保留选中状态
if (this._itemNodes.length === 0) {
this._initLevelList();
void this._initLevelList();
}
}
private _initLevelList(): void {
private async _initLevelList(): Promise<void> {
this._clearList();
this._levelCount = LevelDataManager.instance.getLevelCount();
console.log('[PageWriteLevels] 关卡总数:', this._levelCount);
// 拉取当前用户所有已通关关卡
const levels = await CompletedLevelsManager.instance.fetch();
if (levels === null) {
console.warn('[PageWriteLevels] 获取已通关关卡失败');
ToastManager.instance.show('获取关卡列表失败,请稍后重试');
return;
}
this._levelCount = levels.length;
console.log('[PageWriteLevels] 已通关关卡总数:', this._levelCount);
if (this._levelCount === 0) {
console.warn('[PageWriteLevels] 没有关卡数据');
console.warn('[PageWriteLevels] 用户尚未通关任何关卡');
ToastManager.instance.show('还没有已通关的关卡,快去玩几关吧');
return;
}
@@ -212,6 +220,25 @@ export class PageWriteLevels extends BaseView {
this._selectedIndices.clear();
}
/**
* 水平 padding让整行 item 在 view 宽度内居中
* padding_left = (VIEW_WIDTH - cols*ITEM_WIDTH - (cols-1)*SPACING_X) / 2
*/
private _getHorizontalPadding(): number {
const { VIEW_WIDTH, COLS, ITEM_WIDTH, SPACING_X } = LAYOUT_CONFIG;
const gridWidth = COLS * ITEM_WIDTH + (COLS - 1) * SPACING_X;
return (VIEW_WIDTH - gridWidth) / 2;
}
/**
* 垂直 padding让整列 item 在 view 高度内居中
*/
private _getVerticalPadding(): number {
const { VIEW_HEIGHT, ROWS, ITEM_HEIGHT, SPACING_Y } = LAYOUT_CONFIG;
const gridHeight = ROWS * ITEM_HEIGHT + (ROWS - 1) * SPACING_Y;
return (VIEW_HEIGHT - gridHeight) / 2;
}
private _updateContentSize(): void {
if (!this.listContent) return;
@@ -221,11 +248,12 @@ export class PageWriteLevels extends BaseView {
const totalWidth = this._totalPages * LAYOUT_CONFIG.VIEW_WIDTH;
contentTransform.setContentSize(totalWidth, LAYOUT_CONFIG.VIEW_HEIGHT);
// anchor=(0,1)将 content 左上角对齐到 view 左上角
// content anchor=(0,1)view anchor=(0.5,1)。
// view 本地坐标系下view 的左上角 = (-viewWidth/2, 0)。
// content 的 anchor 点(左上角)需要贴到 view 的左上角。
if (this._viewTransform) {
const viewWidth = this._viewTransform.contentSize.width;
const viewHeight = this._viewTransform.contentSize.height;
this.listContent.setPosition(-viewWidth / 2, viewHeight / 2, 0);
this.listContent.setPosition(-viewWidth / 2, 0, 0);
}
}
@@ -275,12 +303,15 @@ export class PageWriteLevels extends BaseView {
const col = localIndex % LAYOUT_CONFIG.COLS;
const row = Math.floor(localIndex / LAYOUT_CONFIG.COLS);
const paddingLeft = this._getHorizontalPadding();
const paddingTop = this._getVerticalPadding();
const x = pageIndex * LAYOUT_CONFIG.VIEW_WIDTH
+ LAYOUT_CONFIG.PADDING_LEFT
+ paddingLeft
+ col * (LAYOUT_CONFIG.ITEM_WIDTH + LAYOUT_CONFIG.SPACING_X)
+ LAYOUT_CONFIG.ITEM_WIDTH / 2;
const y = -(LAYOUT_CONFIG.PADDING_TOP
const y = -(paddingTop
+ row * (LAYOUT_CONFIG.ITEM_HEIGHT + LAYOUT_CONFIG.SPACING_Y)
+ LAYOUT_CONFIG.ITEM_HEIGHT / 2);
@@ -291,11 +322,12 @@ export class PageWriteLevels extends BaseView {
* 初始化 item 的默认名称和选中状态(不设置封面,由异步加载负责)
*/
private _initItemState(item: Node, index: number): void {
const level = CompletedLevelsManager.instance.getByIndex(index);
const levelName = item.getChildByName('LevelName');
if (levelName) {
const label = levelName.getComponent(Label);
if (label) {
label.string = `${index + 1}`;
label.string = level ? `${level.level}` : `${index + 1}`;
}
}
@@ -315,27 +347,20 @@ export class PageWriteLevels extends BaseView {
}
/**
* 异步加载关卡资源并刷新封面图和名称。
* LevelDataManager 采用懒加载,初始化时只加载了第一关图片,
* 其余关卡通过 ensureLevelReady 按需加载。
* 异步加载关卡封面图并填充到 item
*/
private async _loadAndRefreshCover(item: Node, index: number): Promise<void> {
const config = await LevelDataManager.instance.ensureLevelReady(index);
if (!config || !item.isValid) return;
const level = CompletedLevelsManager.instance.getByIndex(index);
if (!level || !item.isValid) return;
const spriteFrame = await CompletedLevelsManager.instance.loadImage(level.image1Url);
if (!spriteFrame || !item.isValid) return;
const levelCover = item.getChildByName('LevelCover');
if (levelCover) {
const sprite = levelCover.getComponent(Sprite);
if (sprite && config.spriteFrame) {
sprite.spriteFrame = config.spriteFrame;
}
}
const levelName = item.getChildByName('LevelName');
if (levelName) {
const label = levelName.getComponent(Label);
if (label) {
label.string = config.name;
if (sprite) {
sprite.spriteFrame = spriteFrame;
}
}
}
@@ -551,9 +576,9 @@ export class PageWriteLevels extends BaseView {
const ids: string[] = [];
const sortedIndices = Array.from(this._selectedIndices).sort((a, b) => a - b);
for (const index of sortedIndices) {
const config = LevelDataManager.instance.getLevelConfig(index);
if (config) {
ids.push(config.id);
const level = CompletedLevelsManager.instance.getByIndex(index);
if (level) {
ids.push(level.id);
}
}
return ids;
@@ -589,7 +614,7 @@ export class PageWriteLevels extends BaseView {
StorageManager.setUserInfo(userInfo);
// 上传到服务端
const response = await HttpUtil.post(
const response = await HttpUtil.post<ApiEnvelope<unknown>>(
API_ENDPOINTS.USER_INFO,
{
userId: userId,

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { _decorator, Node, Label, AudioClip, AudioSource, view, UITransform, Size } from 'cc';
import { BaseView } from 'db://assets/scripts/core/BaseView';
import { _decorator, Node, Label, AudioClip, AudioSource, view, UITransform, Size, ProgressBar } from 'cc';
import { BaseModal } from 'db://assets/scripts/core/BaseModal';
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
const { ccclass, property } = _decorator;
@@ -13,12 +13,23 @@ export interface PassModalCallbacks {
onShare?: () => void;
}
export interface PassModalTitleInfo {
titleText?: string;
nextTitleProgress?: number;
progressText?: string;
}
interface PassModalParams {
levelIndex?: number;
titleInfo?: PassModalTitleInfo;
}
/**
* 通关弹窗组件
* 继承 BaseView,显示通关成功弹窗,提供"下一关"和"分享给好友"两个按钮
* 继承 BaseModal,显示通关成功弹窗,提供"下一关"和"分享给好友"两个按钮
*/
@ccclass('PassModal')
export class PassModal extends BaseView {
export class PassModal extends BaseModal {
/** 静态常量:弹窗层级 */
public static readonly MODAL_Z_INDEX = 999;
@@ -30,9 +41,17 @@ export class PassModal extends BaseView {
@property(Node)
shareButton: Node | null = null;
/** 提示Label如 +1 生命) */
/** 称号文字 */
@property(Label)
tipLabel: Label | null = null;
titleLevelLabel: Label | null = null;
/** 距离下一个称号的进度 */
@property(ProgressBar)
titleProgressBar: ProgressBar | null = null;
/** 进度提示文案 */
@property(Label)
progressLabel: Label | null = null;
/** 通关音效 */
@property(AudioClip)
@@ -44,6 +63,21 @@ export class PassModal extends BaseView {
/** 缓存的屏幕尺寸 */
private _screenSize: Size | null = null;
/** 称号展示数据 */
private _titleInfo: PassModalTitleInfo = {
titleText: '冷场小白1级',
nextTitleProgress: 0,
progressText: '还差3题获得冷场小白2级'
};
setParams(params: PassModalParams): void {
super.setParams(params);
if (params?.titleInfo) {
this.setTitleInfo(params.titleInfo);
}
}
/**
* 设置回调函数
*/
@@ -51,6 +85,17 @@ export class PassModal extends BaseView {
this._callbacks = callbacks;
}
/**
* 设置称号体系展示数据
*/
setTitleInfo(titleInfo: PassModalTitleInfo): void {
this._titleInfo = {
...this._titleInfo,
...titleInfo
};
this._updateTitleInfo();
}
/**
* 页面首次加载时调用
*/
@@ -63,7 +108,9 @@ export class PassModal extends BaseView {
* 页面每次显示时调用
*/
onViewShow(): void {
super.onViewShow();
this._updateWidget();
this._updateTitleInfo();
this._playSuccessSound();
}
@@ -129,6 +176,37 @@ export class PassModal extends BaseView {
}
}
/**
* 更新称号体系核心变量
*/
private _updateTitleInfo(): void {
if (this.titleLevelLabel && this._titleInfo.titleText !== undefined) {
this.titleLevelLabel.string = this._titleInfo.titleText;
}
if (this.titleProgressBar && this._titleInfo.nextTitleProgress !== undefined) {
this.titleProgressBar.progress = this._normalizeProgress(this._titleInfo.nextTitleProgress);
}
if (this.progressLabel && this._titleInfo.progressText !== undefined) {
this.progressLabel.string = this._titleInfo.progressText;
}
}
/**
* 规范化进度值
* 九宫格 Bar 的 Left+Right border = 240pxtotalLength = 925px
* 当 width < 240px 时圆角会畸变,因此 progress > 0 时强制最小值
*/
private _normalizeProgress(progress: number): number {
if (!Number.isFinite(progress) || progress <= 0) {
return 0;
}
const MIN_PROGRESS = 240 / 925;
return Math.max(MIN_PROGRESS, Math.min(1, progress));
}
/**
* 下一关按钮点击
*/

View File

@@ -0,0 +1,47 @@
import { _decorator, Component, Node, Sprite, Label, SpriteFrame } from 'cc';
const { ccclass, property } = _decorator;
/**
* 预览页单个关卡 item 的视图组件
* 挂在 PagePreviewLevels 的 listTemplate 根节点上,由编辑器拖拽绑定子节点。
* instantiate 克隆 item 时Cocos 会把这些 Node 引用自动重映射到克隆后的子节点,
* 所以每个 item 拿到的都是自己的封面/标签引用,与节点层级/命名解耦。
*/
@ccclass('PreviewLevelItem')
export class PreviewLevelItem extends Component {
@property({ type: Sprite, tooltip: '封面图 Sprite' })
levelCover: Sprite | null = null;
@property({ type: Label, tooltip: '答案 Label' })
answerLabel: Label | null = null;
@property({ type: Label, tooltip: '线索1 Label' })
tips1Label: Label | null = null;
@property({ type: Label, tooltip: '线索2 Label' })
tips2Label: Label | null = null;
@property({ type: Label, tooltip: '线索3 Label' })
tips3Label: Label | null = null;
/**
* 一次性设置所有文本字段
*/
setTexts(opts: {
answer: string;
hint1: string;
hint2: string;
hint3: string;
}): void {
if (this.answerLabel) this.answerLabel.string = `答案:${opts.answer}`;
if (this.tips1Label) this.tips1Label.string = `线索一:${opts.hint1}`;
if (this.tips2Label) this.tips2Label.string = `线索二:${opts.hint2}`;
if (this.tips3Label) this.tips3Label.string = `线索三:${opts.hint3}`;
}
setCover(spriteFrame: SpriteFrame | null): void {
if (this.levelCover && spriteFrame) {
this.levelCover.spriteFrame = spriteFrame;
}
}
}

View File

@@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ddba71db-75ed-468d-ac99-b4632c0b2ae4",
"uuid": "5fb357ec-a670-4fff-a5cd-f47f1a2d8ea3",
"files": [],
"subMetas": {},
"userData": {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "e41c722f-f605-47f7-9ce4-abff0ed2020f",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "TimeoutModal"
}
}

View File

@@ -0,0 +1,162 @@
import { _decorator, Node, view, UITransform, Size } from 'cc';
import { BaseModal } from 'db://assets/scripts/core/BaseModal';
import { WxSDK } from 'db://assets/scripts/utils/WxSDK';
const { ccclass, property } = _decorator;
/**
* TimeoutModal 回调接口
*/
export interface TimeoutModalCallbacks {
/** 点击求助好友回调 */
onShare?: () => void;
/** 点击再次挑战回调 */
onRestart?: () => void;
/** 点击返回主页 / 关闭按钮回调 */
onHome?: () => void;
}
interface TimeoutModalParams {
levelIndex?: number;
}
/**
* 时间耗尽弹窗组件
* 继承 BaseModal显示倒计时结束提示提供"求助好友"、"再次挑战"和"返回主页"三个按钮
*/
@ccclass('TimeoutModal')
export class TimeoutModal extends BaseModal {
/** 静态常量:弹窗层级 */
public static readonly MODAL_Z_INDEX = 999;
/** 关闭按钮 */
@property(Node)
closeBtn: Node | null = null;
/** 求助好友按钮 */
@property(Node)
buttonShare: Node | null = null;
/** 再次挑战按钮 */
@property(Node)
buttonRestart: Node | null = null;
/** 返回主页按钮 */
@property(Node)
buttonHome: Node | null = null;
/** 回调函数 */
private _callbacks: TimeoutModalCallbacks = {};
/** 缓存的屏幕尺寸 */
private _screenSize: Size | null = null;
/**
* 设置回调函数
*/
setCallbacks(callbacks: TimeoutModalCallbacks): void {
this._callbacks = callbacks;
}
/**
* 页面首次加载时调用
*/
onViewLoad(): void {
console.log('[TimeoutModal] onViewLoad');
this._bindButtonEvents();
}
/**
* 页面每次显示时调用
*/
onViewShow(): void {
super.onViewShow();
this._updateWidget();
}
/**
* 页面销毁时调用
*/
onViewDestroy(): void {
this._unbindButtonEvents();
}
/**
* 设置弹窗尺寸为全屏
*/
private _updateWidget(): void {
if (!this._screenSize) {
this._screenSize = view.getVisibleSize();
}
const uiTransform = this.node.getComponent(UITransform);
if (uiTransform) {
uiTransform.setContentSize(this._screenSize.width, this._screenSize.height);
}
}
/**
* 绑定按钮事件
*/
private _bindButtonEvents(): void {
if (this.closeBtn) {
this.closeBtn.on(Node.EventType.TOUCH_END, this._onHomeClick, this);
}
if (this.buttonShare) {
this.buttonShare.on(Node.EventType.TOUCH_END, this._onShareClick, this);
}
if (this.buttonRestart) {
this.buttonRestart.on(Node.EventType.TOUCH_END, this._onRestartClick, this);
}
if (this.buttonHome) {
this.buttonHome.on(Node.EventType.TOUCH_END, this._onHomeClick, this);
}
}
/**
* 解除按钮事件绑定
*/
private _unbindButtonEvents(): void {
if (this.closeBtn && this.closeBtn.isValid) {
this.closeBtn.off(Node.EventType.TOUCH_END, this._onHomeClick, this);
}
if (this.buttonShare && this.buttonShare.isValid) {
this.buttonShare.off(Node.EventType.TOUCH_END, this._onShareClick, this);
}
if (this.buttonRestart && this.buttonRestart.isValid) {
this.buttonRestart.off(Node.EventType.TOUCH_END, this._onRestartClick, this);
}
if (this.buttonHome && this.buttonHome.isValid) {
this.buttonHome.off(Node.EventType.TOUCH_END, this._onHomeClick, this);
}
}
/**
* 求助好友按钮点击
*/
private _onShareClick(): void {
console.log('[TimeoutModal] 点击求助好友');
WxSDK.shareAppMessage({
title: '这道题太难了,快来帮帮我!',
query: `level=${this._params?.levelIndex ?? 1}`
});
this._callbacks.onShare?.();
}
/**
* 再次挑战按钮点击
*/
private _onRestartClick(): void {
console.log('[TimeoutModal] 点击再次挑战');
this._callbacks.onRestart?.();
}
/**
* 返回主页 / 关闭按钮点击
*/
private _onHomeClick(): void {
console.log('[TimeoutModal] 点击返回主页');
this._callbacks.onHome?.();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "bdb18473-6efb-4592-bf67-48555845eec5",
"files": [],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "455c7845-d090-4cd9-aeb4-1f5cad616bb5",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "WrongModal"
}
}

View File

@@ -0,0 +1,111 @@
import { _decorator, Node, view, UITransform, Size } from 'cc';
import { BaseModal } from 'db://assets/scripts/core/BaseModal';
const { ccclass, property } = _decorator;
/**
* WrongModal 回调接口
*/
export interface WrongModalCallbacks {
/** 点击继续挑战 / 关闭按钮回调 */
onContinue?: () => void;
}
/**
* 答案错误弹窗组件
* 继承 BaseModal显示答案错误提示提供"继续挑战"和关闭按钮
*/
@ccclass('WrongModal')
export class WrongModal extends BaseModal {
/** 静态常量:弹窗层级 */
public static readonly MODAL_Z_INDEX = 999;
/** 关闭按钮 */
@property(Node)
closeBtn: Node | null = null;
/** 继续挑战按钮 */
@property(Node)
buttonHint: Node | null = null;
/** 回调函数 */
private _callbacks: WrongModalCallbacks = {};
/** 缓存的屏幕尺寸 */
private _screenSize: Size | null = null;
/**
* 设置回调函数
*/
setCallbacks(callbacks: WrongModalCallbacks): void {
this._callbacks = callbacks;
}
/**
* 页面首次加载时调用
*/
onViewLoad(): void {
console.log('[WrongModal] onViewLoad');
this._bindButtonEvents();
}
/**
* 页面每次显示时调用
*/
onViewShow(): void {
super.onViewShow();
this._updateWidget();
}
/**
* 页面销毁时调用
*/
onViewDestroy(): void {
this._unbindButtonEvents();
}
/**
* 设置弹窗尺寸为全屏
*/
private _updateWidget(): void {
if (!this._screenSize) {
this._screenSize = view.getVisibleSize();
}
const uiTransform = this.node.getComponent(UITransform);
if (uiTransform) {
uiTransform.setContentSize(this._screenSize.width, this._screenSize.height);
}
}
/**
* 绑定按钮事件
*/
private _bindButtonEvents(): void {
if (this.closeBtn) {
this.closeBtn.on(Node.EventType.TOUCH_END, this._onContinueClick, this);
}
if (this.buttonHint) {
this.buttonHint.on(Node.EventType.TOUCH_END, this._onContinueClick, this);
}
}
/**
* 解除按钮事件绑定
*/
private _unbindButtonEvents(): void {
if (this.closeBtn && this.closeBtn.isValid) {
this.closeBtn.off(Node.EventType.TOUCH_END, this._onContinueClick, this);
}
if (this.buttonHint && this.buttonHint.isValid) {
this.buttonHint.off(Node.EventType.TOUCH_END, this._onContinueClick, this);
}
}
/**
* 继续挑战 / 关闭按钮点击
*/
private _onContinueClick(): void {
console.log('[WrongModal] 点击继续挑战');
this._callbacks.onContinue?.();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "972c57fc-10e7-493e-a1da-4849f3c1e555",
"files": [],
"subMetas": {},
"userData": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "85f4694c-9bb3-4a86-b961-6dbe12154989",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "85f4694c-9bb3-4a86-b961-6dbe12154989@6c48a",
"displayName": "BarStam",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "85f4694c-9bb3-4a86-b961-6dbe12154989",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "85f4694c-9bb3-4a86-b961-6dbe12154989@f9941",
"displayName": "BarStam",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0.5,
"trimX": 0,
"trimY": 0,
"width": 208,
"height": 81,
"rawWidth": 208,
"rawHeight": 82,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-104,
-40.5,
0,
104,
-40.5,
0,
-104,
40.5,
0,
104,
40.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
82,
208,
82,
0,
1,
208,
1
],
"nuv": [
0,
0.012195121951219513,
1,
0.012195121951219513,
0,
1,
1,
1
],
"minPos": [
-104,
-40.5,
0
],
"maxPos": [
104,
40.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "85f4694c-9bb3-4a86-b961-6dbe12154989@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "85f4694c-9bb3-4a86-b961-6dbe12154989@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "10047f2e-bf2c-46f0-ba98-6fc23cf7ea87",
"files": [
".jpg",
".json"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "10047f2e-bf2c-46f0-ba98-6fc23cf7ea87@6c48a",
"displayName": "Bg",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "10047f2e-bf2c-46f0-ba98-6fc23cf7ea87",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "10047f2e-bf2c-46f0-ba98-6fc23cf7ea87@f9941",
"displayName": "Bg",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1376,
"height": 3064,
"rawWidth": 1376,
"rawHeight": 3064,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-688,
-1532,
0,
688,
-1532,
0,
-688,
1532,
0,
688,
1532,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
3064,
1376,
3064,
0,
0,
1376,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-688,
-1532,
0
],
"maxPos": [
688,
1532,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "10047f2e-bf2c-46f0-ba98-6fc23cf7ea87@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": false,
"redirect": "10047f2e-bf2c-46f0-ba98-6fc23cf7ea87@6c48a"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "7a8edfce-b0ad-41b3-8dc9-ec1c1ee22153",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "7a8edfce-b0ad-41b3-8dc9-ec1c1ee22153@6c48a",
"displayName": "ButtonSetting",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "7a8edfce-b0ad-41b3-8dc9-ec1c1ee22153",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "7a8edfce-b0ad-41b3-8dc9-ec1c1ee22153@f9941",
"displayName": "ButtonSetting",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 115,
"height": 123,
"rawWidth": 115,
"rawHeight": 123,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-57.5,
-61.5,
0,
57.5,
-61.5,
0,
-57.5,
61.5,
0,
57.5,
61.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
123,
115,
123,
0,
0,
115,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-57.5,
-61.5,
0
],
"maxPos": [
57.5,
61.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "7a8edfce-b0ad-41b3-8dc9-ec1c1ee22153@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "7a8edfce-b0ad-41b3-8dc9-ec1c1ee22153@6c48a"
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "a6e360cb-d0cd-4f57-9e43-10b13f6f0d11",
"files": [],
"subMetas": {},
"userData": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -2,7 +2,7 @@
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "010e6809-3636-4c23-8403-1424c771f0f8",
"uuid": "69f85753-6740-4686-ba40-5d2383292f39",
"files": [
".json",
".png"
@@ -10,14 +10,14 @@
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "010e6809-3636-4c23-8403-1424c771f0f8@6c48a",
"displayName": "startGame",
"uuid": "69f85753-6740-4686-ba40-5d2383292f39@6c48a",
"displayName": "flatIconHelp",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "010e6809-3636-4c23-8403-1424c771f0f8",
"imageUuidOrDatabaseUri": "69f85753-6740-4686-ba40-5d2383292f39",
"isUuid": true,
"visible": false,
"minfilter": "linear",
@@ -34,8 +34,8 @@
},
"f9941": {
"importer": "sprite-frame",
"uuid": "010e6809-3636-4c23-8403-1424c771f0f8@f9941",
"displayName": "startGame",
"uuid": "69f85753-6740-4686-ba40-5d2383292f39@f9941",
"displayName": "flatIconHelp",
"id": "f9941",
"name": "spriteFrame",
"userData": {
@@ -45,10 +45,10 @@
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 216,
"height": 107,
"rawWidth": 216,
"rawHeight": 107,
"width": 468,
"height": 468,
"rawWidth": 468,
"rawHeight": 468,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
@@ -60,17 +60,17 @@
"meshType": 0,
"vertices": {
"rawPosition": [
-108,
-53.5,
-234,
-234,
0,
108,
-53.5,
234,
-234,
0,
-108,
53.5,
-234,
234,
0,
108,
53.5,
234,
234,
0
],
"indexes": [
@@ -83,12 +83,12 @@
],
"uv": [
0,
107,
216,
107,
468,
468,
468,
0,
0,
216,
468,
0
],
"nuv": [
@@ -102,18 +102,18 @@
1
],
"minPos": [
-108,
-53.5,
-234,
-234,
0
],
"maxPos": [
108,
53.5,
234,
234,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "010e6809-3636-4c23-8403-1424c771f0f8@6c48a",
"imageUuidOrDatabaseUri": "69f85753-6740-4686-ba40-5d2383292f39@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
@@ -129,6 +129,6 @@
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "010e6809-3636-4c23-8403-1424c771f0f8@6c48a"
"redirect": "69f85753-6740-4686-ba40-5d2383292f39@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "fb73de29-d734-4eb4-853d-8cbdbb18a9c9",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "fb73de29-d734-4eb4-853d-8cbdbb18a9c9@6c48a",
"displayName": "flatIconHome",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "fb73de29-d734-4eb4-853d-8cbdbb18a9c9",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "fb73de29-d734-4eb4-853d-8cbdbb18a9c9@f9941",
"displayName": "flatIconHome",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 427,
"height": 427,
"rawWidth": 427,
"rawHeight": 427,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-213.5,
-213.5,
0,
213.5,
-213.5,
0,
-213.5,
213.5,
0,
213.5,
213.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
427,
427,
427,
0,
0,
427,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-213.5,
-213.5,
0
],
"maxPos": [
213.5,
213.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "fb73de29-d734-4eb4-853d-8cbdbb18a9c9@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "fb73de29-d734-4eb4-853d-8cbdbb18a9c9@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "1c8a9e95-7b00-4e59-a0a2-72a4aa97c03c",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "1c8a9e95-7b00-4e59-a0a2-72a4aa97c03c@6c48a",
"displayName": "flatIconNext",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "1c8a9e95-7b00-4e59-a0a2-72a4aa97c03c",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "1c8a9e95-7b00-4e59-a0a2-72a4aa97c03c@f9941",
"displayName": "flatIconNext",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 13.5,
"offsetY": -0.5,
"trimX": 82,
"trimY": 29,
"width": 375,
"height": 455,
"rawWidth": 512,
"rawHeight": 512,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-187.5,
-227.5,
0,
187.5,
-227.5,
0,
-187.5,
227.5,
0,
187.5,
227.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
82,
483,
457,
483,
82,
28,
457,
28
],
"nuv": [
0.16015625,
0.0546875,
0.892578125,
0.0546875,
0.16015625,
0.943359375,
0.892578125,
0.943359375
],
"minPos": [
-187.5,
-227.5,
0
],
"maxPos": [
187.5,
227.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "1c8a9e95-7b00-4e59-a0a2-72a4aa97c03c@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "1c8a9e95-7b00-4e59-a0a2-72a4aa97c03c@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "c24760c9-312a-4638-ae56-6422010d93d0",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "c24760c9-312a-4638-ae56-6422010d93d0@6c48a",
"displayName": "flatIconRestart",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "c24760c9-312a-4638-ae56-6422010d93d0",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "c24760c9-312a-4638-ae56-6422010d93d0@f9941",
"displayName": "flatIconRestart",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 490,
"height": 492,
"rawWidth": 490,
"rawHeight": 492,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-245,
-246,
0,
245,
-246,
0,
-245,
246,
0,
245,
246,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
492,
490,
492,
0,
0,
490,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-245,
-246,
0
],
"maxPos": [
245,
246,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "c24760c9-312a-4638-ae56-6422010d93d0@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "c24760c9-312a-4638-ae56-6422010d93d0@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "b9bec232-3d37-48a0-9ca8-62aab6c4083b",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "b9bec232-3d37-48a0-9ca8-62aab6c4083b@6c48a",
"displayName": "flatIconShare",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "b9bec232-3d37-48a0-9ca8-62aab6c4083b",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "b9bec232-3d37-48a0-9ca8-62aab6c4083b@f9941",
"displayName": "flatIconShare",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 22,
"trimY": 11,
"width": 468,
"height": 490,
"rawWidth": 512,
"rawHeight": 512,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-234,
-245,
0,
234,
-245,
0,
-234,
245,
0,
234,
245,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
22,
501,
490,
501,
22,
11,
490,
11
],
"nuv": [
0.04296875,
0.021484375,
0.95703125,
0.021484375,
0.04296875,
0.978515625,
0.95703125,
0.978515625
],
"minPos": [
-234,
-245,
0
],
"maxPos": [
234,
245,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "b9bec232-3d37-48a0-9ca8-62aab6c4083b@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "b9bec232-3d37-48a0-9ca8-62aab6c4083b@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "cf6809c4-f69b-4107-9b50-bd692766213a",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "cf6809c4-f69b-4107-9b50-bd692766213a@6c48a",
"displayName": "flatIconView",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "cf6809c4-f69b-4107-9b50-bd692766213a",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "cf6809c4-f69b-4107-9b50-bd692766213a@f9941",
"displayName": "flatIconView",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 27,
"trimY": 80,
"width": 458,
"height": 352,
"rawWidth": 512,
"rawHeight": 512,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-229,
-176,
0,
229,
-176,
0,
-229,
176,
0,
229,
176,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
27,
432,
485,
432,
27,
80,
485,
80
],
"nuv": [
0.052734375,
0.15625,
0.947265625,
0.15625,
0.052734375,
0.84375,
0.947265625,
0.84375
],
"minPos": [
-229,
-176,
0
],
"maxPos": [
229,
176,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "cf6809c4-f69b-4107-9b50-bd692766213a@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "cf6809c4-f69b-4107-9b50-bd692766213a@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -2,7 +2,7 @@
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "a34c93d2-cae0-42d4-a2eb-c3155052ad20",
"uuid": "5a832eca-5c6c-4fbc-9218-bf5aae721934",
"files": [
".json",
".png"
@@ -10,14 +10,14 @@
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "a34c93d2-cae0-42d4-a2eb-c3155052ad20@6c48a",
"displayName": "ContentBg",
"uuid": "5a832eca-5c6c-4fbc-9218-bf5aae721934@6c48a",
"displayName": "IconAd",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "a34c93d2-cae0-42d4-a2eb-c3155052ad20",
"imageUuidOrDatabaseUri": "5a832eca-5c6c-4fbc-9218-bf5aae721934",
"isUuid": true,
"visible": false,
"minfilter": "linear",
@@ -34,8 +34,8 @@
},
"f9941": {
"importer": "sprite-frame",
"uuid": "a34c93d2-cae0-42d4-a2eb-c3155052ad20@f9941",
"displayName": "ContentBg",
"uuid": "5a832eca-5c6c-4fbc-9218-bf5aae721934@f9941",
"displayName": "IconAd",
"id": "f9941",
"name": "spriteFrame",
"userData": {
@@ -45,10 +45,10 @@
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 304,
"height": 404,
"rawWidth": 304,
"rawHeight": 404,
"width": 63,
"height": 49,
"rawWidth": 63,
"rawHeight": 49,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
@@ -60,17 +60,17 @@
"meshType": 0,
"vertices": {
"rawPosition": [
-152,
-202,
-31.5,
-24.5,
0,
152,
-202,
31.5,
-24.5,
0,
-152,
202,
-31.5,
24.5,
0,
152,
202,
31.5,
24.5,
0
],
"indexes": [
@@ -83,12 +83,12 @@
],
"uv": [
0,
404,
304,
404,
49,
63,
49,
0,
0,
304,
63,
0
],
"nuv": [
@@ -102,18 +102,18 @@
1
],
"minPos": [
-152,
-202,
-31.5,
-24.5,
0
],
"maxPos": [
152,
202,
31.5,
24.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "a34c93d2-cae0-42d4-a2eb-c3155052ad20@6c48a",
"imageUuidOrDatabaseUri": "5a832eca-5c6c-4fbc-9218-bf5aae721934@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
@@ -127,8 +127,8 @@
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "a34c93d2-cae0-42d4-a2eb-c3155052ad20@6c48a"
"fixAlphaTransparencyArtifacts": false,
"redirect": "5a832eca-5c6c-4fbc-9218-bf5aae721934@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "d18c3099-87cb-4ee0-9778-b3b27468f7b7",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "d18c3099-87cb-4ee0-9778-b3b27468f7b7@6c48a",
"displayName": "IconStam2",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "d18c3099-87cb-4ee0-9778-b3b27468f7b7",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "d18c3099-87cb-4ee0-9778-b3b27468f7b7@f9941",
"displayName": "IconStam2",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0.5,
"trimX": 86,
"trimY": 45,
"width": 228,
"height": 309,
"rawWidth": 400,
"rawHeight": 400,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-114,
-154.5,
0,
114,
-154.5,
0,
-114,
154.5,
0,
114,
154.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
86,
355,
314,
355,
86,
46,
314,
46
],
"nuv": [
0.215,
0.115,
0.785,
0.115,
0.215,
0.8875,
0.785,
0.8875
],
"minPos": [
-114,
-154.5,
0
],
"maxPos": [
114,
154.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "d18c3099-87cb-4ee0-9778-b3b27468f7b7@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "d18c3099-87cb-4ee0-9778-b3b27468f7b7@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "a2cba4c9-ed0a-46c4-9184-362cfd0d878d",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "a2cba4c9-ed0a-46c4-9184-362cfd0d878d@6c48a",
"displayName": "Plus",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "a2cba4c9-ed0a-46c4-9184-362cfd0d878d",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "a2cba4c9-ed0a-46c4-9184-362cfd0d878d@f9941",
"displayName": "Plus",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 26,
"height": 26,
"rawWidth": 26,
"rawHeight": 26,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-13,
-13,
0,
13,
-13,
0,
-13,
13,
0,
13,
13,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
26,
26,
26,
0,
0,
26,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-13,
-13,
0
],
"maxPos": [
13,
13,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "a2cba4c9-ed0a-46c4-9184-362cfd0d878d@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "a2cba4c9-ed0a-46c4-9184-362cfd0d878d@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "e9f94fb0-2acf-4004-8c6e-023e9deeb9cb",
"files": [
".jpg",
".json"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "e9f94fb0-2acf-4004-8c6e-023e9deeb9cb@6c48a",
"displayName": "bg_green",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "e9f94fb0-2acf-4004-8c6e-023e9deeb9cb",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "e9f94fb0-2acf-4004-8c6e-023e9deeb9cb@f9941",
"displayName": "bg_green",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1376,
"height": 3064,
"rawWidth": 1376,
"rawHeight": 3064,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-688,
-1532,
0,
688,
-1532,
0,
-688,
1532,
0,
688,
1532,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
3064,
1376,
3064,
0,
0,
1376,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-688,
-1532,
0
],
"maxPos": [
688,
1532,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "e9f94fb0-2acf-4004-8c6e-023e9deeb9cb@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": false,
"redirect": "e9f94fb0-2acf-4004-8c6e-023e9deeb9cb@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "39c1c6c3-43dd-4fa1-bbc2-fb394d8cf6d6",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "39c1c6c3-43dd-4fa1-bbc2-fb394d8cf6d6@6c48a",
"displayName": "BuyButtonBg",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "39c1c6c3-43dd-4fa1-bbc2-fb394d8cf6d6",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "39c1c6c3-43dd-4fa1-bbc2-fb394d8cf6d6@f9941",
"displayName": "BuyButtonBg",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 326,
"height": 143,
"rawWidth": 326,
"rawHeight": 143,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-163,
-71.5,
0,
163,
-71.5,
0,
-163,
71.5,
0,
163,
71.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
143,
326,
143,
0,
0,
326,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-163,
-71.5,
0
],
"maxPos": [
163,
71.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "39c1c6c3-43dd-4fa1-bbc2-fb394d8cf6d6@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "39c1c6c3-43dd-4fa1-bbc2-fb394d8cf6d6@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "02047ea3-2a13-4f17-b4dd-5aefa0e182dc",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "02047ea3-2a13-4f17-b4dd-5aefa0e182dc@6c48a",
"displayName": "HomeButtonGreen",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "02047ea3-2a13-4f17-b4dd-5aefa0e182dc",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "02047ea3-2a13-4f17-b4dd-5aefa0e182dc@f9941",
"displayName": "HomeButtonGreen",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 488,
"height": 197,
"rawWidth": 488,
"rawHeight": 197,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-244,
-98.5,
0,
244,
-98.5,
0,
-244,
98.5,
0,
244,
98.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
197,
488,
197,
0,
0,
488,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-244,
-98.5,
0
],
"maxPos": [
244,
98.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "02047ea3-2a13-4f17-b4dd-5aefa0e182dc@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "02047ea3-2a13-4f17-b4dd-5aefa0e182dc@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "2270362e-66d7-4d0f-ab51-b5cbca25a768",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "2270362e-66d7-4d0f-ab51-b5cbca25a768@6c48a",
"displayName": "HomeButtonOrange",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "2270362e-66d7-4d0f-ab51-b5cbca25a768",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "2270362e-66d7-4d0f-ab51-b5cbca25a768@f9941",
"displayName": "HomeButtonOrange",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 488,
"height": 197,
"rawWidth": 488,
"rawHeight": 197,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-244,
-98.5,
0,
244,
-98.5,
0,
-244,
98.5,
0,
244,
98.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
197,
488,
197,
0,
0,
488,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-244,
-98.5,
0
],
"maxPos": [
244,
98.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "2270362e-66d7-4d0f-ab51-b5cbca25a768@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "2270362e-66d7-4d0f-ab51-b5cbca25a768@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

@@ -2,7 +2,7 @@
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "729b014d-f0e9-4b67-b99d-177756102d0e",
"uuid": "12eb7a16-3f31-40d8-a9cd-30e18900a3a3",
"files": [
".json",
".png"
@@ -10,14 +10,14 @@
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "729b014d-f0e9-4b67-b99d-177756102d0e@6c48a",
"displayName": "IconTips",
"uuid": "12eb7a16-3f31-40d8-a9cd-30e18900a3a3@6c48a",
"displayName": "friendChallenge",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "729b014d-f0e9-4b67-b99d-177756102d0e",
"imageUuidOrDatabaseUri": "12eb7a16-3f31-40d8-a9cd-30e18900a3a3",
"isUuid": true,
"visible": false,
"minfilter": "linear",
@@ -34,21 +34,21 @@
},
"f9941": {
"importer": "sprite-frame",
"uuid": "729b014d-f0e9-4b67-b99d-177756102d0e@f9941",
"displayName": "IconTips",
"uuid": "12eb7a16-3f31-40d8-a9cd-30e18900a3a3@f9941",
"displayName": "friendChallenge",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0.5,
"offsetY": 0,
"trimX": 31,
"trimY": 12,
"width": 139,
"height": 176,
"rawWidth": 200,
"rawHeight": 200,
"offsetX": 0,
"offsetY": -8.5,
"trimX": 170,
"trimY": 243,
"width": 684,
"height": 555,
"rawWidth": 1024,
"rawHeight": 1024,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
@@ -60,17 +60,17 @@
"meshType": 0,
"vertices": {
"rawPosition": [
-69.5,
-88,
-342,
-277.5,
0,
69.5,
-88,
342,
-277.5,
0,
-69.5,
88,
-342,
277.5,
0,
69.5,
88,
342,
277.5,
0
],
"indexes": [
@@ -82,38 +82,38 @@
3
],
"uv": [
31,
188,
170,
188,
31,
12,
781,
854,
781,
170,
12
226,
854,
226
],
"nuv": [
0.155,
0.06,
0.85,
0.06,
0.155,
0.94,
0.85,
0.94
0.166015625,
0.220703125,
0.833984375,
0.220703125,
0.166015625,
0.7626953125,
0.833984375,
0.7626953125
],
"minPos": [
-69.5,
-88,
-342,
-277.5,
0
],
"maxPos": [
69.5,
88,
342,
277.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "729b014d-f0e9-4b67-b99d-177756102d0e@6c48a",
"imageUuidOrDatabaseUri": "12eb7a16-3f31-40d8-a9cd-30e18900a3a3@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
@@ -129,6 +129,6 @@
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "729b014d-f0e9-4b67-b99d-177756102d0e@6c48a"
"redirect": "12eb7a16-3f31-40d8-a9cd-30e18900a3a3@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -2,7 +2,7 @@
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "d532045e-55f8-47c2-9493-b918e18364b0",
"uuid": "894c2169-ec35-4aeb-b656-3caa09604b09",
"files": [
".json",
".png"
@@ -10,14 +10,14 @@
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "d532045e-55f8-47c2-9493-b918e18364b0@6c48a",
"displayName": "Bg",
"uuid": "894c2169-ec35-4aeb-b656-3caa09604b09@6c48a",
"displayName": "homeIconChallenge",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "d532045e-55f8-47c2-9493-b918e18364b0",
"imageUuidOrDatabaseUri": "894c2169-ec35-4aeb-b656-3caa09604b09",
"isUuid": true,
"visible": false,
"minfilter": "linear",
@@ -34,8 +34,8 @@
},
"f9941": {
"importer": "sprite-frame",
"uuid": "d532045e-55f8-47c2-9493-b918e18364b0@f9941",
"displayName": "Bg",
"uuid": "894c2169-ec35-4aeb-b656-3caa09604b09@f9941",
"displayName": "homeIconChallenge",
"id": "f9941",
"name": "spriteFrame",
"userData": {
@@ -45,10 +45,10 @@
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 750,
"height": 1626,
"rawWidth": 750,
"rawHeight": 1626,
"width": 375,
"height": 183,
"rawWidth": 375,
"rawHeight": 183,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
@@ -60,17 +60,17 @@
"meshType": 0,
"vertices": {
"rawPosition": [
-375,
-813,
-187.5,
-91.5,
0,
375,
-813,
187.5,
-91.5,
0,
-375,
813,
-187.5,
91.5,
0,
375,
813,
187.5,
91.5,
0
],
"indexes": [
@@ -83,12 +83,12 @@
],
"uv": [
0,
1626,
750,
1626,
183,
375,
183,
0,
0,
750,
375,
0
],
"nuv": [
@@ -102,18 +102,18 @@
1
],
"minPos": [
-375,
-813,
-187.5,
-91.5,
0
],
"maxPos": [
375,
813,
187.5,
91.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "d532045e-55f8-47c2-9493-b918e18364b0@6c48a",
"imageUuidOrDatabaseUri": "894c2169-ec35-4aeb-b656-3caa09604b09@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
@@ -129,6 +129,6 @@
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "d532045e-55f8-47c2-9493-b918e18364b0@6c48a"
"redirect": "894c2169-ec35-4aeb-b656-3caa09604b09@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "0253c893-2588-492f-9d96-911878b8a673",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "0253c893-2588-492f-9d96-911878b8a673@6c48a",
"displayName": "homeIconCreate",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "0253c893-2588-492f-9d96-911878b8a673",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "0253c893-2588-492f-9d96-911878b8a673@f9941",
"displayName": "homeIconCreate",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": -0.5,
"trimX": 0,
"trimY": 1,
"width": 373,
"height": 232,
"rawWidth": 373,
"rawHeight": 233,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-186.5,
-116,
0,
186.5,
-116,
0,
-186.5,
116,
0,
186.5,
116,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
232,
373,
232,
0,
0,
373,
0
],
"nuv": [
0,
0,
1,
0,
0,
0.9957081545064378,
1,
0.9957081545064378
],
"minPos": [
-186.5,
-116,
0
],
"maxPos": [
186.5,
116,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "0253c893-2588-492f-9d96-911878b8a673@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "0253c893-2588-492f-9d96-911878b8a673@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "a85c793d-50ce-4db1-ac92-2af76b8aa505",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "a85c793d-50ce-4db1-ac92-2af76b8aa505@6c48a",
"displayName": "homeIconFriend",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "a85c793d-50ce-4db1-ac92-2af76b8aa505",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "a85c793d-50ce-4db1-ac92-2af76b8aa505@f9941",
"displayName": "homeIconFriend",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 345,
"height": 260,
"rawWidth": 345,
"rawHeight": 260,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-172.5,
-130,
0,
172.5,
-130,
0,
-172.5,
130,
0,
172.5,
130,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
260,
345,
260,
0,
0,
345,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-172.5,
-130,
0
],
"maxPos": [
172.5,
130,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "a85c793d-50ce-4db1-ac92-2af76b8aa505@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "a85c793d-50ce-4db1-ac92-2af76b8aa505@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "496f93ae-424d-427d-b3eb-54e47e336fc8",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "496f93ae-424d-427d-b3eb-54e47e336fc8@6c48a",
"displayName": "homeIconGift",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "496f93ae-424d-427d-b3eb-54e47e336fc8",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "496f93ae-424d-427d-b3eb-54e47e336fc8@f9941",
"displayName": "homeIconGift",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 283,
"height": 285,
"rawWidth": 283,
"rawHeight": 285,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-141.5,
-142.5,
0,
141.5,
-142.5,
0,
-141.5,
142.5,
0,
141.5,
142.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
285,
283,
285,
0,
0,
283,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-141.5,
-142.5,
0
],
"maxPos": [
141.5,
142.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "496f93ae-424d-427d-b3eb-54e47e336fc8@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "496f93ae-424d-427d-b3eb-54e47e336fc8@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "6cc6bad3-00c6-4416-b48f-0434317d96c2",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "6cc6bad3-00c6-4416-b48f-0434317d96c2@6c48a",
"displayName": "homeIconRank",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "6cc6bad3-00c6-4416-b48f-0434317d96c2",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "6cc6bad3-00c6-4416-b48f-0434317d96c2@f9941",
"displayName": "homeIconRank",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": -0.5,
"trimX": 0,
"trimY": 1,
"width": 346,
"height": 307,
"rawWidth": 346,
"rawHeight": 308,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-173,
-153.5,
0,
173,
-153.5,
0,
-173,
153.5,
0,
173,
153.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
307,
346,
307,
0,
0,
346,
0
],
"nuv": [
0,
0,
1,
0,
0,
0.9967532467532467,
1,
0.9967532467532467
],
"minPos": [
-173,
-153.5,
0
],
"maxPos": [
173,
153.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "6cc6bad3-00c6-4416-b48f-0434317d96c2@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "6cc6bad3-00c6-4416-b48f-0434317d96c2@6c48a"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@6c48a",
"displayName": "1_0022_Layer-19",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@f9941",
"displayName": "1_0022_Layer-19",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1297,
"height": 1004,
"rawWidth": 1297,
"rawHeight": 1004,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-648.5,
-502,
0,
648.5,
-502,
0,
-648.5,
502,
0,
648.5,
502,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
1004,
1297,
1004,
0,
0,
1297,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-648.5,
-502,
0
],
"maxPos": [
648.5,
502,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "3ef3f86f-bc1f-49b7-a50f-0e8683cd2400@6c48a"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "f87d228a-c520-499a-bf3a-e66cbb6def64",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "f87d228a-c520-499a-bf3a-e66cbb6def64@6c48a",
"displayName": "ButtonGreen",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "f87d228a-c520-499a-bf3a-e66cbb6def64",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "f87d228a-c520-499a-bf3a-e66cbb6def64@f9941",
"displayName": "ButtonGreen",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 666,
"height": 235,
"rawWidth": 666,
"rawHeight": 235,
"borderTop": 120,
"borderBottom": 115,
"borderLeft": 120,
"borderRight": 120,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-333,
-117.5,
0,
333,
-117.5,
0,
-333,
117.5,
0,
333,
117.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
235,
666,
235,
0,
0,
666,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-333,
-117.5,
0
],
"maxPos": [
333,
117.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "f87d228a-c520-499a-bf3a-e66cbb6def64@6c48a",
"atlasUuid": "",
"trimType": "none"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "f87d228a-c520-499a-bf3a-e66cbb6def64@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "0366f161-d78c-43b1-b2cb-8d0666dd2fbd",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "0366f161-d78c-43b1-b2cb-8d0666dd2fbd@6c48a",
"displayName": "ButtonOrange",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "0366f161-d78c-43b1-b2cb-8d0666dd2fbd",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "0366f161-d78c-43b1-b2cb-8d0666dd2fbd@f9941",
"displayName": "ButtonOrange",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 666,
"height": 235,
"rawWidth": 666,
"rawHeight": 235,
"borderTop": 112,
"borderBottom": 112,
"borderLeft": 112,
"borderRight": 112,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-333,
-117.5,
0,
333,
-117.5,
0,
-333,
117.5,
0,
333,
117.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
235,
666,
235,
0,
0,
666,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-333,
-117.5,
0
],
"maxPos": [
333,
117.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "0366f161-d78c-43b1-b2cb-8d0666dd2fbd@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "0366f161-d78c-43b1-b2cb-8d0666dd2fbd@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "ba58112d-788c-465d-a2f3-46b468026d03",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "ba58112d-788c-465d-a2f3-46b468026d03@6c48a",
"displayName": "ButtonYellow",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "ba58112d-788c-465d-a2f3-46b468026d03",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "ba58112d-788c-465d-a2f3-46b468026d03@f9941",
"displayName": "ButtonYellow",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": -0.5,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 665,
"height": 235,
"rawWidth": 666,
"rawHeight": 235,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-332.5,
-117.5,
0,
332.5,
-117.5,
0,
-332.5,
117.5,
0,
332.5,
117.5,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
235,
665,
235,
0,
0,
665,
0
],
"nuv": [
0,
0,
0.9984984984984985,
0,
0,
1,
0.9984984984984985,
1
],
"minPos": [
-332.5,
-117.5,
0
],
"maxPos": [
332.5,
117.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "ba58112d-788c-465d-a2f3-46b468026d03@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "ba58112d-788c-465d-a2f3-46b468026d03@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -2,7 +2,7 @@
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26",
"uuid": "31d2998e-4261-4284-bbcd-9a34ad9c73a6",
"files": [
".json",
".png"
@@ -10,14 +10,14 @@
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26@6c48a",
"displayName": "ButtonBg",
"uuid": "31d2998e-4261-4284-bbcd-9a34ad9c73a6@6c48a",
"displayName": "CharBlock",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26",
"imageUuidOrDatabaseUri": "31d2998e-4261-4284-bbcd-9a34ad9c73a6",
"isUuid": true,
"visible": false,
"minfilter": "linear",
@@ -34,8 +34,8 @@
},
"f9941": {
"importer": "sprite-frame",
"uuid": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26@f9941",
"displayName": "ButtonBg",
"uuid": "31d2998e-4261-4284-bbcd-9a34ad9c73a6@f9941",
"displayName": "CharBlock",
"id": "f9941",
"name": "spriteFrame",
"userData": {
@@ -44,11 +44,11 @@
"offsetX": 0,
"offsetY": 0.5,
"trimX": 1,
"trimY": 0,
"width": 306,
"height": 77,
"rawWidth": 308,
"rawHeight": 78,
"trimY": 1,
"width": 177,
"height": 209,
"rawWidth": 179,
"rawHeight": 212,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
@@ -60,17 +60,17 @@
"meshType": 0,
"vertices": {
"rawPosition": [
-153,
-38.5,
-88.5,
-104.5,
0,
153,
-38.5,
88.5,
-104.5,
0,
-153,
38.5,
-88.5,
104.5,
0,
153,
38.5,
88.5,
104.5,
0
],
"indexes": [
@@ -83,37 +83,37 @@
],
"uv": [
1,
78,
307,
78,
211,
178,
211,
1,
1,
307,
1
2,
178,
2
],
"nuv": [
0.003246753246753247,
0.01282051282051282,
0.9967532467532467,
0.01282051282051282,
0.003246753246753247,
1,
0.9967532467532467,
1
0.00558659217877095,
0.009433962264150943,
0.994413407821229,
0.009433962264150943,
0.00558659217877095,
0.9952830188679245,
0.994413407821229,
0.9952830188679245
],
"minPos": [
-153,
-38.5,
-88.5,
-104.5,
0
],
"maxPos": [
153,
38.5,
88.5,
104.5,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26@6c48a",
"imageUuidOrDatabaseUri": "31d2998e-4261-4284-bbcd-9a34ad9c73a6@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
@@ -129,6 +129,6 @@
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "e1267c1b-ceb0-4483-b36d-bc9cb4d2fd26@6c48a"
"redirect": "31d2998e-4261-4284-bbcd-9a34ad9c73a6@6c48a"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "88cc67ba-1a68-4e04-99de-db6fd60fe3d6",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "88cc67ba-1a68-4e04-99de-db6fd60fe3d6@6c48a",
"displayName": "CharBlock2",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "88cc67ba-1a68-4e04-99de-db6fd60fe3d6",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "88cc67ba-1a68-4e04-99de-db6fd60fe3d6@f9941",
"displayName": "CharBlock2",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 178,
"height": 186,
"rawWidth": 178,
"rawHeight": 186,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-89,
-93,
0,
89,
-93,
0,
-89,
93,
0,
89,
93,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
186,
178,
186,
0,
0,
178,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-89,
-93,
0
],
"maxPos": [
89,
93,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "88cc67ba-1a68-4e04-99de-db6fd60fe3d6@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "88cc67ba-1a68-4e04-99de-db6fd60fe3d6@6c48a"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -0,0 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "a428ffd4-ce8c-4682-bf0b-d127509c8f23",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "a428ffd4-ce8c-4682-bf0b-d127509c8f23@6c48a",
"displayName": "IconBulb",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "a428ffd4-ce8c-4682-bf0b-d127509c8f23",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "a428ffd4-ce8c-4682-bf0b-d127509c8f23@f9941",
"displayName": "IconBulb",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 59,
"trimY": 7,
"width": 382,
"height": 486,
"rawWidth": 500,
"rawHeight": 500,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-191,
-243,
0,
191,
-243,
0,
-191,
243,
0,
191,
243,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
59,
493,
441,
493,
59,
7,
441,
7
],
"nuv": [
0.118,
0.014,
0.882,
0.014,
0.118,
0.986,
0.882,
0.986
],
"minPos": [
-191,
-243,
0
],
"maxPos": [
191,
243,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "a428ffd4-ce8c-4682-bf0b-d127509c8f23@6c48a",
"atlasUuid": "",
"trimType": "auto"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "a428ffd4-ce8c-4682-bf0b-d127509c8f23@6c48a"
}
}

Some files were not shown because too many files have changed in this diff Show More