Garou: Mark of the Wolves AutoHitdef

Started by Bannana, March 08, 2021, 02:17:25 AM

Previous topic - Next topic

Bannana

This will be split into three posts to maintain clarity
Part I - INTRODUCTION AND METHODOLOGY
Part II - EXPLORING THE SYSTEM.HITDEF 
Part III - PROPERLY FORMATTING THE HITDEF


Part I - INTRODUCTION AND METHODOLOGY

A majority (if not all, but I'm not going to make that argument) of games have universal pausetimes, hittimes, slidetimes, etc. for all normals and most specials (barring weird things like fireballs and supers). Garou's is incredibly simple, but is highly important for correct combo timings because all frame advantages, especially breaks, align with the pause/hit/slidetime. I dunno if anyone is looking to make more MotW characters, or even do stylistic edits in the manner of Kn (totally worth it), but this would make both of those much faster.

I personally hate the hitdef. It's inefficient because, in an efficient program, all of the valuable information concerning damage, scaling, hittime, pausetime, etc would be backend values that are called upon by the hitdef; therefore, a lot of time is wasted trudging through the cns to edit states, and I thought it would be better, and more efficient, to assign all of these functions to a file that only consists of less than 500 lines that need to be edited; moreover, because no hitdef is ever active on the first frame, so we don't have to deal with theoretical 1 tick delay, it makes perfect sense to just assign these to a helper in general.

So, in Gato+ I decided to diminish the actual amount of physical editing you have to do in any file but the AutoHitdef file,
Spoiler: leading to this (click to show)
[Statedef 200]
type = S
movetype = A
physics = S
ctrl = 0
velset = 0, 0
anim = 200
poweradd = 0
sprpriority = 2

[State 200, play voice]
type = playSnd
trigger1 = time = 0
value = 2, 0+random%2
channel = 0

[State 200, play se]
type = playSnd
trigger1 = animElem = 2
value = 1, 3
channel = 1

[State HitDef]
type = HitDef
trigger1 = animelemtime(helper(222222),var(0))=0
attr = S,NA        ;SCA,NA,SA,HA,NP,SP,HP,NT,ST,HT
hitflag = MAF        ;HLAFD+-
guardflag = M         ;HLA
animtype = medium          ;Light, Medium, Hard, Back, Up, DiagUp
priority = 3, Hit
damage = floor((helper(222222),var(1))*fvar(30)),floor((helper(222222),var(7))*fvar(30))
pausetime = (helper(222222),var(2)),(helper(222222),var(3))
sparkno = -1
guard.sparkno = -1
hitsound = S0, (helper(222222),var(15))+random%2
guardsound = S1, 0
ground.type = high      ;Low, Trip, None
ground.slidetime = (helper(222222),var(4))
guard.slidetime = (helper(222222),var(4))
ground.hittime = (helper(222222),var(5))
guard.hittime = (helper(222222),var(5))
guard.ctrltime = (helper(222222),var(5))
air.hittime = (helper(222222),var(5))
ground.velocity = (-2.29473946*fvar(9))-fvar(11),0
air.velocity = -2.204769373,-6.6174449*fvar(10)
airguard.velocity = -2.204769373*fvar(9),-6.6174449*fvar(10)

getpower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))
givepower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))

[State Explod hit]
type = Explod
trigger1 = movehit
anim = 6000
ID = 6000
pos = (helper(222222),var(10)),(helper(222222),var(11))
postype = P1  ;P2, Front, Back, Left, Right
scale = .5,.5
sprpriority = 2
ownpal = 1
ignorehitpause = 1
persistent=0

[State Explod hit]
type = Explod
trigger1 = moveguarded
anim = 6100
ID = 6100
pos = (helper(222222),var(10)),(helper(222222),var(11))
postype = P1  ;P2, Front, Back, Left, Right
scale = .15,.15
sprpriority = 2
ownpal = 1
ignorehitpause = 1
persistent=0

[State 200, idle]
type = stateTypeSet
trigger1 = animelemtime(helper(222222),var(6))=0
movetype = I

[State 200, end of state]
type = ChangeState
trigger1 = AnimTime = 0
value = 0
ctrl = 1

As far as I'm concerned, an attack state should consist of two sections:
Spoiler: Mandatory front end, e.g. sound clips on startup that are specific to each move (click to show)
[State 200, play voice]
type = playSnd
trigger1 = time = 0
value = 2, 0+random%2
channel = 0

[State 200, play se]
type = playSnd
trigger1 = animElem = 2
value = 1, 3
channel = 1
Spoiler: Absolute backened that should not have to be touched aside from particular specials and supers (click to show)
[State HitDef]
type = HitDef
trigger1 = animelemtime(helper(222222),var(0))=0
attr = S,NA        ;SCA,NA,SA,HA,NP,SP,HP,NT,ST,HT
hitflag = MAF        ;HLAFD+-
guardflag = M         ;HLA
animtype = medium          ;Light, Medium, Hard, Back, Up, DiagUp
priority = 3, Hit
damage = floor((helper(222222),var(1))*fvar(30)),floor((helper(222222),var(7))*fvar(30))
pausetime = (helper(222222),var(2)),(helper(222222),var(3))
sparkno = -1
guard.sparkno = -1
hitsound = S0, (helper(222222),var(15))+random%2
guardsound = S1, 0
ground.type = high      ;Low, Trip, None
ground.slidetime = (helper(222222),var(4))
guard.slidetime = (helper(222222),var(4))
ground.hittime = (helper(222222),var(5))
guard.hittime = (helper(222222),var(5))
guard.ctrltime = (helper(222222),var(5))
air.hittime = (helper(222222),var(5))
ground.velocity = (-2.29473946*fvar(9))-fvar(11),0
air.velocity = -2.204769373,-6.6174449*fvar(10)
airguard.velocity = -2.204769373*fvar(9),-6.6174449*fvar(10)

getpower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))
givepower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))

[State Explod hit]
type = Explod
trigger1 = movehit
anim = 6000
ID = 6000
pos = (helper(222222),var(10)),(helper(222222),var(11))
postype = P1  ;P2, Front, Back, Left, Right
scale = .5,.5
sprpriority = 2
ownpal = 1
ignorehitpause = 1
persistent=0

[State Explod hit]
type = Explod
trigger1 = moveguarded
anim = 6100
ID = 6100
pos = (helper(222222),var(10)),(helper(222222),var(11))
postype = P1  ;P2, Front, Back, Left, Right
scale = .15,.15
sprpriority = 2
ownpal = 1
ignorehitpause = 1
persistent=0

[State 200, idle]
type = stateTypeSet
trigger1 = animelemtime(helper(222222),var(6))=0
movetype = I

[State 200, end of state]
type = ChangeState
trigger1 = AnimTime = 0
value = 0
ctrl = 1
N.B. I use explods here, so if you were using hitsparks in the hitdef then you would just put "(helper(222222),var(10)),(helper(222222),var(11))" there for the x and y pos.


So, how does this work?

essentially, in a backend file we will make a list of all values that, because they never change, should be determined ahead of time, and then define whether those values are frontend or backend, the difference being that the backend are always calculations based on frontend values.
Spoiler: After doing this, I determined my list should be as so (click to show)

Animelem          var(0)
   Idle          var(6) = var(0)+2
Damage          var(1)
Guard Damage       var(7)
Pause time       var(2)
   p2 pause time       var(3) = var(2)-1
   Slide time       var(4) = var(2)+(var(2)/2)
   Hit time          var(5) = var(4)+2

RAW POWER VALUE       fvar(0)
>>>>>provided as raw value
   e.g. 66537
   >>>>>THUS
      x/4194.305 = power corrected per 1000
            e.g.
            4194305/4194.305 = 1000
            (1 = 0 so subtract 1 from your inital result)
            >>>>>(though numbers might be so big it doesn't matter)
         >>>>>formula in terms of MUGEN
            floor((x - 1)/4194.305)

POWER ON HIT
Severity modification   fvar(2)
   formula         fvar(1) = floor(((fvar(0) - 1)/4194.305)*fvar(2))

POWER ON BLOCK
Severity modification   fvar(4)
   formula         fvar(3) = floor(((fvar(0) - 1)/4194.305)*fvar(2))
   >>>>>difference between a guard and a hit?
      x*2 - x
      >>>>>UNLESS light b/c X appears to be the root value && SNK uses
         x - x

   >>>>>Some musings on special values for power gain:
   >>>>>light?
      x*6
      >>>>>heavy?
         x*7   

         >>>>>difference between special raw && hit is equivalent to
            >>>>>light?
               x*2
            >>>>>heavy?
               x*2 + x

Spark X          var(10)
Spark Y          var(11)

Hitsound         var(15)

extra pausetime      var(20)

For each extra hit      var(50)-(59)

And to explain the idea of how the frontend and backend works in practice, take my top two: animelem and idle. In Garou I determined that the average time an idle would occur would generally be 2-3 animelems after the active frame, so my backend calculation for var(6) is as follows
; Idle is ALWAYS set two frames after an active
[State idle set]
type = VarSet
trigger1 = var(0)
v = 6    ;fv = 10
value = var(0)+2
persistent=1
ignorehitpause=1

This sort of backend caluclation makes hittime in Garou easy to work with because all hittimes in Garou are based upon the root p1 pausetime
; pause/hit/slide time notes
; THESE TIMES ARE ABSOLUTELY UNIVERSAL, NOTHING HAS TO BE CHANGED ASIDE FROM SPECIALS
[State p2 pause]
type = VarSet
trigger1 = var(2)
v = 3    ;fv = 10
value = var(2)-1
persistent=1
ignorehitpause=1

[State slide time]
type = VarSet
trigger1 = var(2)
v = 4    ;fv = 10
value = var(2)+(var(2)/2)
persistent=1
ignorehitpause=1

[State hittime time]
type = VarSet
trigger1 = var(4)
v = 5    ;fv = 10
value = var(4)+2
persistent=1
ignorehitpause=1

Thus, a light punch pausetime of 8 will return p2 pausetime of 7, slidetime of 12, and hittime of 14, all of which are accurate to source.

A note on severity and power gain
Prior to revealing the code we need to go over how power works in Garou.

In Garou there is a root power value, 66537 (or, when corrected, 15), that I found is compounded by degrees of severity, so that each move will be allocated a certain level of severity based on movetype, strength, whiffing, hitting, etc.
You can see in my musings above that I thought quite a bit about how the patterns point to a possible methodology for assigning power to moves, and what I decided to do was take this raw value, 66537, set it to fvar(0), and apply two sets of calculations to it:
; Calculation of raw power value
[State raw calculation]
type = VarSet
trigger1 = 1
fv = 1    ;fv = 10
value = floor(((fvar(0) - 1)/4194.305)*fvar(2))
ignorehitpause=1
persistent=1

; Calculation of guard power value
[State guard calculation]
type = VarSet
trigger1 = 1
fv = 3    ;fv = 10
value = floor(((fvar(0) - 1)/4194.305)*fvar(4))
ignorehitpause=1
persistent=1


The severity modifiers here are fvar(2) and fvar(4), which, like p1 pausetime earlier, are the front end that we will edit manually, and the resulting values will be plugged into the hitdef automatically, giving correct power gain values.
With that, let's move on to Part II, where I'll explain how the auto-hitdef file works.

Bannana

#1
Part II - EXPLORING THE SYSTEM.HITDEF
Download the system.hitdef file and open it up in notepad/fighter factory/whatever. Go to line 70, which shows an example of the main type of string for normals
trigger1 = root,stateno = 200 && root,animelemtime(1)=0
trigger1 = var(0):=4 && var(1):=4 && var(7):=0 && var(2):=8 && fvar(2):=1 && fvar(4):=1 && var(10):=46 && var(11):=-69 && var(15)=0


If you've been following so far, or even already looked through the file, each of these variable assignments represents a frontend value. They're explained in the previous part, but I will explain all of them directly here

var(0) = animelem, this is the active frame of your animation (the one with the red clsn)
var(1) = damage, this is either your raw damage, to be corrected later, or the fully corrected version, whatever method you prefer
var(7) = guard damage, follows the same methodology as damage
var(2) = p1 pausetime, this is the only hittime value you need to edit, the rest is done automatically through the backend
fvar(2) = severity modifier on hit
fvar(4) = severity modifier on block
var(10) = spark pos x
var(11) = spark pos y
var(15) = hitsound

What you might have noticed is that I took all of the hitdef values that can be assigned to a variable (i.e. intergers and floats) in MUGEN and had them assigned here. This means that within about 200 lines or so you can assign all normals without having to go through your giant CNS files, saving a great deal of time!

HOWEVER, for a two hit move there is a problem! There are some issues I came up with attempting to rewrite var(0) midway through the state and recalling it as trigger2, so, since the range of 50-59 was free I decided to allocate multihits up to 10 with those.
Take note of this on line 153:
trigger1 = root,stateno = 215 && root,animelemtime(5)=0 ; *2-pt
trigger1 = var(50):=6 && var(1):=6 && var(7):=0 && var(2):=8 && fvar(2):=1 && fvar(4):=1 && var(10):=40 && var(11):=-86 && var(15)=0


Next, specials, because they give power on whiff, follow a similar pattern, but we remove the two severity modifier fvars.
Go to line 166 (and line 198 for the two hit moves):
trigger1 = root,stateno = 1000 && root,animelemtime(1)=0
trigger1 = var(0):=8 && var(1):=10 && var(7):=2 && var(2):=12 && var(10):=60 && var(11):=-63 && var(15)=12


severity for these moves is applied for each move specifically because the power gain values are different for each.
On line 207 this set of assignments shows how it works:
; These cover specials that lie outside of standard hitdef qualities
[State severity determination]
type = Null
trigger1 = root,movetype != A
trigger1 = fvar(2):=0
; throw
trigger2 = (root,stateno = 800)
trigger2 = fvar(2):=3
; light specials whiff
trigger3 = root,time<=4
trigger3 = (root,stateno = 1000) || (root,stateno = 1100) || (root,stateno = 1200)
trigger3 = fvar(2):=6
; light specials hit
trigger4 = root,time>4
trigger4 = (root,stateno = 1000) || (root,stateno = 1100) || (root,stateno = 1200)
trigger4 = fvar(2):=2
; heavy specials whiff
trigger5 = root,time<=4
trigger5 = (root,stateno = 1050) || (root,stateno = 1100) || (root,stateno = 1250)
trigger5 = fvar(2):=7
; heavy specials hit
trigger6 = root,time>4
trigger6 = (root,stateno = 1050) || (root,stateno = 1100) || (root,stateno = 1250)
trigger6 = fvar(2):=3


And for the guard values, go to line 259:
; Guard constant
[State guard determination]
type = Null
trigger1 = root,movetype != A
trigger1 = fvar(4):=0
; light specials
trigger2 = (root,stateno = 1000) || (root,stateno = 1100) || (root,stateno = 1200)
trigger2 = fvar(4):=1
; heavy specials
trigger3 = (root,stateno = 1050) || (root,stateno = 1100) || (root,stateno = 1250)
trigger3 = fvar(4):=2


Much of this should be incredibly straightforward, especially with normals, so what should be difficult is dealing with specific situations with special moves, and sometimes that means you might not be able to use the autohitdef and would be better just making a manual hitdef.
That being said, in Part III I'll explain how this all comes together into the generic hitdef.

Bannana

#2
Part III - PROPERLY FORMATTING THE HITDEF

Having defined all our values, we now need to get the hitdef to recall them. This will be the easiest part and will mostly come down to copy-pasting certain sections into the hitdef.

So here's the hitdef from before, but I've bolded the areas that you can automate and italicized those you can't:

type = HitDef
trigger1 = animelemtime(helper(222222),var(0))=0
attr = S,NA        ;SCA,NA,SA,HA,NP,SP,HP,NT,ST,HT
hitflag = MAF        ;HLAFD+-
guardflag = M         ;HLA
animtype = medium          ;Light, Medium, Hard, Back, Up, DiagUp
priority = 3, Hit

damage = floor((helper(222222),var(1))*fvar(30)),floor((helper(222222),var(7))*fvar(30))
pausetime = (helper(222222),var(2)),(helper(222222),var(3))
sparkno = -1
guard.sparkno = -1
hitsound = S0, (helper(222222),var(15))+random%2
guardsound = S1, 0

ground.type = high      ;Low, Trip, None
ground.slidetime = (helper(222222),var(4))
guard.slidetime = (helper(222222),var(4))
ground.hittime = (helper(222222),var(5))
guard.hittime = (helper(222222),var(5))
guard.ctrltime = (helper(222222),var(5))
air.hittime = (helper(222222),var(5))

ground.velocity = (-2.29473946*fvar(9))-fvar(11),0
air.velocity = -2.204769373,-6.6174449*fvar(10)
airguard.velocity = -2.204769373*fvar(9),-6.6174449*fvar(10)


getpower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))
givepower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))


essentially, this hitdef is complete, all you have to do is follow the variables defined in the system.hitdef. Certain things such as the attributes that don't use ints or floats have to be manually changed, and as you can see I did not automate any velocities because those are highly specific to most moves.
For example, take note of how Gato's close D move works. Not only is it a two hit move, but one with drastically different attributes and velocities between hitdefs.
[State HitDef]
type = HitDef
trigger1 = animelemtime(helper(222222),var(0))>=0
attr = S,NA        ;SCA,NA,SA,HA,NP,SP,HP,NT,ST,HT
hitflag = MAF        ;HLAFD+-
guardflag = M         ;HLA
animtype = hard          ;Light, Medium, Hard, Back, Up, DiagUp
priority = 4, Hit
damage = floor((helper(222222),var(1))*fvar(30)),floor((helper(222222),var(7))*fvar(30))
pausetime = (helper(222222),var(2)),(helper(222222),var(3))
sparkno = -1
guard.sparkno = -1
hitsound = S0, (helper(222222),var(15))+random%2
guardsound = S1, 1
ground.type = low      ;Low, Trip, None
ground.slidetime = (helper(222222),var(4))
guard.slidetime = (helper(222222),var(4))
ground.hittime = (helper(222222),var(5))
guard.hittime = (helper(222222),var(5))
guard.ctrltime = (helper(222222),var(5))
air.hittime = (helper(222222),var(5))
ground.velocity = cond(animelemtime(helper(222222),var(0))=0,(-.5),(-2.29473946*fvar(9)-fvar(11))),0
air.velocity = -2.204769373,-6.6174449*fvar(10)
airguard.velocity = -2.204769373*fvar(9),-6.6174449*fvar(10)

getpower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))
givepower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))

[State HitDef]
type = HitDef
trigger1 = animelemtime(helper(222222),var(50))>=0
attr = S,NA        ;SCA,NA,SA,HA,NP,SP,HP,NT,ST,HT
hitflag = MAF        ;HLAFD+-
guardflag = M         ;HLA
animtype = hard          ;Light, Medium, Hard, Back, Up, DiagUp
priority = 4, Hit
damage = floor((helper(222222),var(1))*fvar(30)),floor((helper(222222),var(7))*fvar(30))
pausetime = (helper(222222),var(2)),(helper(222222),var(3))
sparkno = -1
guard.sparkno = -1
hitsound = S0, (helper(222222),var(15))+random%2
guardsound = S1, 1
ground.type = high      ;Low, Trip, None
ground.slidetime = (helper(222222),var(4))
guard.slidetime = (helper(222222),var(4))
ground.hittime = (helper(222222),var(5))
guard.hittime = (helper(222222),var(5))
guard.ctrltime = (helper(222222),var(5))
air.hittime = (helper(222222),var(5))
ground.velocity = cond(animelemtime(helper(222222),var(0))=0,(-.5),(-2.29473946*fvar(9)-fvar(11))),0
air.velocity = -2.204769373,-6.6174449*fvar(10)
airguard.velocity = -2.204769373*fvar(9),-6.6174449*fvar(10)

getpower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))
givepower = floor((helper(222222),fvar(1))),floor((helper(222222),fvar(1))-(helper(222222),fvar(3)))


So, with a little patience, and a tiny bit of work, you'll have consistent, easily editable hitdefs according to a Garou standard. And, realistically, with a little work on the backend you could modify this for any other kind of game!

If you have any questions, don't hesitate to ask!

Archimonde

#3
Useful for anyone who feel lazy to add more hitdef sctrl but take much more time to adjust these value and timing stuff. And you must know you also need to add at least 1 more hitdef when your character time is zero( mean the beginning of the state )when you want to create the hitdef from the beginning of the state. as reading helper value will cause your character delayed in 1 tick so your character autohitdef can't be used at the beginning of the state after the state transition(changestate, selfstate), unless you have to set these hitdef value from the statedef -1 by using the := operator and set these hitdef value instantly so that autohitdef can work at the beginning of the state.
------Tremble Mortal and Despair. Doom has come to this world------

Bannana

Correct, which is why I think this is best used for normals because they always have a proper startup time, and are usually built according to the same rules/data. Generally I would not use an autohitdef for anything specific, such as a special attack and would instead opt to make a unique hitdef.

Archimonde

Beside, you also need to change hitdef attr too, cause not every state can use the same attr like thay autohitdef and the attr is fixed, not variable like the rest.
------Tremble Mortal and Despair. Doom has come to this world------