From 025c1ba2f2fa05ae59cfa493bae4fc0bb92f97b3 Mon Sep 17 00:00:00 2001 From: hantao Date: Tue, 16 Dec 2025 13:49:05 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- save/player.json | 23 ++++ src/Core/GameCommand.php | 97 +++++++--------- src/Core/ServiceContainer.php | 127 +++++++++++++++++++++ src/Model/Ability.php | 23 ++++ src/Model/Character.php | 78 +++++-------- src/Model/Item.php | 17 ++- src/Model/Player.php | 113 ++++++++++++++++++- src/System/AbilityService.php | 114 +++++++++++++++++++ src/System/BattleService.php | 55 +++++++++- src/System/InputHandler.php | 38 ++++++- src/System/ItemService.php | 82 ++++++++++++++ src/System/LootService.php | 23 +++- src/System/SaveLoadService.php | 139 +++++++++++++++++++++++ src/System/ShopService.php | 195 +++++++++++++++++++++++++++++++++ src/System/UIService.php | 50 +++++++-- 15 files changed, 1038 insertions(+), 136 deletions(-) create mode 100644 save/player.json create mode 100644 src/Core/ServiceContainer.php create mode 100644 src/Model/Ability.php create mode 100644 src/System/AbilityService.php create mode 100644 src/System/ItemService.php create mode 100644 src/System/SaveLoadService.php create mode 100644 src/System/ShopService.php diff --git a/save/player.json b/save/player.json new file mode 100644 index 0000000..1c3ec1d --- /dev/null +++ b/save/player.json @@ -0,0 +1,23 @@ +{ + "name": "hant", + "health": 100, + "maxHealth": 100, + "attack": 15, + "defense": 5, + "level": 1, + "currentXp": 10, + "xpToNextLevel": 100, + "gold": 7, + "inventory": [ + { + "id": 1, + "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", + "type": "potion", + "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", + "value": 10, + "effects": [] + } + ], + "activeQuests": [], + "completedQuests": [] +} \ No newline at end of file diff --git a/src/Core/GameCommand.php b/src/Core/GameCommand.php index 91c5156..d598f21 100644 --- a/src/Core/GameCommand.php +++ b/src/Core/GameCommand.php @@ -8,9 +8,11 @@ use Game\System\BattleService; use Game\System\CharacterService; use Game\System\InputHandler; use Game\System\InteractionSystem; +use Game\System\ItemService; use Game\System\LootService; use Game\System\MapSystem; use Game\System\QuestService; +use Game\System\ShopService; use Game\System\StateManager; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -41,77 +43,52 @@ class GameCommand extends Command { } protected function execute(InputInterface $input, OutputInterface $output): int { - // 1. 初始化数据库管理器 - $this->dbManager = new DatabaseManager(); - $this->dbManager->loadInitialData(); - // 2. 初始化 Event Dispatcher 和所有服务 - $this->initializeServices($input, $output); + // 1. 初始化服务容器并注册所有系统 + $container = new ServiceContainer($input, $output, $this->getHelperSet()); + $this->eventDispatcher = $container->registerServices(); + $this->inputHandler = $container->getInputHandler(); + $this->stateManager = $container->getStateManager(); + $saveLoadService = $container->getSaveLoadService(); // ⭐ 获取 SaveLoadService - // 3. 角色创建/加载存档 + $player = null; $helper = $this->getHelper('question'); - $question = new Question("请输入你的角色名称:", "旅行者"); - $playerName = $helper->ask($input, $output, $question); - // 创建玩家实例 - $player = new Player($playerName, 100, 15, 5); + // 2. ⭐ 存档/加载逻辑 + if ($saveLoadService->hasSaveFile()) { + $player = $saveLoadService->loadGame(); +// $output->writeln("\n检测到存档文件!"); +// $question = new Question("是否加载存档? ([Y]是 / [N]新建):", "Y"); +// $choice = strtoupper($helper->ask($input, $output, $question)); +// +// if ($choice === 'Y') { +// $player = $saveLoadService->loadGame(); +// } + } - // **将玩家实例交给 StateManager 管理** + if (!$player) { + // 新建角色逻辑 + $question = new Question("请输入你的角色名称:", "旅行者"); + $playerName = $helper->ask($input, $output, $question); + + // 创建玩家实例 (初始属性) + $player = new Player($playerName, 100, 15, 5); + $this->eventDispatcher->dispatch(new Event('SystemMessage', ['message' => "角色 {$player->getName()} 创建成功!"])); + } + + // 3. 将玩家实例交给 StateManager 管理 $this->stateManager->setPlayer($player); - // 通知 UI 服务 - $this->eventDispatcher->dispatch(new Event('SystemMessage', ['message' => "角色 {$player->getName()} 创建成功!"])); + // 4. ⭐ 增加存档命令到 InputHandler + $this->inputHandler->setSaveLoadService($saveLoadService); - return $this->mainLoop($input, $output); - } - - private function initializeServices(InputInterface $input, OutputInterface $output): void { - - // 实例化 QuestionHelper - $questionHelper = $this->getHelper('question'); - // 实例化 Event Dispatcher - $this->eventDispatcher = new EventDispatcher(); - $this->stateManager = new StateManager($this->dbManager->getConnection()); - - - - // ⭐ 实例化 CharacterService - $characterService = new CharacterService($this->eventDispatcher, $this->stateManager); - $this->eventDispatcher->registerListener($characterService); - - // ⭐ 实例化 LootService - $lootService = new LootService($this->eventDispatcher, $this->stateManager); - $this->eventDispatcher->registerListener($lootService); - - // ⭐ 实例化 InteractionSystem - $interactionSystem = new InteractionSystem($this->eventDispatcher, $this->stateManager, $input, $output, $questionHelper); - $this->eventDispatcher->registerListener($interactionSystem); - - // ⭐ 实例化 QuestService - $questService = new QuestService($this->eventDispatcher, $this->stateManager); - $this->eventDispatcher->registerListener($questService); - - // ⭐ 实例化 InputHandler 并注入依赖 - $this->inputHandler = new InputHandler($this->eventDispatcher, $input, $output, $questionHelper); - - // 实例化和注册 UIService (监听器) - $this->uiService = new UIService($output, $this->stateManager); - $this->eventDispatcher->registerListener($this->uiService); - - // MapSystem 注册 (需要 EventDispatcher 和 DB 连接) - $this->mapSystem = new MapSystem($this->eventDispatcher, $this->stateManager); - $this->eventDispatcher->registerListener($this->mapSystem); - - $questionHelper = $this->getHelper('question'); - $battleService = new BattleService($this->eventDispatcher, $this->stateManager, $input,$output, $questionHelper); - $this->eventDispatcher->registerListener($battleService); - - // 触发一个初始事件,让 UI 服务打印欢迎信息 + // 5. 触发启动事件 $welcomeEvent = new Event('GameStartEvent', ['message' => '核心系统已就绪,请输入指令开始游戏。']); $this->eventDispatcher->dispatch($welcomeEvent); - } - // src/Core/GameCommand.php (mainLoop 方法片段) + // 6. 启动主循环 + return $this->mainLoop($input, $output); + } private function mainLoop(InputInterface $input, OutputInterface $output): int { $running = true; while ($running) { diff --git a/src/Core/ServiceContainer.php b/src/Core/ServiceContainer.php new file mode 100644 index 0000000..ae0b452 --- /dev/null +++ b/src/Core/ServiceContainer.php @@ -0,0 +1,127 @@ +input = $input; + $this->output = $output; + $this->questionHelper = $helperSet->get('question'); + } + + /** + * 初始化核心基础设施 + */ + private function initializeInfrastructure(): void { + // 数据库 + $this->dbManager = new DatabaseManager(); + $this->dbManager->loadInitialData(); + + // 事件分发器 (核心) + $this->eventDispatcher = new EventDispatcher(); + + // 状态管理器 + $this->stateManager = new StateManager($this->dbManager->getConnection()); + } + + /** + * 实例化并注册所有系统服务 + */ + public function registerServices(): EventDispatcher { + $this->initializeInfrastructure(); + $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->register(CharacterService::class, new CharacterService($this->eventDispatcher, $this->stateManager)); + $this->register(LootService::class, new LootService($this->eventDispatcher, $this->stateManager)); + $this->register(ItemService::class, new ItemService($this->eventDispatcher, $this->stateManager)); + $this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager)); + + // 3. I/O 交互服务 (依赖 Dispatcher, StateManager, I/O 接口) + $this->register(InteractionSystem::class, + new InteractionSystem($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper) + ); + $this->register(BattleService::class, + 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) + ); + + // ⭐ 实例化 AbilityService + $abilityService = new AbilityService($this->eventDispatcher, $this->stateManager); + $this->register(AbilityService::class, $abilityService); + + // 4. 输入处理服务 (驱动主循环,必须最后注册,因为它依赖于所有 I/O 组件) + $this->register(InputHandler::class, + new InputHandler($this->eventDispatcher, $this->input, $this->output, $this->questionHelper) + ); + + // 返回分发器和状态管理器,供 GameCommand 使用 + return $this->eventDispatcher; + } + + public function getSaveLoadService(): SaveLoadService { + return $this->saveLoadService; + } + + /** + * 注册服务并将其作为监听器添加到 Event Dispatcher + */ + private function register(string $key, object $service): void { + $this->services[$key] = $service; + // 仅注册实现了 EventListenerInterface 的服务 + if ($service instanceof EventListenerInterface) { + $this->eventDispatcher->registerListener($service); + } + } + + // 可选:添加 Getter 方法,方便在 GameCommand 中获取 Player 或 InputHandler + public function getInputHandler(): InputHandler { + return $this->services[InputHandler::class]; + } + + public function getStateManager(): StateManager { + return $this->stateManager; + } +} \ No newline at end of file diff --git a/src/Model/Ability.php b/src/Model/Ability.php new file mode 100644 index 0000000..53c8454 --- /dev/null +++ b/src/Model/Ability.php @@ -0,0 +1,23 @@ +id = $id; + $this->name = $name; + $this->type = $type; + $this->manaCost = $manaCost; + $this->power = $power; + $this->scaling = $scaling; + } +} \ No newline at end of file diff --git a/src/Model/Character.php b/src/Model/Character.php index c02c4ef..ae1f572 100644 --- a/src/Model/Character.php +++ b/src/Model/Character.php @@ -11,66 +11,38 @@ class Character { protected int $attack; protected int $defense; - protected array $activeQuests = []; // 存储进行中的任务进度,格式: ['questId' => ['currentCount' => 0, 'isCompleted' => false]] - protected array $completedQuests = []; // 存储已完成的任务 ID -// ⭐ 新增方法:添加/接受任务 - public function addActiveQuest(string $questId, int $targetCount): void { - $this->activeQuests[$questId] = ['currentCount' => 0, 'isCompleted' => false, 'targetCount' => $targetCount]; - } +// ⭐ 新增:魔法值 (MP/Mana) + protected int $mana; + protected int $maxMana; - // ⭐ 新增方法:获取进行中的任务 - public function getActiveQuests(): array { - return $this->activeQuests; - } + // ⭐ 新增 Getter/Setter for Mana + public function getMana(): int { return $this->mana; } + public function getMaxMana(): int { return $this->maxMana; } - // ⭐ 新增方法:更新任务进度 - 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 确认后进行 - } - } + public function spendMana(int $cost): bool { + if ($this->mana >= $cost) { + $this->mana -= $cost; + return true; } + return false; } - // ⭐ 新增方法:标记任务完成 - public function markQuestCompleted(string $questId): void { - unset($this->activeQuests[$questId]); - $this->completedQuests[] = $questId; + public function restoreMana(int $amount): void { + $this->mana = min($this->maxMana, $this->mana + $amount); } - - // ⭐ 新增方法:检查任务是否已完成 - public function isQuestCompleted(string $questId): bool { - return in_array($questId, $this->completedQuests); - } - - // ⭐ 新增:玩家背包 (存储 Item 实例) - protected array $inventory = []; - - // ... 现有构造函数和 Getter ... - - // ⭐ 新增方法:添加物品到背包 - public function addItem(Item $item): void { - $this->inventory[] = $item; - } - - // ⭐ 新增方法:获取背包 - public function getInventory(): array { - return $this->inventory; - } - - public function __construct(string $name, int $maxHealth, int $attack, int $defense) { + public function __construct(string $name, int $maxHealth, int $attack, int $defense, int $maxMana = 0) { $this->name = $name; - $this->maxHealth = $maxHealth; - $this->health = $maxHealth; + $this->attack = $attack; $this->defense = $defense; + // ... 现有初始化 ... + $this->maxHealth = $maxHealth; + $this->health = $maxHealth; + // ... + + // ⭐ 初始化 Mana + $this->maxMana = $maxMana; + $this->mana = $maxMana; } public function getName(): string { return $this->name; } @@ -96,10 +68,12 @@ class Character { /** * 治疗角色 */ - public function heal(int $amount): void { + public function heal(int $amount): int { + $old = $this->health; $this->health += $amount; if ($this->health > $this->maxHealth) { $this->health = $this->maxHealth; } + return $this->health - $old; } } \ No newline at end of file diff --git a/src/Model/Item.php b/src/Model/Item.php index 4299e3b..3be8179 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -10,12 +10,25 @@ class Item { public string $type; // e.g., 'potion', 'weapon', 'material' public string $description; public int $value; // 卖出价格 - - public function __construct(int $id, string $name, string $type, string $description, int $value) { + public array $effects; // ⭐ 新增:存储效果参数 e.g., ['heal' => 20] + public function __construct(int $id, string $name, string $type, string $description, int $value, array $effects = []) { $this->id = $id; $this->name = $name; $this->type = $type; $this->description = $description; $this->value = $value; + $this->effects = $effects; // 赋值 + } + + public function toArray() + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'type' => $this->type, + 'description' => $this->description, + 'value' => $this->value, + 'effects' => $this->effects, + ]; } } \ No newline at end of file diff --git a/src/Model/Player.php b/src/Model/Player.php index 8b1979d..c10f0a3 100644 --- a/src/Model/Player.php +++ b/src/Model/Player.php @@ -7,9 +7,89 @@ class Player extends Character { protected int $currentXp = 0; protected int $xpToNextLevel = 100; - public function __construct(string $name, int $maxHealth, int $attack, int $defense) { + // ⭐ 新增:已学习的技能列表 + protected array $abilities = []; // 存储 Ability 实例 + + public function __construct(string $name, int $maxHealth, int $attack, int $defense,int $maxMana = 50) { // 调用父类 (Character) 的构造函数来初始化核心属性 - parent::__construct($name, $maxHealth, $attack, $defense); + parent::__construct($name, $maxHealth, $attack, $defense,$maxMana); + } + + // ⭐ 新增方法:学习技能 + public function learnAbility(Ability $ability): void { + $this->abilities[$ability->id] = $ability; + } + + // ⭐ 新增方法:获取所有技能 + public function getAbilities(): array { + return $this->abilities; + } + + // ⭐ 新增:货币属性 + protected int $gold = 0; + + // ... 现有构造函数和 Getter ... + + // ⭐ 新增 Getter + public function getGold(): int { return $this->gold; } + + // ⭐ 新增 Setter/Modifier + public function gainGold(int $amount): void { + $this->gold += $amount; + } + + protected array $activeQuests = []; // 存储进行中的任务进度,格式: ['questId' => ['currentCount' => 0, 'isCompleted' => false]] + protected array $completedQuests = []; // 存储已完成的任务 ID +// ⭐ 新增方法:添加/接受任务 + public function addActiveQuest(string $questId, int $targetCount): void { + $this->activeQuests[$questId] = ['currentCount' => 0, 'isCompleted' => false, 'targetCount' => $targetCount]; + } + + // ⭐ 新增方法:获取进行中的任务 + public function getActiveQuests(): array { + return $this->activeQuests; + } + + // ⭐ 新增方法:更新任务进度 + 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 确认后进行 + } + } + } + } + + // ⭐ 新增方法:标记任务完成 + public function markQuestCompleted(string $questId): void { + unset($this->activeQuests[$questId]); + $this->completedQuests[] = $questId; + } + + // ⭐ 新增方法:检查任务是否已完成 + public function isQuestCompleted(string $questId): bool { + return in_array($questId, $this->completedQuests); + } + + // ⭐ 新增:玩家背包 (存储 Item 实例) + protected array $inventory = []; + + // ... 现有构造函数和 Getter ... + + // ⭐ 新增方法:添加物品到背包 + public function addItem(Item $item): void { + $this->inventory[] = $item; + } + + // ⭐ 新增方法:获取背包 + public function getInventory(): array { + return $this->inventory; } // Player 特有的 Getter 方法 @@ -22,4 +102,33 @@ class Player extends Character { $this->currentXp += $amount; // TODO: 未来在这里实现升级逻辑 (LevelUpEvent) } + + public function removeItemByIndex(int $index): bool { + if (isset($this->inventory[$index])) { + unset($this->inventory[$index]); + // 重新索引数组,确保背包索引连续 + $this->inventory = array_values($this->inventory); + return true; + } + return false; + } + + // ⭐ 新增 Player 特有的 Setter: + public function setLevel(int $level): void { $this->level = $level; } + public function setCurrentXp(int $xp): void { $this->currentXp = $xp; } + public function setXpToNextLevel(int $xp): void { $this->xpToNextLevel = $xp; } + public function setGold(int $gold): void { $this->gold = $gold; } + public function setInventory(array $inventory): void { + // WARNING: 这里的 inventory 数组可能只包含序列化数据,需要确保 Item 实例化 + // 在我们当前简化的JSON方案中,暂且直接赋值原始数据。 + $this->inventory = $inventory; + } + public function getCompletedQuests(): array { return $this->completedQuests; } // 确保这个 Getter 存在 + public function setCompletedQuests(array $quests): void { $this->completedQuests = $quests; } + public function setActiveQuests(array $quests): void { $this->activeQuests = $quests; } + + public function setHealth(mixed $health) + { + $this->health = $health; + } } \ No newline at end of file diff --git a/src/System/AbilityService.php b/src/System/AbilityService.php new file mode 100644 index 0000000..76b9737 --- /dev/null +++ b/src/System/AbilityService.php @@ -0,0 +1,114 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + $this->loadAbilityData(); + } + + private function loadAbilityData(): void { + // 模拟加载所有技能数据 + $this->abilityData = [ + 'FIREBALL' => ['name' => '火球术', 'type' => 'damage', 'cost' => 10, 'power' => 25, 'scaling' => 'mana'], + 'HEAL' => ['name' => '初级治疗', 'type' => 'heal', 'cost' => 8, 'power' => 15, 'scaling' => 'mana'], + ]; + } + + /** + * 在游戏开始或升级时,让玩家学习初始技能 + */ + public function learnInitialAbilities(Player $player): void { + // 确保 FIREBALL 存在 + if (isset($this->abilityData['FIREBALL'])) { + $data = $this->abilityData['FIREBALL']; + $player->learnAbility(new Ability('FIREBALL', $data['name'], $data['type'], $data['cost'], $data['power'], $data['scaling'])); + } + if (isset($this->abilityData['HEAL'])) { + $data = $this->abilityData['HEAL']; + $player->learnAbility(new Ability('HEAL', $data['name'], $data['type'], $data['cost'], $data['power'], $data['scaling'])); + } + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'GameStartEvent': + // 确保在游戏开始时玩家获得技能 + $this->learnInitialAbilities($this->stateManager->getPlayer()); + break; + case 'CastAbilityEvent': // 响应 BattleService 的请求 + $this->handleCastAbility($event->getPayload()['abilityId'], $event->getPayload()['target']); + break; + // TODO: 未来监听 LevelUpEvent 来学习新技能 + } + } + + /** + * 处理技能施放的核心逻辑 + */ + private function handleCastAbility(string $abilityId, Enemy $target): void { + $player = $this->stateManager->getPlayer(); + $ability = $player->getAbilities()[$abilityId] ?? null; + + if (!$ability) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 未知的技能。'])); + return; + } + + // 1. 检查资源消耗 + if (!$player->spendMana($ability->manaCost)) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ MP不足,无法施放 ' . $ability->name . '。'])); + return; + } + + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "✨ {$player->getName()} 施放了 {$ability->name}!(-{$ability->manaCost} MP)"])); + + // 2. 计算效果 + $damage = 0; + $heal = 0; + + $scalingValue = match ($ability->scaling) { + 'attack' => $player->getAttack(), + 'mana' => $player->getMaxMana(), // 魔法值越高,技能越强 + default => 0, + }; + + // 基础伤害计算:威力 + (加成属性 * 0.5) + $rawEffect = $ability->power + (int)($scalingValue * 0.5); + + // 3. 应用效果 + switch ($ability->type) { + case 'damage': + $damage = $target->takeDamage($rawEffect); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "💥 {$ability->name} 对 {$target->getName()} 造成了 {$damage} 点伤害!" + ])); + break; + case 'heal': + $heal = $player->heal($rawEffect); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "💖 恢复了 {$heal} 点生命值!" + ])); + break; + } + + // 4. 触发 BattleService 的事件,让其检查战斗是否结束 + $this->dispatcher->dispatch(new Event('AbilityEffectApplied', ['target' => $target])); + } +} \ No newline at end of file diff --git a/src/System/BattleService.php b/src/System/BattleService.php index b95bd6f..1c436c9 100644 --- a/src/System/BattleService.php +++ b/src/System/BattleService.php @@ -38,8 +38,16 @@ class BattleService implements EventListenerInterface { public function handleEvent(Event $event): void { if ($this->inBattle) { - // 如果在战斗中,可以监听 'BattleCommandEvent' 等事件来处理输入 - // 当前版本,我们通过 battleLoop() 内部阻塞输入 + switch ($event->getType()) { + case 'AbilityEffectApplied': // ⭐ 监听技能施放效果 + $target = $event->getPayload()['target']; + if ($target instanceof Enemy && !$target->isAlive()) { + $this->handleWin(); + return; // 战斗结束 + } + // 如果是玩家治疗,则无需返回 + break; + } return; } @@ -93,6 +101,8 @@ class BattleService implements EventListenerInterface { if ($playerAction === 'A') { $this->playerAttack(); + }elseif ($playerAction === 'C') { // ⭐ 施法逻辑 + $this->handleAbilityInput(); } elseif ($playerAction === 'R') { if ($this->tryRunAway()) { $this->endBattle(false); @@ -109,21 +119,54 @@ class BattleService implements EventListenerInterface { } } + private function handleAbilityInput(): void { + $player = $this->stateManager->getPlayer(); + $abilities = $player->getAbilities(); + + if (empty($abilities)) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '你还没有学会任何技能!'])); + return; + } + + $this->output->writeln("\n--- 魔法技能 ---"); + $availableIds = []; + foreach ($abilities as $id => $ability) { + $canCast = $player->getMana() >= $ability->manaCost ? "" : ""; + $this->output->writeln(" [{$id}] {$ability->name} | 消耗: {$canCast}{$ability->manaCost} MP"); + $availableIds[] = $id; + } + $this->output->writeln("----------------"); + + $question = new Question("> 请输入技能 ID (或 X 取消):"); + $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + + if ($choice === 'X') return; + + if (in_array($choice, $availableIds)) { + // ⭐ 触发事件,将处理权交给 AbilityService + $this->dispatcher->dispatch(new Event('CastAbilityEvent', [ + 'abilityId' => $choice, + 'target' => $this->currentEnemy // 暂定为当前敌人 + ])); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的技能 ID。'])); + } + } + /** * 获取玩家战斗指令 (直接使用注入的 I/O 接口) */ private function promptPlayerAction(): string { $this->dispatcher->dispatch(new Event('SystemMessage', [ - 'message' => "\n--- 你的回合 --- [A] 攻击 | [R] 逃跑" + 'message' => "\n--- 你的回合 --- [A] 攻击 | [C] 施法 | [R] 逃跑" // ⭐ 增加 C ])); - $question = new Question("> 请选择指令 (A/R):"); - + $question = new Question("> 请选择指令 (A/C/R):"); // 关键:使用注入的 I/O 接口 $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); // 简单的输入验证 - if (in_array($choice, ['A', 'R'])) { + if (in_array($choice, ['A', 'C', 'R'])) { // ⭐ 增加 C return $choice; } else { $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的战斗指令。'])); diff --git a/src/System/InputHandler.php b/src/System/InputHandler.php index bedbbd1..f21c3d3 100644 --- a/src/System/InputHandler.php +++ b/src/System/InputHandler.php @@ -13,7 +13,7 @@ use Symfony\Component\Console\Helper\QuestionHelper; * 遵循单一职责原则:只处理输入和事件转发。 */ class InputHandler { - + private ?SaveLoadService $saveLoadService = null; // ⭐ 接受 SaveLoadService private EventDispatcher $dispatcher; private InputInterface $input; private OutputInterface $output; @@ -33,11 +33,19 @@ class InputHandler { // 1. 请求 UI 服务打印主菜单 (确保 UI 已输出提示) $this->dispatcher->dispatch(new Event('ShowMenuEvent')); - $question = new Question("> 请选择操作 (M/E/S/I/Q):"); + $question = new Question("> 请选择操作 (L/M/E/S/I/Q/B):"); $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); // 2. 解析并分派事件 switch ($choice) { + case 'L': // ⭐ 保存指令 + if ($this->saveLoadService) { + $this->saveLoadService->saveGame(); + } + break; + case 'B': // ⭐ 新增背包指令 + $this->handleInventoryInput(); + break; case 'M': $this->handleMoveInput(); break; @@ -76,4 +84,30 @@ class InputHandler { } // TODO: 可以在这里添加 handleBattleInput() 等,进一步解耦 BattleService + /** + * 处理背包输入和子菜单 + */ + private function handleInventoryInput(): void { + // 通知 UI 打印背包内容 + $this->dispatcher->dispatch(new Event('ShowInventoryRequest')); + + $question = new Question("> 请输入要使用的物品编号 (或 [X] 退出):"); + $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + + if ($choice === 'X') { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '退出背包。'])); + return; + } + + if (is_numeric($choice)) { + $itemIndex = (int)$choice; + // 触发使用物品事件,ItemService 监听并处理 + $this->dispatcher->dispatch(new Event('UseItemEvent', ['itemIndex' => $itemIndex])); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的编号。'])); + } + } + public function setSaveLoadService(SaveLoadService $service): void { + $this->saveLoadService = $service; + } } \ No newline at end of file diff --git a/src/System/ItemService.php b/src/System/ItemService.php new file mode 100644 index 0000000..b8ed1cb --- /dev/null +++ b/src/System/ItemService.php @@ -0,0 +1,82 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'UseItemEvent': // 响应玩家使用物品的请求 + $payload = $event->getPayload(); + $this->handleUseItem($payload['itemIndex']); + break; + // TODO: 未来添加 'EquipItemEvent' + } + } + + /** + * 处理玩家使用物品的请求 + */ + private function handleUseItem(int $itemIndex): void { + $player = $this->stateManager->getPlayer(); + $inventory = $player->getInventory(); + + if (!isset($inventory[$itemIndex])) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 背包中没有这个物品。'])); + return; + } + + $item = $inventory[$itemIndex]; + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "使用了物品:{$item->name}!" + ])); + + // 执行物品效果 + $this->applyItemEffects($player, $item); + + // 移除消耗品 + if ($item->type === 'potion') { + $player->removeItemByIndex($itemIndex); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "{$item->name} 已消耗。"])); + // 触发 UI 更新 + $this->dispatcher->dispatch(new Event('InventoryUpdateEvent')); + } + } + + /** + * 应用物品的具体效果 + */ + private function applyItemEffects(Player $player, Item $item): void { + if (empty($item->effects)) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这个物品似乎没有效果..."])); + return; + } + + if (isset($item->effects['heal'])) { + $healAmount = $item->effects['heal']; + $player->heal($healAmount); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "❤️ 恢复了 {$healAmount} 点生命值!当前HP: {$player->getHealth()}/{$player->getMaxHealth()}" + ])); + } + + // TODO: 未来添加 'buff', 'damage', 'equip' 等效果 + } +} \ No newline at end of file diff --git a/src/System/LootService.php b/src/System/LootService.php index 8922517..3af88a6 100644 --- a/src/System/LootService.php +++ b/src/System/LootService.php @@ -29,6 +29,11 @@ class LootService implements EventListenerInterface { $lootId = $event->getPayload()['lootId']; $this->handleLootFound($lootId); break; + // ... 现有事件 ... + case 'ShopPurchaseEvent': // ⭐ 响应商店购买 + $itemId = $event->getPayload()['itemId']; + $this->giveItemToPlayer($itemId); + break; } } @@ -36,6 +41,16 @@ class LootService implements EventListenerInterface { * 处理敌人死亡时的掉落逻辑 */ private function handleLootDrop(string $enemyId): void { + + $goldAmount = rand(5, 15); // 随机掉落 5 到 15 金币 + // 1. 发放金币 + $player = $this->stateManager->getPlayer(); + $player->gainGold($goldAmount); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "💰 获得了 {$goldAmount} 金币。" + ])); + // 简化:总是掉落物品 ID 1 (小药水) $roll = rand(1, 100); if ($roll <= 70) { // 70% 掉落几率 @@ -89,11 +104,11 @@ class LootService implements EventListenerInterface { * 模拟从配置中加载物品数据 */ private function loadItemData(int $id): array { - // 实际项目中应从数据库或 JSON 加载 return match ($id) { - 1 => ['name' => '小型治疗药水', 'type' => 'potion', 'description' => '恢复少量生命。', 'value' => 10], - 2 => ['name' => '破旧的短剑', 'type' => 'weapon', 'description' => '攻击力微弱。', 'value' => 50], - default => ['name' => '垃圾', 'type' => 'misc', 'description' => '毫无价值的杂物。', 'value' => 1], + 1 => ['name' => '小型治疗药水', 'type' => 'potion', 'description' => '恢复少量生命。', 'value' => 10, 'effects' => ['heal' => 20]], + 2 => ['name' => '破旧的短剑', 'type' => 'weapon', 'description' => '攻击力微弱。', 'value' => 50, 'effects' => []], + 3 => ['name' => '高级治疗药水', 'type' => 'potion', 'description' => '恢复大量生命。', 'value' => 200, 'effects' => ['heal' => 100]], // 新增 + default => ['name' => '垃圾', 'type' => 'misc', 'description' => '毫无价值的杂物。', 'value' => 1, 'effects' => []], }; } } \ No newline at end of file diff --git a/src/System/SaveLoadService.php b/src/System/SaveLoadService.php new file mode 100644 index 0000000..11b0664 --- /dev/null +++ b/src/System/SaveLoadService.php @@ -0,0 +1,139 @@ +dispatcher = $dispatcher; + $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; + } + + try { + + // ⭐ 修正:处理 Inventory 序列化 + $serializedInventory = []; + /** @var Item $item */ + foreach ($player->getInventory() as $item) { + // 将 Item 对象转换为数组以安全序列化 (可选,但更清晰) + $serializedInventory[] = $item->toArray(); + } + // 序列化 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' => $player->getActiveQuests(), + 'completedQuests' => $player->getCompletedQuests(), + // TODO: 添加地图位置等其他状态 + ]; + + file_put_contents($this->savePath, json_encode($data, JSON_PRETTY_PRINT)); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '💾 游戏已保存!'])); + + } catch (\Exception $e) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 存档失败: ' . $e->getMessage()])); + } + } + + /** + * 从 JSON 文件加载玩家状态,并返回新的 Player 实例 + */ + public function loadGame(): ?Player { + if (!$this->hasSaveFile()) { + return null; + } + + try { + $json = file_get_contents($this->savePath); + $data = json_decode($json, true); + + // 1. 实例化 Player(使用 Character 基类的构造函数) + $player = new Player($data['name'], $data['maxHealth'], $data['attack'], $data['defense']); + + // 2. 恢复运行时状态 + $player->setHealth($data['health']); // 假设 Player 模型中有 setHealth 方法 + + // 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; + } + $player->setInventory($restoredInventory); // 现在赋值的是 Item 对象的数组 + + $player->setActiveQuests($data['activeQuests']); + $player->setCompletedQuests($data['completedQuests']); + + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '📂 游戏存档已加载!'])); + return $player; + + } catch (\Exception $e) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 加载存档失败: ' . $e->getMessage()])); + // 移除损坏的存档文件 + // unlink($this->savePath); + return null; + } + } +} \ No newline at end of file diff --git a/src/System/ShopService.php b/src/System/ShopService.php new file mode 100644 index 0000000..12c1382 --- /dev/null +++ b/src/System/ShopService.php @@ -0,0 +1,195 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + $this->input = $input; + $this->output = $output; + $this->helper = $helper; + $this->loadShopInventory(); + } + + private function loadShopInventory(): void { + // 模拟商店出售的物品配置 (ID 对应 LootService::loadItemData) + $this->shopInventory = [ + // 商店物品 ID => 价格倍数(如果价格不是 Item::value) + 1 => ['stock' => 10, 'price' => 10], // 小药水 + 3 => ['stock' => 5, 'price' => 100], // 新物品:高级药水 + ]; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'OpenShopEvent': // 响应 InteractionSystem 的请求 + $this->startShopping(); + break; + } + } + + /** + * 启动商店界面和循环 + */ + private function startShopping(): void { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "\n欢迎光临!看看我有什么好东西。"])); + $running = true; + + while ($running) { + $player = $this->stateManager->getPlayer(); + $this->displayShopMenu($player); + + $question = new Question("> 请选择操作 (B:购买 | S:出售 | X:退出):"); + $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + + switch ($choice) { + case 'B': + $this->handlePurchase(); + break; + case 'S': + $this->handleSelling(); + break; + case 'X': + $running = false; + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '下次再来!'])); + break; + default: + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的指令。'])); + } + } + } + + // --- 购买逻辑 --- + + private function handlePurchase(): void { + $this->displaySaleItems(); + $player = $this->stateManager->getPlayer(); + + $question = new Question("> 输入要购买的物品编号 (或 X 退出):"); + $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + + if ($choice === 'X') return; + + if (is_numeric($choice)) { + $itemId = (int)$choice; + if (isset($this->shopInventory[$itemId])) { + $price = $this->shopInventory[$itemId]['price']; + + if ($player->spendGold($price)) { + // ⭐ 触发事件请求 LootService 给予物品(重用 LootService 的逻辑) + $this->dispatcher->dispatch(new Event('ShopPurchaseEvent', ['itemId' => $itemId])); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "交易成功!花费 {$price} 💰,剩余 {$player->getGold()} 💰。" + ])); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "金币不足!你需要 {$price} 💰。"])); + } + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '商店没有这个编号的物品。'])); + } + } + } + + // --- 出售逻辑 --- + + private function handleSelling(): void { + $player = $this->stateManager->getPlayer(); + if (empty($player->getInventory())) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '你的背包是空的,没有什么可卖的。'])); + return; + } + + $this->displaySellableItems($player); + + $question = new Question("> 输入要出售的物品编号 (背包索引 | 或 X 退出):"); + $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); + + if ($choice === 'X') return; + + if (is_numeric($choice)) { + $index = (int)$choice; + $inventory = $player->getInventory(); + + if (isset($inventory[$index])) { + $item = $inventory[$index]; + // 简化:出售价格为 Item::value 的一半 + $sellPrice = floor($item->value / 2); + + // 移除物品并获得金币 + $player->removeItemByIndex($index); + $player->gainGold($sellPrice); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "💰 出售了 {$item->name},获得 {$sellPrice} 💰。剩余 {$player->getGold()} 💰。" + ])); + + // 触发 UI 更新 + $this->dispatcher->dispatch(new Event('InventoryUpdateEvent')); + + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的背包编号。'])); + } + } + } + + // --- UI 辅助方法 --- + + private function displayShopMenu(Player $player): void { + $this->output->writeln("\n--- 商店主页 ---"); + $this->output->writeln("你的金币: {$player->getGold()} 💰"); + $this->output->writeln("--------------------------"); + } + + 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 + ]; + + foreach ($this->shopInventory as $itemId => $data) { + $name = $itemsData[$itemId]['name'] ?? "未知物品"; + $price = $data['price']; + $this->output->writeln("[{$itemId}] {$name} | 价格: {$price} 💰"); + } + $this->output->writeln("--------------------------"); + } + + private function displaySellableItems(Player $player): void { + $this->output->writeln("\n--- 📦 出售你的物品 ---"); + $inventory = $player->getInventory(); + + if (empty($inventory)) return; + + foreach ($inventory as $index => $item) { + $sellPrice = floor($item->value / 2); + $this->output->writeln("[{$index}] {$item->name} | 售价: {$sellPrice} 💰"); + } + $this->output->writeln("--------------------------"); + } +} \ No newline at end of file diff --git a/src/System/UIService.php b/src/System/UIService.php index 500e9cb..f8395cc 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\MapTile; // 需要引入 MapTile 模型 use Symfony\Component\Console\Output\OutputInterface; +use function Symfony\Component\String\b; /** * UIService: 负责所有终端输出的监听器。 @@ -58,6 +59,9 @@ class UIService implements EventListenerInterface { $this->output->writeln("UI 错误:无法加载地图区域 {$tileId} {$e->getMessage()}。"); } break; + case 'ShowInventoryRequest': // ⭐ 新增背包显示请求 + $this->displayInventory(); + break; case 'StartBattleEvent': $this->output->writeln("\n\n⚔️ 遭遇战触发!请选择战斗指令..."); @@ -71,18 +75,22 @@ class UIService implements EventListenerInterface { * 打印玩家状态信息 */ private function displayPlayerStats(Player $player): void { - $this->output->writeln("\n--- 角色状态:{$player->getName()} ---"); - $this->output->writeln("等级: {$player->getLevel()}"); - $this->output->writeln("HP: {$player->getHealth()}/{$player->getMaxHealth()}"); - $this->output->writeln("攻击力: {$player->getAttack()}"); - $this->output->writeln("防御力: {$player->getDefense()}"); - $this->output->writeln("经验值: {$player->getCurrentXp()}/{$player->getXpToNextLevel()}"); - $this->output->writeln("--------------------------\n"); + $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()} 💰"); + + // TODO: 未来显示任务和物品数量 + $this->output->writeln("--------------------------"); } private function displayMainMenu(): void { $this->output->writeln("\n--- 主菜单 ---"); - $this->output->writeln(" [M] 移动 | [E] 探索 | [S] 状态 | [I] 交互 | [Q] 退出"); // ⭐ 增加 I 选项 + $this->output->writeln(" [M] 移动 | [E] 探索 | [S] 状态 | [I] 交互 | [B] 背包 | [L] 保存 | [Q] 退出"); // ⭐ 增加 L 选项 } /** * 打印当前地图区域信息 @@ -99,4 +107,30 @@ class UIService implements EventListenerInterface { $this->output->writeln(" -> 可移动方向: " . implode(' | ', $connections)); $this->output->writeln("===================================\n"); } + + /** + * 打印玩家背包内容 + */ + private function displayInventory(): void { + $player = $this->stateManager->getPlayer(); + $inventory = $player->getInventory(); + $this->output->writeln("\n--- 背包 ({$player->getGold()} 💰) ---"); + + if (empty($inventory)) { + $this->output->writeln("背包是空的。"); + $this->output->writeln("--------------------------"); + return; + } + + 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("--------------------------"); + } } \ No newline at end of file