diff --git a/docs/tasks/task_detail_2026_04_11.md b/docs/tasks/task_detail_2026_04_11.md index a02c3cf..3775446 100644 --- a/docs/tasks/task_detail_2026_04_11.md +++ b/docs/tasks/task_detail_2026_04_11.md @@ -53,3 +53,14 @@ 3. 更新状态面板为实时数据显示,并执行构建验证确保可编译运行。 - **执行结果**: 已完成探索层 MVP,玩家可在 3D 场景中移动并获得基础 RPG 反馈,项目继续可在此基础上对接战斗系统。 +# 任务执行摘要 + +## 会话 ID: 1775840072 +- [2026-04-11 00:54:32] +- **执行原因**: 用户继续要求推进开发,目标是从探索占位过渡到可执行的战斗流程。 +- **执行过程**: + 1. 扩展全局游戏状态,加入战斗状态机、行动指令与敌方回合逻辑。 + 2. 新增战斗面板组件,提供攻击/技能/防御/道具操作与战斗日志展示。 + 3. 将场景遇敌逻辑接入战斗入口,并在战斗期间锁定探索移动;完成构建验证。 +- **执行结果**: 已形成“探索移动 -> 遇敌触发 -> 回合战斗 -> 结算返回探索”的可玩闭环 MVP。 + diff --git a/src/App.vue b/src/App.vue index 1fd7984..b19e39c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,6 @@ @@ -15,5 +16,6 @@ import { gameConfig } from './data/gameConfig' + diff --git a/src/data/gameConfig.ts b/src/data/gameConfig.ts index e3bf8e8..d18237e 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: '里程碑 1: 可移动探索 + 碰撞 + 遇敌占位', + tagline: '里程碑 2: 探索遭遇 + 回合制战斗 MVP', } diff --git a/src/game/ThreeScene.vue b/src/game/ThreeScene.vue index 1d5fb8b..0a25641 100644 --- a/src/game/ThreeScene.vue +++ b/src/game/ThreeScene.vue @@ -4,7 +4,7 @@ import * as THREE from 'three' import type { Mesh, Object3D } from 'three' import { GameLoop } from '../core/loop' import { KeyboardInput } from '../core/input' -import { useGameState } from './state' +import { startBattle, useGameState } from './state' const root = ref(null) const gameState = useGameState() @@ -131,6 +131,13 @@ const initScene = () => { const cameraOffset = new THREE.Vector3(4.5, 5.2, 6.2) loop = new GameLoop((dt) => { + if (gameState.battle.active) { + if (renderer && scene && camera) { + renderer.render(scene, camera) + } + return + } + const axis = keyboard?.getMoveAxis() ?? { x: 0, z: 0 } const hasMoveInput = axis.x !== 0 || axis.z !== 0 @@ -152,7 +159,7 @@ const initScene = () => { if (gameState.encounterMeter >= encounterTarget) { gameState.encounterMeter = 0 encounterTarget = 35 + Math.random() * 30 - gameState.notice = '触发遇敌(占位):可切入战斗场景' + startBattle('你在平原中前进') } } else { gameState.notice = '前方有障碍,无法通过' diff --git a/src/game/state.ts b/src/game/state.ts index 37e7911..3091e8a 100644 --- a/src/game/state.ts +++ b/src/game/state.ts @@ -1,6 +1,43 @@ import { reactive } from 'vue' -const state = reactive({ +export type BattleAction = 'attack' | 'skill' | 'defend' | 'item' + +type BattleTurn = 'player' | 'enemy' | 'resolved' + +interface BattleState { + active: boolean + turn: BattleTurn + enemyName: string + enemyHp: number + enemyMaxHp: number + guarding: boolean + victory: boolean | null + message: string + log: string[] +} + +interface PlayerState { + hp: number + maxHp: number + mp: number + maxMp: number + potions: number +} + +interface GameState { + hp: string + mp: string + level: number + area: string + positionText: string + moveSpeed: number + encounterMeter: number + notice: string + player: PlayerState + battle: BattleState +} + +const state = reactive({ hp: '120 / 120', mp: '38 / 38', level: 1, @@ -9,6 +46,173 @@ const state = reactive({ moveSpeed: 3.2, encounterMeter: 0, notice: '按 WASD 或方向键移动', + player: { + hp: 120, + maxHp: 120, + mp: 38, + maxMp: 38, + potions: 3, + }, + battle: { + active: false, + turn: 'player', + enemyName: '', + enemyHp: 0, + enemyMaxHp: 0, + guarding: false, + victory: null, + message: '', + log: [], + }, }) +let enemyActTimer = 0 + +const addBattleLog = (line: string) => { + state.battle.log.push(line) + if (state.battle.log.length > 8) { + state.battle.log.shift() + } +} + +const roll = (min: number, max: number) => { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +const updateDerivedBars = () => { + state.hp = `${Math.max(0, state.player.hp)} / ${state.player.maxHp}` + state.mp = `${Math.max(0, state.player.mp)} / ${state.player.maxMp}` +} + +const resolveBattle = (victory: boolean) => { + state.battle.turn = 'resolved' + state.battle.victory = victory + + if (victory) { + const exp = roll(12, 20) + state.notice = `战斗胜利,获得 ${exp} 经验(占位)` + state.battle.message = '胜利。点击“返回探索”继续。' + addBattleLog(`你击败了 ${state.battle.enemyName}。`) + } else { + state.notice = '战斗失败(占位):已自动复活到 60 HP' + state.battle.message = '失败。点击“返回探索”继续。' + addBattleLog('你被击倒了。') + state.player.hp = 60 + state.player.mp = Math.max(state.player.mp, 10) + updateDerivedBars() + } + + clearTimeout(enemyActTimer) +} + +const enemyTurn = () => { + if (!state.battle.active || state.battle.turn !== 'enemy') return + + const enemyDamage = roll(10, 18) + const damage = state.battle.guarding ? Math.floor(enemyDamage * 0.55) : enemyDamage + state.player.hp -= damage + state.battle.guarding = false + updateDerivedBars() + + addBattleLog(`${state.battle.enemyName} 攻击造成 ${damage} 点伤害。`) + + if (state.player.hp <= 0) { + resolveBattle(false) + return + } + + state.battle.turn = 'player' + state.battle.message = '你的回合:选择一个指令。' +} + +export const startBattle = (reason = '遭遇野怪') => { + if (state.battle.active) return + + const enemies = [ + { name: '平原狼', hp: 65 }, + { name: '洞穴史莱姆', hp: 72 }, + { name: '山地蜥蜴', hp: 78 }, + ] + + 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}` +} + +export const actInBattle = (action: BattleAction) => { + if (!state.battle.active || state.battle.turn !== 'player') return + + let damage = 0 + + if (action === 'attack') { + damage = roll(14, 22) + state.battle.enemyHp -= damage + addBattleLog(`你使用普通攻击,造成 ${damage} 点伤害。`) + } + + if (action === 'skill') { + const cost = 6 + if (state.player.mp < cost) { + addBattleLog('MP 不足,技能释放失败。') + state.battle.message = 'MP 不足,请重新选择。' + return + } + + state.player.mp -= cost + damage = roll(24, 34) + state.battle.enemyHp -= damage + addBattleLog(`你使用火焰术,造成 ${damage} 点伤害。`) + updateDerivedBars() + } + + if (action === 'defend') { + state.battle.guarding = true + addBattleLog('你进入防御姿态,下一次受伤降低。') + } + + if (action === 'item') { + if (state.player.potions <= 0) { + addBattleLog('没有可用药草。') + state.battle.message = '道具不足,请重新选择。' + return + } + + state.player.potions -= 1 + const heal = 30 + state.player.hp = Math.min(state.player.maxHp, state.player.hp + heal) + addBattleLog(`你使用药草,恢复 ${heal} HP。`) + updateDerivedBars() + } + + if (state.battle.enemyHp <= 0) { + state.battle.enemyHp = 0 + resolveBattle(true) + return + } + + state.battle.turn = 'enemy' + state.battle.message = '敌方行动中...' + enemyActTimer = window.setTimeout(enemyTurn, 500) +} + +export const closeBattle = () => { + if (!state.battle.active || state.battle.turn !== 'resolved') return + + state.battle.active = false + state.battle.message = '' + state.battle.log = [] + state.battle.enemyName = '' + state.notice = '战斗结束,继续探索。' +} + export const useGameState = () => state diff --git a/src/ui/BattlePanel.vue b/src/ui/BattlePanel.vue new file mode 100644 index 0000000..184c511 --- /dev/null +++ b/src/ui/BattlePanel.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/src/ui/StatusPanel.vue b/src/ui/StatusPanel.vue index b72c87f..78a3c19 100644 --- a/src/ui/StatusPanel.vue +++ b/src/ui/StatusPanel.vue @@ -11,10 +11,12 @@ const stats = useGameState()
  • Lv. {{ stats.level }}
  • HP: {{ stats.hp }}
  • MP: {{ stats.mp }}
  • +
  • 药草: {{ stats.player.potions }}
  • 区域: {{ stats.area }}
  • 坐标: {{ stats.positionText }}
  • 移速: {{ stats.moveSpeed.toFixed(1) }}
  • 遇敌计量: {{ stats.encounterMeter.toFixed(0) }}
  • +
  • 战斗状态: {{ stats.battle.active ? '战斗中' : '探索中' }}
  • {{ stats.notice }}