1. 接任务:走到 NPC(左下区域)附近,按 E 接取“平原讨伐令”。

2. 做任务:探索遇敌并战斗,胜利后自动累计击败数。
  3. 交任务:击败达到目标后,回 NPC 按 E 交付并领取奖励(药草 + MP)。
This commit is contained in:
zwt13703 2026-04-11 07:58:39 +08:00
parent c2a3283c54
commit affc6ed059
6 changed files with 117 additions and 3 deletions

View File

@ -75,3 +75,14 @@
3. 新增 TransitionOverlay 过渡组件并接入 App完成探索-切场-战斗流程联动与构建验证。
- **执行结果**: 已完成数据驱动战斗基础与切场过渡表现,后续可直接扩充敌人/技能配置并接任务或场景系统。
# 任务执行摘要
## 会话 ID: 1775865108
- [2026-04-11 07:51:48]
- **执行原因**: 用户继续要求推进,目标增加 NPC 交互与任务链闭环。
- **执行过程**:
1. 扩展输入系统,增加按键单次触发能力(用于 E 键交互)。
2. 在全局状态中加入任务模块,实现接取、战斗击杀计数、任务完成与奖励交付逻辑。
3. 在探索场景添加 NPC 模型与互动提示,接入 E 键对话,并更新状态面板展示任务进度。
- **执行结果**: 已形成第一条任务链闭环(接任务 -> 打怪累积 -> 回 NPC 交付),并通过构建验证。

View File

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

View File

@ -1,4 +1,4 @@
export const gameConfig = {
title: 'Breath of Fire-like RPG Demo',
tagline: '里程碑 3: 数据驱动战斗 + 切场过渡',
tagline: '里程碑 4: NPC 任务链(接取/击败/交付)',
}

View File

@ -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)})`

View File

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

View File

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