Multi-Hit Adventures

Let me tell you about my adventures with multi-hit attacks. It was possibly the biggest nightmare I've had in programming in the longest time, not from getting errors or anything, but because of the game logic falling apart or misbehaving in ways I couldn't understand for a while.

To make things easier on myself, multi-hit attacks/skills in Witness to Unity aren’t so much “one attack with multiple hits” as much as “use the same attack multiple times” — battle functions and all. This would become half the problem.

An action is separated out into the following processes:

  • update_phase4_step1: Select the current action. For allies this is done in the UI, for enemies it’s done using their AI. (For a given definition of “AI”.) For allies, the target is also selected here.
  • Prepare certain other variables, resetting some from previous turns. Some of these are defaults made redundant by WTU’s CTB system.
  • update_phase4_step2: Depending on the action type, certain different processes are run. Enemy target determination is now run here. Damage calculation is run and queued up. Display action name.
  • update_phase4_step3: “Prepare” the action — do casting animations. If certain conditions inhibit actions — like being stunned — these are handled instead, and it skips to the final step.
  • update_phase4_step4: Act on the action — do the attacking animation.
    • If any of the targets will reflect the attack, display the reflect shield animation and flag the target.
    • If any of the targets have been flagged, remove them from the target list, and add the attacker.
    • For each of the targets, display the attack animation. This will include the attacker if they were added.
  • update_phase4_step5: For each of the targets, display battle damage and apply battle damage. Update the HUD. Reset variables for end of an active battler’s turn. Reset variables for everyone. End of turn.

The way Witness to Unity does a multi-hit skill is to essentially add a target to the target list multiple times. The actual multi-hit part of this system was easy. The nightmares started once I realised there were problems in determining if a target was hit.

The actual damage processing was fine. But there’s only one @missed variable, which means it only ever kept the last state it was in — overriding for each instance of a hit — and this variable is used to determine the animations. So in the case of a hit sequence that goes [hit, miss, miss], while the damage behaved as expected, the battle animations would all use the “miss” version, because it was the last state it recorded.

So, alright, let's create a “hit sequence” array that’s stored for a turn’s sequence of attacks. Each attack appends to the list, and it’s only wiped at the end of an actual turn — as opposed to the end of an action, since anything there gets altered/reset each time a “hit” is calculated. This behaves as expected! Each animation in a sequence of hits matches the actual underlying hit/miss processed!

... and then there were reflected attacks.

Reflection is a little funky. As can be seen from the “act on the action” step, the effects are actually handled as three separate loops. Reflect effects are displayed, the targets are shuffled about, and the attack continues onwards. This breaks completely with the new “hit sequence” system.

Take a hit sequence scenario like... [miss, hit, miss]. The attack is only reflected once, and the two misses are discarded, but because the first hit was originally a miss, that’s what the reflected hit starts with. The “hit sequence” counts from the start of the vanilla sequence. So once again, the miss animation state is used, even though the attack connected.

  • I tried making misses still displayed, but that didn’t seem to work. Probably because just like @missed, @reflected_skill is also a single variable that changed on each “hit”.
  • I tried handling targets according to individual steps in the target list, instead of removing them entirely, but that didn’t seem to work either, probably due to modifying an array as I was looping over it. Not really sure what was happening here.
  • I tried modifying the “hit sequence” to change values as needed, or even flag them as :reflected, but that didn’t work because the reflect animation is run before the attack animation sequence. When I tried putting the reflect and attack animations in the same loop, the pacing fell apart.

It’s about here I was slowly losing my mind.

As it turns out, Persona and Shin Megami Tensei only roll hit-rate and critical rate for multi-hit skills once, on the first hit. I always presumed hit-rate handled variation in hit count — so an attack that hit 2-5 times was because of its hit-rate stat. But no, this variation is actually handled separately, and when a multi-hit attack hits or misses, every hit will be the same.

And so, after hours of madness, I undid a lot of this code, and simply altered when @missed is altered. Instead of being reset per action — thus per hit — it’s now only reset (to nil) at the end of a turn. In turn, @missed is not updated if the value is true/false, maintaining its value set on first action for every hit.

Presently, critical hits are still rolled per hit, as distinct from SMT/Persona. This is partly out of personal preference, but also because critical hits aren't tied to any visual distinction other than damage popup, which is queued on damage processing.

Now that every action in a turn will (relative to each target) be either all hits or all misses, this solved the issue with animations not matching @missed, and meant reflected skills were consistent too. Like SMT/Persona, I modified the reflection handling to also only reflect once (relative to each target), which resolved issues with how removing reflecting-targets from the target list affected reflected-hit-counts.

All this said, in the end, it reveals an interesting fact when it comes to programming that one maybe needs to remember more often. Sometimes, if logic or a design is giving you no end of trouble, the better move is to design around it than try to create endless bandaids to fix troublesome scenarios.