2025-11-23 18:03:56 +00:00
|
|
|
|
// 寵物系統核心 - 與 API 整合
|
|
|
|
|
|
import { apiService } from './api-service.js'
|
|
|
|
|
|
import { PET_SPECIES } from '../data/pet-species.js'
|
2025-11-24 07:38:44 +00:00
|
|
|
|
import { FATES } from '../data/fates.js'
|
|
|
|
|
|
import { DEITIES } from '../data/deities.js'
|
2025-11-23 18:03:56 +00:00
|
|
|
|
|
|
|
|
|
|
export class PetSystem {
|
2025-11-24 10:34:02 +00:00
|
|
|
|
constructor(api = apiService, achievementSystem = null, inventorySystem = null) {
|
2025-11-23 18:03:56 +00:00
|
|
|
|
this.api = api
|
2025-11-24 10:34:02 +00:00
|
|
|
|
this.achievementSystem = achievementSystem
|
|
|
|
|
|
this.inventorySystem = inventorySystem
|
2025-11-23 18:03:56 +00:00
|
|
|
|
this.state = null
|
|
|
|
|
|
this.speciesConfig = null
|
|
|
|
|
|
this.tickInterval = null
|
|
|
|
|
|
this.eventCheckInterval = null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化寵物(從 API 載入或創建新寵物)
|
|
|
|
|
|
async initialize(speciesId = 'tinyTigerCat') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 從 API 載入現有狀態
|
|
|
|
|
|
this.state = await this.api.getPetState()
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
if (!this.state) {
|
|
|
|
|
|
// 創建新寵物
|
|
|
|
|
|
this.state = this.createInitialState(speciesId)
|
2025-11-24 13:45:09 +00:00
|
|
|
|
// 載入種族配置
|
|
|
|
|
|
this.speciesConfig = PET_SPECIES[this.state.speciesId] || PET_SPECIES[speciesId]
|
|
|
|
|
|
// 計算戰鬥數值(在保存前)
|
|
|
|
|
|
this.calculateCombatStats()
|
|
|
|
|
|
// 保存完整狀態(包含計算後的戰鬥數值)
|
2025-11-23 18:03:56 +00:00
|
|
|
|
await this.api.savePetState(this.state)
|
2025-11-24 10:34:02 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// 確保 achievementBuffs 存在(向後兼容)
|
|
|
|
|
|
if (!this.state.achievementBuffs) {
|
|
|
|
|
|
this.state.achievementBuffs = {}
|
|
|
|
|
|
}
|
2025-11-24 13:45:09 +00:00
|
|
|
|
// 確保 equipmentBuffs 有正確結構(向後兼容)
|
|
|
|
|
|
if (!this.state.equipmentBuffs || typeof this.state.equipmentBuffs !== 'object') {
|
|
|
|
|
|
this.state.equipmentBuffs = { flat: {}, percent: {} }
|
|
|
|
|
|
} else if (!this.state.equipmentBuffs.flat || !this.state.equipmentBuffs.percent) {
|
|
|
|
|
|
// 如果是舊的空對象 {},轉換為新結構
|
|
|
|
|
|
this.state.equipmentBuffs = { flat: {}, percent: {} }
|
|
|
|
|
|
}
|
|
|
|
|
|
// 載入種族配置
|
|
|
|
|
|
this.speciesConfig = PET_SPECIES[this.state.speciesId] || PET_SPECIES[speciesId]
|
|
|
|
|
|
// 計算戰鬥數值
|
|
|
|
|
|
this.calculateCombatStats()
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this.state
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[PetSystem] 初始化失敗:', error)
|
|
|
|
|
|
// 降級到本地狀態
|
|
|
|
|
|
this.state = this.createInitialState(speciesId)
|
|
|
|
|
|
this.speciesConfig = PET_SPECIES[speciesId]
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// 計算戰鬥數值
|
|
|
|
|
|
this.calculateCombatStats()
|
2025-11-23 18:03:56 +00:00
|
|
|
|
return this.state
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 創建初始狀態
|
|
|
|
|
|
createInitialState(speciesId) {
|
|
|
|
|
|
const config = PET_SPECIES[speciesId]
|
2025-11-24 07:38:44 +00:00
|
|
|
|
const state = {
|
2025-11-23 18:03:56 +00:00
|
|
|
|
speciesId,
|
2025-11-26 09:53:03 +00:00
|
|
|
|
name: null, // 寵物名稱,初始為 null
|
2025-11-24 07:38:44 +00:00
|
|
|
|
stage: 'egg',
|
2025-11-23 18:03:56 +00:00
|
|
|
|
hunger: 100,
|
|
|
|
|
|
happiness: 100,
|
|
|
|
|
|
health: 100,
|
2025-11-26 09:53:03 +00:00
|
|
|
|
height: config.lifecycle[0]?.height || config.baseStats.defaultHeight || 10, // 從第一階段獲取身高
|
|
|
|
|
|
weight: config.lifecycle[0]?.baseWeight || config.baseStats.defaultWeight || 500, // 從第一階段獲取體重
|
2025-11-23 18:03:56 +00:00
|
|
|
|
ageSeconds: 0,
|
|
|
|
|
|
poopCount: 0,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
str: 10,
|
|
|
|
|
|
int: 10,
|
|
|
|
|
|
dex: 10,
|
2025-11-23 18:03:56 +00:00
|
|
|
|
luck: config.baseStats.luck || 10,
|
|
|
|
|
|
isSleeping: false,
|
|
|
|
|
|
isSick: false,
|
|
|
|
|
|
isDead: false,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
dyingSeconds: 0, // 瀕死計時
|
2025-11-23 18:03:56 +00:00
|
|
|
|
currentDeityId: 'mazu',
|
|
|
|
|
|
deityFavors: {
|
|
|
|
|
|
mazu: 0,
|
|
|
|
|
|
earthgod: 0,
|
|
|
|
|
|
yuelao: 0,
|
|
|
|
|
|
wenchang: 0,
|
|
|
|
|
|
guanyin: 0
|
|
|
|
|
|
},
|
|
|
|
|
|
dailyPrayerCount: 0,
|
|
|
|
|
|
destiny: null,
|
|
|
|
|
|
buffs: [],
|
|
|
|
|
|
inventory: [],
|
|
|
|
|
|
generation: 1,
|
2025-11-24 10:34:02 +00:00
|
|
|
|
lastTickTime: Date.now(),
|
|
|
|
|
|
achievementBuffs: {}, // 成就加成
|
2025-11-24 13:45:09 +00:00
|
|
|
|
equipmentBuffs: { flat: {}, percent: {} }, // 裝備加成
|
|
|
|
|
|
appearance: {}, // 外觀設定
|
|
|
|
|
|
coins: 100 // 金幣(初始 100)
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
|
|
|
|
|
// 分配命格
|
|
|
|
|
|
this.assignDestiny(state)
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:不在這裡計算戰鬥數值,因為此時 speciesConfig 可能還沒設置
|
|
|
|
|
|
// calculateCombatStats 會在 initialize 之後調用
|
|
|
|
|
|
|
|
|
|
|
|
return state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分配命格
|
|
|
|
|
|
assignDestiny(state) {
|
|
|
|
|
|
// 隨機選擇一個命格
|
|
|
|
|
|
const fateIndex = Math.floor(Math.random() * FATES.length)
|
|
|
|
|
|
state.destiny = FATES[fateIndex]
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🎲 命格分配: ${state.destiny.name} - ${state.destiny.description}`)
|
|
|
|
|
|
console.log(' 加成:', state.destiny.buffs)
|
|
|
|
|
|
|
|
|
|
|
|
// 應用命格初始加成 (如果是直接加數值的)
|
|
|
|
|
|
if (state.destiny.buffs.luck) {
|
|
|
|
|
|
state.luck += state.destiny.buffs.luck
|
|
|
|
|
|
console.log(` 運勢加成: +${state.destiny.buffs.luck} -> ${state.luck}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 獲取所有加成(命格 + 神明基礎 + 神明好感度等級 + 神明滿級 + 成就 + Buff)
|
2025-11-24 07:38:44 +00:00
|
|
|
|
getAllBonuses() {
|
|
|
|
|
|
const bonuses = {}
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 命格加成
|
|
|
|
|
|
if (this.state.destiny && this.state.destiny.buffs) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(this.state.destiny.buffs)) {
|
|
|
|
|
|
bonuses[key] = (bonuses[key] || 0) + value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 神明基礎加成
|
|
|
|
|
|
if (this.state.currentDeityId) {
|
|
|
|
|
|
const deity = DEITIES.find(d => d.id === this.state.currentDeityId)
|
|
|
|
|
|
if (deity && deity.buffs) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(deity.buffs)) {
|
|
|
|
|
|
bonuses[key] = (bonuses[key] || 0) + value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 神明好感度等級加成
|
|
|
|
|
|
if (deity && deity.favorLevelBuffs) {
|
|
|
|
|
|
const favor = this.state.deityFavors?.[this.state.currentDeityId] || 0
|
|
|
|
|
|
const level = Math.floor(favor / deity.favorLevelBuffs.interval)
|
|
|
|
|
|
|
|
|
|
|
|
if (level > 0 && deity.favorLevelBuffs.buffsPerLevel) {
|
|
|
|
|
|
for (const [key, valuePerLevel] of Object.entries(deity.favorLevelBuffs.buffsPerLevel)) {
|
|
|
|
|
|
bonuses[key] = (bonuses[key] || 0) + (valuePerLevel * level)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 神明滿級特殊 Buff(好感度 = 100)
|
|
|
|
|
|
if (deity && deity.maxFavorBuff) {
|
|
|
|
|
|
const favor = this.state.deityFavors?.[this.state.currentDeityId] || 0
|
|
|
|
|
|
if (favor >= 100 && deity.maxFavorBuff.effects) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(deity.maxFavorBuff.effects)) {
|
|
|
|
|
|
bonuses[key] = (bonuses[key] || 0) + value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 5. 成就加成
|
|
|
|
|
|
if (this.state.achievementBuffs) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(this.state.achievementBuffs)) {
|
|
|
|
|
|
bonuses[key] = (bonuses[key] || 0) + value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 裝備加成
|
|
|
|
|
|
if (this.state.equipmentBuffs) {
|
|
|
|
|
|
// 處理 flat 加成(直接數值加成)
|
|
|
|
|
|
if (this.state.equipmentBuffs.flat) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(this.state.equipmentBuffs.flat)) {
|
|
|
|
|
|
bonuses[key] = (bonuses[key] || 0) + value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 處理 percent 加成(百分比加成)
|
|
|
|
|
|
if (this.state.equipmentBuffs.percent) {
|
|
|
|
|
|
for (const [key, value] of Object.entries(this.state.equipmentBuffs.percent)) {
|
|
|
|
|
|
bonuses[key] = (bonuses[key] || 0) + value
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
return bonuses
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 獲取最大健康值(100 + health加成)
|
|
|
|
|
|
getMaxHealth() {
|
|
|
|
|
|
const bonuses = this.getAllBonuses()
|
|
|
|
|
|
return 100 + (bonuses.health || 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 獲取體重狀態
|
|
|
|
|
|
getWeightStatus() {
|
|
|
|
|
|
if (!this.speciesConfig || !this.state) return 'unknown'
|
|
|
|
|
|
|
|
|
|
|
|
const currentStageConfig = this.speciesConfig.lifecycle.find(s => s.stage === this.state.stage)
|
|
|
|
|
|
if (!currentStageConfig || !currentStageConfig.weightRange) return 'unknown'
|
|
|
|
|
|
|
|
|
|
|
|
const { min, max } = currentStageConfig.weightRange
|
|
|
|
|
|
const weight = this.state.weight
|
|
|
|
|
|
|
|
|
|
|
|
if (weight < min) return 'underweight'
|
|
|
|
|
|
if (weight > max) return 'overweight'
|
|
|
|
|
|
return 'normal'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查當前階段是否允許某個動作(資料驅動)
|
|
|
|
|
|
isActionAllowed(action) {
|
|
|
|
|
|
if (!this.speciesConfig || !this.state) return false
|
|
|
|
|
|
const currentStageConfig = this.speciesConfig.lifecycle.find(s => s.stage === this.state.stage)
|
|
|
|
|
|
if (!currentStageConfig) return false
|
|
|
|
|
|
|
|
|
|
|
|
const allowedActions = currentStageConfig.allowedActions || []
|
|
|
|
|
|
return allowedActions.includes(action)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查當前階段是否啟用某個系統(資料驅動)
|
|
|
|
|
|
isSystemEnabled(system) {
|
|
|
|
|
|
if (!this.speciesConfig || !this.state) return true
|
|
|
|
|
|
const currentStageConfig = this.speciesConfig.lifecycle.find(s => s.stage === this.state.stage)
|
|
|
|
|
|
if (!currentStageConfig || !currentStageConfig.enabledSystems) return true
|
|
|
|
|
|
|
|
|
|
|
|
return currentStageConfig.enabledSystems[system] !== false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查當前時間是否在睡眠時段
|
|
|
|
|
|
isInSleepTime() {
|
|
|
|
|
|
if (!this.speciesConfig?.baseStats?.sleepSchedule) return null
|
|
|
|
|
|
|
|
|
|
|
|
const now = new Date()
|
|
|
|
|
|
const currentHour = now.getHours()
|
|
|
|
|
|
const currentMinute = now.getMinutes()
|
|
|
|
|
|
const currentTimeInMinutes = currentHour * 60 + currentMinute
|
|
|
|
|
|
|
|
|
|
|
|
const schedule = this.speciesConfig.baseStats.sleepSchedule
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查夜間睡眠 (21:30 - 08:00)
|
|
|
|
|
|
if (schedule.nightSleep && schedule.nightSleep.autoSleep) {
|
|
|
|
|
|
const startMinutes = schedule.nightSleep.startHour * 60 + schedule.nightSleep.startMinute
|
|
|
|
|
|
const endMinutes = schedule.nightSleep.endHour * 60 + schedule.nightSleep.endMinute
|
|
|
|
|
|
|
|
|
|
|
|
// 處理跨午夜的情況
|
|
|
|
|
|
if (startMinutes > endMinutes) {
|
|
|
|
|
|
// 例如 21:30 (1290分) 到 08:00 (480分)
|
|
|
|
|
|
if (currentTimeInMinutes >= startMinutes || currentTimeInMinutes < endMinutes) {
|
|
|
|
|
|
return 'nightSleep'
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (currentTimeInMinutes >= startMinutes && currentTimeInMinutes < endMinutes) {
|
|
|
|
|
|
return 'nightSleep'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查午休 (12:00 - 13:00)
|
|
|
|
|
|
if (schedule.noonNap && schedule.noonNap.autoSleep) {
|
|
|
|
|
|
const startMinutes = schedule.noonNap.startHour * 60 + schedule.noonNap.startMinute
|
|
|
|
|
|
const endMinutes = schedule.noonNap.endHour * 60 + schedule.noonNap.endMinute
|
|
|
|
|
|
|
|
|
|
|
|
if (currentTimeInMinutes >= startMinutes && currentTimeInMinutes < endMinutes) {
|
|
|
|
|
|
return 'noonNap'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 獲取睡眠時段配置
|
|
|
|
|
|
getSleepPeriod(periodName) {
|
|
|
|
|
|
if (!this.speciesConfig?.baseStats?.sleepSchedule) return null
|
|
|
|
|
|
return this.speciesConfig.baseStats.sleepSchedule[periodName]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 計算戰鬥數值
|
|
|
|
|
|
calculateCombatStats(state = this.state) {
|
|
|
|
|
|
if (!state || !this.speciesConfig) return
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 獲取所有加成
|
|
|
|
|
|
const bonuses = this.getAllBonuses()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 計算有效屬性 (基礎 + 加成)
|
|
|
|
|
|
const effectiveStr = state.str + (bonuses.str || 0)
|
|
|
|
|
|
const effectiveInt = state.int + (bonuses.int || 0)
|
|
|
|
|
|
const effectiveDex = state.dex + (bonuses.dex || 0)
|
|
|
|
|
|
const effectiveLuck = (state.luck || 0) + (bonuses.luck || 0)
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 獲取百分比加成 (Modifiers)
|
|
|
|
|
|
const atkMod = 1 + (bonuses.attack || 0)
|
|
|
|
|
|
const defMod = 1 + (bonuses.defense || 0)
|
|
|
|
|
|
const spdMod = 1 + (bonuses.speed || 0)
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 從配置讀取戰鬥數值計算系數
|
|
|
|
|
|
const formulas = this.speciesConfig.baseStats.combatFormulas || {
|
|
|
|
|
|
attack: { strMultiplier: 2.5, dexMultiplier: 0.5 },
|
|
|
|
|
|
defense: { strMultiplier: 1.0, intMultiplier: 2.0 },
|
|
|
|
|
|
speed: { dexMultiplier: 3.0, intMultiplier: 0.5 }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 計算最終戰鬥屬性
|
|
|
|
|
|
state.attack = (effectiveStr * formulas.attack.strMultiplier + effectiveDex * formulas.attack.dexMultiplier) * atkMod
|
|
|
|
|
|
state.defense = (effectiveStr * formulas.defense.strMultiplier + effectiveInt * formulas.defense.intMultiplier) * defMod
|
|
|
|
|
|
state.speed = (effectiveDex * formulas.speed.dexMultiplier + effectiveInt * formulas.speed.intMultiplier) * spdMod
|
|
|
|
|
|
|
|
|
|
|
|
// 保存有效屬性到 state (供 UI 顯示)
|
|
|
|
|
|
state.effectiveStr = effectiveStr
|
|
|
|
|
|
state.effectiveInt = effectiveInt
|
|
|
|
|
|
state.effectiveDex = effectiveDex
|
|
|
|
|
|
state.effectiveLuck = effectiveLuck
|
|
|
|
|
|
|
|
|
|
|
|
// 根據當前階段設定身高(每個階段固定)
|
|
|
|
|
|
const currentStageConfig = this.speciesConfig.lifecycle.find(s => s.stage === state.stage)
|
|
|
|
|
|
if (currentStageConfig && currentStageConfig.height) {
|
|
|
|
|
|
state.height = currentStageConfig.height
|
|
|
|
|
|
} else if (!state.height) {
|
|
|
|
|
|
state.height = this.speciesConfig.baseStats.defaultHeight || 10
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 體重:如果沒有初始化,使用階段基礎體重
|
|
|
|
|
|
if (!state.weight && currentStageConfig && currentStageConfig.baseWeight) {
|
|
|
|
|
|
state.weight = currentStageConfig.baseWeight
|
|
|
|
|
|
} else if (!state.weight) {
|
|
|
|
|
|
state.weight = this.speciesConfig.baseStats.defaultWeight || 500
|
|
|
|
|
|
}
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新狀態(同步到 API)
|
|
|
|
|
|
async updateState(updates) {
|
2025-11-26 15:31:46 +00:00
|
|
|
|
// 如果更新了 speciesId,重新載入配置
|
|
|
|
|
|
if (updates.speciesId && updates.speciesId !== this.state.speciesId) {
|
|
|
|
|
|
this.speciesConfig = PET_SPECIES[updates.speciesId]
|
|
|
|
|
|
// 可能需要重新計算屬性或重置某些狀態?
|
|
|
|
|
|
// 暫時只更新配置
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
this.state = { ...this.state, ...updates }
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
try {
|
|
|
|
|
|
await this.api.updatePetState(updates)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('[PetSystem] API 更新失敗,使用本地狀態:', error)
|
|
|
|
|
|
}
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
return this.state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 獲取當前狀態
|
|
|
|
|
|
getState() {
|
2025-11-26 09:53:03 +00:00
|
|
|
|
const state = { ...this.state };
|
|
|
|
|
|
// Ensure critical values are valid numbers
|
|
|
|
|
|
if (typeof state.happiness !== 'number' || isNaN(state.happiness)) {
|
|
|
|
|
|
console.warn('[PetSystem] Invalid happiness value, resetting to 100');
|
|
|
|
|
|
state.happiness = 100;
|
|
|
|
|
|
this.state.happiness = 100;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof state.hunger !== 'number' || isNaN(state.hunger)) {
|
|
|
|
|
|
console.warn('[PetSystem] Invalid hunger value, resetting to 100');
|
|
|
|
|
|
state.hunger = 100;
|
|
|
|
|
|
this.state.hunger = 100;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof state.health !== 'number' || isNaN(state.health)) {
|
|
|
|
|
|
console.warn('[PetSystem] Invalid health value, resetting to 100');
|
|
|
|
|
|
state.health = 100;
|
|
|
|
|
|
this.state.health = 100;
|
|
|
|
|
|
}
|
|
|
|
|
|
return state;
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 刪除寵物
|
|
|
|
|
|
async deletePet() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 停止所有循環
|
|
|
|
|
|
this.stopTickLoop()
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
// 從 API 刪除
|
|
|
|
|
|
await this.api.deletePetState()
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
// 重置狀態
|
|
|
|
|
|
this.state = null
|
|
|
|
|
|
this.speciesConfig = null
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
return { success: true, message: '寵物已刪除' }
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[PetSystem] 刪除寵物失敗:', error)
|
|
|
|
|
|
// 即使 API 失敗,也清除本地狀態
|
|
|
|
|
|
this.state = null
|
|
|
|
|
|
this.speciesConfig = null
|
|
|
|
|
|
return { success: true, message: '寵物已刪除(本地)' }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// Tick 循環(生理系统 - 从配置读取间隔)
|
2025-11-23 18:03:56 +00:00
|
|
|
|
startTickLoop(callback) {
|
|
|
|
|
|
if (this.tickInterval) this.stopTickLoop()
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
|
|
|
|
|
// 从配置读取间隔时间
|
|
|
|
|
|
const interval = this.speciesConfig?.baseStats?.physiologyTickInterval || 60000
|
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
|
// 立即執行一次回調,確保 UI 獲得最新狀態
|
|
|
|
|
|
if (callback) callback(this.getState())
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
this.tickInterval = setInterval(async () => {
|
|
|
|
|
|
await this.tick()
|
|
|
|
|
|
if (callback) callback(this.getState())
|
2025-11-24 07:38:44 +00:00
|
|
|
|
}, interval)
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopTickLoop() {
|
|
|
|
|
|
if (this.tickInterval) {
|
|
|
|
|
|
clearInterval(this.tickInterval)
|
|
|
|
|
|
this.tickInterval = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 單次 Tick 執行
|
|
|
|
|
|
async tick() {
|
|
|
|
|
|
if (!this.state || this.state.isDead) return
|
|
|
|
|
|
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const deltaTime = (now - (this.state.lastTickTime || now)) / 1000
|
|
|
|
|
|
this.state.lastTickTime = now
|
|
|
|
|
|
|
|
|
|
|
|
// 年齡增長
|
|
|
|
|
|
this.state.ageSeconds += deltaTime
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查階段轉換
|
|
|
|
|
|
this.checkStageTransition()
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// 檢查自動睡眠(基於真實時間)
|
|
|
|
|
|
if (this.isSystemEnabled('sleep')) {
|
|
|
|
|
|
const sleepPeriod = this.isInSleepTime()
|
|
|
|
|
|
if (sleepPeriod && !this.state.isSleeping) {
|
|
|
|
|
|
// 在睡眠時段且未睡覺,自動進入睡眠
|
|
|
|
|
|
this.state.isSleeping = true
|
|
|
|
|
|
const periodConfig = this.getSleepPeriod(sleepPeriod)
|
|
|
|
|
|
const periodName = sleepPeriod === 'nightSleep' ? '夜間睡眠' : '午休'
|
|
|
|
|
|
console.log(`😴 ${periodName}時間,寵物自動進入睡眠`)
|
|
|
|
|
|
} else if (!sleepPeriod && this.state.isSleeping && this.state._autoSlept) {
|
|
|
|
|
|
// 不在睡眠時段且是自動睡眠狀態,自動醒來
|
|
|
|
|
|
this.state.isSleeping = false
|
|
|
|
|
|
this.state._autoSlept = false
|
|
|
|
|
|
console.log(`⏰ 睡眠時間結束,寵物自動醒來`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根據當前階段配置決定啟用哪些系統(資料驅動)
|
2025-11-23 18:03:56 +00:00
|
|
|
|
const config = this.speciesConfig.baseStats
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// 睡眠狀態下的衰減倍率(從配置讀取)
|
|
|
|
|
|
const sleepDecayMultiplier = this.state.isSleeping ? (config.sleepDecayMultiplier || 0.1) : 1.0
|
|
|
|
|
|
|
|
|
|
|
|
// 獲取加成(用於檢查免疫)
|
|
|
|
|
|
const bonuses = this.getAllBonuses()
|
|
|
|
|
|
const hasImmunity = bonuses.sicknessImmune // 妈祖满级等
|
|
|
|
|
|
|
|
|
|
|
|
// 飢餓系統
|
|
|
|
|
|
if (this.isSystemEnabled('hunger')) {
|
|
|
|
|
|
this.state.hunger = Math.max(0, this.state.hunger - config.hungerDecayPerTick * sleepDecayMultiplier)
|
|
|
|
|
|
|
|
|
|
|
|
// 飢餓過度扣血(有免疫則跳過)
|
|
|
|
|
|
if (!hasImmunity && this.state.hunger <= 0) {
|
|
|
|
|
|
const damage = config.hungerHealthDamage || 1
|
|
|
|
|
|
this.state.health = Math.max(0, this.state.health - damage)
|
|
|
|
|
|
if (damage > 0) console.log(`📉 飢餓扣除健康: -${damage}`)
|
|
|
|
|
|
}
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// 快樂系統
|
|
|
|
|
|
if (this.isSystemEnabled('happiness')) {
|
|
|
|
|
|
this.state.happiness = Math.max(0, this.state.happiness - config.happinessDecayPerTick * sleepDecayMultiplier)
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// 便便系統(睡覺時不會拉屎)
|
|
|
|
|
|
if (this.isSystemEnabled('poop') && !this.state.isSleeping) {
|
|
|
|
|
|
if (Math.random() < config.poopChancePerTick) {
|
|
|
|
|
|
const maxPoop = config.maxPoopCount || 4
|
|
|
|
|
|
this.state.poopCount = Math.min(maxPoop, this.state.poopCount + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 便便扣健康(有免疫則跳過)
|
|
|
|
|
|
if (!hasImmunity && this.isSystemEnabled('poop') && this.state.poopCount > 0) {
|
|
|
|
|
|
const damage = (config.poopHealthDamage || 0.5) * this.state.poopCount
|
|
|
|
|
|
this.state.health = Math.max(0, this.state.health - damage)
|
|
|
|
|
|
if (damage > 0) console.log(`📉 便便扣除健康: -${damage} (數量: ${this.state.poopCount})`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生病系統
|
|
|
|
|
|
if (this.isSystemEnabled('sickness')) {
|
|
|
|
|
|
// 健康過低導致生病(從配置讀取閾值)
|
|
|
|
|
|
const threshold = config.sicknessThreshold || 60
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有免疫生病 Buff,則不會生病
|
|
|
|
|
|
if (!hasImmunity && !this.state.isSick && this.state.health < threshold) {
|
|
|
|
|
|
this.state.isSick = true
|
|
|
|
|
|
console.log(`🤢 健康低於 ${threshold},寵物生病了`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 死亡檢查(從配置讀取瀕死時間)
|
|
|
|
|
|
const dyingTime = config.dyingTimeSeconds || 1800
|
2025-11-23 18:03:56 +00:00
|
|
|
|
if (this.state.health <= 0) {
|
2025-11-24 07:38:44 +00:00
|
|
|
|
this.state.dyingSeconds = (this.state.dyingSeconds || 0) + deltaTime
|
|
|
|
|
|
|
|
|
|
|
|
// 每 60 秒提示一次
|
|
|
|
|
|
if (Math.floor(this.state.dyingSeconds) % 60 === 0) {
|
|
|
|
|
|
const remaining = dyingTime - this.state.dyingSeconds
|
|
|
|
|
|
console.log(`⚠️ 寵物瀕死中!剩餘時間: ${(remaining / 60).toFixed(1)} 分鐘`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.state.dyingSeconds >= dyingTime) {
|
|
|
|
|
|
this.state.isDead = true
|
|
|
|
|
|
console.log('💀 寵物死亡')
|
|
|
|
|
|
await this.updateState({ isDead: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 健康恢復,重置瀕死計時
|
|
|
|
|
|
if (this.state.dyingSeconds > 0) {
|
2025-11-24 10:34:02 +00:00
|
|
|
|
const wasDying = this.state.dyingSeconds > 0
|
2025-11-24 07:38:44 +00:00
|
|
|
|
this.state.dyingSeconds = 0
|
|
|
|
|
|
console.log('✨ 寵物脫離瀕死狀態')
|
2025-11-24 13:45:09 +00:00
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 通知成就系統(如果存在)
|
|
|
|
|
|
if (this.achievementSystem && wasDying) {
|
|
|
|
|
|
this.achievementSystem.recordRecoveryFromDying()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查成就(年齡、階段、完美狀態等)
|
|
|
|
|
|
if (this.achievementSystem) {
|
|
|
|
|
|
await this.achievementSystem.checkAndUnlockAchievements()
|
|
|
|
|
|
this.achievementSystem.checkPerfectState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 減少裝備耐久度(每 tick 減少少量,戰鬥或特殊情況會減少更多)
|
2025-11-26 15:31:46 +00:00
|
|
|
|
// 只對已裝備過的物品減少耐久度(hasBeenEquipped = true)
|
|
|
|
|
|
if (this.state.inventory && Array.isArray(this.state.inventory)) {
|
|
|
|
|
|
const equippedItems = this.state.inventory.filter(item => item.isEquipped && !item.isAppearance);
|
|
|
|
|
|
|
|
|
|
|
|
for (const item of equippedItems) {
|
|
|
|
|
|
// 只對已經被裝備過的物品減少耐久度
|
|
|
|
|
|
if (item.hasBeenEquipped && item.maxDurability && item.maxDurability !== Infinity && item.durability > 0) {
|
|
|
|
|
|
// 每 10 個 tick 減少 1 點耐久度(約每 30 秒)
|
|
|
|
|
|
if (this.state._tickCount && this.state._tickCount % 10 === 0) {
|
|
|
|
|
|
item.durability = Math.max(0, item.durability - 1);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果耐久度降為 0,自動卸下裝備
|
|
|
|
|
|
if (item.durability <= 0) {
|
|
|
|
|
|
item.isEquipped = false;
|
|
|
|
|
|
item.isAppearance = false;
|
|
|
|
|
|
console.log(`⚠️ ${item.name} 耐久度耗盡,已自動卸下`);
|
|
|
|
|
|
|
|
|
|
|
|
// 重新計算裝備加成
|
|
|
|
|
|
this.calculateCombatStats();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Legacy inventory system support (如果使用舊的 inventorySystem)
|
2025-11-24 10:34:02 +00:00
|
|
|
|
if (this.inventorySystem) {
|
|
|
|
|
|
// 只有裝備中的道具才會減少耐久度
|
|
|
|
|
|
const equipped = this.inventorySystem.getEquipped()
|
|
|
|
|
|
for (const [slot, equippedItem] of Object.entries(equipped)) {
|
|
|
|
|
|
if (equippedItem) {
|
|
|
|
|
|
// 每 10 個 tick 減少 1 點耐久度(約每 30 秒)
|
|
|
|
|
|
if (this.state._tickCount && this.state._tickCount % 10 === 0) {
|
|
|
|
|
|
await this.inventorySystem.reduceDurability(slot, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-24 07:38:44 +00:00
|
|
|
|
}
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// 重新计算属性(含加成)
|
|
|
|
|
|
this.calculateCombatStats()
|
|
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 初始化 tick 計數器
|
|
|
|
|
|
if (!this.state._tickCount) {
|
|
|
|
|
|
this.state._tickCount = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
this.state._tickCount++
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
// 同步到 API
|
|
|
|
|
|
await this.updateState({
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// 基礎屬性
|
2025-11-23 18:03:56 +00:00
|
|
|
|
ageSeconds: this.state.ageSeconds,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
stage: this.state.stage,
|
2025-11-23 18:03:56 +00:00
|
|
|
|
hunger: this.state.hunger,
|
|
|
|
|
|
happiness: this.state.happiness,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
health: this.state.health,
|
|
|
|
|
|
weight: this.state.weight,
|
|
|
|
|
|
height: this.state.height,
|
|
|
|
|
|
// 能力值
|
|
|
|
|
|
str: this.state.str,
|
|
|
|
|
|
int: this.state.int,
|
|
|
|
|
|
dex: this.state.dex,
|
|
|
|
|
|
luck: this.state.luck,
|
|
|
|
|
|
// 有效能力值 (含加成)
|
|
|
|
|
|
effectiveStr: this.state.effectiveStr,
|
|
|
|
|
|
effectiveInt: this.state.effectiveInt,
|
|
|
|
|
|
effectiveDex: this.state.effectiveDex,
|
|
|
|
|
|
effectiveLuck: this.state.effectiveLuck,
|
|
|
|
|
|
// 狀態
|
2025-11-23 18:03:56 +00:00
|
|
|
|
poopCount: this.state.poopCount,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
isSleeping: this.state.isSleeping,
|
2025-11-23 18:03:56 +00:00
|
|
|
|
isSick: this.state.isSick,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
isDead: this.state.isDead,
|
|
|
|
|
|
dyingSeconds: this.state.dyingSeconds,
|
|
|
|
|
|
// 戰鬥數值
|
|
|
|
|
|
attack: this.state.attack,
|
|
|
|
|
|
defense: this.state.defense,
|
2025-11-24 13:45:09 +00:00
|
|
|
|
speed: this.state.speed,
|
|
|
|
|
|
// 命格和神明
|
|
|
|
|
|
destiny: this.state.destiny,
|
|
|
|
|
|
currentDeityId: this.state.currentDeityId,
|
|
|
|
|
|
deityFavors: this.state.deityFavors,
|
|
|
|
|
|
// 经济
|
|
|
|
|
|
coins: this.state.coins
|
2025-11-23 18:03:56 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// Debug: 直接設置屬性
|
|
|
|
|
|
async debugSetStat(stat, value) {
|
|
|
|
|
|
if (!this.state) return
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[Debug] 設置 ${stat} = ${value}`)
|
|
|
|
|
|
await this.updateState({ [stat]: value })
|
|
|
|
|
|
|
|
|
|
|
|
// 如果修改的是戰鬥相關屬性,重新計算
|
|
|
|
|
|
if (['str', 'int', 'dex', 'weight'].includes(stat)) {
|
|
|
|
|
|
this.calculateCombatStats()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this.state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
// 檢查階段轉換
|
|
|
|
|
|
checkStageTransition() {
|
2025-11-24 07:38:44 +00:00
|
|
|
|
if (!this.speciesConfig || !this.state) return
|
2025-11-23 18:03:56 +00:00
|
|
|
|
const config = this.speciesConfig
|
|
|
|
|
|
const currentStage = this.state.stage
|
|
|
|
|
|
const ageSeconds = this.state.ageSeconds
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
// 找到當前階段
|
|
|
|
|
|
const currentIndex = config.lifecycle.findIndex(s => s.stage === currentStage)
|
|
|
|
|
|
if (currentIndex < 0 || currentIndex >= config.lifecycle.length - 1) {
|
|
|
|
|
|
return // 找不到或已是最終階段
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const currentStageConfig = config.lifecycle[currentIndex]
|
|
|
|
|
|
const nextStage = config.lifecycle[currentIndex + 1]
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查時間條件:年齡是否超過當前階段的持續時間
|
|
|
|
|
|
const timeConditionMet = ageSeconds >= currentStageConfig.durationSeconds && currentStageConfig.durationSeconds > 0
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查屬性條件
|
|
|
|
|
|
let statsConditionMet = true
|
2025-11-24 13:45:09 +00:00
|
|
|
|
let missingStats = []
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
if (nextStage.conditions) {
|
|
|
|
|
|
if (nextStage.conditions.str && this.state.str < nextStage.conditions.str) {
|
|
|
|
|
|
statsConditionMet = false
|
|
|
|
|
|
missingStats.push(`STR ${this.state.str.toFixed(1)}/${nextStage.conditions.str}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nextStage.conditions.int && this.state.int < nextStage.conditions.int) {
|
|
|
|
|
|
statsConditionMet = false
|
|
|
|
|
|
missingStats.push(`INT ${this.state.int.toFixed(1)}/${nextStage.conditions.int}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nextStage.conditions.dex && this.state.dex < nextStage.conditions.dex) {
|
|
|
|
|
|
statsConditionMet = false
|
|
|
|
|
|
missingStats.push(`DEX ${this.state.dex.toFixed(1)}/${nextStage.conditions.dex}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (timeConditionMet && statsConditionMet) {
|
2025-11-24 13:45:09 +00:00
|
|
|
|
// 檢查是否有進化分支
|
|
|
|
|
|
let targetStage = nextStage.stage
|
|
|
|
|
|
let evolutionBranch = null
|
|
|
|
|
|
|
|
|
|
|
|
if (nextStage.evolutions && nextStage.evolutions.length > 0) {
|
|
|
|
|
|
// 判定進化分支
|
|
|
|
|
|
for (const branch of nextStage.evolutions) {
|
|
|
|
|
|
let branchMet = true
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查分支條件
|
|
|
|
|
|
if (branch.conditions) {
|
|
|
|
|
|
// STR 條件
|
|
|
|
|
|
if (branch.conditions.str) {
|
|
|
|
|
|
if (branch.conditions.str.min && this.state.str < branch.conditions.str.min) branchMet = false
|
|
|
|
|
|
if (branch.conditions.str.dominant && (this.state.str <= this.state.int + this.state.dex)) branchMet = false
|
|
|
|
|
|
}
|
|
|
|
|
|
// INT 條件
|
|
|
|
|
|
if (branch.conditions.int) {
|
|
|
|
|
|
if (branch.conditions.int.min && this.state.int < branch.conditions.int.min) branchMet = false
|
|
|
|
|
|
if (branch.conditions.int.dominant && (this.state.int <= this.state.str + this.state.dex)) branchMet = false
|
|
|
|
|
|
}
|
|
|
|
|
|
// DEX 條件
|
|
|
|
|
|
if (branch.conditions.dex) {
|
|
|
|
|
|
if (branch.conditions.dex.min && this.state.dex < branch.conditions.dex.min) branchMet = false
|
|
|
|
|
|
if (branch.conditions.dex.dominant && (this.state.dex <= this.state.str + this.state.int)) branchMet = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (branchMet) {
|
|
|
|
|
|
evolutionBranch = branch
|
|
|
|
|
|
break // 找到第一個符合的分支就停止(優先級由數組順序決定)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果沒有符合的特殊分支,使用默認分支(通常是最後一個)
|
|
|
|
|
|
if (!evolutionBranch) {
|
|
|
|
|
|
evolutionBranch = nextStage.evolutions[nextStage.evolutions.length - 1]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\n✨ 進化!${currentStage} → ${targetStage} (年齡: ${ageSeconds.toFixed(1)}秒)`)
|
|
|
|
|
|
|
|
|
|
|
|
const updates = { stage: targetStage }
|
|
|
|
|
|
|
2025-11-26 09:53:03 +00:00
|
|
|
|
// 更新身高和體重(從新階段配置中獲取)
|
|
|
|
|
|
const newStageIndex = config.lifecycle.findIndex(s => s.stage === targetStage)
|
|
|
|
|
|
if (newStageIndex >= 0) {
|
|
|
|
|
|
const newStageConfig = config.lifecycle[newStageIndex]
|
|
|
|
|
|
if (newStageConfig.height) {
|
|
|
|
|
|
updates.height = newStageConfig.height
|
|
|
|
|
|
}
|
|
|
|
|
|
if (newStageConfig.baseWeight) {
|
|
|
|
|
|
updates.weight = newStageConfig.baseWeight
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 13:45:09 +00:00
|
|
|
|
// 應用進化分支效果
|
|
|
|
|
|
if (evolutionBranch) {
|
|
|
|
|
|
console.log(`🌟 觸發特殊進化分支:${evolutionBranch.name}`)
|
|
|
|
|
|
updates.evolutionId = evolutionBranch.id
|
|
|
|
|
|
updates.evolutionName = evolutionBranch.name
|
|
|
|
|
|
|
|
|
|
|
|
// 應用屬性修正(永久保存到狀態中)
|
|
|
|
|
|
if (evolutionBranch.statModifiers) {
|
|
|
|
|
|
updates.statModifiers = evolutionBranch.statModifiers
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.state.stage = targetStage
|
2025-11-24 07:38:44 +00:00
|
|
|
|
this.state._evolutionWarned = false // 重置警告狀態
|
2025-11-24 13:45:09 +00:00
|
|
|
|
this.updateState(updates)
|
2025-11-24 07:38:44 +00:00
|
|
|
|
} else if (timeConditionMet && !statsConditionMet) {
|
|
|
|
|
|
// 時間到了但屬性不足,只在第一次提示
|
|
|
|
|
|
if (!this.state._evolutionWarned) {
|
|
|
|
|
|
console.log(`⚠️ 年齡已達到 (${ageSeconds.toFixed(1)}秒),但屬性不足以進化到 ${nextStage.stage}`)
|
|
|
|
|
|
console.log(` 需要: ${missingStats.join(', ')}`)
|
|
|
|
|
|
this.state._evolutionWarned = true
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 餵食
|
2025-11-24 07:38:44 +00:00
|
|
|
|
async feed(amount = 12) {
|
|
|
|
|
|
if (!this.isActionAllowed('feed')) return { success: false, message: '當前階段不能餵食' }
|
2025-11-23 18:03:56 +00:00
|
|
|
|
if (this.state.isDead) return { success: false, message: '寵物已死亡' }
|
2025-11-24 07:38:44 +00:00
|
|
|
|
if (this.state.isSleeping) return { success: false, message: '寵物正在睡覺,無法餵食' }
|
|
|
|
|
|
if (this.state.hunger >= 100) return { success: false, message: '寵物已經吃飽了' }
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
const newHunger = Math.min(100, this.state.hunger + amount)
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
|
|
|
|
|
// 體重隨機變化 ±10%
|
|
|
|
|
|
const baseWeightChange = amount * 0.5
|
|
|
|
|
|
const randomFactor = 0.9 + Math.random() * 0.2 // 0.9 ~ 1.1
|
|
|
|
|
|
const weightChange = baseWeightChange * randomFactor
|
|
|
|
|
|
const newWeight = this.state.weight + weightChange
|
|
|
|
|
|
|
|
|
|
|
|
// 應用命格的力量成長加成
|
|
|
|
|
|
const bonuses = this.getAllBonuses()
|
|
|
|
|
|
const strGainBonus = bonuses.strGain || 0
|
|
|
|
|
|
const strGain = 0.3 * (1 + strGainBonus) // 降低成長速度
|
|
|
|
|
|
|
|
|
|
|
|
if (strGainBonus > 0) {
|
|
|
|
|
|
console.log(`[餵食] 力量成長加成: ${(strGainBonus * 100).toFixed(0)}% -> +${strGain.toFixed(2)} STR`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 自然成長上限為 100
|
|
|
|
|
|
const currentStr = this.state.str
|
|
|
|
|
|
const newStr = currentStr < 100 ? Math.min(100, currentStr + strGain) : currentStr
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
await this.updateState({
|
|
|
|
|
|
hunger: newHunger,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
weight: newWeight,
|
|
|
|
|
|
str: newStr
|
2025-11-23 18:03:56 +00:00
|
|
|
|
})
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 記錄到成就系統
|
|
|
|
|
|
if (this.achievementSystem) {
|
|
|
|
|
|
await this.achievementSystem.recordAction('feed')
|
|
|
|
|
|
this.achievementSystem.checkPerfectState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
this.calculateCombatStats()
|
|
|
|
|
|
|
|
|
|
|
|
return { success: true, hunger: newHunger, weight: newWeight, strGain, weightChange }
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 玩耍
|
2025-11-24 07:38:44 +00:00
|
|
|
|
async play(options = {}) {
|
|
|
|
|
|
if (!this.isActionAllowed('play')) return { success: false, message: '當前階段不能玩耍' }
|
2025-11-23 18:03:56 +00:00
|
|
|
|
if (this.state.isDead || this.state.isSleeping) {
|
|
|
|
|
|
return { success: false, message: '寵物無法玩耍' }
|
|
|
|
|
|
}
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
|
|
|
|
|
// 檢查飢餓度
|
|
|
|
|
|
if (this.state.hunger <= 0) {
|
|
|
|
|
|
return { success: false, message: '寵物太餓了,沒力氣玩耍' }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 可以傳入 { amount: 10, gameType: 'training' | 'puzzle' | 'normal' }
|
|
|
|
|
|
const amount = options.amount || options || 10 // 向後兼容舊的調用方式
|
|
|
|
|
|
const gameType = options.gameType || 'normal'
|
|
|
|
|
|
|
|
|
|
|
|
// 應用命格和神明的屬性成長加成
|
|
|
|
|
|
const bonuses = this.getAllBonuses()
|
|
|
|
|
|
const happinessBonus = bonuses.happinessRecovery || 0
|
|
|
|
|
|
const dexBonus = bonuses.dexGain || 0
|
|
|
|
|
|
const intBonus = bonuses.intGain || 0
|
|
|
|
|
|
|
|
|
|
|
|
const happinessGain = amount * (1 + happinessBonus)
|
|
|
|
|
|
const dexGain = 0.3 * (1 + dexBonus) // 降低成長速度
|
|
|
|
|
|
const intGain = 0.15 * (1 + intBonus) // 降低成長速度
|
|
|
|
|
|
|
|
|
|
|
|
// 體重變化根據遊戲類型,並加入隨機 ±10%
|
|
|
|
|
|
let baseWeightChange = 0
|
|
|
|
|
|
let gameTypeLabel = ''
|
|
|
|
|
|
|
|
|
|
|
|
if (gameType === 'training') {
|
|
|
|
|
|
// 訓練類型:消耗體力,減重
|
|
|
|
|
|
baseWeightChange = -3
|
|
|
|
|
|
gameTypeLabel = '🏃 訓練'
|
|
|
|
|
|
} else if (gameType === 'puzzle') {
|
|
|
|
|
|
// 益智類型:不影響體重
|
|
|
|
|
|
baseWeightChange = 0
|
|
|
|
|
|
gameTypeLabel = '🧩 益智'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 一般玩耍:輕微減重
|
|
|
|
|
|
baseWeightChange = -1
|
|
|
|
|
|
gameTypeLabel = '🎮 玩耍'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加隨機波動 ±10%(除了益智類型)
|
|
|
|
|
|
const randomFactor = baseWeightChange !== 0 ? (0.9 + Math.random() * 0.2) : 0
|
|
|
|
|
|
const weightChange = baseWeightChange * randomFactor
|
|
|
|
|
|
|
|
|
|
|
|
// 顯示加成細節
|
|
|
|
|
|
if (happinessBonus > 0 || dexBonus > 0 || intBonus > 0) {
|
|
|
|
|
|
// 分別計算命格和神明的加成
|
|
|
|
|
|
let fateBonus = 0
|
|
|
|
|
|
let deityBonus = 0
|
|
|
|
|
|
if (this.state.destiny?.buffs?.happinessRecovery) fateBonus += this.state.destiny.buffs.happinessRecovery
|
|
|
|
|
|
const deity = DEITIES.find(d => d.id === this.state.currentDeityId)
|
|
|
|
|
|
if (deity?.buffs?.happinessRecovery) deityBonus += deity.buffs.happinessRecovery
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`[${gameTypeLabel}] 快樂加成: 命格${(fateBonus * 100).toFixed(0)}% + 神明${(deityBonus * 100).toFixed(0)}% = ${(happinessBonus * 100).toFixed(0)}% | DEX${(dexBonus * 100).toFixed(0)}% INT${(intBonus * 100).toFixed(0)}%`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const newHappiness = Math.min(100, this.state.happiness + happinessGain)
|
|
|
|
|
|
|
|
|
|
|
|
// 自然成長上限為 100
|
|
|
|
|
|
const currentDex = this.state.dex
|
|
|
|
|
|
const currentInt = this.state.int
|
|
|
|
|
|
const newDex = currentDex < 100 ? Math.min(100, currentDex + dexGain) : currentDex
|
|
|
|
|
|
const newInt = currentInt < 100 ? Math.min(100, currentInt + intGain) : currentInt
|
|
|
|
|
|
|
|
|
|
|
|
const newWeight = Math.max(50, this.state.weight + weightChange) // 最低50g
|
|
|
|
|
|
const hungerCost = this.speciesConfig.baseStats.playHungerCost || 1
|
|
|
|
|
|
const newHunger = Math.max(0, this.state.hunger - hungerCost) // 玩耍消耗 3 點飢餓
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
await this.updateState({
|
|
|
|
|
|
happiness: newHappiness,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
hunger: newHunger,
|
|
|
|
|
|
dex: newDex,
|
|
|
|
|
|
int: newInt,
|
|
|
|
|
|
weight: newWeight
|
2025-11-23 18:03:56 +00:00
|
|
|
|
})
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 記錄到成就系統
|
|
|
|
|
|
if (this.achievementSystem) {
|
|
|
|
|
|
await this.achievementSystem.recordAction('play')
|
|
|
|
|
|
this.achievementSystem.checkPerfectState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
this.calculateCombatStats()
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
happiness: newHappiness,
|
|
|
|
|
|
happinessGain,
|
|
|
|
|
|
dexGain,
|
|
|
|
|
|
intGain,
|
|
|
|
|
|
weightChange,
|
|
|
|
|
|
gameType: gameTypeLabel
|
|
|
|
|
|
}
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理便便
|
|
|
|
|
|
async cleanPoop() {
|
2025-11-24 07:38:44 +00:00
|
|
|
|
if (!this.isActionAllowed('clean')) return { success: false, message: '當前階段不需要清理' }
|
2025-11-23 18:03:56 +00:00
|
|
|
|
if (this.state.poopCount === 0) {
|
|
|
|
|
|
return { success: false, message: '沒有便便需要清理' }
|
|
|
|
|
|
}
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
const newPoopCount = 0
|
|
|
|
|
|
const newHappiness = Math.min(100, this.state.happiness + 10)
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
await this.updateState({
|
|
|
|
|
|
poopCount: newPoopCount,
|
|
|
|
|
|
happiness: newHappiness
|
|
|
|
|
|
})
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 記錄到成就系統
|
|
|
|
|
|
if (this.achievementSystem) {
|
|
|
|
|
|
await this.achievementSystem.recordAction('clean')
|
|
|
|
|
|
this.achievementSystem.checkPerfectState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
return { success: true, poopCount: newPoopCount, happiness: newHappiness }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 治療
|
|
|
|
|
|
async heal(amount = 20) {
|
2025-11-24 07:38:44 +00:00
|
|
|
|
if (!this.isActionAllowed('heal')) return { success: false, message: '當前階段不需要治療' }
|
2025-11-23 18:03:56 +00:00
|
|
|
|
if (this.state.isDead) return { success: false, message: '寵物已死亡' }
|
2025-11-24 07:38:44 +00:00
|
|
|
|
if (this.state.isSleeping) return { success: false, message: '寵物正在睡覺,無法治療' }
|
|
|
|
|
|
|
|
|
|
|
|
const maxHealth = this.getMaxHealth()
|
|
|
|
|
|
if (this.state.health >= maxHealth) return { success: false, message: '寵物非常健康,不需要治療' }
|
|
|
|
|
|
|
|
|
|
|
|
// 應用健康恢復加成
|
|
|
|
|
|
const bonuses = this.getAllBonuses()
|
|
|
|
|
|
const healAmount = amount * (1 + (bonuses.healthRecovery || 0))
|
|
|
|
|
|
|
|
|
|
|
|
const newHealth = Math.min(maxHealth, this.state.health + healAmount)
|
2025-11-23 18:03:56 +00:00
|
|
|
|
const wasSick = this.state.isSick
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
await this.updateState({
|
|
|
|
|
|
health: newHealth,
|
|
|
|
|
|
isSick: false
|
|
|
|
|
|
})
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 記錄到成就系統
|
|
|
|
|
|
if (this.achievementSystem) {
|
|
|
|
|
|
await this.achievementSystem.recordAction('heal')
|
|
|
|
|
|
this.achievementSystem.checkPerfectState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
return {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
health: newHealth,
|
2025-11-23 18:03:56 +00:00
|
|
|
|
isSick: false,
|
2025-11-24 07:38:44 +00:00
|
|
|
|
cured: wasSick && !this.state.isSick,
|
|
|
|
|
|
healAmount
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 睡覺/起床
|
|
|
|
|
|
async toggleSleep() {
|
2025-11-24 07:38:44 +00:00
|
|
|
|
if (!this.isActionAllowed('sleep')) return { success: false, message: '當前階段不能睡覺' }
|
2025-11-23 18:03:56 +00:00
|
|
|
|
if (this.state.isDead) return { success: false, message: '寵物已死亡' }
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
const newIsSleeping = !this.state.isSleeping
|
|
|
|
|
|
const healthChange = newIsSleeping ? 5 : 0
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
|
|
|
|
|
// 檢查是否在睡眠時段
|
|
|
|
|
|
const sleepPeriod = this.isInSleepTime()
|
|
|
|
|
|
let message = ''
|
|
|
|
|
|
let willRandomSleep = false
|
|
|
|
|
|
|
|
|
|
|
|
if (newIsSleeping) {
|
|
|
|
|
|
// 進入睡眠
|
|
|
|
|
|
this.state._autoSlept = false // 標記為手動睡眠
|
|
|
|
|
|
message = '寵物進入睡眠'
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 嘗試醒來
|
|
|
|
|
|
if (sleepPeriod) {
|
|
|
|
|
|
// 在睡眠時段醒來,可能會隨機再次入睡
|
|
|
|
|
|
const periodConfig = this.getSleepPeriod(sleepPeriod)
|
|
|
|
|
|
const randomWakeChance = periodConfig?.randomWakeChance || 0
|
|
|
|
|
|
|
|
|
|
|
|
if (Math.random() < randomWakeChance) {
|
|
|
|
|
|
// 隨機再次入睡
|
|
|
|
|
|
willRandomSleep = true
|
|
|
|
|
|
this.state._autoSlept = true
|
|
|
|
|
|
const periodName = sleepPeriod === 'nightSleep' ? '夜間' : '午休'
|
|
|
|
|
|
message = `寵物醒來後打了個哈欠,在${periodName}時段又睡著了...`
|
|
|
|
|
|
|
|
|
|
|
|
// 延遲一下再設回睡眠狀態(給用戶看到醒來的訊息)
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
|
if (this.state && !this.state.isDead) {
|
|
|
|
|
|
this.state.isSleeping = true
|
|
|
|
|
|
await this.updateState({ isSleeping: true, _autoSlept: true })
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 100)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message = '寵物醒來了'
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message = '寵物醒來了'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const maxHealth = this.getMaxHealth()
|
|
|
|
|
|
|
2025-11-23 18:03:56 +00:00
|
|
|
|
await this.updateState({
|
2025-11-24 07:38:44 +00:00
|
|
|
|
isSleeping: willRandomSleep ? true : newIsSleeping,
|
|
|
|
|
|
health: Math.min(maxHealth, this.state.health + healthChange),
|
|
|
|
|
|
_autoSlept: willRandomSleep ? true : (newIsSleeping ? false : undefined)
|
2025-11-23 18:03:56 +00:00
|
|
|
|
})
|
2025-11-24 07:38:44 +00:00
|
|
|
|
|
2025-11-24 10:34:02 +00:00
|
|
|
|
// 記錄到成就系統
|
|
|
|
|
|
if (this.achievementSystem && newIsSleeping) {
|
|
|
|
|
|
await this.achievementSystem.recordAction('sleep')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 07:38:44 +00:00
|
|
|
|
return {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
isSleeping: willRandomSleep ? true : newIsSleeping,
|
|
|
|
|
|
message,
|
|
|
|
|
|
randomSleep: willRandomSleep
|
|
|
|
|
|
}
|
2025-11-23 18:03:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|