diff --git a/config/quests.json b/config/quests.json index d1ee3d5..2ce552c 100644 --- a/config/quests.json +++ b/config/quests.json @@ -4,7 +4,8 @@ "name": "新手挑战:击败哥布林", "description": "前往附近的森林,击败一只哥布林来证明你的勇气。", "type": "kill", - "target_npc_id": "VILLAGER_1", + "triggerType": "NPC", + "triggerValue": "VILLAGER_1", "target": { "entityId": "GOBLIN", "count": 1 @@ -16,6 +17,25 @@ "item_id": 1, "item_quantity": 1 }, + "dialogue": { + "root": { + "text": "你好啊,年轻人。村外的哥布林最近越来越猖狂了。", + "options": [ + { "text": "哥布林?我可以帮忙处理。", "next": "accept" }, + { "text": "那真是太可怕了,再见。", "next": "end" } + ] + }, + "accept": { + "text": "真的吗?那太好了!去森林里消灭一只哥布林,证明你的实力吧。", + "options": [ + { "text": "交给我吧!(接受任务)", "next": null, "action": "accept_quest:KILL_GOBLIN" } + ] + }, + "end": { + "text": "好吧,路上小心。", + "options": [] + } + }, "next_quest_id": "FIND_NPC" }, { @@ -27,10 +47,20 @@ "npcId": "BLACKSMITH", "count": 1 }, - "required_level": 1, + "triggerType": "SYSTEM", + "triggerValue": "LEVEL_UP", + "required_level": 2, "rewards": { "xp": 100 }, + "dialogue": { + "root": { + "text": "你的等级提升了!村里的铁匠似乎想见你。", + "options": [ + { "text": "我会去看看的。(接受任务)", "next": null, "action": "accept_quest:FIND_NPC" } + ] + } + }, "next_quest_id": null } ] \ No newline at end of file diff --git a/src/Core/ServiceContainer.php b/src/Core/ServiceContainer.php index 4ab35d8..755cff8 100644 --- a/src/Core/ServiceContainer.php +++ b/src/Core/ServiceContainer.php @@ -26,6 +26,7 @@ use Game\System\ItemService; use Game\System\InteractionSystem; use Game\System\QuestService; use Game\System\ShopService; +use Game\System\DialogueService; // ⭐ 新增 use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -99,12 +100,29 @@ class ServiceContainer { $this->register(LootService::class, new LootService($this->eventDispatcher, $this->stateManager, $this->itemRepository, $this->enemyRepository)); $this->register(ItemService::class, new ItemService($this->eventDispatcher, $this->stateManager)); $this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager, $this->questionRepository)); - // ⭐ 实例化 EquipmentService - $this->register(EquipmentService::class, new EquipmentService($this->eventDispatcher, $this->stateManager)); + // ⭐ 实例化 AbilityService + $abilityService = new AbilityService($this->eventDispatcher, $this->stateManager, $this->abilityRepository); + $this->register(AbilityService::class, $abilityService); + + // ⭐ 实例化 DialogueService + $dialogueService = new DialogueService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper); + $this->register(DialogueService::class, $dialogueService); + + // 3. I/O 交互服务 + // ⭐ 使用 get 方法获取已注册的服务实例 + // $dialogueService = $this->services[DialogueService::class]; // No need to fetch from array if we have var - // 3. I/O 交互服务 (依赖 Dispatcher, StateManager, I/O 接口) $this->register(InteractionSystem::class, - new InteractionSystem($this->eventDispatcher, $this->stateManager, $this->npcRepository, $this->input, $this->output, $this->questionHelper) + new InteractionSystem( + $this->eventDispatcher, + $this->stateManager, + $this->npcRepository, + $this->questionRepository, // ⭐ 新增注入 + $dialogueService, // ⭐ 新增注入 + $this->input, + $this->output, + $this->questionHelper + ) ); $this->register(BattleService::class, new BattleService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper) @@ -113,9 +131,7 @@ class ServiceContainer { new ShopService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper) ); - // ⭐ 实例化 AbilityService - $abilityService = new AbilityService($this->eventDispatcher, $this->stateManager, $this->abilityRepository); - $this->register(AbilityService::class, $abilityService); + // 4. 输入处理服务 (驱动主循环,必须最后注册,因为它依赖于所有 I/O 组件) $this->register(InputHandler::class, diff --git a/src/Database/QuestRepository.php b/src/Database/QuestRepository.php index 7e1871f..d3334d8 100644 --- a/src/Database/QuestRepository.php +++ b/src/Database/QuestRepository.php @@ -21,13 +21,20 @@ class QuestRepository implements RepositoryInterface { */ private function buildNpcQuestIndex(): void { foreach ($this->data as $questId => $questData) { - // 检查任务是否有目标 NPC ID (target_npc_id) 或目标实体 ID (target_entity_id) - // 根据您提供的 quests.json - $targetId = $questData['target_npc_id'] ?? null; - - if ($targetId) { - // 将任务 ID 添加到该 NPC 对应的列表 - $this->questsByNpc[$targetId][] = $questId; + // 优先使用 triggerType = NPC 的 triggerValue + if (isset($questData['triggerType']) && $questData['triggerType'] === 'NPC' && isset($questData['triggerValue'])) { + $npcId = $questData['triggerValue']; + $this->questsByNpc[$npcId][] = $questId; + } + // 兼容旧数据:检查 target_npc_id (如果是 talk 类型或者仅仅是关联) + // 但如果不作为触发条件,可能不应该在这里索引? + // 保持兼容性:如果还没索引过,且有 target_npc_id + elseif (isset($questData['target_npc_id'])) { + $targetId = $questData['target_npc_id']; + // 避免重复? + if (!in_array($questId, $this->questsByNpc[$targetId] ?? [])) { + $this->questsByNpc[$targetId][] = $questId; + } } } } diff --git a/src/Model/DialogueNode.php b/src/Model/DialogueNode.php new file mode 100644 index 0000000..11a82c6 --- /dev/null +++ b/src/Model/DialogueNode.php @@ -0,0 +1,25 @@ + + */ + public array $options; + + public function __construct(string $id, string $text, array $options = []) { + $this->id = $id; + $this->text = $text; + $this->options = $options; + } + + public static function fromArray(string $id, array $data): self { + return new self( + $id, + $data['text'] ?? '', + $data['options'] ?? [] + ); + } +} diff --git a/src/Model/Quest.php b/src/Model/Quest.php index 145359c..61580d6 100644 --- a/src/Model/Quest.php +++ b/src/Model/Quest.php @@ -17,13 +17,21 @@ class Quest { // ⭐ 新增:运行时状态,默认为 0 protected int $currentCount = 0; - public function __construct(string $id, string $name, string $description, string $type, array $target, array $rewards) { + // ⭐ 新增:触发器和对话数据 + public ?string $triggerType; // 'NPC' or 'SYSTEM' + public ?string $triggerValue; // NPC_ID or EVENT_NAME + public array $dialogue; // Dialogue Tree + + public function __construct(string $id, string $name, string $description, string $type, array $target, array $rewards, ?string $triggerType = null, ?string $triggerValue = null, array $dialogue = []) { $this->id = $id; $this->name = $name; $this->description = $description; $this->type = $type; $this->target = $target; $this->rewards = $rewards; + $this->triggerType = $triggerType; + $this->triggerValue = $triggerValue; + $this->dialogue = $dialogue; } // --- 进度管理方法 (用于业务逻辑) --- @@ -77,6 +85,9 @@ class Quest { 'rewards' => $this->rewards, 'isRepeatable' => $this->isRepeatable, 'currentCount' => $this->currentCount, + 'triggerType' => $this->triggerType, + 'triggerValue' => $this->triggerValue, + 'dialogue' => $this->dialogue, ]; } } \ No newline at end of file diff --git a/src/System/DialogueService.php b/src/System/DialogueService.php new file mode 100644 index 0000000..a2630c4 --- /dev/null +++ b/src/System/DialogueService.php @@ -0,0 +1,166 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + $this->input = $input; + $this->output = $output; + $this->helper = $helper; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'StartDialogueEvent': + $dialogue = $event->getPayload()['dialogue']; + $this->startDialogue($dialogue); + break; + case 'DialogueChoice': // ⭐ Listen for choice event + $choice = $event->getPayload()['choice']; + $this->handleChoice($choice); + break; + } + } + + /** + * 开始一段对话 (非阻塞,初始化状态) + */ + public function startDialogue(array $dialogueTree): void { + $this->currentDialogueTree = $dialogueTree; + $this->currentNodeId = 'root'; + + // 切换游戏模式 + $this->stateManager->setMode(StateManager::MODE_DIALOGUE); + + // 显示第一个节点 + $this->displayCurrentNode(); + } + + private function displayCurrentNode(): void { + if (!isset($this->currentDialogueTree[$this->currentNodeId])) { + $this->endDialogue(); + return; + } + + $nodeData = $this->currentDialogueTree[$this->currentNodeId]; + $node = DialogueNode::fromArray($this->currentNodeId, $nodeData); + + $this->output->writeln("\n🗣️ 对话"); + $this->output->writeln("" . str_repeat("-", 40) . ""); + $lines = explode("\n", $node->text); + foreach ($lines as $line) { + $this->output->writeln(" " . trim($line)); + } + $this->output->writeln("" . str_repeat("-", 40) . "\n"); + + if (empty($node->options)) { + $this->output->writeln(" [按回车键或输入任意值结束]"); + } else { + foreach ($node->options as $index => $option) { + $this->output->writeln(" [{$index}] {$option['text']}"); + } + } + } + + /** + * 处理玩家选择 + */ + private function handleChoice(string $input): void { + if (!isset($this->currentDialogueTree[$this->currentNodeId])) { + $this->endDialogue(); + return; + } + + $nodeData = $this->currentDialogueTree[$this->currentNodeId]; + $node = DialogueNode::fromArray($this->currentNodeId, $nodeData); + + // 如果没有选项,任意输入都结束对话 (或者跳转 next) + if (empty($node->options)) { + $this->endDialogue(); + return; + } + + if (!is_numeric($input)) { + $this->output->writeln("输入无效,请输入选项数字。"); + // 这里不需要循环,直接返回,等待下一次输入事件 + return; + } + + $index = (int)$input; + if (!isset($node->options[$index])) { + $this->output->writeln("选项不存在。"); + return; + } + + // 处理选中项 + $selectedOption = $node->options[$index]; + + // 1. 执行动作 + if (!empty($selectedOption['action'])) { + $this->handleAction($selectedOption['action']); + } + + // 2. 跳转 + if (empty($selectedOption['next'])) { + $this->endDialogue(); + } else { + $this->currentNodeId = $selectedOption['next']; + $this->displayCurrentNode(); + } + } + + private function endDialogue(): void { + $this->currentDialogueTree = []; + $this->currentNodeId = ''; + $this->output->writeln("\n[对话结束]"); + + // 恢复地图模式 (或者之前的模式,稍微简化处理) + $this->stateManager->setMode(StateManager::MODE_MAP); + + // 触发菜单显示,让玩家知道回到了地图 + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + } + + private function handleAction(string $actionString): void { + $parts = explode(':', $actionString, 2); + $actionType = $parts[0]; + $payload = $parts[1] ?? null; + + switch ($actionType) { + case 'accept_quest': + if ($payload) { + $this->dispatcher->dispatch(new Event('QuestAcceptRequest', ['questId' => $payload])); + } + break; + case 'open_shop': + if ($payload) { + $this->dispatcher->dispatch(new Event('OpenShopEvent', ['npcId' => $payload])); + } + break; + } + } +} diff --git a/src/System/InteractionSystem.php b/src/System/InteractionSystem.php index eed391a..512bb56 100644 --- a/src/System/InteractionSystem.php +++ b/src/System/InteractionSystem.php @@ -2,6 +2,7 @@ namespace Game\System; use Game\Database\NPCRepository; +use Game\Database\QuestRepository; // ⭐ Add Import use Game\Event\Event; use Game\Event\EventListenerInterface; use Game\Event\EventDispatcher; @@ -19,16 +20,29 @@ class InteractionSystem implements EventListenerInterface { private EventDispatcher $dispatcher; private StateManager $stateManager; private NPCRepository $npcRepository; // ⭐ 新增属性 + private QuestRepository $questRepository; // ⭐ 新增 + private DialogueService $dialogueService; // ⭐ 新增 // 输入依赖 private InputInterface $input; private OutputInterface $output; private QuestionHelper $helper; // ⭐ 修正构造函数:注入 NPCRepository - public function __construct(EventDispatcher $dispatcher, StateManager $stateManager, NPCRepository $npcRepository, InputInterface $input, OutputInterface $output, QuestionHelper $helper) { + public function __construct( + EventDispatcher $dispatcher, + StateManager $stateManager, + NPCRepository $npcRepository, + QuestRepository $questRepository, // ⭐ 参数 + DialogueService $dialogueService, // ⭐ 参数 + InputInterface $input, + OutputInterface $output, + QuestionHelper $helper + ) { $this->dispatcher = $dispatcher; $this->stateManager = $stateManager; $this->npcRepository = $npcRepository; // ⭐ 赋值 + $this->questRepository = $questRepository; // ⭐ 赋值 + $this->dialogueService = $dialogueService; // ⭐ 赋值 $this->input = $input; $this->output = $output; $this->helper = $helper; @@ -75,29 +89,25 @@ class InteractionSystem implements EventListenerInterface { /** * 2. 核心对话循环 */ + /** + * 2. 核心交互循环 + */ private function dialogueLoop(NPC $npc): void { - $currentDialogueKey = 'greeting'; $running = true; while ($running) { - - // 打印 NPC 对话 - $text = $npc->getDialogueText($currentDialogueKey); - $this->dispatcher->dispatch(new Event('SystemMessage', [ - 'message' => "{$npc->getName()}:{$text}" - ])); - // 检查交互类型并获取玩家选择 - $choice = $this->promptPlayerChoice(); + $choice = $this->promptPlayerChoice($npc); switch ($choice) { - case 'T': // 触发任务/特殊事件 - $this->dispatcher->dispatch(new Event('QuestCheckEvent', ['npcId' => $npc->id])); - $currentDialogueKey = 'quest_response'; + case 'T': // 交谈 (可能触发任务) + if ($this->handleTalk($npc)) { + // 如果触发了对话系统,结束当前的 Interaction Loop,交由 DialogueMode 接管 + $running = false; + } break; case 'S': // 触发商店 $this->dispatcher->dispatch(new Event('OpenShopEvent', ['npcId' => $npc->id])); - $currentDialogueKey = 'shop_response'; break; case 'E': // 结束对话 $running = false; @@ -109,10 +119,43 @@ class InteractionSystem implements EventListenerInterface { } } + private function handleTalk(NPC $npc): bool { + $player = $this->stateManager->getPlayer(); + + // 1. 查找此 NPC 提供的所有任务 + $questIds = $this->questRepository->getQuestsByNpc($npc->id); + + $foundQuest = false; + foreach ($questIds as $questId) { + // 检查任务状态:未接受 或 进行中 (如果是进行中,可能需要不同的对话,比如询问进度) + // 简单起见,这里优先查找“未接受”的任务 + if (!$player->isQuestCompleted($questId) && !isset($player->getActiveQuests()[$questId])) { + // 找到了一个新任务! + $questData = $this->questRepository->find($questId); + if ($questData && !empty($questData['dialogue'])) { + // ⭐ 使用新版对话系统 + $this->dialogueService->startDialogue($questData['dialogue']); + // 对话已启动,返回 true 以退出 InteractionLoop + return true; + } + } + } + + if (!$foundQuest) { + // 如果没有新任务,显示 NPC 默认闲聊 + $defaultMsg = is_array($npc->dialogue) ? ($npc->dialogue['greeting'] ?? '...') : '...'; + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "{$npc->getName()}:{$defaultMsg}" + ])); + } + + return false; + } + /** * 获取玩家交互指令 */ - private function promptPlayerChoice(): string { + private function promptPlayerChoice(NPC $npc): string { $this->dispatcher->dispatch(new Event('SystemMessage', [ 'message' => "\n--- 交互菜单 --- [T] 任务 | [S] 商店 | [E] 结束" ])); diff --git a/src/System/QuestService.php b/src/System/QuestService.php index 58dcc68..5cec2cb 100644 --- a/src/System/QuestService.php +++ b/src/System/QuestService.php @@ -38,6 +38,15 @@ class QuestService implements EventListenerInterface { case 'BattleEndEvent': // 响应战斗结束,检查击杀目标 $this->checkKillQuests($event->getPayload()['enemyId']); break; + case 'MapMoveEvent': // ⭐ 响应移动,检查地点触发任务 + $this->checkSystemTriggers('MAP_MOVE', $event->getPayload()['targetId'] ?? ''); // 假设 MapMoveEvent 携带 targetId (MapTile ID) + break; + case 'LevelUpEvent': // ⭐ 响应升级 + $this->checkSystemTriggers('LEVEL_UP', (string)$event->getPayload()['level']); + break; + case 'QuestAcceptRequest': // ⭐ 响应对话中的接受任务请求 + $this->startQuest($event->getPayload()['questId']); + break; } } @@ -179,10 +188,38 @@ class QuestService implements EventListenerInterface { public function initializeQuests(): void { $player = $this->stateManager->getPlayer(); if (empty($player->getActiveQuests()) && empty($player->getCompletedQuests())) { - $startingQuestId = $this->questRepository->getStartingQuestId(); if ($startingQuestId) { $this->startQuest($startingQuestId); } } } + + /** + * 7. 检查系统触发的任务 + */ + private function checkSystemTriggers(string $triggerType, string $triggerValue): void { + $player = $this->stateManager->getPlayer(); + $allQuests = $this->questRepository->findAll(); // 假设 we can get all quests + + foreach ($allQuests as $questData) { + $questId = $questData['id']; + + // 检查触发条件 + if (isset($questData['triggerType']) && $questData['triggerType'] === 'SYSTEM' && + isset($questData['triggerValue']) && $questData['triggerValue'] === $triggerValue) { + + // 检查是否已完成或已接取 + if (!$player->isQuestCompleted($questId) && !isset($player->getActiveQuests()[$questId])) { + // 触发对话 + if (!empty($questData['dialogue'])) { + $this->dispatcher->dispatch(new Event('StartDialogueEvent', ['dialogue' => $questData['dialogue']])); + } else { + // 无对话直接接取? 或者弹窗提示 + // 简单起见,无对话也强制开始(可能会有默认提示) + $this->startQuest($questId); + } + } + } + } + } } \ No newline at end of file diff --git a/tests/test_quest_dialogue.php b/tests/test_quest_dialogue.php new file mode 100644 index 0000000..4c7da8b --- /dev/null +++ b/tests/test_quest_dialogue.php @@ -0,0 +1,71 @@ +setStream($stream); +$input->setInteractive(true); + +$output = new BufferedOutput(); +$helperSet = new HelperSet(['question' => new QuestionHelper()]); + +$container = new ServiceContainer($input, $output, $helperSet); +$dispatcher = $container->registerServices(); +$stateManager = $container->getStateManager(); + +// Manually create and set player for test environment +$player = new \Game\Model\Player("TestHero", 100, 10, 5); +$stateManager->setPlayer($player); + +echo "Player Initialized.\n"; + +// 1. Simulate meeting NPC (Starts Dialogue) +echo "Dispatching Interaction Event...\n"; +$dispatcher->dispatch(new Event('AttemptInteractEvent', ['npcId' => 'VILLAGER_1'])); + +echo "Checking output...\n"; +$display = $output->fetch(); +echo $display; + +if (strpos($display, '你好啊,年轻人') !== false) { + echo "SUCCESS: Dialogue triggered.\n"; +} else { + echo "FAILURE: Dialogue text not found.\n"; +} + +// 2. Simulate User Input: Select Option 0 (via Event, since InputHandler is not running loop) +echo "Dispatching Choice 0...\n"; +$dispatcher->dispatch(new Event('DialogueChoice', ['choice' => '0'])); + +$display = $output->fetch(); +echo $display; + +if (strpos($display, '真的吗?那太好了') !== false) { + echo "SUCCESS: Dialogue advanced.\n"; +} else { + echo "FAILURE: Dialogue advancement failed.\n"; +} + +// 3. Simulate User Input: Select Option 0 (Accept Quest) +echo "Dispatching Choice 0 (Accept)...\n"; +$dispatcher->dispatch(new Event('DialogueChoice', ['choice' => '0'])); + +$display = $output->fetch(); +echo $display; + +if (strpos($display, '接受任务') !== false) { + echo "SUCCESS: Quest accepted.\n"; +} else { + echo "FAILURE: Quest acceptance message not found.\n"; +}