275 lines
9.6 KiB
PHP
275 lines
9.6 KiB
PHP
<?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) {
|
||
switch ($event->getType()) {
|
||
case 'AbilityEffectApplied': // ⭐ 监听技能施放效果
|
||
$target = $event->getPayload()['target'];
|
||
if ($target instanceof Enemy && !$target->isAlive()) {
|
||
$this->handleWin();
|
||
return; // 战斗结束
|
||
}
|
||
// 如果是玩家治疗,则无需返回
|
||
break;
|
||
}
|
||
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 === 'C') { // ⭐ 施法逻辑
|
||
$this->handleAbilityInput();
|
||
} elseif ($playerAction === 'R') {
|
||
if ($this->tryRunAway()) {
|
||
$this->endBattle(false);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!$this->inBattle) break;
|
||
|
||
// 敌人回合
|
||
$this->enemyAttack();
|
||
|
||
if (!$this->inBattle) break;
|
||
}
|
||
}
|
||
|
||
private function handleAbilityInput(): void {
|
||
$player = $this->stateManager->getPlayer();
|
||
$abilities = $player->getAbilities();
|
||
|
||
if (empty($abilities)) {
|
||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '你还没有学会任何技能!']));
|
||
return;
|
||
}
|
||
|
||
$this->output->writeln("\n--- 魔法技能 ---");
|
||
$availableIds = [];
|
||
foreach ($abilities as $id => $ability) {
|
||
$canCast = $player->getMana() >= $ability->manaCost ? "<fg=green>" : "<fg=red>";
|
||
$this->output->writeln(" [{$id}] {$ability->name} | 消耗: {$canCast}{$ability->manaCost} MP</>");
|
||
$availableIds[] = $id;
|
||
}
|
||
$this->output->writeln("----------------");
|
||
|
||
$question = new Question("> 请输入技能 ID (或 X 取消):");
|
||
$choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? '');
|
||
|
||
if ($choice === 'X') return;
|
||
|
||
if (in_array($choice, $availableIds)) {
|
||
// ⭐ 触发事件,将处理权交给 AbilityService
|
||
$this->dispatcher->dispatch(new Event('CastAbilityEvent', [
|
||
'abilityId' => $choice,
|
||
'target' => $this->currentEnemy // 暂定为当前敌人
|
||
]));
|
||
} else {
|
||
$this->dispatcher->dispatch(new Event('SystemMessage', ['message' => '无效的技能 ID。']));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取玩家战斗指令 (直接使用注入的 I/O 接口)
|
||
*/
|
||
private function promptPlayerAction(): string {
|
||
$this->dispatcher->dispatch(new Event('SystemMessage', [
|
||
'message' => "\n--- 你的回合 --- [A] 攻击 | [C] 施法 | [R] 逃跑" // ⭐ 增加 C
|
||
]));
|
||
|
||
$question = new Question("> 请选择指令 (A/C/R):");
|
||
// 关键:使用注入的 I/O 接口
|
||
$choice = strtoupper($this->helper->ask($this->input, $this->output, $question) ?? '');
|
||
|
||
// 简单的输入验证
|
||
if (in_array($choice, ['A', 'C', 'R'])) { // ⭐ 增加 C
|
||
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],
|
||
};
|
||
}
|
||
} |