feat: 进入关卡时 toast 提示体力消耗,修复 StorageManager 接口位置和 WxSDK 访问级别
- 进入关卡成功后显示 toast 提示消耗体力及剩余体力 - 将 StorageManager 中 UserInfo 接口移至模块顶层,修复嵌套接口语法问题 - WxSDK.getWx() 改为 static 公开方法,便于外部调用
This commit is contained in:
665
ARCHITECTURE_DIAGRAM.md
Normal file
665
ARCHITECTURE_DIAGRAM.md
Normal file
@@ -0,0 +1,665 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user