fire-emblem-working-title icon indicating copy to clipboard operation
fire-emblem-working-title copied to clipboard

Define a top-level public-friendly API for our damage calculation logic

Open ajhyndman opened this issue 7 years ago • 7 comments

I'm thinking it will just export a single function, probably:

type HeroInstance = // ...

type Result = // ...

declare module 'fire-emblem-heroes-calculator' {
  declare calculateResult(initiator: HeroInstance, defender: HeroInstance): Result;
};

ajhyndman avatar Apr 19 '17 13:04 ajhyndman

Hmm, this API also needs support for buffs.

We could pass optional "supporting hero" instances, as further arguments, and then just scrape allies for any and all support skills and assume all of them apply. I kind of like that solution. If you don't want a hero to be granting his support skill, you probably shouldn't have the skill equipped, or the supporting hero selected.

Maybe that's too specific to our app though. Maybe this calculator module should just accept raw numbers. It could be proving-grounds's responsibility to translate the supporting units' skills into buff values.

ajhyndman avatar Apr 19 '17 14:04 ajhyndman

Proposal A

type MergeLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
type Rarity = 1 | 2 | 3 | 4 | 5;
type Stat = 'hp' | 'atk' | 'spd' | 'def' | 'res';

type HeroConfiguration = {
  +bane?: ?Stat;
  +boon?: ?Stat;
  +mergeLevel?: ?MergeLevel;
  +name: string;
  +rarity: Rarity;
  +skills: {
    +WEAPON?: ?string;
    +ASSIST?: ?string;
    +SPECIAL?: ?string;
    +PASSIVE_A?: ?string;
    +PASSIVE_B?: ?string;
    +PASSIVE_C?: ?string;
    +SEAL?: ?string;
  };
};

type HeroState = {
  +currentHp?: ?number;
  +currentCooldown?: ?number;
  +buffs?: {
    [stat: Stat]?: {
      persistent?: ?number;
      proximity?: ?number;
    };
  };
};

type Turn = {
  damage: number;
  specialDamage: number;
  numAttacks: number;
  // this can be extended in the future to include, e.g.
  // - life gain
  // - pre-battle damage
  // - post-battle damage
};

declare const calculateResult: ({
  attacker: {
    config: HeroConfiguration;
    state: HeroState;
  };
  defender: {
    config: HeroConfiguration;
    state: HeroState;
  };
}): {
  attacker: {
    state: HeroState;
    turn: Turn;
  };
  defender: {
    state: HeroState;
    turn: Turn;
  };
};

ajhyndman avatar Apr 19 '17 15:04 ajhyndman

The only thing I don't like about the above is that it doesn't preserve enough information in its output to allow us to write a log like this:

image

ajhyndman avatar Apr 19 '17 15:04 ajhyndman

I'll think about this in more detail later, but I suspect that anyone who uses our combat logic will want a bunch of relevant helpers:

GetStat / stats. GetDefaultInstance, getSpecialCooldown, getInheritableSkills

AlmostMatt avatar Apr 20 '17 01:04 AlmostMatt

I'm kind of liking the idea of a timeline-based approach. It's noteworthy that the below proposal is not sufficient information to draw the number of attacks that what would be permissible by speed, if the unit didn't die first (which is what the game's UI does).

Proposal B

type MergeLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
type Rarity = 1 | 2 | 3 | 4 | 5;
type Stat = 'hp' | 'atk' | 'spd' | 'def' | 'res';

type HeroBuild = {
  +bane?: ?Stat;
  +boon?: ?Stat;
  +mergeLevel?: ?MergeLevel;
  +name: string;
  +rarity: Rarity;
  +skills: {
    +WEAPON?: ?string;
    +ASSIST?: ?string;
    +SPECIAL?: ?string;
    +PASSIVE_A?: ?string;
    +PASSIVE_B?: ?string;
    +PASSIVE_C?: ?string;
    +SEAL?: ?string;
  };
};

type HeroState = {
  +currentHp?: ?number;
  +currentCooldown?: ?number;
  +buffs?: {
    [stat: Stat]?: {
      persistent?: ?number;
      proximity?: ?number;
    };
  };
};

type HeroInstance = {
  build: HeroBuild;
  state: HeroState;
};

type Multiplier = {
  type: 'ADVANTAGE' | 'EFFECTIVE';
  value: number;
  // source: ???
};

type Event = {
  type: 'ATTACK',
  target: 'ATTACKER' | 'DEFENDER',
  multipliers: Array<Multiplier>
  damage: number;
} | {
  type: 'PRE-BATTLE',
  target: 'ATTACKER' | 'DEFENDER',
  damage: number;
  source: string; // skill name
} | {
  type: 'POST-BATTLE',
  target: 'ATTACKER' | 'DEFENDER',
  damage: number;
  source: string; // skill name
} | {
  type: 'HEAL',
  target: 'ATTACKER' | 'DEFENDER',
  amount: number;
  source: string; // skill name
};

declare const calculateResult: ({
  attacker: HeroInstance;
  defender: HeroInstance;
}): {
  attackerState: HeroState;
  defenderState: HeroState;
  timeline: Array<Event>;
};

ajhyndman avatar May 07 '17 04:05 ajhyndman

Specials can also multiple damage (aegis/glimmer), add a flat amount of damage (bonfire), reduce def/res, or do something unique (miracle). These could be attack modifiers or they could be SPECIAL events with extraDamage, damageMitigated

Buffs and Debuffs would be another type of action (seal def, etc)

This should be a type { config: HeroConfiguration; state: HeroState; }

buffs/debuffs are persistent, I don't really want to call a spur a buff. Also it's possible to have a buff and debuff to the same stat at once, so a debuff is not the same as a negative buff.

AlmostMatt avatar May 07 '17 06:05 AlmostMatt

I updated Proposal B to change Configuration to Build and named the collection of both to Instance.

ajhyndman avatar May 07 '17 09:05 ajhyndman