From 09383708de9d98cefc6d1c8aa20d82c4c83ec6c3 Mon Sep 17 00:00:00 2001 From: hantao Date: Mon, 22 Dec 2025 18:07:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/npcs.json | 6 + config/quests.json | 2 +- save/player.json | 192 +++++++++++++++++++++------- src/Core/ServiceContainer.php | 16 ++- src/Database/NPCRepository.php | 6 +- src/Model/MapTile.php | 5 +- src/Model/NPC.php | 6 +- src/Model/Player.php | 10 +- src/Model/Quest.php | 12 ++ src/System/BattleService.php | 19 +-- src/System/DialogueService.php | 3 - src/System/InputHandler.php | 117 ++++++++++++++++- src/System/InteractionSystem.php | 144 ++++++++------------- src/System/MapSystem.php | 16 +-- src/System/QuestService.php | 120 +++++++++++------- src/System/SaveLoadService.php | 74 ++++++++++- src/System/ShopService.php | 66 +++++++--- src/System/StateManager.php | 47 +++++++ src/System/UIService.php | 207 ++++++++++++++++++++++++------- tests/test_autosave.php | 63 ++++++++++ tests/test_inventory.php | 58 +++++++++ tests/test_npc_interaction.php | 48 +++++++ tests/test_npc_shop.php | 80 ++++++++++++ tests/test_npc_shop_simple.php | 48 +++++++ tests/test_quest_list.php | 67 ++++++++++ tests/test_quest_turnin.php | 74 +++++++++++ 26 files changed, 1213 insertions(+), 293 deletions(-) create mode 100644 tests/test_autosave.php create mode 100644 tests/test_inventory.php create mode 100644 tests/test_npc_interaction.php create mode 100644 tests/test_npc_shop.php create mode 100644 tests/test_npc_shop_simple.php create mode 100644 tests/test_quest_list.php create mode 100644 tests/test_quest_turnin.php diff --git a/config/npcs.json b/config/npcs.json index f04874a..0833357 100644 --- a/config/npcs.json +++ b/config/npcs.json @@ -13,6 +13,12 @@ "greeting": "欢迎来到我的铁匠铺。", "quest_response": "你有空吗?我的铁矿用完了。", "shop_response": "看一看你需要什么工具和武器。" + }, + "hasShop": true, + "shopInventory": { + "2": {"price": 50}, + "4": {"price": 150}, + "5": {"price": 200} } } } \ No newline at end of file diff --git a/config/quests.json b/config/quests.json index 2ce552c..8b89053 100644 --- a/config/quests.json +++ b/config/quests.json @@ -8,7 +8,7 @@ "triggerValue": "VILLAGER_1", "target": { "entityId": "GOBLIN", - "count": 1 + "count": 10 }, "required_level": 1, "rewards": { diff --git a/save/player.json b/save/player.json index 10ebe69..79fdc28 100644 --- a/save/player.json +++ b/save/player.json @@ -1,23 +1,133 @@ { "player": { - "name": "Hant", - "health": 91, - "maxHealth": 100, - "base_attack": 15, - "base_defense": 5, - "level": 1, - "currentXp": 20, - "gold": 12, + "name": "AutoSaveTest", + "health": 160, + "maxHealth": 160, + "base_attack": 22, + "base_defense": 13, + "level": 5, + "currentXp": 175, + "gold": 241, "inventory": [ { - "id": 2, - "name": "\u7834\u65e7\u7684\u77ed\u5251", - "type": "weapon", - "description": "\u653b\u51fb\u529b\u5fae\u5f31\u3002", - "value": 50, - "effects": [], + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, "stats": [], - "slot": "weapon" + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null + }, + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": { + "heal": 20 + }, + "stats": [], + "slot": null }, { "id": 2, @@ -31,36 +141,34 @@ } ], "equipment": { - "weapon": null, + "weapon": { + "id": 2, + "name": "\u7834\u65e7\u7684\u77ed\u5251", + "type": "weapon", + "description": "\u653b\u51fb\u529b\u5fae\u5f31\u3002", + "value": 50, + "effects": [], + "stats": [], + "slot": "weapon" + }, "armor": null, - "helmet": null - }, - "activeQuests": [ - { - "config": { - "id": "KILL_GOBLIN", - "name": "\u65b0\u624b\u6311\u6218\uff1a\u51fb\u8d25\u54e5\u5e03\u6797", - "description": "\u524d\u5f80\u9644\u8fd1\u7684\u68ee\u6797\uff0c\u51fb\u8d25\u4e00\u53ea\u54e5\u5e03\u6797\u6765\u8bc1\u660e\u4f60\u7684\u52c7\u6c14\u3002", - "type": "kill", - "target": { - "entityId": "GOBLIN", - "count": 1 - }, - "rewards": { - "xp": 50, - "gold": 20, - "item_id": 1, - "item_quantity": 1 - }, - "isRepeatable": false, - "currentCount": 0 - }, - "currentCount": 0 + "helmet": { + "id": 4, + "name": "\u5e03\u7532\u5934\u76d4", + "type": "armor", + "description": "\u63d0\u4f9b\u5c11\u91cf\u9632\u5fa1\u3002", + "value": 30, + "effects": [], + "stats": [], + "slot": "helmet" } - ], - "completedQuests": [] + }, + "activeQuests": [], + "completedQuests": [ + "KILL_GOBLIN" + ] }, "world": { - "currentTileId": "FOREST_01" + "currentTileId": "TOWN_01" } } \ No newline at end of file diff --git a/src/Core/ServiceContainer.php b/src/Core/ServiceContainer.php index 755cff8..c67414f 100644 --- a/src/Core/ServiceContainer.php +++ b/src/Core/ServiceContainer.php @@ -77,7 +77,7 @@ class ServiceContainer { $this->itemRepository = new ItemRepository($jsonLoader); $this->enemyRepository = new EnemyRepository($jsonLoader); $this->abilityRepository = new AbilityRepository($jsonLoader); - $this->questionRepository = new QuestRepository($jsonLoader); + $this->questRepository = new QuestRepository($jsonLoader); // ⭐ 修正变量名 $this->npcRepository = new NPCRepository($jsonLoader); $this->mapRepository = new MapRepository($jsonLoader); } @@ -91,15 +91,17 @@ class ServiceContainer { // 状态管理器 $this->stateManager = new StateManager($this->dbManager->getConnection(),$this->mapRepository); $this->saveLoadService = new SaveLoadService($this->eventDispatcher, $this->stateManager); + // 1. UI 服务 (只需要 Dispatcher 和 StateManager) - $this->register(UIService::class, new UIService($this->output, $this->stateManager)); + $this->register(UIService::class, new UIService($this->output, $this->stateManager, $this->questRepository)); // 2. 核心逻辑服务 (依赖 Dispatcher, StateManager) $this->register(MapSystem::class, new MapSystem($this->eventDispatcher, $this->stateManager,$this->npcRepository,$this->mapRepository)); $this->register(CharacterService::class, new CharacterService($this->eventDispatcher, $this->stateManager)); $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)); + $this->register(EquipmentService::class, new EquipmentService($this->eventDispatcher, $this->stateManager)); // ⭐ 注册装备服务 + $this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager, $this->questRepository)); // ⭐ 实例化 AbilityService $abilityService = new AbilityService($this->eventDispatcher, $this->stateManager, $this->abilityRepository); $this->register(AbilityService::class, $abilityService); @@ -117,7 +119,7 @@ class ServiceContainer { $this->eventDispatcher, $this->stateManager, $this->npcRepository, - $this->questionRepository, // ⭐ 新增注入 + $this->questRepository, // ⭐ 修正变量名 $dialogueService, // ⭐ 新增注入 $this->input, $this->output, @@ -128,10 +130,11 @@ class ServiceContainer { new BattleService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper) ); $this->register(ShopService::class, - new ShopService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper) + new ShopService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper, $this->itemRepository) ); - + // ⭐ 注册SaveLoadService为事件监听器,实现自动保存 + $this->register(SaveLoadService::class, $this->saveLoadService); // 4. 输入处理服务 (驱动主循环,必须最后注册,因为它依赖于所有 I/O 组件) $this->register(InputHandler::class, @@ -170,4 +173,5 @@ class ServiceContainer { public function getItemRepository(): ItemRepository { return $this->itemRepository; } public function getEnemyRepository(): EnemyRepository { return $this->enemyRepository; } public function getAbilityRepository(): AbilityRepository { return $this->abilityRepository; } + public function getNpcRepository(): NPCRepository { return $this->npcRepository; } // ⭐ 新增 } \ No newline at end of file diff --git a/src/Database/NPCRepository.php b/src/Database/NPCRepository.php index 5e53647..dda4107 100644 --- a/src/Database/NPCRepository.php +++ b/src/Database/NPCRepository.php @@ -28,11 +28,13 @@ class NPCRepository implements RepositoryInterface { return null; } - // 假设 NPC 模型的构造函数是 public function __construct(string $id, string $name, array $dialogue) + // ⭐ 支持商店数据 return new NPC( $id, $data['name'], - $data['dialogue'] + $data['dialogue'], + $data['hasShop'] ?? false, + $data['shopInventory'] ?? null ); } } \ No newline at end of file diff --git a/src/Model/MapTile.php b/src/Model/MapTile.php index a0b0682..b49c4ba 100644 --- a/src/Model/MapTile.php +++ b/src/Model/MapTile.php @@ -10,7 +10,9 @@ class MapTile { public ?array $encounterPool; // null 或包含 {"enemyId": "ID", "weight": N} 的数组 public float $encounterChance; // 遇敌几率 (0.0 到 1.0) public array $npcIds; - public function __construct(string $id, string $name, string $description, array $connections, ?array $encounterPool, float $encounterChance,array $npcIds = []) { + public array $lootIds; // ⭐ 新增:宝箱/掉落物ID列表 + + public function __construct(string $id, string $name, string $description, array $connections, ?array $encounterPool, float $encounterChance, array $npcIds = [], array $lootIds = []) { $this->id = $id; $this->name = $name; $this->description = $description; @@ -19,5 +21,6 @@ class MapTile { $this->encounterPool = $encounterPool; $this->encounterChance = $encounterChance; $this->npcIds = $npcIds; // ⭐ 赋值 + $this->lootIds = $lootIds; // ⭐ 赋值 } } \ No newline at end of file diff --git a/src/Model/NPC.php b/src/Model/NPC.php index 5354da7..71094e7 100644 --- a/src/Model/NPC.php +++ b/src/Model/NPC.php @@ -8,11 +8,15 @@ class NPC { public string $id; public string $name; public array $dialogue; // 存储对话行或对话树结构 + public bool $hasShop; // ⭐ 是否有商店 + public ?array $shopInventory; // ⭐ 商店库存 - public function __construct(string $id, string $name, array $dialogue) { + public function __construct(string $id, string $name, array $dialogue, bool $hasShop = false, ?array $shopInventory = null) { $this->id = $id; $this->name = $name; $this->dialogue = $dialogue; + $this->hasShop = $hasShop; + $this->shopInventory = $shopInventory ?? []; } public function getName(): string { diff --git a/src/Model/Player.php b/src/Model/Player.php index 653d523..a4456db 100644 --- a/src/Model/Player.php +++ b/src/Model/Player.php @@ -110,14 +110,8 @@ class Player extends Character { public function updateQuestProgress(string $questId, int $count = 1): void { if (isset($this->activeQuests[$questId])) { $progress = &$this->activeQuests[$questId]; // 使用引用 - if (!$progress['isCompleted']) { - $progress['currentCount'] += $count; - if ($progress['currentCount'] >= $progress['targetCount']) { - $progress['currentCount'] = $progress['targetCount']; - $progress['isCompleted'] = true; - // 触发 QuestCompletedEventRequest - // 注意:实际的奖励和标记完成应在 QuestService 确认后进行 - } + if (!$progress->isCompleted()) { + $progress->incrementProgress($count); } } } diff --git a/src/Model/Quest.php b/src/Model/Quest.php index 61580d6..b564f30 100644 --- a/src/Model/Quest.php +++ b/src/Model/Quest.php @@ -45,6 +45,13 @@ class Quest { $this->currentCount = min($targetCount, $this->currentCount + $amount); } + /** + * ⭐ 简便方法:增加进度(兼容旧API) + */ + public function incrementProgress(int $amount = 1): void { + $this->incrementCurrentCount($amount); + } + /** * 检查任务是否完成 */ @@ -61,6 +68,11 @@ class Quest { public function getType(): string { return $this->type; } public function getTarget(): array { return $this->target; } public function getRewards(): array { return $this->rewards; } + + /** + * ⭐ 获取任务标题(兼容旧API) + */ + public function getTitle(): string { return $this->name; } /** * 获取当前进度 (用于存档) diff --git a/src/System/BattleService.php b/src/System/BattleService.php index b1e8569..ad38a47 100644 --- a/src/System/BattleService.php +++ b/src/System/BattleService.php @@ -248,24 +248,13 @@ class BattleService implements EventListenerInterface { * 结束战斗状态 */ private function endBattle(bool $isWin): void { + $enemy = $this->currentEnemy; $this->inBattle = false; $this->currentEnemy = null; - // 当战斗结束 $this->stateManager->setMode(StateManager::MODE_MAP); - $this->dispatcher->dispatch(new Event('ShowMenuEvent')); // 切换回探索菜单 - } - - /** - * 模拟从配置中加载敌人数据 - */ - private function loadEnemyData(int $id): array { - // 在实际项目中,这应该从数据库或 JSON 文件加载 - return match ($id) { - 1 => ['name' => '弱小的哥布林', 'health' => 20, 'attack' => 5, 'defense' => 1, 'xp' => 10], - 2 => ['name' => '愤怒的野猪', 'health' => 35, 'attack' => 8, 'defense' => 3, 'xp' => 25], - 3 => ['name' => '森林狼', 'health' => 40, 'attack' => 10, 'defense' => 5, 'xp' => 40], - default => ['name' => '未知生物', 'health' => 1, 'attack' => 1, 'defense' => 0, 'xp' => 1], - }; + + // ⭐ 触发战斗结束事件,用于自动保存 + $this->dispatcher->dispatch(new Event('BattleEndEvent', ['isWin' => $isWin,'enemyId' => $enemy->getId()])); } } \ No newline at end of file diff --git a/src/System/DialogueService.php b/src/System/DialogueService.php index a2630c4..e130d45 100644 --- a/src/System/DialogueService.php +++ b/src/System/DialogueService.php @@ -140,9 +140,6 @@ class DialogueService implements EventListenerInterface { // 恢复地图模式 (或者之前的模式,稍微简化处理) $this->stateManager->setMode(StateManager::MODE_MAP); - - // 触发菜单显示,让玩家知道回到了地图 - $this->dispatcher->dispatch(new Event('ShowMenuEvent')); } private function handleAction(string $actionString): void { diff --git a/src/System/InputHandler.php b/src/System/InputHandler.php index 3d967ca..74c5412 100644 --- a/src/System/InputHandler.php +++ b/src/System/InputHandler.php @@ -81,6 +81,9 @@ class InputHandler { case 'I': // 状态 (Status) $this->dispatcher->dispatch(new Event('ShowStatsRequest')); break; + case 'J': // 任务 (Journal/Quest) + $this->dispatcher->dispatch(new Event('ShowQuestListRequest')); + break; case 'L': // 保存 (Save/Load) if ($this->saveLoadService) { $this->saveLoadService->saveGame(); @@ -137,19 +140,121 @@ class InputHandler { $this->stateManager->setMode(StateManager::MODE_MAP); return true; } - // ... 处理物品使用逻辑 ... + + // ⭐ 处理物品使用逻辑 + if (is_numeric($input)) { + $itemIndex = (int)$input; + $player = $this->stateManager->getPlayer(); + $inventory = $player->getInventory(); + + if (!isset($inventory[$itemIndex])) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 编号为 {$itemIndex} 的物品不存在。"])); + return true; + } + + $item = $inventory[$itemIndex]; + + // 根据物品类型决定操作 + if ($item->type === 'potion') { + // 药水类:使用 + $this->dispatcher->dispatch(new Event('UseItemEvent', ['itemIndex' => $itemIndex])); + } elseif ($item->slot) { + // 装备类:装备 + $this->dispatcher->dispatch(new Event('EquipItemEvent', ['itemIndex' => $itemIndex])); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 该物品无法被使用或装备。"])); + } + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 请输入有效的物品编号。'])); + } + return true; } /** 💬 对话逻辑:选项分支 */ private function handleDialogueInput(): bool { - $input = $this->ask("对话选择 (数字)> "); - - if (strtoupper($input) === 'X') { - $this->stateManager->setMode(StateManager::MODE_MAP); + // ⭐ 检查是否有待处理的商店NPC + $pendingShopNpc = $this->stateManager->getPendingShopNpc(); + if ($pendingShopNpc) { + $input = $this->ask("选择 (S/X)> "); + + if ($input === 'S') { + // 打开商店 + $this->stateManager->clearPendingShopNpc(); + $this->dispatcher->dispatch(new Event('OpenShopEvent', [ + 'npc' => $pendingShopNpc + ])); + } elseif ($input === 'X') { + // 离开 + $this->stateManager->clearPendingShopNpc(); + $this->stateManager->setMode(StateManager::MODE_MAP); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '再见!'])); + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '请输入 S 或 X。'])); + } return true; } - $this->dispatcher->dispatch(new Event('DialogueChoice', ['choice' => $input])); + + // ⭐ 检查是否有待交付的任务 + $pendingQuestTurnIn = $this->stateManager->getPendingQuestTurnIn(); + if ($pendingQuestTurnIn) { + $input = $this->ask("选择 (Y/N)> "); + + if ($input === 'Y') { + // 确认交任务 + $this->dispatcher->dispatch(new Event('QuestTurnInConfirm', ['questId' => $pendingQuestTurnIn])); + $this->stateManager->clearPendingQuestTurnIn(); + $this->stateManager->setMode(StateManager::MODE_MAP); + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + } elseif ($input === 'N') { + // 取消交任务 + $this->stateManager->clearPendingQuestTurnIn(); + $this->stateManager->setMode(StateManager::MODE_MAP); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '已取消任务交付。'])); + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '请输入 Y 或 N。'])); + } + return true; + } + + // ⭐ 检查是否有待选择的NPC列表 + $pendingNpcs = $this->stateManager->getPendingNpcSelection(); + + if (!empty($pendingNpcs)) { + // ⭐ 处理NPC选择 + $input = $this->ask("选择NPC (数字或 X 退出)> "); + + if (strtoupper($input) === 'X') { + $this->stateManager->clearPendingNpcSelection(); + $this->stateManager->setMode(StateManager::MODE_MAP); + return true; + } + + if (is_numeric($input)) { + $index = (int)$input; + if (isset($pendingNpcs[$index])) { + $npcId = $pendingNpcs[$index]; + $this->stateManager->clearPendingNpcSelection(); + // ⭐ 触发与选中NPC的交互 + $this->dispatcher->dispatch(new Event('StartInteractionEvent', ['npcId' => $npcId])); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的选择。'])); + } + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '请输入数字选择NPC。'])); + } + } else { + // ⭐ 处理任务对话选项 + $input = $this->ask("对话选择 (X 离开)> "); + + if (strtoupper($input) === 'X') { + $this->stateManager->setMode(StateManager::MODE_MAP); + return true; + } + $this->dispatcher->dispatch(new Event('DialogueChoice', ['choice' => $input])); + } return true; } diff --git a/src/System/InteractionSystem.php b/src/System/InteractionSystem.php index 512bb56..8372b58 100644 --- a/src/System/InteractionSystem.php +++ b/src/System/InteractionSystem.php @@ -51,7 +51,6 @@ class InteractionSystem implements EventListenerInterface { public function handleEvent(Event $event): void { switch ($event->getType()) { case 'AttemptInteractEvent': - // 假设 MapSystem 会提供当前 Tile 上的 NPC ID $npcId = $event->getPayload()['npcId']; $this->startInteraction($npcId); break; @@ -72,6 +71,8 @@ class InteractionSystem implements EventListenerInterface { if (!$npc) { $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里没有人可以交谈 ({$npcId} 不存在)。"])); + // ⭐ 切换回地图模式 + $this->stateManager->setMode(StateManager::MODE_MAP); return; } @@ -79,64 +80,38 @@ class InteractionSystem implements EventListenerInterface { 'message' => "👤 你走近了 {$npc->getName()}。" ])); - // 启动对话流程 - $this->dialogueLoop($npc); - - // 交互结束后,重新打印主菜单请求 - $this->dispatcher->dispatch(new Event('ShowMenuEvent')); - } - - /** - * 2. 核心对话循环 - */ - /** - * 2. 核心交互循环 - */ - private function dialogueLoop(NPC $npc): void { - $running = true; - - while ($running) { - // 检查交互类型并获取玩家选择 - $choice = $this->promptPlayerChoice($npc); - - switch ($choice) { - case 'T': // 交谈 (可能触发任务) - if ($this->handleTalk($npc)) { - // 如果触发了对话系统,结束当前的 Interaction Loop,交由 DialogueMode 接管 - $running = false; - } - break; - case 'S': // 触发商店 - $this->dispatcher->dispatch(new Event('OpenShopEvent', ['npcId' => $npc->id])); - break; - case 'E': // 结束对话 - $running = false; - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "🤝 你结束了与 {$npc->getName()} 的对话。"])); - break; - default: - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的交互指令。'])); + // ⭐ 直接尝试触发任务对话 + $player = $this->stateManager->getPlayer(); + $questIds = $this->questRepository->getQuestsByNpc($npc->id); + $foundQuest = false; + + // ⭐ 优先检查是否有已完成的任务可以提交 + foreach ($questIds as $questId) { + $activeQuests = $player->getActiveQuests(); + if (isset($activeQuests[$questId]) && $activeQuests[$questId]->isCompleted()) { + // 找到已完成的任务,提示玩家交任务 + $this->dispatcher->dispatch(new Event('QuestTurnInPrompt', [ + 'questId' => $questId, + 'npcId' => $npc->id + ])); + $foundQuest = true; + break; } } - } - - 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) { + 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']); + $foundQuest = true; + break; + } } } } @@ -145,46 +120,25 @@ class InteractionSystem implements EventListenerInterface { // 如果没有新任务,显示 NPC 默认闲聊 $defaultMsg = is_array($npc->dialogue) ? ($npc->dialogue['greeting'] ?? '...') : '...'; $this->dispatcher->dispatch(new Event('SystemMessage', [ - 'message' => "{$npc->getName()}:{$defaultMsg}" + 'message' => "{$npc->getName()}{$defaultMsg}" ])); + + // ⭐ 检查NPC是否有商店 + if ($npc->hasShop) { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "\n🛍️ {$npc->getName()} 还经营着一家商店。" + ])); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "输入 [S] 打开商店,[X] 离开" + ])); + + // 设置待处理的商店NPC + $this->stateManager->setPendingShopNpc($npc); + } else { + // ⭐ 没有任务也没有商店时切换回地图模式 + $this->stateManager->setMode(StateManager::MODE_MAP); + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + } } - - return false; - } - - /** - * 获取玩家交互指令 - */ - private function promptPlayerChoice(NPC $npc): string { - $this->dispatcher->dispatch(new Event('SystemMessage', [ - 'message' => "\n--- 交互菜单 --- [T] 任务 | [S] 商店 | [E] 结束" - ])); - - $question = new Question("> 请选择指令 (T/S/E):"); - $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); - - return $choice; - } - - /** - * 模拟从配置中加载 NPC 数据 - */ - private function loadNPC(string $id): ?NPC { - $data = match ($id) { - 'VILLAGER_1' => [ - 'name' => '老村长', - 'dialogue' => [ - 'greeting' => '你好,旅行者。你看起来很强大。', - 'quest_response' => '你想要帮忙吗?我们的地窖里有老鼠。', - 'shop_response' => '我现在没有东西卖给你。', - ] - ], - default => null, - }; - - if ($data) { - return new NPC($id, $data['name'], $data['dialogue']); - } - return null; } } \ No newline at end of file diff --git a/src/System/MapSystem.php b/src/System/MapSystem.php index c3e1d67..d8580c5 100644 --- a/src/System/MapSystem.php +++ b/src/System/MapSystem.php @@ -82,14 +82,14 @@ class MapSystem implements EventListenerInterface { $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里寂静无声,没有人可以交谈。"])); return; } - - // ⭐ 切换到对话模式 - $this->stateManager->setMode(StateManager::MODE_DIALOGUE); - if (count($npcIds) === 1) { + // ⭐ 只有一个NPC,直接交互 + $this->stateManager->setMode(StateManager::MODE_DIALOGUE); $this->dispatcher->dispatch(new Event('StartInteractionEvent', ['npcId' => $npcIds[0]])); } else { - // 多个 NPC 的处理逻辑... + // ⭐ 多个 NPC,切换到对话模式并存储待选择列表 + $this->stateManager->setMode(StateManager::MODE_DIALOGUE); + $this->stateManager->setPendingNpcSelection($npcIds); $this->listNpcsForSelection($npcIds); } } @@ -147,13 +147,13 @@ class MapSystem implements EventListenerInterface { } private function listNpcsForSelection(array $npcIds): void { - $output = "👥 这里的居民:\n"; + $output = "👥 请选择要交谈的对象:\n"; foreach ($npcIds as $index => $id) { $npcData = $this->npcRepository->find($id); $name = $npcData['name'] ?? "神秘人"; - $output .= " [" . ($index + 1) . "] {$name}\n"; + $output .= " [" . ($index) . "] {$name}\n"; } + $output .= " [X] 取消"; $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => $output])); - $this->dispatcher->dispatch(new Event('AwaitingNpcSelection', ['options' => $npcIds])); } } \ No newline at end of file diff --git a/src/System/QuestService.php b/src/System/QuestService.php index 5cec2cb..d1ae97a 100644 --- a/src/System/QuestService.php +++ b/src/System/QuestService.php @@ -32,57 +32,27 @@ class QuestService implements EventListenerInterface { case 'GameStartEvent': $this->initializeQuests(); // 游戏开始时检查是否有初始任务 break; - case 'QuestCheckEvent': // 响应 InteractionSystem 的任务请求 - $this->handleQuestCheck($event->getPayload()['npcId']); - break; case 'BattleEndEvent': // 响应战斗结束,检查击杀目标 - $this->checkKillQuests($event->getPayload()['enemyId']); + $payload = $event->getPayload(); + if (isset($payload['enemyId'])) { + $this->checkKillQuests($payload['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']); + $this->checkSystemTriggers('LEVEL_UP', (string)$event->getPayload()['newLevel']); break; case 'QuestAcceptRequest': // ⭐ 响应对话中的接受任务请求 $this->startQuest($event->getPayload()['questId']); break; + case 'QuestTurnInConfirm': // ⭐ 交付任务确认 + $this->turnInQuest($event->getPayload()['questId']); + break; } } - /** - * 1. 处理 NPC 交互时的任务检查/接受 - */ - private function handleQuestCheck(string $npcId): void { - $player = $this->stateManager->getPlayer(); - $questId = $this->getQuestIdForNpc($npcId); - - if (!$questId) return; - - if ($player->isQuestCompleted($questId)) { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "(村长)我已经没什么可以教你的了,旅行者。"])); - } elseif (isset($player->getActiveQuests()[$questId])) { - $this->checkQuestCompletion($questId); // 检查是否可以提交 - } else { - // 接受任务 - $this->acceptQuest($questId); - } - } - - /** - * 2. 接受任务逻辑 - */ - private function acceptQuest(string $questId): void { - $player = $this->stateManager->getPlayer(); - $questConfig = $this->questData[$questId]; - - $player->addActiveQuest($questId, $questConfig['target']['count']); - - $this->dispatcher->dispatch(new Event('SystemMessage', [ - 'message' => "📜 接受任务:{$questConfig['name']} - 目标:{$questConfig['description']}" - ])); - } - /** * 3. 检查击杀类任务进度 */ @@ -91,17 +61,16 @@ class QuestService implements EventListenerInterface { $activeQuests = $player->getActiveQuests(); foreach ($activeQuests as $questId => $progress) { - $questConfig = $this->questData[$questId]; - - if ($questConfig['type'] === 'kill' && $questConfig['target']['targetId'] == $killedEnemyId) { + $questConfig = $this->questRepository->find($questId); + if ($questConfig['type'] === 'kill' && $questConfig['target']['entityId'] == $killedEnemyId) { // 更新玩家任务进度 $player->updateQuestProgress($questId, 1); $this->dispatcher->dispatch(new Event('SystemMessage', [ - 'message' => "任务进度更新:[{$questConfig['name']}] {$player->getActiveQuests()[$questId]['currentCount']}/{$questConfig['target']['count']}" + 'message' => "任务进度更新:[{$questConfig['name']}] {$player->getActiveQuests()[$questId]->getCurrentCount()}/{$questConfig['target']['count']}" ])); // 如果任务完成,触发 QuestCompletedEventRequest - if ($player->getActiveQuests()[$questId]['isCompleted']) { + if ($player->getActiveQuests()[$questId]->isCompleted()) { $this->dispatcher->dispatch(new Event('SystemMessage', [ 'message' => "任务 [{$questConfig['name']}] 已完成!请回去找NPC提交。" ])); @@ -187,11 +156,68 @@ class QuestService implements EventListenerInterface { */ public function initializeQuests(): void { $player = $this->stateManager->getPlayer(); - if (empty($player->getActiveQuests()) && empty($player->getCompletedQuests())) { - if ($startingQuestId) { - $this->startQuest($startingQuestId); - } + +// if (empty($player->getActiveQuests()) && empty($player->getCompletedQuests())) { +// if ($startingQuestId) { +// $this->startQuest($startingQuestId); +// } +// } + } + + /** + * ⭐ 交付任务 + */ + private function turnInQuest(string $questId): void { + $player = $this->stateManager->getPlayer(); + $activeQuests = $player->getActiveQuests(); + + if (!isset($activeQuests[$questId])) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '任务不存在或已完成。'])); + return; } + + $quest = $activeQuests[$questId]; + if (!$quest->isCompleted()) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '任务还未完成,无法交付。'])); + return; + } + + $questData = $this->questRepository->find($questId); + if (!$questData) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '任务数据出错。'])); + return; + } + + // 发放奖励 + $rewards = $questData['rewards'] ?? []; + + // 经验值奖励 + if (isset($rewards['xp'])) { + $player->gainXp($rewards['xp']); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => " ✅ 获得 {$rewards['xp']} 经验值" + ])); + } + + // 金币奖励 + if (isset($rewards['gold'])) { + $player->gainGold($rewards['gold']); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => " ✅ 获得 {$rewards['gold']} 金币" + ])); + } + + // 物品奖励 + if (isset($rewards['itemId'])) { + $this->dispatcher->dispatch(new Event('LootFoundEvent', ['lootId' => $rewards['itemId']])); + } + + // 标记任务完成 + $player->markQuestCompleted($questId); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "\n🎉 任务已交付! 感谢你的帮助!" + ])); } /** diff --git a/src/System/SaveLoadService.php b/src/System/SaveLoadService.php index c9a8da7..62f08cf 100644 --- a/src/System/SaveLoadService.php +++ b/src/System/SaveLoadService.php @@ -5,12 +5,13 @@ use Game\Model\Item; use Game\Model\Player; use Game\Event\Event; use Game\Event\EventDispatcher; +use Game\Event\EventListenerInterface; use Game\Model\Quest; /** * SaveLoadService: 负责将玩家状态持久化,并支持装备与地图位置的还原。 */ -class SaveLoadService { +class SaveLoadService implements EventListenerInterface { private EventDispatcher $dispatcher; private StateManager $stateManager; @@ -26,6 +27,77 @@ class SaveLoadService { } } + /** + * 处理事件 - 在关键时刻自动保存 + */ + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'BattleEndEvent': // 战斗结束后自动保存 + case 'LevelUpEvent': // 升级后自动保存 + case 'QuestAcceptRequest': // 接受任务后自动保存 + case 'MapMoveEvent': // 移动后自动保存(静默) + case 'UseItemEvent': // 使用物品后 + case 'EquipItemEvent': // 穿装备后 + case 'UnequipItemEvent': // 卸下装备后 + $this->autoSave(); + break; + } + } + + /** + * 自动保存(静默,不显示消息) + */ + private function autoSave(): void { + try { + $player = $this->stateManager->getPlayer(); + $currentTile = $this->stateManager->getCurrentTile(); + + if (!$player) return; + + // 1. 序列化背包 + $serializedInventory = array_map(fn(Item $item) => $this->itemToData($item), $player->getInventory()); + + // 2. 序列化装备 + $serializedEquipment = []; + foreach ($player->getEquipment() as $slot => $item) { + $serializedEquipment[$slot] = $item ? $this->itemToData($item) : null; + } + + // 3. 序列化任务 + $activeQuests = []; + foreach ($player->getActiveQuests() as $quest) { + $activeQuests[] = [ + 'config' => $quest->toArray(), + 'currentCount' => $quest->getCurrentCount() + ]; + } + + $data = [ + 'player' => [ + 'name' => $player->getName(), + 'health' => $player->getHealth(), + 'maxHealth' => $player->getMaxHealth(), + 'base_attack' => $player->attack, + 'base_defense' => $player->defense, + 'level' => $player->getLevel(), + 'currentXp' => $player->getCurrentXp(), + 'gold' => $player->getGold(), + 'inventory' => $serializedInventory, + 'equipment' => $serializedEquipment, + 'activeQuests' => $activeQuests, + 'completedQuests' => $player->getCompletedQuests(), + ], + 'world' => [ + 'currentTileId' => $currentTile->id + ] + ]; + file_put_contents($this->savePath, json_encode($data, JSON_PRETTY_PRINT)); + } catch (\Exception $e) { +// throw $e; + // 静默失败,不打扰玩家 + } + } + public function hasSaveFile(): bool { return file_exists($this->savePath); } diff --git a/src/System/ShopService.php b/src/System/ShopService.php index 12c1382..72af903 100644 --- a/src/System/ShopService.php +++ b/src/System/ShopService.php @@ -6,6 +6,8 @@ use Game\Event\EventListenerInterface; use Game\Event\EventDispatcher; use Game\Model\Item; use Game\Model\Player; +use Game\Model\NPC; // ⭐ 新增 +use Game\Database\ItemRepository; // ⭐ 新增 use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Helper\QuestionHelper; @@ -21,16 +23,26 @@ class ShopService implements EventListenerInterface { private InputInterface $input; private OutputInterface $output; private QuestionHelper $helper; + private ItemRepository $itemRepository; // ⭐ 新增 - // 商店的固定库存(通常从配置加载) + // 商店的固定库存(通常从DPC配置加载) private array $shopInventory; + private ?NPC $currentShopNpc = null; // ⭐ 当前商店NPC - public function __construct(EventDispatcher $dispatcher, StateManager $stateManager, InputInterface $input, OutputInterface $output, QuestionHelper $helper) { + public function __construct( + EventDispatcher $dispatcher, + StateManager $stateManager, + InputInterface $input, + OutputInterface $output, + QuestionHelper $helper, + ItemRepository $itemRepository // ⭐ 新增 + ) { $this->dispatcher = $dispatcher; $this->stateManager = $stateManager; $this->input = $input; $this->output = $output; $this->helper = $helper; + $this->itemRepository = $itemRepository; // ⭐ 赋值 $this->loadShopInventory(); } @@ -46,7 +58,15 @@ class ShopService implements EventListenerInterface { public function handleEvent(Event $event): void { switch ($event->getType()) { case 'OpenShopEvent': // 响应 InteractionSystem 的请求 - $this->startShopping(); + $payload = $event->getPayload(); + $npc = $payload['npc'] ?? null; + if ($npc && $npc->hasShop) { + $this->currentShopNpc = $npc; + $this->shopInventory = $npc->shopInventory; + $this->startShopping(); + } else { + $this->startShopping(); // 使用默认库存 + } break; } } @@ -55,7 +75,8 @@ class ShopService implements EventListenerInterface { * 启动商店界面和循环 */ private function startShopping(): void { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "\n欢迎光临!看看我有什么好东西。"])); + $npcName = $this->currentShopNpc ? $this->currentShopNpc->getName() : '商人'; + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "\n🛍️ 欢迎光临 {$npcName} 的商店!"])); $running = true; while ($running) { @@ -75,6 +96,9 @@ class ShopService implements EventListenerInterface { case 'X': $running = false; $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '下次再来!'])); + // ⭐ 返回地图模式 + $this->stateManager->setMode(StateManager::MODE_MAP); + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); break; default: $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的指令。'])); @@ -165,17 +189,31 @@ class ShopService implements EventListenerInterface { } private function displaySaleItems(): void { - $this->output->writeln("\n--- 🛒 商店出售 ---"); - // 模拟获取 Item 数据的服务(实际应通过 ItemService/DB) - $itemsData = [ - 1 => ['name' => '小型治疗药水', 'value' => 10, 'type' => 'potion'], - 3 => ['name' => '高级治疗药水', 'value' => 200, 'type' => 'potion'], // 假设 ID 3 - ]; - + $this->output->writeln("\n--- 🛍️ 商店出售 ---"); + + if (empty($this->shopInventory)) { + $this->output->writeln(" 商店没有货物。"); + $this->output->writeln("--------------------------"); + return; + } + foreach ($this->shopInventory as $itemId => $data) { - $name = $itemsData[$itemId]['name'] ?? "未知物品"; - $price = $data['price']; - $this->output->writeln("[{$itemId}] {$name} | 价格: {$price} 💰"); + // ⭐ 从 ItemRepository 获取真实数据 + $itemData = $this->itemRepository->find($itemId); + if ($itemData) { + $name = $itemData['name'] ?? "未知物品"; + $price = $data['price'] ?? $itemData['value'] ?? 0; + $type = $itemData['type'] ?? ''; + + $typeLabel = match($type) { + 'potion' => '药水', + 'weapon' => '武器', + 'armor' => '护甲', + default => $type + }; + + $this->output->writeln("[{$itemId}] {$name} {$typeLabel} | 价格: {$price} 💰"); + } } $this->output->writeln("--------------------------"); } diff --git a/src/System/StateManager.php b/src/System/StateManager.php index b420158..dc5ff8e 100644 --- a/src/System/StateManager.php +++ b/src/System/StateManager.php @@ -22,6 +22,13 @@ class StateManager { private string $currentMode = self::MODE_MAP; private MapRepository $mapRepository; + + // ⭐ 新增:存储待选择的NPC列表 + private array $pendingNpcSelection = []; + // ⭐ 新增:待交付的任务ID + private ?string $pendingQuestTurnIn = null; + // ⭐ 新增:待处理的商店NPC + private $pendingShopNpc = null; public function setMode(string $mode): void { $this->currentMode = $mode; @@ -31,6 +38,46 @@ class StateManager { public function getMode(): string { return $this->currentMode; } + + // ⭐ 新增:NPC选择相关方法 + public function setPendingNpcSelection(array $npcIds): void { + $this->pendingNpcSelection = $npcIds; + } + + public function getPendingNpcSelection(): array { + return $this->pendingNpcSelection; + } + + public function clearPendingNpcSelection(): void { + $this->pendingNpcSelection = []; + } + + // ⭐ 交任务状态管理 + public function setPendingQuestTurnIn(?string $questId): void { + $this->pendingQuestTurnIn = $questId; + } + + public function getPendingQuestTurnIn(): ?string { + return $this->pendingQuestTurnIn; + } + + public function clearPendingQuestTurnIn(): void { + $this->pendingQuestTurnIn = null; + } + + // ⭐ 商店NPC状态管理 + public function setPendingShopNpc($npc): void { + $this->pendingShopNpc = $npc; + } + + public function getPendingShopNpc() { + return $this->pendingShopNpc; + } + + public function clearPendingShopNpc(): void { + $this->pendingShopNpc = null; + } + public function __construct(Connection $db,MapRepository $mapRepository) { $this->db = $db; $this->mapRepository = $mapRepository; diff --git a/src/System/UIService.php b/src/System/UIService.php index 5388266..eac3236 100644 --- a/src/System/UIService.php +++ b/src/System/UIService.php @@ -6,6 +6,7 @@ use Game\Event\EventListenerInterface; use Game\Model\Player; use Game\Model\Enemy; use Game\Model\MapTile; +use Game\Database\QuestRepository; // ⭐ 新增 use Symfony\Component\Console\Output\OutputInterface; /** @@ -15,10 +16,12 @@ class UIService implements EventListenerInterface { private OutputInterface $output; private StateManager $stateManager; + private QuestRepository $questRepository; // ⭐ 新增 - public function __construct(OutputInterface $output, StateManager $stateManager) { + public function __construct(OutputInterface $output, StateManager $stateManager, QuestRepository $questRepository) { $this->output = $output; $this->stateManager = $stateManager; + $this->questRepository = $questRepository; // ⭐ 赋值 } public function handleEvent(Event $event): void { @@ -27,6 +30,7 @@ class UIService implements EventListenerInterface { case 'ShowMenuEvent': // 👈 监听这个信号 // 根据模式渲染不同的 UI 底部 if ($mode === StateManager::MODE_MAP) { + $this->displayLocation($this->stateManager->getCurrentTile()); $this->displayMapMenu(); } elseif ($mode === StateManager::MODE_BATTLE) { $this->displayBattleMenu(); @@ -35,12 +39,18 @@ class UIService implements EventListenerInterface { case 'ShowStatsRequest': // $this->displayPlayerStats(); // 👈 真正调用显示逻辑 break; + case 'ShowQuestListRequest': // ⭐ 新增:显示任务列表 + $this->displayQuestList(); + break; + case 'QuestTurnInPrompt': // ⭐ 任务交付提示 + $this->displayQuestTurnInPrompt($event->getPayload()); + break; case 'SystemMessage': $this->output->writeln("📣 {$event->getPayload()['message']}"); break; case 'MapMoveEvent': - $this->refreshScreen(); // 刷新整个视野 +// $this->refreshScreen(); // 刷新整个视野 break; case 'StatUpdateEvent': @@ -67,43 +77,14 @@ class UIService implements EventListenerInterface { * 🗺️ 地图模式菜单:强调移动和探索 */ private function displayMapMenu(): void { - $this->output->writeln("\n 🌍 探索模式指令 "); - - // 移动指令 - $this->output->writeln(" W/A/S/D : 移动角色"); - - // 交互与查看 - $this->output->writeln(sprintf( - " %-5s : %-15s | %-5s : %-15s", - "E", "探索区域", "T", "与 NPC 交谈" - )); - - // 系统指令 - $this->output->writeln(sprintf( - " %-5s : %-15s | %-5s : %-15s", - "B", "打开背包(Inv)", "I", "查看详细状态" - )); - - $this->output->writeln(sprintf( - " %-5s : %-15s | %-5s : %-15s", - "L", "保存进度", "Q", "退出游戏" - )); - $this->output->writeln("" . str_repeat("-", 50) . ""); + $this->output->writeln("\n[W/A/S/D]移动 [E]探索 [T]交谈 [B]背包 [I]状态 [J]任务 [L]保存 [Q]退出"); } /** * ⚔️ 战斗模式菜单:强调数字键操作 */ private function displayBattleMenu(): void { - $this->output->writeln("\n ⚔️ 战斗模式指令 (输入数字) "); - - // 战斗选项 - $this->output->writeln(" [1] 普通攻击 - 对敌人造成基础伤害"); - $this->output->writeln(" [2] 技能攻击 - 消耗魔法值(MP)释放技能"); - $this->output->writeln(" [3] 使用物品 - 恢复生命值或其他效果"); - $this->output->writeln(" [4] 尝试逃跑 - 概率返回上一个地图格"); - - $this->output->writeln("" . str_repeat("~", 50) . ""); + $this->output->writeln("\n[1]攻击 [2]技能 [3]物品 [4]逃跑"); } /** @@ -220,19 +201,32 @@ class UIService implements EventListenerInterface { * 打印当前地图区域信息 (增强版) */ private function displayLocation(MapTile $tile): void { - $this->output->writeln("\n 📍 区域:{$tile->name} "); + $this->output->writeln("\n 📍 {$tile->name} "); $this->output->writeln(" {$tile->description}"); - // 显示出口 - $moveOptions = []; + // 显示出口 - 使用方向箭头表示 + $directionMap = [ + 'N' => ['arrow' => '↑', 'label' => '北'], + 'S' => ['arrow' => '↓', 'label' => '南'], + 'E' => ['arrow' => '→', 'label' => '东'], + 'W' => ['arrow' => '←', 'label' => '西'] + ]; + + $moveDisplay = []; foreach ($tile->connections as $dir => $targetId) { - $moveOptions[] = "{$dir} ({$targetId})"; + if (isset($directionMap[$dir])) { + $arrow = $directionMap[$dir]['arrow']; + $moveDisplay[] = "{$arrow} {$directionMap[$dir]['label']}"; + } + } + + if (!empty($moveDisplay)) { + $this->output->writeln(" 🚪 " . implode(' ', $moveDisplay)); } - $this->output->writeln(" 🚪 出路: " . implode(' | ', $moveOptions)); // 如果有 NPC,也显示出来 if (!empty($tile->npcIds)) { - $this->output->writeln(" 👥 附近的人: " . implode(', ', $tile->npcIds) . ""); + $this->output->writeln(" 👥 " . implode(', ', $tile->npcIds)); } } @@ -244,7 +238,7 @@ class UIService implements EventListenerInterface { $inventory = $player->getInventory(); $this->output->writeln("\n🎒 个人背包内容"); - $this->output->writeln("" . str_repeat("=", 30) . ""); + $this->output->writeln("" . str_repeat("=", 40) . ""); if (empty($inventory)) { $this->output->writeln(" (空空如也)"); @@ -253,12 +247,42 @@ class UIService implements EventListenerInterface { // 简化效果显示 $effectStr = ""; if (!empty($item->effects)) { - $effectStr = " | 效果: " . json_encode($item->effects) . ""; + $parts = []; + foreach ($item->effects as $key => $value) { + $parts[] = "{$key}+{$value}"; + } + $effectStr = " " . implode(', ', $parts) . ""; } - $this->output->writeln(sprintf(" [%d] %s (%s)%s", $index, $item->name, $item->type, $effectStr)); + + // 显示装备属性加成 + $statStr = ""; + if (!empty($item->statModifiers)) { + $parts = []; + foreach ($item->statModifiers as $key => $value) { + $parts[] = "{$key}+{$value}"; + } + $statStr = " " . implode(', ', $parts) . ""; + } + + $typeLabel = match($item->type) { + 'potion' => '药水', + 'weapon' => '武器', + 'armor' => '护甲', + default => $item->type + }; + + $this->output->writeln(sprintf( + " [%d] %s %s%s%s", + $index, + $item->name, + $typeLabel, + $effectStr, + $statStr + )); } + $this->output->writeln("\n提示:输入编号使用/装备物品,输入 X 退出"); } - $this->output->writeln("" . str_repeat("=", 30) . "\n"); + $this->output->writeln("" . str_repeat("=", 40) . ""); } private function displayMainMenu(): void { @@ -267,4 +291,101 @@ class UIService implements EventListenerInterface { $this->output->writeln(" 角色: I (状态) | B (背包) | L (保存)"); $this->output->writeln(" 提示: 输入 'USE 0' 可直接使用背包第一格物品"); } + + /** + * ⭐ 显示任务交付提示 + */ + private function displayQuestTurnInPrompt(array $payload): void { + $questId = $payload['questId']; + $player = $this->stateManager->getPlayer(); + $quest = $player->getActiveQuests()[$questId]; + $questData = $this->questRepository->find($questId); + + $this->output->writeln("\n🎉 任务完成!"); + $this->output->writeln("" . str_repeat("=", 50) . ""); + $this->output->writeln(" {$quest->getTitle()}"); + $this->output->writeln(" {$quest->getDescription()}"); + $this->output->writeln(""); + + // 显示奖励 + $rewards = $questData['rewards'] ?? []; + if (!empty($rewards)) { + $this->output->writeln(" 奖励:"); + if (isset($rewards['xp'])) { + $this->output->writeln(" • 经验值: +{$rewards['xp']}"); + } + if (isset($rewards['gold'])) { + $this->output->writeln(" • 金币: +{$rewards['gold']}"); + } + if (isset($rewards['itemId'])) { + $this->output->writeln(" • 物品: {$rewards['itemId']}"); + } + } + + $this->output->writeln(""); + $this->output->writeln("" . str_repeat("=", 50) . ""); + $this->output->writeln("输入 [Y] 交付任务,[N] 取消"); + + // 设置状态,等待玩家输入 + $this->stateManager->setMode(StateManager::MODE_DIALOGUE); + $this->stateManager->setPendingQuestTurnIn($questId); + } + + /** + * ⭐ 显示任务列表 + */ + private function displayQuestList(): void { + $player = $this->stateManager->getPlayer(); + $activeQuests = $player->getActiveQuests(); + $completedQuests = $player->getCompletedQuests(); + + $this->output->writeln("\n📖 任务日志"); + $this->output->writeln("" . str_repeat("=", 60) . ""); + + // 显示进行中的任务 + $this->output->writeln("\n进行中的任务"); + if (empty($activeQuests)) { + $this->output->writeln(" 暂无任务"); + } else { + foreach ($activeQuests as $quest) { + $progress = ""; + if ($quest->getType() === 'kill' || $quest->getType() === 'collect') { + $current = $quest->getCurrentCount(); + $targetData = $quest->getTarget(); + $target = $targetData['count'] ?? 1; // ⭐ target是数组,需要取count + $percent = $target > 0 ? (int)(($current / $target) * 100) : 0; + $progress = sprintf(" [%d/%d - %d%%]", $current, $target, $percent); + } + + $this->output->writeln(sprintf( + " • %s %s", + $quest->getTitle(), + $progress + )); + $this->output->writeln(sprintf(" %s", $quest->getDescription())); + } + } + + // 显示已完成的任务 + $this->output->writeln("\n已完成的任务"); + if (empty($completedQuests)) { + $this->output->writeln(" 暂无完成任务"); + } else { + $count = 0; + foreach ($completedQuests as $questId) { + $this->output->writeln(" ✓ {$questId}"); + $count++; + if ($count >= 10) { + $remaining = count($completedQuests) - 10; + if ($remaining > 0) { + $this->output->writeln(" ... 还有 {$remaining} 个已完成任务"); + } + break; + } + } + } + + $this->output->writeln("\n" . str_repeat("=", 60) . ""); + $this->output->writeln("统计:进行中 " . count($activeQuests) . " | 已完成 " . count($completedQuests) . ""); + } } \ No newline at end of file diff --git a/tests/test_autosave.php b/tests/test_autosave.php new file mode 100644 index 0000000..18dd13e --- /dev/null +++ b/tests/test_autosave.php @@ -0,0 +1,63 @@ + new QuestionHelper()]); + +$container = new ServiceContainer($input, $output, $helperSet); +$dispatcher = $container->registerServices(); +$stateManager = $container->getStateManager(); + +// 创建测试玩家 +$player = new \Game\Model\Player("AutoSaveTest", 100, 10, 5); +$stateManager->setPlayer($player); +$stateManager->setCurrentTileId('TOWN_01'); + +echo "=== 自动保存功能测试 ===\n\n"; + +// 删除旧存档 +$savePath = __DIR__ . '/../save/player.json'; +if (file_exists($savePath)) { + unlink($savePath); + echo "✓ 已清理旧存档\n"; +} + +echo "\n1. 测试地图移动触发自动保存...\n"; +$dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => 'FOREST_01'])); +if (file_exists($savePath)) { + echo "✓ 地图移动后自动保存成功\n"; + $data = json_decode(file_get_contents($savePath), true); + echo " - 当前位置: " . $data['world']['currentTileId'] . "\n"; +} else { + echo "✗ 自动保存失败\n"; +} + +echo "\n2. 测试升级触发自动保存...\n"; +$player->gainXp(80); +$dispatcher->dispatch(new Event('LevelUpEvent', ['newLevel' => 2])); +if (file_exists($savePath)) { + $data = json_decode(file_get_contents($savePath), true); + echo "✓ 升级后自动保存成功\n"; + echo " - 当前等级: " . $data['player']['level'] . "\n"; + echo " - 当前经验: " . $data['player']['currentXp'] . "\n"; +} else { + echo "✗ 自动保存失败\n"; +} + +echo "\n3. 测试任务接受触发自动保存...\n"; +$dispatcher->dispatch(new Event('QuestAcceptRequest', ['questId' => 'KILL_GOBLIN'])); +if (file_exists($savePath)) { + echo "✓ 任务接受后自动保存成功\n"; +} else { + echo "✗ 自动保存失败\n"; +} + +echo "\n=== 测试完成 ===\n"; diff --git a/tests/test_inventory.php b/tests/test_inventory.php new file mode 100644 index 0000000..fc3ffe9 --- /dev/null +++ b/tests/test_inventory.php @@ -0,0 +1,58 @@ + new QuestionHelper()]); + +$container = new ServiceContainer($input, $output, $helperSet); +$dispatcher = $container->registerServices(); +$stateManager = $container->getStateManager(); + +// 创建测试玩家 +$player = new \Game\Model\Player("ItemTest", 50, 10, 5); +$stateManager->setPlayer($player); +$stateManager->setCurrentTileId('TOWN_01'); + +echo "=== 物品使用功能测试 ===\n\n"; + +// 添加测试物品 +$potion = new Item(1, "小型治疗药水", "potion", "恢复20点生命", 10, ['heal' => 20]); +$weapon = new Item(2, "铁剑", "weapon", "攻击+5", 50, [], 'weapon', ['attack' => 5]); + +$player->addItem($potion); +$player->addItem($weapon); + +echo "1. 初始状态:\n"; +echo " - 生命值: {$player->getHealth()}/{$player->getMaxHealth()}\n"; +echo " - 攻击力: {$player->getAttack()}\n"; +echo " - 背包物品数: " . count($player->getInventory()) . "\n\n"; + +// 扣血测试药水 +$player->takeDamage(30); +echo "2. 受到30点伤害后:\n"; +echo " - 生命值: {$player->getHealth()}/{$player->getMaxHealth()}\n\n"; + +// 测试使用药水 +echo "3. 使用药水 (编号0)...\n"; +$dispatcher->dispatch(new Event('UseItemEvent', ['itemIndex' => 0])); +echo " - 生命值: {$player->getHealth()}/{$player->getMaxHealth()}\n"; +echo " - 背包物品数: " . count($player->getInventory()) . "\n\n"; + +// 测试装备武器 +echo "4. 装备武器 (编号0,因为药水已被使用)...\n"; +$dispatcher->dispatch(new Event('EquipItemEvent', ['itemIndex' => 0])); +echo " - 攻击力: {$player->getAttack()}\n"; +echo " - 背包物品数: " . count($player->getInventory()) . "\n"; +$equipment = $player->getEquipment(); +echo " - 武器槽: " . ($equipment['weapon'] ? $equipment['weapon']->name : '空') . "\n\n"; + +echo "=== 测试完成 ===\n"; diff --git a/tests/test_npc_interaction.php b/tests/test_npc_interaction.php new file mode 100644 index 0000000..26a9ac6 --- /dev/null +++ b/tests/test_npc_interaction.php @@ -0,0 +1,48 @@ +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(); + +// 创建测试玩家 +$player = new \Game\Model\Player("TestHero", 100, 10, 5); +$stateManager->setPlayer($player); + +// 设置地图位置到新手村 +$stateManager->setCurrentTileId('TOWN_01'); + +echo "=== 测试开始 ===\n"; +echo "当前位置: " . $stateManager->getCurrentTile()->name . "\n"; +echo "NPC列表: " . implode(', ', $stateManager->getCurrentTile()->npcIds) . "\n\n"; + +// 触发与NPC交谈 +echo "触发 StartInteractionEvent...\n"; +$dispatcher->dispatch(new Event('AttemptTalkEvent')); + +// 获取输出 +$display = $output->fetch(); +echo "\n=== 输出结果 ===\n"; +echo $display; +$dispatcher->dispatch(new Event('StartInteractionEvent', ['npcId' => 'VILLAGER_1'])); +dd($stateManager->getPendingNpcSelection()); + diff --git a/tests/test_npc_shop.php b/tests/test_npc_shop.php new file mode 100644 index 0000000..a925a6b --- /dev/null +++ b/tests/test_npc_shop.php @@ -0,0 +1,80 @@ + new QuestionHelper()]); + +$container = new ServiceContainer($input, $output, $helperSet); +$dispatcher = $container->registerServices(); +$stateManager = $container->getStateManager(); + +// 创建测试玩家 +$player = new \Game\Model\Player("ShopTest", 100, 10, 5); +$player->gainGold(200); // 给玩家一些金币用于测试 +$stateManager->setPlayer($player); +$stateManager->setCurrentTileId('TOWN_01'); + +echo "=== NPC商店功能测试 ===\n\n"; + +// 获取NPC仓库 +$npcRepo = $container->getNpcRepository(); +$npc = $npcRepo->createNPC('BLACKSMITH'); + +if (!$npc) { + die("❌ 无法创建铁匠NPC\n"); +} + +echo "1. 创建NPC: {$npc->getName()}\n"; +echo " 是否有商店: " . ($npc->hasShop ? '是' : '否') . "\n"; +echo " 商店库存数量: " . count($npc->shopInventory) . "\n\n"; + +echo "2. 玩家初始状态:\n"; +echo " - 金币: {$player->getGold()}\n"; +echo " - 背包物品数: " . count($player->getInventory()) . "\n\n"; + +echo "3. 模拟与NPC交互并打开商店...\n"; +// 模拟InteractionSystem的行为 +$dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "👤 你走近了 {$npc->getName()}。" +])); + +// 模拟NPC对话 +$defaultMsg = is_array($npc->dialogue) ? ($npc->dialogue['greeting'] ?? '...') : '...'; +$dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "{$npc->getName()}:{$defaultMsg}" +])); + +// 检查商店 +if ($npc->hasShop) { + $dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "\n🛍️ {$npc->getName()} 还经营着一家商店。" + ])); + $dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "输入 [S] 打开商店,[X] 离开" + ])); + + // 设置待处理的商店NPC + $stateManager->setPendingShopNpc($npc); + + // 模拟输入S打开商店 + echo "\n模拟输入 S 打开商店...\n"; + $dispatcher->dispatch(new Event('OpenShopEvent', [ + 'npc' => $npc + ])); +} + +echo "\n" . $output->fetch() . "\n"; + +echo "\n4. 测试完成后的状态:\n"; +echo " - 金币: {$player->getGold()}\n"; +echo " - 背包物品数: " . count($player->getInventory()) . "\n"; + +echo "\n=== 测试完成 ===\n"; \ No newline at end of file diff --git a/tests/test_npc_shop_simple.php b/tests/test_npc_shop_simple.php new file mode 100644 index 0000000..b4b2543 --- /dev/null +++ b/tests/test_npc_shop_simple.php @@ -0,0 +1,48 @@ + new QuestionHelper()]); + +$container = new ServiceContainer($input, $output, $helperSet); +$dispatcher = $container->registerServices(); +$stateManager = $container->getStateManager(); + +// 创建测试玩家 +$player = new \Game\Model\Player("ShopTest", 100, 10, 5); +$player->gainGold(200); // 给玩家一些金币用于测试 +$stateManager->setPlayer($player); +$stateManager->setCurrentTileId('TOWN_01'); + +echo "=== NPC商店功能测试 ===\n\n"; + +// 获取NPC仓库 +$npcRepo = $container->getNpcRepository(); +$npc = $npcRepo->createNPC('BLACKSMITH'); + +if (!$npc) { + die("❌ 无法创建铁匠NPC\n"); +} + +echo "1. 创建NPC: {$npc->getName()}\n"; +echo " 是否有商店: " . ($npc->hasShop ? '是' : '否') . "\n"; +echo " 商店库存数量: " . count($npc->shopInventory) . "\n\n"; + +echo "2. 玩家初始状态:\n"; +echo " - 金币: {$player->getGold()}\n"; +echo " - 背包物品数: " . count($player->getInventory()) . "\n\n"; + +echo "3. NPC商店数据验证:\n"; +foreach ($npc->shopInventory as $itemId => $data) { + echo " - 商品ID: {$itemId}, 价格: {$data['price']}\n"; +} + +echo "\n=== 测试完成 ===\n"; \ No newline at end of file diff --git a/tests/test_quest_list.php b/tests/test_quest_list.php new file mode 100644 index 0000000..46536b1 --- /dev/null +++ b/tests/test_quest_list.php @@ -0,0 +1,67 @@ + new QuestionHelper()]); + +$container = new ServiceContainer($input, $output, $helperSet); +$dispatcher = $container->registerServices(); +$stateManager = $container->getStateManager(); + +// 创建测试玩家 +$player = new \Game\Model\Player("QuestTest", 100, 10, 5); +$stateManager->setPlayer($player); +$stateManager->setCurrentTileId('TOWN_01'); + +echo "=== 任务列表功能测试 ===\n\n"; + +// 添加一些测试任务 +$quest1 = new Quest( + 'QUEST_001', + '清理野兽', + '击败5只野狼', + 'kill', + ['entityId' => 'wolf_001', 'count' => 5], + ['gold' => 100, 'xp' => 50] +); +$quest1->incrementProgress(); // 已击败 1 只 + +$quest2 = new Quest( + 'QUEST_002', + '收集草药', + '收集10个草药', + 'collect', + ['entityId' => 'herb_basic', 'count' => 10], + ['gold' => 50, 'xp' => 30] +); +$quest2->incrementProgress(); +$quest2->incrementProgress(); +$quest2->incrementProgress(); // 已收集 3 个 + +$player->addActiveQuest($quest1); +$player->addActiveQuest($quest2); + +// 添加已完成的任务 +$player->markQuestCompleted('TUTORIAL_QUEST'); +$player->markQuestCompleted('FIRST_BATTLE'); + +echo "1. 初始状态:\n"; +echo " - 进行中任务: " . count($player->getActiveQuests()) . "\n"; +echo " - 已完成任务: " . count($player->getCompletedQuests()) . "\n\n"; + +// 测试显示任务列表 +echo "2. 显示任务列表...\n"; +$dispatcher->dispatch(new Event('ShowQuestListRequest')); + +echo "\n" . $output->fetch() . "\n"; + +echo "=== 测试完成 ===\n"; diff --git a/tests/test_quest_turnin.php b/tests/test_quest_turnin.php new file mode 100644 index 0000000..2257e8e --- /dev/null +++ b/tests/test_quest_turnin.php @@ -0,0 +1,74 @@ + new QuestionHelper()]); + +$container = new ServiceContainer($input, $output, $helperSet); +$dispatcher = $container->registerServices(); +$stateManager = $container->getStateManager(); + +// 创建测试玩家 +$player = new \Game\Model\Player("QuestTester", 100, 10, 5); +$stateManager->setPlayer($player); +$stateManager->setCurrentTileId('TOWN_01'); + +echo "=== 任务交付功能测试 ===\n\n"; + +// 创建一个测试任务 +$quest = new Quest( + 'TEST_QUEST_001', + '清理野狼', + '击败3只野狼', + 'kill', + ['entityId' => 'wolf_001', 'count' => 3], + ['gold' => 100, 'xp' => 50] +); + +// 添加任务并完成它 +$player->addActiveQuest($quest); +echo "1. 添加任务: {$quest->getTitle()}\n"; +echo " 状态: 进行中 (0/3)\n\n"; + +// 模拟击杀进度 +$quest->incrementProgress(); +$quest->incrementProgress(); +$quest->incrementProgress(); + +echo "2. 完成任务目标:\n"; +echo " 状态: 已完成 (3/3)\n"; +echo " 是否完成: " . ($quest->isCompleted() ? '是' : '否') . "\n\n"; + +echo "3. 初始状态:\n"; +echo " - 金币: {$player->getGold()}\n"; +echo " - 经验值: {$player->getCurrentXp()}\n"; +echo " - 等级: {$player->getLevel()}\n\n"; + +// 测试交任务 - 直接调用turnInQuest方法前,需要模拟一个repo数据 +echo "4. 模拟交付任务逻辑...\n"; + +// 直接调用player的方法模拟奖励 +$player->gainGold(100); +$player->gainXp(50); +$player->markQuestCompleted('TEST_QUEST_001'); + +echo "\n✅ 模拟奖励发放:\n"; +echo " - 金币 +100\n"; +echo " - 经验值 +50\n"; + +echo "\n5. 交付后状态:\n"; +echo " - 金币: {$player->getGold()}\n"; +echo " - 经验值: {$player->getCurrentXp()}\n"; +echo " - 等级: {$player->getLevel()}\n"; +echo " - 已完成任务: " . (in_array('TEST_QUEST_001', $player->getCompletedQuests()) ? '是' : '否') . "\n"; + +echo "\n=== 测试完成 ===\n";