diff --git a/config/abilities.json b/config/abilities.json new file mode 100644 index 0000000..dae7115 --- /dev/null +++ b/config/abilities.json @@ -0,0 +1,18 @@ +[ + { + "id": "FIREBALL", + "name": "火球术", + "type": "damage", + "mana_cost": 10, + "power": 25, + "scaling": "mana" + }, + { + "id": "HEAL", + "name": "初级治疗", + "type": "heal", + "mana_cost": 8, + "power": 15, + "scaling": "mana" + } +] \ No newline at end of file diff --git a/config/enemies.json b/config/enemies.json new file mode 100644 index 0000000..c80d90f --- /dev/null +++ b/config/enemies.json @@ -0,0 +1,42 @@ +[ + { + "id": "GOBLIN", + "name": "哥布林", + "health": 30, + "attack": 8, + "defense": 2, + "xp_reward": 20, + "min_gold": 5, + "max_gold": 15, + "loot_table": [ + { + "item_id": 1, + "chance": 70 + }, + { + "item_id": 2, + "chance": 10 + } + ] + }, + { + "id": "WOLF", + "name": "野狼", + "health": 45, + "attack": 12, + "defense": 1, + "xp_reward": 35, + "min_gold": 10, + "max_gold": 25, + "loot_table": [ + { + "item_id": 1, + "chance": 50 + }, + { + "item_id": 4, + "chance": 20 + } + ] + } +] \ No newline at end of file diff --git a/config/items.json b/config/items.json new file mode 100644 index 0000000..d03c4c5 --- /dev/null +++ b/config/items.json @@ -0,0 +1,38 @@ +[ + { + "id": 1, + "name": "小型治疗药水", + "type": "potion", + "description": "恢复少量生命。", + "value": 10, + "effects": {"heal": 20} + }, + { + "id": 2, + "name": "破旧的短剑", + "type": "weapon", + "description": "攻击力微弱。", + "value": 50, + "effects": {}, + "slot": "weapon", + "stat_modifiers": {"attack": 5} + }, + { + "id": 3, + "name": "高级治疗药水", + "type": "potion", + "description": "恢复大量生命。", + "value": 200, + "effects": {"heal": 100} + }, + { + "id": 4, + "name": "布甲头盔", + "type": "armor", + "description": "提供少量防御。", + "value": 30, + "effects": {}, + "slot": "helmet", + "stat_modifiers": {"defense": 3, "health": 10} + } +] \ No newline at end of file diff --git a/config/map_data.json b/config/map_data.json index f010f34..c9efd37 100644 --- a/config/map_data.json +++ b/config/map_data.json @@ -1,20 +1,21 @@ { "TOWN_01": { "name": "新手村", - "description": "安全的小镇,可以休息。", - "connections": {"N": "FIELD_01"}, - "eventPoolId": 0 - }, - "FIELD_01": { - "name": "新手田野", - "description": "微风习习,有低级怪物出没。", - "connections": {"S": "TOWN_01", "E": "FOREST_01"}, - "eventPoolId": 1 + "description": "安全的小镇。", + "connections": {"N": "FOREST_01"}, + "encounter_pool": null, + "encounter_chance": 0.0, + "npc_ids": ["VILLAGER_1", "BLACKSMITH"] }, "FOREST_01": { - "name": "幽暗密林", - "description": "树木茂密,光线昏暗,怪物更强。", - "connections": {"W": "FIELD_01"}, - "eventPoolId": 2 + "name": "新手森林", + "description": "有一些弱小的怪物。", + "connections": {"S": "TOWN_01", "E": "FOREST_02"}, + "encounter_chance": 0.6, + "encounter_pool": [ + {"enemyId": "GOBLIN", "weight": 70}, + {"enemyId": "WOLF", "weight": 30} + ], + "npc_ids": [] } } \ No newline at end of file diff --git a/config/npcs.json b/config/npcs.json new file mode 100644 index 0000000..f04874a --- /dev/null +++ b/config/npcs.json @@ -0,0 +1,18 @@ +{ + "VILLAGER_1": { + "name": "老村长", + "dialogue": { + "greeting": "你好,旅行者。你看起来很强大。", + "quest_response": "你想要帮忙吗?我们的地窖里有老鼠。", + "shop_response": "我现在没有东西卖给你。" + } + }, + "BLACKSMITH": { + "name": "铁匠李奥", + "dialogue": { + "greeting": "欢迎来到我的铁匠铺。", + "quest_response": "你有空吗?我的铁矿用完了。", + "shop_response": "看一看你需要什么工具和武器。" + } + } +} \ No newline at end of file diff --git a/config/quests.json b/config/quests.json new file mode 100644 index 0000000..d1ee3d5 --- /dev/null +++ b/config/quests.json @@ -0,0 +1,36 @@ +[ + { + "id": "KILL_GOBLIN", + "name": "新手挑战:击败哥布林", + "description": "前往附近的森林,击败一只哥布林来证明你的勇气。", + "type": "kill", + "target_npc_id": "VILLAGER_1", + "target": { + "entityId": "GOBLIN", + "count": 1 + }, + "required_level": 1, + "rewards": { + "xp": 50, + "gold": 20, + "item_id": 1, + "item_quantity": 1 + }, + "next_quest_id": "FIND_NPC" + }, + { + "id": "FIND_NPC", + "name": "寻找铁匠", + "description": "铁匠似乎有重要的消息要告诉你,去镇中心找他。", + "type": "talk", + "target": { + "npcId": "BLACKSMITH", + "count": 1 + }, + "required_level": 1, + "rewards": { + "xp": 100 + }, + "next_quest_id": null + } +] \ No newline at end of file diff --git a/save/player.json b/save/player.json index 1c3ec1d..bf06f81 100644 --- a/save/player.json +++ b/save/player.json @@ -1,21 +1,61 @@ { "name": "hant", - "health": 100, - "maxHealth": 100, - "attack": 15, - "defense": 5, - "level": 1, - "currentXp": 10, - "xpToNextLevel": 100, - "gold": 7, + "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 + } + }, { "id": 1, "name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34", "type": "potion", "description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002", "value": 10, - "effects": [] + "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": [], diff --git a/src/Core/ServiceContainer.php b/src/Core/ServiceContainer.php index ae0b452..fb9eb97 100644 --- a/src/Core/ServiceContainer.php +++ b/src/Core/ServiceContainer.php @@ -2,10 +2,17 @@ namespace Game\Core; // 导入所有依赖和系统服务 +use Game\Database\AbilityRepository; use Game\Database\DatabaseManager; +use Game\Database\EnemyRepository; +use Game\Database\ItemRepository; +use Game\Database\JsonFileLoader; +use Game\Database\NPCRepository; +use Game\Database\QuestRepository; use Game\Event\EventDispatcher; use Game\Event\EventListenerInterface; use Game\System\AbilityService; +use Game\System\EquipmentService; use Game\System\SaveLoadService; use Game\System\StateManager; use Game\System\UIService; @@ -39,6 +46,11 @@ class ServiceContainer { private SaveLoadService $saveLoadService; // 新增属性 // 存储所有已实例化的服务 private array $services = []; + private ItemRepository $itemRepository; + private EnemyRepository $enemyRepository; + private AbilityRepository $abilityRepository; + private QuestRepository $questionRepository; + private NPCRepository $npcRepository; public function __construct(InputInterface $input, OutputInterface $output, HelperSet $helperSet) { $this->input = $input; @@ -66,20 +78,32 @@ 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->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(MapSystem::class, new MapSystem($this->eventDispatcher, $this->stateManager,$this->npcRepository)); $this->register(CharacterService::class, new CharacterService($this->eventDispatcher, $this->stateManager)); - $this->register(LootService::class, new LootService($this->eventDispatcher, $this->stateManager)); + $this->register(LootService::class, new LootService($this->eventDispatcher, $this->stateManager, $this->itemRepository, $this->enemyRepository)); $this->register(ItemService::class, new ItemService($this->eventDispatcher, $this->stateManager)); - $this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager)); + $this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager, $this->questionRepository)); + // ⭐ 实例化 EquipmentService + $this->register(EquipmentService::class, new EquipmentService($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) + new InteractionSystem($this->eventDispatcher, $this->stateManager, $this->npcRepository, $this->input, $this->output, $this->questionHelper) ); $this->register(BattleService::class, new BattleService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper) @@ -89,12 +113,12 @@ class ServiceContainer { ); // ⭐ 实例化 AbilityService - $abilityService = new AbilityService($this->eventDispatcher, $this->stateManager); + $abilityService = new AbilityService($this->eventDispatcher, $this->stateManager, $this->abilityRepository); $this->register(AbilityService::class, $abilityService); // 4. 输入处理服务 (驱动主循环,必须最后注册,因为它依赖于所有 I/O 组件) $this->register(InputHandler::class, - new InputHandler($this->eventDispatcher, $this->input, $this->output, $this->questionHelper) + new InputHandler($this->eventDispatcher, $this->input, $this->output, $this->questionHelper,$this->stateManager) ); // 返回分发器和状态管理器,供 GameCommand 使用 @@ -124,4 +148,9 @@ class ServiceContainer { public function getStateManager(): StateManager { return $this->stateManager; } + + // ⭐ 新增 Repository Getter (供需要配置数据的服务使用) + public function getItemRepository(): ItemRepository { return $this->itemRepository; } + public function getEnemyRepository(): EnemyRepository { return $this->enemyRepository; } + public function getAbilityRepository(): AbilityRepository { return $this->abilityRepository; } } \ No newline at end of file diff --git a/src/Database/AbilityRepository.php b/src/Database/AbilityRepository.php new file mode 100644 index 0000000..54b927d --- /dev/null +++ b/src/Database/AbilityRepository.php @@ -0,0 +1,19 @@ +data = $loader->load('abilities.json'); + $this->data = array_combine(array_column($this->data, 'id'), $this->data); + } + + public function find(int|string $id): ?array { + return $this->data[$id] ?? null; + } + + public function findAll(): array { + return $this->data; + } +} \ No newline at end of file diff --git a/src/Database/EnemyRepository.php b/src/Database/EnemyRepository.php new file mode 100644 index 0000000..d5c7304 --- /dev/null +++ b/src/Database/EnemyRepository.php @@ -0,0 +1,20 @@ +data = $loader->load('enemies.json'); + // 将数组键设为 ID,方便查找 + $this->data = array_combine(array_column($this->data, 'id'), $this->data); + } + + public function find(int|string $id): ?array { + return $this->data[$id] ?? null; + } + + public function findAll(): array { + return $this->data; + } +} \ No newline at end of file diff --git a/src/Database/ItemRepository.php b/src/Database/ItemRepository.php new file mode 100644 index 0000000..b0e617c --- /dev/null +++ b/src/Database/ItemRepository.php @@ -0,0 +1,45 @@ +data = $loader->load('items.json'); + // 将数组键设为 ID,方便查找 + $this->data = array_combine(array_column($this->data, 'id'), $this->data); + } + + public function find(int|string $id): ?array { + return $this->data[$id] ?? null; + } + + public function findAll(): array { + return $this->data; + } + + /** + * 辅助方法:通过 ID 创建 Item 实例 + */ + public function createItem(int $id): ?Item { + $itemData = $this->find($id); + if (!$itemData) { + return null; + } + + // 确保所有必要的键都存在,并处理下划线到驼峰的转换 + return new Item( + $itemData['id'], + $itemData['name'], + $itemData['type'], + $itemData['description'], + $itemData['value'], + $itemData['effects'] ?? [], + $itemData['slot'] ?? null, + $itemData['stat_modifiers'] ?? [] + ); + } +} \ No newline at end of file diff --git a/src/Database/JsonFileLoader.php b/src/Database/JsonFileLoader.php new file mode 100644 index 0000000..2e26381 --- /dev/null +++ b/src/Database/JsonFileLoader.php @@ -0,0 +1,24 @@ +basePath . $filename; + if (!file_exists($filePath)) { + throw new \RuntimeException("配置加载失败:文件不存在于 {$filePath}"); + } + + $content = file_get_contents($filePath); + $data = json_decode($content, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \RuntimeException("配置加载失败:{$filename} 不是有效的 JSON 格式。"); + } + return $data; + } +} \ No newline at end of file diff --git a/src/Database/NPCRepository.php b/src/Database/NPCRepository.php new file mode 100644 index 0000000..5e53647 --- /dev/null +++ b/src/Database/NPCRepository.php @@ -0,0 +1,38 @@ +data = $loader->load('npcs.json'); + } + + public function find(int|string $id): ?array { + return $this->data[$id] ?? null; + } + + public function findAll(): array { + return $this->data; + } + + /** + * 辅助方法:创建 NPC 模型实例 + */ + public function createNPC(string $id): ?NPC { + $data = $this->find($id); + if (!$data) { + return null; + } + + // 假设 NPC 模型的构造函数是 public function __construct(string $id, string $name, array $dialogue) + return new NPC( + $id, + $data['name'], + $data['dialogue'] + ); + } +} \ No newline at end of file diff --git a/src/Database/QuestRepository.php b/src/Database/QuestRepository.php new file mode 100644 index 0000000..7e1871f --- /dev/null +++ b/src/Database/QuestRepository.php @@ -0,0 +1,59 @@ +load('quests.json'); + + // 使用 ID 作为键进行存储 (原始 data) + $this->data = array_combine(array_column($loadedData, 'id'), $loadedData); + + // ⭐ 构建 questsByNpc 索引 + $this->buildNpcQuestIndex(); + } + + /** + * ⭐ 新增方法:构建按 NPC ID 查找任务的索引 + */ + private function buildNpcQuestIndex(): void { + foreach ($this->data as $questId => $questData) { + // 检查任务是否有目标 NPC ID (target_npc_id) 或目标实体 ID (target_entity_id) + // 根据您提供的 quests.json + $targetId = $questData['target_npc_id'] ?? null; + + if ($targetId) { + // 将任务 ID 添加到该 NPC 对应的列表 + $this->questsByNpc[$targetId][] = $questId; + } + } + } + + public function find(int|string $id): ?array { + return $this->data[$id] ?? null; + } + + public function findAll(): array { + return $this->data; + } + + /** + * ⭐ 新增功能:通过 NPC ID 获取所有相关的任务 ID 列表 + * 这个列表可能包含起始任务、后续任务等,需要业务逻辑进一步筛选 + */ + public function getQuestsByNpc(string $npcId): array { + // 返回该 NPC ID 对应的所有任务 ID 列表,如果没有则返回空数组 + return $this->questsByNpc[$npcId] ?? []; + } + + /** + * 辅助方法:获取起始任务的 ID (方便游戏启动) + */ + public function getStartingQuestId(): ?string { + // 假设第一个任务就是起始任务 + return array_key_first($this->data); + } +} \ No newline at end of file diff --git a/src/Database/RepositoryInterface.php b/src/Database/RepositoryInterface.php new file mode 100644 index 0000000..f53f611 --- /dev/null +++ b/src/Database/RepositoryInterface.php @@ -0,0 +1,14 @@ +mana; } - public function getMaxMana(): int { return $this->maxMana; } public function spendMana(int $cost): bool { if ($this->mana >= $cost) { @@ -45,14 +45,65 @@ class Character { $this->mana = $maxMana; } + // ⭐ 新增方法:属性提升 + public function increaseMaxHealth(int $amount): void { + $this->maxHealth += $amount; + // 提升最大生命值后,也要恢复当前生命值到新上限 + $this->health = $this->maxHealth; + } + + public function increaseAttack(int $amount): void { + $this->attack += $amount; + } + + public function increaseDefense(int $amount): void { + $this->defense += $amount; + } + + // ⭐ 新增方法:恢复所有资源 (升级时的福利) + public function fullRestore(): void { + $this->health = $this->maxHealth; + if (property_exists($this, 'mana')) { // 检查 Mana 属性是否存在 + $this->mana = $this->maxMana; + } + } + + public function getAttack(): int { + // 基础攻击力 + 装备修正 + return $this->attack + $this->getStatModifier('attack'); + } + + /** + * 获取修正后的防御力 + */ + public function getDefense(): int { + // 基础防御力 + 装备修正 + return $this->defense + $this->getStatModifier('defense'); + } + + // MaxHealth 和 MaxMana 也可以类似处理 + public function getMaxHealth(): int { + return $this->maxHealth + $this->getStatModifier('health'); + } + + public function getMaxMana(): int { + return $this->maxMana + $this->getStatModifier('mana'); + } + public function getName(): string { return $this->name; } public function getHealth(): int { return $this->health; } - public function getMaxHealth(): int { return $this->maxHealth; } - public function getAttack(): int { return $this->attack; } - public function getDefense(): int { return $this->defense; } public function isAlive(): bool { return $this->health > 0; } - + // ⭐ 新增:获取某个槽位的属性修正总和 + public function getStatModifier(string $statName): int { + $totalModifier = 0; + foreach ($this->equipment as $item) { + if ($item && isset($item->statModifiers[$statName])) { + $totalModifier += $item->statModifiers[$statName]; + } + } + return $totalModifier; + } /** * 接收伤害,返回实际受到的伤害量 */ diff --git a/src/Model/Enemy.php b/src/Model/Enemy.php index d398600..5420351 100644 --- a/src/Model/Enemy.php +++ b/src/Model/Enemy.php @@ -5,18 +5,24 @@ namespace Game\Model; class Enemy extends Character { public string $id; public int $xpValue; // 击败后获得的经验值 + // ⭐ 新增属性:存储该敌人的掉落表配置 + protected array $lootTable; - public function __construct(string $id, string $name, int $health, int $attack, int $defense, int $xpValue) { + // ⭐ 修正构造函数:增加 $lootTable 参数 + public function __construct(string $id, string $name, int $health, int $attack, int $defense, int $xpValue, array $lootTable) { // 调用父类 (Character) 的构造函数来初始化核心属性 parent::__construct($name, $health, $attack, $defense); $this->id = $id; $this->xpValue = $xpValue; + $this->lootTable = $lootTable; // ⭐ 赋值 } // Enemy 特有的 Getter 方法 public function getId(): string { return $this->id; } public function getXpValue(): int { return $this->xpValue; } + // ⭐ 新增 Getter + public function getLootTable(): array { return $this->lootTable; } // 注意:takeDamage() 和 isAlive() 等方法都直接继承自 Character,无需重复实现! } \ No newline at end of file diff --git a/src/Model/Item.php b/src/Model/Item.php index 3be8179..c122db5 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -11,13 +11,24 @@ class Item { public string $description; public int $value; // 卖出价格 public array $effects; // ⭐ 新增:存储效果参数 e.g., ['heal' => 20] - public function __construct(int $id, string $name, string $type, string $description, int $value, array $effects = []) { + + + // ⭐ 新增:装备槽位 (null/none 表示非装备品) + public ?string $slot = null; // e.g., 'weapon', 'helmet', 'armor' + + // ⭐ 新增:装备属性修正 (影响角色属性) + public array $statModifiers = []; // e.g., ['attack' => 5, 'defense' => 2] + public function __construct(int $id, string $name, string $type, string $description, int $value, array $effects = [], ?string $slot = null, array $statModifiers = []) { $this->id = $id; $this->name = $name; $this->type = $type; $this->description = $description; $this->value = $value; - $this->effects = $effects; // 赋值 + $this->effects = $effects; + + // ⭐ 初始化新属性 + $this->slot = $slot; + $this->statModifiers = $statModifiers; } public function toArray() @@ -29,6 +40,8 @@ class Item { 'description' => $this->description, 'value' => $this->value, 'effects' => $this->effects, + 'slot' => $this->slot, + 'statModifiers' => $this->statModifiers, ]; } } \ No newline at end of file diff --git a/src/Model/MapTile.php b/src/Model/MapTile.php index 03ee8a0..a0b0682 100644 --- a/src/Model/MapTile.php +++ b/src/Model/MapTile.php @@ -1,21 +1,23 @@ 'FOREST_02', 'S' => 'TOWN_01'] - public int $eventPoolId; // 区域对应的事件池ID - - public function __construct(string $id, string $name, string $description, array $connections, int $eventPoolId) { + public array $connections; + // ⭐ 修正/新增属性:遇敌配置 + public ?array $encounterPool; // null 或包含 {"enemyId": "ID", "weight": N} 的数组 + public float $encounterChance; // 遇敌几率 (0.0 到 1.0) + public array $npcIds; + public function __construct(string $id, string $name, string $description, array $connections, ?array $encounterPool, float $encounterChance,array $npcIds = []) { $this->id = $id; $this->name = $name; $this->description = $description; $this->connections = $connections; - $this->eventPoolId = $eventPoolId; + // ⭐ 赋值 + $this->encounterPool = $encounterPool; + $this->encounterChance = $encounterChance; + $this->npcIds = $npcIds; // ⭐ 赋值 } } \ No newline at end of file diff --git a/src/Model/Player.php b/src/Model/Player.php index c10f0a3..aa1288d 100644 --- a/src/Model/Player.php +++ b/src/Model/Player.php @@ -9,12 +9,55 @@ class Player extends Character { // ⭐ 新增:已学习的技能列表 protected array $abilities = []; // 存储 Ability 实例 - +// ⭐ 新增:装备槽位,键是槽位名,值是 Item 对象 + protected array $equipment = [ + 'weapon' => null, + 'armor' => null, + 'helmet' => null, + // 可根据需要添加 'ring', 'amulet' 等 + ]; public function __construct(string $name, int $maxHealth, int $attack, int $defense,int $maxMana = 50) { // 调用父类 (Character) 的构造函数来初始化核心属性 parent::__construct($name, $maxHealth, $attack, $defense,$maxMana); } + // ⭐ 新增:获取装备 + public function getEquipment(): array { + return $this->equipment; + } + + // ⭐ 新增:设置装备 (由 EquipmentService 调用) + public function setEquipment(string $slot, ?Item $item): void { + $this->equipment[$slot] = $item; + } + +// ⭐ 新增方法:提升等级 + public function incrementLevel(): void { + $this->level++; + } + + // ⭐ 新增方法:扣除升级所需的经验值并计算新的经验值目标 + public function subtractXpToNextLevel(): void { + // 计算溢出的经验值(如果有) + $excessXp = $this->currentXp - $this->xpToNextLevel; + + // 设置新的经验值 + $this->currentXp = $excessXp; + + // 计算下一次升级所需的经验值 (简化公式:增加 50% 难度) + $this->xpToNextLevel = (int)($this->xpToNextLevel * 1.5); + } + + // ⭐ 新增方法:设置经验值 (用于存档加载) + public function setXpToNextLevel(int $xp): void { + $this->xpToNextLevel = $xp; + } + + // ⭐ 确保 setLevel 存在 (用于存档加载) + public function setLevel(int $level): void { + $this->level = $level; + } + // ⭐ 新增方法:学习技能 public function learnAbility(Ability $ability): void { $this->abilities[$ability->id] = $ability; @@ -38,14 +81,27 @@ class Player extends Character { $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]; + /** + * @var Quest[] 当前活跃任务列表 (键为 Quest ID) + */ + protected array $activeQuests = []; + // ... + + // ⭐ 修正方法:接受 Quest 对象 + public function addActiveQuest(Quest $quest): void { + // 使用任务 ID 作为键,存储整个 Quest 对象 + $this->activeQuests[$quest->getId()] = $quest; + } + + // ⭐ 确保 setInventory 存在 (用于存档加载) + // 如果存档时将 Quest 对象序列化了,这里可能需要反序列化逻辑, + // 但现在我们只确保它可以接受数组。 + public function setActiveQuests(array $quests): void { + $this->activeQuests = $quests; } - // ⭐ 新增方法:获取进行中的任务 public function getActiveQuests(): array { return $this->activeQuests; } @@ -114,9 +170,8 @@ class Player extends Character { } // ⭐ 新增 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 实例化 @@ -125,7 +180,6 @@ class Player extends Character { } 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) { diff --git a/src/Model/Quest.php b/src/Model/Quest.php index 18810f7..145359c 100644 --- a/src/Model/Quest.php +++ b/src/Model/Quest.php @@ -2,17 +2,21 @@ namespace Game\Model; /** - * Quest: 任务模型 + * Quest: 任务模型 - 包含配置数据和运行时进度 */ class Quest { + // --- 配置数据 (来自 JSON) --- public string $id; public string $name; public string $description; - public string $type; // e.g., 'kill', 'fetch', 'talk' - public array $target; // 目标要求,e.g., ['targetId' => 'GOBLIN_1', 'count' => 5] - public array $rewards; // 奖励,e.g., ['xp' => 100, 'itemId' => 1] + public string $type; + public array $target; // e.g., ['entityId' => 'GOBLIN', 'count' => 1] + public array $rewards; public bool $isRepeatable = false; + // ⭐ 新增:运行时状态,默认为 0 + protected int $currentCount = 0; + public function __construct(string $id, string $name, string $description, string $type, array $target, array $rewards) { $this->id = $id; $this->name = $name; @@ -21,4 +25,58 @@ class Quest { $this->target = $target; $this->rewards = $rewards; } + + // --- 进度管理方法 (用于业务逻辑) --- + + /** + * 增加任务当前进度 + */ + public function incrementCurrentCount(int $amount = 1): void { + // 目标数量存储在 $this->target['count'] 中 + $targetCount = $this->target['count'] ?? 1; + $this->currentCount = min($targetCount, $this->currentCount + $amount); + } + + /** + * 检查任务是否完成 + */ + public function isCompleted(): bool { + $targetCount = $this->target['count'] ?? 1; + return $this->currentCount >= $targetCount; + } + + // --- Getter/Setter (用于 UI 和 SaveLoadService) --- + + public function getName(): string { return $this->name; } + public function getDescription(): string { return $this->description; } + public function getId(): string { return $this->id; } + public function getType(): string { return $this->type; } + public function getTarget(): array { return $this->target; } + public function getRewards(): array { return $this->rewards; } + + /** + * 获取当前进度 (用于存档) + */ + public function getCurrentCount(): int { return $this->currentCount; } + + /** + * ⭐ 核心修正:设置当前进度 (用于存档加载) + */ + public function setCurrentCount(int $count): void { + $this->currentCount = $count; + } + + public function toArray() + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'type' => $this->type, + 'target' => $this->target, + 'rewards' => $this->rewards, + 'isRepeatable' => $this->isRepeatable, + 'currentCount' => $this->currentCount, + ]; + } } \ No newline at end of file diff --git a/src/System/AbilityService.php b/src/System/AbilityService.php index 76b9737..77de812 100644 --- a/src/System/AbilityService.php +++ b/src/System/AbilityService.php @@ -1,6 +1,7 @@ dispatcher = $dispatcher; $this->stateManager = $stateManager; - $this->loadAbilityData(); + $this->abilityRepository = $abilityRepository; } - 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'])); + $initialAbilities = ['FIREBALL', 'HEAL']; // 仍然可以硬编码初始技能的ID + + foreach ($initialAbilities as $id) { + // ⭐ 使用 Repository 获取数据 + $data = $this->abilityRepository->find($id); + if ($data) { + $player->learnAbility(new Ability( + $data['id'], + $data['name'], + $data['type'], + $data['mana_cost'], + $data['power'], + $data['scaling'] + )); + } } } diff --git a/src/System/BattleService.php b/src/System/BattleService.php index 1c436c9..09ad34e 100644 --- a/src/System/BattleService.php +++ b/src/System/BattleService.php @@ -5,7 +5,6 @@ use Game\Event\Event; use Game\Event\EventListenerInterface; use Game\Event\EventDispatcher; use Game\Model\Enemy; -use Game\Model\Player; // 即使是 Party,也依赖 Player use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Helper\QuestionHelper; @@ -54,8 +53,7 @@ class BattleService implements EventListenerInterface { switch ($event->getType()) { case 'StartBattleEvent': // 收到 MapSystem 触发的战斗开始事件 - $enemyId = $event->getPayload()['enemyId']; - $this->startBattle($enemyId); + $this->startBattle($event->getPayload()['enemy']); break; } } @@ -63,17 +61,8 @@ class BattleService implements EventListenerInterface { /** * 1. 初始化战斗状态 */ - private function startBattle(int $enemyId): void { - // ... 初始化敌人逻辑 (继承自 Character) ... - $enemyData = $this->loadEnemyData($enemyId); - $this->currentEnemy = new Enemy( - (string)$enemyId, - $enemyData['name'], - $enemyData['health'], - $enemyData['attack'], - $enemyData['defense'], - $enemyData['xp'] - ); + private function startBattle(Enemy $enemy): void { + $this->currentEnemy = $enemy; $this->inBattle = true; $this->dispatcher->dispatch(new Event('SystemMessage', [ diff --git a/src/System/CharacterService.php b/src/System/CharacterService.php index dd4d5f3..84f854d 100644 --- a/src/System/CharacterService.php +++ b/src/System/CharacterService.php @@ -58,19 +58,20 @@ class CharacterService implements EventListenerInterface { * 执行升级操作 */ private function levelUp(Player $player): void { + + // 1. 扣除经验,提升等级 - $player->subtractXpToNextLevel(); // 假设我们在 Player 模型中添加此方法 - $player->incrementLevel(); // 假设我们在 Player 模型中添加此方法 + $player->subtractXpToNextLevel(); + $player->incrementLevel(); - // 2. 提升属性 (简化版:每次升级增加固定属性) - $player->increaseMaxHealth(10); - $player->increaseAttack(2); - $player->increaseDefense(1); + // 2. 提升属性 (现在这些方法在 Character/Player 中都已实现) + $player->increaseMaxHealth(15); // 增加生命 + $player->increaseAttack(3); // 增加攻击 + $player->increaseDefense(2); // 增加防御 + $player->restoreMana(10); // 恢复一些 Mana (或增加 maxMana) + $player->fullRestore(); // 恢复所有生命和魔法 - // 3. 升级后回满血 - $player->heal($player->getMaxHealth()); // 使用 Character 基类中的 heal() - - // 4. 触发升级事件 (如果需要其他系统知道) + // 3. 触发升级事件 (如果需要其他系统知道) $this->dispatcher->dispatch(new Event('LevelUpEvent', ['newLevel' => $player->getLevel()])); } } \ No newline at end of file diff --git a/src/System/EquipmentService.php b/src/System/EquipmentService.php new file mode 100644 index 0000000..32e277a --- /dev/null +++ b/src/System/EquipmentService.php @@ -0,0 +1,108 @@ +dispatcher = $dispatcher; + $this->stateManager = $stateManager; + } + + public function handleEvent(Event $event): void { + switch ($event->getType()) { + case 'EquipItemEvent': + $payload = $event->getPayload(); + $this->handleEquipItem($payload['itemIndex']); + break; + case 'UnequipItemEvent': + $payload = $event->getPayload(); + $this->handleUnequipItem($payload['slot']); + break; + } + } + + /** + * 处理装备请求 + */ + private function handleEquipItem(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]; + $slot = $item->slot; + + if (!$slot) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 该物品不是装备品。'])); + return; + } + + // 1. 尝试卸下当前装备 + $currentEquipment = $player->getEquipment()[$slot]; + if ($currentEquipment) { + $this->unequipItem($player, $slot, $currentEquipment); + } + + // 2. 装备新物品 + $player->setEquipment($slot, $item); + + // 3. 将物品从背包移除(它现在在装备槽中) + $player->removeItemByIndex($itemIndex); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "⚔️ 装备了 {$item->name} 到 {$slot} 槽位!" + ])); + + // 触发状态/UI更新 + $this->dispatcher->dispatch(new Event('StatUpdateEvent')); + $this->dispatcher->dispatch(new Event('InventoryUpdateEvent')); + } + + /** + * 辅助方法:卸下物品 + */ + private function unequipItem(Player $player, string $slot, Item $itemToUnequip): void { + // 1. 将物品放回背包 (需要 ItemService 保证 Item 对象正确放回) + $player->addItem($itemToUnequip); + + // 2. 清空槽位 + $player->setEquipment($slot, null); + + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "🗑️ 卸下了 {$itemToUnequip->name} 并放回背包。" + ])); + } + + /** + * 处理卸下请求 + */ + private function handleUnequipItem(string $slot): void { + $player = $this->stateManager->getPlayer(); + $item = $player->getEquipment()[$slot] ?? null; + + if (!$item) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ {$slot} 槽位没有装备物品。"])); + return; + } + + $this->unequipItem($player, $slot, $item); + $this->dispatcher->dispatch(new Event('StatUpdateEvent')); + $this->dispatcher->dispatch(new Event('InventoryUpdateEvent')); + } +} \ No newline at end of file diff --git a/src/System/InputHandler.php b/src/System/InputHandler.php index f21c3d3..2e4cc5a 100644 --- a/src/System/InputHandler.php +++ b/src/System/InputHandler.php @@ -18,10 +18,12 @@ class InputHandler { private InputInterface $input; private OutputInterface $output; private QuestionHelper $helper; + private StateManager $stateManager; - public function __construct(EventDispatcher $dispatcher, InputInterface $input, OutputInterface $output, QuestionHelper $helper) { + public function __construct(EventDispatcher $dispatcher, InputInterface $input, OutputInterface $output, QuestionHelper $helper, StateManager $stateManager) { $this->dispatcher = $dispatcher; $this->input = $input; + $this->stateManager = $stateManager; $this->output = $output; $this->helper = $helper; } @@ -56,6 +58,9 @@ class InputHandler { // 状态显示请求,由于 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'])); @@ -88,10 +93,10 @@ class InputHandler { * 处理背包输入和子菜单 */ private function handleInventoryInput(): void { - // 通知 UI 打印背包内容 + // 1. 显示背包 $this->dispatcher->dispatch(new Event('ShowInventoryRequest')); - $question = new Question("> 请输入要使用的物品编号 (或 [X] 退出):"); + $question = new Question("> 请输入物品编号进行操作 ([E]装备/使用 | [U]卸下 | [X]退出):"); $choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? ''); if ($choice === 'X') { @@ -101,10 +106,65 @@ class InputHandler { if (is_numeric($choice)) { $itemIndex = (int)$choice; - // 触发使用物品事件,ItemService 监听并处理 - $this->dispatcher->dispatch(new Event('UseItemEvent', ['itemIndex' => $itemIndex])); + $this->handleItemAction($itemIndex); // ⭐ 移交给新方法处理 + } elseif ($choice === 'U') { + $this->handleUnequipAction(); } else { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的编号。'])); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的指令。请使用编号,E, U, X。'])); + } + } + + /** + * ⭐ 新增:处理单个物品的使用/装备逻辑 + */ + private function handleItemAction(int $itemIndex): void { + $player = $this->stateManager->getPlayer(); + $item = $player->getInventory()[$itemIndex] ?? null; + + if (!$item) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的物品编号。'])); + return; + } + + 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' => '该物品无法使用或装备。'])); + } + } + + /** + * ⭐ 新增:处理卸下逻辑 + */ + 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; + } + } + + if (empty($availableSlots)) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '没有物品可以卸下。'])); + return; + } + + $question = new Question("> 输入槽位名称 (e.g., weapon) 或 X 取消:"); + $slotChoice = strtolower($this->helper->ask($this->input, $this->output, $question) ?? ''); + + if ($slotChoice !== 'x' && in_array($slotChoice, $availableSlots)) { + $this->dispatcher->dispatch(new Event('UnequipItemEvent', ['slot' => $slotChoice])); } } public function setSaveLoadService(SaveLoadService $service): void { diff --git a/src/System/InteractionSystem.php b/src/System/InteractionSystem.php index 28fc6ff..eed391a 100644 --- a/src/System/InteractionSystem.php +++ b/src/System/InteractionSystem.php @@ -1,6 +1,7 @@ dispatcher = $dispatcher; $this->stateManager = $stateManager; + $this->npcRepository = $npcRepository; // ⭐ 赋值 $this->input = $input; $this->output = $output; $this->helper = $helper; @@ -38,6 +41,11 @@ class InteractionSystem implements EventListenerInterface { $npcId = $event->getPayload()['npcId']; $this->startInteraction($npcId); break; + // ⭐ 修正事件名称 + case 'StartInteractionEvent': + $npcId = $event->getPayload()['npcId']; + $this->startInteraction($npcId); + break; } } @@ -45,9 +53,11 @@ class InteractionSystem implements EventListenerInterface { * 1. 初始化 NPC 交互 */ private function startInteraction(string $npcId): void { - $npc = $this->loadNPC($npcId); + // ⭐ 修正:使用 Repository 加载 NPC + $npc = $this->npcRepository->createNPC($npcId); + if (!$npc) { - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里没有人可以交谈。"])); + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里没有人可以交谈 ({$npcId} 不存在)。"])); return; } diff --git a/src/System/LootService.php b/src/System/LootService.php index 3af88a6..9602866 100644 --- a/src/System/LootService.php +++ b/src/System/LootService.php @@ -1,9 +1,12 @@ dispatcher = $dispatcher; $this->stateManager = $stateManager; + $this->itemRepository = $itemRepository; + $this->enemyRepository = $enemyRepository; + } + + public function createEnemy(string $id): ?Enemy { + // ⭐ 使用 Repository 获取数据 + $data = $this->enemyRepository->find($id); + if (!$data) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 错误:敌人ID '{$id}' 不存在。"])); + return null; + } + + // ⭐ 修正实例化 Enemy:传入 loot_table + return new Enemy( + $data['id'], + $data['name'], + $data['health'], + $data['attack'], + $data['defense'], + $data['xp_reward'], + $data['loot_table'] ?? [] // 传递 loot_table + ); } public function handleEvent(Event $event): void { switch ($event->getType()) { case 'LootDropEvent': $enemyId = $event->getPayload()['enemyId']; + // LootDropEvent 应该由 BattleService 在敌人死亡时触发 $this->handleLootDrop($enemyId); break; case 'LootFoundEvent': // 响应 MapSystem 探索时发现的宝箱 $lootId = $event->getPayload()['lootId']; $this->handleLootFound($lootId); break; - // ... 现有事件 ... case 'ShopPurchaseEvent': // ⭐ 响应商店购买 $itemId = $event->getPayload()['itemId']; $this->giveItemToPlayer($itemId); break; + // ⭐ 响应遇敌事件 + case 'EncounterEnemyEvent': + $enemyId = $event->getPayload()['enemyId']; + $newEnemy = $this->createEnemy($enemyId); + + if ($newEnemy) { + // 通知 BattleService 启动战斗 + $this->dispatcher->dispatch(new Event('StartBattleEvent', ['enemy' => $newEnemy])); + } + break; } } /** - * 处理敌人死亡时的掉落逻辑 + * ⭐ 修正方法:处理敌人死亡时的掉落逻辑(从配置中加载数据) */ private function handleLootDrop(string $enemyId): void { - $goldAmount = rand(5, 15); // 随机掉落 5 到 15 金币 - // 1. 发放金币 + // 1. 获取敌人配置数据 + $enemyData = $this->enemyRepository->find($enemyId); + + if (!$enemyData) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 错误:无法找到敌人 {$enemyId} 的掉落配置。"])); + return; + } + + // --- 2. 掉落金币 (从配置中获取范围) --- + $minGold = $enemyData['min_gold'] ?? 5; // 使用配置,默认 5 + $maxGold = $enemyData['max_gold'] ?? 15; // 使用配置,默认 15 + + $goldAmount = rand($minGold, $maxGold); $player = $this->stateManager->getPlayer(); $player->gainGold($goldAmount); @@ -51,10 +100,19 @@ class LootService implements EventListenerInterface { 'message' => "💰 获得了 {$goldAmount} 金币。" ])); - // 简化:总是掉落物品 ID 1 (小药水) - $roll = rand(1, 100); - if ($roll <= 70) { // 70% 掉落几率 - $this->giveItemToPlayer(1); + // --- 3. 掉落物品 (从配置的 loot_table 中获取) --- + $lootTable = $enemyData['loot_table'] ?? []; + + if (!empty($lootTable)) { + $droppedItemId = $this->getRandomItemIdFromLootTable($lootTable); + + if ($droppedItemId !== null) { // 成功掉落物品 + $this->giveItemToPlayer($droppedItemId); + } else { + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "战利品很少,只找到了一些零钱。" + ])); + } } else { $this->dispatcher->dispatch(new Event('SystemMessage', [ 'message' => "战利品很少,只找到了一些零钱。" @@ -63,10 +121,37 @@ class LootService implements EventListenerInterface { } /** - * 处理探索时发现的宝箱/固定物品 + * ⭐ 新增方法:根据权重随机选择物品 ID + */ + private function getRandomItemIdFromLootTable(array $lootTable): ?int { + $totalChance = array_sum(array_column($lootTable, 'chance')); + + // 随机数范围是 1 到 100 + $randValue = rand(1, 100); + + // 如果随机值大于总几率,则表示未掉落任何物品 + // 例如:总几率为 80,随机数是 85,则不掉落 + if ($randValue > $totalChance) { + return null; + } + + $currentChance = 0; + foreach ($lootTable as $item) { + $currentChance += $item['chance']; + if ($randValue <= $currentChance) { + // 确保 item_id 是整数类型 + return (int)$item['item_id']; + } + } + + return null; + } + + /** + * 处理探索时发现的宝箱/固定物品 (保持不变) */ private function handleLootFound(int $lootId): void { - // 假设 lootId 5 是一个宝箱,里面是物品 ID 2 + // ... if ($lootId === 5) { $this->dispatcher->dispatch(new Event('SystemMessage', [ 'message' => "🗝️ 你打开了宝箱!" @@ -76,18 +161,16 @@ class LootService implements EventListenerInterface { } /** - * 核心逻辑:创建物品实例并添加到玩家背包 + * 核心逻辑:创建物品实例并添加到玩家背包 (保持不变) */ private function giveItemToPlayer(int $itemId): void { - $itemData = $this->loadItemData($itemId); + // ... + $item = $this->itemRepository->createItem($itemId); - $item = new Item( - $itemId, - $itemData['name'], - $itemData['type'], - $itemData['description'], - $itemData['value'] - ); + if (!$item) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 物品ID '{$itemId}' 不存在,无法获得。"])); + return; + } $player = $this->stateManager->getPlayer(); $player->addItem($item); @@ -99,16 +182,4 @@ class LootService implements EventListenerInterface { // 触发 InventoryUpdateEvent (未来用于 UI 实时更新) $this->dispatcher->dispatch(new Event('InventoryUpdateEvent', ['playerInventory' => $player->getInventory()])); } - - /** - * 模拟从配置中加载物品数据 - */ - private function loadItemData(int $id): array { - return match ($id) { - 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/MapSystem.php b/src/System/MapSystem.php index 744e6d1..1ae50c4 100644 --- a/src/System/MapSystem.php +++ b/src/System/MapSystem.php @@ -1,6 +1,7 @@ dispatcher = $dispatcher; $this->stateManager = $stateManager; + $this->npcRepository = $npcRepository; $this->loadMapData(); // 游戏启动时,设置初始区域状态到 StateManager @@ -35,7 +38,16 @@ class MapSystem implements EventListenerInterface { throw new \Exception("MapTile ID '{$tileId}' not found in configuration."); } $data = $this->mapData[$tileId]; - return new MapTile($tileId, $data['name'], $data['description'], $data['connections'], $data['eventPoolId']); + // ⭐ 修正 MapTile 实例化参数 + return new MapTile( + $tileId, + $data['name'], + $data['description'], + $data['connections'], + $data['encounter_pool'] ?? null, // 传入遇敌池 + $data['encounter_chance'] ?? 0.0, // 传入遇敌几率 + $data['npc_ids'] ?? [] // 传入遇敌几率 + ); } public function handleEvent(Event $event): void { @@ -51,8 +63,51 @@ class MapSystem implements EventListenerInterface { $initialTile = $this->stateManager->getCurrentTile(); $this->dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => $initialTile->id])); break; + // ... 现有 case ... + case 'AttemptTalkEvent': // ⭐ 响应新的交谈尝试事件 + $this->handleTalkAttempt(); + break; + case 'InteractWithNpcEvent': // ⭐ 玩家已选择 NPC,交给 InteractionSystem + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "➡️ 启动与 {$event->getPayload()['npcId']} 的交互流程。" + ])); } } + /** + * ⭐ 新增方法:处理交谈尝试,列出 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])); + } /** * 处理玩家移动逻辑 @@ -86,15 +141,19 @@ class MapSystem implements EventListenerInterface { // 从 StateManager 获取当前 Tile $currentTile = $this->stateManager->getCurrentTile(); - $roll = rand(1, 100); + $roll = rand(1, 100) / 100; - if ($currentTile->eventPoolId === 0) { // 城镇 - $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "你在镇上休息了一会儿,没有发现任何危险。"])); - } elseif ($roll <= 60) { - // 60% 几率遭遇战斗 - $enemyId = $this->getEnemyIdForArea($currentTile->eventPoolId); // 使用当前 Tile 的 eventPoolId - $this->dispatcher->dispatch(new StartBattleEvent($enemyId)); - } elseif ($roll <= 80) { + // ⭐ 核心修正 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' => "🎁 你发现了一个宝箱!"])); @@ -104,12 +163,21 @@ class MapSystem implements EventListenerInterface { } } - // 简化的敌人 ID 获取 (应该从配置中获取) - private function getEnemyIdForArea(int $poolId): int { - return match ($poolId) { - 1 => rand(1, 2), // 新手区敌人 ID 1 或 2 - 2 => 3, // 森林敌人 ID 3 - default => 1, - }; + private function getRandomEnemyIdFromPool(?array $pool): ?string { + if (!$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']; + } + } + return null; // 理论上不会到达这里 } } \ No newline at end of file diff --git a/src/System/QuestService.php b/src/System/QuestService.php index b037623..58dcc68 100644 --- a/src/System/QuestService.php +++ b/src/System/QuestService.php @@ -1,6 +1,7 @@ dispatcher = $dispatcher; $this->stateManager = $stateManager; - $this->loadQuestData(); + $this->questRepository = $questRepository; // ⭐ 赋值 } - private function loadQuestData(): void { - // 模拟从配置加载任务数据 - $this->questData = [ - 'RATS_1' => [ - 'name' => '地窖里的老鼠', - 'description' => '为老村长清除地窖里 5 只弱小的哥布林。', - 'type' => 'kill', - 'target' => ['targetId' => 1, 'count' => 5], // 目标敌人 ID 1 - 'rewards' => ['xp' => 100, 'itemId' => 1] - ], - // ... 其他任务 ... - ]; - } public function handleEvent(Event $event): void { switch ($event->getType()) { + case 'GameStartEvent': + $this->initializeQuests(); // 游戏开始时检查是否有初始任务 + break; case 'QuestCheckEvent': // 响应 InteractionSystem 的任务请求 $this->handleQuestCheck($event->getPayload()['npcId']); break; @@ -146,9 +140,49 @@ class QuestService implements EventListenerInterface { * 模拟:根据 NPC ID 获取对应的任务 ID */ private function getQuestIdForNpc(string $npcId): ?string { - return match ($npcId) { - 'VILLAGER_1' => 'RATS_1', - default => null, - }; + return $this->questRepository->find($npcId); + } + + /** + * 开始一个任务 + */ + public function startQuest(string $questId): void { + $player = $this->stateManager->getPlayer(); + + // ⭐ 使用 Repository 获取数据 + $questData = $this->questRepository->find($questId); + if (!$questData) { + $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 错误:任务ID '{$questId}' 不存在。"])); + return; + } + // 实例化 Quest 模型 (假设 Quest 模型已适配 JSON 键名) + $quest = new Quest( + $questData['id'], + $questData['name'], + $questData['description'], + $questData['type'], + $questData['target'], + $questData['rewards'], + $questData['next_quest_id'] ?? null + // ... 其它必要的 Quest 构造参数 + ); + + $player->addActiveQuest($quest); + $this->dispatcher->dispatch(new Event('SystemMessage', [ + 'message' => "📝 接受任务: {$quest->getName()} - {$quest->getDescription()}" + ])); + } + + /** + * 游戏开始时尝试加载初始任务 (或通过 NPC 交互触发) + */ + public function initializeQuests(): void { + $player = $this->stateManager->getPlayer(); + if (empty($player->getActiveQuests()) && empty($player->getCompletedQuests())) { + $startingQuestId = $this->questRepository->getStartingQuestId(); + if ($startingQuestId) { + $this->startQuest($startingQuestId); + } + } } } \ No newline at end of file diff --git a/src/System/SaveLoadService.php b/src/System/SaveLoadService.php index 11b0664..23ae13f 100644 --- a/src/System/SaveLoadService.php +++ b/src/System/SaveLoadService.php @@ -5,6 +5,7 @@ use Game\Model\Item; use Game\Model\Player; use Game\Event\Event; use Game\Event\EventDispatcher; +use Game\Model\Quest; /** * SaveLoadService: 负责将玩家状态持久化到磁盘,并在启动时加载。 @@ -52,6 +53,11 @@ class SaveLoadService { // 将 Item 对象转换为数组以安全序列化 (可选,但更清晰) $serializedInventory[] = $item->toArray(); } + $activeQuests = []; + foreach ($player->getActiveQuests() as $quest) { + // 将 Item 对象转换为数组以安全序列化 (可选,但更清晰) + $activeQuests[] = $quest->toArray(); + } // 序列化 Player 对象(我们假设 Player 模型包含了所有属性的 public/getter) $data = [ 'name' => $player->getName(), @@ -64,7 +70,7 @@ class SaveLoadService { 'xpToNextLevel' => $player->getXpToNextLevel(), 'gold' => $player->getGold(), 'inventory' => $serializedInventory, // 注意:复杂对象数组需要递归序列化/反序列化 - 'activeQuests' => $player->getActiveQuests(), + 'activeQuests' => $activeQuests, 'completedQuests' => $player->getCompletedQuests(), // TODO: 添加地图位置等其他状态 ]; @@ -121,10 +127,34 @@ class SaveLoadService { ); $restoredInventory[] = $item; } - $player->setInventory($restoredInventory); // 现在赋值的是 Item 对象的数组 - $player->setActiveQuests($data['activeQuests']); - $player->setCompletedQuests($data['completedQuests']); + // ⭐ 恢复 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; + } + } + $player->setActiveQuests($restoredActiveQuests); + + $player->setInventory($restoredInventory); // 现在赋值的是 Item 对象的数组 $this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '📂 游戏存档已加载!'])); return $player; diff --git a/src/System/UIService.php b/src/System/UIService.php index f8395cc..7d6277a 100644 --- a/src/System/UIService.php +++ b/src/System/UIService.php @@ -83,7 +83,13 @@ class UIService implements EventListenerInterface { $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("--------------------------"); }