fix: good friend
This commit is contained in:
parent
63ee9b71ef
commit
c779fd9a0e
|
|
@ -1,12 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full bg-black text-[#99e550] relative">
|
<div class="flex flex-col h-full bg-black text-[#99e550] relative">
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-center gap-2 text-xl font-bold p-2 border-b-2 border-[#99e550]">
|
|
||||||
<Map class="text-[#e0d8f0]" />
|
|
||||||
<span class="tracking-widest">選擇冒險區域 (SELECT ZONE)</span>
|
|
||||||
<button @click="$emit('close')" class="ml-auto text-white hover:text-red-500"><X /></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-grow overflow-y-auto p-4 custom-scrollbar flex flex-col gap-4">
|
<div class="flex-grow overflow-y-auto p-4 custom-scrollbar flex flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
|
|
@ -21,7 +14,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p class="text-xs text-white mb-4 text-center">
|
<p class="text-xs text-white mb-4">
|
||||||
{{ loc.description }}
|
{{ loc.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
@ -44,37 +37,38 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Button -->
|
<!-- Action Buttons -->
|
||||||
<button
|
<div class="flex gap-2 mt-auto">
|
||||||
@click="!isLocked(loc) && canAfford(loc) && $emit('selectLocation', loc)"
|
<!-- Battle Button: Requires Stats & Costs -->
|
||||||
:disabled="!canAfford(loc) || isLocked(loc)"
|
<button
|
||||||
class="w-full py-2 text-lg tracking-[0.2em] border"
|
@click="!isLocked(loc) && canAfford(loc) && $emit('selectLocation', { location: loc, mode: 'battle' })"
|
||||||
:class="(!canAfford(loc) || isLocked(loc))
|
:disabled="!canAfford(loc) || isLocked(loc)"
|
||||||
? 'border-gray-600 text-gray-500 cursor-not-allowed'
|
class="flex-1 py-2 text-sm tracking-[0.1em] border transition-colors"
|
||||||
: 'border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-black'"
|
:class="(!canAfford(loc) || isLocked(loc))
|
||||||
>
|
? 'border-gray-600 text-gray-500 cursor-not-allowed'
|
||||||
{{ isLocked(loc) ? "能力不足" : !canAfford(loc) ? "資源不足" : "出發 !" }}
|
: 'border-[#d95763] text-[#d95763] hover:bg-[#d95763] hover:text-black'"
|
||||||
</button>
|
>
|
||||||
|
<Swords :size="16" class="inline mr-1" /> 戰鬥
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- View Button: No Restrictions -->
|
||||||
|
<button
|
||||||
|
@click="$emit('selectLocation', { location: loc, mode: 'view' })"
|
||||||
|
class="flex-1 py-2 text-sm tracking-[0.1em] border transition-colors border-[#2ce8f4] text-[#2ce8f4] hover:bg-[#2ce8f4] hover:text-black"
|
||||||
|
>
|
||||||
|
<Eye :size="16" class="inline mr-1" /> 參觀
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Side Decoration Bar -->
|
<!-- Side Decoration Bar -->
|
||||||
<div class="absolute top-2 bottom-2 right-2 w-2" :class="isLocked(loc) ? 'bg-gray-600' : 'bg-[#99e550]'"></div>
|
<div class="absolute top-2 bottom-2 right-2 w-2" :class="isLocked(loc) ? 'bg-gray-600' : 'bg-[#99e550]'"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer / Close Button -->
|
|
||||||
<div class="p-4 border-t border-[#99e550]">
|
|
||||||
<button
|
|
||||||
@click="$emit('close')"
|
|
||||||
class="border border-[#99e550] text-[#99e550] px-4 py-2 hover:bg-[#99e550] hover:text-black"
|
|
||||||
>
|
|
||||||
關閉
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Map, Drumstick, Coins, X, Swords } from 'lucide-vue-next';
|
import { Map, Drumstick, Coins, X, Swords, Eye } from 'lucide-vue-next';
|
||||||
import PixelFrame from './PixelFrame.vue';
|
import PixelFrame from './PixelFrame.vue';
|
||||||
import PixelButton from './PixelButton.vue';
|
import PixelButton from './PixelButton.vue';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,10 @@
|
||||||
<div v-else class="h-full w-full relative overflow-hidden bg-[#0f0816]">
|
<div v-else class="h-full w-full relative overflow-hidden bg-[#0f0816]">
|
||||||
<!-- Background Layer with Darker Filter -->
|
<!-- Background Layer with Darker Filter -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-cover bg-center opacity-50"
|
class="absolute inset-0 bg-cover bg-center transition-all duration-1000"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundImage: `url('https://picsum.photos/seed/dungeon/800/400')`,
|
backgroundImage: `url('${backgroundImage || 'https://picsum.photos/seed/dungeon/800/400'}')`,
|
||||||
filter: 'contrast(1.2) brightness(0.5) sepia(0.5) hue-rotate(260deg) saturate(1.5)'
|
filter: 'contrast(1.1) brightness(0.6) sepia(0.3)'
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -58,33 +58,24 @@
|
||||||
</div>
|
</div>
|
||||||
</PixelFrame>
|
</PixelFrame>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Pet Avatar (Center, Idle) -->
|
|
||||||
<div class="absolute bottom-16 left-1/2 transform -translate-x-1/2 z-10 scale-[3]">
|
|
||||||
<PixelAvatar
|
|
||||||
skinColor="#ffdbac"
|
|
||||||
hairColor="#e0d8f0"
|
|
||||||
outfitColor="#9fd75b"
|
|
||||||
:deityId="currentDeityId"
|
|
||||||
weapon="staff"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue';
|
import { ref, watch, nextTick } from 'vue';
|
||||||
import PixelAvatar from './PixelAvatar.vue';
|
// PixelAvatar removed as requested
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentDeityId?: string;
|
currentDeityId?: string;
|
||||||
isFighting?: boolean;
|
isFighting?: boolean;
|
||||||
battleLogs?: string[];
|
battleLogs?: string[];
|
||||||
|
backgroundImage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
isFighting: false,
|
isFighting: false,
|
||||||
battleLogs: () => []
|
battleLogs: () => [],
|
||||||
|
backgroundImage: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const logEndRef = ref<HTMLDivElement | null>(null);
|
const logEndRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
:currentDeityId="currentDeity"
|
:currentDeityId="currentDeity"
|
||||||
:isFighting="isFighting"
|
:isFighting="isFighting"
|
||||||
:battleLogs="battleLogs"
|
:battleLogs="battleLogs"
|
||||||
|
:backgroundImage="currentLocation?.backgroundImage || ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -129,7 +130,7 @@
|
||||||
title="冒險"
|
title="冒險"
|
||||||
>
|
>
|
||||||
<AdventureOverlay
|
<AdventureOverlay
|
||||||
:locations="ADVENTURE_LOCATIONS"
|
:locations="LOCATIONS"
|
||||||
:playerStats="playerStats"
|
:playerStats="playerStats"
|
||||||
@selectLocation="handleStartAdventure"
|
@selectLocation="handleStartAdventure"
|
||||||
@close="showAdventureSelect = false"
|
@close="showAdventureSelect = false"
|
||||||
|
|
@ -143,7 +144,18 @@
|
||||||
<PartyPopper :size="48" class="text-[#99e550] animate-bounce" />
|
<PartyPopper :size="48" class="text-[#99e550] animate-bounce" />
|
||||||
<h2 class="text-2xl text-[#99e550] font-bold tracking-widest">冒險完成 !</h2>
|
<h2 class="text-2xl text-[#99e550] font-bold tracking-widest">冒險完成 !</h2>
|
||||||
<div class="w-full border-t border-gray-700 my-2"></div>
|
<div class="w-full border-t border-gray-700 my-2"></div>
|
||||||
<p class="text-gray-400 text-sm">這次沒有獲得任何獎勵...</p>
|
<!-- Rewards -->
|
||||||
|
<div v-if="battleResult && battleResult.rewards" class="w-full text-left text-sm text-gray-300">
|
||||||
|
<p v-if="battleResult.rewards.exp && battleResult.rewards.exp > 0">✨ 獲得 {{ battleResult.rewards.exp }} 經驗值</p>
|
||||||
|
<p v-if="battleResult.rewards.items && battleResult.rewards.items.length > 0">🎁 獲得道具:</p>
|
||||||
|
<ul v-if="battleResult.rewards.items && battleResult.rewards.items.length > 0" class="list-disc list-inside ml-4">
|
||||||
|
<li v-for="(item, idx) in battleResult.rewards.items" :key="idx">{{ item.id }} x{{ item.count }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!-- Logs -->
|
||||||
|
<div v-if="battleResult && battleResult.logs && battleResult.logs.length > 0" class="w-full max-h-48 overflow-y-auto custom-scrollbar text-xs text-gray-200">
|
||||||
|
<p class="font-mono" v-for="(log, idx) in battleResult.logs" :key="idx">{{ idx + 1 }}. {{ log }}</p>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="handleCloseBattleResult"
|
@click="handleCloseBattleResult"
|
||||||
class="mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black tracking-widest"
|
class="mt-6 border border-[#99e550] text-[#99e550] px-8 py-2 hover:bg-[#99e550] hover:text-black tracking-widest"
|
||||||
|
|
@ -188,6 +200,7 @@ import { DEITIES } from '../../data/deities.js';
|
||||||
import { ITEMS, ITEM_RARITY, EQUIPMENT_SLOTS } from '../../data/items.js';
|
import { ITEMS, ITEM_RARITY, EQUIPMENT_SLOTS } from '../../data/items.js';
|
||||||
import { ADVENTURES } from '../../data/adventures.js';
|
import { ADVENTURES } from '../../data/adventures.js';
|
||||||
import { ACHIEVEMENTS } from '../../data/achievements.js';
|
import { ACHIEVEMENTS } from '../../data/achievements.js';
|
||||||
|
import { LOCATIONS } from '../../data/locations.js';
|
||||||
|
|
||||||
// Type definitions (minimal, based on actual data structures)
|
// Type definitions (minimal, based on actual data structures)
|
||||||
type EntityStats = any;
|
type EntityStats = any;
|
||||||
|
|
@ -313,11 +326,14 @@ const showGodSystem = ref(false);
|
||||||
const showShop = ref(false);
|
const showShop = ref(false);
|
||||||
const showAdventureSelect = ref(false);
|
const showAdventureSelect = ref(false);
|
||||||
const showBattleResult = ref(false);
|
const showBattleResult = ref(false);
|
||||||
|
const battleResult = ref<{logs:string[], rewards:any}|null>(null);
|
||||||
|
|
||||||
// Battle State
|
// Battle State
|
||||||
const isFighting = ref(false);
|
const isFighting = ref(false);
|
||||||
const battleLogs = ref<string[]>([]);
|
const battleLogs = ref<string[]>([]);
|
||||||
const showNamingOverlay = ref(false);
|
const showNamingOverlay = ref(false);
|
||||||
|
const currentLocation = ref<any>(null);
|
||||||
|
const combatInterval = ref<any>(null);
|
||||||
|
|
||||||
const handleNameSubmit = async (payload: { name: string, species: string }) => {
|
const handleNameSubmit = async (payload: { name: string, species: string }) => {
|
||||||
if (petSystem.value) {
|
if (petSystem.value) {
|
||||||
|
|
@ -347,6 +363,26 @@ const handleDeletePet = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- HELPERS (Moved) ---
|
||||||
|
const ADVENTURE_LOCATIONS = ADVENTURES;
|
||||||
|
const ACHIEVEMENTS_DATA = ACHIEVEMENTS;
|
||||||
|
const updateAchievementsState = () => {
|
||||||
|
if (achievementSystem.value) {
|
||||||
|
const all = achievementSystem.value.achievements;
|
||||||
|
achievementsState.value = all.map(achievement => {
|
||||||
|
const progress = achievementSystem.value!.getAchievementProgress(achievement);
|
||||||
|
const isUnlocked = achievementSystem.value!.unlockedAchievements.includes(achievement.id);
|
||||||
|
return {
|
||||||
|
...achievement,
|
||||||
|
unlocked: isUnlocked,
|
||||||
|
currentValue: progress.current,
|
||||||
|
maxValue: progress.target,
|
||||||
|
progress: Math.floor(progress.progress || 0)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- LIFECYCLE ---
|
// --- LIFECYCLE ---
|
||||||
|
|
||||||
const stateSyncInterval = ref<any>(null);
|
const stateSyncInterval = ref<any>(null);
|
||||||
|
|
@ -438,43 +474,79 @@ const formatAge = (seconds: number) => {
|
||||||
|
|
||||||
// --- HANDLERS ---
|
// --- HANDLERS ---
|
||||||
|
|
||||||
const handleStartAdventure = (location: AdventureLocation) => {
|
const handleStartAdventure = (selection: any) => {
|
||||||
|
const { location, mode } = selection;
|
||||||
showAdventureSelect.value = false;
|
showAdventureSelect.value = false;
|
||||||
|
|
||||||
|
// Deduct costs
|
||||||
if (petSystem.value) {
|
if (petSystem.value) {
|
||||||
petSystem.value.updateState({
|
petSystem.value.updateState({
|
||||||
hunger: Math.max(0, systemState.value.hunger - location.costHunger),
|
hunger: Math.max(0, systemState.value.hunger - (location.costHunger || 0)),
|
||||||
coins: Math.max(0, systemState.value.coins - location.costGold)
|
coins: Math.max(0, systemState.value.coins - (location.costGold || 0))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isFighting.value = true;
|
currentLocation.value = location;
|
||||||
battleLogs.value = [`Entered ${location.name}...`, `Encountered ${location.enemyName}!`];
|
|
||||||
|
|
||||||
let turn = 1;
|
if (mode === 'battle') {
|
||||||
const interval = setInterval(() => {
|
isFighting.value = true;
|
||||||
if (turn > 5) {
|
startCombatLoop(location);
|
||||||
clearInterval(interval);
|
} else {
|
||||||
battleLogs.value.push("Victory!", "Obtained 10 EXP!");
|
isFighting.value = false;
|
||||||
setTimeout(() => {
|
handleViewLocation(location);
|
||||||
isFighting.value = false;
|
}
|
||||||
showBattleResult.value = true;
|
};
|
||||||
}, 1500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPlayerTurn = turn % 2 !== 0;
|
const startCombatLoop = (location: any) => {
|
||||||
if (isPlayerTurn) {
|
// Select random enemy from pool
|
||||||
battleLogs.value.push(`You used Attack! Dealt ${Math.floor(Math.random() * 20) + 10} damage.`);
|
const enemyId = location.enemyPool[Math.floor(Math.random() * location.enemyPool.length)];
|
||||||
} else {
|
const result = petSystem.value.startCombat(enemyId);
|
||||||
battleLogs.value.push(`${location.enemyName} attacked! You took ${Math.floor(Math.random() * 10)} damage.`);
|
|
||||||
|
if (result.success) {
|
||||||
|
battleLogs.value = result.combatState.logs;
|
||||||
|
|
||||||
|
// Clear existing interval if any
|
||||||
|
if (combatInterval.value) clearInterval(combatInterval.value);
|
||||||
|
|
||||||
|
// Start Combat Interval
|
||||||
|
combatInterval.value = setInterval(() => {
|
||||||
|
const roundResult = petSystem.value.combatRound();
|
||||||
|
if (roundResult) {
|
||||||
|
battleLogs.value = [...roundResult.logs]; // Update logs
|
||||||
|
|
||||||
|
if (roundResult.isOver) {
|
||||||
|
clearInterval(combatInterval.value);
|
||||||
|
combatInterval.value = null;
|
||||||
|
|
||||||
|
// Delay before closing or showing result
|
||||||
|
setTimeout(() => {
|
||||||
|
isFighting.value = false;
|
||||||
|
battleResult.value = { logs: roundResult.logs, rewards: roundResult.rewards || {} };
|
||||||
|
showBattleResult.value = true;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1500); // 1.5s per round
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewLocation = (location: any) => {
|
||||||
|
// Trigger random event
|
||||||
|
if (location.events && location.events.length > 0) {
|
||||||
|
const event = location.events[Math.floor(Math.random() * location.events.length)];
|
||||||
|
console.log("Event:", event);
|
||||||
|
// For now, maybe just log it or show a simple alert/notification
|
||||||
|
alert(`[${location.name}] ${event.text}`);
|
||||||
|
if (event.type === 'item') {
|
||||||
|
// Add item logic here if needed
|
||||||
}
|
}
|
||||||
turn++;
|
}
|
||||||
}, 1000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseBattleResult = () => {
|
const handleCloseBattleResult = () => {
|
||||||
showBattleResult.value = false;
|
showBattleResult.value = false;
|
||||||
battleLogs.value = [];
|
battleLogs.value = [];
|
||||||
|
battleResult.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFeed = async () => {
|
const handleFeed = async () => {
|
||||||
|
|
@ -828,24 +900,7 @@ const handleSellItem = async (item: Item) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use imported data instead of mock
|
// Use imported data instead of mock
|
||||||
const ADVENTURE_LOCATIONS = ADVENTURES;
|
|
||||||
const ACHIEVEMENTS_DATA = ACHIEVEMENTS;
|
|
||||||
const updateAchievementsState = () => {
|
|
||||||
if (achievementSystem.value) {
|
|
||||||
const all = achievementSystem.value.achievements;
|
|
||||||
achievementsState.value = all.map(achievement => {
|
|
||||||
const progress = achievementSystem.value!.getAchievementProgress(achievement);
|
|
||||||
const isUnlocked = achievementSystem.value!.unlockedAchievements.includes(achievement.id);
|
|
||||||
return {
|
|
||||||
...achievement,
|
|
||||||
unlocked: isUnlocked,
|
|
||||||
currentValue: progress.current,
|
|
||||||
maxValue: progress.target,
|
|
||||||
progress: Math.floor(progress.progress || 0)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert ITEMS object to array for shop
|
// Convert ITEMS object to array for shop
|
||||||
const SHOP_ITEMS = Object.values(ITEMS).filter((item: any) => item.type === 'consumable' || item.type === 'equipment').map((item: any) => ({
|
const SHOP_ITEMS = Object.values(ITEMS).filter((item: any) => item.type === 'consumable' || item.type === 'equipment').map((item: any) => ({
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { FATES } from '../data/fates.js'
|
||||||
import { DEITIES } from '../data/deities.js'
|
import { DEITIES } from '../data/deities.js'
|
||||||
import { DeitySystem } from './deity-system.js'
|
import { DeitySystem } from './deity-system.js'
|
||||||
import { QUEST_TYPES } from '../data/deity-quests.js'
|
import { QUEST_TYPES } from '../data/deity-quests.js'
|
||||||
|
import { ENEMIES } from '../data/enemies.js'
|
||||||
|
|
||||||
export class PetSystem {
|
export class PetSystem {
|
||||||
constructor(api = apiService, achievementSystem = null, inventorySystem = null) {
|
constructor(api = apiService, achievementSystem = null, inventorySystem = null) {
|
||||||
|
|
@ -1068,6 +1069,7 @@ export class PetSystem {
|
||||||
_autoSlept: willRandomSleep ? true : (newIsSleeping ? false : undefined)
|
_autoSlept: willRandomSleep ? true : (newIsSleeping ? false : undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
// 記錄到成就系統
|
// 記錄到成就系統
|
||||||
if (this.achievementSystem && newIsSleeping) {
|
if (this.achievementSystem && newIsSleeping) {
|
||||||
await this.achievementSystem.recordAction('sleep')
|
await this.achievementSystem.recordAction('sleep')
|
||||||
|
|
@ -1080,5 +1082,169 @@ export class PetSystem {
|
||||||
randomSleep: willRandomSleep
|
randomSleep: willRandomSleep
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// --- COMBAT SYSTEM ---
|
||||||
|
|
||||||
|
startCombat(enemyId) {
|
||||||
|
const enemyConfig = ENEMIES[enemyId];
|
||||||
|
if (!enemyConfig) return { success: false, message: '找不到敵人' };
|
||||||
|
|
||||||
|
// Initialize combat state
|
||||||
|
this.state.combat = {
|
||||||
|
isActive: true,
|
||||||
|
enemyId: enemyId,
|
||||||
|
enemyHp: enemyConfig.stats.hp,
|
||||||
|
enemyMaxHp: enemyConfig.stats.hp,
|
||||||
|
round: 0,
|
||||||
|
logs: [`遭遇 ${enemyConfig.name}!戰鬥開始!`],
|
||||||
|
rewards: { exp: 0, items: [], gold: 0 } // Track rewards
|
||||||
|
};
|
||||||
|
|
||||||
|
return { success: true, combatState: this.state.combat };
|
||||||
|
}
|
||||||
|
|
||||||
|
combatRound() {
|
||||||
|
if (!this.state.combat || !this.state.combat.isActive) return null;
|
||||||
|
|
||||||
|
const enemy = ENEMIES[this.state.combat.enemyId];
|
||||||
|
|
||||||
|
// Ensure combat stats are up to date
|
||||||
|
this.calculateCombatStats();
|
||||||
|
|
||||||
|
// Calculate effective stats including buffs and equipment
|
||||||
|
const pStats = {
|
||||||
|
atk: this.state.attack || (this.state.strength * 2),
|
||||||
|
def: this.state.defense || this.state.intelligence,
|
||||||
|
spd: this.state.speed || this.state.agility,
|
||||||
|
hp: this.state.health,
|
||||||
|
luck: this.state.effectiveLuck || this.state.luck || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply temporary buffs (e.g., from food or items)
|
||||||
|
if (this.state.buffs) {
|
||||||
|
if (this.state.buffs.attackBoost) pStats.atk += this.state.buffs.attackBoost;
|
||||||
|
if (this.state.buffs.defenseBoost) pStats.def += this.state.buffs.defenseBoost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eStats = enemy.stats;
|
||||||
|
const logs = [];
|
||||||
|
|
||||||
|
this.state.combat.round++;
|
||||||
|
logs.push(`--- 第 ${this.state.combat.round} 回合 ---`);
|
||||||
|
|
||||||
|
// HP Recovery (Start of Turn)
|
||||||
|
const hpRecovery = (this.state.buffs?.healthRecovery || 0);
|
||||||
|
if (hpRecovery > 0 && this.state.health < 100) {
|
||||||
|
const recovered = Math.min(100 - this.state.health, hpRecovery);
|
||||||
|
this.state.health += recovered;
|
||||||
|
logs.push(`💚 回復了 ${recovered.toFixed(1)} HP`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player Turn - Calculate Attack Count based on Speed
|
||||||
|
let attackCount = 1;
|
||||||
|
if (pStats.spd > eStats.speed) {
|
||||||
|
const speedDiff = pStats.spd - eStats.speed;
|
||||||
|
attackCount += Math.floor(speedDiff / 10);
|
||||||
|
logs.push(`⚡ 速度優勢!你可以攻擊 ${attackCount} 次!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalPlayerDamage = 0;
|
||||||
|
for (let i = 0; i < attackCount; i++) {
|
||||||
|
const hitChance = 0.9 + (pStats.spd - eStats.speed) * 0.01;
|
||||||
|
if (Math.random() < hitChance) {
|
||||||
|
const isCrit = Math.random() < (pStats.luck * 0.01);
|
||||||
|
let dmg = Math.max(1, pStats.atk - eStats.defense);
|
||||||
|
|
||||||
|
// Random Variance (±10%)
|
||||||
|
const variance = 0.9 + Math.random() * 0.2;
|
||||||
|
dmg = Math.floor(dmg * variance);
|
||||||
|
|
||||||
|
if (isCrit) {
|
||||||
|
dmg = Math.floor(dmg * 1.5);
|
||||||
|
logs.push(`💥 會心一擊!造成了 ${dmg} 點傷害!`);
|
||||||
|
} else {
|
||||||
|
logs.push(`⚔️ 你攻擊了 ${enemy.name},造成 ${dmg} 點傷害`);
|
||||||
|
}
|
||||||
|
this.state.combat.enemyHp -= dmg;
|
||||||
|
totalPlayerDamage += dmg;
|
||||||
|
} else {
|
||||||
|
logs.push(`💨 攻擊落空了!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.combat.enemyHp <= 0) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Enemy Death
|
||||||
|
if (this.state.combat.enemyHp <= 0) {
|
||||||
|
this.state.combat.enemyHp = 0;
|
||||||
|
this.state.combat.isActive = false;
|
||||||
|
logs.push(`🏆 你擊敗了 ${enemy.name}!`);
|
||||||
|
|
||||||
|
// Full HP Recovery on Win
|
||||||
|
this.state.health = 100;
|
||||||
|
logs.push(`💖 勝利讓你的 HP 完全恢復了!`);
|
||||||
|
|
||||||
|
// Rewards
|
||||||
|
this.handleCombatRewards(enemy, logs, pStats.luck);
|
||||||
|
|
||||||
|
this.state.combat.logs = [...this.state.combat.logs, ...logs];
|
||||||
|
return { isOver: true, win: true, logs, rewards: this.state.combat.rewards };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enemy Turn
|
||||||
|
logs.push(`${enemy.name} 發動攻擊!`);
|
||||||
|
let enemyDmg = Math.max(1, eStats.attack - pStats.def);
|
||||||
|
|
||||||
|
// Damage Reduction from Buffs
|
||||||
|
if (this.state.buffs?.damageReduction) {
|
||||||
|
enemyDmg = Math.floor(enemyDmg * (1 - this.state.buffs.damageReduction));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.health = Math.max(0, this.state.health - enemyDmg);
|
||||||
|
logs.push(`💔 你受到了 ${enemyDmg} 點傷害!(HP: ${this.state.health.toFixed(1)})`);
|
||||||
|
|
||||||
|
// Check Player Death
|
||||||
|
if (this.state.health <= 0) {
|
||||||
|
this.state.combat.isActive = false;
|
||||||
|
logs.push(`💀 你被 ${enemy.name} 擊敗了...`);
|
||||||
|
this.state.combat.logs = [...this.state.combat.logs, ...logs];
|
||||||
|
return { isOver: true, win: false, logs };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.combat.logs = [...this.state.combat.logs, ...logs];
|
||||||
|
return { isOver: false, logs };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCombatRewards(enemy, logs, luck) {
|
||||||
|
// Exp (based on enemy HP/Stats)
|
||||||
|
const expGain = Math.floor(enemy.stats.hp / 10) + 5;
|
||||||
|
this.state.trainingExp += expGain;
|
||||||
|
this.state.combat.rewards.exp = expGain;
|
||||||
|
logs.push(`✨ 獲得了 ${expGain} 經驗值!`);
|
||||||
|
|
||||||
|
// Drops
|
||||||
|
if (enemy.drops) {
|
||||||
|
enemy.drops.forEach(drop => {
|
||||||
|
// Luck increases drop chance
|
||||||
|
const dropChance = drop.chance * (1 + luck * 0.01);
|
||||||
|
if (Math.random() < dropChance) {
|
||||||
|
this.addItem(drop.itemId, drop.count || 1);
|
||||||
|
this.state.combat.rewards.items.push({ id: drop.itemId, count: drop.count || 1 });
|
||||||
|
logs.push(`🎁 獲得了 ${drop.itemId} x${drop.count || 1}!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to add item (simplified)
|
||||||
|
addItem(itemId, count) {
|
||||||
|
if (!this.state.inventory) this.state.inventory = [];
|
||||||
|
const existing = this.state.inventory.find(i => i.id === itemId);
|
||||||
|
if (existing) {
|
||||||
|
existing.count = (existing.count || 1) + count;
|
||||||
|
} else {
|
||||||
|
// Push a simple item object
|
||||||
|
this.state.inventory.push({ id: itemId, count });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,8 @@ export const ENEMIES = {
|
||||||
// 森林區敵人
|
// 森林區敵人
|
||||||
snake: {
|
snake: {
|
||||||
id: 'snake',
|
id: 'snake',
|
||||||
name: '毒蛇',
|
name: '青竹絲',
|
||||||
description: '潛伏在草叢中的危險掠食者。',
|
description: '台灣常見的毒蛇,潛伏在草叢中。',
|
||||||
stats: {
|
stats: {
|
||||||
hp: 150,
|
hp: 150,
|
||||||
attack: 35,
|
attack: 35,
|
||||||
|
|
@ -75,23 +75,36 @@ export const ENEMIES = {
|
||||||
speed: 30
|
speed: 30
|
||||||
},
|
},
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'vitality_potion', chance: 0.2, count: 1 },
|
{ itemId: 'vitality_potion', chance: 0.2, count: 1 }
|
||||||
{ itemId: 'magic_wand', chance: 0.05, count: 1 }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
bear: {
|
formosan_bear: {
|
||||||
id: 'bear',
|
id: 'formosan_bear',
|
||||||
name: '暴躁的黑熊',
|
name: '台灣黑熊',
|
||||||
description: '森林中的霸主,力量驚人。',
|
description: '胸前有V字白毛的強壯黑熊,是森林的守護者。',
|
||||||
stats: {
|
stats: {
|
||||||
hp: 300,
|
hp: 500,
|
||||||
attack: 50,
|
attack: 60,
|
||||||
defense: 30,
|
defense: 40,
|
||||||
speed: 10
|
speed: 15
|
||||||
},
|
},
|
||||||
drops: [
|
drops: [
|
||||||
{ itemId: 'gold_coin', chance: 0.5, count: 10 }, // 假設有金幣
|
{ itemId: 'gold_coin', chance: 0.5, count: 50 },
|
||||||
{ itemId: 'hero_sword', chance: 0.02, count: 1 }
|
{ itemId: 'hero_sword', chance: 0.05, count: 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
bad_spirit: {
|
||||||
|
id: 'bad_spirit',
|
||||||
|
name: '遊蕩的惡靈',
|
||||||
|
description: '在寺廟周圍徘徊的負面能量集合體。',
|
||||||
|
stats: {
|
||||||
|
hp: 200,
|
||||||
|
attack: 40,
|
||||||
|
defense: 5,
|
||||||
|
speed: 25
|
||||||
|
},
|
||||||
|
drops: [
|
||||||
|
{ itemId: 'lucky_charm', chance: 0.1, count: 1 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
export const LOCATIONS = [
|
||||||
|
{
|
||||||
|
id: 'taipei_101',
|
||||||
|
name: '台北 101',
|
||||||
|
description: '繁華的信義區,高樓林立,充滿了現代氣息。',
|
||||||
|
backgroundImage: 'https://images.unsplash.com/photo-1596386461350-326256609fe1?q=80&w=800&auto=format&fit=crop',
|
||||||
|
costHunger: 0,
|
||||||
|
costGold: 0,
|
||||||
|
reqStats: null,
|
||||||
|
enemyPool: ['cockroach', 'stray_dog'],
|
||||||
|
enemyRate: 0.3,
|
||||||
|
events: [
|
||||||
|
{ type: 'text', text: '你在 101 前面看到很多觀光客拍照。' },
|
||||||
|
{ type: 'item', itemId: 'bubble_tea', chance: 0.1, text: '你撿到了一杯沒喝完的珍珠奶茶(還是別喝了吧...)' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'night_market',
|
||||||
|
name: '饒河夜市',
|
||||||
|
description: '熱鬧非凡的夜市,空氣中飄散著臭豆腐和藥燉排骨的香氣。',
|
||||||
|
backgroundImage: 'https://images.unsplash.com/photo-1552423316-684441402375?q=80&w=800&auto=format&fit=crop',
|
||||||
|
costHunger: 0,
|
||||||
|
costGold: 0,
|
||||||
|
reqStats: null,
|
||||||
|
enemyPool: ['mouse', 'cockroach', 'stray_dog'],
|
||||||
|
enemyRate: 0.5,
|
||||||
|
events: [
|
||||||
|
{ type: 'text', text: '老闆熱情地招呼你:「帥哥/美女,來坐喔!」' },
|
||||||
|
{ type: 'buff', buffId: 'full', duration: 300, text: '你試吃了一口胡椒餅,感覺充滿了力量!' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'longshan_temple',
|
||||||
|
name: '龍山寺',
|
||||||
|
description: '香火鼎盛的古老寺廟,許多信徒在此虔誠祈禱。',
|
||||||
|
backgroundImage: 'https://images.unsplash.com/photo-1599926676326-877a54913416?q=80&w=800&auto=format&fit=crop',
|
||||||
|
costHunger: 10,
|
||||||
|
costGold: 0,
|
||||||
|
reqStats: null,
|
||||||
|
enemyPool: ['bad_spirit'],
|
||||||
|
enemyRate: 0.4,
|
||||||
|
events: [
|
||||||
|
{ type: 'text', text: '你聽到誦經聲,心靈感到平靜。' },
|
||||||
|
{ type: 'item', itemId: 'incense', chance: 0.2, text: '你獲得了一柱清香。' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alishan',
|
||||||
|
name: '阿里山',
|
||||||
|
description: '雲霧繚繞的高山,擁有神木和日出美景。',
|
||||||
|
backgroundImage: 'https://images.unsplash.com/photo-1512453979798-5ea9ba6a80f4?q=80&w=800&auto=format&fit=crop',
|
||||||
|
costHunger: 20,
|
||||||
|
costGold: 10,
|
||||||
|
reqStats: { str: 50, int: 30 },
|
||||||
|
enemyPool: ['formosan_bear', 'snake'],
|
||||||
|
enemyRate: 0.6,
|
||||||
|
events: [
|
||||||
|
{ type: 'text', text: '你看到了壯觀的雲海。' },
|
||||||
|
{ type: 'text', text: '一隻獼猴搶走了你的香蕉!' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
Loading…
Reference in New Issue