hanli/src/Entities/Monster.php
2025-12-04 18:11:28 +08:00

320 lines
10 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\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);
}
}