984 lines
38 KiB
PHP
984 lines
38 KiB
PHP
<?php
|
||
namespace Game\Modules;
|
||
|
||
use Game\Core\Game;
|
||
use Game\Core\Input;
|
||
use Game\Core\Screen;
|
||
use Game\Core\ItemDisplay;
|
||
use Game\Entities\Player;
|
||
use Game\Entities\Monster;
|
||
use Game\Entities\Partner;
|
||
|
||
class Battle
|
||
{
|
||
public Player $player;
|
||
/** @var Monster[] */
|
||
public array $enemies = [];
|
||
|
||
/** @var array<string, int> 同伴当前HP */
|
||
private array $partnerHp = [];
|
||
|
||
// 法术数据
|
||
private array $spellsData = [];
|
||
|
||
private array $qualityColors = [
|
||
'common' => "\033[37m", // 白色
|
||
'rare' => "\033[34m", // 蓝色
|
||
'epic' => "\033[35m", // 紫色
|
||
'legendary' => "\033[33m", // 黄色
|
||
];
|
||
|
||
// 颜色定义
|
||
private string $red = "\033[31m";
|
||
private string $green = "\033[32m";
|
||
private string $yellow = "\033[33m";
|
||
private string $cyan = "\033[36m";
|
||
private string $white = "\033[37m";
|
||
private string $magenta = "\033[35m";
|
||
private string $bold = "\033[1m";
|
||
private string $reset = "\033[0m";
|
||
|
||
private int $round = 0;
|
||
private int $totalMaxHp = 0;
|
||
|
||
public function __construct(public Game $game)
|
||
{
|
||
$this->player = $game->player;
|
||
$this->spellsData = require __DIR__ . '/../../src/Data/spells.php';
|
||
}
|
||
|
||
/**
|
||
* 初始化同伴HP
|
||
*/
|
||
private function initPartnerHp(): void
|
||
{
|
||
$this->partnerHp = [];
|
||
foreach ($this->player->partners as $partner) {
|
||
// 从Partner对象的hp属性读取,允许队友在战斗外也能恢复
|
||
$this->partnerHp[$partner->id] = $partner->hp;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取存活的同伴
|
||
*/
|
||
private function getAlivePartners(): array
|
||
{
|
||
$alive = [];
|
||
foreach ($this->player->partners as $partner) {
|
||
if (($this->partnerHp[$partner->id] ?? 0) > 0) {
|
||
$alive[] = $partner;
|
||
}
|
||
}
|
||
return $alive;
|
||
}
|
||
|
||
/**
|
||
* 将战斗中的队友HP同步回Partner对象
|
||
*/
|
||
private function syncPartnerHp(): void
|
||
{
|
||
foreach ($this->player->partners as $partner) {
|
||
$partner->hp = $this->partnerHp[$partner->id] ?? 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取存活的敌人
|
||
* @return Monster[]
|
||
*/
|
||
private function getAliveEnemies(): array
|
||
{
|
||
$alive = [];
|
||
foreach ($this->enemies as $enemy) {
|
||
if ($enemy->hp > 0) {
|
||
$alive[] = $enemy;
|
||
}
|
||
}
|
||
return $alive;
|
||
}
|
||
|
||
public function start()
|
||
{
|
||
$out = $this->game->output;
|
||
|
||
// 初始化同伴HP
|
||
$this->initPartnerHp();
|
||
|
||
while ($this->player->hp > 0) {
|
||
Screen::delay(500000);
|
||
|
||
// 创建敌人群组
|
||
$this->enemies = Monster::createGroup($this->game->dungeonId);
|
||
|
||
$this->totalMaxHp = 0;
|
||
foreach ($this->enemies as $enemy) {
|
||
$this->totalMaxHp += $enemy->hp;
|
||
}
|
||
|
||
$this->round = 0;
|
||
|
||
// 显示遭遇界面
|
||
$this->showEncounter($out);
|
||
|
||
$playerFirst = $this->determineFirstStrike();
|
||
|
||
// 战斗循环
|
||
while (true) {
|
||
if ($this->checkExit($out)) {
|
||
$this->syncPartnerHp();
|
||
return;
|
||
}
|
||
|
||
$this->round++;
|
||
$this->renderBattleScreen($out, $playerFirst);
|
||
Screen::delay(800000); // 回合开始停顿
|
||
|
||
if ($playerFirst) {
|
||
// 玩家攻击
|
||
$result = $this->playerAttack($out);
|
||
Screen::delay(800000);
|
||
if ($result) break;
|
||
|
||
// 同伴攻击
|
||
$result = $this->partnersAttack($out);
|
||
Screen::delay(800000);
|
||
if ($result) break;
|
||
|
||
if ($this->checkExit($out)) {
|
||
$this->syncPartnerHp();
|
||
return;
|
||
}
|
||
|
||
// 怪物攻击
|
||
if ($this->enemiesAttack($out)) {
|
||
Screen::delay(1000000);
|
||
$this->syncPartnerHp();
|
||
return;
|
||
}
|
||
Screen::delay(800000);
|
||
} else {
|
||
// 怪物先攻
|
||
if ($this->enemiesAttack($out)) {
|
||
Screen::delay(1000000);
|
||
$this->syncPartnerHp();
|
||
return;
|
||
}
|
||
Screen::delay(800000);
|
||
|
||
if ($this->checkExit($out)) {
|
||
$this->syncPartnerHp();
|
||
return;
|
||
}
|
||
|
||
// 玩家攻击
|
||
$result = $this->playerAttack($out);
|
||
Screen::delay(800000);
|
||
if ($result) break;
|
||
|
||
// 同伴攻击
|
||
$result = $this->partnersAttack($out);
|
||
Screen::delay(800000);
|
||
if ($result) break;
|
||
}
|
||
|
||
Screen::delay(500000);
|
||
// 同步队友HP到Partner对象,然后保存状态
|
||
$this->syncPartnerHp();
|
||
$this->game->saveState();
|
||
}
|
||
}
|
||
}
|
||
|
||
private function showEncounter($out)
|
||
{
|
||
Screen::clear($out);
|
||
$out->writeln("");
|
||
$out->writeln("{$this->yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{$this->reset}");
|
||
$out->writeln("");
|
||
$out->writeln(" {$this->red}⚔️ 遭遇敌人!{$this->reset}");
|
||
$out->writeln("");
|
||
|
||
foreach ($this->enemies as $enemy) {
|
||
$out->writeln(" {$this->bold}{$this->white}{$enemy->name}{$this->reset} {$this->cyan}Lv.{$enemy->level}{$this->reset}");
|
||
}
|
||
|
||
$out->writeln("");
|
||
$out->writeln("{$this->yellow}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{$this->reset}");
|
||
Screen::delay(1000000); // 1秒
|
||
}
|
||
|
||
private function renderBattleScreen($out, bool $playerFirst)
|
||
{
|
||
Screen::clear($out);
|
||
$stats = $this->player->getStats();
|
||
|
||
$out->writeln("{$this->cyan}╔══════════════════════════════════════════╗{$this->reset}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->bold}第 {$this->round} 回合{$this->reset} {$this->white}[q] 逃跑{$this->reset} {$this->cyan}║{$this->reset}");
|
||
$out->writeln("{$this->cyan}╠══════════════════════════════════════════╣{$this->reset}");
|
||
|
||
// 敌人信息
|
||
foreach ($this->enemies as $enemy) {
|
||
if ($enemy->hp <= 0) {
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀{$this->reset} {$this->white}{$enemy->name}{$this->reset} {$this->red}[已击败]{$this->reset}");
|
||
continue;
|
||
}
|
||
$enemyHpPercent = max(0, $enemy->hp) / $enemy->baseHp; // 使用baseHp作为最大值近似,或者应该在hydrate时保存maxHp
|
||
// 实际上Monster没有maxHp属性,hp初始值就是最大值。但在战斗中hp会减少。
|
||
// 我们需要知道最大HP。Monster::create时hp=baseHp+equipHp。
|
||
// 简单起见,假设当前hp <= 初始hp。如果需要精确显示条,应该在Monster类加maxHp。
|
||
// 这里暂时用 $enemy->baseHp + equipHp 估算,或者直接存一个 maxHp。
|
||
// 为了简单,我们假设满血是初始状态。
|
||
// 更好的做法是Monster类加一个maxHp属性。
|
||
// 暂时用 $enemy->hp / $enemy->hp (如果满血) ... 不行。
|
||
// 让我们修改Monster类加maxHp? 或者这里不显示条,只显示数值?
|
||
// 或者我们假定 create 出来的 hp 就是 maxHp。
|
||
// $enemy->maxHp = $enemy->hp; // 在create里做最好。
|
||
// 这里先只显示数值吧,或者大概估算。
|
||
|
||
$hpText = max(0, $enemy->hp);
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}👹{$this->reset} {$this->bold}{$enemy->name}{$this->reset} Lv.{$enemy->level} HP: {$this->red}{$hpText}{$this->reset}");
|
||
}
|
||
|
||
$out->writeln("{$this->cyan}║{$this->reset}");
|
||
|
||
// VS 分隔
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}⚔️ VS ⚔️{$this->reset}");
|
||
$out->writeln("{$this->cyan}║{$this->reset}");
|
||
|
||
// 玩家信息
|
||
$playerHpPercent = $this->player->hp / $stats['maxHp'];
|
||
$playerHpBar = $this->renderHpBar($playerHpPercent, 20);
|
||
$playerHpText = $this->player->hp . "/" . $stats['maxHp'];
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}🧙{$this->reset} {$this->bold}玩家{$this->reset} Lv.{$this->player->level}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$playerHpBar} {$this->white}{$playerHpText}{$this->reset}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}⚔️{$this->reset} {$stats['patk']}/{$stats['matk']} {$this->green}🛡️{$this->reset} {$stats['pdef']}/{$stats['mdef']} {$this->red}💥{$this->reset} {$stats['crit']}%");
|
||
|
||
// 显示同伴信息
|
||
foreach ($this->player->partners as $partner) {
|
||
$partnerStats = $partner->getStats();
|
||
$partnerHp = $this->partnerHp[$partner->id] ?? 0;
|
||
$partnerMaxHp = $partnerStats['maxHp'];
|
||
$partnerHpPercent = $partnerMaxHp > 0 ? $partnerHp / $partnerMaxHp : 0;
|
||
$partnerHpBar = $this->renderHpBar($partnerHpPercent, 15);
|
||
$partnerHpText = $partnerHp . "/" . $partnerMaxHp;
|
||
|
||
$status = $partnerHp > 0 ? "" : " {$this->red}[倒下]{$this->reset}";
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}👤{$this->reset} {$partner->name} Lv.{$partner->level}{$status}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$partnerHpBar} {$this->white}{$partnerHpText}{$this->reset}");
|
||
}
|
||
|
||
$out->writeln("{$this->cyan}╠══════════════════════════════════════════╣{$this->reset}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->white}战斗日志:{$this->reset}");
|
||
}
|
||
|
||
private function renderHpBar(float $percent, int $width): string
|
||
{
|
||
$filled = (int)($percent * $width);
|
||
$empty = max($width - $filled,0);
|
||
|
||
// 根据血量百分比选择颜色
|
||
if ($percent > 0.6) {
|
||
$color = $this->green;
|
||
} elseif ($percent > 0.3) {
|
||
$color = $this->yellow;
|
||
} else {
|
||
$color = $this->red;
|
||
}
|
||
|
||
$bar = $color . str_repeat("█", $filled) . $this->white . str_repeat("░", $empty) . $this->reset;
|
||
return "[" . $bar . "]";
|
||
}
|
||
|
||
private function determineFirstStrike(): bool
|
||
{
|
||
// Use the leader's level for comparison
|
||
$leader = $this->enemies[count($this->enemies) - 1]; // Assume leader is last or first?
|
||
// In createGroup we put minions first, so leader is last.
|
||
// Let's just use the highest level enemy.
|
||
$maxLevel = 0;
|
||
foreach ($this->enemies as $e) {
|
||
if ($e->level > $maxLevel) $maxLevel = $e->level;
|
||
}
|
||
|
||
$levelDiff = $this->player->level - $maxLevel;
|
||
$playerChance = 50;
|
||
$levelBonus = max(-30, min(30, $levelDiff * 5));
|
||
$playerChance += $levelBonus;
|
||
$roll = rand(1, 100);
|
||
return $roll <= $playerChance;
|
||
}
|
||
|
||
/**
|
||
* 玩家选择行动类型
|
||
*/
|
||
private function playerChooseAction(): string
|
||
{
|
||
// 简化:默认使用普通攻击,除非有法术且魔法值充足
|
||
// 为了简化战斗流程,我们先自动选择攻击
|
||
// 在实战中可以添加交互菜单
|
||
|
||
// 检查是否有可以施放的法术
|
||
if (empty($this->player->spells) || $this->player->mana < 15) {
|
||
return 'attack';
|
||
}
|
||
|
||
// 暂时选择法术的概率(可根据需要调整)
|
||
// 如果玩家有法术且魔法值充足,50% 概率选择法术
|
||
$spellChance = rand(1, 100);
|
||
if ($spellChance <= 40) { // 40% 概率使用法术
|
||
return 'spell';
|
||
}
|
||
|
||
return 'attack';
|
||
}
|
||
|
||
/**
|
||
* 玩家施放法术
|
||
*/
|
||
private function playerCastSpell($out): bool
|
||
{
|
||
$stats = $this->player->getStats();
|
||
|
||
// 随机选择一个已学的法术
|
||
$availableSpells = [];
|
||
foreach ($this->player->spells as $spellId => $spellData) {
|
||
$spellInfo = $this->getSpellInfo($spellId);
|
||
if (!$spellInfo) continue;
|
||
|
||
$cost = $spellInfo['cost'] ?? 0;
|
||
$upgrades = $this->spellsData['upgrades'] ?? [];
|
||
$level = $spellData['level'] ?? 1;
|
||
$upgradeInfo = $upgrades[$level] ?? [];
|
||
$costReduction = $upgradeInfo['cost_reduction'] ?? 0;
|
||
$actualCost = max(1, $cost - $costReduction);
|
||
|
||
if ($this->player->mana >= $actualCost) {
|
||
$availableSpells[$spellId] = ['info' => $spellInfo, 'cost' => $actualCost, 'level' => $level];
|
||
}
|
||
}
|
||
|
||
if (empty($availableSpells)) {
|
||
// 如果没有可用的法术,改用普通攻击
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->yellow}✦ 魔法值不足,改为普通攻击{$this->reset}");
|
||
// 递归调用普通攻击
|
||
return $this->executePhysicalAttack($out);
|
||
}
|
||
|
||
// 随机选择一个可用的法术
|
||
$selectedSpellId = array_rand($availableSpells);
|
||
$spellData = $availableSpells[$selectedSpellId];
|
||
$spellInfo = $spellData['info'];
|
||
$actualCost = $spellData['cost'];
|
||
$spellLevel = $spellData['level'];
|
||
|
||
// 消耗魔法值
|
||
$this->player->spendMana($actualCost);
|
||
|
||
// 获取法术升级信息
|
||
$upgrades = $this->spellsData['upgrades'] ?? [];
|
||
$upgradeInfo = $upgrades[$spellLevel] ?? [];
|
||
$damageBonus = $upgradeInfo['damage_bonus'] ?? 0;
|
||
|
||
$type = $spellInfo['type'] ?? '';
|
||
$name = $spellInfo['name'] ?? '未知法术';
|
||
|
||
if ($type === 'damage_single') {
|
||
return $this->castDamageSingleSpell($out, $selectedSpellId, $spellInfo, $stats, $damageBonus, $name);
|
||
} elseif ($type === 'damage_aoe') {
|
||
return $this->castDamageAoeSpell($out, $selectedSpellId, $spellInfo, $stats, $damageBonus, $name);
|
||
} elseif ($type === 'support') {
|
||
return $this->castSupportSpell($out, $spellInfo, $stats, $name);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 施放单体伤害法术
|
||
*/
|
||
private function castDamageSingleSpell($out, int $spellId, array $spellInfo, array $stats, int $damageBonus, string $name): bool
|
||
{
|
||
// 选择目标
|
||
$target = null;
|
||
foreach ($this->enemies as $enemy) {
|
||
if ($enemy->hp > 0) {
|
||
$target = $enemy;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$target) return true;
|
||
|
||
// 计算法术伤害
|
||
$baseDamageMultiplier = $spellInfo['damage'] ?? 1.0;
|
||
$actualDamageMultiplier = $baseDamageMultiplier * (1 + $damageBonus / 100);
|
||
$baseDamage = (int)($stats['matk'] * $actualDamageMultiplier);
|
||
|
||
// 计算抵抗
|
||
$resistance = $target->mdef;
|
||
$damage = max(5, $baseDamage - $resistance);
|
||
|
||
// 暴击机制同样适用
|
||
$critRate = $stats['crit'];
|
||
$isCrit = rand(1, 100) <= $critRate;
|
||
|
||
if ($isCrit) {
|
||
$critDmg = $stats['critdmg'];
|
||
$damage = (int)($damage * ($critDmg / 100));
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}✦{$this->reset} 你施放 {$name}... {$this->red}{$this->bold}暴击!{$this->reset}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}✨ 造成 {$damage} 点魔法伤害!{$this->reset}");
|
||
} else {
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}✦{$this->reset} 你施放 {$name}...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}✨ 造成 {$damage} 点魔法伤害{$this->reset}");
|
||
}
|
||
|
||
$target->hp -= $damage;
|
||
|
||
if ($target->hp <= 0) {
|
||
$target->hp = 0;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$target->name} 被击败了!{$this->reset}");
|
||
|
||
if (empty($this->getAliveEnemies())) {
|
||
Screen::delay(500000);
|
||
$this->showVictory($out, $stats);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 施放AOE伤害法术
|
||
*/
|
||
private function castDamageAoeSpell($out, int $spellId, array $spellInfo, array $stats, int $damageBonus, string $name): bool
|
||
{
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}✦{$this->reset} 你施放 {$name}...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}✨ 魔法在整个战场爆炸!{$this->reset}");
|
||
|
||
// 计算法术伤害
|
||
$baseDamageMultiplier = $spellInfo['damage'] ?? 0.8;
|
||
$actualDamageMultiplier = $baseDamageMultiplier * (1 + $damageBonus / 100);
|
||
|
||
$allEnemiesDefeated = true;
|
||
foreach ($this->enemies as $enemy) {
|
||
if ($enemy->hp <= 0) continue;
|
||
|
||
$baseDamage = (int)($stats['matk'] * $actualDamageMultiplier);
|
||
$resistance = $enemy->mdef;
|
||
$damage = max(5, $baseDamage - $resistance);
|
||
|
||
// AOE 法术也可以暴击
|
||
$critRate = $stats['crit'];
|
||
$isCrit = rand(1, 100) <= ($critRate / 2); // AOE 暴击率减半
|
||
if ($isCrit) {
|
||
$critDmg = $stats['critdmg'];
|
||
$damage = (int)($damage * ($critDmg / 100));
|
||
}
|
||
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$enemy->name} 受到 {$damage} 点伤害");
|
||
$enemy->hp -= $damage;
|
||
|
||
if ($enemy->hp <= 0) {
|
||
$enemy->hp = 0;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$enemy->name} 被击败了!{$this->reset}");
|
||
} else {
|
||
$allEnemiesDefeated = false;
|
||
}
|
||
}
|
||
|
||
if ($allEnemiesDefeated && empty($this->getAliveEnemies())) {
|
||
Screen::delay(500000);
|
||
$this->showVictory($out, $stats);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 施放辅助法术
|
||
*/
|
||
private function castSupportSpell($out, array $spellInfo, array $stats, string $name): bool
|
||
{
|
||
$subtype = $spellInfo['subtype'] ?? '';
|
||
|
||
if ($subtype === 'heal' || $subtype === 'heal_all') {
|
||
if ($subtype === 'heal') {
|
||
// 恢复自己
|
||
$heal = $spellInfo['heal'] ?? 0.5;
|
||
$healBase = $spellInfo['heal_base'] ?? 20;
|
||
$healAmount = (int)($stats['matk'] * $heal + $healBase);
|
||
|
||
$actualHeal = $this->player->heal($healAmount);
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}✦{$this->reset} 你施放 {$name}...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}💚 恢复了 {$actualHeal} 点生命值{$this->reset}");
|
||
} else {
|
||
// 恢复全体
|
||
$heal = $spellInfo['heal'] ?? 0.6;
|
||
$healBase = $spellInfo['heal_base'] ?? 40;
|
||
$healAmount = (int)($stats['matk'] * $heal + $healBase);
|
||
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}✦{$this->reset} 你施放 {$name}...");
|
||
$actualHeal = $this->player->heal($healAmount);
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}💚 你恢复了 {$actualHeal} 点生命值{$this->reset}");
|
||
|
||
// 同伴也恢复
|
||
$alivePartners = $this->getAlivePartners();
|
||
foreach ($alivePartners as $partner) {
|
||
$partnerHeal = (int)($healAmount * 0.8); // 同伴恢复量为玩家的80%
|
||
$actualPartnerHeal = $partner->heal($partnerHeal);
|
||
$this->partnerHp[$partner->id] = $partner->hp;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}💚 {$partner->name} 恢复了 {$actualPartnerHeal} 点生命值{$this->reset}");
|
||
}
|
||
}
|
||
} elseif ($subtype === 'defend') {
|
||
$defenseBoost = $spellInfo['defense_boost'] ?? 30;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->cyan}✦{$this->reset} 你施放 {$name}...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->cyan}🛡️ 防御力提升!{$this->reset}");
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 执行普通物理攻击
|
||
*/
|
||
private function executePhysicalAttack($out): bool
|
||
{
|
||
$stats = $this->player->getStats();
|
||
|
||
$target = null;
|
||
foreach ($this->enemies as $enemy) {
|
||
if ($enemy->hp > 0) {
|
||
$target = $enemy;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$target) return true;
|
||
|
||
$physicalDamage = max(1, $stats['patk'] - $target->pdef);
|
||
$magicDamage = max(0, $stats['matk'] - $target->mdef);
|
||
$baseDamage = $physicalDamage + $magicDamage;
|
||
|
||
$critRate = $stats['crit'];
|
||
$critDmg = $stats['critdmg'];
|
||
|
||
$isCrit = rand(1, 100) <= $critRate;
|
||
|
||
if ($isCrit) {
|
||
$damage = (int)($baseDamage * ($critDmg / 100));
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}➤{$this->reset} 你攻击 {$target->name}... {$this->red}{$this->bold}暴击!{$this->reset}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💥 造成 {$damage} 点伤害!{$this->reset}");
|
||
} else {
|
||
$damage = $baseDamage;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}➤{$this->reset} 你攻击 {$target->name}...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->white}⚔️ 造成 {$damage} 点伤害{$this->reset}");
|
||
}
|
||
|
||
$target->hp -= $damage;
|
||
|
||
if ($target->hp <= 0) {
|
||
$target->hp = 0;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$target->name} 被击败了!{$this->reset}");
|
||
|
||
if (empty($this->getAliveEnemies())) {
|
||
Screen::delay(500000);
|
||
$this->showVictory($out, $stats);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 获取法术信息
|
||
*/
|
||
private function getSpellInfo(int $spellId): ?array
|
||
{
|
||
foreach ($this->spellsData as $category => $spells) {
|
||
if (is_array($spells) && $category !== 'quality_levels' && $category !== 'upgrades') {
|
||
if (isset($spells[$spellId])) {
|
||
return $spells[$spellId];
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 生成法术资源书掉落
|
||
*/
|
||
private function generateSpellTomeDrop(Monster $enemy): ?array
|
||
{
|
||
// 获取当前地牢的法术掉落池
|
||
$dungeonId = $this->game->dungeonId;
|
||
$dungeonSpellDrops = $this->spellsData['dungeon_spell_drops'] ?? [];
|
||
|
||
if (!isset($dungeonSpellDrops[$dungeonId])) {
|
||
return null; // 该地牢没有配置法术掉落
|
||
}
|
||
|
||
$spellIds = $dungeonSpellDrops[$dungeonId];
|
||
|
||
if (empty($spellIds)) {
|
||
return null;
|
||
}
|
||
|
||
// 从该地牢的掉落池中随机选择一个法术
|
||
$spellId = $spellIds[array_rand($spellIds)];
|
||
$spellInfo = $this->getSpellInfo($spellId);
|
||
|
||
if (!$spellInfo) {
|
||
return null;
|
||
}
|
||
|
||
// 创建法术资源书物品
|
||
$tome = [
|
||
'name' => $spellInfo['name'] . '的法术书',
|
||
'type' => 'spell_tome',
|
||
'quality' => $spellInfo['quality'] ?? 'common',
|
||
'level' => $enemy->level,
|
||
'spell_id' => $spellId,
|
||
'spell_name' => $spellInfo['name'],
|
||
'desc' => "能够学习或升级 {$spellInfo['name']} 的法术资源书",
|
||
];
|
||
|
||
return $tome;
|
||
}
|
||
|
||
private function playerAttack($out): bool
|
||
{
|
||
$stats = $this->player->getStats();
|
||
|
||
// 显示玩家选择菜单
|
||
$choice = $this->playerChooseAction();
|
||
|
||
if ($choice === 'spell') {
|
||
return $this->playerCastSpell($out);
|
||
}
|
||
|
||
// 普通攻击逻辑
|
||
// Target first alive enemy
|
||
$target = null;
|
||
foreach ($this->enemies as $enemy) {
|
||
if ($enemy->hp > 0) {
|
||
$target = $enemy;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$target) return true; // All dead
|
||
|
||
// 计算物理伤害和魔法伤害
|
||
$physicalDamage = max(1, $stats['patk'] - $target->pdef);
|
||
$magicDamage = max(0, $stats['matk'] - $target->mdef);
|
||
$baseDamage = $physicalDamage + $magicDamage;
|
||
|
||
$critRate = $stats['crit'];
|
||
$critDmg = $stats['critdmg'];
|
||
|
||
$isCrit = rand(1, 100) <= $critRate;
|
||
|
||
if ($isCrit) {
|
||
$damage = (int)($baseDamage * ($critDmg / 100));
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}➤{$this->reset} 你攻击 {$target->name}... {$this->red}{$this->bold}暴击!{$this->reset}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💥 造成 {$damage} 点伤害!{$this->reset}");
|
||
} else {
|
||
$damage = $baseDamage;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->green}➤{$this->reset} 你攻击 {$target->name}...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->white}⚔️ 造成 {$damage} 点伤害{$this->reset}");
|
||
}
|
||
|
||
$target->hp -= $damage;
|
||
|
||
if ($target->hp <= 0) {
|
||
$target->hp = 0;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$target->name} 被击败了!{$this->reset}");
|
||
|
||
// Check if all enemies are dead
|
||
if (empty($this->getAliveEnemies())) {
|
||
Screen::delay(500000);
|
||
$this->showVictory($out, $stats);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 同伴攻击
|
||
*/
|
||
private function partnersAttack($out): bool
|
||
{
|
||
$alivePartners = $this->getAlivePartners();
|
||
|
||
foreach ($alivePartners as $partner) {
|
||
// Target first alive enemy
|
||
$target = null;
|
||
foreach ($this->enemies as $enemy) {
|
||
if ($enemy->hp > 0) {
|
||
$target = $enemy;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$target) return true; // All dead
|
||
|
||
$stats = $partner->getStats();
|
||
|
||
// 计算物理伤害和魔法伤害
|
||
$physicalDamage = max(1, $stats['patk'] - $target->pdef);
|
||
$magicDamage = max(0, $stats['matk'] - $target->mdef);
|
||
$baseDamage = $physicalDamage + $magicDamage;
|
||
|
||
$critRate = $stats['crit'];
|
||
$critDmg = $stats['critdmg'];
|
||
|
||
$isCrit = rand(1, 100) <= $critRate;
|
||
|
||
if ($isCrit) {
|
||
$damage = (int)($baseDamage * ($critDmg / 100));
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}➤{$this->reset} {$partner->name} 攻击 {$target->name}... {$this->red}{$this->bold}暴击!{$this->reset}");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💥 造成 {$damage} 点伤害!{$this->reset}");
|
||
} else {
|
||
$damage = $baseDamage;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->magenta}➤{$this->reset} {$partner->name} 攻击 {$target->name}...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->white}⚔️ 造成 {$damage} 点伤害{$this->reset}");
|
||
}
|
||
|
||
$target->hp -= $damage;
|
||
|
||
if ($target->hp <= 0) {
|
||
$target->hp = 0;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$target->name} 被击败了!{$this->reset}");
|
||
|
||
if (empty($this->getAliveEnemies())) {
|
||
Screen::delay(500000);
|
||
$this->showVictory($out, $this->player->getStats());
|
||
return true;
|
||
}
|
||
}
|
||
|
||
Screen::delay(400000); // 每个同伴攻击间隔
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private function enemiesAttack($out): bool
|
||
{
|
||
$aliveEnemies = $this->getAliveEnemies();
|
||
|
||
foreach ($aliveEnemies as $enemy) {
|
||
// 选择攻击目标:玩家或存活的同伴
|
||
$alivePartners = $this->getAlivePartners();
|
||
$targets = ['player'];
|
||
foreach ($alivePartners as $partner) {
|
||
$targets[] = $partner->id;
|
||
}
|
||
|
||
$targetIdx = array_rand($targets);
|
||
$target = $targets[$targetIdx];
|
||
|
||
if ($target === 'player') {
|
||
// 攻击玩家
|
||
$playerStats = $this->player->getStats();
|
||
$physicalDamage = max(1, $enemy->patk - $playerStats['pdef']);
|
||
$magicDamage = max(0, $enemy->matk - $playerStats['mdef']);
|
||
$damage = $physicalDamage + $magicDamage;
|
||
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}➤{$this->reset} {$enemy->name} 向你发起攻击...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💢 你受到 {$damage} 点伤害{$this->reset}");
|
||
|
||
$this->player->hp -= $damage;
|
||
|
||
if ($this->player->hp <= 0) {
|
||
$this->player->hp = 0;
|
||
Screen::delay(500000);
|
||
$this->showDefeat($out, $enemy);
|
||
return true;
|
||
}
|
||
} else {
|
||
// 攻击同伴
|
||
$partner = $this->player->partners[$target];
|
||
$partnerStats = $partner->getStats();
|
||
$physicalDamage = max(1, $enemy->patk - $partnerStats['pdef']);
|
||
$magicDamage = max(0, $enemy->matk - $partnerStats['mdef']);
|
||
$damage = $physicalDamage + $magicDamage;
|
||
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}➤{$this->reset} {$enemy->name} 向 {$partner->name} 发起攻击...");
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💢 {$partner->name} 受到 {$damage} 点伤害{$this->reset}");
|
||
|
||
$this->partnerHp[$target] -= $damage;
|
||
|
||
if ($this->partnerHp[$target] <= 0) {
|
||
$this->partnerHp[$target] = 0;
|
||
$out->writeln("{$this->cyan}║{$this->reset} {$this->red}💀 {$partner->name} 倒下了!{$this->reset}");
|
||
}
|
||
}
|
||
Screen::delay(400000);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private function showVictory($out, $stats)
|
||
{
|
||
Screen::clear($out);
|
||
$out->writeln("");
|
||
$out->writeln("{$this->yellow}╔══════════════════════════════════════════╗{$this->reset}");
|
||
$out->writeln("{$this->yellow}║{$this->reset} {$this->yellow}║{$this->reset}");
|
||
$out->writeln("{$this->yellow}║{$this->reset} {$this->green}{$this->bold}🎉 胜 利 ! 🎉{$this->reset} {$this->yellow}║{$this->reset}");
|
||
$out->writeln("{$this->yellow}║{$this->reset} {$this->yellow}║{$this->reset}");
|
||
$out->writeln("{$this->yellow}╠══════════════════════════════════════════╣{$this->reset}");
|
||
|
||
$enemyNames = [];
|
||
foreach ($this->enemies as $e) $enemyNames[] = $e->name;
|
||
$out->writeln("{$this->yellow}║{$this->reset} 击败: {$this->white}" . implode(', ', array_unique($enemyNames)) . "{$this->reset}");
|
||
$out->writeln("{$this->yellow}║{$this->reset} 血量: {$this->green}{$this->player->hp}{$this->reset}/{$stats['maxHp']}");
|
||
|
||
// 汇总经验和灵石
|
||
$totalExp = 0;
|
||
$totalStones = 0;
|
||
$allDrops = [];
|
||
|
||
foreach ($this->enemies as $enemy) {
|
||
$totalExp += $enemy->expReward;
|
||
$totalStones += $enemy->spiritStoneReward;
|
||
|
||
// 掉落
|
||
foreach ($enemy->getEquippedItems() as $item) {
|
||
$this->player->addItem($item);
|
||
$allDrops[] = $item;
|
||
}
|
||
foreach ($enemy->dropTable as $drop) {
|
||
if (rand(1, 100) <= $drop['rate']) {
|
||
$this->player->addItem($drop['item']);
|
||
$allDrops[] = $drop['item'];
|
||
}
|
||
}
|
||
|
||
// 掉落法术资源书
|
||
$spellTomeDropChance = 15; // 15% 概率掉落法术资源书
|
||
if (rand(1, 100) <= $spellTomeDropChance && !empty($this->spellsData)) {
|
||
$spellTome = $this->generateSpellTomeDrop($enemy);
|
||
if ($spellTome) {
|
||
// 添加到玩家的法术资源书库存
|
||
$spellId = $spellTome['spell_id'] ?? 0;
|
||
if ($spellId > 0) {
|
||
$this->player->addSpellBook($spellId, 1);
|
||
$allDrops[] = $spellTome;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 经验
|
||
$levelUpMsg = "";
|
||
if ($this->player->gainExp($totalExp)) {
|
||
$levelUpMsg = " {$this->yellow}🎊 升级! Lv.{$this->player->level}{$this->reset}";
|
||
}
|
||
$out->writeln("{$this->yellow}║{$this->reset} 经验: {$this->cyan}+{$totalExp}{$this->reset}{$levelUpMsg}");
|
||
|
||
// 同伴经验
|
||
$alivePartners = $this->getAlivePartners();
|
||
if (!empty($alivePartners)) {
|
||
$partnerExp = (int)($totalExp * 0.8);
|
||
foreach ($alivePartners as $partner) {
|
||
$partnerLevelUp = "";
|
||
if ($partner->gainExp($partnerExp)) {
|
||
$partnerLevelUp = " {$this->yellow}🎊 升级! Lv.{$partner->level}{$this->reset}";
|
||
}
|
||
$out->writeln("{$this->yellow}║{$this->reset} {$this->magenta}{$partner->name}{$this->reset}: {$this->cyan}+{$partnerExp}{$this->reset}{$partnerLevelUp}");
|
||
}
|
||
}
|
||
|
||
// 灵石
|
||
if ($totalStones > 0) {
|
||
$this->player->addSpiritStones($totalStones);
|
||
$out->writeln("{$this->yellow}║{$this->reset} 灵石: {$this->yellow}+{$totalStones}{$this->reset}");
|
||
}
|
||
|
||
// 恢复魔法值
|
||
$manaRecover = (int)($this->player->maxMana * 0.3); // 恢复30%的最大魔法值
|
||
$actualManaRecover = $this->player->recoverMana($manaRecover);
|
||
$out->writeln("{$this->yellow}║{$this->reset} 魔法: {$this->magenta}+{$actualManaRecover}{$this->reset}");
|
||
|
||
// 恢复队友魔法值
|
||
$alivePartners = $this->getAlivePartners();
|
||
foreach ($alivePartners as $partner) {
|
||
$partnerManaRecover = (int)($partner->maxMana * 0.3);
|
||
$actualPartnerManaRecover = $partner->recoverMana($partnerManaRecover);
|
||
$this->partnerHp[$partner->id] = $partner->hp; // 同步HP
|
||
}
|
||
|
||
if (!empty($allDrops)) {
|
||
$out->writeln("{$this->yellow}║{$this->reset} {$this->white}掉落:{$this->reset}");
|
||
foreach ($allDrops as $item) {
|
||
$out->writeln("{$this->yellow}║{$this->reset} " . ItemDisplay::renderDrop($item, ""));
|
||
}
|
||
}
|
||
|
||
$out->writeln("{$this->yellow}╚══════════════════════════════════════════╝{$this->reset}");
|
||
$out->writeln("");
|
||
|
||
$this->game->saveState();
|
||
Screen::delay(1500000);
|
||
}
|
||
|
||
private function showDefeat($out, ?Monster $killer = null)
|
||
{
|
||
Screen::clear($out);
|
||
$killerName = $killer ? $killer->name : "敌人";
|
||
|
||
$out->writeln("");
|
||
$out->writeln("{$this->red}╔══════════════════════════════════════════╗{$this->reset}");
|
||
$out->writeln("{$this->red}║{$this->reset} {$this->red}║{$this->reset}");
|
||
$out->writeln("{$this->red}║{$this->reset} {$this->red}{$this->bold}💀 战 败 ! 💀{$this->reset} {$this->red}║{$this->reset}");
|
||
$out->writeln("{$this->red}║{$this->reset} {$this->red}║{$this->reset}");
|
||
$out->writeln("{$this->red}╠══════════════════════════════════════════╣{$this->reset}");
|
||
$out->writeln("{$this->red}║{$this->reset} 你被 {$this->white}{$killerName}{$this->reset} 击败了...");
|
||
$out->writeln("{$this->red}║{$this->reset}");
|
||
$out->writeln("{$this->red}║{$this->reset} {$this->white}不要气馁,休整后再战!{$this->reset}");
|
||
$out->writeln("{$this->red}╚══════════════════════════════════════════╝{$this->reset}");
|
||
$out->writeln("");
|
||
|
||
$this->game->state = Game::MENU;
|
||
Screen::pause($out);
|
||
}
|
||
|
||
private function checkExit($out): bool
|
||
{
|
||
// Web 模式下跳过实时输入检测
|
||
$webInput = \Game\Core\WebInput::getInstance();
|
||
if ($webInput->isWebMode()) {
|
||
return false;
|
||
}
|
||
|
||
stream_set_blocking(\STDIN, 0);
|
||
$input = '';
|
||
while (($char = fgetc(\STDIN)) !== false) {
|
||
$input .= $char;
|
||
}
|
||
stream_set_blocking(\STDIN, 1);
|
||
|
||
if (str_contains($input, 'q') || str_contains($input, 'Q')) {
|
||
$this->game->saveState();
|
||
Screen::clear($out);
|
||
$out->writeln("");
|
||
$out->writeln("{$this->yellow}🏃 你逃离了战斗...{$this->reset}");
|
||
$out->writeln("");
|
||
Screen::delay(800000);
|
||
$this->game->state = Game::MENU;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|