This commit is contained in:
hant 2025-12-11 23:01:30 +08:00
parent cd98a5a452
commit 81402717b5
12 changed files with 132 additions and 453 deletions

File diff suppressed because one or more lines are too long

View File

@ -13,8 +13,8 @@ class Actor
public int $maxHp = 100;
public int $patk = 10;
public int $matk = 10;
public int $pdef = 10;
public int $mdef = 10;
public int $pdef = 5;
public int $mdef = 3;
public int $crit = 0;
public float $critdmg = 110.0;
@ -136,11 +136,10 @@ class Actor
public function fullHeal(): void
{
if (property_exists($this, 'maxHp')) {
$this->hp = $this->maxHp;
} elseif (property_exists($this, 'baseHp')) {
$this->hp = $this->baseHp;
}
$status = $this->getStats();
$this->hp = $status['maxHp'];
}
public function recoverMana(int $amount): int

View File

@ -1,6 +1,7 @@
<?php
namespace Game\Entities;
use Game\Modules\Bag\Consume;
use Game\Modules\Bag\Equipment;
use Game\Modules\Bag\Spell;
@ -19,20 +20,16 @@ class Monster extends Actor
// Monster特有的掉落表
public array $dropTable = [];
private bool $is_elite;
private bool $is_boss;
public static function create(int $dungeonId): self
{
// Load data
static $maps = null;
if ($maps === null) {
$maps = require __DIR__ . '/../../src/Data/maps.php';
}
$monsterConfig = self::getMasters($dungeonId);
$monster = new self();
// 1. Get monster list for this dungeon
$monsterConfig = $maps[$dungeonId]['monsters'] ?? [];
if (empty($monsterConfig)) {
$monster->name = '未知怪物';
$monster->expReward = 10;
@ -76,12 +73,8 @@ class Monster extends Actor
public static function createGroup(int $dungeonId,$play_level): array
{
// Load data
static $maps = null;
if ($maps === null) {
$maps = require __DIR__ . '/../../src/Data/maps.php';
}
$monsterConfig = self::getMasters($dungeonId);
$monsterConfig = $maps[$dungeonId]['monsters'] ?? [];
if (empty($monsterConfig)) {
return [self::create($dungeonId)];
}
@ -130,7 +123,8 @@ class Monster extends Actor
$monster = new self();
// Create monster from selected config
$monster->hydrateFromConfig($selectedConfig,$dungeonId);
$status = $monster->getStats();
$monster->hp = $status['maxHp'];
// Add suffix to distinguish multiple monsters of same type
if ($groupSize > 1) {
$monster->name .= " (" . ($i) . ")";
@ -145,6 +139,7 @@ class Monster extends Actor
public function hydrateFromConfig(array $config,$dungeonId): void
{
$this->name = $config['name'];
$this->level = $config['level'] ?? 1;
$this->baseHp = $config['hp'] ?? 20;
$this->hp = $this->baseHp;
@ -161,7 +156,8 @@ class Monster extends Actor
$this->critdmg = $config['critdmg'] ?? 130;
$this->expReward = $config['exp'] ?? 0;
$this->spiritStoneReward = $config['spirit_stones'] ?? 0;
$this->is_elite = $config['is_elite'] ?? false;
$this->is_boss = $config['is_boss'] ?? false;
// 根据等级和基础属性分配天赋点
$this->allocateTalentsByLevel();
static $allItems = null;
@ -169,12 +165,17 @@ class Monster extends Actor
$allItems = json_decode(file_get_contents(__DIR__.'/../Data/items_new.json'),true);
}
$drops = $config['drops'] ?? [];
$index = ($dungeonId - 1) * 5;
$drops_eq = array_slice($allItems,$index,5);
$drops_eq =$allItems[$dungeonId - 1];
$drops = array_merge($drops,$drops_eq);
foreach ($drops as $drop) {
$type = $drop['type'] ?? '';
$rate = $drop['rate'] ?? 20;
if ($this->is_elite){
$rate += 20;
}
if ($this->is_boss){
$rate += 40;
}
if (in_array($type, ['weapon', 'armor', 'boots', 'ring', 'necklace'])) {
if (rand(1, 100) > $rate) continue;
$spec = $drop;
@ -183,6 +184,7 @@ class Monster extends Actor
$this->equip[$type] = $equip;
}
}
$this->dropTable[] = Consume::createItem($dungeonId,$config['level']);
// 为怪物配置法术
$this->generateSpells($config);
@ -246,6 +248,16 @@ class Monster extends Actor
}
}
private static function getMasters(int $dungeonId)
{
$data = file_get_contents(__DIR__.'/../Data/monster.json');
$data = json_decode($data,true);
// dd($data);
$monsters = $data["region_{$dungeonId}_monsters"];
$boss = $data["region_{$dungeonId}_bosses"];
return array_merge($monsters,$boss);
}
/**
* 随机掉落装备物品(从穿着的装备随机掉落)
* @param int $dropRate 掉落概率0-100

View File

@ -10,153 +10,19 @@ use Game\Core\Colors;
class Consume extends Item
{
private static array $typeNames = [
'heal_single' => '单体治疗',
'damage_single' => '单体伤害',
'damage_aoe' => '群体伤害',
'heal_aoe' => '群体治疗',
'support' => '辅助',
];
// 计算方式说明
private static array $calcTypeDescriptions = [
'matk' => '基于魔攻',
'patk' => '基于物攻',
'hp_percent' => '基于最大生命值百分比',
'hybrid' => '基于(魔攻+物攻)混合',
'crit_heal' => '暴击率影响治疗效果',
'crit_damage' => '暴击伤害系数影响伤害',
'crit_aoe' => '暴击率影响范围伤害',
'defense' => '基于防御属性',
'def_pierce' => '防御穿透伤害',
'status_bonus' => '目标状态加成伤害',
'enemy_count_bonus' => '敌人数量加成伤害',
'dispersed_damage' => '伤害分散到所有敌人',
'smart_heal' => '智能治疗(优先低血量)',
'hp_missing' => '基于缺失生命值',
'low_def_bonus' => '对低防御敌人伤害加成',
'matk_scaled' => '随敌人数量加成',
'team_sync' => '基于队伍规模',
];
/**
* 创建法术物品 - 支持新的丰富法术系统
* @param int $spellId 法术ID
* @param int $level 物品等级
* @return array 法术物品数组
*/
public static function createItem(int $spellId, int $level = 1): array
public static function createItem(int $dungeonId, int $level = 1): array
{
static $spellsData = null;
if ($spellsData === null) {
$spellsData = require __DIR__ . '/../../../src/Data/spells.php';
}
// 随机品质
$roll = rand(1, 100);
// $roll = 100;
if ($roll <= 70) $quality = 'common';
elseif ($roll <= 90) $quality = 'rare';
elseif ($roll <= 98) $quality = 'epic';
else $quality = 'legendary';
// 查找法术信息
$spellInfo = null;
foreach ($spellsData as $category => $spells) {
if (is_array($spells) && !in_array($category, ['quality_levels', 'upgrades', 'dungeon_spell_drops', 'quality_drop_rates', 'spells_by_quality'])) {
if (isset($spells[$spellId])) {
$spellInfo = $spells[$spellId];
break;
}
}
}
if (!$spellInfo) {
// 默认法术
return [
'id' => uniqid('spell_'),
'type' => 'spell',
'name' => '未知法术',
'quality' => $quality,
'level' => $level,
'spellId' => $spellId,
'enhanceLevel' => 0,
'calc_type' => 'matk',
'cost' => 20,
'spellType' => 'damage_single',
'desc' => '未知的法术',
];
}
// 品质映射到数组索引 (common=0, rare=1, epic=2, legendary=3)
$qualityIndex = match ($quality) {
'common' => 0,
'rare' => 1,
'epic' => 2,
'legendary' => 3,
default => 0,
};
// 提取品质相关的参数
$healRatio = $spellInfo['heal_ratio'][$qualityIndex] ?? ($spellInfo['heal_ratio'][0] ?? 0);
$damageRatio = $spellInfo['damage_ratio'][$qualityIndex] ?? ($spellInfo['damage_ratio'][0] ?? 1.0);
$healBase = $spellInfo['heal_base'][$qualityIndex] ?? ($spellInfo['heal_base'][0] ?? 0);
$critBonus = $spellInfo['crit_bonus'][$qualityIndex] ?? ($spellInfo['crit_bonus'][0] ?? 0);
$critDmgBonus = $spellInfo['crit_dmg_bonus'][$qualityIndex] ?? ($spellInfo['crit_dmg_bonus'][0] ?? 0);
$enemyCountBonus = $spellInfo['enemy_count_bonus'][$qualityIndex] ?? ($spellInfo['enemy_count_bonus'][0] ?? 0);
$dispersion = $spellInfo['dispersion'][$qualityIndex] ?? ($spellInfo['dispersion'][0] ?? 1.0);
$teamBonus = $spellInfo['team_bonus'][$qualityIndex] ?? ($spellInfo['team_bonus'][0] ?? 0);
$priorityBonus = $spellInfo['priority_bonus'][$qualityIndex] ?? ($spellInfo['priority_bonus'][0] ?? 0);
// 计算基础伤害值(根据法术模板的基础值和成长系数)
$baseValue = 0;
$growth = 0;
if (isset($spellInfo['base']) && isset($spellInfo['growth'])) {
$baseArray = $spellInfo['base'];
$growthArray = $spellInfo['growth'];
// 确保索引在范围内
if (is_array($baseArray) && is_array($growthArray)) {
$baseValue = $baseArray[$qualityIndex] ?? ($baseArray[0] ?? 0);
$growth = $growthArray[$qualityIndex] ?? ($growthArray[0] ?? 0);
// 应用计算公式finalValue = baseValue + (level * growth) + randomBonus
$randomBonus = rand(0, max(1, (int)($baseValue * 3)));
$finalBaseValue = (int)($baseValue + ($level * $growth * 10) + $randomBonus);
} else {
$finalBaseValue = 0;
}
} else {
$finalBaseValue = 0;
}
$data = file_get_contents(__DIR__.'/../../Data/consume.json');
$data = json_decode($data,true);
$name = $data[$dungeonId]['name'];
$heal = $level * 10 * 3;
return [
'id' => uniqid('spell_'),
'type' => 'spell',
'name' => $spellInfo['name'],
'quality' => $quality,
'level' => $level,
'spellId' => $spellId,
'enhanceLevel' => 0,
'calc_type' => $spellInfo['calc_type'] ?? 'matk',
'cost' => $spellInfo['cost'] ?? 20,
'spellType' => $spellInfo['type'] ?? 'damage_single',
'desc' => $spellInfo['desc'] ?? '',
// 品质参数
'heal_ratio' => $healRatio,
'damage_ratio' => $damageRatio,
'heal_base' => $healBase,
'crit_bonus' => $critBonus,
'crit_dmg_bonus' => $critDmgBonus,
'enemy_count_bonus' => $enemyCountBonus,
'dispersion' => $dispersion,
'team_bonus' => $teamBonus,
'priority_bonus' => $priorityBonus,
// 基础伤害值(已计算)
'base' => $finalBaseValue,
'growth' => $growth,
'id' => uniqid('consume_'),
'type' => 'consume',
'rate' => 30,
'heal' => $heal,
'name' => $name,
];
}
@ -171,84 +37,11 @@ class Consume extends Item
public static function getLineShow($item): string
{
$parts = [];
$spell = $item;
// 名称(带品质颜色和强化)
$parts[] = self::formatName($spell);
// 法术类型
$spellType = $spell['spellType'] ?? $spell['type'] ?? 'unknown';
$typeName = self::$typeNames[$spellType];
$parts[] = Colors::GRAY . "[{$typeName}]" . Colors::RESET;
// 计算方式
$calcType = $spell['calc_type'] ?? 'matk';
$calcDesc = self::getCalcTypeDescription($calcType);
$ratio = $spell['damage_ratio'];
$parts[] = Colors::GRAY . "{$calcDesc} x $ratio" . Colors::RESET;
// 基础值
$base = $spell['base'] ?? 0;
$parts[] = Colors::YELLOW . "基础:{$base}" . Colors::RESET;
// 消耗
$cost = $spell['cost'] ?? 0;
$enhanceLevel = $spell['enhanceLevel'] ?? 0;
$actualCost = max(1, $cost - ($enhanceLevel * 2));
if ($enhanceLevel > 0) {
$parts[] = Colors::CYAN . "消耗:{$actualCost}(原:{$cost})" . Colors::RESET;
} else {
$parts[] = Colors::CYAN . "消耗:{$cost}" . Colors::RESET;
}
return implode(" ", $parts);
}
public static function getCalcTypeDescription(string $calcType): string
{
return self::$calcTypeDescriptions[$calcType] ?? $calcType;
}
public static function formatName(array $spell): string
{
$quality = $spell['quality'] ?? 'common';
$color = Colors::getColor($quality);
$name = $spell['name'] ?? '未知法术';
$level = $spell['level'] ?? 1;
$enhanceLevel = $spell['enhanceLevel'] ?? 0;
$enhanceStr = $enhanceLevel > 0 ? Colors::YELLOW . "+{$enhanceLevel}" . Colors::RESET : "";
return $color . $name . Colors::RESET . " Lv.{$level}" . $enhanceStr;
return $item['name'] . '+' .$item['heal'];
}
public static function getDetailShow($item): array
{
return [];
}
protected static function getBaseValue(string $quality, string $type, int $level,bool $isMain = true): float
{
$qualityMultiplier = match ($quality) {
'legendary' => 2.0,
'epic' => 1.5,
'rare' => 1.2,
default => 1.0
};
$base = rand(2*$level, 8*$level);
$multiplier = match ($type) {
'heal_single' => [1.5, 2],
'damage_single' => [1.5, 2],
'damage_aoe' => [1.0, 1.3],
'heal_aoe' => [1.0, 1.3],
default => [1, 1.1]
};
$random = random_int(1, 10);
$multiplier = ($multiplier[1] - $multiplier[0]) * $random / 10 + $multiplier[0];
if ($isMain){
return floor(($base + ($level * $multiplier)) * $qualityMultiplier);
}else{
return floor(($base + ($level * $multiplier)) * $qualityMultiplier / 5 * 3);
}
}
}

View File

@ -2,6 +2,8 @@
namespace Game\Modules\Bag;
use Game\Core\Colors;
/**
* Simple representation of an equipment/consumable item.
*/
@ -11,6 +13,7 @@ abstract class Item
public string $type; // weapon, armor, consume
public string $quality; // common, rare, epic, legendary
public string $desc = '';
public abstract function toArray(): array;
public abstract static function getLineShow($item): string;
@ -21,25 +24,31 @@ abstract class Item
public static function show($item): string
{
if ($item['type'] == 'spell'){
if (!$item) {
return '无';
}
if ($item['type'] == 'potion_pool') {
return Colors::GREEN.'小绿瓶+'.$item['heal'].Colors::RESET;
} else if ($item['type'] == 'spell') {
return Spell::getLineShow($item);
}elseif($item['type'] == 'quest'){
} elseif ($item['type'] == 'quest') {
return Quest::getLineShow($item);
}elseif ($item['type'] == 'consume'){
} elseif ($item['type'] == 'consume') {
return Consume::getLineShow($item);
}else{
} else {
return Equipment::getLineShow($item);
}
}
public static function calcPrice($item): int
{
if ($item['type'] == 'spell'){
if ($item['type'] == 'spell') {
return Spell::calculateSellPrice($item);
}elseif($item['type'] == 'quest'){
} elseif ($item['type'] == 'quest') {
return Quest::calculateSellPrice($item);
}elseif ($item['type'] == 'consume'){
} elseif ($item['type'] == 'consume') {
return Consume::calculateSellPrice($item);
}else{
} else {
return Equipment::calculateSellPrice($item);
}
}

View File

@ -2,18 +2,14 @@
namespace Game\Modules;
use Game\Core\Game;
use Game\Core\Input;
use Game\Core\Screen;
use Game\Core\ItemDisplay;
use Game\Core\SpellDisplay;
use Game\Core\SpellCalculator;
use Game\Core\Colors;
use Game\Core\WebInput;
use Game\Entities\Player;
use Game\Entities\Actor;
use Game\Entities\Monster;
use Game\Entities\Partner;
use Game\Modules\Bag\Equipment;
use Game\Modules\Bag\Item;
class Battle
{
@ -132,7 +128,7 @@ class Battle
}
while ($this->player->hp > 0) {
Screen::delay(500000, $out);
Screen::delay(500000);
// 创建敌人群组
$this->enemies = Monster::createGroup($this->game->dungeonId,$this->player->level);
@ -1061,7 +1057,6 @@ class Battle
foreach ($this->enemies as $enemy) {
$totalExp += $enemy->expReward;
$totalStones += $enemy->spiritStoneReward;
// 掉落 - 从怪物穿着的装备随机掉落50%概率)
foreach ($enemy->getRandomEquipmentDrops(50) as $item) {
$this->player->addItem($item);
@ -1077,7 +1072,7 @@ class Battle
// 掉落 - 从掉落表中随机掉落物品
foreach ($enemy->dropTable as $drop) {
if (rand(1, 100) <= $drop['rate']) {
$item = $drop['item'];
$item = $drop;
// 如果是回复品消耗品且有heal属性直接添加到小绿瓶池
if ($item['type'] === 'consume' && isset($item['heal']) && $item['heal'] > 0) {
$this->player->addPotionPool($item['heal']);
@ -1088,10 +1083,6 @@ class Battle
'heal' => $item['heal'],
'quantity' => 1
];
} else {
// 其他物品正常添加到背包
$this->player->addItem($item);
$allDrops[] = $item;
}
}
}
@ -1139,7 +1130,7 @@ class Battle
if (!empty($allDrops)) {
$out->writeln("{$this->yellow}{$this->reset} {$this->white}掉落:{$this->reset}");
foreach ($allDrops as $item) {
$out->writeln(Equipment::getLineShow($item));
$out->writeln(Item::show($item));
}
}

View File

@ -4,38 +4,12 @@ namespace Game\Modules;
use Game\Core\Screen;
use Game\Core\Input;
use Game\Core\Game;
use Game\Modules\Map\Map;
class DungeonSelectPanel
{
public function __construct(public Game $game) {}
/**
* 检查玩家是否拥有指定物品
*/
private function hasItem(string $itemName): bool
{
foreach ($this->game->player->inventory as $item) {
if (($item['name'] ?? '') === $itemName) {
return true;
}
}
return false;
}
/**
* 检查玩家是否可以进入某个副本
*/
private function canEnterDungeon(array $map): bool
{
// 如果没有key_item要求可以进入
if ($map['key_item'] === null) {
return true;
}
// 检查玩家是否拥有该钥匙
return $this->hasItem($map['key_item']);
}
public function show()
{
Screen::clear($this->game->output);
@ -46,19 +20,11 @@ class DungeonSelectPanel
$out->writeln("╚════════════════════════════════════╝");
$out->writeln("");
$maps = require __DIR__ . '/../../src/Data/maps.php';
$availableMaps = [];
$map = new Map($this->game);
$maps = $map->getMaps();
// 只显示已解锁的地图
foreach ($maps as $id => $map) {
if ($this->canEnterDungeon($map)) {
$availableMaps[$id] = $map;
$out->writeln("[{$id}] {$map['name']} (Lv.{$map['min_level']})");
} else {
// 显示未解锁的地图
$keyItem = $map['key_item'];
$out->writeln("[\033[90m{$id}\033[0m] {$map['name']} (未解锁 - 需要: {$keyItem})");
}
$out->writeln("[{$id}] {$map['name']}");
}
$out->writeln("");
@ -77,17 +43,6 @@ class DungeonSelectPanel
Screen::sleep(1);
return;
}
$selectedMap = $maps[$dungeonId];
// 检查是否有权限进入
if (!$this->canEnterDungeon($selectedMap)) {
$out->writeln("\033[91m该副本尚未解锁\033[0m");
$out->writeln("需要物品: \033[93m{$selectedMap['key_item']}\033[0m");
Screen::sleep(2);
return;
}
// 进入副本
$this->game->dungeonId = (int)$dungeonId;
$this->game->state = Game::BATTLE;

53
src/Modules/Map/Map.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace Game\Modules\Map;
use Game\Core\Game;
class Map
{
private $game;
private $map;
public function __construct(Game $game)
{
$this->game = $game;
$data = file_get_contents(__DIR__.'/../../Data/map.json');
$this->map = json_decode($data,true);
}
public function getMaps()
{
$res = [];
$i = 0;
foreach ($this->map as $item){
if ($this->canEnterDungeon($item)){
$res [++$i] = $item;
}
}
return $res;
}
private function canEnterDungeon(array $map): bool
{
// 如果没有key_item要求可以进入
if ($map['key_item'] === null) {
return true;
}
// 检查玩家是否拥有该钥匙
return $this->hasItem($map['key_item']);
}
private function hasItem(string $itemName): bool
{
foreach ($this->game->player->inventory as $item) {
if (($item['name'] ?? '') === $itemName) {
return true;
}
}
return false;
}
}

View File

@ -216,34 +216,12 @@ class StatsPanel
$out->writeln("{$this->magenta}装备栏{$this->reset}");
$out->writeln("╠════════════════════════════════════╣");
$slotIndex = 1;
$slots = ['weapon', 'armor', 'boots', 'ring', 'necklace'];
$slotNames = [
'weapon' => '武器',
'armor' => '护甲',
'boots' => '鞋子',
'ring' => '戒指',
'necklace' => '项链',
];
foreach ($slots as $slot) {
$item = $actor->equip[$slot] ?? null;
$slotName = $slotNames[$slot];
if ($item) {
$slotLines = ItemDisplay::renderSlot($slotName, $item, "║ [{$slotIndex}] ");
foreach ($slotLines as $i => $line) {
if ($i === 0) {
$out->writeln($line);
} else {
$out->writeln("" . substr($line, 6)); // 缩进对齐
}
}
} else {
$out->writeln("║ [{$slotIndex}] {$this->cyan}{$slotName}{$this->reset}: " .
Colors::GRAY . '(空)' . Colors::RESET);
}
$slotIndex++;
$out->writeln(Item::show($item));
}
$out->writeln("╚════════════════════════════════════╝");
@ -255,23 +233,10 @@ class StatsPanel
$out->writeln("╠════════════════════════════════════╣");
$skillSlots = ['skill1', 'skill2', 'skill3', 'skill4'];
$skillIndex = 1;
foreach ($skillSlots as $slot) {
$spell = $actor->skillSlots[$slot] ?? null;
if ($spell) {
// 法术使用 SpellDisplay 显示
$slotLines = SpellDisplay::renderSlot("技能{$skillIndex}", $spell, "║ [{$skillIndex}] ");
foreach ($slotLines as $i => $line) {
if ($i === 0) {
$out->writeln($line);
} else {
$out->writeln("" . substr($line, 6));
}
}
} else {
$out->writeln("║ [{$skillIndex}] {$this->cyan}技能{$skillIndex}{$this->reset}: " . Colors::GRAY . '(空)' . Colors::RESET);
}
$skillIndex++;
$out->writeln(Item::show($spell));
}
$out->writeln("╚════════════════════════════════════╝");
$out->writeln("");
@ -399,7 +364,7 @@ class StatsPanel
$displayIdx = 1;
$idxMap = [];
foreach ($equipableItems as $realIdx => $item) {
$displayStr = Item::show($equipableItems);
$displayStr = Item::show($item);
$this->game->output->writeln("[{$displayIdx}] {$displayStr}");
$idxMap[$displayIdx] = $realIdx;
$displayIdx++;

View File

@ -2,27 +2,12 @@
use Game\Modules\Bag\Equipment;
use Game\Modules\Bag\Spell;
use Game\Modules\Skill\Enums\DamageType;
use Game\Modules\Skill\Factories\EffectFactory;
require __DIR__ . '/../vendor/autoload.php';
// 效果使用示例
// 创建伤害效果
$damageEffect = EffectFactory::createEffect([
'type' => 'damage',
'id' => 'fire_ball_damage',
'name' => '火球术伤害',
'damage_type' => DamageType::FIRE->value,
'value' => 150, // 150% 攻击力伤害
'is_percentage' => true,
'crit_chance' => 0.1,
'crit_multiplier' => 2.0,
'duration' => 0 // 立即效果
]);
dd($damageEffect);
$res = \Game\Entities\Monster::create(1);
dd($res);