320 lines
10 KiB
PHP
320 lines
10 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/maps.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);
|
||
$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): 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();
|
||
|
||
// Drops & Equipment
|
||
$drops = $config['drops'] ?? [];
|
||
foreach ($drops as $drop) {
|
||
$type = $drop['type'] ?? '';
|
||
$rate = $drop['rate'] ?? 0;
|
||
|
||
if ($type === 'consume') {
|
||
$spec = $drop;
|
||
unset($spec['rate']);
|
||
$item = Item::createFromSpec($spec, $this->level);
|
||
$this->dropTable[] = [
|
||
'item' => $item,
|
||
'rate' => $rate,
|
||
];
|
||
continue;
|
||
}
|
||
|
||
if ($type === 'spell') {
|
||
// Chance to include this spell tome in drop table
|
||
if (rand(1, 100) > $rate) continue;
|
||
|
||
// spell drop spec should include 'spell_id' (or 'id') to identify the spell
|
||
$spellId = $drop['spell_id'] ?? ($drop['id'] ?? null);
|
||
if ($spellId !== null) {
|
||
static $spellsData = null;
|
||
if ($spellsData === null) {
|
||
$spellsData = require __DIR__ . '/../../src/Data/spells.php';
|
||
}
|
||
|
||
$spellInfo = null;
|
||
foreach ($spellsData as $cat => $list) {
|
||
if (!is_array($list) || in_array($cat, ['quality_levels','upgrades'])) continue;
|
||
if (isset($list[$spellId])) { $spellInfo = $list[$spellId]; break; }
|
||
}
|
||
|
||
// Scale tome level by monster level (e.g., level tiers of 5)
|
||
$tomeLevel = max(1, min(10, (int)ceil($this->level / 5)));
|
||
|
||
$tome = [
|
||
'name' => ($spellInfo['name'] ?? ('法术#' . $spellId)) . '的法术书',
|
||
'type' => 'spell_tome',
|
||
'quality' => $spellInfo['quality'] ?? ($drop['quality'] ?? 'common'),
|
||
'level' => $tomeLevel,
|
||
'spell_id' => $spellId,
|
||
'spell_name' => $spellInfo['name'] ?? null,
|
||
'desc' => $drop['desc'] ?? ('能够学习或提升 ' . ($spellInfo['name'] ?? '未知法术')),
|
||
];
|
||
|
||
// Add to drop table with given rate
|
||
$this->dropTable[] = [
|
||
'item' => $tome,
|
||
'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->applyEquipmentStats();
|
||
}
|
||
|
||
/**
|
||
* 应用装备属性加成到怪物属性
|
||
*/
|
||
// Monster-specific application of equipment is handled by Actor::getStats; applyEquipmentStats remains for legacy callers
|
||
public function applyEquipmentStats(): void
|
||
{
|
||
$this->hp = $this->baseHp;
|
||
$this->patk = $this->basePatk;
|
||
$this->matk = $this->baseMatk;
|
||
$this->pdef = $this->basePdef;
|
||
$this->mdef = $this->baseMdef;
|
||
|
||
foreach ($this->equip as $item) {
|
||
if (empty($item)) continue;
|
||
$this->hp += $item['hp'] ?? 0;
|
||
$this->patk += $item['patk'] ?? $item['atk'] ?? 0;
|
||
$this->matk += $item['matk'] ?? 0;
|
||
$this->pdef += $item['pdef'] ?? $item['def'] ?? 0;
|
||
$this->mdef += $item['mdef'] ?? 0;
|
||
$this->crit += $item['crit'] ?? 0;
|
||
$this->critdmg += $item['critdmg'] ?? 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取怪物装备的物品列表(用于战斗胜利时掉落)
|
||
* @return array
|
||
*/
|
||
public function getEquippedItems(): array
|
||
{
|
||
$items = [];
|
||
foreach ($this->equip as $item) {
|
||
if (!empty($item)) {
|
||
$items[] = $item;
|
||
}
|
||
}
|
||
return $items;
|
||
}
|
||
|
||
/**
|
||
* 随机掉落装备物品(从穿着的装备随机掉落)
|
||
* @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;
|
||
}
|
||
|
||
/**
|
||
* 根据等级和基础属性分配天赋点和权重
|
||
* 怪物根据等级获得天赋点,并按基础属性的占比分配权重
|
||
*/
|
||
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);
|
||
}
|
||
} |