Оказалось несколько сложнее чем “поменять пару вызовов”.
Класс ChestRewardCalculator использует редкость сундука как главный модификатор, заменяя собой сложность боя из оригинального кода. Вся логика построена на трех принципах: Множитель Редкости, Накопительный Штраф Шанса и Штраф Качества за гарантированные предметы.
1. Множитель Редкости (CHEST_RARITY_MULTIPLIERS)
Это основа всех расчетов. Чем выше редкость сундука (common, epic, legendary), тем выше базовые награды:
| Редкость | Множитель | Влияние на Базовый Шанс/Деньги |
|---|---|---|
common |
1.0x | Базовые значения |
rare |
2.5x | 250% от базы |
legendary |
7.0x | 700% от базы |
Этот множитель применяется к базовому шансу выпадения первого предмета (BASE_ITEM_DROP_CHANCE) и к базовому количеству денег (BASE_MONEY).
2. Расчет Денег (Money)
- База: Расчет начинается с базового количества денег, умноженного на Множитель Редкости и Бонус Игрока (
moneyBonus). - Масштабирование по Уровню: Результат масштабируется в зависимости от Уровня Игрока (чтобы награда не обесценивалась).
- Случайность: Применяется взвешенное случайное распределение, как и в оригинальном коде (60% — мало, 30% — средне, 10% — много).
- Шанс Дропа: Деньги выпадают только с заданным шансом (
MONEY_DROP_CHANCE, по умолчанию 80%).
3. Комплексная Логика Выпадения Предметов (Items)
Метод calculateItems теперь работает в три последовательных этапа, обеспечивая корректное применение штрафов:
1. Подготовка и Инициализация Шанса
Начальный шанс выпадения предмета ($currentDropChance) определяется как базовая величина (BASE_ITEM_DROP_CHANCE - 70%), умноженная на Множитель Редкости Сундука. Чем реже сундук, тем выше шанс выпадения первого предмета.
2. Применение Штрафа за Гарантированные Предметы
Итерация проходит по всем гарантированным предметам:
- Накопительный Штраф Шанса: За каждый гарантированный предмет (
$item) шанс$currentDropChanceснижается наITEM_DROP_CHANCE_STEP(20%). Это означает, что гарантированные предметы расходуют шанс, делая выпадение следующих случайных предметов менее вероятным. - Штраф Качества: Количество гарантированных предметов определенной редкости (
$guaranteedQualities) используется вdetermineItemQualityдля понижения шансов этой редкости для всех последующих случайных роллов.
3. Ролл Случайных Предметов
После того как шанс скорректирован гарантированными предметами, начинается основной цикл:
- Происходит проверка сниженного шанса (
$currentDropChance). - Если шанс выпадает, определяется качество предмета (с учетом штрафа качества), и шанс снова снижается для следующего предмета.
- Если шанс не выпадает, цикл прекращается.
Таким образом, гарантированные предметы тратят шанс выпадения и влияют на качество всех последующих случайных предметов.
/**
* Класс для расчета и выдачи наград из сундуков
*/
class ChestRewardCalculator
{
// Константы по умолчанию
private const DEFAULT_MAX_MONEY = 5000;
// Базовые значения наград
private const BASE_MONEY = 20;
private const BASE_ITEM_DROP_CHANCE = 0.7; // 70% базовый шанс выпадения предмета
// Модификаторы шанса выпадения
private const ITEM_DROP_CHANCE_STEP = 0.20; // -20% от текущего шанса для следующего предмета (НАКОПИТЕЛЬНЫЙ ШТРАФ)
// Степень уменьшения шанса качества, если оно было гарантировано (например, -50% за каждый гарантированный)
private const GUARANTEED_QUALITY_PENALTY = 0.5; // -50%
// Множители редкости сундука (для денег и базового шанса выпадения)
private const CHEST_RARITY_MULTIPLIERS = [
'common' => 1.0, // Обычный
'uncommon' => 1.5, // Необычный
'rare' => 2.5, // Редкий
'epic' => 4.0, // Эпический
'legendary' => 7.0 // Легендарный
];
// Шансы на деньги
private const MONEY_DROP_CHANCE = 0.8; // 80%
// Шансы качества предметов (в промилле для точности)
private const ITEM_QUALITIES = [
'common' => 700, // 70.0%
'uncommon' => 200, // 20.0%
'rare' => 80, // 8.0%
'epic' => 15, // 1.5%
'legendary' => 5 // 0.5%
];
private $maxMoney;
public function __construct(
int $maxMoney = self::DEFAULT_MAX_MONEY
) {
$this->maxMoney = $maxMoney;
}
/**
* Рассчитывает и выдает награды из сундука
*/
public function calculateRewards(array $chestData): array
{
$rarity = strtolower($chestData['rarity'] ?? 'common');
$playerLevel = $chestData['playerLevel'] ?? 1;
$guaranteedItems = $chestData['guaranteedItems'] ?? [];
$playerBonuses = $chestData['playerBonuses'] ?? [];
$rarityMultiplier = self::CHEST_RARITY_MULTIPLIERS[$rarity] ?? self::CHEST_RARITY_MULTIPLIERS['common'];
$rewards = [
'money' => 0,
'items' => []
];
if ($this->rollChance(self::MONEY_DROP_CHANCE)) {
$rewards['money'] = $this->calculateMoney(
$rarityMultiplier,
$playerLevel,
$playerBonuses['moneyBonus'] ?? 1.0
);
}
// Расчет всех предметов (случайных и гарантированных)
$rewards['items'] = $this->calculateItems(
$rarityMultiplier,
$playerLevel,
$playerBonuses['itemQualityBonus'] ?? [],
$guaranteedItems
);
$this->giveRewardsToPlayer($rewards);
return $rewards;
}
/**
* Расчет денег
*/
private function calculateMoney(float $rarityMultiplier, int $playerLevel, float $moneyBonus): int
{
$baseMoney = self::BASE_MONEY * $rarityMultiplier * $moneyBonus * ($playerLevel / 10 + 1);
$roll = mt_rand(1, 100);
if ($roll <= 60) {
$money = $baseMoney * mt_rand(50, 80) / 100;
} elseif ($roll <= 90) {
$money = $baseMoney * mt_rand(80, 120) / 100;
} else {
$money = $baseMoney * mt_rand(120, 200) / 100;
}
return min(round($money), $this->maxMoney);
}
/**
* Рассчитывает все выпавшие предметы (гарантированные + случайные)
*/
private function calculateItems(
float $rarityMultiplier,
int $playerLevel,
array $qualityBonuses,
array $guaranteedItems
): array {
$items = $guaranteedItems;
$currentDropChance = self::BASE_ITEM_DROP_CHANCE * $rarityMultiplier;
// 1. Подсчет гарантированных для ШТРАФА КАЧЕСТВА
$guaranteedQualities = array_count_values(array_column($guaranteedItems, 'quality'));
// 2. !!! ПРИМЕНЯЕМ ШТРАФ ШАНСА ЗА КАЖДЫЙ ГАРАНТИРОВАННЫЙ ПРЕДМЕТ !!!
foreach ($guaranteedItems as $item) {
// Гарантированный предмет выпал, поэтому он тратит шанс для следующего
$currentDropChance *= (1 - self::ITEM_DROP_CHANCE_STEP);
}
// 3. Продолжаем цикл для СЛУЧАЙНЫХ предметов
$maxItems = 10;
while ($currentDropChance >= 0.01) {
// Проверяем шанс выпадения для следующего СЛУЧАЙНОГО предмета
if ($this->rollChance($currentDropChance)) {
// Шанс выпал: определяем качество с учетом ШТРАФА за гарантированные
$quality = $this->determineItemQuality($qualityBonuses, $guaranteedQualities);
$items[] = [
'quality' => $quality,
'id' => $this->generateRandomItemId($quality, $playerLevel)
];
// Накладываем накопительный штраф на шанс выпадения для СЛЕДУЮЩЕГО предмета
$currentDropChance *= (1 - self::ITEM_DROP_CHANCE_STEP);
} else {
// Шанс не выпал, прекращаем цикл
break;
}
if (count($items) >= $maxItems) break;
}
return $items;
}
/**
* Определяет качество выпавшего предмета, корректируя шансы на основе гарантированных
*/
private function determineItemQuality(array $bonuses, array $guaranteedQualities): string
{
$qualities = self::ITEM_QUALITIES;
// 1. Применяем бонусы игрока
foreach (['legendary', 'epic', 'rare', 'uncommon'] as $q) {
if (isset($bonuses[$q])) {
$qualities[$q] = round($qualities[$q] * $bonuses[$q]);
}
}
// 2. Применяем ШТРАФ за гарантированные предметы (Quality Penalty)
foreach ($guaranteedQualities as $quality => $count) {
if ($count > 0 && in_array($quality, ['uncommon', 'rare', 'epic', 'legendary'])) {
$penalty = self::GUARANTEED_QUALITY_PENALTY ** $count;
$qualities[$quality] = round($qualities[$quality] * $penalty);
}
}
// 3. Пересчитываем шанс обычного качества
$totalSpecial = 0;
foreach (['uncommon', 'rare', 'epic', 'legendary'] as $q) {
$totalSpecial += $qualities[$q];
}
$qualities['common'] = max(0, 1000 - $totalSpecial);
// 4. Выбираем качество
$roll = mt_rand(1, 1000);
$cumulative = 0;
foreach ($qualities as $quality => $chance) {
$cumulative += $chance;
if ($roll <= $cumulative) {
return $quality;
}
}
return 'common';
}
// ... (Методы generateRandomItemId, rollChance, giveRewardsToPlayer остаются без изменений)
private function generateRandomItemId(string $quality, int $playerLevel): int
{
$baseRanges = [
'common' => 1000,
'uncommon' => 2000,
'rare' => 3000,
'epic' => 4000,
'legendary' => 5000,
];
$baseId = $baseRanges[$quality] ?? 1000;
$minId = $baseId + ($playerLevel - 1) * 10;
$maxId = $baseId + $playerLevel * 10;
return mt_rand($minId, $maxId);
}
private function rollChance(float $chance): bool
{
return mt_rand(1, 10000) <= ($chance * 10000);
}
private function giveRewardsToPlayer(array $rewards): void
{
if ($rewards['money'] > 0) {
// Player::giveMoney($rewards['money']);
}
foreach ($rewards['items'] as $item) {
// Player::giveItem($item['id'], $item['quality']);
}
}
}
Пример работы:
// Пример использования
$calculator = new ChestRewardCalculator();
// Открытие сундука с 1 гарантированным легендарным предметом
$rewards = $calculator->calculateRewards([
'rarity' => 'epic',
'playerLevel' => 35,
'guaranteedItems' => [
['quality' => 'legendary', 'id' => 5000],
],
'playerBonuses' => [
'moneyBonus' => 1.5,
'itemQualityBonus' => []
]
]);
echo "<pre>";
print_r($rewards);
echo "</pre>";
Array
(
[money] => 0
[items] => Array
(
[0] => Array
(
[quality] => legendary
[id] => 5000
)
[1] => Array
(
[quality] => common
[id] => 1344
)
[2] => Array
(
[quality] => common
[id] => 1344
)
[3] => Array
(
[quality] => common
[id] => 1349
)
[4] => Array
(
[quality] => common
[id] => 1343
)
[5] => Array
(
[quality] => rare
[id] => 3349
)
[6] => Array
(
[quality] => uncommon
[id] => 2340
)
)
)