From c2a3283c54c7d5b60cf6e654af430cf0fa14c8d1 Mon Sep 17 00:00:00 2001 From: zwt13703 Date: Sat, 11 Apr 2026 07:46:52 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=95=B0=E6=8D=AE=E9=A9=B1=E5=8A=A8?= =?UTF-8?q?=E6=88=98=E6=96=97=E9=85=8D=E7=BD=AE=EF=BC=9A=E6=95=8C=E4=BA=BA?= =?UTF-8?q?=E5=92=8C=E6=8A=80=E8=83=BD=E5=8F=82=E6=95=B0=E4=BB=8E=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=BB=A3=E7=A0=81=E6=8B=86=E5=88=B0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E3=80=82=20=20=202.=20=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=EF=BC=9A=E6=96=B0=E5=A2=9E=20sceneM?= =?UTF-8?q?ode=EF=BC=88explore/battleTransition/battle=EF=BC=89=E3=80=82?= =?UTF-8?q?=20=20=203.=20=E6=88=98=E6=96=97=E5=88=87=E5=85=A5=E8=BF=87?= =?UTF-8?q?=E6=B8=A1=EF=BC=9A=E9=81=87=E6=95=8C=E5=85=88=E8=BF=9B=E5=85=A5?= =?UTF-8?q?=E7=9F=AD=E8=BF=87=E5=9C=BA=EF=BC=8C=E5=86=8D=E5=88=87=E5=88=B0?= =?UTF-8?q?=E6=88=98=E6=96=97=E7=95=8C=E9=9D=A2=E3=80=82=20=20=204.=20?= =?UTF-8?q?=E6=8E=A2=E7=B4=A2=E9=94=81=E5=AE=9A=E8=A7=84=E5=88=99=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=EF=BC=9A=E5=8F=AA=E5=9C=A8=20explore=20=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=85=81=E8=AE=B8=E7=A7=BB=E5=8A=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 2 ++ src/data/battleData.ts | 53 +++++++++++++++++++++++++++ src/data/gameConfig.ts | 2 +- src/game/ThreeScene.vue | 2 +- src/game/state.ts | 69 ++++++++++++++++++++++-------------- src/ui/StatusPanel.vue | 3 +- src/ui/TransitionOverlay.vue | 64 +++++++++++++++++++++++++++++++++ 7 files changed, 166 insertions(+), 29 deletions(-) create mode 100644 src/data/battleData.ts create mode 100644 src/ui/TransitionOverlay.vue 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 @@ + + + + +