This commit is contained in:
hantao 2025-12-16 13:49:05 +08:00
parent 8e3c3a52de
commit 025c1ba2f2
15 changed files with 1038 additions and 136 deletions

23
save/player.json Normal file
View File

@ -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": []
}

View File

@ -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<info>检测到存档文件!</info>");
// $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) {

View File

@ -0,0 +1,127 @@
<?php
namespace Game\Core;
// 导入所有依赖和系统服务
use Game\Database\DatabaseManager;
use Game\Event\EventDispatcher;
use Game\Event\EventListenerInterface;
use Game\System\AbilityService;
use Game\System\SaveLoadService;
use Game\System\StateManager;
use Game\System\UIService;
use Game\System\MapSystem;
use Game\System\InputHandler;
use Game\System\BattleService;
use Game\System\CharacterService;
use Game\System\LootService;
use Game\System\ItemService;
use Game\System\InteractionSystem;
use Game\System\QuestService;
use Game\System\ShopService;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
use Game\Event\Event;
/**
* ServiceContainer: 负责实例化所有核心服务、管理依赖,并注册事件监听器。
*/
class ServiceContainer {
private DatabaseManager $dbManager;
private EventDispatcher $eventDispatcher;
private StateManager $stateManager;
private InputInterface $input;
private OutputInterface $output;
private QuestionHelper $questionHelper;
private SaveLoadService $saveLoadService; // 新增属性
// 存储所有已实例化的服务
private array $services = [];
public function __construct(InputInterface $input, OutputInterface $output, HelperSet $helperSet) {
$this->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;
}
}

23
src/Model/Ability.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace Game\Model;
/**
* Ability: 技能模型
*/
class Ability {
public string $id;
public string $name;
public string $type; // e.g., 'damage', 'heal', 'buff'
public int $manaCost;
public int $power; // 基础威力值
public string $scaling; // 伤害加成属性 (e.g., 'attack', 'mana')
public function __construct(string $id, string $name, string $type, int $manaCost, int $power, string $scaling) {
$this->id = $id;
$this->name = $name;
$this->type = $type;
$this->manaCost = $manaCost;
$this->power = $power;
$this->scaling = $scaling;
}
}

View File

@ -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;
}
}

View File

@ -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,
];
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace Game\System;
use Game\Event\Event;
use Game\Event\EventListenerInterface;
use Game\Event\EventDispatcher;
use Game\Model\Ability;
use Game\Model\Player;
use Game\Model\Enemy; // 假设战斗逻辑中需要 Enemy
/**
* AbilityService: 负责技能的加载、管理和效果计算。
*/
class AbilityService implements EventListenerInterface {
private EventDispatcher $dispatcher;
private StateManager $stateManager;
private array $abilityData; // 存储所有技能配置
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager) {
$this->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()} 施放了 <fg=blue>{$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()} 造成了 <fg=red>{$damage}</> 点伤害!"
]));
break;
case 'heal':
$heal = $player->heal($rawEffect);
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => "💖 恢复了 <fg=green>{$heal}</> 点生命值!"
]));
break;
}
// 4. 触发 BattleService 的事件,让其检查战斗是否结束
$this->dispatcher->dispatch(new Event('AbilityEffectApplied', ['target' => $target]));
}
}

View File

@ -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 ? "<fg=green>" : "<fg=red>";
$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' => '无效的战斗指令。']));

View File

@ -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;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Game\System;
use Game\Event\Event;
use Game\Event\EventListenerInterface;
use Game\Event\EventDispatcher;
use Game\Model\Item;
use Game\Model\Player;
/**
* ItemService: 负责处理物品的使用、装备和消耗逻辑。
*/
class ItemService implements EventListenerInterface {
private EventDispatcher $dispatcher;
private StateManager $stateManager;
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager) {
$this->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' => "使用了物品:<fg=green>{$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' => "❤️ 恢复了 <fg=red>{$healAmount}</> 点生命值当前HP: {$player->getHealth()}/{$player->getMaxHealth()}"
]));
}
// TODO: 未来添加 'buff', 'damage', 'equip' 等效果
}
}

View File

@ -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' => "💰 获得了 <fg=yellow>{$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' => []],
};
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace Game\System;
use Game\Model\Item;
use Game\Model\Player;
use Game\Event\Event;
use Game\Event\EventDispatcher;
/**
* SaveLoadService: 负责将玩家状态持久化到磁盘,并在启动时加载。
*/
class SaveLoadService {
private EventDispatcher $dispatcher;
private StateManager $stateManager;
private string $savePath;
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager, string $savePath = 'save/player.json') {
$this->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;
}
}
}

195
src/System/ShopService.php Normal file
View File

@ -0,0 +1,195 @@
<?php
namespace Game\System;
use Game\Event\Event;
use Game\Event\EventListenerInterface;
use Game\Event\EventDispatcher;
use Game\Model\Item;
use Game\Model\Player;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Question\Question;
/**
* ShopService: 负责处理商店的购买和出售交易。
*/
class ShopService implements EventListenerInterface {
private EventDispatcher $dispatcher;
private StateManager $stateManager;
private InputInterface $input;
private OutputInterface $output;
private QuestionHelper $helper;
// 商店的固定库存(通常从配置加载)
private array $shopInventory;
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager, InputInterface $input, OutputInterface $output, QuestionHelper $helper) {
$this->dispatcher = $dispatcher;
$this->stateManager = $stateManager;
$this->input = $input;
$this->output = $output;
$this->helper = $helper;
$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' => "💰 出售了 <fg=white>{$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("你的金币: <fg=yellow>{$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("[<fg=green>{$itemId}</>] {$name} | 价格: <fg=yellow>{$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("[<fg=green>{$index}</>] <fg=white>{$item->name}</> | 售价: <fg=yellow>{$sellPrice}</> 💰");
}
$this->output->writeln("--------------------------");
}
}

View File

@ -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("<error>UI 错误:无法加载地图区域 {$tileId} {$e->getMessage()}。</error>");
}
break;
case 'ShowInventoryRequest': // ⭐ 新增背包显示请求
$this->displayInventory();
break;
case 'StartBattleEvent':
$this->output->writeln("\n\n<fg=red;options=bold>⚔️ 遭遇战触发!请选择战斗指令...</>");
@ -71,18 +75,22 @@ class UIService implements EventListenerInterface {
* 打印玩家状态信息
*/
private function displayPlayerStats(Player $player): void {
$this->output->writeln("\n--- <info>角色状态:{$player->getName()}</info> ---");
$this->output->writeln("等级: <comment>{$player->getLevel()}</comment>");
$this->output->writeln("HP: <fg=red>{$player->getHealth()}</>/{$player->getMaxHealth()}");
$this->output->writeln("攻击力: <fg=yellow>{$player->getAttack()}</>");
$this->output->writeln("防御力: <fg=blue>{$player->getDefense()}</>");
$this->output->writeln("经验值: {$player->getCurrentXp()}/{$player->getXpToNextLevel()}");
$this->output->writeln("--------------------------\n");
$this->output->writeln("\n--- <fg=green;options=bold>角色状态</> ---");
$this->output->writeln("姓名: <fg=cyan>{$player->getName()}</>");
$this->output->writeln("等级: <fg=yellow>{$player->getLevel()}</> (XP: {$player->getCurrentXp()}/{$player->getXpToNextLevel()})");
$this->output->writeln("生命值: <fg=red>{$player->getHealth()}</>/{$player->getMaxHealth()}");
$this->output->writeln("魔法值: <fg=blue>{$player->getMana()}</>/{$player->getMaxMana()}");
$this->output->writeln("攻击力: <fg=yellow>{$player->getAttack()}</> | 防御力: <fg=blue>{$player->getDefense()}</>");
// ⭐ 新增金币显示
$this->output->writeln("金币: <fg=yellow>{$player->getGold()}</> 💰");
// TODO: 未来显示任务和物品数量
$this->output->writeln("--------------------------");
}
private function displayMainMenu(): void {
$this->output->writeln("\n--- <fg=white>主菜单</> ---");
$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--- <fg=yellow;options=bold>背包 ({$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("[<fg=green>{$index}</>] <fg=white>{$item->name}</> ({$item->type}) | 效果: [{$effects}]");
}
$this->output->writeln("--------------------------");
}
}