jaroslavtyc/drd-plus-therugist-spells

View on GitHub
DrdPlus/Theurgist/Spells/Formula.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
declare(strict_types=1);

namespace DrdPlus\Theurgist\Spells;

use DrdPlus\Codes\Units\DistanceUnitCode;
use DrdPlus\Tables\Measurements\Distance\Distance;
use DrdPlus\Tables\Measurements\Distance\DistanceBonus;
use DrdPlus\Tables\Measurements\Distance\DistanceTable;
use DrdPlus\Codes\Theurgist\FormulaCode;
use DrdPlus\Codes\Theurgist\FormulaMutableSpellParameterCode;
use DrdPlus\Codes\Theurgist\ModifierCode;
use DrdPlus\Codes\Theurgist\ModifierMutableSpellParameterCode;
use DrdPlus\Theurgist\Spells\SpellParameters\Attack;
use DrdPlus\Theurgist\Spells\SpellParameters\Brightness;
use DrdPlus\Theurgist\Spells\SpellParameters\CastingRounds;
use DrdPlus\Theurgist\Spells\SpellParameters\DetailLevel;
use DrdPlus\Theurgist\Spells\SpellParameters\Duration;
use DrdPlus\Theurgist\Spells\SpellParameters\EpicenterShift;
use DrdPlus\Theurgist\Spells\SpellParameters\Evocation;
use DrdPlus\Theurgist\Spells\SpellParameters\FormulaDifficulty;
use DrdPlus\Theurgist\Spells\SpellParameters\Partials\CastingParameter;
use DrdPlus\Theurgist\Spells\SpellParameters\Power;
use DrdPlus\Theurgist\Spells\SpellParameters\Radius;
use DrdPlus\Theurgist\Spells\SpellParameters\Realm;
use DrdPlus\Theurgist\Spells\SpellParameters\RealmsAffection;
use DrdPlus\Theurgist\Spells\SpellParameters\SizeChange;
use DrdPlus\Theurgist\Spells\SpellParameters\SpellSpeed;
use Granam\Integer\Tools\ToInteger;
use Granam\Strict\Object\StrictObject;
use Granam\String\StringTools;
use Granam\Tools\ValueDescriber;

class Formula extends StrictObject
{
    use ToFlatArrayTrait;

    /** @var FormulaCode */
    private $formulaCode;
    /** @var FormulasTable */
    private $formulasTable;
    /** @var DistanceTable */
    private $distanceTable;
    /** @var int[] */
    private $formulaSpellParameterChanges;
    /** @var Modifier[] */
    private $modifiers;
    /** @var SpellTrait[] */
    private $formulaSpellTraits;

    /**
     * @param FormulaCode $formulaCode
     * @param FormulasTable $formulasTable
     * @param DistanceTable $distanceTable
     * @param array $formulaSpellParameterValues Current values of spell parameters (changes will be calculated from them)
     * by @see FormulaMutableSpellParameterCode value indexed its value change
     * @param array|Modifier[] $modifiers
     * @param array|SpellTrait[] $formulaSpellTraits
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\UselessValueForUnusedSpellParameter
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\UnknownFormulaParameter
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\InvalidValueForFormulaParameter
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\InvalidModifier
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\InvalidSpellTrait
     */
    public function __construct(
        FormulaCode $formulaCode,
        FormulasTable $formulasTable,
        DistanceTable $distanceTable,
        array $formulaSpellParameterValues = [],
        array $modifiers = [],
        array $formulaSpellTraits = []
    )
    {
        $this->formulaCode = $formulaCode;
        $this->formulasTable = $formulasTable;
        $this->distanceTable = $distanceTable;
        // gets spell parameter changes as delta of current values and default values
        $this->formulaSpellParameterChanges = $this->sanitizeSpellParameterChanges($formulaSpellParameterValues);
        $this->modifiers = $this->getCheckedModifiers($this->toFlatArray($modifiers));
        $this->formulaSpellTraits = $this->getCheckedSpellTraits($this->toFlatArray($formulaSpellTraits));
    }

    /**
     * @param array $spellParameterValues
     * @return array
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\UselessValueForUnusedSpellParameter
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\InvalidValueForFormulaParameter
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\UnknownFormulaParameter
     */
    private function sanitizeSpellParameterChanges(array $spellParameterValues): array
    {
        $sanitizedChanges = [];
        foreach (FormulaMutableSpellParameterCode::getPossibleValues() as $mutableSpellParameter) {
            if (!\array_key_exists($mutableSpellParameter, $spellParameterValues)) {
                $sanitizedChanges[$mutableSpellParameter] = 0;
                continue;
            }
            try {
                $sanitizedValue = ToInteger::toInteger($spellParameterValues[$mutableSpellParameter]);
            } catch (\Granam\Integer\Tools\Exceptions\Exception $exception) {
                throw new Exceptions\InvalidValueForFormulaParameter(
                    'Expected integer, got ' . ValueDescriber::describe($spellParameterValues[$mutableSpellParameter])
                    . ' for ' . $mutableSpellParameter . ": '{$exception->getMessage()}'"
                );
            }
            /** like @see FormulasTable::getCastingRounds() */
            $getParameter = StringTools::assembleGetterForName($mutableSpellParameter);
            /** @var CastingParameter $baseParameter */
            $baseParameter = $this->formulasTable->$getParameter($this->getFormulaCode());
            if ($baseParameter === null) {
                throw new Exceptions\UselessValueForUnusedSpellParameter(
                    "Casting parameter {$mutableSpellParameter} is not used for formula {$this->formulaCode}"
                    . ', so given non-zero addition ' . ValueDescriber::describe($spellParameterValues[$mutableSpellParameter])
                    . ' is thrown away'
                );
            }
            $parameterChange = $sanitizedValue - $baseParameter->getDefaultValue();
            $sanitizedChanges[$mutableSpellParameter] = $parameterChange;

            unset($spellParameterValues[$mutableSpellParameter]);
        }
        if (\count($spellParameterValues) > 0) { // there are some remains
            throw new Exceptions\UnknownFormulaParameter(
                'Unexpected mutable spells parameter(s) [' . \implode(', ', array_keys($spellParameterValues)) . ']. Expected only '
                . \implode(', ', FormulaMutableSpellParameterCode::getPossibleValues())
            );
        }

        return $sanitizedChanges;
    }

    /**
     * @param array $modifiers
     * @return array|Modifier[]
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\InvalidModifier
     */
    private function getCheckedModifiers(array $modifiers): array
    {
        foreach ($modifiers as $modifier) {
            if (!is_a($modifier, Modifier::class)) {
                throw new Exceptions\InvalidModifier(
                    'Expected instance of ' . Modifier::class . ', got ' . ValueDescriber::describe($modifier)
                );
            }
        }

        return $modifiers;
    }

    /**
     * @param array $spellTraits
     * @return array|SpellTrait[]
     * @throws \DrdPlus\Theurgist\Spells\Exceptions\InvalidSpellTrait
     */
    private function getCheckedSpellTraits(array $spellTraits): array
    {
        foreach ($spellTraits as $spellTrait) {
            if (!is_a($spellTrait, SpellTrait::class)) {
                throw new Exceptions\InvalidSpellTrait(
                    'Expected instance of ' . Modifier::class . ', got ' . ValueDescriber::describe($spellTrait)
                );
            }
        }

        return $spellTraits;
    }

    /**
     * All modifiers in a flat array (with removed tree structure)
     *
     * @return array|Modifier[]
     */
    public function getModifiers(): array
    {
        return $this->modifiers;
    }

    /**
     * @return FormulaDifficulty
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentDifficulty(): FormulaDifficulty
    {
        $formulaParameters = [
            $this->getAttackWithAddition(),
            $this->getBrightnessWithAddition(),
            $this->getDetailLevelWithAddition(),
            $this->getDurationWithAddition(),
            $this->getEpicenterShiftWithAddition(),
            $this->getPowerWithAddition(),
            $this->getRadiusWithAddition(),
            $this->getSizeChangeWithAddition(),
            $this->getSpellSpeedWithAddition(),
        ];
        $formulaParameters = array_filter(
            $formulaParameters,
            function (CastingParameter $formulaParameter = null) {
                return $formulaParameter !== null;
            }
        );
        $parametersDifficultyChangeSum = 0;
        /** @var CastingParameter $formulaParameter */
        foreach ($formulaParameters as $formulaParameter) {
            $parametersDifficultyChangeSum += $formulaParameter->getAdditionByDifficulty()->getCurrentDifficultyIncrement();
        }
        $modifiersDifficultyChangeSum = 0;
        foreach ($this->modifiers as $modifier) {
            $modifiersDifficultyChangeSum += $modifier->getDifficultyChange()->getValue();
        }
        $spellTraitsDifficultyChangeSum = 0;
        foreach ($this->formulaSpellTraits as $spellTrait) {
            $spellTraitsDifficultyChangeSum += $spellTrait->getDifficultyChange()->getValue();
        }
        $formulaDifficulty = $this->formulasTable->getFormulaDifficulty($this->getFormulaCode());

        return $formulaDifficulty->createWithChange(
            $parametersDifficultyChangeSum
            + $modifiersDifficultyChangeSum
            + $spellTraitsDifficultyChangeSum
        );
    }

    /**
     * @return CastingRounds
     */
    public function getCurrentCastingRounds(): CastingRounds
    {
        $castingRoundsSum = 0;
        foreach ($this->modifiers as $modifier) {
            $castingRoundsSum += $modifier->getCastingRounds()->getValue();
        }
        $castingRoundsSum += $this->formulasTable->getCastingRounds($this->getFormulaCode())->getValue();

        return new CastingRounds([$castingRoundsSum]);
    }

    /**
     * Evocation time is not affected by any modifier or trait.
     *
     * @return Evocation
     */
    public function getCurrentEvocation(): Evocation
    {
        return $this->formulasTable->getEvocation($this->getFormulaCode());
    }

    /**
     * Daily, monthly and lifetime affections of realms
     *
     * @return array|RealmsAffection[]
     */
    public function getCurrentRealmsAffections(): array
    {
        $realmsAffections = [];
        foreach ($this->getRealmsAffectionsSum() as $periodName => $periodSum) {
            $realmsAffections[$periodName] = new RealmsAffection([$periodSum, $periodName]);
        }

        return $realmsAffections;
    }

    /**
     * @return array|int[] by affection period indexed summary of that period realms-affection
     */
    private function getRealmsAffectionsSum(): array
    {
        $baseRealmsAffection = $this->formulasTable->getRealmsAffection($this->getFormulaCode());
        $realmsAffectionsSum = [
            // like daily => -2
            $baseRealmsAffection->getAffectionPeriod()->getValue() => $baseRealmsAffection->getValue(),
        ];
        foreach ($this->modifiers as $modifier) {
            $modifierRealmsAffection = $modifier->getRealmsAffection();
            if ($modifierRealmsAffection === null) {
                continue;
            }
            $modifierRealmsAffectionPeriod = $modifierRealmsAffection->getAffectionPeriod()->getValue();
            if (!array_key_exists($modifierRealmsAffectionPeriod, $realmsAffectionsSum)) {
                $realmsAffectionsSum[$modifierRealmsAffectionPeriod] = 0;
            }
            $realmsAffectionsSum[$modifierRealmsAffectionPeriod] += $modifierRealmsAffection->getValue();
        }

        return $realmsAffectionsSum;
    }

    /**
     * Gives the highest required realm (by difficulty, by formula itself or by one of its modifiers)
     *
     * @return Realm
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getRequiredRealm(): Realm
    {
        $realmsIncrement = $this->getCurrentDifficulty()->getCurrentRealmsIncrement();
        $realm = $this->formulasTable->getRealm($this->getFormulaCode());
        $requiredRealm = $realm->add($realmsIncrement);

        foreach ($this->modifiers as $modifier) {
            $byModifierRequiredRealm = $modifier->getRequiredRealm();
            if ($requiredRealm->getValue() < $byModifierRequiredRealm->getValue()) {
                // some modifier requires even higher realm, so we are forced to increase it
                $requiredRealm = $byModifierRequiredRealm;
            }
        }

        return $requiredRealm;
    }

    /**
     * @return FormulaCode
     */
    public function getFormulaCode(): FormulaCode
    {
        return $this->formulaCode;
    }

    /**
     * Final radius including direct formula change and all its active traits and modifiers.
     *
     * @return Radius|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentRadius(): ?Radius
    {
        $radiusWithAddition = $this->getRadiusWithAddition();
        if (!$radiusWithAddition) {
            return null;
        }

        $radiusModifiersChange = $this->getParameterBonusFromModifiers(ModifierMutableSpellParameterCode::RADIUS);
        if (!$radiusModifiersChange) {
            return new Radius([$radiusWithAddition->getValue(), 0]);
        }

        return new Radius([$radiusWithAddition->getValue() + $radiusModifiersChange, 0]);
    }

    /**
     * Formula radius extended by direct formula change
     *
     * @return Radius|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getRadiusWithAddition(): ?Radius
    {
        $baseRadius = $this->formulasTable->getRadius($this->formulaCode);
        if ($baseRadius === null) {
            return null;
        }

        return $baseRadius->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::RADIUS]);
    }

    /**
     * Any formula (spell) can be shifted
     *
     * @return EpicenterShift|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentEpicenterShift(): ?EpicenterShift
    {
        $epicenterShiftWithAddition = $this->getEpicenterShiftWithAddition();
        $epicenterShiftByModifiers = $this->getParameterBonusFromModifiers(ModifierMutableSpellParameterCode::EPICENTER_SHIFT);
        if ($epicenterShiftWithAddition === null) {
            if ($epicenterShiftByModifiers === false) {
                return null;
            }

            return new EpicenterShift(
                [$epicenterShiftByModifiers['bonus'], 0 /* no added difficulty*/],
                new Distance($epicenterShiftByModifiers['meters'], DistanceUnitCode::METER, $this->distanceTable)
            );
        }
        if ($epicenterShiftByModifiers === false) {
            return $epicenterShiftWithAddition;
        }
        $meters = $epicenterShiftWithAddition->getDistance($this->distanceTable)->getMeters();
        $meters += $epicenterShiftByModifiers['meters'];

        $distance = new Distance($meters, DistanceUnitCode::METER, $this->distanceTable);

        return new EpicenterShift([$distance->getBonus(), 0 /* no added difficulty */], $distance);
    }

    /**
     * @return EpicenterShift|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getEpicenterShiftWithAddition(): ?EpicenterShift
    {
        $baseEpicenterShift = $this->formulasTable->getEpicenterShift($this->formulaCode);
        if ($baseEpicenterShift === null) {
            return null;
        }

        return $baseEpicenterShift->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::EPICENTER_SHIFT]);
    }

    /**
     * Any formula (spell) can get a power, even if was passive and not harming before
     *
     * @return Power|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentPower(): ?Power
    {
        $powerWithAddition = $this->getPowerWithAddition();
        $powerBonus = $this->getParameterBonusFromModifiers(ModifierMutableSpellParameterCode::POWER);
        if (!$powerWithAddition && $powerBonus === false) {
            return null;
        }

        return new Power([
            ($powerWithAddition
                ? $powerWithAddition->getValue()
                : 0)
            + (int)$powerBonus,
            0, // no addition
        ]);
    }

    /**
     * @return Power|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getPowerWithAddition(): ?Power
    {
        $basePower = $this->formulasTable->getPower($this->formulaCode);
        if ($basePower === null) {
            return null;
        }

        return $basePower->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::POWER]);
    }

    /**
     * Attack can be only increased, not added.
     *
     * @return Attack|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentAttack(): ?Attack
    {
        $attackWithAddition = $this->getAttackWithAddition();
        if (!$attackWithAddition) {
            return null;
        }

        return new Attack([
            $attackWithAddition->getValue()
            + (int)$this->getParameterBonusFromModifiers(ModifierMutableSpellParameterCode::ATTACK),
            0 // no addition
        ]);
    }

    /**
     * @return Attack|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getAttackWithAddition(): ?Attack
    {
        $baseAttack = $this->formulasTable->getAttack($this->formulaCode);
        if ($baseAttack === null) {
            return null;
        }

        return $baseAttack->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::ATTACK]);
    }

    /**
     * @param string $parameterName
     * @return bool|int|array|int[]
     */
    private function getParameterBonusFromModifiers(string $parameterName)
    {
        $bonusParts = [];
        foreach ($this->modifiers as $modifier) {
            if ($modifier->getModifierCode()->getValue() === ModifierCode::GATE) {
                continue; // gate does not give bonus to a parameter, it is standalone being with its own parameters
            }
            if ($parameterName === ModifierMutableSpellParameterCode::POWER
                && $modifier->getModifierCode()->getValue() === ModifierCode::THUNDER
            ) {
                continue; // thunder power means a noise, does not affects formula power
            }
            $getParameterWithAddition = StringTools::assembleGetterForName($parameterName . 'WithAddition');
            /** like @see Modifier::getAttackWithAddition() */
            $parameter = $modifier->$getParameterWithAddition();
            if ($parameter === null) {
                continue;
            }
            /** @var CastingParameter $parameter */
            $bonusParts[] = $parameter->getValue();
        }
        if (\count($bonusParts) === 0) {
            return false;
        }

        // transpositions are chained in sequence and their values (distances) have to be summed, not bonuses
        if ($parameterName === ModifierMutableSpellParameterCode::EPICENTER_SHIFT) {
            $meters = 0;
            foreach ($bonusParts as $bonusPart) {
                $meters += (new DistanceBonus($bonusPart, $this->distanceTable))->getDistance()->getMeters();
            }

            return [
                'bonus' => (new Distance($meters, DistanceUnitCode::METER, $this->distanceTable))->getBonus()->getValue(),
                'meters' => $meters,
            ];
        }

        return (int)\array_sum($bonusParts);
    }

    /**
     * Any formula (spell) can get a speed, even if was static before
     *
     * @return SpellSpeed|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentSpellSpeed(): ?SpellSpeed
    {
        $spellSpeedWithAddition = $this->getSpellSpeedWithAddition();
        $spellSpeedBonus = $this->getParameterBonusFromModifiers(ModifierMutableSpellParameterCode::SPELL_SPEED);
        if (!$spellSpeedWithAddition && $spellSpeedBonus === false) {
            return null;
        }

        return new SpellSpeed([
            ($spellSpeedWithAddition
                ? $spellSpeedWithAddition->getValue()
                : 0)
            + (int)$spellSpeedBonus,
            0,
        ]);
    }

    /**
     * @return SpellSpeed|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getSpellSpeedWithAddition(): ?SpellSpeed
    {
        $baseSpellSpeed = $this->formulasTable->getSpellSpeed($this->formulaCode);
        if ($baseSpellSpeed === null) {
            return null;
        }

        return $baseSpellSpeed->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::SPELL_SPEED]);
    }

    /**
     * @return DetailLevel|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentDetailLevel(): ?DetailLevel
    {
        return $this->getDetailLevelWithAddition();
    }

    /**
     * @return DetailLevel|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getDetailLevelWithAddition(): ?DetailLevel
    {
        $baseDetailLevel = $this->formulasTable->getDetailLevel($this->formulaCode);
        if ($baseDetailLevel === null) {
            return null;
        }

        return $baseDetailLevel->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::DETAIL_LEVEL]);
    }

    /**
     * @return Brightness|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentBrightness(): ?Brightness
    {
        return $this->getBrightnessWithAddition();
    }

    /**
     * @return Brightness|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getBrightnessWithAddition(): ?Brightness
    {
        $baseBrightness = $this->formulasTable->getBrightness($this->formulaCode);
        if ($baseBrightness === null) {
            return null;
        }

        return $baseBrightness->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::BRIGHTNESS]);
    }

    /**
     * @return Duration
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentDuration(): Duration
    {
        return $this->getDurationWithAddition();
    }

    /**
     * @return Duration
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getDurationWithAddition(): Duration
    {
        $baseDuration = $this->formulasTable->getDuration($this->formulaCode);

        return $baseDuration->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::DURATION]);
    }

    /**
     * @return SizeChange|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    public function getCurrentSizeChange(): ?SizeChange
    {
        return $this->getSizeChangeWithAddition();
    }

    /**
     * @return SizeChange|null
     * @throws \Granam\Integer\Tools\Exceptions\Exception
     */
    private function getSizeChangeWithAddition(): ?SizeChange
    {
        $baseSizeChange = $this->formulasTable->getSizeChange($this->formulaCode);
        if ($baseSizeChange === null) {
            return null;
        }

        return $baseSizeChange->getWithAddition($this->formulaSpellParameterChanges[FormulaMutableSpellParameterCode::SIZE_CHANGE]);
    }

    public function __toString()
    {
        return $this->getFormulaCode()->getValue();
    }

}