功能完善

This commit is contained in:
hant 2025-12-18 23:52:11 +08:00
parent ce5a101ebe
commit 2d7481c0e7
12 changed files with 754 additions and 466 deletions

View File

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

View File

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

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

View File

@ -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;
}
/**
* 治疗角色
*/

View File

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

View File

@ -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')); // 切换回探索菜单
}
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -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' 可直接使用背包第一格物品</>");
}
}