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