ATB strategy MVC architecture refactoring

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
5
down vote

favorite













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


//================================================================================
// 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







share|improve this question





















  • Looks a bit like Final Fantasy X.
    – Mast
    Mar 2 at 12:08
















up vote
5
down vote

favorite













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


//================================================================================
// 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







share|improve this question





















  • Looks a bit like Final Fantasy X.
    – Mast
    Mar 2 at 12:08












up vote
5
down vote

favorite









up vote
5
down vote

favorite












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


//================================================================================
// 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







share|improve this question














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


//================================================================================
// 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









share|improve this question












share|improve this question




share|improve this question








edited Mar 4 at 9:34
























asked Mar 2 at 11:28









Gulvan

263




263











  • Looks a bit like Final Fantasy X.
    – Mast
    Mar 2 at 12:08
















  • Looks a bit like Final Fantasy X.
    – Mast
    Mar 2 at 12:08















Looks a bit like Final Fantasy X.
– Mast
Mar 2 at 12:08




Looks a bit like Final Fantasy X.
– Mast
Mar 2 at 12:08















active

oldest

votes











Your Answer




StackExchange.ifUsing("editor", function ()
return StackExchange.using("mathjaxEditing", function ()
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
);
);
, "mathjax-editing");

StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");

StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);

else
createEditor();

);

function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
convertImagesToLinks: false,
noModals: false,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);



);








 

draft saved


draft discarded


















StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f188670%2fatb-strategy-mvc-architecture-refactoring%23new-answer', 'question_page');

);

Post as a guest



































active

oldest

votes













active

oldest

votes









active

oldest

votes






active

oldest

votes










 

draft saved


draft discarded


























 


draft saved


draft discarded














StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f188670%2fatb-strategy-mvc-architecture-refactoring%23new-answer', 'question_page');

);

Post as a guest













































































Popular posts from this blog

Greedy Best First Search implementation in Rust

Function to Return a JSON Like Objects Using VBA Collections and Arrays

C++11 CLH Lock Implementation