hanli/src/Modules/Battle.php
hant e593d81942 重构:将天赋系统统一移到 Actor 基类
- 将天赋系统 (talents, talentWeights, talentBonus, getTalentStats) 集中在 Actor 基类
- 添加 allocateTalent(), resetTalents(), autoAllocateTalents(), gainExp() 到 Actor
- Monster 保留特有的基础属性、奖励和掉落表
- NPC 保留特有的标识和配置相关属性
- Player 保留特有的名称映射、NPC 标记、同伴系统和升级治疗逻辑
- 删除所有重复代码,提高代码复用性和可维护性

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 21:57:59 +08:00

1001 lines
39 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Game\Modules;
use Game\Core\Game;
use Game\Core\Input;
use Game\Core\Screen;
use Game\Core\ItemDisplay;
use Game\Core\Colors;
use Game\Entities\Player;
use Game\Entities\Actor;
use Game\Entities\Monster;
use Game\Entities\Partner;
class Battle
{
public Player $player;
/** @var Actor[] */
public array $enemies = [];
/** @var array<string, int> 同伴当前HP */
private array $partnerHp = [];
// 法术数据
private array $spellsData = [];
private array $qualityColors = [];
// 颜色定义 (use Colors constants)
private string $red;
private string $green;
private string $yellow;
private string $cyan;
private string $white;
private string $magenta;
private string $bold;
private string $reset;
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';
// Initialize color palette from centralized Colors
$this->red = Colors::RED;
$this->green = Colors::GREEN;
$this->yellow = Colors::YELLOW;
$this->cyan = Colors::CYAN;
$this->white = Colors::WHITE;
$this->magenta = Colors::MAGENTA;
$this->bold = Colors::BOLD;
$this->reset = Colors::RESET;
$this->qualityColors = [
'common' => Colors::WHITE,
'rare' => Colors::BLUE,
'epic' => Colors::MAGENTA,
'legendary' => Colors::YELLOW,
];
}
/**
* 初始化同伴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 Actor[]
*/
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(Actor $enemy): ?array
{
// 尝试从独立的副本法术映射文件获取(每个副本有自己的法术池)
$dungeonId = $this->game->dungeonId;
$spellIds = null;
$dungeonSpellFile = __DIR__ . '/../../src/Data/dungeon_spells.php';
if (file_exists($dungeonSpellFile)) {
$dungeonMap = require $dungeonSpellFile;
$spellIds = $dungeonMap[$dungeonId] ?? null;
}
// 回退到全局配置里的 dungeon_spell_drops
if (empty($spellIds)) {
$dungeonSpellDrops = $this->spellsData['dungeon_spell_drops'] ?? [];
$spellIds = $dungeonSpellDrops[$dungeonId] ?? null;
}
if (empty($spellIds) || !is_array($spellIds)) {
return null;
}
// 从法术池中随机选择一个法术ID
$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->getRandomEquipmentDrops(50) 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, ?Actor $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;
}
}