diff --git a/docs/tasks/task_detail_2026_04_11.md b/docs/tasks/task_detail_2026_04_11.md index a564e91..9146d85 100644 --- a/docs/tasks/task_detail_2026_04_11.md +++ b/docs/tasks/task_detail_2026_04_11.md @@ -75,3 +75,14 @@ 3. 新增 TransitionOverlay 过渡组件并接入 App,完成探索-切场-战斗流程联动与构建验证。 - **执行结果**: 已完成数据驱动战斗基础与切场过渡表现,后续可直接扩充敌人/技能配置并接任务或场景系统。 +# 任务执行摘要 + +## 会话 ID: 1775865108 +- [2026-04-11 07:51:48] +- **执行原因**: 用户继续要求推进,目标增加 NPC 交互与任务链闭环。 +- **执行过程**: + 1. 扩展输入系统,增加按键单次触发能力(用于 E 键交互)。 + 2. 在全局状态中加入任务模块,实现接取、战斗击杀计数、任务完成与奖励交付逻辑。 + 3. 在探索场景添加 NPC 模型与互动提示,接入 E 键对话,并更新状态面板展示任务进度。 +- **执行结果**: 已形成第一条任务链闭环(接任务 -> 打怪累积 -> 回 NPC 交付),并通过构建验证。 + diff --git a/src/core/input.ts b/src/core/input.ts index b28dd6e..8466bee 100644 --- a/src/core/input.ts +++ b/src/core/input.ts @@ -1,7 +1,11 @@ export class KeyboardInput { private readonly pressed = new Set() + private readonly justPressed = new Set() 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 + } } diff --git a/src/data/gameConfig.ts b/src/data/gameConfig.ts index ce05db3..f7d5b59 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: '里程碑 3: 数据驱动战斗 + 切场过渡', + tagline: '里程碑 4: NPC 任务链(接取/击败/交付)', } diff --git a/src/game/ThreeScene.vue b/src/game/ThreeScene.vue index 3cde608..e8fd22d 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 { startBattle, useGameState } from './state' +import { interactQuestNpc, startBattle, useGameState } from './state' const root = ref(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)})` diff --git a/src/game/state.ts b/src/game/state.ts index de09b83..c092690 100644 --- a/src/game/state.ts +++ b/src/game/state.ts @@ -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({ @@ -70,6 +82,14 @@ const state = reactive({ 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 diff --git a/src/ui/StatusPanel.vue b/src/ui/StatusPanel.vue index a8b9a73..b99760a 100644 --- a/src/ui/StatusPanel.vue +++ b/src/ui/StatusPanel.vue @@ -18,6 +18,9 @@ const stats = useGameState()
  • 遇敌计量: {{ stats.encounterMeter.toFixed(0) }}
  • 场景状态: {{ stats.sceneMode }}
  • 战斗状态: {{ stats.battle.active ? '战斗中' : '未战斗' }}
  • +
  • 任务: {{ stats.quest.title }}
  • +
  • 任务状态: {{ stats.quest.status }}
  • +
  • 任务进度: {{ stats.quest.currentKills }} / {{ stats.quest.targetKills }}
  • {{ stats.notice }}