336 lines
9.9 KiB
JavaScript
336 lines
9.9 KiB
JavaScript
|
|
// 成就系統核心
|
|||
|
|
import { apiService } from './api-service.js'
|
|||
|
|
import { ACHIEVEMENTS } from '../data/achievements.js'
|
|||
|
|
|
|||
|
|
export class AchievementSystem {
|
|||
|
|
constructor(petSystem, eventSystem, templeSystem, api = apiService) {
|
|||
|
|
this.petSystem = petSystem
|
|||
|
|
this.eventSystem = eventSystem
|
|||
|
|
this.templeSystem = templeSystem
|
|||
|
|
this.api = api
|
|||
|
|
this.achievements = ACHIEVEMENTS
|
|||
|
|
this.unlockedAchievements = [] // 已解鎖的成就 ID 列表
|
|||
|
|
this.achievementStats = {
|
|||
|
|
// 動作計數
|
|||
|
|
actionCounts: {
|
|||
|
|
feed: 0,
|
|||
|
|
play: 0,
|
|||
|
|
clean: 0,
|
|||
|
|
heal: 0,
|
|||
|
|
sleep: 0,
|
|||
|
|
pray: 0,
|
|||
|
|
drawFortune: 0
|
|||
|
|
},
|
|||
|
|
// 事件計數
|
|||
|
|
eventCount: 0,
|
|||
|
|
eventTypeCounts: {
|
|||
|
|
good: 0,
|
|||
|
|
bad: 0,
|
|||
|
|
weird: 0,
|
|||
|
|
rare: 0
|
|||
|
|
},
|
|||
|
|
// 特殊狀態
|
|||
|
|
recoveredFromDying: false,
|
|||
|
|
perfectStateReached: false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化(從 API 載入已解鎖成就)
|
|||
|
|
async initialize() {
|
|||
|
|
try {
|
|||
|
|
const saved = await this.api.getAchievements()
|
|||
|
|
if (saved) {
|
|||
|
|
this.unlockedAchievements = saved.unlocked || []
|
|||
|
|
this.achievementStats = { ...this.achievementStats, ...saved.stats }
|
|||
|
|
}
|
|||
|
|
console.log(`[AchievementSystem] 載入 ${this.unlockedAchievements.length} 個已解鎖成就`)
|
|||
|
|
|
|||
|
|
// 重新應用已解鎖成就的加成
|
|||
|
|
await this.reapplyAchievementBuffs()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('[AchievementSystem] 載入成就失敗:', error)
|
|||
|
|
// 降級到本地載入
|
|||
|
|
const saved = localStorage.getItem('achievements')
|
|||
|
|
if (saved) {
|
|||
|
|
const data = JSON.parse(saved)
|
|||
|
|
this.unlockedAchievements = data.unlocked || []
|
|||
|
|
this.achievementStats = { ...this.achievementStats, ...data.stats }
|
|||
|
|
|
|||
|
|
// 重新應用已解鎖成就的加成
|
|||
|
|
await this.reapplyAchievementBuffs()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 重新應用已解鎖成就的加成(初始化時使用)
|
|||
|
|
async reapplyAchievementBuffs() {
|
|||
|
|
if (!this.petSystem) return
|
|||
|
|
|
|||
|
|
const unlocked = this.achievements.filter(a => this.unlockedAchievements.includes(a.id))
|
|||
|
|
const achievementBuffs = {}
|
|||
|
|
|
|||
|
|
// 累積所有已解鎖成就的加成
|
|||
|
|
for (const achievement of unlocked) {
|
|||
|
|
if (achievement.reward && achievement.reward.buffs) {
|
|||
|
|
for (const [key, value] of Object.entries(achievement.reward.buffs)) {
|
|||
|
|
achievementBuffs[key] = (achievementBuffs[key] || 0) + value
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新寵物狀態
|
|||
|
|
if (Object.keys(achievementBuffs).length > 0) {
|
|||
|
|
await this.petSystem.updateState({ achievementBuffs })
|
|||
|
|
// 重新計算戰鬥數值
|
|||
|
|
this.petSystem.calculateCombatStats()
|
|||
|
|
console.log(`[AchievementSystem] 已重新應用 ${unlocked.length} 個成就的加成`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 檢查成就條件
|
|||
|
|
checkCondition(achievement) {
|
|||
|
|
const condition = achievement.condition
|
|||
|
|
const state = this.petSystem.getState()
|
|||
|
|
|
|||
|
|
switch (condition.type) {
|
|||
|
|
case 'age':
|
|||
|
|
return state.ageSeconds >= condition.value
|
|||
|
|
|
|||
|
|
case 'stage':
|
|||
|
|
return state.stage === condition.value
|
|||
|
|
|
|||
|
|
case 'stats':
|
|||
|
|
// 檢查所有指定屬性是否達到要求
|
|||
|
|
for (const [stat, value] of Object.entries(condition.stats)) {
|
|||
|
|
if ((state[stat] || 0) < value) {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
|
|||
|
|
case 'action_count':
|
|||
|
|
return this.achievementStats.actionCounts[condition.action] >= condition.value
|
|||
|
|
|
|||
|
|
case 'deity_favor':
|
|||
|
|
// 檢查任一神明好感度
|
|||
|
|
const favors = state.deityFavors || {}
|
|||
|
|
return Object.values(favors).some(favor => favor >= condition.value)
|
|||
|
|
|
|||
|
|
case 'event_count':
|
|||
|
|
return this.achievementStats.eventCount >= condition.value
|
|||
|
|
|
|||
|
|
case 'event_type_count':
|
|||
|
|
return this.achievementStats.eventTypeCounts[condition.eventType] >= condition.value
|
|||
|
|
|
|||
|
|
case 'recovered_from_dying':
|
|||
|
|
return this.achievementStats.recoveredFromDying === condition.value
|
|||
|
|
|
|||
|
|
case 'perfect_state':
|
|||
|
|
if (condition.value) {
|
|||
|
|
return state.hunger >= 100 &&
|
|||
|
|
state.happiness >= 100 &&
|
|||
|
|
state.health >= 100
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 檢查並解鎖成就
|
|||
|
|
async checkAndUnlockAchievements() {
|
|||
|
|
const newlyUnlocked = []
|
|||
|
|
|
|||
|
|
for (const achievement of this.achievements) {
|
|||
|
|
// 如果已經解鎖,跳過
|
|||
|
|
if (this.unlockedAchievements.includes(achievement.id)) {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 檢查條件
|
|||
|
|
if (this.checkCondition(achievement)) {
|
|||
|
|
await this.unlockAchievement(achievement)
|
|||
|
|
newlyUnlocked.push(achievement)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return newlyUnlocked
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解鎖成就
|
|||
|
|
async unlockAchievement(achievement) {
|
|||
|
|
if (this.unlockedAchievements.includes(achievement.id)) {
|
|||
|
|
return // 已經解鎖
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.unlockedAchievements.push(achievement.id)
|
|||
|
|
|
|||
|
|
// 應用成就獎勵(永久 Buff)
|
|||
|
|
if (achievement.reward && achievement.reward.buffs) {
|
|||
|
|
// 將成就加成添加到寵物狀態
|
|||
|
|
const currentState = this.petSystem.getState()
|
|||
|
|
const achievementBuffs = currentState.achievementBuffs || {}
|
|||
|
|
|
|||
|
|
// 合併加成
|
|||
|
|
for (const [key, value] of Object.entries(achievement.reward.buffs)) {
|
|||
|
|
achievementBuffs[key] = (achievementBuffs[key] || 0) + value
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await this.petSystem.updateState({ achievementBuffs })
|
|||
|
|
|
|||
|
|
// 重新計算戰鬥數值
|
|||
|
|
this.petSystem.calculateCombatStats()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 同步到 API
|
|||
|
|
try {
|
|||
|
|
await this.api.saveAchievements({
|
|||
|
|
unlocked: this.unlockedAchievements,
|
|||
|
|
stats: this.achievementStats
|
|||
|
|
})
|
|||
|
|
} catch (error) {
|
|||
|
|
console.warn('[AchievementSystem] API 同步失敗:', error)
|
|||
|
|
// 降級到 localStorage
|
|||
|
|
localStorage.setItem('achievements', JSON.stringify({
|
|||
|
|
unlocked: this.unlockedAchievements,
|
|||
|
|
stats: this.achievementStats
|
|||
|
|
}))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 顯示成就通知
|
|||
|
|
console.log(`\n${'='.repeat(50)}`)
|
|||
|
|
console.log(`🏆 成就解鎖:${achievement.icon} ${achievement.name}`)
|
|||
|
|
console.log(`📝 ${achievement.description}`)
|
|||
|
|
if (achievement.reward && achievement.reward.buffs) {
|
|||
|
|
console.log(`✨ 獎勵:`)
|
|||
|
|
for (const [key, value] of Object.entries(achievement.reward.buffs)) {
|
|||
|
|
const sign = value > 0 ? '+' : ''
|
|||
|
|
if (typeof value === 'number' && value < 1 && value > 0) {
|
|||
|
|
console.log(` ${key}: ${sign}${(value * 100).toFixed(0)}%`)
|
|||
|
|
} else {
|
|||
|
|
console.log(` ${key}: ${sign}${value}`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
console.log('='.repeat(50) + '\n')
|
|||
|
|
|
|||
|
|
return achievement
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 記錄動作
|
|||
|
|
async recordAction(action) {
|
|||
|
|
if (this.achievementStats.actionCounts[action] !== undefined) {
|
|||
|
|
this.achievementStats.actionCounts[action]++
|
|||
|
|
await this.checkAndUnlockAchievements()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 記錄事件
|
|||
|
|
async recordEvent(event) {
|
|||
|
|
this.achievementStats.eventCount++
|
|||
|
|
if (event.type && this.achievementStats.eventTypeCounts[event.type] !== undefined) {
|
|||
|
|
this.achievementStats.eventTypeCounts[event.type]++
|
|||
|
|
}
|
|||
|
|
await this.checkAndUnlockAchievements()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 記錄從瀕死恢復
|
|||
|
|
recordRecoveryFromDying() {
|
|||
|
|
if (!this.achievementStats.recoveredFromDying) {
|
|||
|
|
this.achievementStats.recoveredFromDying = true
|
|||
|
|
this.checkAndUnlockAchievements()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 檢查完美狀態
|
|||
|
|
checkPerfectState() {
|
|||
|
|
const state = this.petSystem.getState()
|
|||
|
|
if (state.hunger >= 100 &&
|
|||
|
|
state.happiness >= 100 &&
|
|||
|
|
state.health >= 100 &&
|
|||
|
|
!this.achievementStats.perfectStateReached) {
|
|||
|
|
this.achievementStats.perfectStateReached = true
|
|||
|
|
this.checkAndUnlockAchievements()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 獲取已解鎖成就
|
|||
|
|
getUnlockedAchievements() {
|
|||
|
|
return this.achievements.filter(a => this.unlockedAchievements.includes(a.id))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 獲取未解鎖成就
|
|||
|
|
getLockedAchievements() {
|
|||
|
|
return this.achievements.filter(a => !this.unlockedAchievements.includes(a.id))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 獲取成就進度
|
|||
|
|
getAchievementProgress(achievement) {
|
|||
|
|
const condition = achievement.condition
|
|||
|
|
const state = this.petSystem.getState()
|
|||
|
|
|
|||
|
|
switch (condition.type) {
|
|||
|
|
case 'age':
|
|||
|
|
return {
|
|||
|
|
current: state.ageSeconds,
|
|||
|
|
target: condition.value,
|
|||
|
|
progress: Math.min(100, (state.ageSeconds / condition.value) * 100)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case 'action_count':
|
|||
|
|
return {
|
|||
|
|
current: this.achievementStats.actionCounts[condition.action] || 0,
|
|||
|
|
target: condition.value,
|
|||
|
|
progress: Math.min(100, ((this.achievementStats.actionCounts[condition.action] || 0) / condition.value) * 100)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case 'stats':
|
|||
|
|
// 返回最低進度
|
|||
|
|
let minProgress = 100
|
|||
|
|
for (const [stat, value] of Object.entries(condition.stats)) {
|
|||
|
|
const current = state[stat] || 0
|
|||
|
|
const progress = Math.min(100, (current / value) * 100)
|
|||
|
|
minProgress = Math.min(minProgress, progress)
|
|||
|
|
}
|
|||
|
|
return { progress: minProgress }
|
|||
|
|
|
|||
|
|
case 'deity_favor':
|
|||
|
|
const favors = state.deityFavors || {}
|
|||
|
|
const maxFavor = Math.max(...Object.values(favors), 0)
|
|||
|
|
return {
|
|||
|
|
current: maxFavor,
|
|||
|
|
target: condition.value,
|
|||
|
|
progress: Math.min(100, (maxFavor / condition.value) * 100)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case 'event_count':
|
|||
|
|
return {
|
|||
|
|
current: this.achievementStats.eventCount,
|
|||
|
|
target: condition.value,
|
|||
|
|
progress: Math.min(100, (this.achievementStats.eventCount / condition.value) * 100)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
return { progress: 0 }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 獲取所有成就加成(用於顯示)
|
|||
|
|
getAllAchievementBonuses() {
|
|||
|
|
const bonuses = {}
|
|||
|
|
const unlocked = this.getUnlockedAchievements()
|
|||
|
|
|
|||
|
|
for (const achievement of unlocked) {
|
|||
|
|
if (achievement.reward && achievement.reward.buffs) {
|
|||
|
|
for (const [key, value] of Object.entries(achievement.reward.buffs)) {
|
|||
|
|
bonuses[key] = (bonuses[key] || 0) + value
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return bonuses
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|