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

Hi All,

I was doing some script work on module 2 of my campaign and observed that some scripts I was using were not recognising an item offering “immunity” to certain saving throws that I was asking the scripts to make. I thrashed this out with KevL, who patiently helped me get to the bottom of this. So my thanks goes to him for helping me get there.

Anyway, the bottom line is, when it comes to detecting immunities using the “My” functions, MySavingThrow (and MyResistSpell), they rely on the resultant effect rather than check any items carried. So, for example, when writing our own scripts that use these functions, the immunity check can sometimes be missed if we (for whatever reason) alter the end result.

For example, I have a script that the PCs have to save versus fear, and if they fail have a shaken type effect, which applies a penalty to attack and saves. The point is, the final applied effect I have written does not apply the EffectFrightened, which is the effect that is hard-coded to work with the “My” functions mentioned above. So, when I use the “My” functions in a way similar to the way you see them in spells, they fail to recognise the altered spell effect and so any immunity checks for “fear” fail and the PC with an item that makes them immune to “fear” fails to do so.

The bottom line is that any scripts you have written that use the “My” functions will only work properly (especially with respect to immunities) if your scripts end up applying the effect the functions were expecting, so that the hard-code immunity check is fired correctly.

I am currently working on a LBSavingThrowResult function script that I hope will work with custom scripts that require immunity checks (which I imagine would include most of us). I do have one already, but I want to take a closer look at it to see if I can improve on it before posting it. At least you are informed of this “issue” now.

NB: All testing has only been with the Fear (EffectFrightened) to date.

Here are my results …

This script woks for me as a “complete” replacement for the two “My” functions …

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: UPDATE: 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;
}

Again, this is only currently tested with the fear spell that I have allowed to be changed effect with a lower game setting difficulty. However, as the function checks for general immunity if the spell resistance checks fail, then it still works.

1 Like

I suspect the behavior of the saving throw functions will be idential in nwn2 so you can try this

code snipped from Community Patch:

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;
}
3 Likes

@Shadooow

Yes, that’s the kind of thing I am looking at … and that does look like it would address the issues …

I am also trying to avoid editing the official “nw_i0_spells” library script if I can help it, so it may mean implementing the fix in the spell scripts only (bad enough) … and offer a solution for builders to have this incorporated in their own function moving forwards.

Thats not going to work. You need not only to edit nw_i0_spells, but also recompile all spellscripts with it and put their scripts into your module/server override or in the worst case a hak

If I edit each spell script that requires it (with the necessary code fixes), that will address them. (I know it’s quite a few spells. :cold_face:) But I save these scripts as Campaign scripts (with my module release) which then override the official ones. That fixes those.

Then I am going to look at creating a function that works with other home-brew requirements for people to add to their own library, to use instead of the MySavingThrow/MyResistSpell if possible. At the moment, I am sorting a lot of other stuff and have not had time to check this side, but even if that is not possible, then I will just release a version that does “basic” checks for people who want just basic “immune” feedback, which I am already using myself, but would like to see if it can be “improved”.

I may be able to incorporate the two projects.

I will post it here if I succeed.

Yes of course, but why would you? It is much complicated and worky solution for no reason. I don’t get it. Why editing “official” library is no-no and editing “official” spellscript is allowed?

Because I don’t want to recompile a library that I may use elsewhere with another module (outside of my own)? I don’t know, perhaps I am just avoiding something I need not.

If I edit the nw_i0_spells, and then recompile it, what happens to all related scripts that I am not using? Are they added to my campaign or module folder?

Its always been something I have avoided because I don’t really understand the full consequences. Are you able to elaborate what would actually happen and maybe convince me?

That just makes no sense. You edit the nw_i0_spells, put it into your module and then it affects only your module and only spells you put into module.

It was a question …

Anyway let me ask this way instead …

If I edit a library (let’s say nw_i0_spells), can I then assume it asks me to save to my “module” like any other script? Yes/no?

If yes, can I then move it to my Campaign folder, just like I do with any official script I have altered?

Then, once in the campaign folder, when I come to recompile it, you say it will only be relevant to scripts I use in my own campaign?

Here is where I get confused … When I have edited scripts that require a library separately from one another (i.e. Do not recompile all scripts associated with that file in one go), then I sometimes get scripts fail. So, I usually recompile all scripts after altering the library.

Assuming I recompile all scripts with this new copy of the library, which scripts does it recompile - does it only do scripts associated with my own campaign? Again, assuming this to be the case, what happens with respect to those official includes that are still required by the library I just recompiled. I assume from your reaction, they do not get affected.

So, if I understand what you are saying correctly, I can effectively save a copy of an official library to my module, change it to a campaign script, and continue to use it, edit it, recompile it, and anything I do to this copy has no impact outside of my campaign environment?

I mean it sounds logical, but for some reason I have never had the confidence to try that. However, if you are confidant this is not an issue, I am willing to try that. :slight_smile:

i.e. The modified official library sits inside the campaign environment just like any other script? Does it impact in any other way because I am using my own copy rather than the original? Performance?

I ask because I have no idea about this point and am happy to go this way if all is safe to do so without any impact elsewhere.

I may just try doing/adding this (as an experiment) to see what happens then … I will now assume then that altering a library is no different from altering a basic script … unless anybody comes back in the next ten minutes and warns me not to. :wink:

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;
}

Is it safe?

I know the position is reversed here, but it reminded me of this scene. :slight_smile:

1 Like

@Shadooow

OK, I have a final question before I bite the bullet and press recompile all with my modified campaign nw_i0_spells script.

What happens if I wanted to revert to the original OC script and recompile - How would I do that?

Just in case I ever needed to?

I mean, if I understand this correctly, I would need to delete my altered file and then search for every file that was using the altered nw_i0_spells library and resave every one individually, because I cannot recompile all from the official script can I?

Or would just making a copy of the original and recompiling all from that, and then deleting the copy work?

yes

As for the rest of your post, I don’t understand the term campaign that you use. Is this something related to NWN2 toolset? I am working with NWN1 only so my experiences might not be 100% applicable for you. But in NWN1 there is no difference between a “library” and “script”, both are scripts, both can be put into module. And modified library still affects only scripts you recompile with it. Modifying library itself will not cause any changes and include scripts generally cannot be compiled itself.

Hence you still have to put all the spells’ scripts that you want to be fixed into your module along with the library. It is all about work needed to be done. Either you need to replace MySavingThrow in every spellscript with your own function and add #include “my_include” at the top of the each spell script. Or you just put them there and recompile all scripts using the modified nw_i0_spells. The result will be same.

In my opinion, this shouldn’t be done by a module builder. But I know the general opinion of “unofficial” patches in NWN is, so if you have to fix it you will have to do it yourself.

Or you can ignore this issue as everyone else, you are probably the first person who found this to be a problem (except me and I work with NWN1 :smiley: ).

@Shadooow

If only I could, but it’s like a wobbly picture to me now that I am aware of it. :wink: And to clarify something, this is to fix the point for my campaign, not the official campaign. Although builders may like to be aware of the issue for their own and alter as much as they feel fit.

Actually, I have just tried contacting @kevL_s as well about this, as he works in NWN2 as well, and so if anybody would know if there are potential pitfuls for NWN2 doing it this way, then he would know.

I mean I was all good to go until I recognised that there may be issues trying to revert to an original state after making any alterations. I mean I see no reason why I could not do what I said (make a copy of the original unaltered and recompile all and delete that copy), but I would like some kind of reassurance from someone who may have already tried this.

Maybe I can start another campaign build to test the theory, as if it is possible, then it would potentially save a lot of “work” dealing with the issue from the library than “patching” individual spell scripts. I have considered this so many times in the past, but always chickened out. In some ways that has been a good thing, because I would have set myself up with a lot of issues due to poor coding in the past. Maybe nowadays I am a little better, and maybe more confidant, so perhaps it will happen sooner than later … and especially if I do test outside my current work first.

Anyway, thanks for the code, it saves me time looking at the requirements anyway. :+1:

PS: Actually, the number of spell scripts that may need altering are fewer than first thought, as it is (as far as I can see) only a few that alter due to “difficulty options”. I have already altered these spell scripts with a functions, so I can effectively (I hope) just rewrite that function, with perhaps some new checks for each spell.

Yes the problem is not in all of them, but it is much easier to bring all their scripts inside your module and recompile them with the changed nw_i0_spells than to research and playtest which spells has this issue and which doesn’t.

Agreed.

I am just in communication with KevL now to get some clarification about any potential reversion to the official libraries if ever needed. If reversion is simple enough, then I am all ready to delve into editing the library - which may be the first of many if it is without any issues. :+1:

@Shadooow

Nwn2 has a Campaign folder (if builder has set his module(s) up to be a campaign – Campaigns link 1 or more Modules together, so in Nwn2 we have releases with more than 1 Module). The Campaign folder has a priority higher than Module but lower than /override.

The thing about Lance and his modules/campaigns is that they are very nearly total conversions of nwn2 … so his changes/control really needs to be extensive.

 
between you, me, and Lance, I have all spellscripts (etc) in my /override and work on them (along with all #includes) whenever i feel like it,

@Lance, once you get the habit i predict you’ll want all the scripts accessible to quick & easy modification …

2 Likes

I think you may well be correct … :+1: Maybe that time draws ever closer …

1 Like

Hi all,

This script woks for me as a “complete” replacement for the two “My” functions …

NB: UPDATE: 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;
}

Note the comments and usage in this fear spell snippet example …

//Make SR Check (REPLACED WITH LBSavingThrowResult)
//if(!MyResistSpell(OBJECT_SELF, oTarget, fDelay))
//{
	//Make a will save (REPLACED WITH LBSavingThrowResult)
	//if(!MySavingThrow(SAVING_THROW_WILL, oTarget, GetSpellSaveDC(), SAVING_THROW_TYPE_FEAR, OBJECT_SELF, fDelay))
	
	// NEW FUNCTION DOES SPELL RESISTANCE AND SAVES COMBINED (BASED ON ITEMS CARRIED RATHER THAN FINAL EFFECT)
	if(!LBSavingThrowResult(SAVING_THROW_WILL, oTarget, GetSpellSaveDC(), SAVING_THROW_TYPE_FEAR, OBJECT_SELF, 0))
	{
		// FRIGHTENED OPTION FIX (SO IF A DIFF EFFECT WAS RETURNED HERE, THEN THE NEW SAVE FUNCTION STILL WORKS CORRECTLY)
		eFear = FIXEDGetScaledEffect(eFear, oTarget);	
		
		// ONLY NOW LINK ANY VFX
		eFear = EffectLinkEffects( eFear, eVis );						
		
		//Apply the linked effects and the VFX impact
		DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eFear, oTarget, 6.0));
	}
//}

Again, this is only currently tested with the fear spell that I have allowed to be changed effect with a lower game setting difficulty. However, as the function checks for general immunity if the spell resistance checks fail, then it still works.

Actually, I misunderstood the problem. I thought you want to unify the immunity feedback as some functions are not printing “immune to” when checking for a saving throw against something.

But you are solving a different problem and thats the fact that because this happens and MySavingThrow function returns failure with “target is immune” player gets penalties.

That needs to be indeed coded inside the spellscripts itself, here are few examples of how I solved this in community patch in NWN1:

Scare:

if(!GetIsImmune(spell.Target,IMMUNITY_TYPE_FEAR,spell.Caster) && !GetIsImmune(spell.Target,IMMUNITY_TYPE_MIND_SPELLS,spell.Caster))
{
//1.70: attack and damage penalty linked together, but only when target not immune
eLink = EffectLinkEffects(eLink, eLink2);
}

Ghoul touch:

        if(!GetIsImmune(oTarget, IMMUNITY_TYPE_POISON, aoe.Creator))
        {
            ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eLink, oTarget, RoundsToSeconds(nDuration));
            ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oTarget);
        }
        else
        {
            //engine workaround to get proper immunity feedback
            ApplyEffectToObject(DURATION_TYPE_PERMANENT, EffectPoison(POISON_ETTERCAP_VENOM), oTarget);
        }

basically you check if the target is immune to the descriptor the spell uses and if he is, you apply on him an effect that the immunity will block and cause the immunity feedback to be printed instead of normal spell effect

i think it’s related. We’re certainly in the same block of code, so to speak

so … nwn1 also applies the effect (even if target is Immune) and the hardcode handles it ? That’s how nwn2 deals with Immunities (99% of the time)

very interesting. So if target is Immune, the CP applies an arbitrary effect (one that you know will trigger the proper Immunity feedback/result) …

this is a difference (iirc) i noticed between the nwn1 and 2 spellscripts: the 2 spellscripts tend not to check GetIsImmune() … but personally I’ve been putting those checks back in (iirc) because … it just seems that special handling is often wanted/needed. (whether for player-feedback or handling secondary spell-effects, eg. or, as you note, whether or not to link effects that target may or may not be Immune to)

If by “descriptor” you mean Spells.2da “ImmunityType” – nwn2 no longer uses that col (apparently). I tend to, even strongly suspect that nwn2 has added some hardcode to deal with Immunities, that nwn1 does not have …

if you’re still interested, these are the stock nwn2 MyResistSpell() and MySavingThrow() functions →

int MyResistSpell(object oCaster,
                  object oTarget,
                  float fDelay = 0.0)
{
    if (fDelay > 0.5)
    {
        fDelay = fDelay - 0.1;
    }

    int nResist = ResistSpell(oCaster, oTarget);

    effect eSR     = EffectVisualEffect(VFX_DUR_SPELL_SPELL_RESISTANCE);
    effect eGlobe  = EffectVisualEffect(VFX_DUR_SPELL_GLOBE_INV_LESS);
    effect eMantle = EffectVisualEffect(VFX_DUR_SPELL_SPELL_MANTLE);

    if (nResist == 1) // Spell Resistance
    {
        DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eSR, oTarget));
    }
    else if (nResist == 2) // Globe
    {
        DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eGlobe, oTarget));
    }
    else if (nResist == 3) // Spell Mantle
    {
        if (fDelay > 0.5)
        {
            fDelay = fDelay - 0.1;
        }
        DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eMantle, oTarget));
    }
    return nResist;
}

int MySavingThrow(int nSavingThrow,
                  object oTarget,
                  int nDC,
                  int nSaveType = SAVING_THROW_TYPE_NONE,
                  object oSaveVersus = OBJECT_SELF,
                  float fDelay = 0.0)
{
    if (nDC<1)
    {
       nDC = 1;
    }
    else if (nDC > 255)
    {
      nDC = 255;
    }

    effect eVis;
    int bValid = FALSE;
    int nSpellID;

    if (nSavingThrow == SAVING_THROW_FORT)
    {
        bValid = FortitudeSave(oTarget, nDC, nSaveType, oSaveVersus);
        if (bValid == 1)
        {
            // no longer using NWN1 VFX; there is no analogous NWN2 VFX
        }
    }
    else if (nSavingThrow == SAVING_THROW_REFLEX)
    {
        bValid = ReflexSave(oTarget, nDC, nSaveType, oSaveVersus);
        if (bValid == 1)
        {
            // no longer using NWN1 VFX; there is no analogous NWN2 VFX
        }
    }
    else if (nSavingThrow == SAVING_THROW_WILL)
    {
        bValid = WillSave(oTarget, nDC, nSaveType, oSaveVersus);
        if (bValid == 1)
        {
            // no longer using NWN1 VFX; there is no analogous NWN2 VFX
        }
    }

    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_WEIRD
                || nSpellID == SPELL_FINGER_OF_DEATH)
            && nSpellID != SPELL_HORRID_WILTING)
        {
            eVis = EffectVisualEffect(VFX_IMP_DEATH);
            DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oTarget));
        }
    }

    if (bValid == 1 || bValid == 2)
    {
        if (bValid == 2)
        {
            eVis = EffectVisualEffect(VFX_DUR_SPELL_SPELL_RESISTANCE);

            /* 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
            returning false and not true. True blocks the application of effects. */

            bValid = FALSE;
        }
        DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oTarget));
    }
    return bValid;
}

Maybe they were trying to fix your initial concern:

like i said, nwn2 does not use the “ImmunityType” col in Spells.2da (or so i believe). I think they [Obsidian] just left it there to rot …

yes, applying the effect that target is immune to in NWN1 will print the immunity feedback “target is immune to effect”

exactly, this is a workaround for the problem that not all effects are blocked by the engine as described above and linking effects doesn’t work correctly either (in NWN1 in some cases it blocks the whole link, in others only the single effect from link while the rest gets applied). So this is a way how to make sure it will behave as rest of the spells.

That is not a difference because this is the case in NWN1 too. This code is my own modification from community patch. In vanilla NWN1 it doesn’t do this and thus obviously has same issue as you are trying to solve - ie. Scare spell applying AB penalty if target is immune (and without any save).

No that is not used in NWN1 either. This is a term from description which determines spell classification/specialization, but it is not actually respected in spells a lot. Another thing I changed in community patch. For example Sound/Sonic descriptor means that the spell won’t work if target doesn’t hear it. Ie. in NWN1 Community Patch, Banshee’s Veil does nothing if the target is silenced/deaf. Etc.

identical with NWN1