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 @@ + + + + +