#include "character_moves.hpp"

#include "kungflowutils.hpp"
#include "defines.hpp"

#include "playstate.hpp"

unsigned int CharacterMove::_moveCounter = 0;

CharacterMove::CharacterMove(const PlayState *playState, const e_MoveType &moveType, AnimatedSprite *targetSprite) : playState(playState), targetSprite(targetSprite), moveType(moveType) {
  active = false;
  timeSinceLastActivation_ms = 1000000;
  //duration_ms = 1000;
  //cooldownTime_ms = 0;
}

CharacterMove::~CharacterMove() {
}


// walk

CharacterWalkMove::CharacterWalkMove(const PlayState *playState, AnimatedSprite *targetSprite, const WalkStats &stats) : CharacterMove(playState, e_MoveType_Walk, targetSprite) {
  SetStats(stats);
  walkDurationOverride_ms = 0;
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterWalkMove::~CharacterWalkMove() {
}

void CharacterWalkMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  float maxChangeMPS = 20.0f * stats.agilityFactor * _WALK_AGILITYFACTOR * _PHYSICSSCALE;
  //return CalculatePhysicsMovement(currentMovement, userInputMovement, maxChangeMPS, timeStep_ms);

  // move on the beat
  /*
  float musicPowerFactor = cos(playState->GetCurrentMusic()->GetBeatOffset() * 2 * pi) * 0.5f + 0.5f;
  musicPowerFactor *= 2.0f;
  float musicPowerInfluence = 0.3f;
  musicPowerFactor = musicPowerFactor * musicPowerInfluence + 1.0f * (1.0f - musicPowerInfluence);
  */
  //maxChangeMPS = 1000;

  resultingMovement = CalculatePhysicsMovement(currentMovement, targetMovement, maxChangeMPS, timeStep_ms);

  resultingZMovement = currentZMovement;
  ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  radian maxRadiansPerSecond = 2.0f * pi;
  resultingBodyAngle = GetCappedAngle(currentBodyAngle, userInputBodyAngle, timeStep_ms, maxRadiansPerSecond);
}

/*
Vector2 CharacterWalkMove::CalculateMovement(const Vector2 &currentMovement, const Vector2 &userInputMovement, unsigned int timeStep_ms) const {
  float maxChangeMPS = 20 * stats.agilityFactor * _PHYSICSSCALE;
  //return CalculatePhysicsMovement(currentMovement, userInputMovement, maxChangeMPS, timeStep_ms);
  return CalculatePhysicsMovement(currentMovement, targetMovement, maxChangeMPS, timeStep_ms);
}

radian CharacterWalkMove::CalculateBodyAngle(radian currentAngle, radian desiredBodyAngle, unsigned int timeStep_ms) const {
  radian maxRadiansPerSecond = 4.0f * pi;
  return GetCappedAngle(currentAngle, desiredBodyAngle, timeStep_ms, maxRadiansPerSecond);
}
*/

void CharacterWalkMove::SelectSpriteSequence(int directionID) const {
  unsigned int desiredSequenceIndex;
  if (targetMovement.GetLength() < 1.0f) desiredSequenceIndex = spriteSequence_Idle_IDs[directionID];
    else desiredSequenceIndex = spriteSequence_Walking_IDs[directionID];
  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

void CharacterWalkMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    AnimatedSpriteSequence idleSequence;
    idleSequence.push_back(AnimatedSpriteFrame(0, direction, 1000));
    unsigned int id = targetSprite->AddSequence(idleSequence);

    spriteSequence_Idle_IDs[direction] = id;


    AnimatedSpriteSequence walkSequence;
    walkSequence.push_back(AnimatedSpriteFrame(2, direction, 200));
    walkSequence.push_back(AnimatedSpriteFrame(0, direction, 100));
    walkSequence.push_back(AnimatedSpriteFrame(3, direction, 200));
    walkSequence.push_back(AnimatedSpriteFrame(0, direction, 100));
/*
    walkSequence.push_back(AnimatedSpriteFrame(2, direction, GetDuration_ms() / 3.0f));
    walkSequence.push_back(AnimatedSpriteFrame(0, direction, GetDuration_ms() / 6.0f));
    walkSequence.push_back(AnimatedSpriteFrame(3, direction, GetDuration_ms() / 3.0f));
    walkSequence.push_back(AnimatedSpriteFrame(0, direction, GetDuration_ms() / 6.0f));
*/

    id = targetSprite->AddSequence(walkSequence);

    spriteSequence_Walking_IDs[direction] = id;

  }

}


// jab

//CharacterJabMove::CharacterJabMove(AnimatedSprite *targetSprite) : CharacterMove(e_MoveType_Jab, targetSprite) {
//  InitSpriteSequences(targetSprite);
//  Reset();
//}

CharacterJabMove::CharacterJabMove(const PlayState *playState, AnimatedSprite *targetSprite, const JabStats &stats) : CharacterMove(playState, e_MoveType_Jab, targetSprite) {
  SetStats(stats);
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterJabMove::~CharacterJabMove() {
}

void CharacterJabMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  //float timeFactor = GetCurrentTimeFactor(); (float)GetTimeSinceLastActivation_ms() / (float)GetDuration_ms();
  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset

    float maxChangeMPS = 2.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    // pull back
    //resultingMovement = CalculatePhysicsMovement(currentMovement, -this->targetDirection * (400.0f / (float)stats.onsetDuration_ms), maxChangeMPS, timeStep_ms);
    // stand still
    resultingMovement = CalculatePhysicsMovement(currentMovement, Vector2(0), maxChangeMPS, timeStep_ms);

    resultingZMovement = currentZMovement;
    ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute

    float timeFactor = (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)stats.executeDuration_ms;
    int currentMovementFalloff_ms = 200;
    //float power = sin(-0.5f * pi + timeFactor * 2.0f * pi) * 0.5f + 0.5f;
    //float jabPower = sin(jabTimeFactor * pi);
    float power = 1.0f - timeFactor;
    power = Clamp(power, 0.0f, 1.0f);
    power = pow(power, 2.5f);
    power /= (float)stats.executeDuration_ms * 0.001f; // correct for duration -- we want to always cover the same distance regardless of duration
    //power -= 0.04f;
    //jabPower = jabPower * 0.8f + 0.2f;
    //if (id == 0) jabPower += (1.0f - jabTimeFactor) * 0.3f; // some extra starting boost for our hero
    resultingMovement = this->targetDirection * power * 2.5f * stats.distanceFactor * _PHYSICSSCALE;
    resultingMovement += this->startMovement * std::max(0.0f, (1.0f - (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)currentMovementFalloff_ms)); // start with current movement

    resultingZMovement = 0;

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + activeSustainDuration_ms) {

    // sustain

    if (GetHitSomething() == false) {
      float maxChangeMPS = 1.0f * _AGILITYFACTOR * _PHYSICSSCALE;
      resultingMovement = CalculatePhysicsMovement(currentMovement, userInputMovement, maxChangeMPS, timeStep_ms);
    } else {
      // 'recoil', moving back a little gives breathing space and makes quick followup stabs seem more powerful
      float maxChangeMPS = 5.0f * _AGILITYFACTOR * _PHYSICSSCALE;
      resultingMovement = CalculatePhysicsMovement(currentMovement, -this->targetDirection * 3.0f, maxChangeMPS, timeStep_ms);
    }

    resultingZMovement = currentZMovement;
    ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  }

  // todo: generalize so that other moves can also do this without c/p code
  //float maxHeightPerSecond = 6.0f;
  //float maxDiff = maxHeightPerSecond * (timeStep_ms * 0.001f);
  //resultingZMovement = Clamp(0, currentZMovement - maxDiff, currentZMovement + maxDiff);

  radian desiredJabAngle = this->targetDirection.GetAngle();
  radian maxRadiansPerSecond = 5.0f * pi;
  resultingBodyAngle = GetCappedAngle(currentBodyAngle, desiredJabAngle, timeStep_ms, maxRadiansPerSecond);
}

void CharacterJabMove::SelectSpriteSequence(int directionID) const {

  unsigned int desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset
    desiredSequenceIndex = spriteSequence_Onset_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute
    desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + activeSustainDuration_ms) {

    // sustain
    desiredSequenceIndex = spriteSequence_Sustain_IDs[directionID];

  }

  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

bool CharacterJabMove::IsLethal() const {
  if (GetTimeSinceLastActivation_ms() > stats.onsetDuration_ms && GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {
    return true;
  } else {
    return false;
  }
}

float CharacterJabMove::GetDamagePotentialFactor() const {
  if (!IsLethal()) return 0.0f;
  float timeFactor = (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)stats.executeDuration_ms;
  timeFactor = Clamp(timeFactor, 0.0f, 1.0f);
  float damageTimeFactorBegin = 1.0f - pow(timeFactor, 1.5f); // most damaging in the beginning
  float damageTimeFactorMiddle = sin(timeFactor * pi); // most damaging halfway (remember that with high timefactors, you hardly ever hit someone because you're almost standing still)
  float damageTimeFactor = damageTimeFactorBegin * 0.5f + damageTimeFactorMiddle * 0.5f;
  //printf("(%f) == %f\n", timeFactor, damageTimeFactor);
  return (damageTimeFactor * 0.7f + 0.3f);// * (this->distanceFactor * 0.9f + 0.1f);
}

void CharacterJabMove::Reset() {
  hitSomething = false;
  activeSustainDuration_ms = stats.failureSustainDuration_ms;
}

void CharacterJabMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    // onset
    AnimatedSpriteSequence onsetSequence;
    onsetSequence.push_back(AnimatedSpriteFrame(3, direction, 2000));
    unsigned int id = targetSprite->AddSequence(onsetSequence);

    spriteSequence_Onset_IDs[direction] = id;

    // execute
    AnimatedSpriteSequence executeSequence;
    executeSequence.push_back(AnimatedSpriteFrame(5, direction, 2000));
    id = targetSprite->AddSequence(executeSequence);

    spriteSequence_Execute_IDs[direction] = id;

    // sustain
    AnimatedSpriteSequence sustainSequence;
    sustainSequence.push_back(AnimatedSpriteFrame(2, direction, 2000));
    id = targetSprite->AddSequence(sustainSequence);

    spriteSequence_Sustain_IDs[direction] = id;

  }

}


// dash

CharacterDashMove::CharacterDashMove(const PlayState *playState, AnimatedSprite *targetSprite, const DashStats &stats) : CharacterMove(playState, e_MoveType_Dash, targetSprite) {
  SetStats(stats);
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterDashMove::~CharacterDashMove() {
}

void CharacterDashMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  //float timeFactor = GetCurrentTimeFactor(); (float)GetTimeSinceLastActivation_ms() / (float)GetDuration_ms();
  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset

    float maxChangeMPS = 10.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    resultingMovement = CalculatePhysicsMovement(currentMovement, Vector2(0), maxChangeMPS, timeStep_ms);

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + executeDuration_ms) {

    // execute

    int currentMovementFalloff_ms = 50;//160;
    //float timeFactor = (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)executeDuration_ms;
    //float power = sin(-0.5f * pi + timeFactor * 2.0f * pi) * 0.5f + 0.5f;
    //float power = sin(timeFactor * pi);
    //float power = 1.0f - timeFactor;
    //power = Clamp(power, 0.0f, 1.0f);
    //power = pow(power, 2.0f);
    //power = power * 0.8f + 0.2f;

    float timeFactor = (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)executeDuration_ms;
    //float intensityCurve = timeFactor;
    //float intensityCurve = sin(timeFactor * pi);
    float intensityCurve = sin((timeFactor - 0.5f) * pi) * 0.5f + 0.5f;

    resultingMovement = toTargetTranslation / (executeDuration_ms * 0.001f) * intensityCurve * 2.0f;
    //resultingMovement = this->targetDirection * power * 12.0f * _PHYSICSSCALE;
    resultingMovement += this->startMovement * std::max(0.0f, (1.0f - (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)currentMovementFalloff_ms)); // start with current movement

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + executeDuration_ms + stats.sustainDuration_ms) {

    // sustain

    float maxChangeMPS = 50.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    resultingMovement = CalculatePhysicsMovement(currentMovement, Vector2(0), maxChangeMPS, timeStep_ms);
    resultingMovement.NormalizeMax(10);

  }

  resultingZMovement = currentZMovement;

  radian maxRadiansPerSecond = 5.0f * pi;
  resultingBodyAngle = GetCappedAngle(currentBodyAngle, targetBodyAngle, timeStep_ms, maxRadiansPerSecond);
}

void CharacterDashMove::SelectSpriteSequence(int directionID) const {

  unsigned int desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset
    desiredSequenceIndex = spriteSequence_Onset_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + executeDuration_ms) {

    // execute
    desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + executeDuration_ms + stats.sustainDuration_ms) {

    // sustain
    desiredSequenceIndex = spriteSequence_Sustain_IDs[directionID];

  }

  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

bool CharacterDashMove::IsLethal() const {
  if (GetTimeSinceLastActivation_ms() > stats.onsetDuration_ms && GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + executeDuration_ms) {
    return true;
  } else {
    return false;
  }
}

float CharacterDashMove::GetDamagePotentialFactor() const {
  if (!IsLethal()) return 0.0f;
  float timeFactor = (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)executeDuration_ms;
  timeFactor = Clamp(timeFactor, 0.0f, 1.0f);
  return (0.5f + 0.5f * timeFactor) * stats.damageFactor; // also injure people hit along the way
  /*
  float damageTimeFactorBegin = 1.0f - pow(timeFactor, 1.5f); // most damaging in the beginning
  float damageTimeFactorMiddle = sin(timeFactor * pi); // most damaging halfway (remember that with high timefactors, you hardly ever hit someone because you're almost standing still)
  float damageTimeFactor = damageTimeFactorBegin * 0.5f + damageTimeFactorMiddle * 0.5f;
  //printf("(%f) == %f\n", timeFactor, damageTimeFactor);
  return (damageTimeFactor * 0.7f + 0.3f) * (this->powerFactor * 0.9f + 0.1f);
  */
}

void CharacterDashMove::Reset() {
  hitSomething = false;
  //effectiveStats = stats;
}

void CharacterDashMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    // onset
    AnimatedSpriteSequence onsetSequence;
    onsetSequence.push_back(AnimatedSpriteFrame(3, direction, 2000));
    unsigned int id = targetSprite->AddSequence(onsetSequence);

    spriteSequence_Onset_IDs[direction] = id;

    // execute
    AnimatedSpriteSequence executeSequence;
    executeSequence.push_back(AnimatedSpriteFrame(5, direction, 2000));
    id = targetSprite->AddSequence(executeSequence);

    spriteSequence_Execute_IDs[direction] = id;

    // sustain (stunned)
    AnimatedSpriteSequence sustainSequence;
    sustainSequence.push_back(AnimatedSpriteFrame(2, direction, 2000));
    id = targetSprite->AddSequence(sustainSequence);

    spriteSequence_Sustain_IDs[direction] = id;

  }

}


// block

CharacterBlockMove::CharacterBlockMove(const PlayState *playState, AnimatedSprite *targetSprite, const BlockStats &stats) : CharacterMove(playState, e_MoveType_Block, targetSprite) {
  SetStats(stats);
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterBlockMove::~CharacterBlockMove() {
}

void CharacterBlockMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  //float timeFactor = GetCurrentTimeFactor(); (float)GetTimeSinceLastActivation_ms() / (float)GetDuration_ms();
  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset

    float maxChangeMPS = 40.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    resultingMovement = CalculatePhysicsMovement(currentMovement, this->targetDirection, maxChangeMPS, timeStep_ms);

    resultingZMovement = currentZMovement;
    ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute

    float timeFactor = (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)stats.executeDuration_ms;
    int currentMovementFalloff_ms = 80;
    //float power = sin(-0.5f * pi + timeFactor * 2.0f * pi) * 0.5f + 0.5f;
    //float power = sin(timeFactor * pi);
    float power = 1.0f - timeFactor;
    //power = Clamp(power, 0.0f, 1.0f);
    //power = pow(power, 2.0f);
    //power = power * 0.8f + 0.2f; // maintain some movement @ outgoing
    power /= (float)stats.executeDuration_ms * 0.001f; // correct for duration -- we want to always cover the same distance regardless of duration
    resultingMovement = this->targetDirection * power * -1.5f * stats.distanceFactor * _PHYSICSSCALE;
    resultingMovement += this->startMovement * std::max(0.0f, (1.0f - (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)currentMovementFalloff_ms)); // start with current movement

    resultingZMovement = 0;

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms) {

    // sustain

    float maxChangeMPS = 10.0f * _AGILITYFACTOR * _PHYSICSSCALE; // can accel fast because we stabilized ourselves during the execute phase! or something like that!
    resultingMovement = CalculatePhysicsMovement(currentMovement, userInputMovement, maxChangeMPS, timeStep_ms);

    resultingZMovement = currentZMovement;
    ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  }

  //resultingZMovement = currentZMovement;

  radian maxRadiansPerSecond = 5.0f * pi;
  resultingBodyAngle = GetCappedAngle(currentBodyAngle, targetBodyAngle, timeStep_ms, maxRadiansPerSecond);
}

void CharacterBlockMove::SelectSpriteSequence(int directionID) const {

  unsigned int desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset
    desiredSequenceIndex = spriteSequence_Onset_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute
    desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms) {

    // sustain
    desiredSequenceIndex = spriteSequence_Sustain_IDs[directionID];

  }

  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

void CharacterBlockMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    // onset
    AnimatedSpriteSequence onsetSequence;
    onsetSequence.push_back(AnimatedSpriteFrame(2, direction, 2000));
    unsigned int id = targetSprite->AddSequence(onsetSequence);

    spriteSequence_Onset_IDs[direction] = id;

    // execute
    AnimatedSpriteSequence executeSequence;
    executeSequence.push_back(AnimatedSpriteFrame(3, direction, 2000)); // 2
    id = targetSprite->AddSequence(executeSequence);

    spriteSequence_Execute_IDs[direction] = id;

    // sustain (stunned)
    AnimatedSpriteSequence sustainSequence;
    sustainSequence.push_back(AnimatedSpriteFrame(0, direction, 2000));
    id = targetSprite->AddSequence(sustainSequence);

    spriteSequence_Sustain_IDs[direction] = id;

  }

}


// dodge

CharacterDodgeMove::CharacterDodgeMove(const PlayState *playState, AnimatedSprite *targetSprite, const DodgeStats &stats) : CharacterMove(playState, e_MoveType_Dodge, targetSprite) {
  SetStats(stats);
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterDodgeMove::~CharacterDodgeMove() {
}

void CharacterDodgeMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  //float timeFactor = GetCurrentTimeFactor(); (float)GetTimeSinceLastActivation_ms() / (float)GetDuration_ms();
  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset

    float maxChangeMPS = 1.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    resultingMovement = CalculatePhysicsMovement(currentMovement, this->targetDirection, maxChangeMPS, timeStep_ms);

    resultingZMovement = currentZMovement;
    ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute

    float timeFactor = (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)stats.executeDuration_ms;
    int currentMovementFalloff_ms = 160;
    //float power = sin(-0.5f * pi + timeFactor * 2.0f * pi) * 0.5f + 0.5f;
    //float power = sin(timeFactor * pi);
    float power = 1.0f - timeFactor;
    power = Clamp(power, 0.0f, 1.0f);
    power = pow(power, 2.0f);
    power = power * 0.9f + 0.1f; // maintain some movement @ outgoing
    power /= (float)stats.executeDuration_ms * 0.001f; // correct for duration -- we want to always cover the same distance regardless of duration
    resultingMovement = this->targetDirection * power * 2.2f * stats.distanceFactor * _PHYSICSSCALE;
    resultingMovement += this->startMovement * std::max(0.0f, (1.0f - (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)currentMovementFalloff_ms)); // start with current movement

    resultingZMovement = 0;

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms) {

    // sustain

    float maxChangeMPS = 15.0f * _AGILITYFACTOR * _PHYSICSSCALE; // can accel fast because we stabilized ourselves during the execute phase! or something like that!
    resultingMovement = CalculatePhysicsMovement(currentMovement, userInputMovement, maxChangeMPS, timeStep_ms);

    resultingZMovement = currentZMovement;
    ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  }

  //resultingZMovement = currentZMovement;

  radian maxRadiansPerSecond = 5.0f * pi;
  resultingBodyAngle = GetCappedAngle(currentBodyAngle, targetBodyAngle, timeStep_ms, maxRadiansPerSecond);
}

void CharacterDodgeMove::SelectSpriteSequence(int directionID) const {

  unsigned int desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset
    desiredSequenceIndex = spriteSequence_Onset_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute
    desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms) {

    // sustain
    desiredSequenceIndex = spriteSequence_Sustain_IDs[directionID];

  }

  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

void CharacterDodgeMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    // onset
    AnimatedSpriteSequence onsetSequence;
    onsetSequence.push_back(AnimatedSpriteFrame(3, direction, 2000));
    unsigned int id = targetSprite->AddSequence(onsetSequence);

    spriteSequence_Onset_IDs[direction] = id;

    // execute
    AnimatedSpriteSequence executeSequence;
    executeSequence.push_back(AnimatedSpriteFrame(2, direction, 2000));
    id = targetSprite->AddSequence(executeSequence);

    spriteSequence_Execute_IDs[direction] = id;

    // sustain (stunned)
    AnimatedSpriteSequence sustainSequence;
    sustainSequence.push_back(AnimatedSpriteFrame(0, direction, 2000));
    id = targetSprite->AddSequence(sustainSequence);

    spriteSequence_Sustain_IDs[direction] = id;

  }

}


// throw

CharacterThrowMove::CharacterThrowMove(const PlayState *playState, AnimatedSprite *targetSprite, const ThrowStats &stats) : CharacterMove(playState, e_MoveType_Throw, targetSprite), latestUserInputBodyAngle(0), hasThrown(false) {
  SetStats(stats);
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterThrowMove::~CharacterThrowMove() {
}

void CharacterThrowMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  latestUserInputMovement = userInputMovement;
  latestUserInputBodyAngle = userInputBodyAngle;

  //float timeFactor = GetCurrentTimeFactor(); (float)GetTimeSinceLastActivation_ms() / (float)GetDuration_ms();
  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset

    float maxChangeMPS = 10.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    // stand still
    resultingMovement = CalculatePhysicsMovement(currentMovement, Vector2(0), maxChangeMPS, timeStep_ms);

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute

    float timeFactor = (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)stats.executeDuration_ms;
    int currentMovementFalloff_ms = 40;
    //float power = sin(-0.5f * pi + timeFactor * 2.0f * pi) * 0.5f + 0.5f;
    //float power = 0.5f;
    //float power = sin(timeFactor * pi);
    float power = 1.0f - timeFactor;
    power = Clamp(power, 0.0f, 1.0f);
    power = pow(power, 1.6f);
    //power *= 1.5f;
    power *= 0.7f;
    //power = power * 0.8f + 0.2f;
    power /= (float)stats.executeDuration_ms * 0.001f; // correct for duration -- we want to always cover the same distance regardless of duration
    Vector2 adaptedTargetDirection = (-this->targetDirection + userInputMovement.GetNormalized(0) * 0.7f).GetNormalized(this->targetDirection);
    resultingMovement = adaptedTargetDirection * power * 1.0f * _PHYSICSSCALE;
    resultingMovement += this->startMovement * std::max(0.0f, (1.0f - (float)(GetTimeSinceLastActivation_ms() - stats.onsetDuration_ms) / (float)currentMovementFalloff_ms)); // start with current movement
/*
    //resultingMovement = 0;
    float maxChangeMPS = 40.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    // pull back
    resultingMovement = CalculatePhysicsMovement(currentMovement, -targetDirection, maxChangeMPS, timeStep_ms);
*/

    //playState->GetStage()->SpawnEnemy(e_EnemyType_Grenade, character->GetPosition() + shotDirection * 0.5f, character->GetZPosition() + character->GetHeight() * 0.5f, shotDirection * 12.0f, 0.0f);

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms) {

    // sustain

    //resultingMovement = 0;
    float maxChangeMPS = 40.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    // recover movement
    resultingMovement = CalculatePhysicsMovement(currentMovement, userInputMovement, maxChangeMPS, timeStep_ms);

  }

  resultingZMovement = currentZMovement;
  ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  radian maxRadiansPerSecond = 5.0f * pi;
  resultingBodyAngle = GetCappedAngle(currentBodyAngle, targetDirection.GetAngle(), timeStep_ms, maxRadiansPerSecond);
}

void CharacterThrowMove::SelectSpriteSequence(int directionID) const {

  unsigned int desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset
    desiredSequenceIndex = spriteSequence_Onset_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute
    desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms) {

    // sustain
    desiredSequenceIndex = spriteSequence_Sustain_IDs[directionID];

  }

  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

void CharacterThrowMove::OnStepTime() {
 if (!hasThrown && GetTimeSinceLastActivation_ms() >= stats.onsetDuration_ms + stats.executeDuration_ms * 0.2f) {
    sig_Throw(latestUserInputMovement, toTargetTranslation);
    hasThrown = true;
  }
}

void CharacterThrowMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    // onset
    AnimatedSpriteSequence onsetSequence;
    onsetSequence.push_back(AnimatedSpriteFrame(3, direction, 2000));
    unsigned int id = targetSprite->AddSequence(onsetSequence);

    spriteSequence_Onset_IDs[direction] = id;

    // execute
    AnimatedSpriteSequence executeSequence;
    executeSequence.push_back(AnimatedSpriteFrame(5, direction, 2000));
    id = targetSprite->AddSequence(executeSequence);

    spriteSequence_Execute_IDs[direction] = id;

    // sustain (stunned)
    AnimatedSpriteSequence sustainSequence;
    sustainSequence.push_back(AnimatedSpriteFrame(2, direction, 2000));
    id = targetSprite->AddSequence(sustainSequence);

    spriteSequence_Sustain_IDs[direction] = id;

  }

}


// jump

CharacterJumpMove::CharacterJumpMove(const PlayState *playState, AnimatedSprite *targetSprite, const JumpStats &stats) : CharacterMove(playState, e_MoveType_Jump, targetSprite) {
  SetStats(stats);
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterJumpMove::~CharacterJumpMove() {
}

void CharacterJumpMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  //float timeFactor = GetCurrentTimeFactor(); (float)GetTimeSinceLastActivation_ms() / (float)GetDuration_ms();
  if (GetTimeSinceLastActivation_ms() <= effectiveStats.onsetDuration_ms) {

    // onset

    float maxChangeMPS = 5.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    //resultingMovement = CalculatePhysicsMovement(currentMovement, this->targetDirection, maxChangeMPS, timeStep_ms);
    resultingMovement = CalculatePhysicsMovement(currentMovement, 0, maxChangeMPS, timeStep_ms);

    resultingZMovement = currentZMovement;
    ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  } else if (GetTimeSinceLastActivation_ms() <= effectiveStats.onsetDuration_ms + effectiveStats.executeDuration_ms) {

    // execute

    float timeFactor = (float)(GetTimeSinceLastActivation_ms() - effectiveStats.onsetDuration_ms) / (float)effectiveStats.executeDuration_ms;

    float power = 1.0f - timeFactor;
    power = Clamp(power, 0.0f, 1.0f);
    power = pow(power, 2.0f);
    power = power * 0.5f + 0.5f;

    Vector2 forcedMovement = this->targetDirection * power * effectiveStats.distanceFactor * 3.5f * _PHYSICSSCALE;
    forcedMovement += startMovement * 0.5f;

    // mid air steering, because f#ck you!
    //float maxChangeMPS = 10.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    //resultingMovement = forcedMovement; // no steering
    resultingMovement = forcedMovement * 0.4f + userInputMovement * 0.2f; // mild steering
    //resultingMovement = forcedMovement * 0.1f + userInputMovement * 0.7f; // agile
    //resultingMovement = CalculatePhysicsMovement(resultingMovement, userInputMovement * 0.5f + this->targetDirection * power * effectiveStats.distanceFactor * 3.5f * _PHYSICSSCALE * 0.5f + startMovement * 0.5f, maxChangeMPS, timeStep_ms);

    float jumpPower = 4.0f;//8.0f * (this->powerFactor * 0.2f + 0.8f);
    jumpPower /= (float)effectiveStats.executeDuration_ms * 0.001f; // correct for duration -- we want to always cover the same distance regardless of duration
    resultingZMovement = (1.0f - timeFactor * 2.0f) * jumpPower;
    //resultingZMovement = sin(timeFactor * pi) * jumpPower;

  } else if (GetTimeSinceLastActivation_ms() <= effectiveStats.onsetDuration_ms + effectiveStats.executeDuration_ms + effectiveStats.sustainDuration_ms) {

    // sustain

    float maxChangeMPS = 5.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    resultingMovement = CalculatePhysicsMovement(currentMovement, userInputMovement * 0.3f, maxChangeMPS, timeStep_ms);

    resultingZMovement = currentZMovement;
    ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  }

  radian maxRadiansPerSecond = 5.0f * pi;
  resultingBodyAngle = GetCappedAngle(currentBodyAngle, this->targetDirection.GetAngle(), timeStep_ms, maxRadiansPerSecond);
}

void CharacterJumpMove::SelectSpriteSequence(int directionID) const {

  unsigned int desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms) {

    // onset
    desiredSequenceIndex = spriteSequence_Onset_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms) {

    // execute
    desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.onsetDuration_ms + stats.executeDuration_ms + stats.sustainDuration_ms) {

    // sustain
    desiredSequenceIndex = spriteSequence_Sustain_IDs[directionID];

  }

  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

void CharacterJumpMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    // onset
    AnimatedSpriteSequence onsetSequence;
    onsetSequence.push_back(AnimatedSpriteFrame(3, direction, 2000));
    unsigned int id = targetSprite->AddSequence(onsetSequence);

    spriteSequence_Onset_IDs[direction] = id;

    // execute
    AnimatedSpriteSequence executeSequence;
    executeSequence.push_back(AnimatedSpriteFrame(2, direction, 2000));
    id = targetSprite->AddSequence(executeSequence);

    spriteSequence_Execute_IDs[direction] = id;

    // sustain (stunned)
    AnimatedSpriteSequence sustainSequence;
    sustainSequence.push_back(AnimatedSpriteFrame(0, direction, 2000));
    id = targetSprite->AddSequence(sustainSequence);

    spriteSequence_Sustain_IDs[direction] = id;

  }

}


// landing

CharacterLandingMove::CharacterLandingMove(const PlayState *playState, AnimatedSprite *targetSprite, const LandingStats &stats) : CharacterMove(playState, e_MoveType_Landing, targetSprite) {
  SetStats(stats);
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterLandingMove::~CharacterLandingMove() {
}

void CharacterLandingMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  if (GetTimeSinceLastActivation_ms() <= stats.executeDuration_ms * durationMultiplier) {

    // execute
    float timeFactor = (float)GetTimeSinceLastActivation_ms() / (stats.executeDuration_ms * durationMultiplier);
    float powerFactor = Clamp(1.0f - timeFactor, 0.0f, 1.0f);
    powerFactor = pow(powerFactor, 1.5f);
    powerFactor = powerFactor * 0.7f + 0.3f;
    //Vector2 desiredDirection = (this->targetDirection * 0.8f + userInputMovement.GetNormalized(0) * 0.2f).GetNormalized(0);
    //Vector2 desiredDirection = (_DEFAULTVECTOR.GetRotated(currentBodyAngle) * 0.3f + this->targetDirection * 0.3f).GetNormalized();
    Vector2 desiredMovement = this->targetDirection * 0.6f + userInputMovement * 0.2f;
    resultingMovement = desiredMovement * powerFactor * 5.0f * _PHYSICSSCALE; // 8

    /*
    float timeFactor = (float)GetTimeSinceLastActivation_ms() / (stats.executeDuration_ms * durationMultiplier);
    float powerFactor = 2.0f - timeFactor * 2.0f; // to get an average of 1.0f;
    Vector2 resultingTranslation = (translation / ((stats.executeDuration_ms * durationMultiplier) / (float)timeStep_ms)) * powerFactor;
    resultingMovement = resultingTranslation / ((float)timeStep_ms * 0.001f);
    */
  }

  resultingZMovement = currentZMovement;
  ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  resultingBodyAngle = this->targetDirection.GetAngle();
  //radian maxRadiansPerSecond = 5.0f * pi;
  //return GetCappedRotation(currentAngle, desiredMoveAngle, timeStep_ms, maxRadiansPerSecond);
}

void CharacterLandingMove::SelectSpriteSequence(int directionID) const {

  unsigned int desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  if (GetTimeSinceLastActivation_ms() <= stats.executeDuration_ms * durationMultiplier) {

    // slide
    desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  }

  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

void CharacterLandingMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    // execute
    AnimatedSpriteSequence executeSequence;
    executeSequence.push_back(AnimatedSpriteFrame(3, direction, 2000)); // slide to a halt
    unsigned int id = targetSprite->AddSequence(executeSequence);

    spriteSequence_Execute_IDs[direction] = id;

  }

}


// hit

CharacterHitMove::CharacterHitMove(const PlayState *playState, AnimatedSprite *targetSprite, const HitStats &stats) : CharacterMove(playState, e_MoveType_Hit, targetSprite) {
  SetStats(stats);
  InitSpriteSequences(targetSprite);
  Reset();
}

CharacterHitMove::~CharacterHitMove() {
}

void CharacterHitMove::Calculate(const Vector2 &currentMovement, float currentZMovement, const radian currentBodyAngle, const Vector2 &userInputMovement, radian userInputBodyAngle, unsigned int timeStep_ms, Vector2 &resultingMovement, float &resultingZMovement, radian &resultingBodyAngle) const {

  if (GetTimeSinceLastActivation_ms() <= stats.executeDuration_ms) {

    // execute
    float timeFactor = (float)GetTimeSinceLastActivation_ms() / stats.executeDuration_ms;
    float powerFactor = 2.0f - timeFactor * 2.0f; // to get an average of 1.0f;
    Vector2 resultingTranslation = (translation / (stats.executeDuration_ms / (float)timeStep_ms)) * powerFactor;
    resultingMovement = resultingTranslation / ((float)timeStep_ms * 0.001f);
    //resultingMovement.NormalizeMax(50);

  } else if (GetTimeSinceLastActivation_ms() <= stats.executeDuration_ms + stats.sustainDuration_ms * sustainMultiplier) {

    // sustain
    float maxChangeMPS = 1.0f * _AGILITYFACTOR * _PHYSICSSCALE;
    resultingMovement = CalculatePhysicsMovement(currentMovement, userInputMovement, maxChangeMPS, timeStep_ms);

  }

  resultingZMovement = currentZMovement;
  ApplyGravity(resultingZMovement, 10.0f * _GRAVITYFACTOR, playState->GetPhysicsTimeStep_ms());

  resultingBodyAngle = (-this->translation.GetNormalized(_DEFAULTVECTOR)).GetAngle();
  //radian maxRadiansPerSecond = 5.0f * pi;
  //return GetCappedRotation(currentAngle, desiredMoveAngle, timeStep_ms, maxRadiansPerSecond);
}

void CharacterHitMove::SelectSpriteSequence(int directionID) const {

  unsigned int desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  if (GetTimeSinceLastActivation_ms() <= stats.executeDuration_ms) {

    // pain
    desiredSequenceIndex = spriteSequence_Execute_IDs[directionID];

  } else if (GetTimeSinceLastActivation_ms() <= stats.executeDuration_ms + stats.sustainDuration_ms * sustainMultiplier) {

    // sustain
    desiredSequenceIndex = spriteSequence_Sustain_IDs[directionID];

  }

  if (targetSprite->GetSelectedSequenceIndex() != desiredSequenceIndex) targetSprite->SelectSequence(desiredSequenceIndex);
}

void CharacterHitMove::InitSpriteSequences(AnimatedSprite *targetSprite) {

  for (unsigned int direction = 0; direction < 8; direction++) {

    // execute
    AnimatedSpriteSequence executeSequence;
    executeSequence.push_back(AnimatedSpriteFrame(7, direction, 2000));
    unsigned int id = targetSprite->AddSequence(executeSequence);

    spriteSequence_Execute_IDs[direction] = id;

    // sustain (stunned)
    AnimatedSpriteSequence sustainSequence;
    sustainSequence.push_back(AnimatedSpriteFrame(3, direction, 2000));
    id = targetSprite->AddSequence(sustainSequence);

    spriteSequence_Sustain_IDs[direction] = id;

  }

}
