"My" Saving Throw Functions Observations (Immunities) (With Alternative Function.)

holy diana, thanks for the info. It sounds like 1/2 are more similar than i thought. (re. spells and application of effects) Basically, it looks like nwn2 uses a different graphics-engine and that’s it.

 
conclusion: rethink/rewrite all spellscripts …

@Shadooow
given that there is a discrepancy between the hardcoded Immunity check and the function GetIsImmune()

Is it your policy to rely on GetIsImmune() [in the spellscripts] and bypass the hardcoded Immunity check(s) [in application of effect]?

 
I’m wondering if GetIsImmune() is (more) accurate and reliable (when used properly). That is, does it account for target’s items and equipment and buffs and whatever else ? Is it a good function?

GetIsImmune checks all, but I only use it when the spell is applying effect that is not “immune-able” or different from the spell immunity.

See the above cases - scare applies AB decrease, ghould touch applies paralysis but the immunity should be against poison.

These are special cases, the rest is handled automatically with MySavingThrows and the adjustment I posted at the very beginning of this thread that fixes cases where the function doesnt return value 2 when it should.

ah, that makes more sense – less re-coding req’d.

sonic, for instance(?)

Good afternoon all,

@Shadooow

This is the approach I am trying to take. Allow the official code to do as much work as it can and then picking up the pieces that fail.

For instance, the basic WillSave type functions (not the “My” functions) do appear to correctly discern most immunities, even from items. Although I am having to look at adding the GetIsImmune for poison and curse effects due to their inherently different approach.

I am looking at the function I posted again today to see where to place it. I imagine I can (hopefully) just check the ResistSpell function return and pick up any -1 results prior checking with the base function save checks as I have it currently standing.

@kevL_s

It certainly seems like it, or maybe I don’t understand the nuances.

My overall aim is to unify the saving throw type functions to cater for all instances where a saving throw or spell resistance is required, and to deliver a uniform VFX response subject to immune, resist or fail. Be the source a spell (where the “My” functions work most of the time) or something we simply want to check against regardless of the end result. < That was a key point for me, as it seemed to me that the end result is the thing that triggers the checks. Some I have been able to get around (or so testing appears to show) whereas others (like poison and curse) are hard-coded in a way more difficult to intercept and need those extra GetIsImmune checks.

I believe another key point that I need to address is when we pass effects that do not come from a “spell” (spells.2da), because ResistSpell passes -1. I think (although I am still checking at this stage), that we may be able to continue to check the effect for immunity via the normal “Save” functions if they are not poison or curse. That is what I am currently testing. I will report back.

1 Like

That is certainly interesting, but I wonder what is it good for. If the script isn’t spell, why would you use MyResistSpell ?, and MySavingThrows works in all scripts properly, well at least with my immunity adjusment that I posted in my first post. And if the script is spellscript then all the effects gets automatically spell ID set.

But what you can do is to make effect ignoring immunity to mind spells if you set the spell id of such effect to be 28 (Control Undead). Well at least in NWN1.

@Shadooow

As I say in a previous post, my aim was to unify the functions in a way that a builder can confidently use a saving throw function, knowing all points related to saving have been tested for, whether the source be a spell, an altered spell result (due to difficulty setting options), or a home-brew effect …especially where immunities are involved.

Therefore, when my function is called, if it is not a spell, it matters not, because it will then go on to check for other immunities the PC has, which is what the “my” functions failed to employ correctly. So rather than have to alter spells or scripts individually (apart from the spells that will need it), in theory, I hope I will simply use my function everywhere, and have the same VFX return whether the PC saves, is immune or fails, consistently, with default feedback or builders own customized feedback for extra customization.

EDIT OUT - INCORRECT INFO

I just find it easier knowing I can safely use one function to do save tests rather than have to consider a couple and then add other code in places to ensure the end result. It may be that the function may yet still need to be tweaked, or that certain spells will still need individual treatment, but I am hoping this will reduce the overall amount.

EDIT OUT - INCORRECT INFO

@Shadooow
@kevL_s

Unfortunately, it looks like it still fails when a PC fails their save, as it still double checks a poison. :frowning:

I will continue looking, but it may be that poison/curses will only be 100% secure via direct GetIsImmune prior any function calling. I’ll try incorporating somehow … but …

1 Like

I don’tt understand what are you talking about in last 3 posts. It seems to me that you are trying to fix a problem which you have been offered a simple cure with a nuclear warhead.

If you use the code snipped I posted in my first post, then MySavingThrow will return FALSE if the target is immune to poison. No saving throw will be rolled. Then it is up to you to find the offending scripts that applies effects that are not automatically rejected by the engine such as ability decreases, attack bonus penalties, or completely different effect (ie. blindness - one of the diseases applies it). And use the GetIsImmune == TRUE ApplyEffect(EffectPoison()/EffectDisease() etc. to force the immunity feedback).

This is what I did in my Community Patch for NWN1. You can see the full changelog regarding spells here and I bet 90% of these issues are shared with NWN2.

Alternatively you can do it the opposite way, ie. check if the target is immune for a saving_throw_type you are checking in MySavingThrows and return 1 - ie. success. In majority of the spells, that cause the spellscript to not do anything. Then you would have to print a fake immunity feedback. However this solution has two issues - fake feedback will never be mulltilanguaged. If you are trying to code something like my Community Patch to be used by various builders, then this is quite big disadvvantage. Another problem is that few spells are applying effects when the target success - Finger of Death, Destruction, maybe more - don’t know NWN2 spells but I know there are more of them, so is it likely some of them does this also. And these would have to be rewritten with GetIsImmune again.

Here is the complete function MySavingThrows from Community Patch:

int MySavingThrow(int nSavingThrow, object oTarget, int nDC, int nSaveType=SAVING_THROW_TYPE_NONE, object oSaveVersus = OBJECT_SELF, float fDelay = 0.0)
{
    if(nSavingThrow == 4) return FALSE;
    // -------------------------------------------------------------------------
    // GZ: sanity checks to prevent wrapping around
    // -------------------------------------------------------------------------
    if (nDC<1)
    {
       nDC = 1;
    }
    else if (nDC > 255)
    {
      nDC = 255;
    }

    effect eVis;
    int bValid = FALSE;
    if((nSaveType == SAVING_THROW_TYPE_FEAR && (GetIsImmune(oTarget,IMMUNITY_TYPE_FEAR,oSaveVersus) || GetIsImmune(oTarget,IMMUNITY_TYPE_MIND_SPELLS,oSaveVersus))) ||
       (nSaveType == SAVING_THROW_TYPE_MIND_SPELLS && GetIsImmune(oTarget,IMMUNITY_TYPE_MIND_SPELLS,oSaveVersus)) ||
       (nSaveType == SAVING_THROW_TYPE_DEATH && GetIsImmune(oTarget,IMMUNITY_TYPE_DEATH,oSaveVersus)) ||
       (nSaveType == SAVING_THROW_TYPE_POISON && GetIsImmune(oTarget,IMMUNITY_TYPE_POISON,oSaveVersus)) ||
       (nSaveType == /*SAVING_THROW_TYPE_PARALYSE*/20 && (GetIsImmune(oTarget,IMMUNITY_TYPE_PARALYSIS,oSaveVersus) || (GetIsImmune(oTarget,IMMUNITY_TYPE_MIND_SPELLS,oSaveVersus) && !GetModuleSwitchValue("72_DISABLE_PARALYZE_MIND_SPELL_IMMUNITY")))) ||//1.72: count with the paralyze module switch
       (nSaveType == SAVING_THROW_TYPE_DISEASE && GetIsImmune(oTarget,IMMUNITY_TYPE_DISEASE,oSaveVersus)))
    {
    //1.70: Engine workaround for bug in saving throw functions, where not all subtypes check the immunity correctly.
    bValid = 2;
    }
    else
    {
        if(oSaveVersus != OBJECT_SELF && GetObjectType(OBJECT_SELF) == OBJECT_TYPE_AREA_OF_EFFECT)//1.72: special AOE handling for new nwnx_patch fix
        {
            //this checks whether is nwnx_patch or nwncx_patch in use; using internal code to avoid including 70_inc_nwnx
            SetLocalString(GetModule(),"NWNX!PATCH!FUNCS!12",".");
            DeleteLocalString(GetModule(),"NWNX!PATCH!FUNCS!12");
            int retVal = GetLocalInt(GetModule(),"NWNXPATCH_RESULT");
            DeleteLocalInt(GetModule(),"NWNXPATCH_RESULT");
            if(retVal >= 201)//in version 2.01 saving throws from AOE spell will count spellcraft, however to make this work requires to put AOE object into the save functions
            {                //there are good reasons why community patch changed all AOE spells to put AOE creator into this function, namely double debug, so this switcheroo
                oSaveVersus = OBJECT_SELF;//will be performed only when nwnx_patch/nwncx_patch is running (which also fixes the double debug along other issues)
            }
        }
        if(nSavingThrow == SAVING_THROW_FORT)
        {
            bValid = FortitudeSave(oTarget, nDC, nSaveType, oSaveVersus);
            if(bValid == 1)
            {
                eVis = EffectVisualEffect(VFX_IMP_FORTITUDE_SAVING_THROW_USE);
            }
        }
        else if(nSavingThrow == SAVING_THROW_REFLEX)
        {
            bValid = ReflexSave(oTarget, nDC, nSaveType, oSaveVersus);
            if(bValid == 1)
            {
                eVis = EffectVisualEffect(VFX_IMP_REFLEX_SAVE_THROW_USE);
            }
        }
        else if(nSavingThrow == SAVING_THROW_WILL)
        {
            bValid = WillSave(oTarget, nDC, nSaveType, oSaveVersus);
            if(bValid == 1)
            {
                eVis = EffectVisualEffect(VFX_IMP_WILL_SAVING_THROW_USE);
            }
        }
    }

    int nSpellID = GetSpellId();

    /*
        return 0 = FAILED SAVE
        return 1 = SAVE SUCCESSFUL
        return 2 = IMMUNE TO WHAT WAS BEING SAVED AGAINST
    */
    if(bValid == 0)
    {
        if(nSaveType == SAVING_THROW_TYPE_DEATH && nSpellID != SPELL_HORRID_WILTING && nSpellID != SPELL_DESTRUCTION)
        {
            eVis = EffectVisualEffect(VFX_IMP_DEATH);
            DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oTarget));
        }
    }
    else //if(bValid == 1 || bValid == 2)
    {
        if(bValid == 2)
        {
            eVis = EffectVisualEffect(VFX_IMP_MAGIC_RESISTANCE_USE);
            if(nSaveType == SAVING_THROW_TYPE_DEATH)//1.70: special workaround for action cancel issue
            {
                if(GetSpellId() == -1)
                {
                    object oWorkaround = GetObjectByTag("72_EC_DEATH");
                    if(!GetIsObjectValid(oWorkaround))
                    {
                        oWorkaround = CreateObject(OBJECT_TYPE_PLACEABLE,"plc_invisobj",GetStartingLocation(),FALSE,"72_EC_DEATH");
                        ApplyEffectToObject(DURATION_TYPE_PERMANENT,ExtraordinaryEffect(EffectVisualEffect(VFX_DUR_CUTSCENE_INVISIBILITY)),oWorkaround);
                        SetPlotFlag(oWorkaround,TRUE);
                    }
                    SetLocalObject(oWorkaround,"TARGET",oTarget);
                    AssignCommand(oWorkaround,ActionCastSpellAtObject(386,oWorkaround,METAMAGIC_ANY,TRUE,0,PROJECTILE_PATH_TYPE_DEFAULT,TRUE));
                }
                else
                {//this is now fixed in NWN:EE
                    //SetCommandable(FALSE,oTarget);
                    ApplyEffectToObject(DURATION_TYPE_INSTANT,EffectDeath(),oTarget);//1.70: engine hack to get proper feedback
                    //SetCommandable(TRUE,oTarget);
                }
            }
            else
            {
                /*
                If the spell is save immune then the link must be applied in order to get the true immunity
                to be resisted.  That is the reason for returing false and not true.  True blocks the
                application of effects. Shadooow: doesn't work for death in order to fix action cancel bug.
                */
                bValid = FALSE;
            }
        }
        DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oTarget));
    }
    return bValid;
}
1 Like

@Shadooow

That is what I do/have done. I just thought I had found a solution to avoid having to use GetIsImmune in the function (for poison), but had not. i.e. A fail save ended up being checked twice, so I reverted to simply using a GetIsImmune to bypass other checks and let the default functions do their job.

Anyway, I believe that is as much as I am able to do here, and at least it serves the purpose I aimed for … Ref poison, you do need to ensure you send the POISON subtype, or else it will still not report correctly on a fail save still … but hopefully a builder who might use the function will add that when required.

On a positive note, curses do not appear to be an issue and work with the function fine, because there is no “immunity” for curses as such. And even though we can get immunities to ability decreases, I am content that they do not qualify in this case. i.e. The effect is a curse specifically, and the results of that are not considered as independent effects that can be “immunized” against. Spell resistance does still work though, and as I do not intend to alter the spell in this instance, it’s irrelevant anyway. However, I have tested a curse via another source and it appeared to work fine.

So, here it is, as far as I am going to take it … The only real addition is the GetIsImmune versus a POISON save effect that required extra attention. As you say, I may not have covered everything, but I will check it as time goes by to see if other effects that use it require any extra attention.

It has been tested with a few effects, but should be good with all. Just double check results to be sure you are happy with them.

NB: Can be used with all effect types, but care should be taken when asking for a poison save check - MUST pass the SAVING_THROW_TYPE_POISON.

//////////////////////////////////////////////////////////////////////////////////
// SAVING THROW WITH VFX DEPENDING UPON IMMUNE OR RESISTED OR FAILED (LANCE BOTELLE)
// SIMPLY RETURNS 0 ON SAVE FAIL, RETURNS 1 ON SAVE MADE, RETURNS 2 ON IMMUNE (WITH VFXs)
// NB 1: ANY POISON SAVE IGNORES ANY DC PASSED & USES THE POISON.2DA REF INSTEAD (iSupMess IS MADE 1 IF NOT IMMUNE)
// NB 2: Reports integer only. Any result from that (like death) must be handled seperately.
// NB 3: This does not test for spell cast timings between effect and requirement.
// Therefore, the moment this function is called, the check is made with VFX.
// oTarget is the PC or creature making the saving throw.
// iSAVETYPE CAN BE: SAVING_THROW_FORT, SAVING_THROW_REFLEX OR SAVING_THROW_WILL
// iSUBTYPE IS SAVING_THROW_TYPE_XXXX FORMAT (SAVING_THROW_TYPE_ALL IS DEFAULT)
// iDC IS THE DC TO SAVE AGAINST (DEFAULT 15) USE iSupMess = 1 IF USING SPECIFIC FEEDBACK.
// According to official source: If using with an area of effect script (On Enter
// On Exit or On Heartbeat) wemust pass GetAreaOfEffectCreator into oSaveVersus.
//////////////////////////////////////////////////////////////////////////////////
int LBSavingThrowResult(int iSAVETYPE, object oTarget, int iDC = 15, int iSUBTYPE = SAVING_THROW_TYPE_ALL, object oSaveVersus = OBJECT_SELF, int iSupMess = 0);
int LBSavingThrowResult(int iSAVETYPE, object oTarget, int iDC = 15, int iSUBTYPE = SAVING_THROW_TYPE_ALL, object oSaveVersus = OBJECT_SELF, int iSupMess = 0)
{
	// LIMIT THE DC
	if (iDC<1){iDC = 1;}else if (iDC > 255){iDC = 255;}
		
	// NOT A PLAYER SPELL INTERCEPT
	int iRESULT = ResistSpell(oSaveVersus, oTarget);
		
	// NOW CHECK FOR OTHER IMMUNITIES/RESISTANCES OR SAVES
	if(iRESULT < 1)
	{		
		// POISON HAS TO BE HANDLED VIA HARD-CODE IF NO IMMUNITY
		if(iSUBTYPE == SAVING_THROW_TYPE_POISON)
		{
			if(GetIsImmune(oTarget, IMMUNITY_TYPE_POISON, oSaveVersus)){iRESULT = 2;}
			else{iSupMess = 1;}
		}
	
		// DETERMINE HOW PC SAVES FIRST (DOUBLE CHECK ANY IMMUNITY VIA SPELL AFTER ANY DEFAULT)
		else if(iSAVETYPE == SAVING_THROW_FORT){iRESULT = FortitudeSave(oTarget, iDC, iSUBTYPE, oSaveVersus);}
		else if(iSAVETYPE == SAVING_THROW_REFLEX){iRESULT = ReflexSave(oTarget, iDC, iSUBTYPE, oSaveVersus);}
		else if(iSAVETYPE == SAVING_THROW_WILL){iRESULT = WillSave(oTarget, iDC, iSUBTYPE, oSaveVersus);}
	}
	
	//////////////////////////////////////////////////////////////////////////////////////////////////
	// PLAY A VFX ACCORDING TO RESULT
	//////////////////////////////////////////////////////////////////////////////////////////////////
	
	// PC SAVES
	if(iRESULT == 1)
	{			
		effect eSR = EffectVisualEffect( VFX_DUR_SPELL_SPELL_RESISTANCE );	// uses NWN2 VFX
		ApplyEffectToObject(DURATION_TYPE_INSTANT, eSR, oTarget);
		
		if(iSupMess == 0)
		{
			SendMessageToPC(GetFirstPC(FALSE), "<i>" + GetName(oTarget) + " shakes off the effect!</i>");			
		}
	}
	
	// PC IMMUNE
	else if(iRESULT == 2)
	{	
		// BRIEF INDICATION
		effect eGlobe = EffectVisualEffect( VFX_DUR_SPELL_GLOBE_INV_LESS );	// uses NWN2 VFX					
		ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eGlobe, oTarget, 0.5);
		
		if(iSupMess == 0)
		{
			SendMessageToPC(GetFirstPC(FALSE), "<i>" + GetName(oTarget) + " is immune to the effect!</i>");						
		}
	}
	
	// PC FAILS
	else
	{	
		effect eSR = EffectVisualEffect( VFX_DUR_MIND_AFFECTING_FEAR );	// uses NWN2 VFX
		ApplyEffectToObject(DURATION_TYPE_INSTANT, eSR, oTarget);
		
		if(iSupMess == 0)
		{
			SendMessageToPC(GetFirstPC(FALSE), "<i>" + GetName(oTarget) + " succumbs to the effect!</i>");			
		}
	}		
	
	// INFORM RESULT
	return iRESULT;
}

Thanks, I will take a closer look at that! :+1:

Just a quick example of what I am trying to illustrate is that where you have additional GetIsImmune checks, I currently do not require them (as far as I can see) because the basic Save functions appear to consider them correctly. Only the POISON check I could not get to report as I hoped.

e.g. If a PC has a ring of fear immunity, my function will pass back that the PC is immune when ever they make a saving throw roll versus a FEAR subtype without the need to specifically add GetIsImmune - even if the resultant effect does not use EffectFrightened, which it seemed to me the functions required before the immunity checks would fire.

This was an important consideration for me because of (as you say) spell alterations where a different final effect may be resultant from a spell that simply uses a save versus x. E.g. I alter the result of EffectFrightened to simply an attack penalty on lower settings. Using the official scripts, if a player had a ring immune to fear, then they would not gain the benefits of said ring against a save versus fear because the final effect applied was different to “fear” expected and so they had to make a save when one should not have been required. Where you use GetIsImmune prior to the checks, I simply make use of the official functions that do return the correct result for the save type checked, rather than the final result applied.

Thats good. In NWN1 curses are blocked by immunity to ability decrease and poisons and diseases too and it is all hardcoded.

The issues in the SavingThrow functions might have been fixed in NWN2 then. In NWN1 it is the death subtype which is still rolling save despite player is immune. I don’t remember the rest of the cases but it was related to checking the subtype with “rare” saving throw type, ie. ReflexSave with mind spells subtype etc. Not something you encounter in vanilla spells much.

1 Like

@Shadooow

Thanks for highlighting this. It may still be a problem in NWN2, but I have not yet designed any effects that have required testing it that far yet. :grinning: Existing code would suggest you are correct and that it will still need adding … but I have not changed the official “death” spells and leave the original code in place for those. Of course, if I add my own “death” effect, then it may well raise its ugly head from my function … and then I will have to update the function.

My function is (I suppose) a starting point for me to overcome the immediate issues I have found when PCs wear immunities to save types and the resultant effect differs - that were forcing a hard-code response that was undesirable.

For this very reason, I have kept copies of all scripts both you and KevL have posted in case I find an issue that you guys have already considered. If I had the time, I would test stuff to “death”, but I don’t have that time … and I really do need to do the plot stuff alongside the mechanics. :wink:

I had to deal with this issue up to this point though because it niggled me that a player could have gone to the trouble to acquire an immunity item only to find it not work just because the save code did not recognise the saving throw type.

@kevL_s
@Shadooow

Thanks for all your help and advise, as always it’s been a pleasure and helpful. :+1:

1 Like