- 进入关卡成功后显示 toast 提示消耗体力及剩余体力 - 将 StorageManager 中 UserInfo 接口移至模块顶层,修复嵌套接口语法问题 - WxSDK.getWx() 改为 static 公开方法,便于外部调用
666 lines
26 KiB
Markdown
666 lines
26 KiB
Markdown
# Architecture & Flow Diagrams
|
|
|
|
## System Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ Cocos Creator Runtime │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
│ │ UI Layer (This Analysis) │ │
|
|
│ ├──────────────────────────────────────────────────────────┤ │
|
|
│ │ │ │
|
|
│ │ ┌────────────────┐ ┌──────────────┐ │ │
|
|
│ │ │ main.ts │────────▶│ ViewManager │ │ │
|
|
│ │ │ (Bootstrap) │ │ (Singleton) │ │ │
|
|
│ │ └────────────────┘ └──────────────┘ │ │
|
|
│ │ │ │ │
|
|
│ │ Pages (All extend BaseView): │ │ │
|
|
│ │ ┌─────────────────────────────────┼──────────────┐ │ │
|
|
│ │ │ • PageLoading (init data) │ │ │ │
|
|
│ │ │ • PageHome (hub) │ │ │ │
|
|
│ │ │ • PageLevel (game - complex) ◀┘ │ │ │
|
|
│ │ │ • PagePreviewLevels (list) │ │ │
|
|
│ │ │ • PassModal (modal) │ │ │
|
|
│ │ └────────────────────────────────────────────────┘ │ │
|
|
│ │ │ │
|
|
│ │ ┌────────────────┐ ┌──────────────┐ │ │
|
|
│ │ │ ToastManager │────────▶│ Toast │ │ │
|
|
│ │ │ (Singleton) │ │ (Component) │ │ │
|
|
│ │ └────────────────┘ └──────────────┘ │ │
|
|
│ │ │ │
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
│ │ Data Layer (External - Not Analyzed) │ │
|
|
│ ├──────────────────────────────────────────────────────────┤ │
|
|
│ │ • AuthManager • LevelDataManager │ │
|
|
│ │ • StorageManager • UserAssetsManager │ │
|
|
│ │ • ShareManager • WxSDK │ │
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Page State Machine
|
|
|
|
```
|
|
┌─────────────────────────────┐
|
|
│ Application Starts │
|
|
└──────────────┬──────────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────┐
|
|
│ main.onLoad() │
|
|
│ Registers all pages │
|
|
└──────────┬───────────┘
|
|
│
|
|
▼
|
|
┌──────────────────────────────┐
|
|
│ PageLoading displayed │
|
|
│ - Load auth + levels │
|
|
│ - Sync progress │
|
|
│ - Check share code │
|
|
└──────────────┬───────────────┘
|
|
│
|
|
┌──────────────┴──────────────┐
|
|
│ Normal Mode │ Share Mode │
|
|
▼ ▼ │
|
|
┌──────────────────┐ ┌───────────────┐ │
|
|
│ PageHome │ │ PageLevel │ │
|
|
│ [Start] [PK] │ │ (in share) │ │
|
|
└────┬─────┬───────┘ └───────┬───────┘ │
|
|
│ │ │ │
|
|
┌────────▼─┐ └────────────┬──────┴────────┘
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
PageLevel PageWrite (Repeat levels)
|
|
(game) Levels │
|
|
│ (create) ▼
|
|
│ │ PassModal
|
|
│ ▼ [Share]
|
|
│ PagePreview │
|
|
│ Levels ▼
|
|
│ (verify) Back to Home
|
|
│ │
|
|
│ ▼
|
|
│ PassModal
|
|
│ [Next/Share]
|
|
│ │
|
|
└──────────┼─────────┐
|
|
│ │
|
|
┌────▼──┐ ┌───▼────┐
|
|
│ Next │ │ Share │
|
|
│ Loop │ │ to WX │
|
|
└───────┘ └────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Page Stack Visualization
|
|
|
|
### Session Trace
|
|
|
|
```
|
|
INITIALIZATION:
|
|
┌────────────────────────────────────────────┐
|
|
│ Stack: [] │
|
|
│ (Nothing loaded yet) │
|
|
└────────────────────────────────────────────┘
|
|
│
|
|
▼ main.onLoad()
|
|
┌────────────────────────────────────────────┐
|
|
│ Stack: [] │
|
|
│ ViewManager + pages registered in registry │
|
|
└────────────────────────────────────────────┘
|
|
│
|
|
▼ PageLoading.start() → ViewManager.open('PageHome')
|
|
┌────────────────────────────────────────────┐
|
|
│ Stack: [PageHome] │
|
|
│ PageHome._doShow() called │
|
|
└────────────────────────────────────────────┘
|
|
|
|
USER CLICKS "START GAME":
|
|
▼ PageHome._onStartGameClick() → ViewManager.open('PageLevel')
|
|
┌────────────────────────────────────────────┐
|
|
│ Stack: [PageHome, PageLevel] │
|
|
│ PageHome._doHide() called │
|
|
│ PageLevel._doShow() called │
|
|
└────────────────────────────────────────────┘
|
|
|
|
COMPLETES LEVEL:
|
|
▼ PageLevel shows PassModal (not in stack, drawn on top)
|
|
┌────────────────────────────────────────────┐
|
|
│ Stack: [PageHome, PageLevel] │
|
|
│ UI Layer: [PageLevel, PassModal@z999] │
|
|
│ PassModal shown with instantiate() │
|
|
└────────────────────────────────────────────┘
|
|
|
|
CLICKS "NEXT LEVEL":
|
|
▼ PassModal closed, PageLevel.nextLevel()
|
|
┌────────────────────────────────────────────┐
|
|
│ Stack: [PageHome, PageLevel] │
|
|
│ PageLevel loads next level config │
|
|
│ PassModal destroyed │
|
|
└────────────────────────────────────────────┘
|
|
|
|
CLICKS BACK FROM PageLevel:
|
|
▼ PageLevel.onIconSettingClick() → ViewManager.back()
|
|
┌────────────────────────────────────────────┐
|
|
│ Stack: [PageHome] │
|
|
│ PageLevel._doHide() called │
|
|
│ PageLevel destroyed (cache=true, not kept) │
|
|
│ PageHome._doShow() called │
|
|
└────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Component Communication Diagram
|
|
|
|
```
|
|
┌─────────────────────┐
|
|
│ ViewManager │
|
|
│ (Singleton) │
|
|
└──────────┬──────────┘
|
|
│
|
|
┌──────────────┼──────────────┐
|
|
│ │ │
|
|
▼ ▼ ▼
|
|
open() back() replace()
|
|
│ │ │
|
|
│ │ │
|
|
┌───────────┴────┐ ┌───┴────┐ ┌──┴───────┐
|
|
│ │ │ │ │ │
|
|
▼ ▼ ▼ ▼ ▼ ▼
|
|
PageHome PageLevel PassModal (back) (new page)
|
|
│ │ │
|
|
│ ├─ Timer ────┐
|
|
│ │ │
|
|
│ ├─ Points │
|
|
│ │ ↑ │
|
|
│ │ │ │
|
|
▼ ▼ │ ▼
|
|
ToastMgr Hints │ PassModal
|
|
│ │ │
|
|
├──────┤ └──▶ Share to WX
|
|
│ │
|
|
Unlock Earn Points
|
|
Consume (UserAssets)
|
|
(check hasPoints)
|
|
```
|
|
|
|
---
|
|
|
|
## Data Flow: Level Completion
|
|
|
|
```
|
|
USER ENTERS ANSWER:
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ PageLevel.onSubmitAnswer() │
|
|
└─────────────────────────────────────────────────────────┘
|
|
│
|
|
├─ getAnswer() ──▶ [Retrieve from EditBox]
|
|
│
|
|
├─ Compare with _currentConfig.answer
|
|
│
|
|
▼
|
|
┌────────────────────────────────────────┐
|
|
│ CORRECT ANSWER │
|
|
└────────────────────────────────────────┘
|
|
│
|
|
├─ _isTransitioning = true [Prevent double-submit]
|
|
│
|
|
├─ stopCountdown()
|
|
│
|
|
├─ playSuccessSound()
|
|
│
|
|
├─ Calculate: timeSpent = 60 - _countdown
|
|
│
|
|
├─┐ Normal Mode:
|
|
│ │
|
|
│ └─▶ UserAssetsManager.earnPoint(levelId, timeSpent)
|
|
│ │
|
|
│ ▼
|
|
│ └─ UPDATE SERVER + LOCAL
|
|
│ └─ Get points amount
|
|
│ └─ PageLevel.updatePointsLabel()
|
|
│
|
|
├─┐ Share Mode:
|
|
│ │
|
|
│ └─▶ ShareManager.reportLevelProgress(...)
|
|
│ │
|
|
│ ▼
|
|
│ └─ SEND TO SERVER (fire-and-forget)
|
|
│
|
|
├─ _showPassModal()
|
|
│ │
|
|
│ ├─ instantiate(passModalPrefab)
|
|
│ │
|
|
│ ├─ Set z-index = 999 (topmost)
|
|
│ │
|
|
│ ├─ Add to Canvas
|
|
│ │
|
|
│ └─ Call passModal.onViewLoad() + onViewShow()
|
|
│
|
|
└─ WAIT FOR USER:
|
|
│
|
|
├─ [Next Level] ──▶ PassModal closed
|
|
│ │
|
|
│ ▼
|
|
│ nextLevel()
|
|
│ │
|
|
│ ┌────────┴────────┐
|
|
│ │ No More Levels │ Next Level
|
|
│ ▼ ▼
|
|
│ BACK HOME Load & Display
|
|
│ Restart Timer
|
|
│
|
|
└─ [Share] ──▶ WxSDK.shareAppMessage()
|
|
│
|
|
└─ Modal stays open
|
|
```
|
|
|
|
---
|
|
|
|
## Hint Unlocking Flow
|
|
|
|
```
|
|
USER CLICKS UNLOCK BUTTON:
|
|
┌──────────────────────────────────────────────┐
|
|
│ PageLevel.onUnlockClue(index: 2 or 3) │
|
|
└──────────────────────────────────────────────┘
|
|
│
|
|
├─ Check: _isUnlocking == true?
|
|
│ └─ YES: Return immediately (prevent double-click)
|
|
│ └─ NO: Continue
|
|
│
|
|
├─ Check: StorageManager.hasPoints()?
|
|
│ └─ NO: Show toast "积分不足!"
|
|
│ └─ YES: Continue
|
|
│
|
|
├─ Set: _isUnlocking = true
|
|
│
|
|
├─ await UserAssetsManager.consumePoint(levelId, index)
|
|
│ │
|
|
│ ├─ ASYNC: Contact server/storage
|
|
│ │
|
|
│ └─ Returns: success boolean
|
|
│
|
|
├─ If success:
|
|
│ │
|
|
│ ├─ updatePointsLabel()
|
|
│ │
|
|
│ ├─ playClickSound()
|
|
│ │
|
|
│ ├─ hideUnlockButton(index)
|
|
│ │
|
|
│ ├─ showClue(index)
|
|
│ │
|
|
│ └─ setClue(index, config.clueN)
|
|
│
|
|
├─ If failed:
|
|
│ │
|
|
│ └─ Show toast "积分不足!"
|
|
│
|
|
└─ Finally: _isUnlocking = false
|
|
```
|
|
|
|
---
|
|
|
|
## Timer Sequence Diagram
|
|
|
|
```
|
|
Timeline: 1s 2s 3s ... 59s 60s 61s
|
|
│ │ │ │ │ │
|
|
▼ ▼ ▼ ▼ ▼ ▼
|
|
countdown: 60 59 58 1 0 (ended)
|
|
│ │ │ │ │
|
|
display: "60s" "59s" "58s" ......... "1s" "0s"
|
|
│ │ │ │ │
|
|
▼ ▼ ▼ ▼ ▼
|
|
|
|
LEVEL LOAD:
|
|
│
|
|
└─ startCountdown()
|
|
│
|
|
├─ _countdown = 60
|
|
├─ _isTimeUp = false
|
|
└─ schedule(onCountdownTick, 1.0) [Repeat every 1 sec]
|
|
│
|
|
▼ (Every 1 second)
|
|
onCountdownTick()
|
|
│
|
|
├─ if (_isTimeUp) return
|
|
│
|
|
├─ _countdown-- (60→59→58...→0)
|
|
│
|
|
├─ updateClockLabel() [Display: "59s"]
|
|
│
|
|
└─ if (_countdown <= 0):
|
|
│
|
|
├─ _isTimeUp = true
|
|
│
|
|
├─ stopCountdown()
|
|
│ └─ unschedule(onCountdownTick)
|
|
│
|
|
└─ onTimeUp()
|
|
│
|
|
└─ playFailSound()
|
|
|
|
ANSWER SUBMITTED (Correct):
|
|
│
|
|
└─ showSuccess()
|
|
│
|
|
├─ stopCountdown()
|
|
│ └─ Timer stops, no more ticks
|
|
│
|
|
└─ _showPassModal()
|
|
|
|
OR:
|
|
|
|
ANSWER SUBMITTED (Wrong):
|
|
│
|
|
└─ Timer continues running...
|
|
│
|
|
└─ User keeps playing
|
|
```
|
|
|
|
---
|
|
|
|
## Input Box Creation & Management
|
|
|
|
```
|
|
LEVEL LOADS:
|
|
│
|
|
└─ _applyLevelConfig(config)
|
|
│
|
|
└─ createSingleInput(config.answer.length)
|
|
│
|
|
├─ clearInputNodes()
|
|
│ │
|
|
│ └─ Destroy old input nodes
|
|
│ (If level changed)
|
|
│
|
|
├─ Hide inputTemplate
|
|
│
|
|
├─ instantiate(inputTemplate)
|
|
│
|
|
├─ Set properties:
|
|
│ │
|
|
│ ├─ active = true
|
|
│ ├─ name = 'singleInput'
|
|
│ ├─ position = (0, 0, 0)
|
|
│ │
|
|
│ └─ Get EditBox component
|
|
│ │
|
|
│ ├─ placeholder = "(Xlength个字)"
|
|
│ ├─ maxLength = answerLength
|
|
│ ├─ string = ""
|
|
│ │
|
|
│ └─ Listen events:
|
|
│ ├─ TEXT_CHANGED → onInputTextChanged()
|
|
│ └─ EDITING_DID_ENDED → onInputEditingEnded()
|
|
│
|
|
├─ Calculate width:
|
|
│ └─ Math.min(600, Math.max(200, length*60+40))
|
|
│
|
|
├─ Set UITransform contentSize to width x 100
|
|
│
|
|
├─ Adjust underline width to match
|
|
│
|
|
├─ Add to inputLayout
|
|
│
|
|
└─ Store in _inputNodes[0]
|
|
|
|
USER ENTERS TEXT:
|
|
│
|
|
├─ EditBox updates string property
|
|
└─ TEXT_CHANGED event fires
|
|
|
|
USER SUBMITS:
|
|
│
|
|
├─ getAnswer()
|
|
│ │
|
|
│ └─ _inputNodes[0].getComponent(EditBox).string.trim()
|
|
│
|
|
└─ Compare with config.answer
|
|
|
|
USER NAVIGATES AWAY:
|
|
│
|
|
└─ onViewDestroy() → clearInputNodes()
|
|
│
|
|
└─ Destroy all input nodes
|
|
```
|
|
|
|
---
|
|
|
|
## Share Challenge Mode Flow
|
|
|
|
```
|
|
LAUNCH FROM WeChat LINK:
|
|
│
|
|
└─ PageLoading.start()
|
|
│
|
|
└─ _startPreload()
|
|
│
|
|
├─ Initialize auth + levels (parallel)
|
|
│
|
|
├─ WxSDK.getShareCodeFromLaunch()
|
|
│ │
|
|
│ └─ Extract from wx.getLaunchOptionsSync().query
|
|
│
|
|
├─ if shareCode exists AND loginSuccess:
|
|
│ │
|
|
│ └─ ShareManager.instance.joinShare(shareCode)
|
|
│ │
|
|
│ ├─ ASYNC: Fetch share session from server
|
|
│ │
|
|
│ ├─ Get shared level set
|
|
│ │
|
|
│ └─ Return: success boolean
|
|
│
|
|
├─ if (joinSuccess):
|
|
│ │
|
|
│ ├─ Set progress = 1
|
|
│ │
|
|
│ ├─ ViewManager.open('PageLevel', {
|
|
│ │ params: { shareMode: true }
|
|
│ │ })
|
|
│ │
|
|
│ └─ Destroy self (PageLoading)
|
|
│
|
|
└─ else (normal flow)
|
|
│
|
|
└─ Preload PageHome → Open PageHome
|
|
|
|
IN SHARE MODE (PageLevel):
|
|
│
|
|
├─ onViewLoad():
|
|
│ │
|
|
│ ├─ params = getParams()
|
|
│ ├─ _isShareMode = params?.shareMode === true
|
|
│ │
|
|
│ ├─ if shareMode:
|
|
│ │ └─ currentLevelIndex = 0 (start at first)
|
|
│ │
|
|
│ └─ initLevel() loads from ShareManager, not LevelDataManager
|
|
│
|
|
├─ showSuccess():
|
|
│ │
|
|
│ ├─ if normal mode:
|
|
│ │ └─ earn points
|
|
│ │
|
|
│ └─ if share mode:
|
|
│ │
|
|
│ └─ ShareManager.reportLevelProgress(levelId, true, timeSpent)
|
|
│ │
|
|
│ └─ FIRE-AND-FORGET (no await)
|
|
│
|
|
├─ nextLevel():
|
|
│ │
|
|
│ ├─ if NOT shareMode:
|
|
│ │ └─ StorageManager.onLevelCompleted() [Save progress]
|
|
│ │
|
|
│ ├─ Check level count:
|
|
│ │ │
|
|
│ │ ├─ Share: ShareManager.getShareLevelCount()
|
|
│ │ └─ Normal: LevelDataManager.getLevelCount()
|
|
│ │
|
|
│ ├─ if (allLevelsComplete):
|
|
│ │ │
|
|
│ │ ├─ if shareMode:
|
|
│ │ │ │
|
|
│ │ │ ├─ ShareManager.clearShareMode()
|
|
│ │ │ │
|
|
│ │ │ └─ ViewManager.replace('PageHome')
|
|
│ │ │
|
|
│ │ └─ if normal:
|
|
│ │ └─ ViewManager.back()
|
|
│ │
|
|
│ └─ else (more levels):
|
|
│ └─ Load next level
|
|
│
|
|
└─ Back to PageHome (replaces, clears share state)
|
|
```
|
|
|
|
---
|
|
|
|
## ViewManager Caching Mechanism
|
|
|
|
```
|
|
OPEN 'PageHome' (first time):
|
|
│
|
|
└─ ViewManager.open('PageHome')
|
|
│
|
|
├─ Check cache: _viewCache.get('PageHome')
|
|
│ └─ Cache miss (undefined)
|
|
│
|
|
├─ _instantiateView('PageHome', prefab)
|
|
│ │
|
|
│ ├─ instantiate(prefab)
|
|
│ │
|
|
│ ├─ Get BaseView component
|
|
│ │
|
|
│ ├─ Set viewId, config, params
|
|
│ │
|
|
│ ├─ Add to container
|
|
│ │
|
|
│ ├─ Call view.onViewLoad() ◄─── Called once
|
|
│ │
|
|
│ ├─ if (config.cache === true): ◄─── PageHome has cache:true
|
|
│ │ └─ _viewCache.set('PageHome', view)
|
|
│ │
|
|
│ └─ _showView(view)
|
|
│ │
|
|
│ └─ view._doShow()
|
|
│ └─ view.onViewShow() ◄─── Called now
|
|
│
|
|
└─ Stack: [PageHome]
|
|
|
|
OPEN 'PageLevel':
|
|
│
|
|
└─ ViewManager.open('PageLevel')
|
|
│
|
|
├─ PageHome._doHide()
|
|
│ └─ view.onViewHide() ◄─── First hide
|
|
│
|
|
└─ (Same as above for PageLevel)
|
|
│
|
|
└─ Stack: [PageHome, PageLevel]
|
|
|
|
BACK FROM 'PageLevel':
|
|
│
|
|
└─ ViewManager.back()
|
|
│
|
|
├─ Pop stack: PageLevel
|
|
│
|
|
├─ PageLevel._doHide()
|
|
│ └─ view.onViewHide()
|
|
│
|
|
├─ Decide: shouldDestroy?
|
|
│ │
|
|
│ └─ PageLevel has cache:true
|
|
│ └─ shouldDestroy = false (keep cached)
|
|
│
|
|
├─ Get current: PageHome
|
|
│
|
|
├─ PageHome._doShow()
|
|
│ └─ view.onViewShow() ◄─── Called again
|
|
│
|
|
└─ Stack: [PageHome]
|
|
Caches: {PageHome, PageLevel}
|
|
|
|
OPEN 'PageLevel' AGAIN (from cache):
|
|
│
|
|
└─ ViewManager.open('PageLevel')
|
|
│
|
|
├─ Check cache: _viewCache.get('PageLevel')
|
|
│ └─ Cache hit! (PageLevel instance)
|
|
│
|
|
├─ _showView(cachedView)
|
|
│ │
|
|
│ ├─ PageHome._doHide()
|
|
│ │
|
|
│ └─ cachedView._doShow()
|
|
│ └─ view.onViewShow() ◄─── Called again (2nd time)
|
|
│ onViewLoad() NOT called again ◄─── Important!
|
|
│
|
|
└─ Stack: [PageHome, PageLevel]
|
|
Same instance reused
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling Paths
|
|
|
|
```
|
|
LEVEL LOADING ERROR:
|
|
│
|
|
├─ initLevel()
|
|
│ │
|
|
│ └─ await LevelDataManager.ensureLevelReady(index)
|
|
│ │
|
|
│ └─ Returns null (error)
|
|
│
|
|
└─ Log warning, return early
|
|
│
|
|
└─ UI stays as-is (might be blank)
|
|
|
|
INSUFFICIENT POINTS:
|
|
│
|
|
└─ onUnlockClue()
|
|
│
|
|
├─ StorageManager.hasPoints() → false
|
|
│
|
|
└─ ToastManager.show("积分不足!")
|
|
│
|
|
└─ User sees notification, unlock blocked
|
|
|
|
DOUBLE-SUBMIT PREVENTION:
|
|
│
|
|
├─ onSubmitAnswer()
|
|
│ │
|
|
│ └─ if (_isTransitioning) return ◄─── Blocks multiple calls
|
|
│
|
|
└─ showSuccess()
|
|
│
|
|
└─ _isTransitioning = true ◄─── Set to block
|
|
│
|
|
└─ After modal shown/closed
|
|
└─ (Would be reset on nextLevel)
|
|
|
|
DOUBLE-CLICK UNLOCK:
|
|
│
|
|
└─ onUnlockClue()
|
|
│
|
|
└─ if (_isUnlocking) return ◄─── Blocks rapid clicks
|
|
│
|
|
└─ _isUnlocking = true at start
|
|
└─ Finally: _isUnlocking = false ◄─── Reset after async
|
|
```
|
|
|