Выдача наград за поединки

Идейный наследник ItemRoulette, который умер вместе со старым форумом.

Минимальная версия языка: php7. Под 5 перетачивайте синтаксис языка, а то я уже не помню нюансов. Константы я брал из потолка исходя из здравого смысла и ролевых игр.

Логика расчета наград:

1. Модификатор сложности битвы:

  • Базовый множитель = 1.0
  • За каждый уровень противника выше игрока: +20%
  • За каждый уровень противника ниже игрока: -30%
  • За каждого дополнительного противника (если уровень >= игрока): +10%
  • Если игрок в команде: делим на корень из количества союзников

2. Расчет опыта:

  • Базовый опыт * модификатор сложности * бонус игрока
  • Ограничен константой maxExp

3. Расчет денег:

  • Базовый шанс получения: 70%
  • Сумма: база * модификатор сложности * бонус игрока
  • Распределение: 60% малая сумма, 30% средняя, 10% большая

4. Выпадение предметов:

  • Начальный шанс: 50% * модификатор сложности
  • После каждого предмета: шанс * (1 - itemDropChanceStep)
  • Качество определяется взвешенной случайностью с учетом бонусов

Основные компоненты:

1. Модификатор сложности:

  • Учитывает разницу уровней (бонус за сильных, штраф за слабых противников)
  • Учитывает количество врагов (только если они не слабее игрока)
  • Делит награду на команду через квадратный корень (2 игрока = /1.41, 4 игрока = /2)

2. Система опыта:

  • Всегда выдается
  • База * модификатор сложности * бонус игрока
  • Добавлена случайность ±20% для разнообразия

3. Система денег:

  • 70% шанс получить деньги
  • Взвешенное распределение: чаще мало, реже много
  • Учитывает модификаторы и бонусы

4. Система предметов:

  • Начальный шанс 50% * модификатор сложности
  • После каждого предмета шанс падает на 20% (настраивается)
  • Качество: common (88.9%), uncommon (10%), rare (1%), legendary (0.1%)
  • Уникальные предметы только с уникальных боссов

5. Гибкость:

  • Все константы вынесены и настраиваются
  • Поддержка бонусов игрока на все типы наград
  • Легко расширяется новыми параметрами

Награды выдаются в методе giveRewardsToPlayer. Туда надо подставить свои функции выдачи денег и предметов. Сами ID выдаваемых предметов либо подсовывать в $ranges в методе generateRandomItemId, либо уже самостоятельно выкусывать из базы.


/**
 * Класс для расчета и выдачи наград игрокам
 */
class RewardCalculator
{
    // Константы по умолчанию
    private const DEFAULT_MAX_EXP = 10000;
    private const DEFAULT_MAX_MONEY = 10000;
    private const DEFAULT_ITEM_DROP_CHANCE_STEP = 0.20; // 20%

    // Базовые значения наград
    private const BASE_EXP = 100;
    private const BASE_MONEY = 50;
    private const BASE_ITEM_DROP_CHANCE = 0.5; // 50%

    // Модификаторы сложности
    private const LEVEL_BONUS_MULTIPLIER = 0.2; // +20% за каждый уровень выше
    private const LEVEL_PENALTY_MULTIPLIER = 0.3; // -30% за каждый уровень ниже
    private const ENEMY_COUNT_BONUS = 0.1; // +10% за каждого дополнительного противника

    // Шансы на деньги
    private const MONEY_DROP_CHANCE = 0.7; // 70%

    // Качество предметов и их шансы (в промилле для точности)
    private const ITEM_QUALITIES = [
        'common' => 889,      // 88.9%
        'uncommon' => 100,    // 10% (1:10)
        'rare' => 10,         // 1% (1:100)
        'legendary' => 1,     // 0.1% (1:1000)
        'unique' => 0         // Только для уникальных противников
    ];

    private $maxExp;
    private $maxMoney;
    private $itemDropChanceStep;

    public function __construct(
        int $maxExp = self::DEFAULT_MAX_EXP,
        int $maxMoney = self::DEFAULT_MAX_MONEY,
        float $itemDropChanceStep = self::DEFAULT_ITEM_DROP_CHANCE_STEP
    ) {
        $this->maxExp = $maxExp;
        $this->maxMoney = $maxMoney;
        $this->itemDropChanceStep = $itemDropChanceStep;
    }

    /**
     * Рассчитывает и выдает награды игроку
     *
     * @param array $battleData Данные о битве
     * [
     *     'playerLevel' => int,
     *     'enemyLevel' => int,
     *     'enemyCount' => int,
     *     'teamSize' => int, // Количество союзников (1 = соло)
     *     'isUniqueEnemy' => bool,
     *     'playerBonuses' => [
     *         'expBonus' => float, // Множитель опыта (1.0 = +0%, 1.5 = +50%)
     *         'moneyBonus' => float, // Множитель денег
     *         'itemQualityBonus' => [ // Бонус к шансу качества
     *             'uncommon' => float,
     *             'rare' => float,
     *             'legendary' => float
     *         ]
     *     ]
     * ]
     *
     * @return array Массив с наградами
     */
    public function calculateRewards(array $battleData): array
    {
        // Извлекаем данные
        $playerLevel = $battleData['playerLevel'];
        $enemyLevel = $battleData['enemyLevel'];
        $enemyCount = $battleData['enemyCount'] ?? 1;
        $teamSize = $battleData['teamSize'] ?? 1;
        $isUniqueEnemy = $battleData['isUniqueEnemy'] ?? false;
        $playerBonuses = $battleData['playerBonuses'] ?? [];

        // Рассчитываем модификатор сложности битвы
        $difficultyModifier = $this->calculateDifficultyModifier(
            $playerLevel,
            $enemyLevel,
            $enemyCount,
            $teamSize
        );

        $rewards = [
            'experience' => 0,
            'money' => 0,
            'items' => []
        ];

        // 1. Расчет опыта (всегда выдается)
        $rewards['experience'] = $this->calculateExperience(
            $difficultyModifier,
            $playerBonuses['expBonus'] ?? 1.0
        );

        // 2. Расчет денег (с шансом)
        if ($this->rollChance(self::MONEY_DROP_CHANCE)) {
            $rewards['money'] = $this->calculateMoney(
                $difficultyModifier,
                $playerBonuses['moneyBonus'] ?? 1.0
            );
        }

        // 3. Расчет предметов
        $rewards['items'] = $this->calculateItems(
            $difficultyModifier,
            $isUniqueEnemy,
            $playerBonuses['itemQualityBonus'] ?? []
        );

        // Выдаем награды игроку
        $this->giveRewardsToPlayer($rewards);

        return $rewards;
    }

    /**
     * Рассчитывает модификатор сложности битвы
     */
    private function calculateDifficultyModifier(
        int $playerLevel,
        int $enemyLevel,
        int $enemyCount,
        int $teamSize
    ): float {
        $modifier = 1.0;

        // Модификатор за разницу уровней
        $levelDiff = $enemyLevel - $playerLevel;
        if ($levelDiff > 0) {
            // Противник сильнее - бонус
            $modifier += $levelDiff * self::LEVEL_BONUS_MULTIPLIER;
        } elseif ($levelDiff < 0) {
            // Противник слабее - штраф
            $modifier -= abs($levelDiff) * self::LEVEL_PENALTY_MULTIPLIER;
            $modifier = max(0.1, $modifier); // Минимум 10% от базы
        }

        // Модификатор за количество противников (только если они не слабее)
        if ($enemyLevel >= $playerLevel && $enemyCount > 1) {
            $modifier += ($enemyCount - 1) * self::ENEMY_COUNT_BONUS;
        }

        // Модификатор за команду (уменьшает награду)
        if ($teamSize > 1) {
            $modifier /= sqrt($teamSize);
        }

        return max(0.1, $modifier); // Минимальный модификатор 0.1
    }

    /**
     * Рассчитывает количество опыта
     */
    private function calculateExperience(float $difficultyModifier, float $expBonus): int
    {
        $exp = self::BASE_EXP * $difficultyModifier * $expBonus;

        // Добавляем случайность ±20%
        $exp *= mt_rand(80, 120) / 100;

        return min(round($exp), $this->maxExp);
    }

    /**
     * Рассчитывает количество денег
     */
    private function calculateMoney(float $difficultyModifier, float $moneyBonus): int
    {
        $baseMoney = self::BASE_MONEY * $difficultyModifier * $moneyBonus;

        // Взвешенное распределение суммы
        $roll = mt_rand(1, 100);
        if ($roll <= 60) {
            // Малая сумма (50-80% от базы)
            $money = $baseMoney * mt_rand(50, 80) / 100;
        } elseif ($roll <= 90) {
            // Средняя сумма (80-120% от базы)
            $money = $baseMoney * mt_rand(80, 120) / 100;
        } else {
            // Большая сумма (120-200% от базы)
            $money = $baseMoney * mt_rand(120, 200) / 100;
        }

        return min(round($money), $this->maxMoney);
    }

    /**
     * Рассчитывает выпавшие предметы
     */
    private function calculateItems(
        float $difficultyModifier,
        bool $isUniqueEnemy,
        array $qualityBonuses
    ): array {
        $items = [];
        $currentDropChance = self::BASE_ITEM_DROP_CHANCE * $difficultyModifier;

        // Пытаемся выдать предметы, пока есть шанс
        while ($this->rollChance($currentDropChance)) {
            $quality = $this->determineItemQuality($isUniqueEnemy, $qualityBonuses);

            $items[] = [
                'quality' => $quality,
                'id' => $this->generateRandomItemId($quality)
            ];

            // Уменьшаем шанс для следующего предмета
            $currentDropChance *= (1 - $this->itemDropChanceStep);

            // Прерываем, если шанс стал слишком мал
            if ($currentDropChance < 0.01) {
                break;
            }
        }

        return $items;
    }

    /**
     * Определяет качество выпавшего предмета
     */
    private function determineItemQuality(bool $isUniqueEnemy, array $bonuses): string
    {
        // Для уникальных противников есть шанс на уникальный предмет
        if ($isUniqueEnemy && $this->rollChance(0.1)) { // 10% шанс уникального с уникального босса
            return 'unique';
        }

        // Копируем шансы и применяем бонусы
        $qualities = self::ITEM_QUALITIES;

        // Применяем бонусы к шансам (увеличиваем шанс редких качеств)
        if (isset($bonuses['legendary'])) {
            $qualities['legendary'] = round($qualities['legendary'] * $bonuses['legendary']);
        }
        if (isset($bonuses['rare'])) {
            $qualities['rare'] = round($qualities['rare'] * $bonuses['rare']);
        }
        if (isset($bonuses['uncommon'])) {
            $qualities['uncommon'] = round($qualities['uncommon'] * $bonuses['uncommon']);
        }

        // Пересчитываем шанс обычного качества
        $totalSpecial = $qualities['uncommon'] + $qualities['rare'] + $qualities['legendary'];
        $qualities['common'] = 1000 - $totalSpecial;

        // Выбираем качество
        $roll = mt_rand(1, 1000);
        $cumulative = 0;

        foreach ($qualities as $quality => $chance) {
            $cumulative += $chance;
            if ($roll <= $cumulative) {
                return $quality;
            }
        }

        return 'common'; // На всякий случай
    }

    /**
     * Генерирует случайный ID предмета на основе качества
     */
    private function generateRandomItemId(string $quality): int
    {
        // Псевдо-генерация ID в зависимости от качества
        $ranges = [
            'common' => [1000, 2000],
            'uncommon' => [2001, 3000],
            'rare' => [3001, 4000],
            'legendary' => [4001, 5000],
            'unique' => [5001, 6000]
        ];

        return mt_rand($ranges[$quality][0], $ranges[$quality][1]);
    }

    /**
     * Проверяет, выпал ли шанс
     */
    private function rollChance(float $chance): bool
    {
        return mt_rand(1, 10000) <= ($chance * 10000);
    }

    /**
     * Выдает награды игроку (псевдокод)
     */
    private function giveRewardsToPlayer(array $rewards): void
    {
        // Player::giveExperience($rewards['experience']);

        if ($rewards['money'] > 0) {
            // Player::giveMoney($rewards['money']);
        }

        foreach ($rewards['items'] as $item) {
            // Player::giveItem($item['id'], $item['quality']);
        }
    }
}

Использование:

// Пример использования
$calculator = new RewardCalculator();

// Битва: игрок 10 уровня против 2х монстров 12 уровня
$rewards = $calculator->calculateRewards([
    'playerLevel' => 10,
    'enemyLevel' => 12,
    'enemyCount' => 2,
    'teamSize' => 1, // Соло
    'isUniqueEnemy' => false,
    'playerBonuses' => [
        'expBonus' => 1.2, // +20% к опыту
        'moneyBonus' => 1.0,
        'itemQualityBonus' => [
            'rare' => 2.0, // Удваиваем шанс редких предметов
        ]
    ]
]);


echo "<pre>";
print_r($rewards);
echo "</pre>";

// Результат:
// [
//     'experience' => 168,  // Базовый опыт с модификаторами
//     'money' => 75,        // Если выпал шанс
//     'items' => [          // Массив выпавших предметов
//         ['quality' => 'common', 'id' => 1523],
//         ['quality' => 'uncommon', 'id' => 2341]
//     ]
// ]

Из косяков: я не придумал как красиво обыграть расчёт групповых поединков, поэтому в массив $battleData playerLevel и enemyLevel прилетают целым числом. Что сюда засовывать если есть несколько противников разного уровня придумывайте сами, потому что как балансировать награды Игрок[5], Игрок[12] против Игрок[8], Игрок[8], Игрок[10]сложна, сложна, ничего не понятно!

1 лайк