good version

This commit is contained in:
王性驊 2025-11-24 21:45:09 +08:00
parent 310844bf1e
commit 384c8df9c7
12 changed files with 1771 additions and 101 deletions

File diff suppressed because it is too large Load Diff

View File

@ -331,5 +331,44 @@ export class AchievementSystem {
return bonuses
}
// 重置成就系統(刪除寵物時使用)
async reset() {
this.unlockedAchievements = []
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
try {
await this.api.saveAchievements({
unlocked: this.unlockedAchievements,
stats: this.achievementStats
})
} catch (error) {
console.warn('[AchievementSystem] API 同步失敗:', error)
// 降級到 localStorage
localStorage.removeItem('achievements')
}
console.log('[AchievementSystem] 成就已重置')
}
}

288
core/adventure-system.js Normal file
View File

@ -0,0 +1,288 @@
// 冒險系統核心
import { apiService } from './api-service.js'
import { ADVENTURES } from '../data/adventures.js'
import { ENEMIES } from '../data/enemies.js'
export class AdventureSystem {
constructor(petSystem, inventorySystem, api = apiService) {
this.petSystem = petSystem
this.inventorySystem = inventorySystem
this.api = api
this.currentAdventure = null
this.adventureTimer = null
this.logs = []
}
// 獲取所有冒險區域
getAdventures() {
return ADVENTURES
}
// 開始冒險
async startAdventure(adventureId) {
const adventure = ADVENTURES.find(a => a.id === adventureId)
if (!adventure) return { success: false, message: '找不到該冒險區域' }
const state = this.petSystem.getState()
// 1. 檢查條件
if (state.isSick || state.isDead || state.isSleeping) {
return { success: false, message: '寵物狀態不適合冒險' }
}
// 檢查等級/階段要求 (這裡簡化為檢查階段)
// 實際應根據 levelRequirement 檢查
// 檢查屬性要求
if (adventure.statsRequirement) {
for (const [stat, value] of Object.entries(adventure.statsRequirement)) {
if ((state[stat] || 0) < value) {
return { success: false, message: `屬性不足:需要 ${stat.toUpperCase()} ${value}` }
}
}
}
// 2. 檢查消耗
if (state.hunger < adventure.cost.hunger) {
return { success: false, message: '飢餓度不足' }
}
if (state.happiness < adventure.cost.happiness) {
return { success: false, message: '快樂度不足' }
}
// 3. 扣除消耗
await this.petSystem.updateState({
hunger: state.hunger - adventure.cost.hunger,
happiness: state.happiness - adventure.cost.happiness
})
// 4. 初始化冒險狀態
this.currentAdventure = {
id: adventureId,
startTime: Date.now(),
duration: adventure.duration * 1000, // 轉為毫秒
endTime: Date.now() + adventure.duration * 1000,
logs: [],
rewards: { items: [], coins: 0 },
encounters: []
}
this.addLog(`出發前往 ${adventure.name}`)
// 5. 啟動計時器
this.startAdventureLoop()
return { success: true, adventure: this.currentAdventure }
}
// 冒險循環
startAdventureLoop() {
if (this.adventureTimer) clearInterval(this.adventureTimer)
this.adventureTimer = setInterval(async () => {
if (!this.currentAdventure) {
this.stopAdventureLoop()
return
}
const now = Date.now()
const adventure = ADVENTURES.find(a => a.id === this.currentAdventure.id)
// 檢查是否結束
if (now >= this.currentAdventure.endTime) {
await this.completeAdventure()
return
}
// 隨機遭遇敵人
if (Math.random() < 0.1) { // 每秒 10% 機率遭遇
await this.encounterEnemy(adventure)
}
}, 1000)
}
stopAdventureLoop() {
if (this.adventureTimer) {
clearInterval(this.adventureTimer)
this.adventureTimer = null
}
}
// 遭遇敵人
async encounterEnemy(adventure) {
if (!adventure.enemyPool || adventure.enemyPool.length === 0) return
// 隨機選擇敵人
const enemyId = adventure.enemyPool[Math.floor(Math.random() * adventure.enemyPool.length)]
const enemyConfig = ENEMIES[enemyId]
if (!enemyConfig) return
this.addLog(`遭遇了 ${enemyConfig.name}!準備戰鬥!`)
// 執行戰鬥
const combatResult = this.calculateCombat(enemyConfig)
// 記錄戰鬥過程
combatResult.logs.forEach(log => this.addLog(log))
if (combatResult.win) {
this.addLog(`戰鬥勝利!`)
// 計算掉落
if (enemyConfig.drops) {
for (const drop of enemyConfig.drops) {
if (Math.random() < drop.chance) {
// 檢查是否為金幣
if (drop.itemId === 'gold_coin') {
// 金幣直接加到獎勵中
if (!this.currentAdventure.rewards.coins) {
this.currentAdventure.rewards.coins = 0
}
this.currentAdventure.rewards.coins += drop.count || 1
this.addLog(`獲得金幣:${drop.count || 1} 💰`)
} else {
// 其他道具加到物品列表
this.currentAdventure.rewards.items.push({
itemId: drop.itemId,
count: drop.count || 1,
name: drop.itemId // 暫時用 ID實際應查表
})
this.addLog(`獲得戰利品:${drop.itemId} x${drop.count || 1}`)
}
}
}
}
} else {
this.addLog(`戰鬥失敗... 寵物受傷逃跑了。`)
// 扣除健康
const state = this.petSystem.getState()
await this.petSystem.updateState({
health: Math.max(0, state.health - 10)
})
}
}
// 計算戰鬥 (簡化版回合制)
calculateCombat(enemy) {
const state = this.petSystem.getState()
const petStats = {
hp: state.health, // 用健康度當 HP
attack: state.attack || 10,
defense: state.defense || 0,
speed: state.speed || 10
}
const enemyStats = { ...enemy.stats }
const logs = []
let round = 1
let win = false
while (round <= 10) { // 最多 10 回合
// 速度決定先手
const petFirst = petStats.speed >= enemyStats.speed
// 雙方攻擊
const attacker = petFirst ? petStats : enemyStats
const defender = petFirst ? enemyStats : petStats
const attackerName = petFirst ? '你' : enemy.name
const defenderName = petFirst ? enemy.name : '你'
// 第一擊
let damage = Math.max(1, attacker.attack - defender.defense)
// 隨機浮動 0.8 ~ 1.2
damage = Math.floor(damage * (0.8 + Math.random() * 0.4))
defender.hp -= damage
logs.push(`[回合${round}] ${attackerName}${defenderName} 造成 ${damage} 點傷害`)
if (defender.hp <= 0) {
win = petFirst
break
}
// 第二擊
damage = Math.max(1, defender.attack - attacker.defense)
damage = Math.floor(damage * (0.8 + Math.random() * 0.4))
attacker.hp -= damage
logs.push(`[回合${round}] ${defenderName}${attackerName} 造成 ${damage} 點傷害`)
if (attacker.hp <= 0) {
win = !petFirst
break
}
round++
}
if (round > 10) {
logs.push('戰鬥超時,雙方平手(視為失敗)')
win = false
}
return { win, logs }
}
// 完成冒險
async completeAdventure() {
this.stopAdventureLoop()
const adventure = ADVENTURES.find(a => a.id === this.currentAdventure.id)
this.addLog('冒險結束!')
// 發放基礎獎勵
if (adventure.rewards) {
if (adventure.rewards.items) {
for (const reward of adventure.rewards.items) {
if (Math.random() < reward.chance) {
this.currentAdventure.rewards.items.push({
itemId: reward.itemId,
count: 1,
name: reward.itemId
})
}
}
}
}
// 實際發放道具到背包
if (this.inventorySystem) {
for (const item of this.currentAdventure.rewards.items) {
await this.inventorySystem.addItem(item.itemId, item.count)
}
}
// 發放金幣
if (this.currentAdventure.rewards.coins > 0) {
const state = this.petSystem.getState()
await this.petSystem.updateState({
coins: (state.coins || 0) + this.currentAdventure.rewards.coins
})
this.addLog(`獲得金幣:${this.currentAdventure.rewards.coins} 💰`)
}
// 觸發完成回調(如果有 UI 監聽)
if (this.onAdventureComplete) {
this.onAdventureComplete(this.currentAdventure)
}
this.currentAdventure = null
}
// 添加日誌
addLog(message) {
if (this.currentAdventure) {
const time = new Date().toLocaleTimeString()
this.currentAdventure.logs.push(`[${time}] ${message}`)
// 觸發更新回調
if (this.onLogUpdate) {
this.onLogUpdate(this.currentAdventure.logs)
}
}
}
// 獲取當前冒險狀態
getCurrentAdventure() {
return this.currentAdventure
}
}

View File

@ -55,6 +55,12 @@ export class ApiService {
return this.getMockInventory()
case 'inventory/save':
return this.saveMockInventory(options.body)
case 'adventure/list':
return this.getMockAdventures()
case 'adventure/start':
return { success: true, message: '冒險開始' } // 實際邏輯在 AdventureSystem
case 'adventure/complete':
return { success: true, message: '冒險完成' }
default:
throw new Error(`Unknown endpoint: ${endpoint}`)
}
@ -175,6 +181,25 @@ export class ApiService {
})
}
// 冒險相關
async getAdventures() {
return this.request('adventure/list')
}
async startAdventure(adventureId) {
return this.request('adventure/start', {
method: 'POST',
body: { adventureId }
})
}
async completeAdventure(result) {
return this.request('adventure/complete', {
method: 'POST',
body: result
})
}
// ========== Mock 資料方法 ==========
getMockPetState() {
@ -188,6 +213,11 @@ export class ApiService {
updateMockPetState(updates) {
const current = this.getMockPetState()
if (!current) {
console.warn('[ApiService] No current state found, cannot update')
return { success: false, message: '找不到當前狀態' }
}
// 使用深度合併,確保不會丟失原有字段
const updated = { ...current, ...updates }
localStorage.setItem('petState', JSON.stringify(updated))
return { success: true, data: updated }
@ -345,6 +375,11 @@ export class ApiService {
return { success: false, message: '儲存失敗: ' + error.message }
}
}
async getMockAdventures() {
const { ADVENTURES } = await import('../data/adventures.js')
return ADVENTURES
}
}
// 預設實例

View File

@ -337,7 +337,8 @@ export class InventorySystem {
// 裝備道具
// equipType: 'equipment' | 'appearance' | 'auto' (自動判斷)
async equipItem(itemId, slot = null, equipType = 'auto') {
// instanceId: 指定要裝備的實例ID可選如果不指定則自動選擇第一個未裝備的
async equipItem(itemId, slot = null, equipType = 'auto', instanceId = null) {
const item = this.items[itemId]
if (!item) {
return { success: false, message: '道具不存在' }

View File

@ -24,19 +24,29 @@ export class PetSystem {
if (!this.state) {
// 創建新寵物
this.state = this.createInitialState(speciesId)
// 載入種族配置
this.speciesConfig = PET_SPECIES[this.state.speciesId] || PET_SPECIES[speciesId]
// 計算戰鬥數值(在保存前)
this.calculateCombatStats()
// 保存完整狀態(包含計算後的戰鬥數值)
await this.api.savePetState(this.state)
} else {
// 確保 achievementBuffs 存在(向後兼容)
if (!this.state.achievementBuffs) {
this.state.achievementBuffs = {}
}
// 確保 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]
// 計算戰鬥數值(在 speciesConfig 設置後)
// 計算戰鬥數值
this.calculateCombatStats()
}
return this.state
} catch (error) {
@ -85,8 +95,9 @@ export class PetSystem {
generation: 1,
lastTickTime: Date.now(),
achievementBuffs: {}, // 成就加成
equipmentBuffs: {}, // 裝備加成
appearance: {} // 外觀設定
equipmentBuffs: { flat: {}, percent: {} }, // 裝備加成
appearance: {}, // 外觀設定
coins: 100 // 金幣(初始 100
}
// 分配命格
@ -559,7 +570,13 @@ export class PetSystem {
// 戰鬥數值
attack: this.state.attack,
defense: this.state.defense,
speed: this.state.speed
speed: this.state.speed,
// 命格和神明
destiny: this.state.destiny,
currentDeityId: this.state.currentDeityId,
deityFavors: this.state.deityFavors,
// 经济
coins: this.state.coins
})
}
@ -599,7 +616,8 @@ export class PetSystem {
// 檢查屬性條件
let statsConditionMet = true
const missingStats = []
let missingStats = []
if (nextStage.conditions) {
if (nextStage.conditions.str && this.state.str < nextStage.conditions.str) {
statsConditionMet = false
@ -616,10 +634,65 @@ export class PetSystem {
}
if (timeConditionMet && statsConditionMet) {
console.log(`\n✨ 進化!${currentStage}${nextStage.stage} (年齡: ${ageSeconds.toFixed(1)}秒)`)
this.state.stage = nextStage.stage
// 檢查是否有進化分支
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 }
// 應用進化分支效果
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
this.state._evolutionWarned = false // 重置警告狀態
this.updateState({ stage: nextStage.stage })
this.updateState(updates)
} else if (timeConditionMet && !statsConditionMet) {
// 時間到了但屬性不足,只在第一次提示
if (!this.state._evolutionWarned) {

View File

@ -131,7 +131,9 @@ export class TempleSystem {
// 獲取好感度星級(每 20 點一星)
getFavorStars(deityId) {
if (!deityId) return '☆☆☆☆☆'
const state = this.petSystem.getState()
if (!state || !state.deityFavors) return '☆☆☆☆☆'
const favor = state.deityFavors[deityId] || 0
const stars = Math.floor(favor / 20)
return '★'.repeat(stars) + '☆'.repeat(5 - stars)

59
data/adventures.js Normal file
View File

@ -0,0 +1,59 @@
// 冒險區域配置(資料驅動)
export const ADVENTURES = [
{
id: 'backyard',
name: '自家後院',
description: '安全的新手探險地,偶爾會有小蟲子。',
statsRequirement: null, // 無屬性要求
cost: {
hunger: 5,
happiness: 5
},
duration: 10, // 測試用10秒 (實際可能 60秒)
enemyPool: ['cockroach', 'mouse'],
enemyRate: 0.6, // 60% 機率遇到敵人
rewards: {
items: [
{ itemId: 'cookie', chance: 0.2 }
]
}
},
{
id: 'park',
name: '附近的公園',
description: '熱鬧的公園,但也潛藏著流浪動物的威脅。',
statsRequirement: { str: 20 }, // 需要力量 20
cost: {
hunger: 15,
happiness: 10
},
duration: 30, // 測試用30秒
enemyPool: ['stray_dog', 'wild_cat'],
enemyRate: 0.7,
rewards: {
items: [
{ itemId: 'tuna_can', chance: 0.3 },
{ itemId: 'ball', chance: 0.2 }
]
}
},
{
id: 'forest',
name: '神秘森林',
description: '危險的未知區域,只有強者才能生存。',
statsRequirement: { str: 50, int: 30 },
cost: {
hunger: 30,
happiness: 20
},
duration: 60, // 測試用60秒
enemyPool: ['snake', 'bear'],
enemyRate: 0.8,
rewards: {
items: [
{ itemId: 'vitality_potion', chance: 0.3 },
{ itemId: 'magic_wand', chance: 0.1 }
]
}
}
]

97
data/enemies.js Normal file
View File

@ -0,0 +1,97 @@
// 敵人配置(資料驅動)
export const ENEMIES = {
// 新手區敵人
cockroach: {
id: 'cockroach',
name: '巨大的蟑螂',
description: '生命力頑強的害蟲,雖然弱小但很噁心。',
stats: {
hp: 20,
attack: 5,
defense: 0,
speed: 5
},
drops: [
{ itemId: 'cookie', chance: 0.3, count: 1 }
]
},
mouse: {
id: 'mouse',
name: '偷吃的老鼠',
description: '動作敏捷的小偷,喜歡偷吃東西。',
stats: {
hp: 35,
attack: 8,
defense: 2,
speed: 15
},
drops: [
{ itemId: 'cookie', chance: 0.4, count: 1 },
{ itemId: 'wooden_sword', chance: 0.05, count: 1 }
]
},
// 公園區敵人
stray_dog: {
id: 'stray_dog',
name: '兇猛的野狗',
description: '為了搶地盤而變得兇暴的野狗。',
stats: {
hp: 80,
attack: 15,
defense: 5,
speed: 10
},
drops: [
{ itemId: 'tuna_can', chance: 0.3, count: 1 },
{ itemId: 'leather_armor', chance: 0.1, count: 1 }
]
},
wild_cat: {
id: 'wild_cat',
name: '流浪貓老大',
description: '這片區域的老大,身手矯健。',
stats: {
hp: 100,
attack: 20,
defense: 8,
speed: 25
},
drops: [
{ itemId: 'premium_food', chance: 0.2, count: 1 },
{ itemId: 'lucky_charm', chance: 0.05, count: 1 }
]
},
// 森林區敵人
snake: {
id: 'snake',
name: '毒蛇',
description: '潛伏在草叢中的危險掠食者。',
stats: {
hp: 150,
attack: 35,
defense: 10,
speed: 30
},
drops: [
{ itemId: 'vitality_potion', chance: 0.2, count: 1 },
{ itemId: 'magic_wand', chance: 0.05, count: 1 }
]
},
bear: {
id: 'bear',
name: '暴躁的黑熊',
description: '森林中的霸主,力量驚人。',
stats: {
hp: 300,
attack: 50,
defense: 30,
speed: 10
},
drops: [
{ itemId: 'gold_coin', chance: 0.5, count: 10 }, // 假設有金幣
{ itemId: 'hero_sword', chance: 0.02, count: 1 }
]
}
}

View File

@ -153,7 +153,67 @@ export const PET_SPECIES = {
str: 50, // 3天內累積
int: 50,
dex: 50
},
// 進化分支配置
evolutions: [
{
id: 'warrior_tiger',
name: '戰士猛虎',
icon: '🐯',
description: '力量強大的猛虎形態',
conditions: {
str: { min: 50, dominant: true } // STR 需大於 INT+DEX
},
statModifiers: {
attack: 1.3,
defense: 1.2,
speed: 0.8
}
},
{
id: 'agile_cat',
name: '敏捷靈貓',
icon: '🐈',
description: '身手矯健的靈貓形態',
conditions: {
dex: { min: 50, dominant: true } // DEX 需大於 STR+INT
},
statModifiers: {
attack: 1.0,
defense: 0.8,
speed: 1.4
}
},
{
id: 'sage_cat',
name: '智者賢貓',
icon: '😺',
description: '充滿智慧的賢貓形態',
conditions: {
int: { min: 50, dominant: true } // INT 需大於 STR+DEX
},
statModifiers: {
attack: 0.9,
defense: 1.1,
speed: 1.0,
magic: 1.5
}
},
{
id: 'balanced_cat',
name: '成年貓',
icon: '😸',
description: '均衡發展的成年貓形態',
conditions: {
// 默認分支,無特殊條件
},
statModifiers: {
attack: 1.0,
defense: 1.0,
speed: 1.0
}
}
]
}
],
personality: ['活潑', '黏人']

121
data/shop.js Normal file
View File

@ -0,0 +1,121 @@
// 商店商品配置(資料驅動 - 隨機刷新機制)
// 商品池 - 所有可能出現的商品及其出現機率
export const SHOP_POOL = [
// 食物類 - 高機率
{
itemId: 'cookie',
category: 'food',
price: 10,
appearChance: 0.8, // 80% 機率出現
sellPrice: 5 // 賣出價格為購買價一半
},
{
itemId: 'tuna_can',
category: 'food',
price: 30,
appearChance: 0.6,
sellPrice: 15
},
{
itemId: 'premium_food',
category: 'food',
price: 50,
appearChance: 0.3, // 30% 機率
sellPrice: 25
},
// 藥品類 - 中機率
{
itemId: 'vitality_potion',
category: 'medicine',
price: 40,
appearChance: 0.5,
sellPrice: 20
},
// 裝備類 - 低機率
{
itemId: 'wooden_sword',
category: 'equipment',
price: 80,
appearChance: 0.3,
sellPrice: 40
},
{
itemId: 'leather_armor',
category: 'equipment',
price: 100,
appearChance: 0.25,
sellPrice: 50
},
{
itemId: 'magic_wand',
category: 'equipment',
price: 150,
appearChance: 0.15, // 稀有
sellPrice: 75
},
{
itemId: 'hero_sword',
category: 'equipment',
price: 300,
appearChance: 0.05, // 極稀有
sellPrice: 150
},
// 玩具類
{
itemId: 'ball',
category: 'toy',
price: 20,
appearChance: 0.6,
sellPrice: 10
},
// 飾品類 - 稀有
{
itemId: 'lucky_charm',
category: 'accessory',
price: 150,
appearChance: 0.2,
sellPrice: 75
}
]
// 商品分類
export const SHOP_CATEGORIES = {
food: { name: '食物', icon: '🍖' },
medicine: { name: '藥品', icon: '💊' },
equipment: { name: '裝備', icon: '⚔️' },
toy: { name: '玩具', icon: '🎾' },
accessory: { name: '飾品', icon: '✨' }
}
// 生成隨機商店商品列表
export function generateShopItems() {
const items = []
for (const poolItem of SHOP_POOL) {
// 按機率決定是否出現
if (Math.random() < poolItem.appearChance) {
items.push({
...poolItem,
stock: -1 // 無限庫存
})
}
}
// 確保至少有3個商品
if (items.length < 3) {
// 隨機補充商品
const remaining = SHOP_POOL.filter(p => !items.find(i => i.itemId === p.itemId))
while (items.length < 3 && remaining.length > 0) {
const randomIndex = Math.floor(Math.random() * remaining.length)
items.push({
...remaining[randomIndex],
stock: -1
})
remaining.splice(randomIndex, 1)
}
}
return items
}