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