feat: 进入关卡时 toast 提示体力消耗,修复 StorageManager 接口位置和 WxSDK 访问级别
- 进入关卡成功后显示 toast 提示消耗体力及剩余体力 - 将 StorageManager 中 UserInfo 接口移至模块顶层,修复嵌套接口语法问题 - WxSDK.getWx() 改为 static 公开方法,便于外部调用
This commit is contained in:
@@ -1,196 +1,348 @@
|
||||
# QUICK REFERENCE - Game Points/Score System
|
||||
# UI Components - Quick Reference Guide
|
||||
|
||||
## 🎮 What is the "Currency"?
|
||||
**LIVES** - Not traditional points/coins, but a renewable "health" resource.
|
||||
## Component Overview
|
||||
|
||||
## 📊 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
|
||||
| Component | Purpose | Extends | Key Responsibilities |
|
||||
|-----------|---------|---------|----------------------|
|
||||
| **main.ts** | App bootstrap | Component | Register pages, init ViewManager & ToastManager |
|
||||
| **PageLoading.ts** | Splash screen | Component | Load data, sync progress, detect share code |
|
||||
| **PageHome.ts** | Landing page | BaseView | Navigation hub (game/PK buttons) |
|
||||
| **PageLevel.ts** | Game level | BaseView | Core gameplay (timer, hints, scoring, input) |
|
||||
| **PagePreviewLevels.ts** | Level preview | BaseView | Scrollable list of selected levels |
|
||||
| **PassModal.ts** | Completion modal | BaseView | Next level & share buttons, success sound |
|
||||
| **Toast.ts** | Notification | Component | Display message with fade animation |
|
||||
| **ToastManager.ts** | Toast system | Singleton | Centralized toast creation & display |
|
||||
| **BaseView.ts** | Page base class | Component | Lifecycle interface for all pages |
|
||||
| **ViewManager.ts** | Page manager | Singleton | Navigation, stacking, caching, lifecycle |
|
||||
|
||||
---
|
||||
|
||||
**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.
|
||||
## Lifecycle Stages
|
||||
|
||||
### Page Lifecycle (BaseView)
|
||||
```
|
||||
onViewLoad() → onViewShow() → onViewHide() → onViewDestroy()
|
||||
(once) (each show) (each hide) (on destroy)
|
||||
```
|
||||
|
||||
### App Startup Flow
|
||||
```
|
||||
main.onLoad()
|
||||
↓ (Initialize ViewManager + register pages)
|
||||
PageLoading.start()
|
||||
↓ (Load auth + levels in parallel, check share code)
|
||||
PageHome (normal) OR PageLevel (share mode)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Data Flows
|
||||
|
||||
### Level Progression
|
||||
```
|
||||
PageLevel loads config → Displays image + clue1 → User enters answer
|
||||
↓ (correct) → Earn points → Show PassModal → Next level
|
||||
↓ (wrong) → Show toast → Continue playing
|
||||
↓ (timeout) → Play fail sound
|
||||
```
|
||||
|
||||
### Hint Unlocking
|
||||
```
|
||||
User clicks unlock button → Check points available
|
||||
↓ (yes) → Consume point from UserAssetsManager
|
||||
↓ → Hide unlock button, show clue
|
||||
↓ (no) → Show "insufficient points" toast
|
||||
```
|
||||
|
||||
### Share Challenge Mode
|
||||
```
|
||||
WeChat link → PageLoading detects share code
|
||||
↓ → ShareManager.joinShare(code)
|
||||
↓ → Play through shared levels (doesn't save local progress)
|
||||
↓ → Report progress to server
|
||||
↓ → Return to PageHome
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Properties Summary
|
||||
|
||||
### PageLevel (Most Complex)
|
||||
**UI Elements:**
|
||||
- inputLayout, submitButton, inputTemplate, actionNode
|
||||
- iconSetting (back button)
|
||||
- tipsLayout, tipsItem1/2/3 (clues)
|
||||
- unLockItem2/3 (unlock buttons)
|
||||
- clockLabel (timer), liveLabel (points)
|
||||
- mainImage (level image)
|
||||
|
||||
**Audio:**
|
||||
- clickAudio, successAudio, failAudio
|
||||
|
||||
**Internal State:**
|
||||
- _countdown (60 sec timer)
|
||||
- _isTimeUp (timer expired flag)
|
||||
- _currentConfig (level data)
|
||||
- _isTransitioning (prevent double-submit)
|
||||
- _isUnlocking (prevent double-click unlock)
|
||||
- _passModalNode (modal reference)
|
||||
- _isShareMode (shared vs normal)
|
||||
|
||||
**Key Methods:**
|
||||
- onViewLoad/Show/Hide/Destroy (lifecycle)
|
||||
- initLevel() (async load config)
|
||||
- createSingleInput(length) (input box)
|
||||
- onUnlockClue(index) (consume points for hints)
|
||||
- onSubmitAnswer() (check answer)
|
||||
- showSuccess() (correct: earn points, show modal)
|
||||
- showError() (wrong: toast + vibration)
|
||||
- nextLevel() (progress to next)
|
||||
- startCountdown() / onCountdownTick() (60s timer)
|
||||
|
||||
---
|
||||
|
||||
## Points & Scoring System
|
||||
|
||||
**Earning Points:**
|
||||
- `UserAssetsManager.earnPoint(levelId, timeSpent)`
|
||||
- Called in PageLevel.showSuccess()
|
||||
- Time spent = 60 - countdown
|
||||
|
||||
**Consuming Points:**
|
||||
- `UserAssetsManager.consumePoint(levelId, hintIndex)`
|
||||
- Called in PageLevel.onUnlockClue()
|
||||
- Check `StorageManager.hasPoints()` before consuming
|
||||
|
||||
**Displaying Points:**
|
||||
- `StorageManager.getPoints()` → Display as "x {points}"
|
||||
- Called in PageLevel.updatePointsLabel()
|
||||
|
||||
---
|
||||
|
||||
## Timer System
|
||||
|
||||
**60-Second Countdown:**
|
||||
- Starts when level loads: `startCountdown()`
|
||||
- Ticks every 1 second: `onCountdownTick()`
|
||||
- Displayed as: "60s", "59s", ..., "0s"
|
||||
- On timeout: plays fail sound, doesn't end game
|
||||
|
||||
**Stop Timer:**
|
||||
- `stopCountdown()` called when:
|
||||
- Answer submitted (correct)
|
||||
- Page destroyed
|
||||
- All levels completed
|
||||
|
||||
---
|
||||
|
||||
## Hint/Clue System
|
||||
|
||||
**3 Clues Per Level:**
|
||||
1. **Clue 1** - Always visible (free)
|
||||
2. **Clue 2** - Unlock button, costs points
|
||||
3. **Clue 3** - Unlock button, costs points
|
||||
|
||||
**Display:**
|
||||
- Text in tipsItem1/2/3 nodes
|
||||
- Hidden if locked (tipsItem.active = false)
|
||||
- Unlock button visible if locked
|
||||
|
||||
**Unlock Flow:**
|
||||
1. User clicks unLockItem2 or unLockItem3
|
||||
2. Check points: `StorageManager.hasPoints()`
|
||||
3. Consume: `UserAssetsManager.consumePoint(levelId, index)`
|
||||
4. Hide button, show clue, update UI
|
||||
|
||||
---
|
||||
|
||||
## Navigation Stack
|
||||
|
||||
**Structure:**
|
||||
- LIFO stack managed by ViewManager
|
||||
- Each open() adds to stack
|
||||
- back() pops from stack
|
||||
- replace() swaps top
|
||||
|
||||
**Example Session:**
|
||||
```
|
||||
[PageHome]
|
||||
↓ open PageLevel
|
||||
[PageHome, PageLevel]
|
||||
↓ open PassModal (or inline)
|
||||
[PageHome, PageLevel, PassModal?]
|
||||
↓ close/back
|
||||
[PageHome]
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `ViewManager.open(viewId, options?)` - Push to stack
|
||||
- `ViewManager.back()` - Pop from stack, show previous
|
||||
- `ViewManager.replace(viewId, options?)` - Pop current, push new
|
||||
|
||||
---
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
**Cached Views (default):**
|
||||
- Instance reused on re-open
|
||||
- onViewLoad() called once
|
||||
- onViewShow() called each time
|
||||
- Memory persists
|
||||
|
||||
**Non-Cached Views:**
|
||||
- New instance each time
|
||||
- onViewLoad() called each time
|
||||
- Memory released on close
|
||||
|
||||
**Configuration:**
|
||||
```typescript
|
||||
// In main.ts registration
|
||||
ViewManager.instance.register('PageHome', {
|
||||
prefab: pageHomePrefab,
|
||||
cache: true, // ← Reuse instance
|
||||
zIndex: 0 // ← Layer depth
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Toast System
|
||||
|
||||
**Show Toast:**
|
||||
```typescript
|
||||
ToastManager.show("消息内容", 2000); // duration in ms
|
||||
```
|
||||
|
||||
**Initialization (main.ts):**
|
||||
```typescript
|
||||
ToastManager.instance.init(toastPrefab, canvasNode);
|
||||
```
|
||||
|
||||
**Toast Behavior:**
|
||||
- Creates Toast instance from prefab
|
||||
- Displays for duration (default 2000ms)
|
||||
- Fades out over 300ms
|
||||
- Auto-destroys
|
||||
|
||||
---
|
||||
|
||||
## WeChat Integration
|
||||
|
||||
**Share Initialization (PageHome):**
|
||||
```typescript
|
||||
WxSDK.initShare({
|
||||
title: '写英语',
|
||||
imageUrl: '',
|
||||
query: ''
|
||||
});
|
||||
```
|
||||
|
||||
**Share in Challenge (PassModal):**
|
||||
```typescript
|
||||
WxSDK.shareAppMessage({
|
||||
title: '快来一起玩这款游戏吧',
|
||||
query: `level={levelIndex}`
|
||||
});
|
||||
```
|
||||
|
||||
**Share Code Detection (PageLoading):**
|
||||
```typescript
|
||||
const shareCode = WxSDK.getShareCodeFromLaunch();
|
||||
if (shareCode) {
|
||||
const joined = await ShareManager.instance.joinShare(shareCode);
|
||||
// Launch PageLevel in share mode
|
||||
}
|
||||
```
|
||||
|
||||
**Other WeChat Features:**
|
||||
- `WxSDK.vibrateLong()` - Haptic feedback on wrong answer
|
||||
- `WxSDK.isWechat()` - Check if running in WeChat
|
||||
- `checkPrivacySetting()` - Check privacy authorization
|
||||
- `requirePrivacyAuthorize()` - Prompt for privacy consent
|
||||
|
||||
---
|
||||
|
||||
## Error Handling & Edge Cases
|
||||
|
||||
**Level Loading:**
|
||||
- Try cache first, then async load
|
||||
- Preload next level without blocking
|
||||
- Handle missing config gracefully
|
||||
|
||||
**Point Consumption:**
|
||||
- Check availability before consuming
|
||||
- Show toast if insufficient
|
||||
- Prevent double-click with flag
|
||||
|
||||
**Answer Submission:**
|
||||
- Prevent multiple submissions with transition flag
|
||||
- Trim whitespace from answer
|
||||
- Case-sensitive comparison
|
||||
|
||||
**Page Navigation:**
|
||||
- Share mode: clears share state before returning home
|
||||
- Normal mode: simple back() navigation
|
||||
- Modal: stays on top (z-index 999)
|
||||
|
||||
---
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Add Toast
|
||||
```typescript
|
||||
ToastManager.show('提示信息');
|
||||
```
|
||||
|
||||
### Navigate to Page
|
||||
```typescript
|
||||
ViewManager.instance.open('PageLevel'); // Push
|
||||
ViewManager.instance.back(); // Pop
|
||||
ViewManager.instance.replace('PageHome');// Swap
|
||||
```
|
||||
|
||||
### Get Current Level
|
||||
```typescript
|
||||
const index = StorageManager.getCurrentLevelIndex();
|
||||
```
|
||||
|
||||
### Check/Consume Points
|
||||
```typescript
|
||||
if (StorageManager.hasPoints()) {
|
||||
await UserAssetsManager.consumePoint(levelId, hintIndex);
|
||||
}
|
||||
```
|
||||
|
||||
### Start Game
|
||||
```typescript
|
||||
// From PageHome
|
||||
ViewManager.instance.open('PageLevel');
|
||||
```
|
||||
|
||||
### Create & Share Challenge
|
||||
```typescript
|
||||
// User flow:
|
||||
PageHome → [PK Button] → PageWriteLevels [Select 6]
|
||||
→ PagePreviewLevels [Verify]
|
||||
→ PassModal [Share Button]
|
||||
→ WxSDK.shareAppMessage()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Size & Complexity
|
||||
|
||||
| File | Lines | Complexity |
|
||||
|------|-------|-----------|
|
||||
| main.ts | 92 | Simple (init only) |
|
||||
| PageLoading.ts | 141 | Medium (parallel loading, sync) |
|
||||
| PageHome.ts | 119 | Simple (two buttons) |
|
||||
| PageLevel.ts | 823 | **Very High** (core game logic) |
|
||||
| PagePreviewLevels.ts | 247 | Medium (scroll list) |
|
||||
| PassModal.ts | 155 | Simple (two buttons) |
|
||||
| Toast.ts | 50 | Simple (fade animation) |
|
||||
| ToastManager.ts | 59 | Simple (factory pattern) |
|
||||
| BaseView.ts | 132 | Medium (lifecycle) |
|
||||
| ViewManager.ts | 320 | High (stack navigation) |
|
||||
|
||||
**Total: ~2,138 lines** | **Focal Point: PageLevel.ts (38% of code)**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user