diff --git a/config/map_data.json b/config/map.json similarity index 100% rename from config/map_data.json rename to config/map.json diff --git a/save/player.json b/save/player.json index bf06f81..10ebe69 100644 --- a/save/player.json +++ b/save/player.json @@ -1,63 +1,66 @@ { - "name": "hant", - "health": 130, - "maxHealth": 130, - "attack": 31, - "defense": 9, - "level": 3, - "currentXp": 25, - "xpToNextLevel": 225, - "gold": 321, - "inventory": [ - { - "id": 2, - "name": "\u7834\u65e7\u7684\u77ed\u5251", - "type": "weapon", - "description": "\u653b\u51fb\u529b\u5fae\u5f31\u3002", - "value": 50, - "effects": [], - "slot": "weapon", - "statModifiers": { - "attack": 5 + "player": { + "name": "Hant", + "health": 91, + "maxHealth": 100, + "base_attack": 15, + "base_defense": 5, + "level": 1, + "currentXp": 20, + "gold": 12, + "inventory": [ + { + "id": 2, + "name": "\u7834\u65e7\u7684\u77ed\u5251", + "type": "weapon", + "description": "\u653b\u51fb\u529b\u5fae\u5f31\u3002", + "value": 50, + "effects": [], + "stats": [], + "slot": "weapon" + }, + { + "id": 2, + "name": "\u7834\u65e7\u7684\u77ed\u5251", + "type": "weapon", + "description": "\u653b\u51fb\u529b\u5fae\u5f31\u3002", + "value": 50, + "effects": [], + "stats": [], + "slot": "weapon" } + ], + "equipment": { + "weapon": null, + "armor": null, + "helmet": 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 - }, - "slot": null, - "statModifiers": [] - }, - { - "id": 1, - "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", - "type": "potion", - "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", - "value": 10, - "effects": { - "heal": 20 - }, - "slot": null, - "statModifiers": [] - }, - { - "id": 1, - "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", - "type": "potion", - "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", - "value": 10, - "effects": { - "heal": 20 - }, - "slot": null, - "statModifiers": [] - } - ], - "activeQuests": [], - "completedQuests": [] + "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 + } + ], + "completedQuests": [] + }, + "world": { + "currentTileId": "FOREST_01" + } } \ No newline at end of file diff --git a/src/Core/ServiceContainer.php b/src/Core/ServiceContainer.php index fb9eb97..4ab35d8 100644 --- a/src/Core/ServiceContainer.php +++ b/src/Core/ServiceContainer.php @@ -7,6 +7,7 @@ use Game\Database\DatabaseManager; use Game\Database\EnemyRepository; use Game\Database\ItemRepository; use Game\Database\JsonFileLoader; +use Game\Database\MapRepository; use Game\Database\NPCRepository; use Game\Database\QuestRepository; use Game\Event\EventDispatcher; @@ -51,6 +52,7 @@ class ServiceContainer { private AbilityRepository $abilityRepository; private QuestRepository $questionRepository; private NPCRepository $npcRepository; + private MapRepository $mapRepository; public function __construct(InputInterface $input, OutputInterface $output, HelperSet $helperSet) { $this->input = $input; @@ -69,8 +71,14 @@ class ServiceContainer { // 事件分发器 (核心) $this->eventDispatcher = new EventDispatcher(); - // 状态管理器 - $this->stateManager = new StateManager($this->dbManager->getConnection()); + // ⭐ 实例化 Repository 层 + $jsonLoader = new JsonFileLoader(); + $this->itemRepository = new ItemRepository($jsonLoader); + $this->enemyRepository = new EnemyRepository($jsonLoader); + $this->abilityRepository = new AbilityRepository($jsonLoader); + $this->questionRepository = new QuestRepository($jsonLoader); + $this->npcRepository = new NPCRepository($jsonLoader); + $this->mapRepository = new MapRepository($jsonLoader); } /** @@ -79,21 +87,14 @@ class ServiceContainer { public function registerServices(): EventDispatcher { $this->initializeInfrastructure(); - - // ⭐ 实例化 Repository 层 - $jsonLoader = new JsonFileLoader(); - $this->itemRepository = new ItemRepository($jsonLoader); - $this->enemyRepository = new EnemyRepository($jsonLoader); - $this->abilityRepository = new AbilityRepository($jsonLoader); - $this->questionRepository = new QuestRepository($jsonLoader); - $this->npcRepository = new NPCRepository($jsonLoader); - + // 状态管理器 + $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)); // 2. 核心逻辑服务 (依赖 Dispatcher, StateManager) - $this->register(MapSystem::class, new MapSystem($this->eventDispatcher, $this->stateManager,$this->npcRepository)); + $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)); diff --git a/src/Database/MapRepository.php b/src/Database/MapRepository.php new file mode 100644 index 0000000..b170384 --- /dev/null +++ b/src/Database/MapRepository.php @@ -0,0 +1,70 @@ +load('map.json'); + foreach ($data as $id => $tileData){ + $this->mapData[$id] = [ + 'id' => $id, + 'name' => $tileData['name'], + 'description' => $tileData['description'], + 'encounter_chance' => $tileData['encounter_chance'] ?? 0, + 'connections' => $tileData['connections'] ?? [], + 'encounter_pool' => $tileData['encounter_pool'] ?? [], + 'npc_ids' => $tileData['npc_ids'] ?? [], + 'loot_ids' => $tileData['loot_ids'] ?? [] + ]; + } + + } + + /** + * 根据 ID 获取地图对象 + */ + public function find(string $id): ?array + { + return $this->mapData[$id] ?? null; + } + + /** + * ⭐ 核心优化:工厂方法 + * 将数据数组转换为 MapTile 对象 + */ + public function createTile(string $id): ?MapTile + { + // 1. 检查缓存,避免重复创建同一张地图 + if (isset($this->instances[$id])) { + return $this->instances[$id]; + } + + // 2. 获取原始数据 + $data = $this->find($id); + if (!$data) return null; + + // 3. 实例化模型 (确保 MapTile 构造函数匹配这些字段) + $tile = new MapTile( + $data['id'], + $data['name'], + $data['description'], + $data['connections'] ?? [], + $data['encounter_pool'] ?? [], + $data['encounter_chance'] ?? 0, + $data['npc_ids'] ?? [], + $data['loot_ids'] ?? [] + ); + + // 4. 存入缓存并返回 + $this->instances[$id] = $tile; + return $tile; + } +} \ No newline at end of file diff --git a/src/Model/Character.php b/src/Model/Character.php index bc22702..0687a79 100644 --- a/src/Model/Character.php +++ b/src/Model/Character.php @@ -8,8 +8,8 @@ class Character { protected string $name; protected int $health; protected int $maxHealth; - protected int $attack; - protected int $defense; + public int $attack; + public int $defense; // ⭐ 新增:魔法值 (MP/Mana) protected int $mana; @@ -116,6 +116,18 @@ class Character { return $actualDamage; } + public function getEquipmentBonus(): array { + $bonuses = ['attack' => 0, 'defense' => 0, 'hp_max' => 0]; + foreach ($this->equipment as $item) { + if ($item && isset($item->stats)) { + foreach ($item->stats as $stat => $value) { + $bonuses[$stat] = ($bonuses[$stat] ?? 0) + $value; + } + } + } + return $bonuses; + } + /** * 治疗角色 */ diff --git a/src/Model/Player.php b/src/Model/Player.php index aa1288d..653d523 100644 --- a/src/Model/Player.php +++ b/src/Model/Player.php @@ -185,4 +185,12 @@ class Player extends Character { { $this->health = $health; } + + +// 确保在装备时不会因为背包里没有该对象而报错(加载时是从存档恢复的对象) + public function equipItem(Item $item): void { + if ($item->slot) { + $this->equipment[$item->slot] = $item; + } + } } \ No newline at end of file diff --git a/src/System/BattleService.php b/src/System/BattleService.php index 09ad34e..b1e8569 100644 --- a/src/System/BattleService.php +++ b/src/System/BattleService.php @@ -62,12 +62,18 @@ class BattleService implements EventListenerInterface { * 1. 初始化战斗状态 */ private function startBattle(Enemy $enemy): void { + + // 当战斗开始 + $this->stateManager->setMode(StateManager::MODE_BATTLE); + + $this->currentEnemy = $enemy; $this->inBattle = true; $this->dispatcher->dispatch(new Event('SystemMessage', [ 'message' => "⚔️ 你遭遇了 {$this->currentEnemy->getName()}!战斗开始!" ])); + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); // 立即要求 UI 刷新菜单 // 立即进入战斗循环 $this->battleLoop(); @@ -245,8 +251,9 @@ class BattleService implements EventListenerInterface { $this->inBattle = false; $this->currentEnemy = null; - // 战斗结束后,重新打印主菜单请求,以继续主循环 - $this->dispatcher->dispatch(new Event('ShowMenuEvent')); + // 当战斗结束 + $this->stateManager->setMode(StateManager::MODE_MAP); + $this->dispatcher->dispatch(new Event('ShowMenuEvent')); // 切换回探索菜单 } /** diff --git a/src/System/InputHandler.php b/src/System/InputHandler.php index 2e4cc5a..3d967ca 100644 --- a/src/System/InputHandler.php +++ b/src/System/InputHandler.php @@ -8,12 +8,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Helper\QuestionHelper; -/** - * InputHandler: 负责从 CLI 获取用户输入,并将其解析为系统事件。 - * 遵循单一职责原则:只处理输入和事件转发。 - */ class InputHandler { - private ?SaveLoadService $saveLoadService = null; // ⭐ 接受 SaveLoadService + private ?SaveLoadService $saveLoadService = null; private EventDispatcher $dispatcher; private InputInterface $input; private OutputInterface $output; @@ -29,144 +25,202 @@ class InputHandler { } /** - * 获取主循环操作指令,并分派事件 + * 核心操作循环 */ public function handleMainCommand(): bool { - // 1. 请求 UI 服务打印主菜单 (确保 UI 已输出提示) + // 获取当前模式 + $mode = $this->stateManager->getMode(); + + // 根据模式调用不同的子处理器 + return match ($mode) { + StateManager::MODE_MAP => $this->handleMapInput(), + StateManager::MODE_BATTLE => $this->handleBattleInput(), + StateManager::MODE_INVENTORY => $this->handleInventoryInput(), + StateManager::MODE_DIALOGUE => $this->handleDialogueInput(), + default => $this->handleMapInput(), + }; + } + + /** + * 🗺️ 地图模式输入处理:补全版 + */ + private function handleMapInput(): bool { + // 1. 每次等待输入前先展示当前模式的菜单提示 $this->dispatcher->dispatch(new Event('ShowMenuEvent')); - $question = new Question("> 请选择操作 (L/M/E/S/I/Q/B):"); - $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + // 获取输入并转为大写 + $input = $this->ask("> "); - // 2. 解析并分派事件 - switch ($choice) { - case 'L': // ⭐ 保存指令 + switch ($input) { + // --- 移动指令 (支持 WASD 和 NSEW) --- + case 'W': case 'N': + $this->move('N'); + break; + case 'S': + $this->move('S'); + break; + case 'A': + $this->move('W'); // WASD 的左是西 (West) + break; + case 'D': + $this->move('E'); + break; + + // --- 功能指令 --- + case 'E': // 探索 (Explore) + $this->dispatcher->dispatch(new Event('MapExploreRequest')); + break; + case 'T': // 交谈 (Talk) + $this->dispatcher->dispatch(new Event('AttemptTalkEvent')); + break; + case 'B': // 背包 (Inventory/Bag) + $this->stateManager->setMode(StateManager::MODE_INVENTORY); + // 切换模式后,主动触发一次背包显示 + $this->dispatcher->dispatch(new Event('ShowInventoryRequest')); + break; + case 'I': // 状态 (Status) + $this->dispatcher->dispatch(new Event('ShowStatsRequest')); + break; + case 'L': // 保存 (Save/Load) if ($this->saveLoadService) { $this->saveLoadService->saveGame(); } break; - case 'B': // ⭐ 新增背包指令 - $this->handleInventoryInput(); - break; - case 'M': - $this->handleMoveInput(); - break; - case 'E': - $this->dispatcher->dispatch(new Event('MapExploreRequest')); - break; - case 'S': - // 状态显示请求,由于 StateManager 是状态持有者,EventDispatcher 会分发给 UIService - $this->dispatcher->dispatch(new Event('ShowStatsRequest')); - break; - case 'T': // T 代表交谈 (Talk) - $this->dispatcher->dispatch(new Event('AttemptTalkEvent')); - break; - case 'I': // ⭐ 新增交互指令 - // 模拟 MapSystem 发现了一个 NPC - $this->dispatcher->dispatch(new Event('AttemptInteractEvent', ['npcId' => 'VILLAGER_1'])); - break; - case 'Q': - return false; // 返回 false 通知 GameCommand 退出主循环 + + // --- 系统指令 --- + case 'H': case 'HELP': + $this->showHelp(); // 假设你有一个显示详细说明的方法 + break; + case 'Q': // 退出 + return false; + default: - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的指令。请使用 M, E, S, I, Q。'])); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "无效指令 '{$input}'。移动请按 WASD 或 NSEW。" + ])); + break; } - return true; // 继续主循环 + return true; } /** - * 处理移动输入和事件分派 + * 核心移动分发器 + * @param string $direction 方向标识 (N, S, E, W) */ - private function handleMoveInput(): void { - $directionQuestion = new Question("请输入移动方向 (N/S/E/W):"); - $direction = strtoupper($this->helper->ask($this->input, $this->output, $directionQuestion) ?? ''); + private function move(string $direction): void { + // 这里可以添加一些通用的输入反馈 + // $this->output->writeln("正在尝试向方向 [{$direction}] 移动..."); - $validDirections = ['N', 'S', 'E', 'W']; - if (in_array($direction, $validDirections)) { - $this->dispatcher->dispatch(new Event('AttemptMoveEvent', ['direction' => $direction])); - } else { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的移动方向。'])); - } + $this->dispatcher->dispatch(new Event('AttemptMoveEvent', [ + 'direction' => $direction + ])); } - // TODO: 可以在这里添加 handleBattleInput() 等,进一步解耦 BattleService - /** - * 处理背包输入和子菜单 - */ - private function handleInventoryInput(): void { - // 1. 显示背包 + /** ⚔️ 战斗逻辑:菜单选择式 */ + private function handleBattleInput(): bool { + $this->output->writeln("\n[1] 攻击 | [2] 技能 | [3] 物品 | [4] 逃跑"); + $choice = $this->ask("战斗指令> "); + + switch ($choice) { + case '1': $this->dispatcher->dispatch(new Event('BattleAction', ['type' => 'attack'])); break; + case '4': $this->dispatcher->dispatch(new Event('BattleAction', ['type' => 'flee'])); break; + } + return true; + } + + /** 🎒 背包逻辑:管理模式 */ + private function handleInventoryInput(): bool { $this->dispatcher->dispatch(new Event('ShowInventoryRequest')); + $input = $this->ask("背包操作 (输入编号或 X 退出)> "); - $question = new Question("> 请输入物品编号进行操作 ([E]装备/使用 | [U]卸下 | [X]退出):"); - $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); - - if ($choice === 'X') { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '退出背包。'])); - return; + if (strtoupper($input) === 'X') { + $this->stateManager->setMode(StateManager::MODE_MAP); + return true; } + // ... 处理物品使用逻辑 ... + return true; + } - if (is_numeric($choice)) { - $itemIndex = (int)$choice; - $this->handleItemAction($itemIndex); // ⭐ 移交给新方法处理 - } elseif ($choice === 'U') { - $this->handleUnequipAction(); - } else { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的指令。请使用编号,E, U, X。'])); + /** 💬 对话逻辑:选项分支 */ + private function handleDialogueInput(): bool { + $input = $this->ask("对话选择 (数字)> "); + + if (strtoupper($input) === 'X') { + $this->stateManager->setMode(StateManager::MODE_MAP); + return true; } + $this->dispatcher->dispatch(new Event('DialogueChoice', ['choice' => $input])); + return true; + } + + private function ask(string $prompt): string { + $question = new Question($prompt); + return strtoupper(trim($this->helper->ask($this->input, $this->output, $question) ?? '')); } /** - * ⭐ 新增:处理单个物品的使用/装备逻辑 + * 处理物品操作 */ - private function handleItemAction(int $itemIndex): void { + private function handleItemAction(int $itemIndex, string $preferredAction): void { $player = $this->stateManager->getPlayer(); - $item = $player->getInventory()[$itemIndex] ?? null; + $inventory = $player->getInventory(); - if (!$item) { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的物品编号。'])); + if (!isset($inventory[$itemIndex])) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "编号为 {$itemIndex} 的物品不存在。"])); return; } + $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' => '该物品无法使用或装备。'])); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "该物品无法被使用或装备。"])); } } /** - * ⭐ 新增:处理卸下逻辑 + * 辅助帮助信息 + */ + private function showHelp(): void { + $msg = "\n--- 快捷指令帮助 ---\n" . + "移动: W/A/S/D 或 N/S/E/W\n" . + "探索: E | 交谈: T\n" . + "状态: S | 背包: I\n" . + "物品: USE | EQUIP | UNEQUIP\n" . + "系统: L (保存) | Q (退出)\n"; + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => $msg])); + } + + /** + * 卸下逻辑 (保持原样) */ private function handleUnequipAction(): void { $player = $this->stateManager->getPlayer(); $equipment = $player->getEquipment(); - - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '--- 选择要卸下的槽位 ---'])); - - $availableSlots = []; - foreach ($equipment as $slot => $item) { - if ($item) { - $this->dispatcher->dispatch(new Event('SystemMessage', [" [{$slot}] : {$item->name}"])); - $availableSlots[] = $slot; - } - } + $availableSlots = array_filter($equipment, fn($item) => $item !== null); if (empty($availableSlots)) { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '没有物品可以卸下。'])); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '身上没有可卸下的装备。'])); return; } - $question = new Question("> 输入槽位名称 (e.g., weapon) 或 X 取消:"); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '--- 输入要卸下的槽位 (例如: weapon) ---'])); + foreach ($availableSlots as $slot => $item) { + $this->dispatcher->dispatch(new Event('SystemMessage', [" - {$slot}: {$item->name}"])); + } + + $question = new Question("> "); $slotChoice = strtolower($this->helper->ask($this->input, $this->output, $question) ?? ''); - if ($slotChoice !== 'x' && in_array($slotChoice, $availableSlots)) { + if (isset($availableSlots[$slotChoice])) { $this->dispatcher->dispatch(new Event('UnequipItemEvent', ['slot' => $slotChoice])); } } + public function setSaveLoadService(SaveLoadService $service): void { $this->saveLoadService = $service; } diff --git a/src/System/MapSystem.php b/src/System/MapSystem.php index 1ae50c4..c3e1d67 100644 --- a/src/System/MapSystem.php +++ b/src/System/MapSystem.php @@ -2,52 +2,29 @@ namespace Game\System; use Game\Database\NPCRepository; +use Game\Database\MapRepository; // 注入新的仓库 use Game\Event\Event; use Game\Event\EventListenerInterface; use Game\Event\EventDispatcher; -use Game\Model\MapTile; // 引入 MapTile 模型 -use Game\Event\StartBattleEvent; +use Game\Model\MapTile; class MapSystem implements EventListenerInterface { private EventDispatcher $dispatcher; private StateManager $stateManager; - private array $mapData; private NPCRepository $npcRepository; + private MapRepository $mapRepository; // ⭐ 新增 - public function __construct(EventDispatcher $dispatcher, StateManager $stateManager,NPCRepository $npcRepository) { + public function __construct( + EventDispatcher $dispatcher, + StateManager $stateManager, + NPCRepository $npcRepository, + MapRepository $mapRepository // ⭐ 注入 + ) { $this->dispatcher = $dispatcher; $this->stateManager = $stateManager; $this->npcRepository = $npcRepository; - $this->loadMapData(); - - // 游戏启动时,设置初始区域状态到 StateManager - $this->stateManager->setCurrentTile($this->getTile('TOWN_01')); - } - - private function loadMapData(): void { - $jsonPath = __DIR__ . '/../../config/map_data.json'; - if (!file_exists($jsonPath)) { - throw new \Exception("Map configuration file not found."); - } - $this->mapData = json_decode(file_get_contents($jsonPath), true); - } - - public function getTile(string $tileId): MapTile { - if (!isset($this->mapData[$tileId])) { - throw new \Exception("MapTile ID '{$tileId}' not found in configuration."); - } - $data = $this->mapData[$tileId]; - // ⭐ 修正 MapTile 实例化参数 - return new MapTile( - $tileId, - $data['name'], - $data['description'], - $data['connections'], - $data['encounter_pool'] ?? null, // 传入遇敌池 - $data['encounter_chance'] ?? 0.0, // 传入遇敌几率 - $data['npc_ids'] ?? [] // 传入遇敌几率 - ); + $this->mapRepository = $mapRepository; } public function handleEvent(Event $event): void { @@ -55,129 +32,128 @@ class MapSystem implements EventListenerInterface { case 'AttemptMoveEvent': $this->handleMoveAttempt($event->getPayload()['direction']); break; - case 'MapExploreRequest': // 修正:监听 MapExploreRequest 事件 (来自 GameCommand) + case 'MapExploreRequest': $this->handleExplore(); break; - case 'GameStartEvent': - // 游戏开始,通知 UI 打印初始位置 - $initialTile = $this->stateManager->getCurrentTile(); - $this->dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => $initialTile->id])); - break; - // ... 现有 case ... - case 'AttemptTalkEvent': // ⭐ 响应新的交谈尝试事件 + case 'AttemptTalkEvent': $this->handleTalkAttempt(); break; - case 'InteractWithNpcEvent': // ⭐ 玩家已选择 NPC,交给 InteractionSystem - $this->dispatcher->dispatch(new Event('SystemMessage', [ - 'message' => "➡️ 启动与 {$event->getPayload()['npcId']} 的交互流程。" - ])); + case 'GameStartEvent': + // 游戏开始,通知 UI 打印当前位置(此时 StateManager 已由外部初始化好位置) + $initialTile = $this->stateManager->getCurrentTile(); + if ($initialTile) { + $this->dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => $initialTile->id])); + } + break; } } - /** - * ⭐ 新增方法:处理交谈尝试,列出 NPC - */ - private function handleTalkAttempt(): void { - $currentTile = $this->stateManager->getCurrentTile(); - $npcIds = $currentTile->npcIds; - - if (empty($npcIds)) { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里没有人可以交谈。"])); - return; - } - - if (count($npcIds) === 1) { - // 只有一个 NPC,直接开始交互 - $npcId = $npcIds[0]; - $this->dispatcher->dispatch(new Event('StartInteractionEvent', ['npcId' => $npcId])); - return; - } - - // 多个 NPC,需要玩家选择 - $options = []; - $i = 1; - foreach ($npcIds as $id) { - // 使用 NPC Repository 查找 NPC 名字 - $npcData = $this->npcRepository->find($id); // 假设 MapSystem 已经注入了 NPCRepository - $options[] = "[{$i}] {$npcData['name']}"; - $i++; - } - - $this->dispatcher->dispatch(new Event('SystemMessage', [ - 'message' => "👥 你想和谁交谈?\n" . implode("\n", $options) - ])); - // 🚨 这一步需要更复杂的输入处理来获取用户选择,我们暂时简化为直接转发到 InteractionSystem - $this->dispatcher->dispatch(new Event('AwaitingNpcSelection', ['options' => $npcIds])); - } /** * 处理玩家移动逻辑 */ private function handleMoveAttempt(string $direction): void { $direction = strtoupper($direction); - - // 从 StateManager 获取当前 Tile 的连接信息 $currentTile = $this->stateManager->getCurrentTile(); - $connections = $currentTile->connections; - if (isset($connections[$direction])) { - $newTileId = $connections[$direction]; - $newTile = $this->getTile($newTileId); + if (isset($currentTile->connections[$direction])) { + $newTileId = $currentTile->connections[$direction]; - // 1. 成功移动:更新 StateManager 中的状态 - $this->stateManager->setCurrentTile($newTile); + // ⭐ 核心修改:利用 StateManager 内部的逻辑(它会调用 MapRepository::createTile) + $this->stateManager->setCurrentTileId($newTileId); - // 2. 触发 MapMoveEvent,通知 UI 和其他系统 + // 检查移动后是否自动触发遇敌(可选:有些游戏移动即遇敌) $this->dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => $newTileId])); + + // 每次移动后有较小几率直接触发遇敌,增加张力 + $this->checkRandomEncounter(0.1); } else { - // 移动失败:通知 UI - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 该方向 ({$direction}) 没有道路或无法通行。"])); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 该方向没有道路。"])); } } /** - * 处理玩家探索逻辑:根据 eventPoolId 决定遭遇什么 + * 处理交谈尝试 + */ + private function handleTalkAttempt(): void { + $currentTile = $this->stateManager->getCurrentTile(); + $npcIds = $currentTile->npcIds; + + if (empty($npcIds)) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里寂静无声,没有人可以交谈。"])); + return; + } + + // ⭐ 切换到对话模式 + $this->stateManager->setMode(StateManager::MODE_DIALOGUE); + + if (count($npcIds) === 1) { + $this->dispatcher->dispatch(new Event('StartInteractionEvent', ['npcId' => $npcIds[0]])); + } else { + // 多个 NPC 的处理逻辑... + $this->listNpcsForSelection($npcIds); + } + } + + /** + * 探索逻辑 */ private function handleExplore(): void { - // 从 StateManager 获取当前 Tile $currentTile = $this->stateManager->getCurrentTile(); - $roll = rand(1, 100) / 100; - // ⭐ 核心修正 1: 直接使用 Tile 中的几率 - if ($currentTile->encounterChance > 0 && $roll <= $currentTile->encounterChance) { - // ⭐ 核心修正 2: 使用 Tile 中的 Pool 获取随机敌人 ID - $enemyId = $this->getRandomEnemyIdFromPool($currentTile->encounterPool); - - if ($enemyId) { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❗️ 你遇到了麻烦..."])); - $this->dispatcher->dispatch(new Event('EncounterEnemyEvent', ['enemyId' => $enemyId])); - } - - } elseif ($roll <= 0.8) { - // 20% 几率发现宝箱/物品 - $this->dispatcher->dispatch(new Event('LootFoundEvent', ['lootId' => 5])); - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "🎁 你发现了一个宝箱!"])); - } else { - // 20% 几率没有遭遇 - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "你在周围探索了一番,但什么也没发现。"])); + // 1. 尝试触发遇敌 + if ($this->checkRandomEncounter($currentTile->encounterChance ?? 0.5)) { + return; } + + // 2. 尝试发现物品 + if ($roll <= 0.3 && !empty($currentTile->lootIds)) { + $lootId = $currentTile->lootIds[array_rand($currentTile->lootIds)]; + $this->dispatcher->dispatch(new Event('LootFoundEvent', ['lootId' => $lootId])); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "你仔细搜索了四周,但一无所获。"])); + } + } + + /** + * 内部辅助:检查遇敌 + */ + private function checkRandomEncounter(float $chance): bool { + $currentTile = $this->stateManager->getCurrentTile(); + if (!$currentTile->encounterPool || rand(1, 100) / 100 > $chance) { + return false; + } + + $enemyId = $this->getRandomEnemyIdFromPool($currentTile->encounterPool); + if ($enemyId) { + // ⭐ 发现敌人,强制切换到战斗模式 + $this->stateManager->setMode(StateManager::MODE_BATTLE); + $this->dispatcher->dispatch(new Event('EncounterEnemyEvent', ['enemyId' => $enemyId])); + return true; + } + return false; } private function getRandomEnemyIdFromPool(?array $pool): ?string { - if (!$pool) { - return null; - } - + if (empty($pool)) return null; $totalWeight = array_sum(array_column($pool, 'weight')); $randValue = mt_rand(1, $totalWeight); - $currentWeight = 0; foreach ($pool as $item) { $currentWeight += $item['weight']; - if ($randValue <= $currentWeight) { - return $item['enemyId']; - } + if ($randValue <= $currentWeight) return $item['enemyId']; } - return null; // 理论上不会到达这里 + return null; + } + + private function listNpcsForSelection(array $npcIds): void { + $output = "👥 这里的居民:\n"; + foreach ($npcIds as $index => $id) { + $npcData = $this->npcRepository->find($id); + $name = $npcData['name'] ?? "神秘人"; + $output .= " [" . ($index + 1) . "] {$name}\n"; + } + $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/SaveLoadService.php b/src/System/SaveLoadService.php index 23ae13f..c9a8da7 100644 --- a/src/System/SaveLoadService.php +++ b/src/System/SaveLoadService.php @@ -8,7 +8,7 @@ use Game\Event\EventDispatcher; use Game\Model\Quest; /** - * SaveLoadService: 负责将玩家状态持久化到磁盘,并在启动时加载。 + * SaveLoadService: 负责将玩家状态持久化,并支持装备与地图位置的还原。 */ class SaveLoadService { @@ -21,62 +21,65 @@ class SaveLoadService { $this->stateManager = $stateManager; $this->savePath = $savePath; - // 确保保存目录存在 if (!is_dir(dirname($this->savePath))) { mkdir(dirname($this->savePath), 0777, true); } } - /** - * 检查是否存在存档文件 - */ public function hasSaveFile(): bool { return file_exists($this->savePath); } /** - * 将玩家状态保存到 JSON 文件 + * 将完整游戏状态保存 */ public function saveGame(): void { $player = $this->stateManager->getPlayer(); - if (!$player) { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 无法保存:玩家状态为空。'])); - return; - } + $currentTile = $this->stateManager->getCurrentTile(); + + if (!$player) return; try { + // 1. 序列化背包 + $serializedInventory = array_map(fn(Item $item) => $this->itemToData($item), $player->getInventory()); - // ⭐ 修正:处理 Inventory 序列化 - $serializedInventory = []; - /** @var Item $item */ - foreach ($player->getInventory() as $item) { - // 将 Item 对象转换为数组以安全序列化 (可选,但更清晰) - $serializedInventory[] = $item->toArray(); + // 2. ⭐ 新增:序列化当前装备槽位 + $serializedEquipment = []; + foreach ($player->getEquipment() as $slot => $item) { + $serializedEquipment[$slot] = $item ? $this->itemToData($item) : null; } + + // 3. 序列化任务 $activeQuests = []; foreach ($player->getActiveQuests() as $quest) { - // 将 Item 对象转换为数组以安全序列化 (可选,但更清晰) - $activeQuests[] = $quest->toArray(); + $activeQuests[] = [ + 'config' => $quest->toArray(), + 'currentCount' => $quest->getCurrentCount() + ]; } - // 序列化 Player 对象(我们假设 Player 模型包含了所有属性的 public/getter) + $data = [ - 'name' => $player->getName(), - 'health' => $player->getHealth(), - 'maxHealth' => $player->getMaxHealth(), - 'attack' => $player->getAttack(), - 'defense' => $player->getDefense(), - 'level' => $player->getLevel(), - 'currentXp' => $player->getCurrentXp(), - 'xpToNextLevel' => $player->getXpToNextLevel(), - 'gold' => $player->getGold(), - 'inventory' => $serializedInventory, // 注意:复杂对象数组需要递归序列化/反序列化 - 'activeQuests' => $activeQuests, - 'completedQuests' => $player->getCompletedQuests(), - // TODO: 添加地图位置等其他状态 + '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)); - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '💾 游戏已保存!'])); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '💾 进度已安全存入魔法卷轴!'])); } catch (\Exception $e) { $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 存档失败: ' . $e->getMessage()])); @@ -84,86 +87,82 @@ class SaveLoadService { } /** - * 从 JSON 文件加载玩家状态,并返回新的 Player 实例 + * 加载存档并还原所有复杂对象 */ public function loadGame(): ?Player { - if (!$this->hasSaveFile()) { - return null; - } + if (!$this->hasSaveFile()) return null; try { - $json = file_get_contents($this->savePath); - $data = json_decode($json, true); + $data = json_decode(file_get_contents($this->savePath), true); + $pData = $data['player']; - // 1. 实例化 Player(使用 Character 基类的构造函数) - $player = new Player($data['name'], $data['maxHealth'], $data['attack'], $data['defense']); + // 1. 创建基础玩家对象 + $player = new Player($pData['name'], $pData['maxHealth'], $pData['base_attack'], $pData['base_defense']); + $player->setHealth($pData['health']); + $player->setLevel($pData['level']); + $player->setCurrentXp($pData['currentXp']); + $player->setGold($pData['gold']); + $player->setCompletedQuests($pData['completedQuests'] ?? []); - // 2. 恢复运行时状态 - $player->setHealth($data['health']); // 假设 Player 模型中有 setHealth 方法 + // 2. 还原背包物品 (包含 Stats 属性) + $inventory = array_map(fn($itemData) => $this->dataToItem($itemData), $pData['inventory']); + $player->setInventory($inventory); - // 3. 恢复 Player 特有属性 - $player->setLevel($data['level']); // 假设 Player 模型中有 setLevel 方法 - $player->setCurrentXp($data['currentXp']); - $player->setXpToNextLevel($data['xpToNextLevel']); - $player->setGold($data['gold']); - - // 4. 恢复复杂对象 (Inventory, Quests) - // Inventory 恢复需要重新实例化 Item 对象(这里是简化的难点) - // 简化处理:目前我们只将 Item 属性数据恢复,而不是完整的 Item 对象 - // 实际中需要一个 ItemFactory 来根据数据重建对象 - - // ⭐ 关键修正:恢复复杂对象 Inventory - $restoredInventory = []; - // 确保 Item 类被引入: use Game\Model\Item; - foreach ($data['inventory'] as $itemData) { - // 重新实例化 Item 对象 - $item = new \Game\Model\Item( - $itemData['id'], - $itemData['name'], - $itemData['type'], - $itemData['description'], - $itemData['value'], - $itemData['effects'] ?? [] // 确保 effects 存在 - ); - $restoredInventory[] = $item; - } - - // ⭐ 恢复 Active Quests (修正逻辑) - $restoredActiveQuests = []; - // ... (Repository 获取逻辑) ... - - foreach ($data['activeQuests'] as $questId => $serializedQuestData) { - $questConfig = $serializedQuestData; - if ($questConfig) { - // ⭐ 核心修正:直接从配置中获取 target 数组 - $targetArray = $questConfig['target'] ?? ['count' => 1]; - - // 重新实例化 Quest (使用配置数据) - $quest = new Quest( - $questConfig['id'], $questConfig['name'], $questConfig['description'], - $questConfig['type'], $targetArray, $questConfig['rewards'] - ); - - // 恢复运行时进度 (currentCount) - if (isset($serializedQuestData['currentCount'])) { - $quest->setCurrentCount($serializedQuestData['currentCount']); - } - - $restoredActiveQuests[$questId] = $quest; + // 3. ⭐ 还原装备 (确保 Stats 效果重新生效) + foreach ($pData['equipment'] as $slot => $itemData) { + if ($itemData) { + $item = $this->dataToItem($itemData); + $player->equipItem($item); // 使用装备方法,确保属性刷新 } } - $player->setActiveQuests($restoredActiveQuests); - $player->setInventory($restoredInventory); // 现在赋值的是 Item 对象的数组 + // 4. 还原任务进度 + $activeQuests = []; + foreach ($pData['activeQuests'] as $qData) { + $config = $qData['config']; + $quest = new Quest($config['id'], $config['name'], $config['description'], $config['type'], $config['target'], $config['rewards']); + $quest->setCurrentCount($qData['currentCount']); + $activeQuests[$config['id']] = $quest; + } + $player->setActiveQuests($activeQuests); - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '📂 游戏存档已加载!'])); + // 5. ⭐ 还原地图位置 + if (isset($data['world']['currentTileId'])) { + $this->stateManager->setCurrentTileId($data['world']['currentTileId']); + } + + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '📂 欢迎回来,勇者!进度已加载。'])); return $player; } catch (\Exception $e) { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 加载存档失败: ' . $e->getMessage()])); - // 移除损坏的存档文件 - // unlink($this->savePath); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 加载失败: ' . $e->getMessage()])); return null; } } + + /** + * 辅助:Item 转 数组 (包含新增的 stats 字段) + */ + private function itemToData(Item $item): array { + return [ + 'id' => $item->id, + 'name' => $item->name, + 'type' => $item->type, + 'description' => $item->description, + 'value' => $item->value, + 'effects' => $item->effects ?? [], + 'stats' => $item->stats ?? [], // ⭐ 对应 D 阶段的属性加成 + 'slot' => $item->slot ?? null + ]; + } + + /** + * 辅助:数组 转 Item + */ + private function dataToItem(array $d): Item { + $item = new Item($d['id'], $d['name'], $d['type'], $d['description'], $d['value'], $d['effects']); + $item->stats = $d['stats'] ?? []; + $item->slot = $d['slot'] ?? null; + return $item; + } } \ No newline at end of file diff --git a/src/System/StateManager.php b/src/System/StateManager.php index a5263fe..b420158 100644 --- a/src/System/StateManager.php +++ b/src/System/StateManager.php @@ -1,6 +1,7 @@ currentMode = $mode; + // 模式切换时,可以自动触发 UI 刷新 + } + + public function getMode(): string { + return $this->currentMode; + } + public function __construct(Connection $db,MapRepository $mapRepository) { $this->db = $db; + $this->mapRepository = $mapRepository; } /** @@ -42,7 +60,6 @@ class StateManager { public function getCurrentTile(): MapTile { if (!isset($this->currentTile)) { - // 假设 MapSystem 会在初始化时设置初始 Tile throw new \RuntimeException("Current MapTile not set in StateManager."); } return $this->currentTile; @@ -64,4 +81,17 @@ class StateManager { } // TODO: loadGame() 方法 + /** + * ⭐ 关键:设置当前地图 ID 并同步更新 Tile 对象 + */ + public function setCurrentTileId(string $tileId): void { + $this->currentTileId = $tileId; + + // 从仓库中加载完整的地图瓦片数据 + $tileData = $this->mapRepository->createTile($tileId); + if ($tileData) { + // 假设 MapRepository 有一个工厂方法可以生成对象 + $this->currentTile = $this->mapRepository->createTile($tileId); + } + } } \ No newline at end of file diff --git a/src/System/UIService.php b/src/System/UIService.php index 7d6277a..5388266 100644 --- a/src/System/UIService.php +++ b/src/System/UIService.php @@ -4,139 +4,267 @@ namespace Game\System; use Game\Event\Event; use Game\Event\EventListenerInterface; use Game\Model\Player; -use Game\Model\MapTile; // 需要引入 MapTile 模型 +use Game\Model\Enemy; +use Game\Model\MapTile; use Symfony\Component\Console\Output\OutputInterface; -use function Symfony\Component\String\b; /** - * UIService: 负责所有终端输出的监听器。 - * 确保 UI 与游戏状态一致,遵循单一输出源原则。 + * UIService: 负责所有终端输出的渲染。 */ class UIService implements EventListenerInterface { private OutputInterface $output; - private StateManager $stateManager; // 替换 MapSystem + private StateManager $stateManager; - /** - * 注意:UIService 需要 MapSystem 实例来查询地图数据进行显示 - */ public function __construct(OutputInterface $output, StateManager $stateManager) { $this->output = $output; $this->stateManager = $stateManager; } public function handleEvent(Event $event): void { + $mode = $this->stateManager->getMode(); switch ($event->getType()) { - case 'GameStartEvent': - $this->output->writeln("🔔 {$event->getPayload()['message']}"); - break; - - case 'ShowStatsEvent': - // 确保 Payload 中包含 Player 实例 - if (isset($event->getPayload()['player']) && $event->getPayload()['player'] instanceof Player) { - $this->displayPlayerStats($event->getPayload()['player']); + case 'ShowMenuEvent': // 👈 监听这个信号 + // 根据模式渲染不同的 UI 底部 + if ($mode === StateManager::MODE_MAP) { + $this->displayMapMenu(); + } elseif ($mode === StateManager::MODE_BATTLE) { + $this->displayBattleMenu(); } break; - + case 'ShowStatsRequest': // + $this->displayPlayerStats(); // 👈 真正调用显示逻辑 + break; case 'SystemMessage': $this->output->writeln("📣 {$event->getPayload()['message']}"); break; - case 'ShowStatsRequest': - // UIService 现在从 StateManager 获取 Player 实例 - $player = $this->stateManager->getPlayer(); - $this->displayPlayerStats($player); - break; case 'MapMoveEvent': - // 收到 MapSystem 触发的移动成功事件 - $tileId = $event->getPayload()['newTileId']; - try { - // 通过 MapSystem 获取最新的 MapTile 数据 - $tile = $this->stateManager->getCurrentTile(); -// dd($tile); - $this->displayLocation($tile); - } catch (\Exception $e) { - $this->output->writeln("UI 错误:无法加载地图区域 {$tileId} {$e->getMessage()}。"); - } + $this->refreshScreen(); // 刷新整个视野 break; - case 'ShowInventoryRequest': // ⭐ 新增背包显示请求 + + case 'StatUpdateEvent': + $this->renderHUD(); // 仅刷新状态栏 + break; + + case 'ShowInventoryRequest': $this->displayInventory(); break; - case 'StartBattleEvent': - $this->output->writeln("\n\n⚔️ 遭遇战触发!请选择战斗指令..."); + case 'BattleStatusEvent': + // 收到战斗每回合的状态更新(包含 player 和 enemy 对象) + $payload = $event->getPayload(); + $this->renderBattleScene($payload['player'], $payload['enemy']); break; - // TODO: 在后续步骤中添加 BattleEndEvent, DamageDealtEvent 等处理 + case 'StartBattleEvent': + $this->output->writeln("\n ⚔️ 进入战斗阶段! \n"); + break; } } /** - * 打印玩家状态信息 + * 🗺️ 地图模式菜单:强调移动和探索 */ - private function displayPlayerStats(Player $player): void { - $this->output->writeln("\n--- 角色状态 ---"); - $this->output->writeln("姓名: {$player->getName()}"); - $this->output->writeln("等级: {$player->getLevel()} (XP: {$player->getCurrentXp()}/{$player->getXpToNextLevel()})"); - $this->output->writeln("生命值: {$player->getHealth()}/{$player->getMaxHealth()}"); - $this->output->writeln("魔法值: {$player->getMana()}/{$player->getMaxMana()}"); - $this->output->writeln("攻击力: {$player->getAttack()} | 防御力: {$player->getDefense()}"); - // ⭐ 新增金币显示 - $this->output->writeln("金币: {$player->getGold()} 💰"); - // ⭐ 显示当前装备 - $this->output->writeln("\n--- 装备 ---"); - $equipment = $player->getEquipment(); - foreach ($equipment as $slot => $item) { - $itemName = $item ? "{$item->name}" : '空'; - $this->output->writeln(" {$slot}: {$itemName}"); - } - // TODO: 未来显示任务和物品数量 - $this->output->writeln("--------------------------"); + 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) . ""); } - private function displayMainMenu(): void { - $this->output->writeln("\n--- 主菜单 ---"); - $this->output->writeln(" [M] 移动 | [E] 探索 | [S] 状态 | [I] 交互 | [B] 背包 | [L] 保存 | [Q] 退出"); // ⭐ 增加 L 选项 - } /** - * 打印当前地图区域信息 + * ⚔️ 战斗模式菜单:强调数字键操作 + */ + 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) . ""); + } + + /** + * 打印玩家状态信息 (属性增强版) + */ + private function displayPlayerStats(): void { + $player = $this->stateManager->getPlayer(); + $bonuses = $player->getEquipmentBonus(); + + $this->output->writeln("\n" . str_repeat("=", 40)); + $this->output->writeln(" 👤 角色:{$player->getName()} 等级: {$player->getLevel()}"); + $this->output->writeln(str_repeat("-", 40)); + + // 攻击力显示:基础值 (+加成) + $atkStr = "{$player->attack}"; + if ($bonuses['attack'] > 0) { + $atkStr .= " (+{$bonuses['attack']})"; + } + + // 防御力显示:基础值 (+加成) + $defStr = "{$player->defense}"; + if ($bonuses['defense'] > 0) { + $defStr .= " (+{$bonuses['defense']})"; + } + + $this->output->writeln(" ⚔️ 攻击力: {$atkStr}"); + $this->output->writeln(" 🛡️ 防御力: {$defStr}"); + $this->output->writeln(" ❤️ 生命值: {$player->getHealth()}/{$player->getMaxHealth()}"); + $this->output->writeln(" 💰 金币: {$player->getGold()}"); + + $this->output->writeln("\n 当前装备:"); + foreach ($player->getEquipment() as $slot => $item) { + $name = $item ? "{$item->name}" : "空"; + $this->output->writeln(" - " . str_pad(ucfirst($slot) . ":", 10) . $name); + } + $this->output->writeln(str_repeat("=", 40) . "\n"); + } + + /** + * 核心:刷新整个屏幕显示(地图 + 状态栏) + */ + private function refreshScreen(): void { + $tile = $this->stateManager->getCurrentTile(); + $this->displayLocation($tile); + $this->renderHUD(); + } + + /** + * ⭐ 优化:顶部状态栏 (HUD) + * 在每次移动或升级后自动显示,无需手动请求 + */ + private function renderHUD(): void { + $player = $this->stateManager->getPlayer(); + + // 1. 生成生命值血条 + $hpBar = $this->generateProgressBar($player->getHealth(), $player->getMaxHealth(), 10, 'red'); + + // 2. 构造一行简洁的状态 + $hud = sprintf( + "❤️ HP: %s %d/%d | 💰 金币: %d | ⭐ 等级: %d (XP: %d/%d)", + $hpBar, + $player->getHealth(), + $player->getMaxHealth(), + $player->getGold(), + $player->getLevel(), + $player->getCurrentXp(), + $player->getXpToNextLevel() + ); + + $this->output->writeln($hud); + $this->output->writeln("" . str_repeat("-", 60) . ""); + } + + /** + * ⭐ 优化:战斗场景渲染 + * 显示敌人和玩家的对峙血条 + */ + private function renderBattleScene(Player $player, Enemy $enemy): void { + $this->output->writeln("\n" . str_repeat("~", 40)); + + // 敌人信息 + $enemyHpBar = $this->generateProgressBar($enemy->getHealth(), $enemy->getMaxHealth(), 20, 'red'); + $this->output->writeln(sprintf("👾 %s", str_pad($enemy->getName(), 15))); + $this->output->writeln("HP: {$enemyHpBar} {$enemy->getHealth()}/{$enemy->getMaxHealth()}"); + + $this->output->writeln("\n VS \n"); + + // 玩家信息 + $playerHpBar = $this->generateProgressBar($player->getHealth(), $player->getMaxHealth(), 20, 'green'); + $this->output->writeln(sprintf("🛡️ %s (你)", str_pad($player->getName(), 15))); + $this->output->writeln("HP: {$playerHpBar} {$player->getHealth()}/{$player->getMaxHealth()}"); + + $this->output->writeln(str_repeat("~", 40) . "\n"); + } + + /** + * ⭐ 辅助:生成 ASCII 进度条 + */ + private function generateProgressBar(int $current, int $max, int $width, string $color): string { + if ($max <= 0) return "[ ]"; + $percent = max(0, min(1, $current / $max)); + $filledLength = (int)round($percent * $width); + $emptyLength = $width - $filledLength; + + return sprintf( + "[%s%s]", + $color, + str_repeat("■", $filledLength), + str_repeat(" ", $emptyLength) + ); + } + + /** + * 打印当前地图区域信息 (增强版) */ private function displayLocation(MapTile $tile): void { - // 清屏(可选,增强沉浸感) - // $this->output->write("\033[2J\033[H"); - - $this->output->writeln("\n======== [ {$tile->name} ] ========"); + $this->output->writeln("\n 📍 区域:{$tile->name} "); $this->output->writeln(" {$tile->description}"); - // 格式化连接信息 - $connections = array_map(fn($dir, $id) => "{$dir}({$id})", array_keys($tile->connections), $tile->connections); - $this->output->writeln(" -> 可移动方向: " . implode(' | ', $connections)); - $this->output->writeln("===================================\n"); + // 显示出口 + $moveOptions = []; + foreach ($tile->connections as $dir => $targetId) { + $moveOptions[] = "{$dir} ({$targetId})"; + } + $this->output->writeln(" 🚪 出路: " . implode(' | ', $moveOptions)); + + // 如果有 NPC,也显示出来 + if (!empty($tile->npcIds)) { + $this->output->writeln(" 👥 附近的人: " . implode(', ', $tile->npcIds) . ""); + } } /** - * 打印玩家背包内容 + * 打印玩家背包内容 (保持您现有的逻辑并微调样式) */ private function displayInventory(): void { $player = $this->stateManager->getPlayer(); $inventory = $player->getInventory(); - $this->output->writeln("\n--- 背包 ({$player->getGold()} 💰) ---"); + + $this->output->writeln("\n🎒 个人背包内容"); + $this->output->writeln("" . str_repeat("=", 30) . ""); if (empty($inventory)) { - $this->output->writeln("背包是空的。"); - $this->output->writeln("--------------------------"); - return; + $this->output->writeln(" (空空如也)"); + } else { + foreach ($inventory as $index => $item) { + // 简化效果显示 + $effectStr = ""; + if (!empty($item->effects)) { + $effectStr = " | 效果: " . json_encode($item->effects) . ""; + } + $this->output->writeln(sprintf(" [%d] %s (%s)%s", $index, $item->name, $item->type, $effectStr)); + } } + $this->output->writeln("" . str_repeat("=", 30) . "\n"); + } - foreach ($inventory as $index => $item) { - $effects = implode(', ', array_map( - fn($k, $v) => "{$k}:{$v}", - array_keys($item->effects??[]), - $item->effects??[] - )); - - $this->output->writeln("[{$index}] {$item->name} ({$item->type}) | 效果: [{$effects}]"); - } - $this->output->writeln("--------------------------"); + private function displayMainMenu(): void { + $this->output->writeln("\n 操作指南 "); + $this->output->writeln(" 移动: W/A/S/D | 探索: E | 交谈: T"); + $this->output->writeln(" 角色: I (状态) | B (背包) | L (保存)"); + $this->output->writeln(" 提示: 输入 'USE 0' 可直接使用背包第一格物品"); } } \ No newline at end of file