343 lines
11 KiB
PHP
343 lines
11 KiB
PHP
<?php
|
||
namespace Game\Entities;
|
||
|
||
class Monster extends Actor
|
||
{
|
||
// Monster特有的基础属性(不含装备加成)
|
||
public int $baseHp = 20;
|
||
public int $basePatk = 4;
|
||
public int $baseMatk = 2;
|
||
public int $basePdef = 0;
|
||
public int $baseMdef = 0;
|
||
|
||
// Monster特有的奖励属性
|
||
public int $expReward = 10;
|
||
public int $spiritStoneReward = 0;
|
||
|
||
// Monster特有的掉落表
|
||
public array $dropTable = [];
|
||
|
||
public static function create(int $dungeonId): self
|
||
{
|
||
// Load data
|
||
static $maps = null;
|
||
if ($maps === null) {
|
||
$maps = require __DIR__ . '/../../src/Data/map_new.php';
|
||
}
|
||
|
||
$monster = new self();
|
||
|
||
// 1. Get monster list for this dungeon
|
||
$monsterConfig = $maps[$dungeonId]['monsters'] ?? [];
|
||
|
||
if (empty($monsterConfig)) {
|
||
$monster->name = '未知怪物';
|
||
$monster->expReward = 10;
|
||
return $monster;
|
||
}
|
||
|
||
// 2. Pick a monster using weighted random from map config
|
||
$totalWeight = 0;
|
||
foreach ($monsterConfig as $m) {
|
||
$totalWeight += $m['weight'] ?? 100;
|
||
}
|
||
|
||
$rand = rand(1, $totalWeight);
|
||
$selectedMonster = null;
|
||
|
||
foreach ($monsterConfig as $m) {
|
||
$rand -= $m['weight'] ?? 100;
|
||
if ($rand <= 0) {
|
||
$selectedMonster = $m;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Fallback
|
||
if (!$selectedMonster) {
|
||
$selectedMonster = $monsterConfig[0];
|
||
}
|
||
|
||
// 3. Hydrate monster base stats from maps.php
|
||
$monster->hydrateFromConfig($selectedMonster,$dungeonId);
|
||
$status = $monster->getStats();
|
||
$monster->hp = $status['maxHp'];
|
||
return $monster;
|
||
}
|
||
|
||
/**
|
||
* Create a group of monsters with random, diverse enemies (1-5 monsters)
|
||
* Each monster is independently selected from the dungeon's monster pool using weighted random selection
|
||
* @return Actor[]
|
||
*/
|
||
public static function createGroup(int $dungeonId): array
|
||
{
|
||
// Load data
|
||
static $maps = null;
|
||
if ($maps === null) {
|
||
$maps = require __DIR__ . '/../../src/Data/maps.php';
|
||
}
|
||
|
||
$monsterConfig = $maps[$dungeonId]['monsters'] ?? [];
|
||
if (empty($monsterConfig)) {
|
||
return [self::create($dungeonId)];
|
||
}
|
||
|
||
// Determine group size (1-5 enemies)
|
||
$groupSize = rand(1, 5);
|
||
$group = [];
|
||
|
||
// Create each enemy independently using weighted random selection
|
||
for ($i = 0; $i < $groupSize; $i++) {
|
||
$totalWeight = 0;
|
||
foreach ($monsterConfig as $m) {
|
||
$totalWeight += $m['weight'] ?? 100;
|
||
}
|
||
|
||
$rand = rand(1, $totalWeight);
|
||
$selectedConfig = null;
|
||
|
||
foreach ($monsterConfig as $m) {
|
||
$rand -= $m['weight'] ?? 100;
|
||
if ($rand <= 0) {
|
||
$selectedConfig = $m;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$selectedConfig) {
|
||
$selectedConfig = $monsterConfig[0];
|
||
}
|
||
|
||
// Create monster from selected config
|
||
$monster = self::create($dungeonId);
|
||
|
||
// Add suffix to distinguish multiple monsters of same type
|
||
if ($groupSize > 1) {
|
||
$monster->name .= " (" . ($i + 1) . ")";
|
||
}
|
||
|
||
$group[] = $monster;
|
||
}
|
||
|
||
return $group;
|
||
}
|
||
|
||
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;
|
||
$this->maxHp = $this->baseHp;
|
||
$this->basePatk = $config['patk'] ?? $config['atk'] ?? 4;
|
||
$this->patk = $this->basePatk;
|
||
$this->baseMatk = $config['matk'] ?? 2;
|
||
$this->matk = $this->baseMatk;
|
||
$this->basePdef = $config['pdef'] ?? $config['def'] ?? 0;
|
||
$this->pdef = $this->basePdef;
|
||
$this->baseMdef = $config['mdef'] ?? 0;
|
||
$this->mdef = $this->baseMdef;
|
||
$this->crit = $config['crit'] ?? 5;
|
||
$this->critdmg = $config['critdmg'] ?? 130;
|
||
$this->expReward = $config['exp'] ?? 0;
|
||
$this->spiritStoneReward = $config['spirit_stones'] ?? 0;
|
||
|
||
// 根据等级和基础属性分配天赋点
|
||
$this->allocateTalentsByLevel();
|
||
static $allItems = null;
|
||
if ($allItems === null) {
|
||
$allItems = require __DIR__ . '/../../src/Data/item_new.php';
|
||
}
|
||
// Drops & Equipment & Spells
|
||
$drops = $config['drops'] ?? [];
|
||
$index = ($dungeonId - 1) * 5;
|
||
$drops_eq = array_slice($allItems,$index,5);
|
||
$drops = array_merge($drops,$drops_eq);
|
||
foreach ($drops as $drop) {
|
||
$type = $drop['type'] ?? '';
|
||
$rate = $drop['rate'] ?? 20;
|
||
|
||
// 处理任务物品
|
||
if ($type === 'quest_item') {
|
||
$item = Item::createQuestItem($drop['name'] ?? '未知道具');
|
||
$this->dropTable[] = [
|
||
'item' => $item,
|
||
'rate' => $rate,
|
||
];
|
||
continue;
|
||
}
|
||
|
||
if ($type === 'consume') {
|
||
$spec = $drop;
|
||
unset($spec['rate']);
|
||
$item = Item::createConsume($spec, $this->level);
|
||
$this->dropTable[] = [
|
||
'item' => $item,
|
||
'rate' => $rate,
|
||
];
|
||
continue;
|
||
}
|
||
if (in_array($type, ['weapon', 'armor', 'boots', 'ring', 'necklace'])) {
|
||
if (rand(1, 100) > $rate) continue;
|
||
$spec = $drop;
|
||
unset($spec['rate']);
|
||
$item = Item::createFromSpecWithConfig($spec, $this->level);
|
||
$this->equip[$type] = $item;
|
||
}
|
||
}
|
||
|
||
// 为怪物配置法术
|
||
$this->generateSpells($config);
|
||
|
||
}
|
||
|
||
/**
|
||
* 为怪物随机生成法术(穿着法术,类似装备)
|
||
* 配置示例 1: 'spells' => [1, 3, 10] - 随机从这些法术ID中选择
|
||
* 配置示例 2: 'spells' => [
|
||
* ['id' => 1, 'name' => '治愈术'],
|
||
* ['id' => 10, 'name' => '火焰术'],
|
||
* ['id' => 30, 'name' => '防御法术'],
|
||
* ] - 为怪物分配具体的法术(可选指定名称)
|
||
*/
|
||
private function generateSpells(array $config): void
|
||
{
|
||
$spellConfigs = $config['spells'] ?? [];
|
||
|
||
if (empty($spellConfigs)) {
|
||
return;
|
||
}
|
||
|
||
static $spellsData = null;
|
||
if ($spellsData === null) {
|
||
$spellsData = require __DIR__ . '/../../src/Data/spells.php';
|
||
}
|
||
|
||
// 处理两种配置格式
|
||
$selectedSpells = [];
|
||
|
||
// 格式2: 配置数组 [['id' => 1, 'name' => '治愈术', 'rate' => 30]]
|
||
foreach ($spellConfigs as $spellConfig) {
|
||
if (is_array($spellConfig) && isset($spellConfig['id'])) {
|
||
$selectedSpells[] = [
|
||
'id' => $spellConfig['id'],
|
||
'name' => $spellConfig['name'] ?? null,
|
||
'rate' => $spellConfig['rate'] ?? 30, // 默认30%掉落概率
|
||
];
|
||
}
|
||
}
|
||
|
||
foreach ($selectedSpells as $spellConfig) {
|
||
$spellId = $spellConfig['id'];
|
||
$customName = $spellConfig['name'] ?? null;
|
||
$dropRate = $spellConfig['rate'] ?? 30;
|
||
if (rand(1, 100) > $dropRate) continue;
|
||
|
||
// 随机决定法术品质:70% 普通, 20% 稀有, 8% 史诗, 2% 传奇
|
||
$roll = rand(1, 100);
|
||
if ($roll <= 70) $quality = 'common';
|
||
elseif ($roll <= 90) $quality = 'rare';
|
||
elseif ($roll <= 98) $quality = 'epic';
|
||
else $quality = 'legendary';
|
||
|
||
// 创建法术物品
|
||
$spell = Item::createSpell($spellId, $quality, $this->level);
|
||
|
||
// 如果提供了自定义名称,则覆盖默认名称
|
||
if ($customName) {
|
||
$spell['name'] = $customName;
|
||
}
|
||
foreach ($this->skillSlots as $slot => $content) {
|
||
if ($content === null) {
|
||
$this->skillSlots[$slot] = $spell;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 随机掉落装备物品(从穿着的装备随机掉落)
|
||
* @param int $dropRate 掉落概率(0-100)
|
||
* @return array 掉落的物品列表
|
||
*/
|
||
public function getRandomEquipmentDrops(int $dropRate = 50): array
|
||
{
|
||
$drops = [];
|
||
foreach ($this->equip as $item) {
|
||
if (!empty($item)) {
|
||
// 每件装备有独立的掉落概率
|
||
if (rand(1, 100) <= $dropRate) {
|
||
$drops[] = $item;
|
||
}
|
||
}
|
||
}
|
||
return $drops;
|
||
}
|
||
|
||
/**
|
||
* 随机掉落法术(从穿着的法术随机掉落)
|
||
* 使用每个法术配置的掉落概率,如果未指定则使用默认值
|
||
* @param int $defaultRate 默认掉落概率(0-100),当法术未指定rate时使用
|
||
* @return array 掉落的法术列表
|
||
*/
|
||
public function getRandomSpellDrops(int $dropRate = 50): array
|
||
{
|
||
$drops = [];
|
||
foreach ($this->skillSlots as $spell) {
|
||
if (!empty($spell)) {
|
||
// 每个法术有独立的掉落概率
|
||
if (rand(1, 100) <= $dropRate) {
|
||
$drops[] = $spell;
|
||
}
|
||
}
|
||
}
|
||
return $drops;
|
||
}
|
||
|
||
/**
|
||
* 根据等级和基础属性分配天赋点和权重
|
||
* 怪物根据等级获得天赋点,并按基础属性的占比分配权重
|
||
*/
|
||
private function allocateTalentsByLevel(): void
|
||
{
|
||
// 每级获得 3 点天赋点(与玩家一致)
|
||
$talentPoints = ($this->level - 1) * 3;
|
||
|
||
if ($talentPoints <= 0) {
|
||
return;
|
||
}
|
||
|
||
// 计算基础属性的权重比(考虑每点天赋的效果)
|
||
// talentBonus 定义了每点天赋对应的属性增益
|
||
$talentBonusMap = [
|
||
'hp' => 10,
|
||
'patk' => 5,
|
||
'matk' => 4,
|
||
'pdef' => 3,
|
||
'mdef' => 3,
|
||
'crit' => 1,
|
||
'critdmg' => 5,
|
||
];
|
||
|
||
// 计算每个属性的权重(基础属性值 / 天赋加成)
|
||
$weights = [
|
||
'hp' => max(1, (int)($this->baseHp / $talentBonusMap['hp'])),
|
||
'patk' => max(1, (int)($this->basePatk / $talentBonusMap['patk'])),
|
||
'matk' => max(1, (int)($this->baseMatk / $talentBonusMap['matk'])),
|
||
'pdef' => max(1, (int)($this->basePdef / $talentBonusMap['pdef'])),
|
||
'mdef' => max(1, (int)($this->baseMdef / $talentBonusMap['mdef'])),
|
||
'crit' => max(1, (int)($this->crit / $talentBonusMap['crit'])),
|
||
'critdmg' => 0,
|
||
];
|
||
|
||
// dd($weights);
|
||
// 设置权重
|
||
$this->talentWeights = $weights;
|
||
$this->talentPoints = $talentPoints;
|
||
|
||
// 自动按权重分配天赋点
|
||
$this->autoAllocateTalents($talentPoints);
|
||
}
|
||
} |