5
\$\begingroup\$

NOTE: this question isn't as long as it appears to be. I added the comments to the code only to answer some possible questions that may appear.

I'm making an active time battle strategy. Its mechanics are very similar to the ones introduced in Final Fantasy, but I can't remember which one in the series was the closest to my game. There is also a flash game, called Sonny, that is 99.9% similar to the game of mine.

The goal of the game is to kill entire enemy team. Every unit has an alacrity pool, its regeneration speed depends on unit's agility aka. flow. Whenever this pool is full, the unit is allowed to cast one spell from his wheel.

The player's turn consists of two stages: choosing ability (choosing) and choosing the ability's target (targeting).

Battle sketch

I'm using a modified version of MVC, where Controller sends requests to the Model, recieves answers with data and then sends this data to the Vision. I think this approach is too rigid, too unflexible, but that's one of the reasons I'm here. That's how it's done now:

Controller layout

The lever block consists of methods that modify model in a general ways (add/substract hp/mana, cast/dispell buff etc.) and are accessible from any part of the project.

The triggering block consists of functions that handle user input (not raw, primary processing is located in the Vision).

For example, if user presses the digit key, the code in the Vision (see below) handles this input and then calls the respective Controller method choose(), deciding (on its own!) which ability the player wants to choose (I'm not sure if it is a good approach to leave this type of logic in the Vision rather than in the Controller).

The useAbility block contains the useAbility() method and its auxiliary methods. Note that choose() and target() are called only when the player makes turn, but useAbility() is called whenever any unit does it.

The fact that useAbility() consists of three stages (and thus is called three times to avoid creating three different methods) is conditioned by the way the ability usage is animated. For example, when the unit kicks the target, he rushes to it, then the target recieves damage and then the unit that kicked it returns to his initial position. Different types of abilities are animated differently, but the vision-model-vision sequence remains the same.

To be able to work with the same target, caster and ability across all iterations of useAbility() method, I store this values as Controller's properties.

If the player makes turn, setUA() is called inside the target() method, else it is called inside one of the model's methods.

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;
    }
}

Then we come to the Model:

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 = [];
    }

}

Finally, there is a Vision, but I don't think it's necessary to post all its code as it's primarily consists of visual programming, so I'll just leave there a general layout (important methods are fully listed):

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];
    }
}

Note that all the sprites, actors, graphical objects are stored in different variables to make the work with graphic easier. Is it a good approach, though?

Unit class. Represents player, allies and enemies.

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;
    }

}

Ability class

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;
    }

}

Abilities class (not to be confused with Ability class) contains methods that describe what the ability does, such as that:

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);
    }

This class contains one public method called useAbility that chooses one of private methods based on the ability ID.

Units class contains the methods for each unit (except player) that analyze the current game situation and return the ability the bot wants to use and the target of this ability.

Similar to Abilities class, this class contains one public method that chooses which private method to call based on the unit ID.

Buff and Buffs classes are very similar to Ability and Abilities classes, just have a bit different mechanic (ticking instead of using)


I need to rework this code to make it simpler and to adapt it for the possible future changes including:

  1. Autotesting

    As I continue to develop my game, I'll need to balance it, so I want to have a possibility to launch series of bot vs. bot matches without graphic output just to gather statistics.

    It is the most important feature and the biggest reason to ask a question here, so please pay attention to this point.

  2. Data collection

    Ability/Class pick/win rates, most popular combos etc. I'm gonna save it to a file.

  3. Multiplayer

    I have never really dived into this subject, but someday I'll want to implement a simple 1x1 PvP mode. Then I may want to upgrade it up to 3x3 fights. I don't want to rewrite all the game from the scratch so any advices how to avoid this in the future are welcome.

  4. Dialogues

    The game stops if the condition is met and starts to play dialogue boxes. The conditions may vary: turn number, % of enemy's hp reached, enemy uses special ability. The diversity of the conditions is the main problem that doesn't allow me to make an obvious architectural decision.

I wonder what approaches can I apply to pave the way for these four features and make the code better overall. Maybe some patterns could help with this?

Any questions are welcome

\$\endgroup\$
  • \$\begingroup\$ Looks a bit like Final Fantasy X. \$\endgroup\$ – Mast Mar 2 '18 at 12:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.