Стратегия АТБ архитектуре MVC рефакторинг


Примечание: этот вопрос не так долго, как кажется. Я добавил комментарии к коду только, чтобы ответить на некоторые возможные вопросы, которые могут появиться.

Я делаю стратегии активное время битвы. По своей механике очень похожи на те, что введены в последней фантазии, но я не могу вспомнить, что в серии был ближайший к моей игре. Появилась флэш-игра, под названием Сонни, что на 99,9% похожа на игру моя.

Цель игры-убить всю команду противника. Каждый блок обладает готовностью бассейн, его регенерации скорость зависит от единицы ловкости ака. потока. Всякий раз, когда этот бассейн полон, юнит может кастовать одно заклинание из его колеса.

Игрок в свою очередь состоит из двух этапов: Выбор возможность (выбор) и выбора способности цели (таргетинг).

Battle sketch

Я использую модифицированную версию MVC, где контроллер посылает запросы к модели, получает ответы С данные, а затем отправляет эти данные в видение. Я думаю, что такой подход является слишком жестким, слишком unflexible, но это одна из причин, почему я здесь. Вот как это сделано сейчас:

Controller layout

Рычаг блок состоит из методик, которые изменяют модели в общем способов (добавить/вычесть хп/маны, литой/dispell бафф и т. д.) и доступны из любой части проекта.

Пусковым блок состоит из функций, которые обрабатывают пользовательский ввод (не сырьевая, первичной переработки расположен в видении).

Например, если пользователь нажимает цифру ключа, код в видении (см. ниже) обрабатывает этот ввод и затем вызывает соответствующий метод контроллера выбрать(), принятия решения (по своей инициативе!) какие способности игрок хочет выбрать (я не уверен, если это хороший подход, чтобы оставить этот тип логики в понимании, а не в контроллере).

Удобство использования блок содержит юзабилити() метод и вспомогательные приемы. Обратите внимание, что выбрать() и Target() вызывается только тогда, когда игрок делает ход, но юзабилити() вызывается всякий раз, когда любая группа делает это.

Тот факт, что юзабилити() состоит из трех этапов (и, следовательно, называется три раза, чтобы избежать создания трех различных методов) определяется способом использования возможностей анимации. Например, когда блок удары в створ, он бросается к ней, то цель получает урон, а затем блок, который пнул возвращается к своей первоначальной позиции. Различные типы способностей анимированы по-разному, но видение-модель-видение последовательность остается той же.

Чтобы иметь возможность работать с одной и той же цели, литейщик и способность во всех итерациях юзабилити() метод, я хранить эти значения в свойства контроллера.

Если игрок делает ход, setUA() вызывается внутри объекта() метод, то он называется внутри одного из методов модели.

class Controller extends Sprite
{

    public static var instance:Null<Controller>;
    private var model:Model;
    private var vision:Vision;

    public var inputMode:InputMode;

    private var chosenAbility:Int;
    private var uatarget:UnitCoords;
    private var uacaster:UnitCoords;
    private var uaability:Ability;
    private var uaiterator:Int;

    //================================================================================
    // Levers
    //================================================================================

    public function changeUnitHP(target:Unit, caster:Unit, dhp:Int, element:Element, source:Source)
    {
        var modelOutput:HPChangerOutput = model.changeUnitHP(target, caster, dhp, source);

        vision.changeUnitHP(target, modelOutput.dhp, element, modelOutput.crit, source);

        if (target.hpPool.value == 0)
            vision.die(new UnitCoords(target.team, target.position));
    }

    public function changeUnitMana(target:Unit, caster:Unit, dmana:Int, source:Source)
    {
        var finalValue:Int = model.changeUnitMana(target, caster, dmana, source);

        vision.changeUnitMana(target, finalValue, source);
    }

    public function changeUnitAlacrity(target:Unit, caster:Unit, dalac:Float, source:Source)
    {
        var finalValue:Float = model.changeUnitAlacrity(target, caster, dalac, source);

        vision.changeUnitAlacrity(target, finalValue, source);
    }

    public function castBuff(id:ID, target:Unit, caster:Unit, duration:Int)
    {
        model.castBuff(id, target, caster, duration);
        vision.castBuff(id, duration);
    }

    public function dispellBuffs(target:Unit, ?elements:Array<Element>, ?count:Int = -1)
    {
        var newBuffArray:Array<Buff> = model.dispellBuffs(target, elements, count);
        vision.redrawBuffs(target, newBuffArray);
    }

    //================================================================================
    // Triggering blocks
    //================================================================================

    public function choose(abilityNum:Int)
    {
        switch (model.checkChoose(abilityNum))
        {
            case ChooseResult.Ok:
                inputMode = InputMode.Targeting;
                chosenAbility = abilityNum;
                vision.selectAbility(abilityNum);
            case ChooseResult.Empty:
                vision.printWarning("There is no ability in this slot");
            case ChooseResult.Manacost:
                vision.printWarning("Not enough mana");
            case ChooseResult.Cooldown:
                vision.printWarning("This ability is currently on cooldown");
        }
    }

    public function use(targetCoords:UnitCoords)
    {
        switch (model.checkTarget(targetCoords, chosenAbility))
        {
            case TargetResult.Ok:
                inputMode = InputMode.None;

                vision.target(targetCoords);
                vision.deselectAbility(chosenAbility);

                setUA(targetCoords, new UnitCoords(battle.enums.Team.Left, 0), model.getPlayerAbility(chosenAbility));

                chosenAbility = -1;

                useAbility();
            case TargetResult.Invalid:
                vision.printWarning("Chosen ability cannot be used on this target");
                vision.deselectAbility(chosenAbility);

                chosenAbility = -1;

                inputMode = InputMode.Choosing;
            case TargetResult.Nonexistent, TargetResult.Dead:
                //Ignore silently
        }
    }

    public function skipTurnAttempt():Bool
    {
        if (inputMode != InputMode.None)
        {
            inputMode = InputMode.None;
            model.postTurnProcess(new UnitCoords(Team.Left, 0));
            return true;
        }

        return false;
    }

    public function end(winner:Null<Team>)
    {
        inputMode = InputMode.None;

        if (winner == Team.Left)
            vision.printWarning("You won!!!");
        else if (winner == Team.Right)
            vision.printWarning("You lost(");
        else 
            vision.printWarning("A draw...");

        removeChild(vision);

        Main.onBattleOver();
    }

    //================================================================================
    // useAbility
    //================================================================================  

    public function useAbility()
    {
        switch (uaiterator++)
        {
            case 0:
                vision.abilityIntro(uatarget, uacaster, {type:uaability.type, element:uaability.element});
            case 1:
                if (model.checkUse(uatarget, uacaster, uaability) == UseResult.Miss)
                    vision.unitMiss(uatarget, uaability.element);
                else
                    model.useAbility(uatarget, uacaster, uaability);

                vision.abilityOutro(uatarget, uacaster, {id:uaability.id, type:uaability.type});
            case 2:
                model.postTurnProcess(uacaster);
            default:
                clearUA();
                useAbility();
        }
    }

    public function setUA(target:UnitCoords, caster:UnitCoords, ability:Ability)
    {
        uatarget = target;
        uacaster = caster;
        uaability = ability;
    }

    private function clearUA()
    {
        uatarget = new UnitCoords(Team.Left, -1);
        uacaster = new UnitCoords(Team.Left, -1);
        uaability = new Ability(ID.NullID);
        uaiterator = 0;
    }

    //================================================================================
    // INIT + Constructor
    //================================================================================  

    public function destroy()
    {
        instance = null;
    }

    public function init(zone:Int, stage:Int, allies:Array<Unit>)
    {
        var enemyIDs:Array<ID> = XMLUtils.parseStage(zone, stage);
        var enemies:Array<Unit> = [];
        for (i in 0...enemyIDs.length)
            enemies.push(new Unit(enemyIDs[i], Team.Right, i));

        model = new Model(allies, enemies);
        vision = new Vision();
        addChild(vision);
        vision.init(zone, allies, enemies);

        uatarget = new UnitCoords(Team.Left, -1);
        uacaster = new UnitCoords(Team.Left, -1);
        uaability = new Ability(ID.NullID);
        uaiterator = 0;

        model.alacrityIncrement();
    }

    public function new() 
    {
        super();
        instance = this;
    }
}

Тогда мы приходим к модели:

typedef AbilityInfo = {
    var name:String;
    var describition:String;
    var type:AbilityType;
    var maxCooldown:Int;
    var currentCooldown:Int;
    var manacost:Int;
    var target:AbilityTarget;
}

typedef UnitInfo = {
    var name:String;
    var buffQueue:BuffQueue;
}

typedef HPChangerOutput = {
    var dhp:Int;
    var crit:Bool;
}

enum ChooseResult 
{
    Ok;
    Empty;
    Manacost;
    Cooldown;
}

enum TargetResult 
{
    Ok;
    Invalid;
    Nonexistent;
    Dead;
}

enum UseResult 
{
    Ok;
    Miss;
}

class Model 
{

    private var allies:Array<Unit>;
    private var enemies:Array<Unit>;

    private var unitToProcess:Null<Unit>;
    private var readyUnits:Array<Unit>;

    //================================================================================
    // Levers
    //================================================================================  

    public function changeUnitHP(target:Unit, caster:Unit, dhp:Int, source:Source):HPChangerOutput
    {
        var processedDelta:Int = dhp;
        var crit:Bool = false;

        if (source != Source.God)
        {   
            if (dhp > 0)
                processedDelta = Math.round(Linear.combination([target.healIn, caster.healOut]).apply(processedDelta));
            else
                processedDelta = Math.round(Linear.combination([target.damageIn, caster.damageOut]).apply(processedDelta));

            if (Math.random() < caster.critChance.apply(1))
            {
                processedDelta = Math.round(caster.critDamage.apply(processedDelta));
                crit = true;
            }
        }

        target.hpPool.value += processedDelta;  
        return {dhp:processedDelta, crit:crit};
    }

    public function changeUnitMana(target:Unit, caster:Unit, dmana:Int, source:Source):Int
    {
        target.manaPool.value += dmana;
        return dmana;
    }

    public function changeUnitAlacrity(target:Unit, caster:Unit, dalac:Float, source:Source):Float
    {
        target.alacrityPool.value += dalac;
        return dalac;
    }

    public function castBuff(id:ID, target:Unit, caster:Unit, duration:Int)
    {
        target.buffQueue.addBuff(new battle.Buff(id, target, caster, duration)); 
    }

    public function dispellBuffs(target:Unit, ?elements:Array<Element>, ?count:Int = -1):Array<battle.Buff>
    {
        target.buffQueue.dispell(elements, count);
        return target.buffQueue.queue;
    }

    //================================================================================
    // Input
    //================================================================================

    public function checkChoose(abilityPos:Int):ChooseResult
    {
        var hero:Unit = allies[0];
        var ability:battle.Ability = hero.wheel.get(abilityPos);

        if (ability.checkEmpty())
            return ChooseResult.Empty;
        if (ability.checkOnCooldown())
            return ChooseResult.Cooldown;
        if (!hero.checkManacost(abilityPos))
            return ChooseResult.Manacost;

        return ChooseResult.Ok;
    }

    public function checkTarget(targetCoords:UnitCoords, abilityPos:Int):TargetResult
    {
        var target:Unit = getUnit(targetCoords);
        var ability:battle.Ability = allies[0].wheel.get(abilityPos);

        if (target == null)
            return TargetResult.Nonexistent;
        if (target.hpPool.value == 0)
            return TargetResult.Dead;
        if (!ability.checkValidity(target, allies[0]))
            return TargetResult.Invalid;

        return TargetResult.Ok;
    }

    public function checkUse(targetCoords:UnitCoords, casterCoords:UnitCoords, ability:Ability):UseResult
    {
        return UseResult.Ok;
    }

    public function getPlayerAbility(pos:Int):battle.Ability
    {
        return allies[0].wheel.get(pos);
    }

    public function useAbility(target:UnitCoords, caster:UnitCoords, ability:battle.Ability)
    {
        ability.use(getUnit(target), getUnit(caster));
    }

    //================================================================================
    // Cycle control
    //================================================================================

    public function alacrityIncrement()
    {
        for (unit in allies.concat(enemies))
            if (checkAlive([unit]))
            {
                Controller.instance.changeUnitAlacrity(unit, unit, getAlacrityGain(unit), Source.God);

                if (unit.alacrityPool.value == 100)
                    readyUnits.push(unit);
            }

        if (Lambda.empty(readyUnits))
            alacrityIncrement();
        else
        {
            try 
            {
                sortByFlow(readyUnits);
                processReady();
            } 
            catch (e:Dynamic)
            {
                trace(e);
                trace(CallStack.toString(CallStack.exceptionStack()));
                Sys.exit(1);
            }
        }
    }

    private function processReady()
    {
        if (!Lambda.empty(readyUnits))
        {
            var unit:Unit = readyUnits[0];
            readyUnits.splice(0, 1);
            Controller.instance.changeUnitAlacrity(unit, unit, -100, Source.God);

            if (!unit.isStunned()) 
            {
                if (unit.team == Team.Left && unit.position == 0)
                    Controller.instance.inputMode = InputMode.Choosing;
                else
                    botMakeTurn(unit);
            }
            else
                postTurnProcess(new UnitCoords(unit.team, unit.position));
        }
        else
            alacrityIncrement();
    }

    public function postTurnProcess(coords:UnitCoords)
    {
        var unit:Unit = getUnit(coords);

        if (!bothTeamsAlive()) 
        {
            Controller.instance.end(defineWinner());
            return;
        }

        if (unit.hpPool.value > 0)
            unit.tick();

        if (!bothTeamsAlive()) 
        {
            Controller.instance.end(defineWinner());
            return;
        }

        processReady();
    }

    private function botMakeTurn(bot:Unit)
    {
        var decision:BotDecision = Units.decide(bot.id, allies, enemies);
        trace(bot.wheel.get(decision.abilityNum));
        Controller.instance.setUA(decision.target, getCoords(bot), bot.wheel.get(decision.abilityNum));
        Controller.instance.useAbility();
    }

    private function getAlacrityGain(unit:Unit):Float
    {
        var sum:Float = 0;
        for (unitI in allies.concat(enemies))
            if (checkAlive([unitI]))
                sum += unitI.flow;
        return unit.flow / sum;
    }

    private function sortByFlow(array:Array<Unit>)
    {
        function swap(j1:Int, j2:Int)
        {
            var t:Unit = array[j1];
            array[j1] = array[j2];
            array[j2] = t;
        }

        for (i in 1...array.length)
            for (j in i...array.length)
                if (array[j - 1].flow < array[j].flow)
                    swap(j - 1, j);
                else if (array[j - 1].flow == array[j].flow)
                    if (MathUtils.flip())
                        swap(j - 1, j);
    }

    //================================================================================
    // Battle end utilities
    //================================================================================

    public function bothTeamsAlive():Bool
    {
        return checkAlive(allies) && checkAlive(enemies);
    }

    public function defineWinner():Null<Team>
    {
        if (checkAlive(allies))
            return Team.Left;
        else if (checkAlive(enemies))
            return Team.Right;
        else
            return null;
    }

    private function checkAlive(array:Array<Unit>):Bool
    {
        for (unit in array)
            if (unit.hpPool.value > 0)
                return true;
        return false;
    }



//================================================================================
        // Other
        //================================================================================

    private inline function getUnit(coords:UnitCoords):Null<Unit>
    {
        var array:Array<Unit> = (coords.team == battle.enums.Team.Left)? allies : enemies;
        return array[coords.pos];
    }

    private inline function getCoords(unit:Unit):UnitCoords
    {
        return new UnitCoords(unit.team, unit.position);
    }

    //================================================================================
    // Constructor
    //================================================================================

    public function new(allies:Array<Unit>, enemies:Array<Unit>) 
    {
        this.allies = allies;
        this.enemies = enemies;
        this.readyUnits = [];
    }

}

Наконец, есть видение, но я не думаю, что это надо постить весь свой код, как это в основном состоит из визуального программирования, поэтому я просто оставлю здесь генеральный план (важных методов полностью перечисленных):

class Vision extends SSprite
{

    private var bg:DisplayObject;
    private var upperBar:DisplayObject;
    private var bottomBar:DisplayObject;
    private var skipTurn:DisplayObject;
    private var leaveBattle:DisplayObject;

    private var alliesVision:Array<MovieClip>;
    private var enemiesVision:Array<MovieClip>;
    private var abilitiesVision:Array<MovieClip>;

    private var allyNames:Array<TextField>;
    private var allyHPs:Array<TextField>;
    private var allyManas:Array<TextField>;
    private var enemyNames:Array<TextField>;
    private var enemyHPs:Array<TextField>;
    private var enemyManas:Array<TextField>;

    private var shiftKey:Bool;

    //================================================================================
    // Levers - display the canges in the game model
    //================================================================================  

    public function changeUnitHP(target:Unit, dhp:Int, element:Element, crit:Bool, source:Source)
    public function changeUnitMana(target:Unit, dmana:Int, source:Source)
    public function changeUnitAlacrity(unit:Unit, dalac:Float, source:Source)
    public function castBuff(id:ID, duration:Int)
    public function redrawBuffs(target:Unit, buffs:Array<Buff>)
    public function unitMiss(target:UnitCoords, element:Element)
    public function die(unit:UnitCoords)

    //================================================================================
    // Input responses - some more visual stuff to make the game more responsive
    //================================================================================      

    public function selectAbility(num:Int)
    public function deselectAbility(num:Int)
    public function target(coords:UnitCoords)
    public function printWarning(text:String)
    }

    //================================================================================
    // Basic animations - abstract animations
    //================================================================================

    public function abilityIntro(target:UnitCoords, caster:UnitCoords, ability:{type:AbilityType, element:Element})
    {
        var callback:Dynamic = Controller.instance.useAbility;

        switch (ability.type)
        {
            case AbilityType.Bolt:
                animateBolt(target, caster, ability.element, callback);
            case AbilityType.Kick:
                animateKickIn(target, caster, callback);
            default:
                cleanAndCallback(callback);
        }
    }

    public function abilityOutro(target:UnitCoords, caster:UnitCoords, ability:{id:ID, type:AbilityType})
    {
        var callback:Dynamic = Controller.instance.useAbility;

        switch (ability.type)
        {
            case battle.enums.AbilityType.Kick:
                animateKickOut(caster, callback);
            case battle.enums.AbilityType.Spell:
                animateSpell(ability.id, target, callback);
            default:
                cleanAndCallback(callback);
        }
    }

    //================================================================================
    // Animation supply - concrete animations
    //================================================================================  

    private function animateBolt(target:UnitCoords, caster:UnitCoords, element:Element, callback:Dynamic)
    private function animateKickIn(target:UnitCoords, caster:UnitCoords, callback:Dynamic)
    private function animateKickOut(caster:UnitCoords, callback:Dynamic)
    private function animateSpell(abilityID:ID, target:UnitCoords, callback:Dynamic)

    private function cleanAndCallback(callback:Dynamic, ?animation:Null<MovieClip>)
    {
        if (animation != null)
            remove(animation);

        Reflect.callMethod(callback, callback, []);


    //================================================================================
    // Input handlers
    //================================================================================  

    private function keyUpHandler(e:KeyboardEvent)
    {
        trace("keyUp handled: " + e.keyCode);

        if (e.keyCode == 16)
            shiftKey = false;
    }

    private function keyHandler(e:KeyboardEvent)
    {
        trace("key handled: " + e.keyCode);

        if (e.keyCode == 16)
        {
            shiftKey = true;
        }
        else if (MathUtils.inRange(e.keyCode, 49, 57))
        {
            if (shiftKey)
                Controller.instance.printAbilityInfo(e.keyCode - 49);
            else if (Controller.instance.inputMode != InputMode.None)
                Controller.instance.choose(e.keyCode - 49);
        }
    }

    private function clickHandler(e:MouseEvent)
    {
        //...
    }

    //================================================================================
    // INIT + CONSTRUCTOR
    //================================================================================

    public function init(zone:Int, allies:Array<battle.Unit>, enemies:Array<battle.Unit>) 
    {
        bg = Assets.getBattleBG(zone);
        upperBar = new UpperBattleBar();
        bottomBar = new BottomBattleBar();
        skipTurn = new SkipTurn();
        leaveBattle = new LeaveBattle();

        alliesVision = [];
        enemiesVision = [];
        abilitiesVision = [];

        allyNames = [];
        allyHPs = [];
        allyManas = [];
        enemyNames = [];
        enemyHPs = [];
        enemyManas = [];

        for (ally in allies)
            alliesVision.push(Assets.getBattleUnit(ally.id));
        for (enemy in enemies)
            enemiesVision.push(Assets.getBattleUnit(enemy.id));
        for (i in 0...10)
            abilitiesVision.push(Assets.getBattleAbility(allies[0].wheel.get(i).id));

        //...drawing the screen...

        shiftKey = false;

        stage.addEventListener(KeyboardEvent.KEY_DOWN, keyHandler);
        stage.addEventListener(KeyboardEvent.KEY_UP, keyUpHandler);
        stage.addEventListener(MouseEvent.CLICK, clickHandler);
    }

    public function new()
    {
        super();
    }

    //================================================================================
    // Inline map - used to incapsulate concrete values
    //================================================================================

    private static inline function abilityX(i:Int):Float
    private static inline function unitX(pos:Int, team:battle.enums.Team):Float
    private static inline function unitY(pos:Int):Float
    private static inline function unitInfoX(team:Team, type:String)
    private static inline function unitInfoY(pos:Int):Float

    //================================================================================
    // Graphic utils
    //================================================================================

    private inline function getUnitBounds(pos:Int, team:Team):Rectangle
    private function addTextfield(targetArray:Array<TextField>, text:String, font:String, size:Int, color:Null<Int> = null, bold:Null<Bool> = null)
    private function playOnce(mc:MovieClip, x:Float, y:Float, ?onComplete:Null<Dynamic>, ?onCompleteParams:Null<Array<Dynamic>>)

    //================================================================================
    // Other
    //================================================================================

    private function getUnit(coords:UnitCoords):MovieClip
    {
        var array:Array<MovieClip> = (coords.team == battle.enums.Team.Left)? alliesVision : enemiesVision;
        return array[coords.pos];
    }
}

Обратите внимание, что все спрайты, актеры, графические объекты, хранящиеся в разных переменных, чтобы сделать работу с графикой. Хотя это хороший подход?

Единица класса. Представляет игрока, союзников и врагов.

typedef ParameterList = {
    var name:String;
    var hp:Int;
    var mana:Int;
    var wheel:Array<ID>;

    var strength:Int;
    var flow:Int;
    var intellect:Int;
}

class Unit
{

    public var id(default, null):ID;
    public var name(default, null):String;
    public var team(default, null):Team;
    public var position(default, null):Int;

    public var wheel(default, null):Wheel;
    public var hpPool(default, null):Pool;
    public var manaPool(default, null):Pool;
    public var alacrityPool(default, null):FloatPool;
    public var buffQueue(default, null):BuffQueue;

    public var strength:Int;
    public var flow:Int;
    public var intellect:Int;

    public var damageIn:Linear;
    public var damageOut:Linear;
    public var healIn:Linear;
    public var healOut:Linear;
    public var critChance:Linear;
    public var critDamage:Linear;

    public function useAbility(target:Unit, abilityNum:Int)
    {
        Assert.assert(MathUtils.inRange(abilityNum, 0, 7));
        wheel.get(abilityNum).use(target, this);
    }

    public function tick()
    {
        wheel.tick();
        buffQueue.tick();
    }

    public function isStunned():Bool
    {
        return false;
    }

    public function new(id:ID, team:Team, position:Int, ?parameters:Null<ParameterList>) 
    {
        Assert.assert(position >= 0 && position <= 2);

        if (parameters == null)
            parameters = XMLUtils.parseUnit(id);

        this.id = id;
        this.name = parameters.name;
        this.team = team;
        this.position = position;

        this.wheel = new Wheel(parameters.wheel, 8);
        this.hpPool = new Pool(parameters.hp, parameters.hp);
        this.manaPool = new Pool(parameters.mana, parameters.mana);
        this.alacrityPool = new FloatPool(0, 100);
        this.buffQueue = new BuffQueue();

        this.strength = parameters.strength;
        this.flow = parameters.flow;
        this.intellect = parameters.intellect;

        this.damageIn = new Linear(1, 0);
        this.damageOut = new Linear(1, 0);
        this.healIn = new Linear(1, 0);
        this.healOut = new Linear(1, 0);
    }

    public function figureRelation(unit:Unit):UnitType
    {
        if (team != unit.team)
            return UnitType.Enemy;
        else if (position == unit.position)
            return UnitType.Self;
        else
            return UnitType.Ally;
    }

    public inline function checkManacost(abilityNum:Int):Bool
    {
        return manaPool.value >= wheel.get(abilityNum).manacost;
    }

}

Способность класса

class Ability 
{

    public var id(default, null):ID;
    public var name(default, null):String;
    public var description(default, null):String;
    public var type(default, null):AbilityType;
    public var possibleTarget(default, null):AbilityTarget;
    public var element(default, null):Element;

    private var _cooldown:Countdown;
    public var cooldown(get, null):Int;
    public var manacost(default, null):Int;

    public function use(target:Unit, caster:Unit)
    {
        Abilities.useAbility(id, target, caster, element); 
        Controller.instance.changeUnitMana(caster, caster, -manacost, battle.enums.Source.God);
        _cooldown.value = _cooldown.keyValue;
    }

    public function tick()
    {
        if (checkOnCooldown())
            _cooldown.value--;
    }

    public function new(id:ID) 
    {
        this.id = id;
        if (!checkEmpty())
        {
            this.name = XMLUtils.parseAbility(id, "name", "");
            this.description = XMLUtils.parseAbility(id, "description", "");
            this.type = XMLUtils.parseAbility(id, "type", AbilityType.Bolt);
            this._cooldown = new Countdown(XMLUtils.parseAbility(id, "delay", 0), XMLUtils.parseAbility(id, "cooldown", 0));
            this.manacost = XMLUtils.parseAbility(id, "manacost", 0);
            this.possibleTarget = XMLUtils.parseAbility(id, "target", AbilityTarget.All);
            this.element = XMLUtils.parseAbility(id, "element", Element.Physical);
        }
    }

    //================================================================================
    // Checkers
    //================================================================================

    public inline function checkOnCooldown():Bool
    {
        return _cooldown.value > 0;
    }

    public inline function checkEmpty():Bool
    {
        return id == ID.EmptyAbility || id == ID.LockAbility;
    }

    public inline function checkValidity(target:Unit, caster:Unit):Bool
    {
        var relation:battle.enums.UnitType = caster.figureRelation(target);
        switch (possibleTarget)
        {
            case battle.enums.AbilityTarget.Enemy:
                return relation == battle.enums.UnitType.Enemy;
            case battle.enums.AbilityTarget.Allied:
                return relation == battle.enums.UnitType.Ally || relation == battle.enums.UnitType.Self;
            case battle.enums.AbilityTarget.Self:
                return relation == battle.enums.UnitType.Self;
            case battle.enums.AbilityTarget.All:
                return true;
            default:
                return false;
        }
    }

    //================================================================================
    // Getters
    //================================================================================

    function get_cooldown():Int
    {
        return _cooldown.value;
    }

}

Способности класса (не путать с возможностью) класс содержит методы, которые описывают, что умения, такие как:

private static function highVoltage(target:Unit, caster:Unit, element:Element)
    {
        var damage:Int = 40 + caster.intellect * 10;

        Controller.instance.changeUnitHP(target, caster, -damage, element, Source.Ability);
        Controller.instance.castBuff(ID.BuffLgConductivity, target, caster, 2);
    }

Этот класс содержит один публичный метод, называемый юзабилити, который выбирает одного из частных методов на основании идентификатора возможности.

Единиц класс содержит методы для каждого блока (кроме игрока), которые анализируют текущую игровую ситуацию и вернуть способность бот хочет использовать и эту возможность.

Подобные классовые умения, этот класс содержит один общедоступный способ, который выбирает собственный метод для вызова на основе идентификатора устройства.

Баффы и баффы классов очень похожи на способности и способности классов, просто немного другая механика (тикают вместо использования)


Мне нужно переделать этот код, чтобы сделать его проще и адаптировать его для возможных будущих изменений, в том числе:

  1. Автоцестернам

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

    Это важнейшая особенность и главная причина, чтобы задать вопрос здесь, поэтому, пожалуйста, обратите внимание на этот момент.

  2. Сбор данных

    Способности/класс выбрать/винрейты, самые популярные комбо и т. д. Я собираюсь сохранить его в файл.

  3. Мультиплеер

    Я никогда не погружался в эту тему, но когда-нибудь я хочу, чтобы реализовать простой режим 1х1 пвп. Тогда я, возможно, захотите обновить его до 3х3 бои. Я не хочу, чтобы переписать всю игру с нуля, поэтому любые советы, как избежать этого в будущем могут.

  4. Диалоги

    Игра прекращается, если условие выполняется и начинает играть диалогах. Условия могут различаться: очередь количество, % от максимального здоровья противника достиг, противник использует специальные способности. Разнообразие условий является основной проблемой, которая не позволяет мне сделать однозначный архитектурное решение.

Мне интересно, какие подходы можно применить, чтобы подготовить почву для этих четырех функций и сделать код лучше. Возможно, некоторые модели могут помочь с этим?

Любые вопросы приветствуются



Комментарии