NOTE: I'm using the Ikemen go version of add004, on 0.96, these might be ikemen specific but if you experience this behavior on mugen, this may help. you will have to translate it to mugen cns if you’re on the mugen version
To download a prebuilt file with example characters, use this link: https://drive.google.com/file/d/1cDHE6JHCGLpR_Ekb1rnxfa3rwNWsPEfl/view?usp=sharing
Ok so after 8 months of using add004 for ikemen and understanding the ins and outs, the current vanilla version has quite a few flaws, and hopefully my post will assist users in optimizing it for better usage. I've now basically modified it enough on my end where it responds way better and reacts as it should. If you're big into fighters like me then one of the most important things is responsiveness and timing. By default, the timing of when you can do actions (like tag in or tag out) is not that great, and tag cancel actions don't respond well at all depending on the move. Below I’ll go through some fixes that solves all this:
One has to do with the amount of time you go from state 190190 (exiting state) to state 190192(standby). The problem is, the interval (amount of time) in the main loop you can call for a character switch is shorter than your actual going to standby state. So, what happens (as I still see in some peoples’ videos and games) is if you switch and spam the switch button quick enough, your exiting partner won’t have enough time to exit and it'll force whoever is coming up to come in and "land" at the edge of the screen instead of where your partner's position was supposed to be. The run/jump out cycle is way too slow in conjunction with the tag in/out interval. So, in state 190190, to your liking, in "#;sys::partner_run-vel2" change the vel x to something higher, for me I put 12.6. But take note, you'll have to calculate everything else then and time it perfectly, these are the numbers I use. They all must be perfectly timed well.
Another problem is when the round starts, there is a small bug where if player 2 is near the edge of the side your p1 partners exited during the first seconds of the game, they will automatically turn to that edge randomly for a quick second. This is because in "#;sys::player_pos_y_outscreen1" in state 190190, your partner's presence suddenly triggers p2 to turn since they are near that position again for a few frames. This only happens at the start of a round since it SETS your partners to pos y of 0, triggering the sudden enemy p2 turn to the edge. to fix this I put "if TimeElapsed > 200" before it which avoids this all together. In Mugen you can just spawn an invisible explod for 200 or so frames and trigger it when numexplod(whatever number) equals zero.
Another problem is once you adjust the timing, it messes a bit with how player 2's ability to tag or call for an assist is in the beginning. What happens is p2 gets to call for an assist or switch in the beginning of the round quicker, to fix this I also put the same timeelapsed >200 trigger in the beginning of the #;sys::partner_goto-standby1 section, since this only happens as the round starts. Still in that section, the time it takes to go to state 190191 is 1 second, I changed mine to half a second. Instead of time?59, I put time >29.
In the section #;sys::partner_goto-standby2 #;;<<- (158F) interval / charge-time ; I also changed "time >= 158" to "time>=29". a HUGE change, this is because the default interval when you can switch in and out or call an assist is so short, we're adjusting it accordingly so it doesn’t bug out and make your partner land on the edge of the screen (if switched quick enough).
And finally, the biggest part I changed, is how var changes happen. Making the player have to set var(48) to 11 BEFORE doing a tag cancel is counterproductive. I just separated them. There are a lot of problems with assist interval counter resetting even if it didn’t execute. Now they look like as follows:
ignorehitpause if var(48)=11 { mapset{map:"i_Assist_Interval"; value: 100} mapset{map:"time"; value: 1} }
ignorehitpause if var(48)=21 { mapset{map:"i_Assist_Interval"; value: 100} mapset{map:"cancelexit";value:1}} #ORIGINALLY 30
ignorehitpause if var(48)=12 { mapset{map:"i_Assist_Interval"; value: 120} }
if you are using my custom hyper cancel/tag code, just adjust accordingly and literally make it like the chunk of tag cancel code, but instead tirrger it when stateno = [3000,4000].
Notice how I adjusted the interval as well; this is because we’ve adjusted timing and also, I find this delay in timing personally for me works best. anywhere from 80 and above work better. Feel free to adjust lower but you WILL run into switching to the wrong character issues if switching too fast or characters ending on the edge of the screen. I have all the varsets all separate, it’s easier to keep track this way. instead of just var48 trigger and var48=12.
Again, we want the timer to run correctly and make sure that timer resets correctly if partner exits so you don't get sometimes buggy switches (which happens by default, not often, but sometimes). That’s why I have another mapset I put when var48 is set to 11 and 21. Then I added the below:
ignorehitpause if map(i_Assist_Interval)=0 { mapset{map:"time"; value: 0} }
ignorehitpause if map(time)=1 &&
(playerid(var(15)),target,incustomstate=1 ||
playerid(var(15)),target,gethitvar(isBound)=1 ||
playerid(var(15)),hitdefattr = SCA, NT ) {
mapset{map:"i_Assist_Interval"; value: 0} }
Personally, for me, if you are grappling, I don't want to just switch while they are in a grapple move, so this prevents a grapple to cancel out of.
and then it proceeds to start the next if statement. Please feel free to remove any playerid(var(15)),numtarget triggers there, because in my case I wanted characters to be able to tag cancel if they hit someone with a projectile. Move contact does not count projectiles. Numtarget is buggy if not properly managed (I haven’t posted the code here since it might be too much). Anyway......
This should prevent both those things from happening. What ends up happening is it won’t reset the interval back to 120 (at least in my case the number I set to) again. Assisting in and out should be way smoother now.
Then I made changes to the triggers of how the varsets are made because we want to separate a tagout and a tag cancel, we don’t want to have to set var48 to 11 before canceling to set 12, this just brings a slew of bugs. So I modified it to this (in #;>>sys::tag_mode_cmd_change):
#;; change [v48=11] ; 交替
#;>>sys::tag_mode_cmd_change
ignorehitpause if map(i_Assist_Interval)=0 && map(supertimer)=0 &&
roundstate=2 && numpartner && var(22)=4 && !var(48) && map(i_Assist_Interval)=0 && map(supertimer)=0 &&
(playerid(var(15)),statetype != A && (playerid(var(15)),stateno!=[45,51] || playerid(var(15)),movecontact != 0)) &&
(playerid(var(15)),movetype = I && playerid(var(15)),ctrl = 1 && playerid(var(15)),statetype != A && playerid(var(15)),movetype != A && playerid(var(15)),movetype != H ){
ignorehitpause if var(15)!=var(49) && playeridexist(var(49)) {
ignorehitpause if (playerid(var(49)),stateno=190192) && (playeridexist(var(15))) && (playerid(var(15)),stateno!=[2999,3999]) { #; if in hyper cancel, ignore p1 idle status if P3 so P3 can hyper cancel
##;;--- player-controlled ;; 操作交替
ignorehitpause if ((root,command="shift_fwd") || (root,command="shift_back")) {
ignorehitpause if var(51)<1 && var(28)>200 && (var(7)&1024) {
var(48):=11;
}}
##;;--- ai
if var(51)>0 && var(28)>200 && (var(7)&1024) {
if playerid(var(15)),sysfvar(4)<1 && (random%50=0) && playerid(var(15)),life<playerid(var(49)),life {
var(48):=11;
}}
##;;--- forced change (not-alive, stand-by) ;; 強制交替(終止、待機中)
if playeridexist(var(15)) {
if (playerid(var(15)),alive=0 && playerid(var(15)),stateno=5150) || (playerid(var(15)),stateno=[190191,190192]) {
var(48):=11;
}}
}}}
and for var48=21 (tag cancel), important note, you must add the AI code from var48=11 to this now since they are seperate and so cpu can use these functions :
#;; "Switch-Canceling"
ignorehitpause if map(i_Assist_Interval)=0 && map(supertimer)=0 && var(15)!=var(49) && playeridexist(var(49)) {
ignorehitpause if playerid(var(49)),stateno=190192 && power>=500 {
ignorehitpause if playerid(var(15)),sysfvar(4)<1 && playerid(var(15)),alive && playerid(var(15)),stateno!=[2999,3999] {
ignorehitpause if playerid(var(15)),movetype=A && playerid(var(15)),gethitvar(isbound)=0 &&
((playerid(var(15)),movecontact && playerid(var(15)),target,movetype=H) ||
(playerid(var(15)),movecontact) ||
(playerid(var(15)),numtarget>0 && playerid(var(15)),target,incustomstate=0 && playerid(var(15)),Target,StateNo != [5110,5955] && playerid(var(15)),target,movetype=H)) {
ignorehitpause if ((root,name!="BAGAN" && ((root,command="shift_fwd") || (root,command="shift_back"))) && var(51)<1 && var(28)>200 && (var(7)&1024)) ||
(var(51)>0 && var(28)>200 && (var(7)&1024) && playerid(var(15)),sysfvar(4)<1 && (random%50=0) && playerid(var(15)),life<playerid(var(49)),life ) {
var(48):=21;
}}}}}
Please modify to how you create your system, for example switch cancelling does not cancel on states 2999->3999 in the trigger because i have a custom hyper cancel tag I use for those statenumbers, so please look at the code and adjust accordingly before simply copy pasting.
Now this is optional, this is to FORCE cancelling partner to exit after you initiate a tag cancel, by default you may be able to do a movecontact combo with the character that is supposed to exit until they are no longer in an attack state, I prefer they exit after a cancel initiates since this messes up timing and 99% of fighting games behavior is like this, in main loop 90900 in a4b_main, I put:
ignorehitpause if map(cancelexit) > 0 { mapset{map:"cancelexit"; value: map(cancelexit) + 1} }
We already set up the map cancelexit in the above when var(48)=21. So now that it is 1, it will continue increasing by 1 each frame, we want this so in statedef -4 we can trigger a force exit with this code (put in statedef -4 to ignore superpauses):
ignorehitpause if playerid(floor(sysfvar(0))),map(cancelexit) > 5 && (stateno != [190190,190196] || prevstateno!= [190195,190196]) && movetype = A && pos y >= 0{
changestate{value:190190}
mapset{map:"cancelexit"; value: 0; RedirectID:floor(sysfvar(0))}
}
ignorehitpause if playerid(floor(sysfvar(0))),map(cancelexit) > 5 && (stateno != [190190,190196] || prevstateno!= [190195,190196]) && movetype = A && pos y < 0{
changestate{value:50;ctrl:0}
velset{x:0;}
mapset{map:"cancelexit"; value: 0; RedirectID:floor(sysfvar(0))}
}
Ok that's good, but now we must set it up so it doesnt bug out any other characters to exit, so in state 190190 && for good measure also in 190191 in a4b_tag we put:
mapset{map:"cancelexit"; value: 0; RedirectID:floor(sysfvar(0))}
We redirect the mapset to the helper90900 and set it to 0 whenit confirms the person is exiting
Then in statedef 190195 we put:
ignorehitpause if playerid(floor(sysfvar(0))),map(cancelexit) > 0 { mapset{map:"cancelexit"; value: 0; RedirectID:floor(sysfvar(0))} }
Just so before landing in as the main character, teh cancelexit is still not running and accidently sends the character exiting.
And in statedef 190193, replace the code sections referenced below with mine here. The default does not calculate the main player's postition right in some cases which ends up making them land on the edge of the screen. This should fix that bug:
#;sys::partner-out-set2
if sysvar(0)>0 && playeridexist(sysvar(0)) {
if facing!=playerid(sysvar(0)),facing { turn{} }
posset{
x: (playerid(sysvar(0)),pos x) - ( playerid(sysvar(0)),backedgebodydist +const240p(90) )*facing;
y:-const240p(180) }
#;sys::partner-landing-pos2
if !time && !ishelper && roundstate=2 && sysfvar(4)=0 && !numhelper(909606) && sysfvar(0)>0 && playeridexist(floor(sysfvar(0))) {
if (playerid(floor(sysfvar(0))),var(0)=90900) && !(playerid(floor(sysfvar(0))),var(7)&65536) {
helper{
pos:(playerid(floor(sysfvar(0))),pos x-pos x)*facing, (playerid(floor(sysfvar(0))),pos y-pos y);
id:909606; stateno:190202 }
}}
#;sys::partner-out-vel2
velset{x:abs(pos x-playerid(floor(sysfvar(0))),pos x)*0.05; y:abs(pos y)*0.05 }
#;sys::partner-out-vel3
if (roundstate>2) {
velset{x:abs(pos x-playerid(sysvar(0)),pos x +const240p(28)*(id-sysvar(0))*facing )*0.035;
y:abs(pos y)*0.035 }
}
}
Then to prevent some weird things from happening like characters activating a switch but not actually switching (super rare), I added this when var(48) is set to 11 in state 90900:
ignorehitpause if var(48)=11 { mapset{map:"i_Assist_Interval"; value: 100} mapset{map:"time"; value: 1} mapset{map:"unsettimer"; value: 1}}
Below I also added to check if it stays stuck and no characters actually switched in, to set it back to 0:
ignorehitpause if map(i_Assist_Interval)=100 && map(time)=1 && map(unsettimer) > 2 && playerid(var(15)),stateno!=[190193,190195] { mapset{map:"i_Assist_Interval"; value: 0} mapset{map:"time"; value: 0}}
Now we remove the current #;sys::partner-landing-pos2 and replace with these two:
#;sys::partner-landing-pos2
if timeelapsed <= 235 && !time && !ishelper && roundstate=2 && sysfvar(4)=0 && !numhelper(909606) && sysfvar(0)>0 && playeridexist(floor(sysfvar(0))) {
if (playerid(floor(sysfvar(0))),var(0)=90900) && !(playerid(floor(sysfvar(0))),var(7)&65536) {
helper{
pos:(playerid(floor(sysfvar(0))),pos x-pos x)*facing, 50+(playerid(floor(sysfvar(0))),pos y-pos y);
id:909606; stateno:190202 }
}}
#;sys::partner-landing-pos2
if timeelapsed > 235 && !time && !ishelper && roundstate=2 && sysfvar(4)=0 && !numhelper(909606) && sysfvar(0)>0 && playeridexist(floor(sysfvar(0))) {
if (playerid(floor(sysfvar(0))),var(0)=90900) && !(playerid(floor(sysfvar(0))),var(7)&65536) {
helper{
pos:(playerid(floor(sysfvar(0))),pos x-pos x)*facing, floor(playerid(sysvar(0)),pos y-pos y);
id:909606; stateno:190202 }
}}
Also its good practice to make sure noturntarget is asserted if the character you are sending out is not the main character, this is because enemies will still try to automatically turn to your standby characters if called out and are close enough, but this ruins the mechanics. This is NOT behavior in tagteam fighting games, main fighters should not be turning to standby fighters who are called in for assisting.
The combo counter system in add004 is also innacurate, compare it with the actual fight.def counter on and you'll see add004 drops some hits sometimes. I'd disable it all together and just enable native combo count system on ikemen/mugen.
Mini rant here, I personally think a lot of stuff here is over-engineered and complex for no reason (for example the explod system for messages). I don't understand the point of making every single thing a variable and writing confusing code no one can really read. I mean cmon, what is this:
var(14):= var(15)+1+(time%numpartner) -(var(15)-sysvar(0)+1+(time%numpartner)>numpartner)*(numpartner+1);
pos:var(3)*(teamside=2)+floor(5*fvar(0))*ifelse(teamside=1,1,-1) , floor((50+(var(47)%100)*8)*fvar(0));
What are we even trying to do here? Imo, some good examples of easy and clean code to read concerning tag systems are unotag and ikemen's tag system that gacel and k4thos created. I think add004 is waaaay to over complicated for no good reason, it could benefit from some refactoring. For example, why are there 2 standby states? having 2 kind of bugs everything out on default and making it be able to enter as a main player in state190191 doesn't make sense if theres 1 more standby state you have to go thorugh, why not just 1 standby state at that point then? I digress.
I think that’s pretty much the basics but this should make things way smoother and more responsive. Works way better for me this way. Again, unsure of how the mugen version is, but at least in Ikemen it was this way until I did all these modifications.