project-topaz / topaz

Server emulator for FFXI
Other
159 stars 224 forks source link

Fix determining relative entity positions, angles, and conal attacks #1402

Closed ibm2431 closed 3 years ago

ibm2431 commented 3 years ago

⚠️ Highly recommended reading: http://wiki.project-topaz.com/Spatial-Orientation-and-Relative-Positions

^ No really, you don't stand a chance of understanding the changes in this PR until you have~!

Currently, everything relating to positions will encounter issues at Rotation 0 (due East). This all stems from one little lie told in the documentation for getangle:

*  Function: getAngle()
*  Purpose : Returns the angle between two entities relative to front
*  Example : player:getAngle(target)
*  Notes   : 0 degrees front; 180 behind

That is not what getAngle did at all.

What getangle was returning was the "world angle" between two entities - not a facing angle. It was also using a different scale than advertised. This leads to some problems: current_function

We also had places which weren't even using getangle - but instead subtracting two raw rotations from each other - which caused them to suffer from the "rotation overflow" problem above.

Entity A: Rotation 255 (East), Entity B: Rotation 1 (East)
255 - 1 = 254, which isn't less than 64

(Note: This was reported in https://github.com/DarkstarProject/darkstar/issues/6415)

Naturally, this is all very no bueno. So I made getangle into the more specific getWorldAngle, which serves the same functional purpose that getangle actually did. As a nice bonus I threw in, it now lets you optionally pass in a custom number of degrees to scale to, provided that they're a multiple of 4.

BOLD FOR EMPHASIS: You can now get the world angle between two entities on a custom scale. 4 = NESW, 8 adds NE, SE, SW, and SE. Increase for additional cardinal axis. This means you, Ballista Rooks, VWNMs, and Geomancer!

Afterwards I added getFacingAngle to do what the previous getangle advertised, and several positioning arc functions like isInfront, isBeside, and isBehind. Each of the positioning functions can take a custom angle (always 256 scaled), but will otherwise use a default of 64 (equivalent to 90 degrees on "360 scale").

So then came changes to conal attacks. Wikis claim that damage drops from the center line of the cone. So I wrote a new lua utility function to handle that. It takes a "minimum percentage" argument, which is, well, the minimum damage you'll take if you're hit with that breath attack at its farthest end. The closer you are to the damage line of the attack, the closer you'll get to taking the passed-in max damage.

I couldn't find any sources on how this "damage reduction if you're farther away" is calculated. I'm a skeptic, so at the moment I'm not convinced it's real. So I set all dragon breath attacks to have a minimum percentage of 90% because I'd rather damage be overtuned than undertuned! Normally in this circumstance I'd keep it at 100%, but I wanted to illustrate that the damage adjustment function works and how. Servers can set each breath's minimum percentages to their taste.

Due to what has been ingrained in all of us - "stand on the paws" - there were some questions to how these breaths worked. Why are we standing on the paws? Do we dodge damage? Do we reduce damage? Hopefully by now everyone knows the dangers of myths. @zach2good and @UynGH did a video test on retail, and they produced this one beautiful image which shows a lot. breath3 That's Nyu, who had hate, getting hit with Dragon Breath while almost 90 degrees ("360 scale") to Fafnir's right. You can see it targeting him in Zach's log, and Nyu being on fire from the animation. Also notice that Zach wasn't hit.

Dragon Breath can be used on someone so long as they're within the front 180 degrees ("360 scale") of the mob. Additional targets are based on the conal fan from the primary target - not the mob's face. This matches a TODO in targetfind's findWithinCone.

(There is a video of this, but I won't publicly link it here. You might be able to ask Zach on Discord if you don't believe me.)

With the help of a new additional utility function (relativeAngle), targets are now hit based on the primary target instead of where the mob is facing.

(Edit: See later comments; I reverted the above back to additional targets from the mob's face.)

This presented a problem for breath damage reduction, which will need to be solved later. It'd require a pretty massive restructuring of how mob skills work. When targets get hit with a skill, the lua script is executed individually on each one, with no methods of storing arbitrary data about what the skill did to others - like the primary target's world angle. So while the targets hit are correctly selected, the "center damage line" used for breath damage reduction is unfortunately still the mob's face. Keep this in mind when adjusting minimum damage percentages.

I affirm:

zach2good commented 3 years ago

image

ibm2431 commented 3 years ago

(feel free to post the video on the PR)

Hitting target at almost 90 degrees to the right ("360 scale"): https://www.youtube.com/watch?v=j8m3xFQXQFo&t=2m35s

Exact cone angle (two Dragon Breaths back-to-back): https://www.youtube.com/watch?v=j8m3xFQXQFo&t=15m52s

InoUno commented 3 years ago

Nice stuff! I also started looking into fixing conal attacks some time ago, since they are all kinds of wrong in DSP/topaz. I had to prioritize other things though, so I had to drop it for a while, but I did manage to gather a bunch of retail evidence before that. I'll gladly share my notes and findings, since you guys are looking into it now too.

Additional targets are based on the conal fan from the primary target - not the mob's face.

I'm not sure if this is different for each type of breath attack, or maybe it's a difference based on if it's a mob conal vs a player conal, but I tested this extensively with BLU spells, and found that the cone does have its center at the face of the caster -- not the primary target like you have concluded. I'd love to compare notes and go through the data together!

ibm2431 commented 3 years ago

Looking like we'll have to define and use flags to determine the center of a cone~

ibm2431 commented 3 years ago

After talking it over with Ino a bit (and seeing a very illuminating video), I've set conal centers back to be mob facing. They'll still hit off-face primary targets like in the Fafnir video, but additional targets will treat the mob's rotation as the center.

More Fafnir testing may be in the future.

ibm2431 commented 3 years ago

I wanted to open this as a Pull Request to have more eyes on checking it. But what I haven't mentioned until now is that the explicit purpose of this rework was initially to resolve an exploit in which players could cause HNM wyrms to deal almost no damage from breaths.

According to wikis, breath attacks are supposed to deal less damage depending on the target's distance from the center of the cone. These breaths were previously implemented like so:

    local angle = mob:getAngle(target)

    angle = mob:getRotPos() - angle
    dmgmod = dmgmod * ((128-math.abs(angle))/128)
    dmgmod = utils.clamp(dmgmod, 50, 1600)

Let's take a step though this, referencing the image on the old getangle above.

Remember that rotations start at 0 and wrap around back to zero at 255, going clockwise. So let's say that you're fighting Fafnir, and knowing this exploit, you cause Fafnir to face roughly due E (rotation zero). This can be precisely rotation zero, but for safety you can have it be a little south, so let's say rotation 1. If you then stand slightly north of due east, relative to Fafnir, you'll be at angle 255, since the return of getangle was absolutely world aligned instead of facing aligned.

Fafnir is facing 1, you're at 255. 1 - 255 = 254. Congratulations, you're 254 degrees away from Fafnir's damage source! damage = damage ((128 - 254)/128) damage = damage -0.98

Well, that's bad. Fafnir is going to be healing you for just about the full amount of damage it's trying to do!

We're "saved" by the clamp though: utils.clamp(dmgmod, 50, 1600)

So instead of outright healing you - and being a much more obvious bug - Dragon Breath instead does only 50 damage. I know players encourage Dragon Breaths instead of Spike Flails, but this isn't quite the reason why. 😉

Public server operators should have been informed that this was an exploit fix. If you're a public server operator who didn't know this comment was coming, get in touch with either zach2good or myself on Discord.