This commit is contained in:
hantao 2025-12-22 18:07:05 +08:00
parent 6f6125e640
commit 09383708de
26 changed files with 1213 additions and 293 deletions

View File

@ -13,6 +13,12 @@
"greeting": "欢迎来到我的铁匠铺。",
"quest_response": "你有空吗?我的铁矿用完了。",
"shop_response": "看一看你需要什么工具和武器。"
},
"hasShop": true,
"shopInventory": {
"2": {"price": 50},
"4": {"price": 150},
"5": {"price": 200}
}
}
}

View File

@ -8,7 +8,7 @@
"triggerValue": "VILLAGER_1",
"target": {
"entityId": "GOBLIN",
"count": 1
"count": 10
},
"required_level": 1,
"rewards": {

View File

@ -1,23 +1,133 @@
{
"player": {
"name": "Hant",
"health": 91,
"maxHealth": 100,
"base_attack": 15,
"base_defense": 5,
"level": 1,
"currentXp": 20,
"gold": 12,
"name": "AutoSaveTest",
"health": 160,
"maxHealth": 160,
"base_attack": 22,
"base_defense": 13,
"level": 5,
"currentXp": 175,
"gold": 241,
"inventory": [
{
"id": 2,
"name": "\u7834\u65e7\u7684\u77ed\u5251",
"type": "weapon",
"description": "\u653b\u51fb\u529b\u5fae\u5f31\u3002",
"value": 50,
"effects": [],
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": "weapon"
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 1,
"name": "\u5c0f\u578b\u6cbb\u7597\u836f\u6c34",
"type": "potion",
"description": "\u6062\u590d\u5c11\u91cf\u751f\u547d\u3002",
"value": 10,
"effects": {
"heal": 20
},
"stats": [],
"slot": null
},
{
"id": 2,
@ -31,36 +141,34 @@
}
],
"equipment": {
"weapon": null,
"weapon": {
"id": 2,
"name": "\u7834\u65e7\u7684\u77ed\u5251",
"type": "weapon",
"description": "\u653b\u51fb\u529b\u5fae\u5f31\u3002",
"value": 50,
"effects": [],
"stats": [],
"slot": "weapon"
},
"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
"helmet": {
"id": 4,
"name": "\u5e03\u7532\u5934\u76d4",
"type": "armor",
"description": "\u63d0\u4f9b\u5c11\u91cf\u9632\u5fa1\u3002",
"value": 30,
"effects": [],
"stats": [],
"slot": "helmet"
}
],
"completedQuests": []
},
"activeQuests": [],
"completedQuests": [
"KILL_GOBLIN"
]
},
"world": {
"currentTileId": "FOREST_01"
"currentTileId": "TOWN_01"
}
}

View File

@ -77,7 +77,7 @@ class ServiceContainer {
$this->itemRepository = new ItemRepository($jsonLoader);
$this->enemyRepository = new EnemyRepository($jsonLoader);
$this->abilityRepository = new AbilityRepository($jsonLoader);
$this->questionRepository = new QuestRepository($jsonLoader);
$this->questRepository = new QuestRepository($jsonLoader); // ⭐ 修正变量名
$this->npcRepository = new NPCRepository($jsonLoader);
$this->mapRepository = new MapRepository($jsonLoader);
}
@ -91,15 +91,17 @@ class ServiceContainer {
// 状态管理器
$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));
$this->register(UIService::class, new UIService($this->output, $this->stateManager, $this->questRepository));
// 2. 核心逻辑服务 (依赖 Dispatcher, StateManager)
$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));
$this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager, $this->questionRepository));
$this->register(EquipmentService::class, new EquipmentService($this->eventDispatcher, $this->stateManager)); // ⭐ 注册装备服务
$this->register(QuestService::class, new QuestService($this->eventDispatcher, $this->stateManager, $this->questRepository));
// ⭐ 实例化 AbilityService
$abilityService = new AbilityService($this->eventDispatcher, $this->stateManager, $this->abilityRepository);
$this->register(AbilityService::class, $abilityService);
@ -117,7 +119,7 @@ class ServiceContainer {
$this->eventDispatcher,
$this->stateManager,
$this->npcRepository,
$this->questionRepository, // ⭐ 新增注入
$this->questRepository, // ⭐ 修正变量名
$dialogueService, // ⭐ 新增注入
$this->input,
$this->output,
@ -128,10 +130,11 @@ class ServiceContainer {
new BattleService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper)
);
$this->register(ShopService::class,
new ShopService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper)
new ShopService($this->eventDispatcher, $this->stateManager, $this->input, $this->output, $this->questionHelper, $this->itemRepository)
);
// ⭐ 注册SaveLoadService为事件监听器实现自动保存
$this->register(SaveLoadService::class, $this->saveLoadService);
// 4. 输入处理服务 (驱动主循环,必须最后注册,因为它依赖于所有 I/O 组件)
$this->register(InputHandler::class,
@ -170,4 +173,5 @@ class ServiceContainer {
public function getItemRepository(): ItemRepository { return $this->itemRepository; }
public function getEnemyRepository(): EnemyRepository { return $this->enemyRepository; }
public function getAbilityRepository(): AbilityRepository { return $this->abilityRepository; }
public function getNpcRepository(): NPCRepository { return $this->npcRepository; } // ⭐ 新增
}

View File

@ -28,11 +28,13 @@ class NPCRepository implements RepositoryInterface {
return null;
}
// 假设 NPC 模型的构造函数是 public function __construct(string $id, string $name, array $dialogue)
// ⭐ 支持商店数据
return new NPC(
$id,
$data['name'],
$data['dialogue']
$data['dialogue'],
$data['hasShop'] ?? false,
$data['shopInventory'] ?? null
);
}
}

View File

@ -10,7 +10,9 @@ class MapTile {
public ?array $encounterPool; // null 或包含 {"enemyId": "ID", "weight": N} 的数组
public float $encounterChance; // 遇敌几率 (0.0 到 1.0)
public array $npcIds;
public function __construct(string $id, string $name, string $description, array $connections, ?array $encounterPool, float $encounterChance,array $npcIds = []) {
public array $lootIds; // ⭐ 新增:宝箱/掉落物ID列表
public function __construct(string $id, string $name, string $description, array $connections, ?array $encounterPool, float $encounterChance, array $npcIds = [], array $lootIds = []) {
$this->id = $id;
$this->name = $name;
$this->description = $description;
@ -19,5 +21,6 @@ class MapTile {
$this->encounterPool = $encounterPool;
$this->encounterChance = $encounterChance;
$this->npcIds = $npcIds; // ⭐ 赋值
$this->lootIds = $lootIds; // ⭐ 赋值
}
}

View File

@ -8,11 +8,15 @@ class NPC {
public string $id;
public string $name;
public array $dialogue; // 存储对话行或对话树结构
public bool $hasShop; // ⭐ 是否有商店
public ?array $shopInventory; // ⭐ 商店库存
public function __construct(string $id, string $name, array $dialogue) {
public function __construct(string $id, string $name, array $dialogue, bool $hasShop = false, ?array $shopInventory = null) {
$this->id = $id;
$this->name = $name;
$this->dialogue = $dialogue;
$this->hasShop = $hasShop;
$this->shopInventory = $shopInventory ?? [];
}
public function getName(): string {

View File

@ -110,14 +110,8 @@ class Player extends Character {
public function updateQuestProgress(string $questId, int $count = 1): void {
if (isset($this->activeQuests[$questId])) {
$progress = &$this->activeQuests[$questId]; // 使用引用
if (!$progress['isCompleted']) {
$progress['currentCount'] += $count;
if ($progress['currentCount'] >= $progress['targetCount']) {
$progress['currentCount'] = $progress['targetCount'];
$progress['isCompleted'] = true;
// 触发 QuestCompletedEventRequest
// 注意:实际的奖励和标记完成应在 QuestService 确认后进行
}
if (!$progress->isCompleted()) {
$progress->incrementProgress($count);
}
}
}

View File

@ -45,6 +45,13 @@ class Quest {
$this->currentCount = min($targetCount, $this->currentCount + $amount);
}
/**
* 简便方法增加进度兼容旧API
*/
public function incrementProgress(int $amount = 1): void {
$this->incrementCurrentCount($amount);
}
/**
* 检查任务是否完成
*/
@ -62,6 +69,11 @@ class Quest {
public function getTarget(): array { return $this->target; }
public function getRewards(): array { return $this->rewards; }
/**
* 获取任务标题兼容旧API
*/
public function getTitle(): string { return $this->name; }
/**
* 获取当前进度 (用于存档)
*/

View File

@ -248,24 +248,13 @@ class BattleService implements EventListenerInterface {
* 结束战斗状态
*/
private function endBattle(bool $isWin): void {
$enemy = $this->currentEnemy;
$this->inBattle = false;
$this->currentEnemy = null;
// 当战斗结束
$this->stateManager->setMode(StateManager::MODE_MAP);
$this->dispatcher->dispatch(new Event('ShowMenuEvent')); // 切换回探索菜单
}
/**
* 模拟从配置中加载敌人数据
*/
private function loadEnemyData(int $id): array {
// 在实际项目中,这应该从数据库或 JSON 文件加载
return match ($id) {
1 => ['name' => '弱小的哥布林', 'health' => 20, 'attack' => 5, 'defense' => 1, 'xp' => 10],
2 => ['name' => '愤怒的野猪', 'health' => 35, 'attack' => 8, 'defense' => 3, 'xp' => 25],
3 => ['name' => '森林狼', 'health' => 40, 'attack' => 10, 'defense' => 5, 'xp' => 40],
default => ['name' => '未知生物', 'health' => 1, 'attack' => 1, 'defense' => 0, 'xp' => 1],
};
// ⭐ 触发战斗结束事件,用于自动保存
$this->dispatcher->dispatch(new Event('BattleEndEvent', ['isWin' => $isWin,'enemyId' => $enemy->getId()]));
}
}

View File

@ -140,9 +140,6 @@ class DialogueService implements EventListenerInterface {
// 恢复地图模式 (或者之前的模式,稍微简化处理)
$this->stateManager->setMode(StateManager::MODE_MAP);
// 触发菜单显示,让玩家知道回到了地图
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
}
private function handleAction(string $actionString): void {

View File

@ -81,6 +81,9 @@ class InputHandler {
case 'I': // 状态 (Status)
$this->dispatcher->dispatch(new Event('ShowStatsRequest'));
break;
case 'J': // 任务 (Journal/Quest)
$this->dispatcher->dispatch(new Event('ShowQuestListRequest'));
break;
case 'L': // 保存 (Save/Load)
if ($this->saveLoadService) {
$this->saveLoadService->saveGame();
@ -137,19 +140,121 @@ class InputHandler {
$this->stateManager->setMode(StateManager::MODE_MAP);
return true;
}
// ... 处理物品使用逻辑 ...
// ⭐ 处理物品使用逻辑
if (is_numeric($input)) {
$itemIndex = (int)$input;
$player = $this->stateManager->getPlayer();
$inventory = $player->getInventory();
if (!isset($inventory[$itemIndex])) {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 编号为 {$itemIndex} 的物品不存在。"]));
return true;
}
$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' => "❌ 该物品无法被使用或装备。"]));
}
} else {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '❌ 请输入有效的物品编号。']));
}
return true;
}
/** 💬 对话逻辑:选项分支 */
private function handleDialogueInput(): bool {
$input = $this->ask("对话选择 (数字)> ");
// ⭐ 检查是否有待处理的商店NPC
$pendingShopNpc = $this->stateManager->getPendingShopNpc();
if ($pendingShopNpc) {
$input = $this->ask("选择 (S/X)> ");
if ($input === 'S') {
// 打开商店
$this->stateManager->clearPendingShopNpc();
$this->dispatcher->dispatch(new Event('OpenShopEvent', [
'npc' => $pendingShopNpc
]));
} elseif ($input === 'X') {
// 离开
$this->stateManager->clearPendingShopNpc();
$this->stateManager->setMode(StateManager::MODE_MAP);
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '再见!']));
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
} else {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '请输入 S 或 X。']));
}
return true;
}
// ⭐ 检查是否有待交付的任务
$pendingQuestTurnIn = $this->stateManager->getPendingQuestTurnIn();
if ($pendingQuestTurnIn) {
$input = $this->ask("选择 (Y/N)> ");
if ($input === 'Y') {
// 确认交任务
$this->dispatcher->dispatch(new Event('QuestTurnInConfirm', ['questId' => $pendingQuestTurnIn]));
$this->stateManager->clearPendingQuestTurnIn();
$this->stateManager->setMode(StateManager::MODE_MAP);
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
} elseif ($input === 'N') {
// 取消交任务
$this->stateManager->clearPendingQuestTurnIn();
$this->stateManager->setMode(StateManager::MODE_MAP);
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '已取消任务交付。']));
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
} else {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '请输入 Y 或 N。']));
}
return true;
}
// ⭐ 检查是否有待选择的NPC列表
$pendingNpcs = $this->stateManager->getPendingNpcSelection();
if (!empty($pendingNpcs)) {
// ⭐ 处理NPC选择
$input = $this->ask("选择NPC (数字或 X 退出)> ");
if (strtoupper($input) === 'X') {
$this->stateManager->clearPendingNpcSelection();
$this->stateManager->setMode(StateManager::MODE_MAP);
return true;
}
if (is_numeric($input)) {
$index = (int)$input;
if (isset($pendingNpcs[$index])) {
$npcId = $pendingNpcs[$index];
$this->stateManager->clearPendingNpcSelection();
// ⭐ 触发与选中NPC的交互
$this->dispatcher->dispatch(new Event('StartInteractionEvent', ['npcId' => $npcId]));
} else {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的选择。']));
}
} else {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '请输入数字选择NPC。']));
}
} else {
// ⭐ 处理任务对话选项
$input = $this->ask("对话选择 (X 离开)> ");
if (strtoupper($input) === 'X') {
$this->stateManager->setMode(StateManager::MODE_MAP);
return true;
}
$this->dispatcher->dispatch(new Event('DialogueChoice', ['choice' => $input]));
}
return true;
}

View File

@ -51,7 +51,6 @@ class InteractionSystem implements EventListenerInterface {
public function handleEvent(Event $event): void {
switch ($event->getType()) {
case 'AttemptInteractEvent':
// 假设 MapSystem 会提供当前 Tile 上的 NPC ID
$npcId = $event->getPayload()['npcId'];
$this->startInteraction($npcId);
break;
@ -72,6 +71,8 @@ class InteractionSystem implements EventListenerInterface {
if (!$npc) {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里没有人可以交谈 ({$npcId} 不存在)。"]));
// ⭐ 切换回地图模式
$this->stateManager->setMode(StateManager::MODE_MAP);
return;
}
@ -79,64 +80,38 @@ class InteractionSystem implements EventListenerInterface {
'message' => "👤 你走近了 <fg=cyan;options=bold>{$npc->getName()}</>。"
]));
// 启动对话流程
$this->dialogueLoop($npc);
// 交互结束后,重新打印主菜单请求
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
}
/**
* 2. 核心对话循环
*/
/**
* 2. 核心交互循环
*/
private function dialogueLoop(NPC $npc): void {
$running = true;
while ($running) {
// 检查交互类型并获取玩家选择
$choice = $this->promptPlayerChoice($npc);
switch ($choice) {
case 'T': // 交谈 (可能触发任务)
if ($this->handleTalk($npc)) {
// 如果触发了对话系统,结束当前的 Interaction Loop交由 DialogueMode 接管
$running = false;
}
break;
case 'S': // 触发商店
$this->dispatcher->dispatch(new Event('OpenShopEvent', ['npcId' => $npc->id]));
break;
case 'E': // 结束对话
$running = false;
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "🤝 你结束了与 {$npc->getName()} 的对话。"]));
break;
default:
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的交互指令。']));
}
}
}
private function handleTalk(NPC $npc): bool {
// ⭐ 直接尝试触发任务对话
$player = $this->stateManager->getPlayer();
// 1. 查找此 NPC 提供的所有任务
$questIds = $this->questRepository->getQuestsByNpc($npc->id);
$foundQuest = false;
// ⭐ 优先检查是否有已完成的任务可以提交
foreach ($questIds as $questId) {
// 检查任务状态:未接受 或 进行中 (如果是进行中,可能需要不同的对话,比如询问进度)
// 简单起见,这里优先查找“未接受”的任务
$activeQuests = $player->getActiveQuests();
if (isset($activeQuests[$questId]) && $activeQuests[$questId]->isCompleted()) {
// 找到已完成的任务,提示玩家交任务
$this->dispatcher->dispatch(new Event('QuestTurnInPrompt', [
'questId' => $questId,
'npcId' => $npc->id
]));
$foundQuest = true;
break;
}
}
// 如果没有已完成的任务,检查是否有新任务
if (!$foundQuest) {
foreach ($questIds as $questId) {
// 检查任务状态:未接受 或 进行中
if (!$player->isQuestCompleted($questId) && !isset($player->getActiveQuests()[$questId])) {
// 找到了一个新任务!
$questData = $this->questRepository->find($questId);
if ($questData && !empty($questData['dialogue'])) {
// ⭐ 使用新版对话系统
$this->dialogueService->startDialogue($questData['dialogue']);
// 对话已启动,返回 true 以退出 InteractionLoop
return true;
$foundQuest = true;
break;
}
}
}
}
@ -145,46 +120,25 @@ class InteractionSystem implements EventListenerInterface {
// 如果没有新任务,显示 NPC 默认闲聊
$defaultMsg = is_array($npc->dialogue) ? ($npc->dialogue['greeting'] ?? '...') : '...';
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => "<fg=cyan>{$npc->getName()}</><fg=white>{$defaultMsg}</>"
'message' => "<fg=cyan>{$npc->getName()}</><fg=white>{$defaultMsg}</>"
]));
}
return false;
}
/**
* 获取玩家交互指令
*/
private function promptPlayerChoice(NPC $npc): string {
// ⭐ 检查NPC是否有商店
if ($npc->hasShop) {
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => "\n--- 交互菜单 --- [T] 任务 | [S] 商店 | [E] 结束"
'message' => "\n🛍️ <fg=yellow>{$npc->getName()}</> 还经营着一家商店。"
]));
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => "<fg=gray>输入 [S] 打开商店,[X] 离开</>"
]));
$question = new Question("> 请选择指令 (T/S/E)");
$choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? '');
return $choice;
}
/**
* 模拟从配置中加载 NPC 数据
*/
private function loadNPC(string $id): ?NPC {
$data = match ($id) {
'VILLAGER_1' => [
'name' => '老村长',
'dialogue' => [
'greeting' => '你好,旅行者。你看起来很强大。',
'quest_response' => '你想要帮忙吗?我们的地窖里有老鼠。',
'shop_response' => '我现在没有东西卖给你。',
]
],
default => null,
};
if ($data) {
return new NPC($id, $data['name'], $data['dialogue']);
}
return null;
// 设置待处理的商店NPC
$this->stateManager->setPendingShopNpc($npc);
} else {
// ⭐ 没有任务也没有商店时切换回地图模式
$this->stateManager->setMode(StateManager::MODE_MAP);
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
}
}
}
}

View File

@ -82,14 +82,14 @@ class MapSystem implements EventListenerInterface {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里寂静无声,没有人可以交谈。"]));
return;
}
// ⭐ 切换到对话模式
$this->stateManager->setMode(StateManager::MODE_DIALOGUE);
if (count($npcIds) === 1) {
// ⭐ 只有一个NPC直接交互
$this->stateManager->setMode(StateManager::MODE_DIALOGUE);
$this->dispatcher->dispatch(new Event('StartInteractionEvent', ['npcId' => $npcIds[0]]));
} else {
// 多个 NPC 的处理逻辑...
// ⭐ 多个 NPC切换到对话模式并存储待选择列表
$this->stateManager->setMode(StateManager::MODE_DIALOGUE);
$this->stateManager->setPendingNpcSelection($npcIds);
$this->listNpcsForSelection($npcIds);
}
}
@ -147,13 +147,13 @@ class MapSystem implements EventListenerInterface {
}
private function listNpcsForSelection(array $npcIds): void {
$output = "👥 这里的居民\n";
$output = "👥 请选择要交谈的对象\n";
foreach ($npcIds as $index => $id) {
$npcData = $this->npcRepository->find($id);
$name = $npcData['name'] ?? "神秘人";
$output .= " <fg=yellow>[" . ($index + 1) . "]</> {$name}\n";
$output .= " <fg=yellow>[" . ($index) . "]</> {$name}\n";
}
$output .= " <fg=gray>[X] 取消</>";
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => $output]));
$this->dispatcher->dispatch(new Event('AwaitingNpcSelection', ['options' => $npcIds]));
}
}

View File

@ -32,57 +32,27 @@ class QuestService implements EventListenerInterface {
case 'GameStartEvent':
$this->initializeQuests(); // 游戏开始时检查是否有初始任务
break;
case 'QuestCheckEvent': // 响应 InteractionSystem 的任务请求
$this->handleQuestCheck($event->getPayload()['npcId']);
break;
case 'BattleEndEvent': // 响应战斗结束,检查击杀目标
$this->checkKillQuests($event->getPayload()['enemyId']);
$payload = $event->getPayload();
if (isset($payload['enemyId'])) {
$this->checkKillQuests($payload['enemyId']);
}
break;
case 'MapMoveEvent': // ⭐ 响应移动,检查地点触发任务
$this->checkSystemTriggers('MAP_MOVE', $event->getPayload()['targetId'] ?? ''); // 假设 MapMoveEvent 携带 targetId (MapTile ID)
break;
case 'LevelUpEvent': // ⭐ 响应升级
$this->checkSystemTriggers('LEVEL_UP', (string)$event->getPayload()['level']);
$this->checkSystemTriggers('LEVEL_UP', (string)$event->getPayload()['newLevel']);
break;
case 'QuestAcceptRequest': // ⭐ 响应对话中的接受任务请求
$this->startQuest($event->getPayload()['questId']);
break;
case 'QuestTurnInConfirm': // ⭐ 交付任务确认
$this->turnInQuest($event->getPayload()['questId']);
break;
}
}
/**
* 1. 处理 NPC 交互时的任务检查/接受
*/
private function handleQuestCheck(string $npcId): void {
$player = $this->stateManager->getPlayer();
$questId = $this->getQuestIdForNpc($npcId);
if (!$questId) return;
if ($player->isQuestCompleted($questId)) {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "(村长)我已经没什么可以教你的了,旅行者。"]));
} elseif (isset($player->getActiveQuests()[$questId])) {
$this->checkQuestCompletion($questId); // 检查是否可以提交
} else {
// 接受任务
$this->acceptQuest($questId);
}
}
/**
* 2. 接受任务逻辑
*/
private function acceptQuest(string $questId): void {
$player = $this->stateManager->getPlayer();
$questConfig = $this->questData[$questId];
$player->addActiveQuest($questId, $questConfig['target']['count']);
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => "📜 <fg=yellow>接受任务:{$questConfig['name']}</> - 目标:{$questConfig['description']}"
]));
}
/**
* 3. 检查击杀类任务进度
*/
@ -91,17 +61,16 @@ class QuestService implements EventListenerInterface {
$activeQuests = $player->getActiveQuests();
foreach ($activeQuests as $questId => $progress) {
$questConfig = $this->questData[$questId];
if ($questConfig['type'] === 'kill' && $questConfig['target']['targetId'] == $killedEnemyId) {
$questConfig = $this->questRepository->find($questId);
if ($questConfig['type'] === 'kill' && $questConfig['target']['entityId'] == $killedEnemyId) {
// 更新玩家任务进度
$player->updateQuestProgress($questId, 1);
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => "任务进度更新:[{$questConfig['name']}] {$player->getActiveQuests()[$questId]['currentCount']}/{$questConfig['target']['count']}"
'message' => "任务进度更新:[{$questConfig['name']}] {$player->getActiveQuests()[$questId]->getCurrentCount()}/{$questConfig['target']['count']}"
]));
// 如果任务完成,触发 QuestCompletedEventRequest
if ($player->getActiveQuests()[$questId]['isCompleted']) {
if ($player->getActiveQuests()[$questId]->isCompleted()) {
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => "任务 [{$questConfig['name']}] <fg=green;options=bold>已完成!</>请回去找NPC提交。"
]));
@ -187,11 +156,68 @@ class QuestService implements EventListenerInterface {
*/
public function initializeQuests(): void {
$player = $this->stateManager->getPlayer();
if (empty($player->getActiveQuests()) && empty($player->getCompletedQuests())) {
if ($startingQuestId) {
$this->startQuest($startingQuestId);
// if (empty($player->getActiveQuests()) && empty($player->getCompletedQuests())) {
// if ($startingQuestId) {
// $this->startQuest($startingQuestId);
// }
// }
}
/**
* 交付任务
*/
private function turnInQuest(string $questId): void {
$player = $this->stateManager->getPlayer();
$activeQuests = $player->getActiveQuests();
if (!isset($activeQuests[$questId])) {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '任务不存在或已完成。']));
return;
}
$quest = $activeQuests[$questId];
if (!$quest->isCompleted()) {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '任务还未完成,无法交付。']));
return;
}
$questData = $this->questRepository->find($questId);
if (!$questData) {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '任务数据出错。']));
return;
}
// 发放奖励
$rewards = $questData['rewards'] ?? [];
// 经验值奖励
if (isset($rewards['xp'])) {
$player->gainXp($rewards['xp']);
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => " ✅ 获得 <fg=yellow>{$rewards['xp']}</> 经验值"
]));
}
// 金币奖励
if (isset($rewards['gold'])) {
$player->gainGold($rewards['gold']);
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => " ✅ 获得 <fg=yellow>{$rewards['gold']}</> 金币"
]));
}
// 物品奖励
if (isset($rewards['itemId'])) {
$this->dispatcher->dispatch(new Event('LootFoundEvent', ['lootId' => $rewards['itemId']]));
}
// 标记任务完成
$player->markQuestCompleted($questId);
$this->dispatcher->dispatch(new Event('SystemMessage', [
'message' => "\n🎉 <fg=green;options=bold>任务已交付!</> 感谢你的帮助!"
]));
}
/**

View File

@ -5,12 +5,13 @@ use Game\Model\Item;
use Game\Model\Player;
use Game\Event\Event;
use Game\Event\EventDispatcher;
use Game\Event\EventListenerInterface;
use Game\Model\Quest;
/**
* SaveLoadService: 负责将玩家状态持久化,并支持装备与地图位置的还原。
*/
class SaveLoadService {
class SaveLoadService implements EventListenerInterface {
private EventDispatcher $dispatcher;
private StateManager $stateManager;
@ -26,6 +27,77 @@ class SaveLoadService {
}
}
/**
* 处理事件 - 在关键时刻自动保存
*/
public function handleEvent(Event $event): void {
switch ($event->getType()) {
case 'BattleEndEvent': // 战斗结束后自动保存
case 'LevelUpEvent': // 升级后自动保存
case 'QuestAcceptRequest': // 接受任务后自动保存
case 'MapMoveEvent': // 移动后自动保存(静默)
case 'UseItemEvent': // 使用物品后
case 'EquipItemEvent': // 穿装备后
case 'UnequipItemEvent': // 卸下装备后
$this->autoSave();
break;
}
}
/**
* 自动保存(静默,不显示消息)
*/
private function autoSave(): void {
try {
$player = $this->stateManager->getPlayer();
$currentTile = $this->stateManager->getCurrentTile();
if (!$player) return;
// 1. 序列化背包
$serializedInventory = array_map(fn(Item $item) => $this->itemToData($item), $player->getInventory());
// 2. 序列化装备
$serializedEquipment = [];
foreach ($player->getEquipment() as $slot => $item) {
$serializedEquipment[$slot] = $item ? $this->itemToData($item) : null;
}
// 3. 序列化任务
$activeQuests = [];
foreach ($player->getActiveQuests() as $quest) {
$activeQuests[] = [
'config' => $quest->toArray(),
'currentCount' => $quest->getCurrentCount()
];
}
$data = [
'player' => [
'name' => $player->getName(),
'health' => $player->getHealth(),
'maxHealth' => $player->getMaxHealth(),
'base_attack' => $player->attack,
'base_defense' => $player->defense,
'level' => $player->getLevel(),
'currentXp' => $player->getCurrentXp(),
'gold' => $player->getGold(),
'inventory' => $serializedInventory,
'equipment' => $serializedEquipment,
'activeQuests' => $activeQuests,
'completedQuests' => $player->getCompletedQuests(),
],
'world' => [
'currentTileId' => $currentTile->id
]
];
file_put_contents($this->savePath, json_encode($data, JSON_PRETTY_PRINT));
} catch (\Exception $e) {
// throw $e;
// 静默失败,不打扰玩家
}
}
public function hasSaveFile(): bool {
return file_exists($this->savePath);
}

View File

@ -6,6 +6,8 @@ use Game\Event\EventListenerInterface;
use Game\Event\EventDispatcher;
use Game\Model\Item;
use Game\Model\Player;
use Game\Model\NPC; // ⭐ 新增
use Game\Database\ItemRepository; // ⭐ 新增
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
@ -21,16 +23,26 @@ class ShopService implements EventListenerInterface {
private InputInterface $input;
private OutputInterface $output;
private QuestionHelper $helper;
private ItemRepository $itemRepository; // ⭐ 新增
// 商店的固定库存(通常从配置加载)
// 商店的固定库存(通常从DPC配置加载)
private array $shopInventory;
private ?NPC $currentShopNpc = null; // ⭐ 当前商店NPC
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager, InputInterface $input, OutputInterface $output, QuestionHelper $helper) {
public function __construct(
EventDispatcher $dispatcher,
StateManager $stateManager,
InputInterface $input,
OutputInterface $output,
QuestionHelper $helper,
ItemRepository $itemRepository // ⭐ 新增
) {
$this->dispatcher = $dispatcher;
$this->stateManager = $stateManager;
$this->input = $input;
$this->output = $output;
$this->helper = $helper;
$this->itemRepository = $itemRepository; // ⭐ 赋值
$this->loadShopInventory();
}
@ -46,7 +58,15 @@ class ShopService implements EventListenerInterface {
public function handleEvent(Event $event): void {
switch ($event->getType()) {
case 'OpenShopEvent': // 响应 InteractionSystem 的请求
$payload = $event->getPayload();
$npc = $payload['npc'] ?? null;
if ($npc && $npc->hasShop) {
$this->currentShopNpc = $npc;
$this->shopInventory = $npc->shopInventory;
$this->startShopping();
} else {
$this->startShopping(); // 使用默认库存
}
break;
}
}
@ -55,7 +75,8 @@ class ShopService implements EventListenerInterface {
* 启动商店界面和循环
*/
private function startShopping(): void {
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "\n欢迎光临!看看我有什么好东西。"]));
$npcName = $this->currentShopNpc ? $this->currentShopNpc->getName() : '商人';
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "\n🛍️ 欢迎光临 <fg=yellow>{$npcName}</> 的商店!"]));
$running = true;
while ($running) {
@ -75,6 +96,9 @@ class ShopService implements EventListenerInterface {
case 'X':
$running = false;
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '下次再来!']));
// ⭐ 返回地图模式
$this->stateManager->setMode(StateManager::MODE_MAP);
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
break;
default:
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的指令。']));
@ -165,17 +189,31 @@ class ShopService implements EventListenerInterface {
}
private function displaySaleItems(): void {
$this->output->writeln("\n--- 🛒 商店出售 ---");
// 模拟获取 Item 数据的服务(实际应通过 ItemService/DB
$itemsData = [
1 => ['name' => '小型治疗药水', 'value' => 10, 'type' => 'potion'],
3 => ['name' => '高级治疗药水', 'value' => 200, 'type' => 'potion'], // 假设 ID 3
];
$this->output->writeln("\n--- 🛍️ 商店出售 ---");
if (empty($this->shopInventory)) {
$this->output->writeln(" <fg=gray>商店没有货物。</>");
$this->output->writeln("--------------------------");
return;
}
foreach ($this->shopInventory as $itemId => $data) {
$name = $itemsData[$itemId]['name'] ?? "未知物品";
$price = $data['price'];
$this->output->writeln("[<fg=green>{$itemId}</>] {$name} | 价格: <fg=yellow>{$price}</> 💰");
// ⭐ 从 ItemRepository 获取真实数据
$itemData = $this->itemRepository->find($itemId);
if ($itemData) {
$name = $itemData['name'] ?? "未知物品";
$price = $data['price'] ?? $itemData['value'] ?? 0;
$type = $itemData['type'] ?? '';
$typeLabel = match($type) {
'potion' => '<fg=green>药水</>',
'weapon' => '<fg=red>武器</>',
'armor' => '<fg=blue>护甲</>',
default => $type
};
$this->output->writeln("[<fg=green>{$itemId}</>] <fg=white;options=bold>{$name}</> {$typeLabel} | 价格: <fg=yellow>{$price}</> 💰");
}
}
$this->output->writeln("--------------------------");
}

View File

@ -23,6 +23,13 @@ class StateManager {
private string $currentMode = self::MODE_MAP;
private MapRepository $mapRepository;
// ⭐ 新增存储待选择的NPC列表
private array $pendingNpcSelection = [];
// ⭐ 新增待交付的任务ID
private ?string $pendingQuestTurnIn = null;
// ⭐ 新增待处理的商店NPC
private $pendingShopNpc = null;
public function setMode(string $mode): void {
$this->currentMode = $mode;
// 模式切换时,可以自动触发 UI 刷新
@ -31,6 +38,46 @@ class StateManager {
public function getMode(): string {
return $this->currentMode;
}
// ⭐ 新增NPC选择相关方法
public function setPendingNpcSelection(array $npcIds): void {
$this->pendingNpcSelection = $npcIds;
}
public function getPendingNpcSelection(): array {
return $this->pendingNpcSelection;
}
public function clearPendingNpcSelection(): void {
$this->pendingNpcSelection = [];
}
// ⭐ 交任务状态管理
public function setPendingQuestTurnIn(?string $questId): void {
$this->pendingQuestTurnIn = $questId;
}
public function getPendingQuestTurnIn(): ?string {
return $this->pendingQuestTurnIn;
}
public function clearPendingQuestTurnIn(): void {
$this->pendingQuestTurnIn = null;
}
// ⭐ 商店NPC状态管理
public function setPendingShopNpc($npc): void {
$this->pendingShopNpc = $npc;
}
public function getPendingShopNpc() {
return $this->pendingShopNpc;
}
public function clearPendingShopNpc(): void {
$this->pendingShopNpc = null;
}
public function __construct(Connection $db,MapRepository $mapRepository) {
$this->db = $db;
$this->mapRepository = $mapRepository;

View File

@ -6,6 +6,7 @@ use Game\Event\EventListenerInterface;
use Game\Model\Player;
use Game\Model\Enemy;
use Game\Model\MapTile;
use Game\Database\QuestRepository; // ⭐ 新增
use Symfony\Component\Console\Output\OutputInterface;
/**
@ -15,10 +16,12 @@ class UIService implements EventListenerInterface {
private OutputInterface $output;
private StateManager $stateManager;
private QuestRepository $questRepository; // ⭐ 新增
public function __construct(OutputInterface $output, StateManager $stateManager) {
public function __construct(OutputInterface $output, StateManager $stateManager, QuestRepository $questRepository) {
$this->output = $output;
$this->stateManager = $stateManager;
$this->questRepository = $questRepository; // ⭐ 赋值
}
public function handleEvent(Event $event): void {
@ -27,6 +30,7 @@ class UIService implements EventListenerInterface {
case 'ShowMenuEvent': // 👈 监听这个信号
// 根据模式渲染不同的 UI 底部
if ($mode === StateManager::MODE_MAP) {
$this->displayLocation($this->stateManager->getCurrentTile());
$this->displayMapMenu();
} elseif ($mode === StateManager::MODE_BATTLE) {
$this->displayBattleMenu();
@ -35,12 +39,18 @@ class UIService implements EventListenerInterface {
case 'ShowStatsRequest': //
$this->displayPlayerStats(); // 👈 真正调用显示逻辑
break;
case 'ShowQuestListRequest': // ⭐ 新增:显示任务列表
$this->displayQuestList();
break;
case 'QuestTurnInPrompt': // ⭐ 任务交付提示
$this->displayQuestTurnInPrompt($event->getPayload());
break;
case 'SystemMessage':
$this->output->writeln("📣 <fg=magenta>{$event->getPayload()['message']}</>");
break;
case 'MapMoveEvent':
$this->refreshScreen(); // 刷新整个视野
// $this->refreshScreen(); // 刷新整个视野
break;
case 'StatUpdateEvent':
@ -67,43 +77,14 @@ class UIService implements EventListenerInterface {
* 🗺️ 地图模式菜单:强调移动和探索
*/
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) . "</>");
$this->output->writeln("\n<fg=cyan>[W/A/S/D]移动 [E]探索 [T]交谈 [B]背包 [I]状态 [J]任务 [L]保存 [Q]退出</>");
}
/**
* ⚔️ 战斗模式菜单:强调数字键操作
*/
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) . "</>");
$this->output->writeln("\n<fg=red>[1]攻击 [2]技能 [3]物品 [4]逃跑</>");
}
/**
@ -220,19 +201,32 @@ class UIService implements EventListenerInterface {
* 打印当前地图区域信息 (增强版)
*/
private function displayLocation(MapTile $tile): void {
$this->output->writeln("\n<fg=black;bg=cyan;options=bold> 📍 区域:{$tile->name} </>");
$this->output->writeln("\n<fg=black;bg=cyan;options=bold> 📍 {$tile->name} </>");
$this->output->writeln(" <fg=white>{$tile->description}</>");
// 显示出口
$moveOptions = [];
// 显示出口 - 使用方向箭头表示
$directionMap = [
'N' => ['arrow' => '↑', 'label' => '北'],
'S' => ['arrow' => '↓', 'label' => '南'],
'E' => ['arrow' => '→', 'label' => '东'],
'W' => ['arrow' => '←', 'label' => '西']
];
$moveDisplay = [];
foreach ($tile->connections as $dir => $targetId) {
$moveOptions[] = "<fg=green;options=bold>{$dir}</> (<fg=yellow>{$targetId}</>)";
if (isset($directionMap[$dir])) {
$arrow = $directionMap[$dir]['arrow'];
$moveDisplay[] = "<fg=green>{$arrow}</> {$directionMap[$dir]['label']}";
}
}
if (!empty($moveDisplay)) {
$this->output->writeln(" 🚪 " . implode(' ', $moveDisplay));
}
$this->output->writeln(" 🚪 出路: " . implode(' | ', $moveOptions));
// 如果有 NPC也显示出来
if (!empty($tile->npcIds)) {
$this->output->writeln(" 👥 附近的人: <fg=cyan>" . implode(', ', $tile->npcIds) . "</>");
$this->output->writeln(" 👥 " . implode(', ', $tile->npcIds));
}
}
@ -244,7 +238,7 @@ class UIService implements EventListenerInterface {
$inventory = $player->getInventory();
$this->output->writeln("\n🎒 <fg=yellow;options=bold>个人背包内容</>");
$this->output->writeln("<fg=gray>" . str_repeat("=", 30) . "</>");
$this->output->writeln("<fg=gray>" . str_repeat("=", 40) . "</>");
if (empty($inventory)) {
$this->output->writeln(" (空空如也)");
@ -253,12 +247,42 @@ class UIService implements EventListenerInterface {
// 简化效果显示
$effectStr = "";
if (!empty($item->effects)) {
$effectStr = " | <fg=gray>效果: " . json_encode($item->effects) . "</>";
$parts = [];
foreach ($item->effects as $key => $value) {
$parts[] = "{$key}+{$value}";
}
$this->output->writeln(sprintf(" [<fg=green>%d</>] <fg=white>%s</> (%s)%s", $index, $item->name, $item->type, $effectStr));
$effectStr = " <fg=green>" . implode(', ', $parts) . "</>";
}
// 显示装备属性加成
$statStr = "";
if (!empty($item->statModifiers)) {
$parts = [];
foreach ($item->statModifiers as $key => $value) {
$parts[] = "{$key}+{$value}";
}
$this->output->writeln("<fg=gray>" . str_repeat("=", 30) . "</>\n");
$statStr = " <fg=cyan>" . implode(', ', $parts) . "</>";
}
$typeLabel = match($item->type) {
'potion' => '<fg=green>药水</>',
'weapon' => '<fg=red>武器</>',
'armor' => '<fg=blue>护甲</>',
default => $item->type
};
$this->output->writeln(sprintf(
" [<fg=yellow>%d</>] <fg=white;options=bold>%s</> %s%s%s",
$index,
$item->name,
$typeLabel,
$effectStr,
$statStr
));
}
$this->output->writeln("\n<fg=gray>提示:输入编号使用/装备物品,输入 X 退出</>");
}
$this->output->writeln("<fg=gray>" . str_repeat("=", 40) . "</>");
}
private function displayMainMenu(): void {
@ -267,4 +291,101 @@ class UIService implements EventListenerInterface {
$this->output->writeln(" 角色: <fg=yellow>I</> (状态) | <fg=yellow>B</> (背包) | <fg=yellow>L</> (保存)");
$this->output->writeln(" <fg=gray>提示: 输入 'USE 0' 可直接使用背包第一格物品</>");
}
/**
* 显示任务交付提示
*/
private function displayQuestTurnInPrompt(array $payload): void {
$questId = $payload['questId'];
$player = $this->stateManager->getPlayer();
$quest = $player->getActiveQuests()[$questId];
$questData = $this->questRepository->find($questId);
$this->output->writeln("\n🎉 <fg=green;options=bold>任务完成!</>");
$this->output->writeln("<fg=gray>" . str_repeat("=", 50) . "</>");
$this->output->writeln(" <fg=yellow;options=bold>{$quest->getTitle()}</>");
$this->output->writeln(" <fg=gray>{$quest->getDescription()}</>");
$this->output->writeln("");
// 显示奖励
$rewards = $questData['rewards'] ?? [];
if (!empty($rewards)) {
$this->output->writeln(" <fg=cyan>奖励:</>");
if (isset($rewards['xp'])) {
$this->output->writeln(" • <fg=yellow>经验值: +{$rewards['xp']}</>");
}
if (isset($rewards['gold'])) {
$this->output->writeln(" • <fg=yellow>金币: +{$rewards['gold']}</>");
}
if (isset($rewards['itemId'])) {
$this->output->writeln(" • <fg=green>物品: {$rewards['itemId']}</>");
}
}
$this->output->writeln("");
$this->output->writeln("<fg=gray>" . str_repeat("=", 50) . "</>");
$this->output->writeln("<fg=white>输入 [Y] 交付任务,[N] 取消</>");
// 设置状态,等待玩家输入
$this->stateManager->setMode(StateManager::MODE_DIALOGUE);
$this->stateManager->setPendingQuestTurnIn($questId);
}
/**
* 显示任务列表
*/
private function displayQuestList(): void {
$player = $this->stateManager->getPlayer();
$activeQuests = $player->getActiveQuests();
$completedQuests = $player->getCompletedQuests();
$this->output->writeln("\n📖 <fg=yellow;options=bold>任务日志</>");
$this->output->writeln("<fg=gray>" . str_repeat("=", 60) . "</>");
// 显示进行中的任务
$this->output->writeln("\n<fg=cyan;options=bold>进行中的任务</>");
if (empty($activeQuests)) {
$this->output->writeln(" <fg=gray>暂无任务</>");
} else {
foreach ($activeQuests as $quest) {
$progress = "";
if ($quest->getType() === 'kill' || $quest->getType() === 'collect') {
$current = $quest->getCurrentCount();
$targetData = $quest->getTarget();
$target = $targetData['count'] ?? 1; // ⭐ target是数组需要取count
$percent = $target > 0 ? (int)(($current / $target) * 100) : 0;
$progress = sprintf(" <fg=yellow>[%d/%d - %d%%]</>", $current, $target, $percent);
}
$this->output->writeln(sprintf(
" • <fg=white;options=bold>%s</> %s",
$quest->getTitle(),
$progress
));
$this->output->writeln(sprintf(" <fg=gray>%s</>", $quest->getDescription()));
}
}
// 显示已完成的任务
$this->output->writeln("\n<fg=green;options=bold>已完成的任务</>");
if (empty($completedQuests)) {
$this->output->writeln(" <fg=gray>暂无完成任务</>");
} else {
$count = 0;
foreach ($completedQuests as $questId) {
$this->output->writeln(" ✓ <fg=green>{$questId}</>");
$count++;
if ($count >= 10) {
$remaining = count($completedQuests) - 10;
if ($remaining > 0) {
$this->output->writeln(" <fg=gray>... 还有 {$remaining} 个已完成任务</>");
}
break;
}
}
}
$this->output->writeln("\n<fg=gray>" . str_repeat("=", 60) . "</>");
$this->output->writeln("<fg=gray>统计:进行中 " . count($activeQuests) . " | 已完成 " . count($completedQuests) . "</>");
}
}

63
tests/test_autosave.php Normal file
View File

@ -0,0 +1,63 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Game\Core\ServiceContainer;
use Game\Event\Event;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
$input = new ArrayInput([]);
$output = new BufferedOutput();
$helperSet = new HelperSet(['question' => new QuestionHelper()]);
$container = new ServiceContainer($input, $output, $helperSet);
$dispatcher = $container->registerServices();
$stateManager = $container->getStateManager();
// 创建测试玩家
$player = new \Game\Model\Player("AutoSaveTest", 100, 10, 5);
$stateManager->setPlayer($player);
$stateManager->setCurrentTileId('TOWN_01');
echo "=== 自动保存功能测试 ===\n\n";
// 删除旧存档
$savePath = __DIR__ . '/../save/player.json';
if (file_exists($savePath)) {
unlink($savePath);
echo "✓ 已清理旧存档\n";
}
echo "\n1. 测试地图移动触发自动保存...\n";
$dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => 'FOREST_01']));
if (file_exists($savePath)) {
echo "✓ 地图移动后自动保存成功\n";
$data = json_decode(file_get_contents($savePath), true);
echo " - 当前位置: " . $data['world']['currentTileId'] . "\n";
} else {
echo "✗ 自动保存失败\n";
}
echo "\n2. 测试升级触发自动保存...\n";
$player->gainXp(80);
$dispatcher->dispatch(new Event('LevelUpEvent', ['newLevel' => 2]));
if (file_exists($savePath)) {
$data = json_decode(file_get_contents($savePath), true);
echo "✓ 升级后自动保存成功\n";
echo " - 当前等级: " . $data['player']['level'] . "\n";
echo " - 当前经验: " . $data['player']['currentXp'] . "\n";
} else {
echo "✗ 自动保存失败\n";
}
echo "\n3. 测试任务接受触发自动保存...\n";
$dispatcher->dispatch(new Event('QuestAcceptRequest', ['questId' => 'KILL_GOBLIN']));
if (file_exists($savePath)) {
echo "✓ 任务接受后自动保存成功\n";
} else {
echo "✗ 自动保存失败\n";
}
echo "\n=== 测试完成 ===\n";

58
tests/test_inventory.php Normal file
View File

@ -0,0 +1,58 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Game\Core\ServiceContainer;
use Game\Event\Event;
use Game\Model\Item;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
$input = new ArrayInput([]);
$output = new BufferedOutput();
$helperSet = new HelperSet(['question' => new QuestionHelper()]);
$container = new ServiceContainer($input, $output, $helperSet);
$dispatcher = $container->registerServices();
$stateManager = $container->getStateManager();
// 创建测试玩家
$player = new \Game\Model\Player("ItemTest", 50, 10, 5);
$stateManager->setPlayer($player);
$stateManager->setCurrentTileId('TOWN_01');
echo "=== 物品使用功能测试 ===\n\n";
// 添加测试物品
$potion = new Item(1, "小型治疗药水", "potion", "恢复20点生命", 10, ['heal' => 20]);
$weapon = new Item(2, "铁剑", "weapon", "攻击+5", 50, [], 'weapon', ['attack' => 5]);
$player->addItem($potion);
$player->addItem($weapon);
echo "1. 初始状态:\n";
echo " - 生命值: {$player->getHealth()}/{$player->getMaxHealth()}\n";
echo " - 攻击力: {$player->getAttack()}\n";
echo " - 背包物品数: " . count($player->getInventory()) . "\n\n";
// 扣血测试药水
$player->takeDamage(30);
echo "2. 受到30点伤害后:\n";
echo " - 生命值: {$player->getHealth()}/{$player->getMaxHealth()}\n\n";
// 测试使用药水
echo "3. 使用药水 (编号0)...\n";
$dispatcher->dispatch(new Event('UseItemEvent', ['itemIndex' => 0]));
echo " - 生命值: {$player->getHealth()}/{$player->getMaxHealth()}\n";
echo " - 背包物品数: " . count($player->getInventory()) . "\n\n";
// 测试装备武器
echo "4. 装备武器 (编号0因为药水已被使用)...\n";
$dispatcher->dispatch(new Event('EquipItemEvent', ['itemIndex' => 0]));
echo " - 攻击力: {$player->getAttack()}\n";
echo " - 背包物品数: " . count($player->getInventory()) . "\n";
$equipment = $player->getEquipment();
echo " - 武器槽: " . ($equipment['weapon'] ? $equipment['weapon']->name : '空') . "\n\n";
echo "=== 测试完成 ===\n";

View File

@ -0,0 +1,48 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Game\Core\ServiceContainer;
use Game\Event\Event;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
// Mock Input with stream
$stream = fopen('php://memory', 'r+');
fwrite($stream, "0\n0\n"); // 选择对话选项0然后再选0接受任务
rewind($stream);
$input = new ArrayInput([]);
$input->setStream($stream);
$input->setInteractive(true);
$output = new BufferedOutput();
$helperSet = new HelperSet(['question' => new QuestionHelper()]);
$container = new ServiceContainer($input, $output, $helperSet);
$dispatcher = $container->registerServices();
$stateManager = $container->getStateManager();
// 创建测试玩家
$player = new \Game\Model\Player("TestHero", 100, 10, 5);
$stateManager->setPlayer($player);
// 设置地图位置到新手村
$stateManager->setCurrentTileId('TOWN_01');
echo "=== 测试开始 ===\n";
echo "当前位置: " . $stateManager->getCurrentTile()->name . "\n";
echo "NPC列表: " . implode(', ', $stateManager->getCurrentTile()->npcIds) . "\n\n";
// 触发与NPC交谈
echo "触发 StartInteractionEvent...\n";
$dispatcher->dispatch(new Event('AttemptTalkEvent'));
// 获取输出
$display = $output->fetch();
echo "\n=== 输出结果 ===\n";
echo $display;
$dispatcher->dispatch(new Event('StartInteractionEvent', ['npcId' => 'VILLAGER_1']));
dd($stateManager->getPendingNpcSelection());

80
tests/test_npc_shop.php Normal file
View File

@ -0,0 +1,80 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Game\Core\ServiceContainer;
use Game\Event\Event;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
$input = new ArrayInput([]);
$output = new BufferedOutput();
$helperSet = new HelperSet(['question' => new QuestionHelper()]);
$container = new ServiceContainer($input, $output, $helperSet);
$dispatcher = $container->registerServices();
$stateManager = $container->getStateManager();
// 创建测试玩家
$player = new \Game\Model\Player("ShopTest", 100, 10, 5);
$player->gainGold(200); // 给玩家一些金币用于测试
$stateManager->setPlayer($player);
$stateManager->setCurrentTileId('TOWN_01');
echo "=== NPC商店功能测试 ===\n\n";
// 获取NPC仓库
$npcRepo = $container->getNpcRepository();
$npc = $npcRepo->createNPC('BLACKSMITH');
if (!$npc) {
die("❌ 无法创建铁匠NPC\n");
}
echo "1. 创建NPC: {$npc->getName()}\n";
echo " 是否有商店: " . ($npc->hasShop ? '是' : '否') . "\n";
echo " 商店库存数量: " . count($npc->shopInventory) . "\n\n";
echo "2. 玩家初始状态:\n";
echo " - 金币: {$player->getGold()}\n";
echo " - 背包物品数: " . count($player->getInventory()) . "\n\n";
echo "3. 模拟与NPC交互并打开商店...\n";
// 模拟InteractionSystem的行为
$dispatcher->dispatch(new Event('SystemMessage', [
'message' => "👤 你走近了 <fg=cyan;options=bold>{$npc->getName()}</>。"
]));
// 模拟NPC对话
$defaultMsg = is_array($npc->dialogue) ? ($npc->dialogue['greeting'] ?? '...') : '...';
$dispatcher->dispatch(new Event('SystemMessage', [
'message' => "<fg=cyan>{$npc->getName()}</><fg=white>{$defaultMsg}</>"
]));
// 检查商店
if ($npc->hasShop) {
$dispatcher->dispatch(new Event('SystemMessage', [
'message' => "\n🛍️ <fg=yellow>{$npc->getName()}</> 还经营着一家商店。"
]));
$dispatcher->dispatch(new Event('SystemMessage', [
'message' => "<fg=gray>输入 [S] 打开商店,[X] 离开</>"
]));
// 设置待处理的商店NPC
$stateManager->setPendingShopNpc($npc);
// 模拟输入S打开商店
echo "\n模拟输入 S 打开商店...\n";
$dispatcher->dispatch(new Event('OpenShopEvent', [
'npc' => $npc
]));
}
echo "\n" . $output->fetch() . "\n";
echo "\n4. 测试完成后的状态:\n";
echo " - 金币: {$player->getGold()}\n";
echo " - 背包物品数: " . count($player->getInventory()) . "\n";
echo "\n=== 测试完成 ===\n";

View File

@ -0,0 +1,48 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Game\Core\ServiceContainer;
use Game\Event\Event;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
$input = new ArrayInput([]);
$output = new BufferedOutput();
$helperSet = new HelperSet(['question' => new QuestionHelper()]);
$container = new ServiceContainer($input, $output, $helperSet);
$dispatcher = $container->registerServices();
$stateManager = $container->getStateManager();
// 创建测试玩家
$player = new \Game\Model\Player("ShopTest", 100, 10, 5);
$player->gainGold(200); // 给玩家一些金币用于测试
$stateManager->setPlayer($player);
$stateManager->setCurrentTileId('TOWN_01');
echo "=== NPC商店功能测试 ===\n\n";
// 获取NPC仓库
$npcRepo = $container->getNpcRepository();
$npc = $npcRepo->createNPC('BLACKSMITH');
if (!$npc) {
die("❌ 无法创建铁匠NPC\n");
}
echo "1. 创建NPC: {$npc->getName()}\n";
echo " 是否有商店: " . ($npc->hasShop ? '是' : '否') . "\n";
echo " 商店库存数量: " . count($npc->shopInventory) . "\n\n";
echo "2. 玩家初始状态:\n";
echo " - 金币: {$player->getGold()}\n";
echo " - 背包物品数: " . count($player->getInventory()) . "\n\n";
echo "3. NPC商店数据验证:\n";
foreach ($npc->shopInventory as $itemId => $data) {
echo " - 商品ID: {$itemId}, 价格: {$data['price']}\n";
}
echo "\n=== 测试完成 ===\n";

67
tests/test_quest_list.php Normal file
View File

@ -0,0 +1,67 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Game\Core\ServiceContainer;
use Game\Event\Event;
use Game\Model\Quest;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
$input = new ArrayInput([]);
$output = new BufferedOutput();
$helperSet = new HelperSet(['question' => new QuestionHelper()]);
$container = new ServiceContainer($input, $output, $helperSet);
$dispatcher = $container->registerServices();
$stateManager = $container->getStateManager();
// 创建测试玩家
$player = new \Game\Model\Player("QuestTest", 100, 10, 5);
$stateManager->setPlayer($player);
$stateManager->setCurrentTileId('TOWN_01');
echo "=== 任务列表功能测试 ===\n\n";
// 添加一些测试任务
$quest1 = new Quest(
'QUEST_001',
'清理野兽',
'击败5只野狼',
'kill',
['entityId' => 'wolf_001', 'count' => 5],
['gold' => 100, 'xp' => 50]
);
$quest1->incrementProgress(); // 已击败 1 只
$quest2 = new Quest(
'QUEST_002',
'收集草药',
'收集10个草药',
'collect',
['entityId' => 'herb_basic', 'count' => 10],
['gold' => 50, 'xp' => 30]
);
$quest2->incrementProgress();
$quest2->incrementProgress();
$quest2->incrementProgress(); // 已收集 3 个
$player->addActiveQuest($quest1);
$player->addActiveQuest($quest2);
// 添加已完成的任务
$player->markQuestCompleted('TUTORIAL_QUEST');
$player->markQuestCompleted('FIRST_BATTLE');
echo "1. 初始状态:\n";
echo " - 进行中任务: " . count($player->getActiveQuests()) . "\n";
echo " - 已完成任务: " . count($player->getCompletedQuests()) . "\n\n";
// 测试显示任务列表
echo "2. 显示任务列表...\n";
$dispatcher->dispatch(new Event('ShowQuestListRequest'));
echo "\n" . $output->fetch() . "\n";
echo "=== 测试完成 ===\n";

View File

@ -0,0 +1,74 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Game\Core\ServiceContainer;
use Game\Event\Event;
use Game\Model\Quest;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
$input = new ArrayInput([]);
$output = new BufferedOutput();
$helperSet = new HelperSet(['question' => new QuestionHelper()]);
$container = new ServiceContainer($input, $output, $helperSet);
$dispatcher = $container->registerServices();
$stateManager = $container->getStateManager();
// 创建测试玩家
$player = new \Game\Model\Player("QuestTester", 100, 10, 5);
$stateManager->setPlayer($player);
$stateManager->setCurrentTileId('TOWN_01');
echo "=== 任务交付功能测试 ===\n\n";
// 创建一个测试任务
$quest = new Quest(
'TEST_QUEST_001',
'清理野狼',
'击败3只野狼',
'kill',
['entityId' => 'wolf_001', 'count' => 3],
['gold' => 100, 'xp' => 50]
);
// 添加任务并完成它
$player->addActiveQuest($quest);
echo "1. 添加任务: {$quest->getTitle()}\n";
echo " 状态: 进行中 (0/3)\n\n";
// 模拟击杀进度
$quest->incrementProgress();
$quest->incrementProgress();
$quest->incrementProgress();
echo "2. 完成任务目标:\n";
echo " 状态: 已完成 (3/3)\n";
echo " 是否完成: " . ($quest->isCompleted() ? '是' : '否') . "\n\n";
echo "3. 初始状态:\n";
echo " - 金币: {$player->getGold()}\n";
echo " - 经验值: {$player->getCurrentXp()}\n";
echo " - 等级: {$player->getLevel()}\n\n";
// 测试交任务 - 直接调用turnInQuest方法前需要模拟一个repo数据
echo "4. 模拟交付任务逻辑...\n";
// 直接调用player的方法模拟奖励
$player->gainGold(100);
$player->gainXp(50);
$player->markQuestCompleted('TEST_QUEST_001');
echo "\n✅ 模拟奖励发放:\n";
echo " - 金币 +100\n";
echo " - 经验值 +50\n";
echo "\n5. 交付后状态:\n";
echo " - 金币: {$player->getGold()}\n";
echo " - 经验值: {$player->getCurrentXp()}\n";
echo " - 等级: {$player->getLevel()}\n";
echo " - 已完成任务: " . (in_array('TEST_QUEST_001', $player->getCompletedQuests()) ? '是' : '否') . "\n";
echo "\n=== 测试完成 ===\n";