|
@@ -50,6 +50,7 @@ type Card = {
|
|
|
isFlipped: boolean;
|
|
isFlipped: boolean;
|
|
|
isMatched: boolean;
|
|
isMatched: boolean;
|
|
|
pairId: number;
|
|
pairId: number;
|
|
|
|
|
+ isAnimating?: boolean; // 添加这一行
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
type Difficulty = 'easy' | 'medium' | 'hard';
|
|
type Difficulty = 'easy' | 'medium' | 'hard';
|
|
@@ -71,12 +72,25 @@ export class WordMatchPage {
|
|
|
cards: [] as Card[],
|
|
cards: [] as Card[],
|
|
|
flippedIndices: [] as number[],
|
|
flippedIndices: [] as number[],
|
|
|
matchedPairs: 0,
|
|
matchedPairs: 0,
|
|
|
- timeLeft: 90,
|
|
|
|
|
|
|
+ timeLeft: 60,
|
|
|
isGameOver: false,
|
|
isGameOver: false,
|
|
|
score: 0,
|
|
score: 0,
|
|
|
timer: null as any,
|
|
timer: null as any,
|
|
|
difficulty: 'medium' as Difficulty
|
|
difficulty: 'medium' as Difficulty
|
|
|
};
|
|
};
|
|
|
|
|
+
|
|
|
|
|
+ // 在类顶部添加时间奖励常量
|
|
|
|
|
+private readonly SCORE_RULES = {
|
|
|
|
|
+ BASE_MATCH: 10, // 每对匹配基础分
|
|
|
|
|
+ COMPLETION_BONUS: 5, // 完成游戏基础奖励
|
|
|
|
|
+ TIME_BONUS: [ // 时间奖励阶梯
|
|
|
|
|
+ { threshold: 10, bonus: 20 },
|
|
|
|
|
+ { threshold: 15, bonus: 15 },
|
|
|
|
|
+ { threshold: 20, bonus: 10 },
|
|
|
|
|
+ { threshold: 30, bonus: 8 },
|
|
|
|
|
+ { threshold: Infinity, bonus: 0 }
|
|
|
|
|
+ ]
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
selectedDifficulty: Difficulty | null = null;
|
|
selectedDifficulty: Difficulty | null = null;
|
|
|
private flipTimeout: any = null;
|
|
private flipTimeout: any = null;
|
|
@@ -143,36 +157,39 @@ export class WordMatchPage {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private setupCards() {
|
|
private setupCards() {
|
|
|
- const pairCount = this.getCardCountByDifficulty() / 2;
|
|
|
|
|
-
|
|
|
|
|
- // 从静态数据中随机选取指定数量的单词对
|
|
|
|
|
- const shuffledWords = [...STATIC_WORDS].sort(() => Math.random() - 0.5);
|
|
|
|
|
- const selectedWords = shuffledWords.slice(0, pairCount);
|
|
|
|
|
-
|
|
|
|
|
- // Create word cards
|
|
|
|
|
- const wordCards: Card[] = selectedWords.map((word, index) => ({
|
|
|
|
|
- id: index * 2,
|
|
|
|
|
- content: word.english,
|
|
|
|
|
- isWord: true,
|
|
|
|
|
- isFlipped: false,
|
|
|
|
|
- isMatched: false,
|
|
|
|
|
- pairId: index
|
|
|
|
|
- }));
|
|
|
|
|
-
|
|
|
|
|
- // Create definition cards
|
|
|
|
|
- const definitionCards: Card[] = selectedWords.map((word, index) => ({
|
|
|
|
|
- id: index * 2 + 1,
|
|
|
|
|
- content: word.chinese,
|
|
|
|
|
- isWord: false,
|
|
|
|
|
- isFlipped: false,
|
|
|
|
|
- isMatched: false,
|
|
|
|
|
- pairId: index
|
|
|
|
|
- }));
|
|
|
|
|
-
|
|
|
|
|
- // Combine and shuffle
|
|
|
|
|
- this.gameState.cards = [...wordCards, ...definitionCards]
|
|
|
|
|
- .sort(() => Math.random() - 0.5);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const pairCount = this.getCardCountByDifficulty() / 2;
|
|
|
|
|
+
|
|
|
|
|
+ // 从静态数据中随机选取指定数量的单词对
|
|
|
|
|
+ const shuffledWords = [...STATIC_WORDS].sort(() => Math.random() - 0.5);
|
|
|
|
|
+ const selectedWords = shuffledWords.slice(0, pairCount);
|
|
|
|
|
+
|
|
|
|
|
+ // Create word cards
|
|
|
|
|
+ const wordCards: Card[] = selectedWords.map((word, index) => ({
|
|
|
|
|
+ id: index * 2,
|
|
|
|
|
+ content: word.english,
|
|
|
|
|
+ isWord: true,
|
|
|
|
|
+ isFlipped: false,
|
|
|
|
|
+ isMatched: false,
|
|
|
|
|
+ pairId: index,
|
|
|
|
|
+ isAnimating: false // 添加这一行
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // Create definition cards
|
|
|
|
|
+ const definitionCards: Card[] = selectedWords.map((word, index) => ({
|
|
|
|
|
+ id: index * 2 + 1,
|
|
|
|
|
+ content: word.chinese,
|
|
|
|
|
+ isWord: false,
|
|
|
|
|
+ isFlipped: false,
|
|
|
|
|
+ isMatched: false,
|
|
|
|
|
+ pairId: index,
|
|
|
|
|
+ isAnimating: false // 添加这一行
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // Combine and shuffle
|
|
|
|
|
+ this.gameState.cards = [...wordCards, ...definitionCards]
|
|
|
|
|
+ .sort(() => Math.random() - 0.5);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
|
|
|
private startTimer() {
|
|
private startTimer() {
|
|
|
this.gameState.timer = setInterval(() => {
|
|
this.gameState.timer = setInterval(() => {
|
|
@@ -214,72 +231,102 @@ export class WordMatchPage {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private checkMatch() {
|
|
|
|
|
- const [firstIndex, secondIndex] = this.gameState.flippedIndices;
|
|
|
|
|
- const firstCard = this.gameState.cards[firstIndex];
|
|
|
|
|
- const secondCard = this.gameState.cards[secondIndex];
|
|
|
|
|
|
|
+ private checkMatch() {
|
|
|
|
|
+ const [firstIndex, secondIndex] = this.gameState.flippedIndices;
|
|
|
|
|
+ const firstCard = this.gameState.cards[firstIndex];
|
|
|
|
|
+ const secondCard = this.gameState.cards[secondIndex];
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否匹配
|
|
|
|
|
+ if (firstCard.pairId === secondCard.pairId && firstCard.isWord !== secondCard.isWord) {
|
|
|
|
|
+ // 匹配成功 - 先等待两张卡片都完成翻转动画
|
|
|
|
|
+ this.canFlip = false;
|
|
|
|
|
|
|
|
- // 检查是否匹配(一个英文一个中文,且pairId相同)
|
|
|
|
|
- if (firstCard.pairId === secondCard.pairId && firstCard.isWord !== secondCard.isWord) {
|
|
|
|
|
- // 匹配成功
|
|
|
|
|
- firstCard.isMatched = true;
|
|
|
|
|
- secondCard.isMatched = true;
|
|
|
|
|
- this.gameState.matchedPairs++;
|
|
|
|
|
-
|
|
|
|
|
- // 加分
|
|
|
|
|
- this.gameState.score += 10;
|
|
|
|
|
-
|
|
|
|
|
- // 清空翻转索引
|
|
|
|
|
- this.gameState.flippedIndices = [];
|
|
|
|
|
- this.canFlip = true;
|
|
|
|
|
-
|
|
|
|
|
- // 检查是否获胜
|
|
|
|
|
- if (this.gameState.matchedPairs === this.getTotalPairs()) {
|
|
|
|
|
- this.endGame(true);
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- // 匹配失败,延迟后翻回
|
|
|
|
|
- this.flipTimeout = setTimeout(() => {
|
|
|
|
|
- this.ngZone.run(() => {
|
|
|
|
|
- // 翻回卡片
|
|
|
|
|
- if (firstIndex !== undefined && this.gameState.cards[firstIndex]) {
|
|
|
|
|
- this.gameState.cards[firstIndex].isFlipped = false;
|
|
|
|
|
- }
|
|
|
|
|
- if (secondIndex !== undefined && this.gameState.cards[secondIndex]) {
|
|
|
|
|
- this.gameState.cards[secondIndex].isFlipped = false;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 扣分
|
|
|
|
|
- this.gameState.score = Math.max(0, this.gameState.score - 2);
|
|
|
|
|
-
|
|
|
|
|
- // 清空翻转索引并允许继续翻转
|
|
|
|
|
- this.gameState.flippedIndices = [];
|
|
|
|
|
|
|
+ // 延迟400ms确保动画完成
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.ngZone.run(() => {
|
|
|
|
|
+ firstCard.isMatched = true;
|
|
|
|
|
+ secondCard.isMatched = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 添加匹配成功动画
|
|
|
|
|
+ firstCard.isAnimating = true;
|
|
|
|
|
+ secondCard.isAnimating = true;
|
|
|
|
|
+
|
|
|
|
|
+ this.gameState.matchedPairs++;
|
|
|
|
|
+ this.gameState.score += this.SCORE_RULES.BASE_MATCH;
|
|
|
|
|
+ this.gameState.flippedIndices = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 300ms后移除动画
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ firstCard.isAnimating = false;
|
|
|
|
|
+ secondCard.isAnimating = false;
|
|
|
this.canFlip = true;
|
|
this.canFlip = true;
|
|
|
- this.flipTimeout = null;
|
|
|
|
|
- });
|
|
|
|
|
- }, 800); // 800ms后翻回
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否获胜
|
|
|
|
|
+ if (this.gameState.matchedPairs === this.getTotalPairs()) {
|
|
|
|
|
+ // 立即停止计时器
|
|
|
|
|
+ clearInterval(this.gameState.timer);
|
|
|
|
|
+ this.gameState.timer = null;
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟1.5秒后进入结算(仅用于动画效果)
|
|
|
|
|
+ setTimeout(() => this.endGame(true), 1500);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 300);
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 400);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 匹配失败,延迟后翻回
|
|
|
|
|
+ this.flipTimeout = setTimeout(() => {
|
|
|
|
|
+ this.ngZone.run(() => {
|
|
|
|
|
+ // 翻回卡片
|
|
|
|
|
+ if (firstIndex !== undefined && this.gameState.cards[firstIndex]) {
|
|
|
|
|
+ this.gameState.cards[firstIndex].isFlipped = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (secondIndex !== undefined && this.gameState.cards[secondIndex]) {
|
|
|
|
|
+ this.gameState.cards[secondIndex].isFlipped = false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 扣分
|
|
|
|
|
+ this.gameState.score = Math.max(0, this.gameState.score - 2);
|
|
|
|
|
+
|
|
|
|
|
+ // 清空翻转索引并允许继续翻转
|
|
|
|
|
+ this.gameState.flippedIndices = [];
|
|
|
|
|
+ this.canFlip = true;
|
|
|
|
|
+ this.flipTimeout = null;
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 800);
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- endGame(isWin = false) {
|
|
|
|
|
- clearInterval(this.gameState.timer);
|
|
|
|
|
- this.gameState.isGameOver = true;
|
|
|
|
|
-
|
|
|
|
|
- if (isWin) {
|
|
|
|
|
- // 获胜奖励:剩余时间加分
|
|
|
|
|
- this.gameState.score += Math.floor(this.gameState.timeLeft * 2);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+endGame(isWin = false) {
|
|
|
|
|
+ clearInterval(this.gameState.timer);
|
|
|
|
|
+ this.gameState.isGameOver = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 计算时间奖励(剩余时间 = 总时间90秒 - 已用时间)
|
|
|
|
|
+ const timeUsed = 90 - this.gameState.timeLeft;
|
|
|
|
|
+ let timeBonus = 0;
|
|
|
|
|
|
|
|
- getCardCountByDifficulty(): number {
|
|
|
|
|
- switch (this.gameState.difficulty) {
|
|
|
|
|
- case 'easy': return 8; // 4对
|
|
|
|
|
- case 'medium': return 12; // 6对
|
|
|
|
|
- case 'hard': return 16; // 8对
|
|
|
|
|
- default: return 12;
|
|
|
|
|
|
|
+ if (isWin) {
|
|
|
|
|
+ // 应用时间阶梯奖励
|
|
|
|
|
+ for (const rule of this.SCORE_RULES.TIME_BONUS) {
|
|
|
|
|
+ if (timeUsed <= rule.threshold) {
|
|
|
|
|
+ timeBonus = rule.bonus;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 总得分 = 匹配分 + 完成奖励 + 时间奖励
|
|
|
|
|
+ this.gameState.score += this.SCORE_RULES.COMPLETION_BONUS + timeBonus;
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
|
|
+private getCardCountByDifficulty(): number {
|
|
|
|
|
+ switch (this.gameState.difficulty) {
|
|
|
|
|
+ case 'easy': return 6; // 3对 (6张卡片)
|
|
|
|
|
+ case 'medium': return 10; // 5对 (10张卡片)
|
|
|
|
|
+ case 'hard': return 12; // 6对 (12张卡片)
|
|
|
|
|
+ default: return 10;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
getTotalPairs(): number {
|
|
getTotalPairs(): number {
|
|
|
return this.getCardCountByDifficulty() / 2;
|
|
return this.getCardCountByDifficulty() / 2;
|
|
|
}
|
|
}
|