diff --git a/src/App.vue b/src/App.vue
index b19e39c..a7048d6 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -2,6 +2,7 @@
import ThreeScene from './game/ThreeScene.vue'
import BattlePanel from './ui/BattlePanel.vue'
import StatusPanel from './ui/StatusPanel.vue'
+import TransitionOverlay from './ui/TransitionOverlay.vue'
import { gameConfig } from './data/gameConfig'
@@ -16,6 +17,7 @@ import { gameConfig } from './data/gameConfig'
+
diff --git a/src/data/battleData.ts b/src/data/battleData.ts
new file mode 100644
index 0000000..a0c10eb
--- /dev/null
+++ b/src/data/battleData.ts
@@ -0,0 +1,53 @@
+export interface EnemyDef {
+ id: string
+ name: string
+ hp: number
+ minAtk: number
+ maxAtk: number
+ rewardExp: [number, number]
+}
+
+export interface SkillDef {
+ id: string
+ name: string
+ mpCost: number
+ minDamage: number
+ maxDamage: number
+}
+
+export const enemies: EnemyDef[] = [
+ {
+ id: 'plain-wolf',
+ name: '平原狼',
+ hp: 65,
+ minAtk: 10,
+ maxAtk: 18,
+ rewardExp: [12, 20],
+ },
+ {
+ id: 'cave-slime',
+ name: '洞穴史莱姆',
+ hp: 72,
+ minAtk: 9,
+ maxAtk: 16,
+ rewardExp: [14, 22],
+ },
+ {
+ id: 'mountain-lizard',
+ name: '山地蜥蜴',
+ hp: 78,
+ minAtk: 11,
+ maxAtk: 19,
+ rewardExp: [16, 24],
+ },
+]
+
+export const skills: SkillDef[] = [
+ {
+ id: 'flame',
+ name: '火焰术',
+ mpCost: 6,
+ minDamage: 24,
+ maxDamage: 34,
+ },
+]
diff --git a/src/data/gameConfig.ts b/src/data/gameConfig.ts
index d18237e..ce05db3 100644
--- a/src/data/gameConfig.ts
+++ b/src/data/gameConfig.ts
@@ -1,4 +1,4 @@
export const gameConfig = {
title: 'Breath of Fire-like RPG Demo',
- tagline: '里程碑 2: 探索遭遇 + 回合制战斗 MVP',
+ tagline: '里程碑 3: 数据驱动战斗 + 切场过渡',
}
diff --git a/src/game/ThreeScene.vue b/src/game/ThreeScene.vue
index 0a25641..3cde608 100644
--- a/src/game/ThreeScene.vue
+++ b/src/game/ThreeScene.vue
@@ -131,7 +131,7 @@ const initScene = () => {
const cameraOffset = new THREE.Vector3(4.5, 5.2, 6.2)
loop = new GameLoop((dt) => {
- if (gameState.battle.active) {
+ if (gameState.sceneMode !== 'explore') {
if (renderer && scene && camera) {
renderer.render(scene, camera)
}
diff --git a/src/game/state.ts b/src/game/state.ts
index 3091e8a..de09b83 100644
--- a/src/game/state.ts
+++ b/src/game/state.ts
@@ -1,8 +1,10 @@
import { reactive } from 'vue'
+import { enemies, skills, type EnemyDef } from '../data/battleData'
export type BattleAction = 'attack' | 'skill' | 'defend' | 'item'
type BattleTurn = 'player' | 'enemy' | 'resolved'
+type SceneMode = 'explore' | 'battleTransition' | 'battle'
interface BattleState {
active: boolean
@@ -14,6 +16,7 @@ interface BattleState {
victory: boolean | null
message: string
log: string[]
+ enemyData: EnemyDef | null
}
interface PlayerState {
@@ -33,6 +36,7 @@ interface GameState {
moveSpeed: number
encounterMeter: number
notice: string
+ sceneMode: SceneMode
player: PlayerState
battle: BattleState
}
@@ -46,6 +50,7 @@ const state = reactive({
moveSpeed: 3.2,
encounterMeter: 0,
notice: '按 WASD 或方向键移动',
+ sceneMode: 'explore',
player: {
hp: 120,
maxHp: 120,
@@ -63,10 +68,12 @@ const state = reactive({
victory: null,
message: '',
log: [],
+ enemyData: null,
},
})
let enemyActTimer = 0
+let battleEnterTimer = 0
const addBattleLog = (line: string) => {
state.battle.log.push(line)
@@ -79,6 +86,8 @@ const roll = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1)) + min
}
+const pickRandom = (arr: T[]) => arr[roll(0, arr.length - 1)]
+
const updateDerivedBars = () => {
state.hp = `${Math.max(0, state.player.hp)} / ${state.player.maxHp}`
state.mp = `${Math.max(0, state.player.mp)} / ${state.player.maxMp}`
@@ -89,7 +98,8 @@ const resolveBattle = (victory: boolean) => {
state.battle.victory = victory
if (victory) {
- const exp = roll(12, 20)
+ const rewardRange = state.battle.enemyData?.rewardExp ?? [12, 20]
+ const exp = roll(rewardRange[0], rewardRange[1])
state.notice = `战斗胜利,获得 ${exp} 经验(占位)`
state.battle.message = '胜利。点击“返回探索”继续。'
addBattleLog(`你击败了 ${state.battle.enemyName}。`)
@@ -108,7 +118,9 @@ const resolveBattle = (victory: boolean) => {
const enemyTurn = () => {
if (!state.battle.active || state.battle.turn !== 'enemy') return
- const enemyDamage = roll(10, 18)
+ const minAtk = state.battle.enemyData?.minAtk ?? 10
+ const maxAtk = state.battle.enemyData?.maxAtk ?? 18
+ const enemyDamage = roll(minAtk, maxAtk)
const damage = state.battle.guarding ? Math.floor(enemyDamage * 0.55) : enemyDamage
state.player.hp -= damage
state.battle.guarding = false
@@ -126,27 +138,28 @@ const enemyTurn = () => {
}
export const startBattle = (reason = '遭遇野怪') => {
- if (state.battle.active) return
+ if (state.battle.active || state.sceneMode === 'battleTransition') return
- const enemies = [
- { name: '平原狼', hp: 65 },
- { name: '洞穴史莱姆', hp: 72 },
- { name: '山地蜥蜴', hp: 78 },
- ]
+ const enemy = pickRandom(enemies)
+ state.sceneMode = 'battleTransition'
+ state.notice = `遭遇 ${enemy.name},准备进入战斗...`
- const enemy = enemies[roll(0, enemies.length - 1)]
-
- state.battle.active = true
- state.battle.turn = 'player'
- state.battle.enemyName = enemy.name
- state.battle.enemyHp = enemy.hp
- state.battle.enemyMaxHp = enemy.hp
- state.battle.guarding = false
- state.battle.victory = null
- state.battle.message = '你的回合:选择一个指令。'
- state.battle.log = [`${reason},出现了 ${enemy.name}!`]
- state.encounterMeter = 0
- state.notice = `进入战斗:${enemy.name}`
+ clearTimeout(battleEnterTimer)
+ battleEnterTimer = window.setTimeout(() => {
+ state.battle.active = true
+ state.sceneMode = 'battle'
+ state.battle.turn = 'player'
+ state.battle.enemyName = enemy.name
+ state.battle.enemyHp = enemy.hp
+ state.battle.enemyMaxHp = enemy.hp
+ state.battle.guarding = false
+ state.battle.victory = null
+ state.battle.message = '你的回合:选择一个指令。'
+ state.battle.log = [`${reason},出现了 ${enemy.name}!`]
+ state.battle.enemyData = enemy
+ state.encounterMeter = 0
+ state.notice = `进入战斗:${enemy.name}`
+ }, 450)
}
export const actInBattle = (action: BattleAction) => {
@@ -161,17 +174,19 @@ export const actInBattle = (action: BattleAction) => {
}
if (action === 'skill') {
- const cost = 6
- if (state.player.mp < cost) {
+ const skill = skills[0]
+ if (!skill) return
+
+ if (state.player.mp < skill.mpCost) {
addBattleLog('MP 不足,技能释放失败。')
state.battle.message = 'MP 不足,请重新选择。'
return
}
- state.player.mp -= cost
- damage = roll(24, 34)
+ state.player.mp -= skill.mpCost
+ damage = roll(skill.minDamage, skill.maxDamage)
state.battle.enemyHp -= damage
- addBattleLog(`你使用火焰术,造成 ${damage} 点伤害。`)
+ addBattleLog(`你使用${skill.name},造成 ${damage} 点伤害。`)
updateDerivedBars()
}
@@ -212,6 +227,8 @@ export const closeBattle = () => {
state.battle.message = ''
state.battle.log = []
state.battle.enemyName = ''
+ state.battle.enemyData = null
+ state.sceneMode = 'explore'
state.notice = '战斗结束,继续探索。'
}
diff --git a/src/ui/StatusPanel.vue b/src/ui/StatusPanel.vue
index 78a3c19..a8b9a73 100644
--- a/src/ui/StatusPanel.vue
+++ b/src/ui/StatusPanel.vue
@@ -16,7 +16,8 @@ const stats = useGameState()
坐标: {{ stats.positionText }}
移速: {{ stats.moveSpeed.toFixed(1) }}
遇敌计量: {{ stats.encounterMeter.toFixed(0) }}
- 战斗状态: {{ stats.battle.active ? '战斗中' : '探索中' }}
+ 场景状态: {{ stats.sceneMode }}
+ 战斗状态: {{ stats.battle.active ? '战斗中' : '未战斗' }}
{{ stats.notice }}
diff --git a/src/ui/TransitionOverlay.vue b/src/ui/TransitionOverlay.vue
new file mode 100644
index 0000000..29e8580
--- /dev/null
+++ b/src/ui/TransitionOverlay.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+