任务
This commit is contained in:
parent
2d7481c0e7
commit
6f6125e640
|
|
@ -4,7 +4,8 @@
|
||||||
"name": "新手挑战:击败哥布林",
|
"name": "新手挑战:击败哥布林",
|
||||||
"description": "前往附近的森林,击败一只哥布林来证明你的勇气。",
|
"description": "前往附近的森林,击败一只哥布林来证明你的勇气。",
|
||||||
"type": "kill",
|
"type": "kill",
|
||||||
"target_npc_id": "VILLAGER_1",
|
"triggerType": "NPC",
|
||||||
|
"triggerValue": "VILLAGER_1",
|
||||||
"target": {
|
"target": {
|
||||||
"entityId": "GOBLIN",
|
"entityId": "GOBLIN",
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|
@ -16,6 +17,25 @@
|
||||||
"item_id": 1,
|
"item_id": 1,
|
||||||
"item_quantity": 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"
|
"next_quest_id": "FIND_NPC"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -27,10 +47,20 @@
|
||||||
"npcId": "BLACKSMITH",
|
"npcId": "BLACKSMITH",
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
},
|
||||||
"required_level": 1,
|
"triggerType": "SYSTEM",
|
||||||
|
"triggerValue": "LEVEL_UP",
|
||||||
|
"required_level": 2,
|
||||||
"rewards": {
|
"rewards": {
|
||||||
"xp": 100
|
"xp": 100
|
||||||
},
|
},
|
||||||
|
"dialogue": {
|
||||||
|
"root": {
|
||||||
|
"text": "你的等级提升了!村里的铁匠似乎想见你。",
|
||||||
|
"options": [
|
||||||
|
{ "text": "我会去看看的。(接受任务)", "next": null, "action": "accept_quest:FIND_NPC" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"next_quest_id": null
|
"next_quest_id": null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -26,6 +26,7 @@ use Game\System\ItemService;
|
||||||
use Game\System\InteractionSystem;
|
use Game\System\InteractionSystem;
|
||||||
use Game\System\QuestService;
|
use Game\System\QuestService;
|
||||||
use Game\System\ShopService;
|
use Game\System\ShopService;
|
||||||
|
use Game\System\DialogueService; // ⭐ 新增
|
||||||
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
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(LootService::class, new LootService($this->eventDispatcher, $this->stateManager, $this->itemRepository, $this->enemyRepository));
|
||||||
$this->register(ItemService::class, new ItemService($this->eventDispatcher, $this->stateManager));
|
$this->register(ItemService::class, new ItemService($this->eventDispatcher, $this->stateManager));
|
||||||
$this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager, $this->questionRepository));
|
$this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager, $this->questionRepository));
|
||||||
// ⭐ 实例化 EquipmentService
|
// ⭐ 实例化 AbilityService
|
||||||
$this->register(EquipmentService::class, new EquipmentService($this->eventDispatcher, $this->stateManager));
|
$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,
|
$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,
|
$this->register(BattleService::class,
|
||||||
new BattleService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper)
|
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)
|
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 组件)
|
// 4. 输入处理服务 (驱动主循环,必须最后注册,因为它依赖于所有 I/O 组件)
|
||||||
$this->register(InputHandler::class,
|
$this->register(InputHandler::class,
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,20 @@ class QuestRepository implements RepositoryInterface {
|
||||||
*/
|
*/
|
||||||
private function buildNpcQuestIndex(): void {
|
private function buildNpcQuestIndex(): void {
|
||||||
foreach ($this->data as $questId => $questData) {
|
foreach ($this->data as $questId => $questData) {
|
||||||
// 检查任务是否有目标 NPC ID (target_npc_id) 或目标实体 ID (target_entity_id)
|
// 优先使用 triggerType = NPC 的 triggerValue
|
||||||
// 根据您提供的 quests.json
|
if (isset($questData['triggerType']) && $questData['triggerType'] === 'NPC' && isset($questData['triggerValue'])) {
|
||||||
$targetId = $questData['target_npc_id'] ?? null;
|
$npcId = $questData['triggerValue'];
|
||||||
|
$this->questsByNpc[$npcId][] = $questId;
|
||||||
if ($targetId) {
|
}
|
||||||
// 将任务 ID 添加到该 NPC 对应的列表
|
// 兼容旧数据:检查 target_npc_id (如果是 talk 类型或者仅仅是关联)
|
||||||
$this->questsByNpc[$targetId][] = $questId;
|
// 但如果不作为触发条件,可能不应该在这里索引?
|
||||||
|
// 保持兼容性:如果还没索引过,且有 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
src/Model/DialogueNode.php
Normal file
25
src/Model/DialogueNode.php
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
namespace Game\Model;
|
||||||
|
|
||||||
|
class DialogueNode {
|
||||||
|
public string $id;
|
||||||
|
public string $text;
|
||||||
|
/**
|
||||||
|
* @var array<int, array{text: string, next: ?string, action: ?string}>
|
||||||
|
*/
|
||||||
|
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'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,13 +17,21 @@ class Quest {
|
||||||
// ⭐ 新增:运行时状态,默认为 0
|
// ⭐ 新增:运行时状态,默认为 0
|
||||||
protected int $currentCount = 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->id = $id;
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->description = $description;
|
$this->description = $description;
|
||||||
$this->type = $type;
|
$this->type = $type;
|
||||||
$this->target = $target;
|
$this->target = $target;
|
||||||
$this->rewards = $rewards;
|
$this->rewards = $rewards;
|
||||||
|
$this->triggerType = $triggerType;
|
||||||
|
$this->triggerValue = $triggerValue;
|
||||||
|
$this->dialogue = $dialogue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 进度管理方法 (用于业务逻辑) ---
|
// --- 进度管理方法 (用于业务逻辑) ---
|
||||||
|
|
@ -77,6 +85,9 @@ class Quest {
|
||||||
'rewards' => $this->rewards,
|
'rewards' => $this->rewards,
|
||||||
'isRepeatable' => $this->isRepeatable,
|
'isRepeatable' => $this->isRepeatable,
|
||||||
'currentCount' => $this->currentCount,
|
'currentCount' => $this->currentCount,
|
||||||
|
'triggerType' => $this->triggerType,
|
||||||
|
'triggerValue' => $this->triggerValue,
|
||||||
|
'dialogue' => $this->dialogue,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
166
src/System/DialogueService.php
Normal file
166
src/System/DialogueService.php
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
<?php
|
||||||
|
namespace Game\System;
|
||||||
|
|
||||||
|
use Game\Event\Event;
|
||||||
|
use Game\Event\EventDispatcher;
|
||||||
|
use Game\Model\DialogueNode;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||||
|
use Symfony\Component\Console\Question\Question;
|
||||||
|
|
||||||
|
use Game\Event\EventListenerInterface; // ⭐ Add import
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DialogueService: 负责处理对话树的流程逻辑和交互。
|
||||||
|
*/
|
||||||
|
class DialogueService implements EventListenerInterface {
|
||||||
|
|
||||||
|
private EventDispatcher $dispatcher;
|
||||||
|
private StateManager $stateManager; // ⭐ Add StateManager
|
||||||
|
private InputInterface $input;
|
||||||
|
private OutputInterface $output;
|
||||||
|
private QuestionHelper $helper;
|
||||||
|
|
||||||
|
private array $currentDialogueTree = [];
|
||||||
|
private string $currentNodeId = '';
|
||||||
|
|
||||||
|
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager, InputInterface $input, OutputInterface $output, QuestionHelper $helper) {
|
||||||
|
$this->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<fg=cyan>🗣️ 对话</>");
|
||||||
|
$this->output->writeln("<fg=white>" . str_repeat("-", 40) . "</>");
|
||||||
|
$lines = explode("\n", $node->text);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$this->output->writeln(" " . trim($line));
|
||||||
|
}
|
||||||
|
$this->output->writeln("<fg=white>" . str_repeat("-", 40) . "</>\n");
|
||||||
|
|
||||||
|
if (empty($node->options)) {
|
||||||
|
$this->output->writeln(" [按回车键或输入任意值结束]");
|
||||||
|
} else {
|
||||||
|
foreach ($node->options as $index => $option) {
|
||||||
|
$this->output->writeln(" [<fg=yellow>{$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("<fg=red>输入无效,请输入选项数字。</>");
|
||||||
|
// 这里不需要循环,直接返回,等待下一次输入事件
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$index = (int)$input;
|
||||||
|
if (!isset($node->options[$index])) {
|
||||||
|
$this->output->writeln("<fg=red>选项不存在。</>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
namespace Game\System;
|
namespace Game\System;
|
||||||
|
|
||||||
use Game\Database\NPCRepository;
|
use Game\Database\NPCRepository;
|
||||||
|
use Game\Database\QuestRepository; // ⭐ Add Import
|
||||||
use Game\Event\Event;
|
use Game\Event\Event;
|
||||||
use Game\Event\EventListenerInterface;
|
use Game\Event\EventListenerInterface;
|
||||||
use Game\Event\EventDispatcher;
|
use Game\Event\EventDispatcher;
|
||||||
|
|
@ -19,16 +20,29 @@ class InteractionSystem implements EventListenerInterface {
|
||||||
private EventDispatcher $dispatcher;
|
private EventDispatcher $dispatcher;
|
||||||
private StateManager $stateManager;
|
private StateManager $stateManager;
|
||||||
private NPCRepository $npcRepository; // ⭐ 新增属性
|
private NPCRepository $npcRepository; // ⭐ 新增属性
|
||||||
|
private QuestRepository $questRepository; // ⭐ 新增
|
||||||
|
private DialogueService $dialogueService; // ⭐ 新增
|
||||||
// 输入依赖
|
// 输入依赖
|
||||||
private InputInterface $input;
|
private InputInterface $input;
|
||||||
private OutputInterface $output;
|
private OutputInterface $output;
|
||||||
private QuestionHelper $helper;
|
private QuestionHelper $helper;
|
||||||
|
|
||||||
// ⭐ 修正构造函数:注入 NPCRepository
|
// ⭐ 修正构造函数:注入 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->dispatcher = $dispatcher;
|
||||||
$this->stateManager = $stateManager;
|
$this->stateManager = $stateManager;
|
||||||
$this->npcRepository = $npcRepository; // ⭐ 赋值
|
$this->npcRepository = $npcRepository; // ⭐ 赋值
|
||||||
|
$this->questRepository = $questRepository; // ⭐ 赋值
|
||||||
|
$this->dialogueService = $dialogueService; // ⭐ 赋值
|
||||||
$this->input = $input;
|
$this->input = $input;
|
||||||
$this->output = $output;
|
$this->output = $output;
|
||||||
$this->helper = $helper;
|
$this->helper = $helper;
|
||||||
|
|
@ -75,29 +89,25 @@ class InteractionSystem implements EventListenerInterface {
|
||||||
/**
|
/**
|
||||||
* 2. 核心对话循环
|
* 2. 核心对话循环
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 2. 核心交互循环
|
||||||
|
*/
|
||||||
private function dialogueLoop(NPC $npc): void {
|
private function dialogueLoop(NPC $npc): void {
|
||||||
$currentDialogueKey = 'greeting';
|
|
||||||
$running = true;
|
$running = true;
|
||||||
|
|
||||||
while ($running) {
|
while ($running) {
|
||||||
|
|
||||||
// 打印 NPC 对话
|
|
||||||
$text = $npc->getDialogueText($currentDialogueKey);
|
|
||||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
|
||||||
'message' => "<fg=cyan>{$npc->getName()}</>:<fg=white>{$text}</>"
|
|
||||||
]));
|
|
||||||
|
|
||||||
// 检查交互类型并获取玩家选择
|
// 检查交互类型并获取玩家选择
|
||||||
$choice = $this->promptPlayerChoice();
|
$choice = $this->promptPlayerChoice($npc);
|
||||||
|
|
||||||
switch ($choice) {
|
switch ($choice) {
|
||||||
case 'T': // 触发任务/特殊事件
|
case 'T': // 交谈 (可能触发任务)
|
||||||
$this->dispatcher->dispatch(new Event('QuestCheckEvent', ['npcId' => $npc->id]));
|
if ($this->handleTalk($npc)) {
|
||||||
$currentDialogueKey = 'quest_response';
|
// 如果触发了对话系统,结束当前的 Interaction Loop,交由 DialogueMode 接管
|
||||||
|
$running = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'S': // 触发商店
|
case 'S': // 触发商店
|
||||||
$this->dispatcher->dispatch(new Event('OpenShopEvent', ['npcId' => $npc->id]));
|
$this->dispatcher->dispatch(new Event('OpenShopEvent', ['npcId' => $npc->id]));
|
||||||
$currentDialogueKey = 'shop_response';
|
|
||||||
break;
|
break;
|
||||||
case 'E': // 结束对话
|
case 'E': // 结束对话
|
||||||
$running = false;
|
$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' => "<fg=cyan>{$npc->getName()}</>:<fg=white>{$defaultMsg}</>"
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取玩家交互指令
|
* 获取玩家交互指令
|
||||||
*/
|
*/
|
||||||
private function promptPlayerChoice(): string {
|
private function promptPlayerChoice(NPC $npc): string {
|
||||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||||
'message' => "\n--- 交互菜单 --- [T] 任务 | [S] 商店 | [E] 结束"
|
'message' => "\n--- 交互菜单 --- [T] 任务 | [S] 商店 | [E] 结束"
|
||||||
]));
|
]));
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,15 @@ class QuestService implements EventListenerInterface {
|
||||||
case 'BattleEndEvent': // 响应战斗结束,检查击杀目标
|
case 'BattleEndEvent': // 响应战斗结束,检查击杀目标
|
||||||
$this->checkKillQuests($event->getPayload()['enemyId']);
|
$this->checkKillQuests($event->getPayload()['enemyId']);
|
||||||
break;
|
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 {
|
public function initializeQuests(): void {
|
||||||
$player = $this->stateManager->getPlayer();
|
$player = $this->stateManager->getPlayer();
|
||||||
if (empty($player->getActiveQuests()) && empty($player->getCompletedQuests())) {
|
if (empty($player->getActiveQuests()) && empty($player->getCompletedQuests())) {
|
||||||
$startingQuestId = $this->questRepository->getStartingQuestId();
|
|
||||||
if ($startingQuestId) {
|
if ($startingQuestId) {
|
||||||
$this->startQuest($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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
71
tests/test_quest_dialogue.php
Normal file
71
tests/test_quest_dialogue.php
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?php
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Game\Core\ServiceContainer;
|
||||||
|
use Game\Event\Event;
|
||||||
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
|
use Symfony\Component\Console\Helper\HelperSet;
|
||||||
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||||
|
|
||||||
|
// Mock Input with stream for InteractionSystem blocking prompt
|
||||||
|
$stream = fopen('php://memory', 'r+');
|
||||||
|
fwrite($stream, "T\n"); // Select 'Target' (Talk)
|
||||||
|
rewind($stream);
|
||||||
|
|
||||||
|
$input = new ArrayInput([]);
|
||||||
|
$input->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";
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user