1. 接任务:走到 NPC(左下区域)附近,按 E 接取“平原讨伐令”。
2. 做任务:探索遇敌并战斗,胜利后自动累计击败数。 3. 交任务:击败达到目标后,回 NPC 按 E 交付并领取奖励(药草 + MP)。
This commit is contained in:
parent
c2a3283c54
commit
affc6ed059
|
|
@ -75,3 +75,14 @@
|
|||
3. 新增 TransitionOverlay 过渡组件并接入 App,完成探索-切场-战斗流程联动与构建验证。
|
||||
- **执行结果**: 已完成数据驱动战斗基础与切场过渡表现,后续可直接扩充敌人/技能配置并接任务或场景系统。
|
||||
|
||||
# 任务执行摘要
|
||||
|
||||
## 会话 ID: 1775865108
|
||||
- [2026-04-11 07:51:48]
|
||||
- **执行原因**: 用户继续要求推进,目标增加 NPC 交互与任务链闭环。
|
||||
- **执行过程**:
|
||||
1. 扩展输入系统,增加按键单次触发能力(用于 E 键交互)。
|
||||
2. 在全局状态中加入任务模块,实现接取、战斗击杀计数、任务完成与奖励交付逻辑。
|
||||
3. 在探索场景添加 NPC 模型与互动提示,接入 E 键对话,并更新状态面板展示任务进度。
|
||||
- **执行结果**: 已形成第一条任务链闭环(接任务 -> 打怪累积 -> 回 NPC 交付),并通过构建验证。
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
export class KeyboardInput {
|
||||
private readonly pressed = new Set<string>()
|
||||
private readonly justPressed = new Set<string>()
|
||||
|
||||
private readonly onKeyDown = (event: KeyboardEvent) => {
|
||||
if (!this.pressed.has(event.code)) {
|
||||
this.justPressed.add(event.code)
|
||||
}
|
||||
this.pressed.add(event.code)
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +22,7 @@ export class KeyboardInput {
|
|||
window.removeEventListener('keydown', this.onKeyDown)
|
||||
window.removeEventListener('keyup', this.onKeyUp)
|
||||
this.pressed.clear()
|
||||
this.justPressed.clear()
|
||||
}
|
||||
|
||||
getMoveAxis() {
|
||||
|
|
@ -31,4 +36,12 @@ export class KeyboardInput {
|
|||
|
||||
return { x, z }
|
||||
}
|
||||
|
||||
consumePressed(code: string) {
|
||||
const hit = this.justPressed.has(code)
|
||||
if (hit) {
|
||||
this.justPressed.delete(code)
|
||||
}
|
||||
return hit
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const gameConfig = {
|
||||
title: 'Breath of Fire-like RPG Demo',
|
||||
tagline: '里程碑 3: 数据驱动战斗 + 切场过渡',
|
||||
tagline: '里程碑 4: NPC 任务链(接取/击败/交付)',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { startBattle, useGameState } from './state'
|
||||
import { interactQuestNpc, startBattle, useGameState } from './state'
|
||||
|
||||
const root = ref<HTMLDivElement | null>(null)
|
||||
const gameState = useGameState()
|
||||
|
|
@ -100,6 +100,20 @@ const initScene = () => {
|
|||
portal.rotation.x = Math.PI / 2
|
||||
scene.add(portal)
|
||||
|
||||
const npc = new THREE.Mesh(
|
||||
new THREE.CapsuleGeometry(0.35, 0.8, 4, 8),
|
||||
new THREE.MeshStandardMaterial({ color: '#f4b45b' }),
|
||||
)
|
||||
npc.position.set(-5.2, 0.8, -4.8)
|
||||
scene.add(npc)
|
||||
|
||||
const npcMark = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.18, 16, 16),
|
||||
new THREE.MeshStandardMaterial({ color: '#ffe071', emissive: '#6c5617', emissiveIntensity: 0.7 }),
|
||||
)
|
||||
npcMark.position.set(-5.2, 1.95, -4.8)
|
||||
scene.add(npcMark)
|
||||
|
||||
for (const obstacle of obstacleDefs) {
|
||||
const stone = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(obstacle.radius, obstacle.radius, 1.2, 16),
|
||||
|
|
@ -169,11 +183,20 @@ const initScene = () => {
|
|||
}
|
||||
|
||||
portal.rotation.z += dt
|
||||
npcMark.position.y = 1.95 + Math.sin(performance.now() * 0.004) * 0.12
|
||||
|
||||
if (hero.position.distanceTo(portal.position) < 1.1) {
|
||||
gameState.notice = '进入传送点(占位):可切换场景'
|
||||
}
|
||||
|
||||
const npcDistance = hero.position.distanceTo(npc.position)
|
||||
if (npcDistance < 1.5) {
|
||||
gameState.notice = '按 E 与 NPC 对话'
|
||||
if (keyboard?.consumePressed('KeyE')) {
|
||||
interactQuestNpc()
|
||||
}
|
||||
}
|
||||
|
||||
updateArea(hero.position.x)
|
||||
gameState.positionText = `(${hero.position.x.toFixed(1)}, ${hero.position.z.toFixed(1)})`
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,18 @@ 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'
|
||||
export type SceneMode = 'explore' | 'battleTransition' | 'battle'
|
||||
|
||||
type QuestStatus = 'not_started' | 'in_progress' | 'completed' | 'turned_in'
|
||||
|
||||
interface QuestState {
|
||||
id: string
|
||||
title: string
|
||||
status: QuestStatus
|
||||
targetKills: number
|
||||
currentKills: number
|
||||
rewardPotions: number
|
||||
}
|
||||
|
||||
interface BattleState {
|
||||
active: boolean
|
||||
|
|
@ -39,6 +50,7 @@ interface GameState {
|
|||
sceneMode: SceneMode
|
||||
player: PlayerState
|
||||
battle: BattleState
|
||||
quest: QuestState
|
||||
}
|
||||
|
||||
const state = reactive<GameState>({
|
||||
|
|
@ -70,6 +82,14 @@ const state = reactive<GameState>({
|
|||
log: [],
|
||||
enemyData: null,
|
||||
},
|
||||
quest: {
|
||||
id: 'wolf-subjugation',
|
||||
title: '平原讨伐令',
|
||||
status: 'not_started',
|
||||
targetKills: 3,
|
||||
currentKills: 0,
|
||||
rewardPotions: 2,
|
||||
},
|
||||
})
|
||||
|
||||
let enemyActTimer = 0
|
||||
|
|
@ -93,6 +113,20 @@ const updateDerivedBars = () => {
|
|||
state.mp = `${Math.max(0, state.player.mp)} / ${state.player.maxMp}`
|
||||
}
|
||||
|
||||
const updateQuestProgressOnVictory = () => {
|
||||
if (state.quest.status !== 'in_progress') {
|
||||
return
|
||||
}
|
||||
|
||||
state.quest.currentKills += 1
|
||||
if (state.quest.currentKills >= state.quest.targetKills) {
|
||||
state.quest.status = 'completed'
|
||||
state.notice = `任务完成:请回 NPC 领取奖励(+${state.quest.rewardPotions} 药草)`
|
||||
} else {
|
||||
state.notice = `任务进度:${state.quest.currentKills}/${state.quest.targetKills}`
|
||||
}
|
||||
}
|
||||
|
||||
const resolveBattle = (victory: boolean) => {
|
||||
state.battle.turn = 'resolved'
|
||||
state.battle.victory = victory
|
||||
|
|
@ -103,6 +137,7 @@ const resolveBattle = (victory: boolean) => {
|
|||
state.notice = `战斗胜利,获得 ${exp} 经验(占位)`
|
||||
state.battle.message = '胜利。点击“返回探索”继续。'
|
||||
addBattleLog(`你击败了 ${state.battle.enemyName}。`)
|
||||
updateQuestProgressOnVictory()
|
||||
} else {
|
||||
state.notice = '战斗失败(占位):已自动复活到 60 HP'
|
||||
state.battle.message = '失败。点击“返回探索”继续。'
|
||||
|
|
@ -137,6 +172,35 @@ const enemyTurn = () => {
|
|||
state.battle.message = '你的回合:选择一个指令。'
|
||||
}
|
||||
|
||||
export const interactQuestNpc = () => {
|
||||
if (state.sceneMode !== 'explore') {
|
||||
return
|
||||
}
|
||||
|
||||
if (state.quest.status === 'not_started') {
|
||||
state.quest.status = 'in_progress'
|
||||
state.quest.currentKills = 0
|
||||
state.notice = `接取任务:${state.quest.title}(击败 ${state.quest.targetKills} 个敌人)`
|
||||
return
|
||||
}
|
||||
|
||||
if (state.quest.status === 'in_progress') {
|
||||
state.notice = `任务进行中:${state.quest.currentKills}/${state.quest.targetKills}`
|
||||
return
|
||||
}
|
||||
|
||||
if (state.quest.status === 'completed') {
|
||||
state.quest.status = 'turned_in'
|
||||
state.player.potions += state.quest.rewardPotions
|
||||
state.player.mp = Math.min(state.player.maxMp, state.player.mp + 8)
|
||||
updateDerivedBars()
|
||||
state.notice = `任务已交付:获得 ${state.quest.rewardPotions} 药草,MP +8`
|
||||
return
|
||||
}
|
||||
|
||||
state.notice = 'NPC:谢谢你,平原暂时安全了。'
|
||||
}
|
||||
|
||||
export const startBattle = (reason = '遭遇野怪') => {
|
||||
if (state.battle.active || state.sceneMode === 'battleTransition') return
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ const stats = useGameState()
|
|||
<li>遇敌计量: {{ stats.encounterMeter.toFixed(0) }}</li>
|
||||
<li>场景状态: {{ stats.sceneMode }}</li>
|
||||
<li>战斗状态: {{ stats.battle.active ? '战斗中' : '未战斗' }}</li>
|
||||
<li>任务: {{ stats.quest.title }}</li>
|
||||
<li>任务状态: {{ stats.quest.status }}</li>
|
||||
<li>任务进度: {{ stats.quest.currentKills }} / {{ stats.quest.targetKills }}</li>
|
||||
</ul>
|
||||
<p class="notice">{{ stats.notice }}</p>
|
||||
</aside>
|
||||
|
|
|
|||
Loading…
Reference in New Issue