first
This commit is contained in:
commit
8e3c3a52de
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/vendor/
|
||||
/tmp/
|
||||
/saves/
|
||||
/data/
|
||||
/build/
|
||||
/.claude/
|
||||
/.idea
|
||||
18
composer.json
Normal file
18
composer.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "hant/fanren",
|
||||
"description": "A PHP terminal game using Symfony src/Console",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/var-dumper": "^6.4",
|
||||
"symfony/console": "^6.4",
|
||||
"symfony/yaml": "^6.4",
|
||||
"doctrine/dbal": "^3.5",
|
||||
"ext-readline": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Game\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
1438
composer.lock
generated
Normal file
1438
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
config/enemy_data.json
Normal file
18
config/enemy_data.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[
|
||||
{
|
||||
"name": "森林哥布林",
|
||||
"health": 30,
|
||||
"attack": 10,
|
||||
"defense": 5,
|
||||
"xp": 25,
|
||||
"dropId": 1
|
||||
},
|
||||
{
|
||||
"name": "愤怒的野猪",
|
||||
"health": 45,
|
||||
"attack": 15,
|
||||
"defense": 5,
|
||||
"xp": 40,
|
||||
"dropId": 2
|
||||
}
|
||||
]
|
||||
20
config/map_data.json
Normal file
20
config/map_data.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"TOWN_01": {
|
||||
"name": "新手村",
|
||||
"description": "安全的小镇,可以休息。",
|
||||
"connections": {"N": "FIELD_01"},
|
||||
"eventPoolId": 0
|
||||
},
|
||||
"FIELD_01": {
|
||||
"name": "新手田野",
|
||||
"description": "微风习习,有低级怪物出没。",
|
||||
"connections": {"S": "TOWN_01", "E": "FOREST_01"},
|
||||
"eventPoolId": 1
|
||||
},
|
||||
"FOREST_01": {
|
||||
"name": "幽暗密林",
|
||||
"description": "树木茂密,光线昏暗,怪物更强。",
|
||||
"connections": {"W": "FIELD_01"},
|
||||
"eventPoolId": 2
|
||||
}
|
||||
}
|
||||
23
index.php
Normal file
23
index.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
// 加载 Composer 自动加载器
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// 引入 Symfony Console 类
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
// 引入我们的主命令类 (稍后创建)
|
||||
use Game\Core\GameCommand;
|
||||
|
||||
// 1. 实例化 Symfony Console Application
|
||||
$application = new Application('PHP CLI RPG', 'v0.1.0');
|
||||
|
||||
// 2. 将游戏的命令添加到 Application 中
|
||||
// GameCommand 将是游戏的入口和主循环驱动者
|
||||
$application->add(new GameCommand());
|
||||
|
||||
// 3. 设置默认命令,让用户直接运行 `php index.php` 即可进入游戏
|
||||
$application->setDefaultCommand('game:start', true);
|
||||
|
||||
// 4. 运行 Application
|
||||
$application->run();
|
||||
123
src/Core/GameCommand.php
Normal file
123
src/Core/GameCommand.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
namespace Game\Core;
|
||||
|
||||
use Game\Event\MapExploreEvent;
|
||||
use Game\Event\MapMoveEvent;
|
||||
use Game\Event\ShowStatsEvent;
|
||||
use Game\System\BattleService;
|
||||
use Game\System\CharacterService;
|
||||
use Game\System\InputHandler;
|
||||
use Game\System\InteractionSystem;
|
||||
use Game\System\LootService;
|
||||
use Game\System\MapSystem;
|
||||
use Game\System\QuestService;
|
||||
use Game\System\StateManager;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Game\Database\DatabaseManager;
|
||||
use Game\Event\EventDispatcher;
|
||||
use Game\Event\Event;
|
||||
use Game\Model\Player;
|
||||
use Game\System\UIService; // 引入 UIService
|
||||
|
||||
class GameCommand extends Command {
|
||||
|
||||
protected static $defaultName = 'game:start';
|
||||
|
||||
private DatabaseManager $dbManager;
|
||||
private EventDispatcher $eventDispatcher;
|
||||
private UIService $uiService;
|
||||
private Player $player;
|
||||
private MapSystem $mapSystem;
|
||||
private StateManager $stateManager; // 新增 StateManager 属性
|
||||
private InputHandler $inputHandler;
|
||||
|
||||
protected function configure(): void {
|
||||
$this
|
||||
->setDescription('Starts the main PHP CLI RPG game loop.')
|
||||
->setHelp('Runs the main interactive game loop in the console.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
// 1. 初始化数据库管理器
|
||||
$this->dbManager = new DatabaseManager();
|
||||
$this->dbManager->loadInitialData();
|
||||
|
||||
// 2. 初始化 Event Dispatcher 和所有服务
|
||||
$this->initializeServices($input, $output);
|
||||
|
||||
// 3. 角色创建/加载存档
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question("请输入你的角色名称:", "旅行者");
|
||||
$playerName = $helper->ask($input, $output, $question);
|
||||
|
||||
// 创建玩家实例
|
||||
$player = new Player($playerName, 100, 15, 5);
|
||||
|
||||
// **将玩家实例交给 StateManager 管理**
|
||||
$this->stateManager->setPlayer($player);
|
||||
|
||||
// 通知 UI 服务
|
||||
$this->eventDispatcher->dispatch(new Event('SystemMessage', ['message' => "角色 {$player->getName()} 创建成功!"]));
|
||||
|
||||
return $this->mainLoop($input, $output);
|
||||
}
|
||||
|
||||
private function initializeServices(InputInterface $input, OutputInterface $output): void {
|
||||
|
||||
// 实例化 QuestionHelper
|
||||
$questionHelper = $this->getHelper('question');
|
||||
// 实例化 Event Dispatcher
|
||||
$this->eventDispatcher = new EventDispatcher();
|
||||
$this->stateManager = new StateManager($this->dbManager->getConnection());
|
||||
|
||||
|
||||
|
||||
// ⭐ 实例化 CharacterService
|
||||
$characterService = new CharacterService($this->eventDispatcher, $this->stateManager);
|
||||
$this->eventDispatcher->registerListener($characterService);
|
||||
|
||||
// ⭐ 实例化 LootService
|
||||
$lootService = new LootService($this->eventDispatcher, $this->stateManager);
|
||||
$this->eventDispatcher->registerListener($lootService);
|
||||
|
||||
// ⭐ 实例化 InteractionSystem
|
||||
$interactionSystem = new InteractionSystem($this->eventDispatcher, $this->stateManager, $input, $output, $questionHelper);
|
||||
$this->eventDispatcher->registerListener($interactionSystem);
|
||||
|
||||
// ⭐ 实例化 QuestService
|
||||
$questService = new QuestService($this->eventDispatcher, $this->stateManager);
|
||||
$this->eventDispatcher->registerListener($questService);
|
||||
|
||||
// ⭐ 实例化 InputHandler 并注入依赖
|
||||
$this->inputHandler = new InputHandler($this->eventDispatcher, $input, $output, $questionHelper);
|
||||
|
||||
// 实例化和注册 UIService (监听器)
|
||||
$this->uiService = new UIService($output, $this->stateManager);
|
||||
$this->eventDispatcher->registerListener($this->uiService);
|
||||
|
||||
// MapSystem 注册 (需要 EventDispatcher 和 DB 连接)
|
||||
$this->mapSystem = new MapSystem($this->eventDispatcher, $this->stateManager);
|
||||
$this->eventDispatcher->registerListener($this->mapSystem);
|
||||
|
||||
$questionHelper = $this->getHelper('question');
|
||||
$battleService = new BattleService($this->eventDispatcher, $this->stateManager, $input,$output, $questionHelper);
|
||||
$this->eventDispatcher->registerListener($battleService);
|
||||
|
||||
// 触发一个初始事件,让 UI 服务打印欢迎信息
|
||||
$welcomeEvent = new Event('GameStartEvent', ['message' => '核心系统已就绪,请输入指令开始游戏。']);
|
||||
$this->eventDispatcher->dispatch($welcomeEvent);
|
||||
}
|
||||
// src/Core/GameCommand.php (mainLoop 方法片段)
|
||||
|
||||
private function mainLoop(InputInterface $input, OutputInterface $output): int {
|
||||
$running = true;
|
||||
while ($running) {
|
||||
$running = $this->inputHandler->handleMainCommand();
|
||||
}
|
||||
$output->writeln("<info>游戏结束。感谢游玩!</info>");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
104
src/Database/DatabaseManager.php
Normal file
104
src/Database/DatabaseManager.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
namespace Game\Database;
|
||||
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* DatabaseManager: 负责 Doctrine DBAL 连接,SQLite Schema 初始化和初始配置数据填充。
|
||||
*/
|
||||
class DatabaseManager {
|
||||
private Connection $connection;
|
||||
|
||||
public function __construct() {
|
||||
// 确保使用绝对路径定位 storage 目录下的 SQLite 文件
|
||||
$dbPath = __DIR__ . '/../../storage/game.sqlite';
|
||||
|
||||
// 1. 设置连接参数
|
||||
$connectionParams = [
|
||||
'url' => "sqlite:///{$dbPath}",
|
||||
];
|
||||
|
||||
// 2. 建立连接
|
||||
$this->connection = DriverManager::getConnection($connectionParams);
|
||||
|
||||
// 3. 检查并创建数据库结构
|
||||
$this->initializeSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化/更新数据库结构 (Schema)
|
||||
*/
|
||||
private function initializeSchema(): void {
|
||||
$schemaManager = $this->connection->getSchemaManager();
|
||||
|
||||
// 只有在数据库为空时才尝试创建 Schema
|
||||
if (!$schemaManager->getDatabasePlatform()->supportsSchemas() && empty($schemaManager->listTables())) {
|
||||
$schema = new Schema();
|
||||
|
||||
// --- 游戏配置表:敌人数据 ---
|
||||
$enemiesTable = $schema->createTable('enemies');
|
||||
$enemiesTable->addColumn('id', 'integer', ['autoincrement' => true]);
|
||||
$enemiesTable->addColumn('name', 'string', ['length' => 50]);
|
||||
$enemiesTable->addColumn('health', 'integer');
|
||||
$enemiesTable->addColumn('attack', 'integer');
|
||||
$enemiesTable->addColumn('defense', 'integer'); // 新增 defense 字段
|
||||
$enemiesTable->addColumn('xp_value', 'integer');
|
||||
$enemiesTable->addColumn('drop_table_id', 'integer');
|
||||
$enemiesTable->setPrimaryKey(['id']);
|
||||
|
||||
// --- 玩家存档表:存档数据 (后续使用) ---
|
||||
$saveTable = $schema->createTable('player_save');
|
||||
$saveTable->addColumn('id', 'integer', ['autoincrement' => true]);
|
||||
$saveTable->addColumn('player_name', 'string');
|
||||
$saveTable->addColumn('data_json', 'text'); // 存储玩家对象序列化数据
|
||||
$saveTable->setPrimaryKey(['id']);
|
||||
|
||||
// 应用 Schema 变更
|
||||
$queries = $schema->toSql($this->connection->getDatabasePlatform());
|
||||
foreach ($queries as $query) {
|
||||
// 安全地执行 SQL 语句
|
||||
$this->connection->executeStatement($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getConnection(): Connection {
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始配置加载:将 JSON 数据填充到 SQLite
|
||||
*/
|
||||
public function loadInitialData(): void {
|
||||
$jsonPath = __DIR__ . '/../../config/enemy_data.json';
|
||||
if (!file_exists($jsonPath)) {
|
||||
echo "❌ 警告: 找不到初始配置数据文件 enemy_data.json\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// 仅在敌人表为空时填充数据
|
||||
if ($this->connection->fetchOne('SELECT COUNT(*) FROM enemies') == 0) {
|
||||
$data = json_decode(file_get_contents($jsonPath), true);
|
||||
$this->connection->beginTransaction();
|
||||
try {
|
||||
foreach ($data as $enemy) {
|
||||
$this->connection->insert('enemies', [
|
||||
'name' => $enemy['name'],
|
||||
'health' => $enemy['health'],
|
||||
'attack' => $enemy['attack'],
|
||||
'defense' => $enemy['defense'],
|
||||
'xp_value' => $enemy['xp'],
|
||||
'drop_table_id' => $enemy['dropId']
|
||||
]);
|
||||
}
|
||||
$this->connection->commit();
|
||||
echo "✅ 初始敌人数据已载入 SQLite 数据库。\n";
|
||||
} catch (\Exception $e) {
|
||||
$this->connection->rollBack();
|
||||
echo "❌ 数据填充失败: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Event/BattleEndEvent.php
Normal file
17
src/Event/BattleEndEvent.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
namespace Game\Event;
|
||||
|
||||
use Game\Model\Enemy;
|
||||
|
||||
/**
|
||||
* 战斗结束时触发,无论胜负。
|
||||
*/
|
||||
class BattleEndEvent extends Event {
|
||||
public function __construct(bool $isWin, Enemy $enemy) {
|
||||
// Payload 携带战斗结果和被击败的敌人实例
|
||||
parent::__construct('BattleEndEvent', [
|
||||
'isWin' => $isWin,
|
||||
'enemy' => $enemy
|
||||
]);
|
||||
}
|
||||
}
|
||||
32
src/Event/Event.php
Normal file
32
src/Event/Event.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
namespace Game\Event;
|
||||
|
||||
/**
|
||||
* Event: 基础事件类 (Abstract DTO)
|
||||
* 所有具体的事件都应继承此类。
|
||||
*/
|
||||
class Event {
|
||||
protected string $type;
|
||||
protected array $payload;
|
||||
|
||||
public function __construct(string $type, array $payload = []) {
|
||||
$this->type = $type;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件的类型字符串。
|
||||
*/
|
||||
public final function getType(): string {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件携带的数据。
|
||||
*/
|
||||
public final function getPayload(): array {
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
// final 关键字防止子类修改核心 getter 方法,保证事件数据的一致性。
|
||||
}
|
||||
43
src/Event/EventDispatcher.php
Normal file
43
src/Event/EventDispatcher.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
namespace Game\Event;
|
||||
|
||||
/**
|
||||
* EventDispatcher: 游戏的事件总线,负责事件的注册和分发。
|
||||
*/
|
||||
class EventDispatcher {
|
||||
/**
|
||||
* @var array<string, array<EventListenerInterface>>
|
||||
*/
|
||||
private array $listeners = [];
|
||||
|
||||
/**
|
||||
* 注册一个监听器到总线。
|
||||
* 监听器需要自己判断对哪些事件感兴趣。
|
||||
*/
|
||||
public function registerListener(EventListenerInterface $listener): void {
|
||||
// 我们可以简化处理,直接将监听器按类名存储,因为我们只有一个 handleEvent 方法
|
||||
$listenerClass = get_class($listener);
|
||||
if (!in_array($listener, $this->listeners, true)) {
|
||||
$this->listeners[] = $listener;
|
||||
}
|
||||
|
||||
// 更好做法是按事件类型注册,但这里为了简化,我们让所有监听器接收所有事件
|
||||
}
|
||||
|
||||
/**
|
||||
* 分发一个事件给所有注册的监听器。
|
||||
*/
|
||||
public function dispatch(Event $event): void {
|
||||
//
|
||||
|
||||
$eventType = $event->getType();
|
||||
|
||||
// 打印调试信息(可选,但推荐)
|
||||
// echo ">> DEBUG: 触发事件: {$eventType}\n";
|
||||
|
||||
foreach ($this->listeners as $listener) {
|
||||
// 将事件转发给每个监听器处理
|
||||
$listener->handleEvent($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/Event/EventListenerInterface.php
Normal file
6
src/Event/EventListenerInterface.php
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
namespace Game\Event;
|
||||
|
||||
interface EventListenerInterface {
|
||||
public function handleEvent(Event $event): void;
|
||||
}
|
||||
11
src/Event/MapExploreEvent.php
Normal file
11
src/Event/MapExploreEvent.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
namespace Game\Event;
|
||||
|
||||
/**
|
||||
* 当玩家选择“原地探索”时触发,用于决定遭遇什么(战斗、宝箱、对话)。
|
||||
*/
|
||||
class MapExploreEvent extends Event {
|
||||
public function __construct(string $tileId) {
|
||||
parent::__construct('MapExploreEvent', ['tileId' => $tileId]);
|
||||
}
|
||||
}
|
||||
12
src/Event/MapMoveEvent.php
Normal file
12
src/Event/MapMoveEvent.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
namespace Game\Event;
|
||||
|
||||
/**
|
||||
* 当玩家移动到一个新的 MapTile 时触发。
|
||||
*/
|
||||
class MapMoveEvent extends Event {
|
||||
public function __construct(string $newTileId) {
|
||||
// Payload 携带新区域的 ID
|
||||
parent::__construct('MapMoveEvent', ['newTileId' => $newTileId]);
|
||||
}
|
||||
}
|
||||
14
src/Event/ShowStatsEvent.php
Normal file
14
src/Event/ShowStatsEvent.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
namespace Game\Event;
|
||||
|
||||
use Game\Model\Player;
|
||||
|
||||
/**
|
||||
* 玩家要求查看状态时触发。
|
||||
*/
|
||||
class ShowStatsEvent extends Event {
|
||||
public function __construct(Player $player) {
|
||||
// Payload 携带玩家实例本身
|
||||
parent::__construct('ShowStatsEvent', ['player' => $player]);
|
||||
}
|
||||
}
|
||||
12
src/Event/StartBattleEvent.php
Normal file
12
src/Event/StartBattleEvent.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
namespace Game\Event;
|
||||
|
||||
/**
|
||||
* 触发一场新的战斗。
|
||||
*/
|
||||
class StartBattleEvent extends Event {
|
||||
public function __construct(int $enemyId) {
|
||||
// Payload 携带要加载的敌人配置 ID (来自 SQLite)
|
||||
parent::__construct('StartBattleEvent', ['enemyId' => $enemyId]);
|
||||
}
|
||||
}
|
||||
105
src/Model/Character.php
Normal file
105
src/Model/Character.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
namespace Game\Model;
|
||||
|
||||
/**
|
||||
* Character: 所有游戏角色的基类 (Model)
|
||||
*/
|
||||
class Character {
|
||||
protected string $name;
|
||||
protected int $health;
|
||||
protected int $maxHealth;
|
||||
protected int $attack;
|
||||
protected int $defense;
|
||||
|
||||
protected array $activeQuests = []; // 存储进行中的任务进度,格式: ['questId' => ['currentCount' => 0, 'isCompleted' => false]]
|
||||
protected array $completedQuests = []; // 存储已完成的任务 ID
|
||||
// ⭐ 新增方法:添加/接受任务
|
||||
public function addActiveQuest(string $questId, int $targetCount): void {
|
||||
$this->activeQuests[$questId] = ['currentCount' => 0, 'isCompleted' => false, 'targetCount' => $targetCount];
|
||||
}
|
||||
|
||||
// ⭐ 新增方法:获取进行中的任务
|
||||
public function getActiveQuests(): array {
|
||||
return $this->activeQuests;
|
||||
}
|
||||
|
||||
// ⭐ 新增方法:更新任务进度
|
||||
public function updateQuestProgress(string $questId, int $count = 1): void {
|
||||
if (isset($this->activeQuests[$questId])) {
|
||||
$progress = &$this->activeQuests[$questId]; // 使用引用
|
||||
if (!$progress['isCompleted']) {
|
||||
$progress['currentCount'] += $count;
|
||||
if ($progress['currentCount'] >= $progress['targetCount']) {
|
||||
$progress['currentCount'] = $progress['targetCount'];
|
||||
$progress['isCompleted'] = true;
|
||||
// 触发 QuestCompletedEventRequest
|
||||
// 注意:实际的奖励和标记完成应在 QuestService 确认后进行
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ⭐ 新增方法:标记任务完成
|
||||
public function markQuestCompleted(string $questId): void {
|
||||
unset($this->activeQuests[$questId]);
|
||||
$this->completedQuests[] = $questId;
|
||||
}
|
||||
|
||||
// ⭐ 新增方法:检查任务是否已完成
|
||||
public function isQuestCompleted(string $questId): bool {
|
||||
return in_array($questId, $this->completedQuests);
|
||||
}
|
||||
|
||||
// ⭐ 新增:玩家背包 (存储 Item 实例)
|
||||
protected array $inventory = [];
|
||||
|
||||
// ... 现有构造函数和 Getter ...
|
||||
|
||||
// ⭐ 新增方法:添加物品到背包
|
||||
public function addItem(Item $item): void {
|
||||
$this->inventory[] = $item;
|
||||
}
|
||||
|
||||
// ⭐ 新增方法:获取背包
|
||||
public function getInventory(): array {
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
public function __construct(string $name, int $maxHealth, int $attack, int $defense) {
|
||||
$this->name = $name;
|
||||
$this->maxHealth = $maxHealth;
|
||||
$this->health = $maxHealth;
|
||||
$this->attack = $attack;
|
||||
$this->defense = $defense;
|
||||
}
|
||||
|
||||
public function getName(): string { return $this->name; }
|
||||
public function getHealth(): int { return $this->health; }
|
||||
public function getMaxHealth(): int { return $this->maxHealth; }
|
||||
public function getAttack(): int { return $this->attack; }
|
||||
public function getDefense(): int { return $this->defense; }
|
||||
|
||||
public function isAlive(): bool { return $this->health > 0; }
|
||||
|
||||
/**
|
||||
* 接收伤害,返回实际受到的伤害量
|
||||
*/
|
||||
public function takeDamage(int $damage): int {
|
||||
$actualDamage = max(0, $damage - $this->defense);
|
||||
$this->health -= $actualDamage;
|
||||
if ($this->health < 0) {
|
||||
$this->health = 0;
|
||||
}
|
||||
return $actualDamage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 治疗角色
|
||||
*/
|
||||
public function heal(int $amount): void {
|
||||
$this->health += $amount;
|
||||
if ($this->health > $this->maxHealth) {
|
||||
$this->health = $this->maxHealth;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Model/Enemy.php
Normal file
22
src/Model/Enemy.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
namespace Game\Model;
|
||||
|
||||
// 继承 Character 基类
|
||||
class Enemy extends Character {
|
||||
public string $id;
|
||||
public int $xpValue; // 击败后获得的经验值
|
||||
|
||||
public function __construct(string $id, string $name, int $health, int $attack, int $defense, int $xpValue) {
|
||||
// 调用父类 (Character) 的构造函数来初始化核心属性
|
||||
parent::__construct($name, $health, $attack, $defense);
|
||||
|
||||
$this->id = $id;
|
||||
$this->xpValue = $xpValue;
|
||||
}
|
||||
|
||||
// Enemy 特有的 Getter 方法
|
||||
public function getId(): string { return $this->id; }
|
||||
public function getXpValue(): int { return $this->xpValue; }
|
||||
|
||||
// 注意:takeDamage() 和 isAlive() 等方法都直接继承自 Character,无需重复实现!
|
||||
}
|
||||
21
src/Model/Item.php
Normal file
21
src/Model/Item.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
namespace Game\Model;
|
||||
|
||||
/**
|
||||
* Item: 所有游戏物品的基类
|
||||
*/
|
||||
class Item {
|
||||
public int $id;
|
||||
public string $name;
|
||||
public string $type; // e.g., 'potion', 'weapon', 'material'
|
||||
public string $description;
|
||||
public int $value; // 卖出价格
|
||||
|
||||
public function __construct(int $id, string $name, string $type, string $description, int $value) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->type = $type;
|
||||
$this->description = $description;
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
21
src/Model/MapTile.php
Normal file
21
src/Model/MapTile.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
namespace Game\Model;
|
||||
|
||||
/**
|
||||
* MapTile: 单个地图区域的数据模型。
|
||||
*/
|
||||
class MapTile {
|
||||
public string $id;
|
||||
public string $name;
|
||||
public string $description;
|
||||
public array $connections; // 存储连接:['N' => 'FOREST_02', 'S' => 'TOWN_01']
|
||||
public int $eventPoolId; // 区域对应的事件池ID
|
||||
|
||||
public function __construct(string $id, string $name, string $description, array $connections, int $eventPoolId) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->description = $description;
|
||||
$this->connections = $connections;
|
||||
$this->eventPoolId = $eventPoolId;
|
||||
}
|
||||
}
|
||||
28
src/Model/NPC.php
Normal file
28
src/Model/NPC.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
namespace Game\Model;
|
||||
|
||||
// NPC 可以继承 Character,但如果它不参与战斗,可以简化为独立模型。
|
||||
// 为了简化,我们让它独立存在,只关注对话。
|
||||
|
||||
class NPC {
|
||||
public string $id;
|
||||
public string $name;
|
||||
public array $dialogue; // 存储对话行或对话树结构
|
||||
|
||||
public function __construct(string $id, string $name, array $dialogue) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->dialogue = $dialogue;
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定对话ID的文本
|
||||
*/
|
||||
public function getDialogueText(string $key): string {
|
||||
return $this->dialogue[$key] ?? "NPC:对不起,我不知道你在说什么。";
|
||||
}
|
||||
}
|
||||
25
src/Model/Player.php
Normal file
25
src/Model/Player.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
namespace Game\Model;
|
||||
|
||||
// 继承 Character 基类
|
||||
class Player extends Character {
|
||||
protected int $level = 1;
|
||||
protected int $currentXp = 0;
|
||||
protected int $xpToNextLevel = 100;
|
||||
|
||||
public function __construct(string $name, int $maxHealth, int $attack, int $defense) {
|
||||
// 调用父类 (Character) 的构造函数来初始化核心属性
|
||||
parent::__construct($name, $maxHealth, $attack, $defense);
|
||||
}
|
||||
|
||||
// Player 特有的 Getter 方法
|
||||
public function getLevel(): int { return $this->level; }
|
||||
public function getCurrentXp(): int { return $this->currentXp; }
|
||||
public function getXpToNextLevel(): int { return $this->xpToNextLevel; }
|
||||
|
||||
// Player 特有的 Setter/Modifier 方法
|
||||
public function gainXp(int $amount): void {
|
||||
$this->currentXp += $amount;
|
||||
// TODO: 未来在这里实现升级逻辑 (LevelUpEvent)
|
||||
}
|
||||
}
|
||||
24
src/Model/Quest.php
Normal file
24
src/Model/Quest.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
namespace Game\Model;
|
||||
|
||||
/**
|
||||
* Quest: 任务模型
|
||||
*/
|
||||
class Quest {
|
||||
public string $id;
|
||||
public string $name;
|
||||
public string $description;
|
||||
public string $type; // e.g., 'kill', 'fetch', 'talk'
|
||||
public array $target; // 目标要求,e.g., ['targetId' => 'GOBLIN_1', 'count' => 5]
|
||||
public array $rewards; // 奖励,e.g., ['xp' => 100, 'itemId' => 1]
|
||||
public bool $isRepeatable = false;
|
||||
|
||||
public function __construct(string $id, string $name, string $description, string $type, array $target, array $rewards) {
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
$this->description = $description;
|
||||
$this->type = $type;
|
||||
$this->target = $target;
|
||||
$this->rewards = $rewards;
|
||||
}
|
||||
}
|
||||
232
src/System/BattleService.php
Normal file
232
src/System/BattleService.php
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Event\Event;
|
||||
use Game\Event\EventListenerInterface;
|
||||
use Game\Event\EventDispatcher;
|
||||
use Game\Model\Enemy;
|
||||
use Game\Model\Player; // 即使是 Party,也依赖 Player
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
/**
|
||||
* BattleService: 负责处理回合制战斗逻辑。
|
||||
* 战斗循环采用阻塞模式 (CLI 常见),直接获取输入。
|
||||
*/
|
||||
class BattleService implements EventListenerInterface {
|
||||
|
||||
private EventDispatcher $dispatcher;
|
||||
private StateManager $stateManager;
|
||||
|
||||
// 战斗内输入依赖 (暂时不解耦,简化战斗循环)
|
||||
private InputInterface $input;
|
||||
private OutputInterface $output;
|
||||
private QuestionHelper $helper;
|
||||
|
||||
private bool $inBattle = false;
|
||||
private ?Enemy $currentEnemy = null; // 使用 ?Type 确保在非战斗状态下为 null
|
||||
|
||||
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager, InputInterface $input, OutputInterface $output, QuestionHelper $helper) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stateManager = $stateManager;
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->helper = $helper;
|
||||
}
|
||||
|
||||
public function handleEvent(Event $event): void {
|
||||
if ($this->inBattle) {
|
||||
// 如果在战斗中,可以监听 'BattleCommandEvent' 等事件来处理输入
|
||||
// 当前版本,我们通过 battleLoop() 内部阻塞输入
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($event->getType()) {
|
||||
case 'StartBattleEvent':
|
||||
// 收到 MapSystem 触发的战斗开始事件
|
||||
$enemyId = $event->getPayload()['enemyId'];
|
||||
$this->startBattle($enemyId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 初始化战斗状态
|
||||
*/
|
||||
private function startBattle(int $enemyId): void {
|
||||
// ... 初始化敌人逻辑 (继承自 Character) ...
|
||||
$enemyData = $this->loadEnemyData($enemyId);
|
||||
$this->currentEnemy = new Enemy(
|
||||
(string)$enemyId,
|
||||
$enemyData['name'],
|
||||
$enemyData['health'],
|
||||
$enemyData['attack'],
|
||||
$enemyData['defense'],
|
||||
$enemyData['xp']
|
||||
);
|
||||
$this->inBattle = true;
|
||||
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "⚔️ 你遭遇了 <fg=red;options=bold>{$this->currentEnemy->getName()}</>!战斗开始!"
|
||||
]));
|
||||
|
||||
// 立即进入战斗循环
|
||||
$this->battleLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 核心战斗循环
|
||||
*/
|
||||
private function battleLoop(): void {
|
||||
while ($this->inBattle) {
|
||||
|
||||
// 确保 UI 服务打印了战斗状态
|
||||
$this->dispatcher->dispatch(new Event('ShowBattleStatsEvent', [
|
||||
'enemy' => $this->currentEnemy,
|
||||
'player' => $this->stateManager->getPlayer(),
|
||||
]));
|
||||
|
||||
// 玩家回合 (阻塞输入)
|
||||
$playerAction = $this->promptPlayerAction();
|
||||
|
||||
if ($playerAction === 'A') {
|
||||
$this->playerAttack();
|
||||
} elseif ($playerAction === 'R') {
|
||||
if ($this->tryRunAway()) {
|
||||
$this->endBattle(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->inBattle) break;
|
||||
|
||||
// 敌人回合
|
||||
$this->enemyAttack();
|
||||
|
||||
if (!$this->inBattle) break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取玩家战斗指令 (直接使用注入的 I/O 接口)
|
||||
*/
|
||||
private function promptPlayerAction(): string {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "\n--- 你的回合 --- [A] 攻击 | [R] 逃跑"
|
||||
]));
|
||||
|
||||
$question = new Question("> 请选择指令 (A/R):");
|
||||
|
||||
// 关键:使用注入的 I/O 接口
|
||||
$choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? '');
|
||||
|
||||
// 简单的输入验证
|
||||
if (in_array($choice, ['A', 'R'])) {
|
||||
return $choice;
|
||||
} else {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的战斗指令。']));
|
||||
return $this->promptPlayerAction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家攻击逻辑
|
||||
*/
|
||||
private function playerAttack(): void {
|
||||
$player = $this->stateManager->getPlayer();
|
||||
$rawDamage = $player->getAttack();
|
||||
|
||||
$actualDamage = $this->currentEnemy->takeDamage($rawDamage);
|
||||
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "⚡ 你对 {$this->currentEnemy->getName()} 造成了 <fg=yellow>{$actualDamage}</> 点伤害!"
|
||||
]));
|
||||
|
||||
if (!$this->currentEnemy->isAlive()) {
|
||||
$this->handleWin();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 敌人攻击逻辑
|
||||
*/
|
||||
private function enemyAttack(): void {
|
||||
$player = $this->stateManager->getPlayer();
|
||||
$rawDamage = $this->currentEnemy->getAttack();
|
||||
|
||||
// Player 的 takeDamage 会更新 StateManager 中的 Player 实例
|
||||
$actualDamage = $player->takeDamage($rawDamage);
|
||||
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "💥 {$this->currentEnemy->getName()} 对你造成了 <fg=red>{$actualDamage}</> 点伤害!"
|
||||
]));
|
||||
|
||||
if (!$player->isAlive()) {
|
||||
$this->handleLoss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试逃跑
|
||||
*/
|
||||
private function tryRunAway(): bool {
|
||||
if (rand(1, 100) > 50) {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "💨 你成功逃离了战斗!"]));
|
||||
return true;
|
||||
} else {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "被 {$this->currentEnemy->getName()} 拦住了!逃跑失败。"]));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 战斗胜利处理
|
||||
*/
|
||||
private function handleWin(): void {
|
||||
$xpGained = $this->currentEnemy->getXpValue();
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "🏆 恭喜你击败了 {$this->currentEnemy->getName()}!"
|
||||
]));
|
||||
|
||||
// 触发 XP 和 Loot 事件,交由其他系统处理
|
||||
$this->dispatcher->dispatch(new Event('XpGainedEvent', ['xp' => $xpGained]));
|
||||
$this->dispatcher->dispatch(new Event('LootDropEvent', ['enemyId' => $this->currentEnemy->getId()]));
|
||||
|
||||
$this->endBattle(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. 战斗失败处理
|
||||
*/
|
||||
private function handleLoss(): void {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "💀 你被击败了... 游戏结束。"]));
|
||||
// TODO: 触发 GameEndEvent 或 RespawnEvent
|
||||
$this->endBattle(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束战斗状态
|
||||
*/
|
||||
private function endBattle(bool $isWin): void {
|
||||
$this->inBattle = false;
|
||||
$this->currentEnemy = null;
|
||||
|
||||
// 战斗结束后,重新打印主菜单请求,以继续主循环
|
||||
$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],
|
||||
};
|
||||
}
|
||||
}
|
||||
76
src/System/CharacterService.php
Normal file
76
src/System/CharacterService.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Event\Event;
|
||||
use Game\Event\EventListenerInterface;
|
||||
use Game\Event\EventDispatcher;
|
||||
use Game\Model\Player;
|
||||
|
||||
/**
|
||||
* CharacterService: 负责处理角色的成长、经验值和升级逻辑。
|
||||
*/
|
||||
class CharacterService implements EventListenerInterface {
|
||||
|
||||
private EventDispatcher $dispatcher;
|
||||
private StateManager $stateManager;
|
||||
|
||||
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stateManager = $stateManager;
|
||||
}
|
||||
|
||||
public function handleEvent(Event $event): void {
|
||||
switch ($event->getType()) {
|
||||
case 'XpGainedEvent':
|
||||
$this->handleXpGain($event->getPayload()['xp']);
|
||||
break;
|
||||
// TODO: 未来添加 'HealRequest' 或 'UseItemEvent' 等事件处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理经验值获取和潜在升级逻辑
|
||||
*/
|
||||
private function handleXpGain(int $xpAmount): void {
|
||||
$player = $this->stateManager->getPlayer();
|
||||
$initialLevel = $player->getLevel();
|
||||
|
||||
// 尝试添加经验值 (这个方法现在在 Player 模型中)
|
||||
$player->gainXp($xpAmount);
|
||||
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "🌟 获得了 <fg=yellow>{$xpAmount}</> 点经验值!"
|
||||
]));
|
||||
|
||||
// 检查是否升级
|
||||
while ($player->getCurrentXp() >= $player->getXpToNextLevel()) {
|
||||
$this->levelUp($player);
|
||||
}
|
||||
|
||||
if ($player->getLevel() > $initialLevel) {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "🎉 <fg=green;options=bold>恭喜你,升到了等级 {$player->getLevel()}!</>"
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行升级操作
|
||||
*/
|
||||
private function levelUp(Player $player): void {
|
||||
// 1. 扣除经验,提升等级
|
||||
$player->subtractXpToNextLevel(); // 假设我们在 Player 模型中添加此方法
|
||||
$player->incrementLevel(); // 假设我们在 Player 模型中添加此方法
|
||||
|
||||
// 2. 提升属性 (简化版:每次升级增加固定属性)
|
||||
$player->increaseMaxHealth(10);
|
||||
$player->increaseAttack(2);
|
||||
$player->increaseDefense(1);
|
||||
|
||||
// 3. 升级后回满血
|
||||
$player->heal($player->getMaxHealth()); // 使用 Character 基类中的 heal()
|
||||
|
||||
// 4. 触发升级事件 (如果需要其他系统知道)
|
||||
$this->dispatcher->dispatch(new Event('LevelUpEvent', ['newLevel' => $player->getLevel()]));
|
||||
}
|
||||
}
|
||||
79
src/System/InputHandler.php
Normal file
79
src/System/InputHandler.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Event\Event;
|
||||
use Game\Event\EventDispatcher;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
|
||||
/**
|
||||
* InputHandler: 负责从 CLI 获取用户输入,并将其解析为系统事件。
|
||||
* 遵循单一职责原则:只处理输入和事件转发。
|
||||
*/
|
||||
class InputHandler {
|
||||
|
||||
private EventDispatcher $dispatcher;
|
||||
private InputInterface $input;
|
||||
private OutputInterface $output;
|
||||
private QuestionHelper $helper;
|
||||
|
||||
public function __construct(EventDispatcher $dispatcher, InputInterface $input, OutputInterface $output, QuestionHelper $helper) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->helper = $helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主循环操作指令,并分派事件
|
||||
*/
|
||||
public function handleMainCommand(): bool {
|
||||
// 1. 请求 UI 服务打印主菜单 (确保 UI 已输出提示)
|
||||
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
|
||||
|
||||
$question = new Question("> 请选择操作 (M/E/S/I/Q):");
|
||||
$choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? '');
|
||||
|
||||
// 2. 解析并分派事件
|
||||
switch ($choice) {
|
||||
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 'I': // ⭐ 新增交互指令
|
||||
// 模拟 MapSystem 发现了一个 NPC
|
||||
$this->dispatcher->dispatch(new Event('AttemptInteractEvent', ['npcId' => 'VILLAGER_1']));
|
||||
break;
|
||||
case 'Q':
|
||||
return false; // 返回 false 通知 GameCommand 退出主循环
|
||||
default:
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的指令。请使用 M, E, S, I, Q。']));
|
||||
}
|
||||
return true; // 继续主循环
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理移动输入和事件分派
|
||||
*/
|
||||
private function handleMoveInput(): void {
|
||||
$directionQuestion = new Question("请输入移动方向 (<fg=yellow>N/S/E/W</>):");
|
||||
$direction = strtoupper($this->helper->ask($this->input, $this->output, $directionQuestion) ?? '');
|
||||
|
||||
$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' => '无效的移动方向。']));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 可以在这里添加 handleBattleInput() 等,进一步解耦 BattleService
|
||||
}
|
||||
137
src/System/InteractionSystem.php
Normal file
137
src/System/InteractionSystem.php
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Event\Event;
|
||||
use Game\Event\EventListenerInterface;
|
||||
use Game\Event\EventDispatcher;
|
||||
use Game\Model\NPC;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
/**
|
||||
* InteractionSystem: 负责处理玩家与 NPC 的对话和交互流程。
|
||||
*/
|
||||
class InteractionSystem implements EventListenerInterface {
|
||||
|
||||
private EventDispatcher $dispatcher;
|
||||
private StateManager $stateManager;
|
||||
|
||||
// 输入依赖
|
||||
private InputInterface $input;
|
||||
private OutputInterface $output;
|
||||
private QuestionHelper $helper;
|
||||
|
||||
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager, InputInterface $input, OutputInterface $output, QuestionHelper $helper) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stateManager = $stateManager;
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->helper = $helper;
|
||||
}
|
||||
|
||||
public function handleEvent(Event $event): void {
|
||||
switch ($event->getType()) {
|
||||
case 'AttemptInteractEvent':
|
||||
// 假设 MapSystem 会提供当前 Tile 上的 NPC ID
|
||||
$npcId = $event->getPayload()['npcId'];
|
||||
$this->startInteraction($npcId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 初始化 NPC 交互
|
||||
*/
|
||||
private function startInteraction(string $npcId): void {
|
||||
$npc = $this->loadNPC($npcId);
|
||||
if (!$npc) {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "这里没有人可以交谈。"]));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "👤 你走近了 <fg=cyan;options=bold>{$npc->getName()}</>。"
|
||||
]));
|
||||
|
||||
// 启动对话流程
|
||||
$this->dialogueLoop($npc);
|
||||
|
||||
// 交互结束后,重新打印主菜单请求
|
||||
$this->dispatcher->dispatch(new Event('ShowMenuEvent'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 核心对话循环
|
||||
*/
|
||||
private function dialogueLoop(NPC $npc): void {
|
||||
$currentDialogueKey = 'greeting';
|
||||
$running = true;
|
||||
|
||||
while ($running) {
|
||||
|
||||
// 打印 NPC 对话
|
||||
$text = $npc->getDialogueText($currentDialogueKey);
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "<fg=cyan>{$npc->getName()}</>:<fg=white>{$text}</>"
|
||||
]));
|
||||
|
||||
// 检查交互类型并获取玩家选择
|
||||
$choice = $this->promptPlayerChoice();
|
||||
|
||||
switch ($choice) {
|
||||
case 'T': // 触发任务/特殊事件
|
||||
$this->dispatcher->dispatch(new Event('QuestCheckEvent', ['npcId' => $npc->id]));
|
||||
$currentDialogueKey = 'quest_response';
|
||||
break;
|
||||
case 'S': // 触发商店
|
||||
$this->dispatcher->dispatch(new Event('OpenShopEvent', ['npcId' => $npc->id]));
|
||||
$currentDialogueKey = 'shop_response';
|
||||
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 promptPlayerChoice(): string {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "\n--- 交互菜单 --- [T] 任务 | [S] 商店 | [E] 结束"
|
||||
]));
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
99
src/System/LootService.php
Normal file
99
src/System/LootService.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Event\Event;
|
||||
use Game\Event\EventListenerInterface;
|
||||
use Game\Event\EventDispatcher;
|
||||
use Game\Model\Item;
|
||||
|
||||
/**
|
||||
* LootService: 负责处理物品掉落、背包管理和物品获取。
|
||||
*/
|
||||
class LootService implements EventListenerInterface {
|
||||
|
||||
private EventDispatcher $dispatcher;
|
||||
private StateManager $stateManager;
|
||||
|
||||
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stateManager = $stateManager;
|
||||
}
|
||||
|
||||
public function handleEvent(Event $event): void {
|
||||
switch ($event->getType()) {
|
||||
case 'LootDropEvent':
|
||||
$enemyId = $event->getPayload()['enemyId'];
|
||||
$this->handleLootDrop($enemyId);
|
||||
break;
|
||||
case 'LootFoundEvent': // 响应 MapSystem 探索时发现的宝箱
|
||||
$lootId = $event->getPayload()['lootId'];
|
||||
$this->handleLootFound($lootId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理敌人死亡时的掉落逻辑
|
||||
*/
|
||||
private function handleLootDrop(string $enemyId): void {
|
||||
// 简化:总是掉落物品 ID 1 (小药水)
|
||||
$roll = rand(1, 100);
|
||||
if ($roll <= 70) { // 70% 掉落几率
|
||||
$this->giveItemToPlayer(1);
|
||||
} else {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "战利品很少,只找到了一些零钱。"
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理探索时发现的宝箱/固定物品
|
||||
*/
|
||||
private function handleLootFound(int $lootId): void {
|
||||
// 假设 lootId 5 是一个宝箱,里面是物品 ID 2
|
||||
if ($lootId === 5) {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "🗝️ 你打开了宝箱!"
|
||||
]));
|
||||
$this->giveItemToPlayer(2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心逻辑:创建物品实例并添加到玩家背包
|
||||
*/
|
||||
private function giveItemToPlayer(int $itemId): void {
|
||||
$itemData = $this->loadItemData($itemId);
|
||||
|
||||
$item = new Item(
|
||||
$itemId,
|
||||
$itemData['name'],
|
||||
$itemData['type'],
|
||||
$itemData['description'],
|
||||
$itemData['value']
|
||||
);
|
||||
|
||||
$player = $this->stateManager->getPlayer();
|
||||
$player->addItem($item);
|
||||
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "➕ 获得了物品:<fg=green>{$item->name}</>!"
|
||||
]));
|
||||
|
||||
// 触发 InventoryUpdateEvent (未来用于 UI 实时更新)
|
||||
$this->dispatcher->dispatch(new Event('InventoryUpdateEvent', ['playerInventory' => $player->getInventory()]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟从配置中加载物品数据
|
||||
*/
|
||||
private function loadItemData(int $id): array {
|
||||
// 实际项目中应从数据库或 JSON 加载
|
||||
return match ($id) {
|
||||
1 => ['name' => '小型治疗药水', 'type' => 'potion', 'description' => '恢复少量生命。', 'value' => 10],
|
||||
2 => ['name' => '破旧的短剑', 'type' => 'weapon', 'description' => '攻击力微弱。', 'value' => 50],
|
||||
default => ['name' => '垃圾', 'type' => 'misc', 'description' => '毫无价值的杂物。', 'value' => 1],
|
||||
};
|
||||
}
|
||||
}
|
||||
115
src/System/MapSystem.php
Normal file
115
src/System/MapSystem.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Event\Event;
|
||||
use Game\Event\EventListenerInterface;
|
||||
use Game\Event\EventDispatcher;
|
||||
use Game\Model\MapTile; // 引入 MapTile 模型
|
||||
use Game\Event\StartBattleEvent;
|
||||
|
||||
class MapSystem implements EventListenerInterface {
|
||||
|
||||
private EventDispatcher $dispatcher;
|
||||
private StateManager $stateManager;
|
||||
private array $mapData;
|
||||
|
||||
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stateManager = $stateManager;
|
||||
$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];
|
||||
return new MapTile($tileId, $data['name'], $data['description'], $data['connections'], $data['eventPoolId']);
|
||||
}
|
||||
|
||||
public function handleEvent(Event $event): void {
|
||||
switch ($event->getType()) {
|
||||
case 'AttemptMoveEvent':
|
||||
$this->handleMoveAttempt($event->getPayload()['direction']);
|
||||
break;
|
||||
case 'MapExploreRequest': // 修正:监听 MapExploreRequest 事件 (来自 GameCommand)
|
||||
$this->handleExplore();
|
||||
break;
|
||||
case 'GameStartEvent':
|
||||
// 游戏开始,通知 UI 打印初始位置
|
||||
$initialTile = $this->stateManager->getCurrentTile();
|
||||
$this->dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => $initialTile->id]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家移动逻辑
|
||||
*/
|
||||
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);
|
||||
|
||||
// 1. 成功移动:更新 StateManager 中的状态
|
||||
$this->stateManager->setCurrentTile($newTile);
|
||||
|
||||
// 2. 触发 MapMoveEvent,通知 UI 和其他系统
|
||||
$this->dispatcher->dispatch(new Event('MapMoveEvent', ['newTileId' => $newTileId]));
|
||||
} else {
|
||||
// 移动失败:通知 UI
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "❌ 该方向 ({$direction}) 没有道路或无法通行。"]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理玩家探索逻辑:根据 eventPoolId 决定遭遇什么
|
||||
*/
|
||||
private function handleExplore(): void {
|
||||
// 从 StateManager 获取当前 Tile
|
||||
$currentTile = $this->stateManager->getCurrentTile();
|
||||
|
||||
$roll = rand(1, 100);
|
||||
|
||||
if ($currentTile->eventPoolId === 0) { // 城镇
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "你在镇上休息了一会儿,没有发现任何危险。"]));
|
||||
} elseif ($roll <= 60) {
|
||||
// 60% 几率遭遇战斗
|
||||
$enemyId = $this->getEnemyIdForArea($currentTile->eventPoolId); // 使用当前 Tile 的 eventPoolId
|
||||
$this->dispatcher->dispatch(new StartBattleEvent($enemyId));
|
||||
} elseif ($roll <= 80) {
|
||||
// 20% 几率发现宝箱/物品
|
||||
$this->dispatcher->dispatch(new Event('LootFoundEvent', ['lootId' => 5]));
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "🎁 你发现了一个宝箱!"]));
|
||||
} else {
|
||||
// 20% 几率没有遭遇
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => "你在周围探索了一番,但什么也没发现。"]));
|
||||
}
|
||||
}
|
||||
|
||||
// 简化的敌人 ID 获取 (应该从配置中获取)
|
||||
private function getEnemyIdForArea(int $poolId): int {
|
||||
return match ($poolId) {
|
||||
1 => rand(1, 2), // 新手区敌人 ID 1 或 2
|
||||
2 => 3, // 森林敌人 ID 3
|
||||
default => 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
154
src/System/QuestService.php
Normal file
154
src/System/QuestService.php
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Event\Event;
|
||||
use Game\Event\EventListenerInterface;
|
||||
use Game\Event\EventDispatcher;
|
||||
use Game\Model\Quest;
|
||||
use Game\Model\Player;
|
||||
|
||||
/**
|
||||
* QuestService: 管理任务的接受、追踪和奖励发放。
|
||||
*/
|
||||
class QuestService implements EventListenerInterface {
|
||||
|
||||
private EventDispatcher $dispatcher;
|
||||
private StateManager $stateManager;
|
||||
private array $questData; // 存储所有任务配置
|
||||
|
||||
public function __construct(EventDispatcher $dispatcher, StateManager $stateManager) {
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->stateManager = $stateManager;
|
||||
$this->loadQuestData();
|
||||
}
|
||||
|
||||
private function loadQuestData(): void {
|
||||
// 模拟从配置加载任务数据
|
||||
$this->questData = [
|
||||
'RATS_1' => [
|
||||
'name' => '地窖里的老鼠',
|
||||
'description' => '为老村长清除地窖里 5 只弱小的哥布林。',
|
||||
'type' => 'kill',
|
||||
'target' => ['targetId' => 1, 'count' => 5], // 目标敌人 ID 1
|
||||
'rewards' => ['xp' => 100, 'itemId' => 1]
|
||||
],
|
||||
// ... 其他任务 ...
|
||||
];
|
||||
}
|
||||
|
||||
public function handleEvent(Event $event): void {
|
||||
switch ($event->getType()) {
|
||||
case 'QuestCheckEvent': // 响应 InteractionSystem 的任务请求
|
||||
$this->handleQuestCheck($event->getPayload()['npcId']);
|
||||
break;
|
||||
case 'BattleEndEvent': // 响应战斗结束,检查击杀目标
|
||||
$this->checkKillQuests($event->getPayload()['enemyId']);
|
||||
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. 检查击杀类任务进度
|
||||
*/
|
||||
private function checkKillQuests(string $killedEnemyId): void {
|
||||
$player = $this->stateManager->getPlayer();
|
||||
$activeQuests = $player->getActiveQuests();
|
||||
|
||||
foreach ($activeQuests as $questId => $progress) {
|
||||
$questConfig = $this->questData[$questId];
|
||||
|
||||
if ($questConfig['type'] === 'kill' && $questConfig['target']['targetId'] == $killedEnemyId) {
|
||||
// 更新玩家任务进度
|
||||
$player->updateQuestProgress($questId, 1);
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "任务进度更新:[{$questConfig['name']}] {$player->getActiveQuests()[$questId]['currentCount']}/{$questConfig['target']['count']}"
|
||||
]));
|
||||
|
||||
// 如果任务完成,触发 QuestCompletedEventRequest
|
||||
if ($player->getActiveQuests()[$questId]['isCompleted']) {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "任务 [{$questConfig['name']}] <fg=green;options=bold>已完成!</>请回去找NPC提交。"
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. 检查任务是否可以提交并给予奖励
|
||||
*/
|
||||
private function checkQuestCompletion(string $questId): void {
|
||||
$player = $this->stateManager->getPlayer();
|
||||
$progress = $player->getActiveQuests()[$questId];
|
||||
$questConfig = $this->questData[$questId];
|
||||
|
||||
if ($progress['isCompleted']) {
|
||||
// 发放奖励
|
||||
$rewards = $questConfig['rewards'];
|
||||
|
||||
// 经验值奖励 (交给 CharacterService)
|
||||
if (isset($rewards['xp'])) {
|
||||
$this->dispatcher->dispatch(new Event('XpGainedEvent', ['xp' => $rewards['xp']]));
|
||||
}
|
||||
|
||||
// 物品奖励 (交给 LootService)
|
||||
if (isset($rewards['itemId'])) {
|
||||
$this->dispatcher->dispatch(new Event('LootFoundEvent', ['lootId' => $rewards['itemId']]));
|
||||
}
|
||||
|
||||
// 标记玩家任务完成
|
||||
$player->markQuestCompleted($questId);
|
||||
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "🎉 <fg=yellow>任务提交成功!</> 获得了奖励。"
|
||||
]));
|
||||
} else {
|
||||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||||
'message' => "(村长)任务 [{$questConfig['name']}] 还没有完成。目标:{$progress['currentCount']}/{$progress['targetCount']}"
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟:根据 NPC ID 获取对应的任务 ID
|
||||
*/
|
||||
private function getQuestIdForNpc(string $npcId): ?string {
|
||||
return match ($npcId) {
|
||||
'VILLAGER_1' => 'RATS_1',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
67
src/System/StateManager.php
Normal file
67
src/System/StateManager.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Model\Player;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Game\Model\MapTile;
|
||||
|
||||
/**
|
||||
* StateManager: 集中管理和持久化核心游戏状态 (Player, MapTile, etc.)
|
||||
*/
|
||||
class StateManager {
|
||||
|
||||
private Connection $db;
|
||||
private Player $player;
|
||||
private MapTile $currentTile;
|
||||
|
||||
public function __construct(Connection $db) {
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化或加载玩家状态
|
||||
* @param Player|null $player 如果为空,则尝试从数据库加载
|
||||
*/
|
||||
public function setPlayer(Player $player): void {
|
||||
$this->player = $player;
|
||||
}
|
||||
|
||||
public function getPlayer(): Player {
|
||||
// 确保在访问前玩家实例已设置
|
||||
if (!isset($this->player)) {
|
||||
throw new \RuntimeException("Player instance not initialized in StateManager.");
|
||||
}
|
||||
return $this->player;
|
||||
}
|
||||
|
||||
// --- 地图状态管理 ---
|
||||
|
||||
public function setCurrentTile(MapTile $tile): void {
|
||||
$this->currentTile = $tile;
|
||||
}
|
||||
|
||||
public function getCurrentTile(): MapTile {
|
||||
if (!isset($this->currentTile)) {
|
||||
// 假设 MapSystem 会在初始化时设置初始 Tile
|
||||
throw new \RuntimeException("Current MapTile not set in StateManager.");
|
||||
}
|
||||
return $this->currentTile;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 实现存档逻辑 (使用 Doctrine DBAL)
|
||||
*/
|
||||
public function saveGame(): void {
|
||||
// 示例:将玩家对象序列化并存入数据库
|
||||
/*
|
||||
$data = serialize($this->player);
|
||||
$this->db->update('player_save',
|
||||
['data_json' => $data],
|
||||
['player_name' => $this->player->getName()]
|
||||
);
|
||||
*/
|
||||
// $this->eventDispatcher->dispatch(new Event('SystemMessage', ['message' => '游戏已保存。']));
|
||||
}
|
||||
|
||||
// TODO: loadGame() 方法
|
||||
}
|
||||
102
src/System/UIService.php
Normal file
102
src/System/UIService.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
namespace Game\System;
|
||||
|
||||
use Game\Event\Event;
|
||||
use Game\Event\EventListenerInterface;
|
||||
use Game\Model\Player;
|
||||
use Game\Model\MapTile; // 需要引入 MapTile 模型
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* UIService: 负责所有终端输出的监听器。
|
||||
* 确保 UI 与游戏状态一致,遵循单一输出源原则。
|
||||
*/
|
||||
class UIService implements EventListenerInterface {
|
||||
|
||||
private OutputInterface $output;
|
||||
private StateManager $stateManager; // 替换 MapSystem
|
||||
|
||||
/**
|
||||
* 注意:UIService 需要 MapSystem 实例来查询地图数据进行显示
|
||||
*/
|
||||
public function __construct(OutputInterface $output, StateManager $stateManager) {
|
||||
$this->output = $output;
|
||||
$this->stateManager = $stateManager;
|
||||
}
|
||||
|
||||
public function handleEvent(Event $event): void {
|
||||
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']);
|
||||
}
|
||||
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>");
|
||||
}
|
||||
break;
|
||||
|
||||
case 'StartBattleEvent':
|
||||
$this->output->writeln("\n\n<fg=red;options=bold>⚔️ 遭遇战触发!请选择战斗指令...</>");
|
||||
break;
|
||||
|
||||
// TODO: 在后续步骤中添加 BattleEndEvent, DamageDealtEvent 等处理
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印玩家状态信息
|
||||
*/
|
||||
private function displayPlayerStats(Player $player): void {
|
||||
$this->output->writeln("\n--- <info>角色状态:{$player->getName()}</info> ---");
|
||||
$this->output->writeln("等级: <comment>{$player->getLevel()}</comment>");
|
||||
$this->output->writeln("HP: <fg=red>{$player->getHealth()}</>/{$player->getMaxHealth()}");
|
||||
$this->output->writeln("攻击力: <fg=yellow>{$player->getAttack()}</>");
|
||||
$this->output->writeln("防御力: <fg=blue>{$player->getDefense()}</>");
|
||||
$this->output->writeln("经验值: {$player->getCurrentXp()}/{$player->getXpToNextLevel()}");
|
||||
$this->output->writeln("--------------------------\n");
|
||||
}
|
||||
|
||||
private function displayMainMenu(): void {
|
||||
$this->output->writeln("\n--- <fg=white>主菜单</> ---");
|
||||
$this->output->writeln(" [M] 移动 | [E] 探索 | [S] 状态 | [I] 交互 | [Q] 退出"); // ⭐ 增加 I 选项
|
||||
}
|
||||
/**
|
||||
* 打印当前地图区域信息
|
||||
*/
|
||||
private function displayLocation(MapTile $tile): void {
|
||||
// 清屏(可选,增强沉浸感)
|
||||
// $this->output->write("\033[2J\033[H");
|
||||
|
||||
$this->output->writeln("\n======== [ <fg=cyan>{$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");
|
||||
}
|
||||
}
|
||||
BIN
storage/game.sqlite
Normal file
BIN
storage/game.sqlite
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user