1. 数据驱动战斗配置:敌人和技能参数从逻辑代码拆到配置文件。
2. 场景状态机:新增 sceneMode(explore/battleTransition/battle)。 3. 战斗切入过渡:遇敌先进入短过场,再切到战斗界面。 4. 探索锁定规则升级:只在 explore 状态允许移动。
This commit is contained in:
parent
f2da55321f
commit
c2a3283c54
|
|
@ -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'
|
||||
</script>
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ import { gameConfig } from './data/gameConfig'
|
|||
<StatusPanel />
|
||||
<ThreeScene />
|
||||
</main>
|
||||
<TransitionOverlay />
|
||||
<BattlePanel />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export const gameConfig = {
|
||||
title: 'Breath of Fire-like RPG Demo',
|
||||
tagline: '里程碑 2: 探索遭遇 + 回合制战斗 MVP',
|
||||
tagline: '里程碑 3: 数据驱动战斗 + 切场过渡',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GameState>({
|
|||
moveSpeed: 3.2,
|
||||
encounterMeter: 0,
|
||||
notice: '按 WASD 或方向键移动',
|
||||
sceneMode: 'explore',
|
||||
player: {
|
||||
hp: 120,
|
||||
maxHp: 120,
|
||||
|
|
@ -63,10 +68,12 @@ const state = reactive<GameState>({
|
|||
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 = <T>(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 = '战斗结束,继续探索。'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ const stats = useGameState()
|
|||
<li>坐标: {{ stats.positionText }}</li>
|
||||
<li>移速: {{ stats.moveSpeed.toFixed(1) }}</li>
|
||||
<li>遇敌计量: {{ stats.encounterMeter.toFixed(0) }}</li>
|
||||
<li>战斗状态: {{ stats.battle.active ? '战斗中' : '探索中' }}</li>
|
||||
<li>场景状态: {{ stats.sceneMode }}</li>
|
||||
<li>战斗状态: {{ stats.battle.active ? '战斗中' : '未战斗' }}</li>
|
||||
</ul>
|
||||
<p class="notice">{{ stats.notice }}</p>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
<script setup lang="ts">
|
||||
import { useGameState } from '../game/state'
|
||||
|
||||
const gameState = useGameState()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="gameState.sceneMode === 'battleTransition'" class="overlay">
|
||||
<div class="flash" />
|
||||
<p>Encounter...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 45;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.15), rgba(8, 12, 16, 0.95));
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.flash {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(
|
||||
-30deg,
|
||||
rgba(255, 255, 255, 0.06) 0px,
|
||||
rgba(255, 255, 255, 0.06) 6px,
|
||||
transparent 6px,
|
||||
transparent 18px
|
||||
);
|
||||
animation: stripeMove 0.7s linear infinite;
|
||||
}
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
letter-spacing: 1px;
|
||||
color: #e6eef4;
|
||||
text-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
@keyframes stripeMove {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue