Files
mp-xieyingeng/ARCHITECTURE_DIAGRAM.md
richarjiang 69c0986996 feat: 进入关卡时 toast 提示体力消耗,修复 StorageManager 接口位置和 WxSDK 访问级别
- 进入关卡成功后显示 toast 提示消耗体力及剩余体力
- 将 StorageManager 中 UserInfo 接口移至模块顶层,修复嵌套接口语法问题
- WxSDK.getWx() 改为 static 公开方法,便于外部调用
2026-04-10 10:10:19 +08:00

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
```