Files
PRC8/nwn/nwnprc/trunk/include/inc_infusion.nss
Jaysyn904 246f8d56aa 2025/08/22 Update
Reverted to 4.56 to track down a bug that started in 4.57.  Bugfixed back to 4.61.
It's amazing the damage that one skipped case statement can do.
2025-08-22 17:54:55 -04:00

481 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//:://////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//::///////////////////////////////////////////////
//::
/*
Script: inc_infusion
Author: Jaysyn
Created: 2025-08-11 17:01:26
Description:
Contains most functions related to the Create
Infusion feat.
*/
//::
//:://////////////////////////////////////////////
#include "prc_inc_spells"
int GetMaxDivineSpellLevel(object oCaster, int nClass);
int GetCastSpellCasterLevelFromItem(object oItem, int nSpellID);
int GetIsClassSpell(object oCaster, int nSpellID, int nClass);
int GetHasSpellOnClassList(object oCaster, int nSpellID);
void InfusionSecondSave(object oUser, int nDC);
/**
* @brief Finds the class index for which the given spell is available to the specified caster.
*
* This function iterates through all possible classes and returns the first class
* index for which the specified spell is on the caster's spell list.
*
* @param oCaster The creature object to check.
* @param nSpellID The spell ID to find the class for.
*
* @return The class index that has the spell on its class spell list for the caster,
* or -1 if no matching class is found.
*/
int FindSpellCastingClass(object oCaster, int nSpellID)
{
int i = 0;
int nClassFound = -1;
int nClass;
// Only loop through caster's classes
for (i = 0; i <= 8; i++)
{
nClass = GetClassByPosition(i, oCaster);
if (nClass == CLASS_TYPE_INVALID) continue;
if (GetIsClassSpell(oCaster, nSpellID, nClass))
{
nClassFound = nClass;
break;
}
}
return nClassFound;
}
/**
* @brief Performs validation checks to determine if the caster can use a spell infusion from the specified item.
*
* This function verifies that the item is a valid infused herb, checks the caster's relevant class and ability scores,
* confirms the caster is a divine spellcaster with the necessary caster level, and ensures the spell is on the caster's class spell list.
*
* @param oCaster The creature attempting to use the infusion.
* @param oItem The infused herb item containing the spell.
* @param nSpellID The spell ID of the infusion spell being cast.
*
* @return TRUE if all infusion use checks pass and the caster can use the infusion; FALSE otherwise.
*/
int DoInfusionUseChecks(object oCaster, object oItem, int nSpellID)
{
int bPnPHerbs = GetPRCSwitch(PRC_CREATE_INFUSION_OPTIONAL_HERBS);
if(GetBaseItemType(oItem) != BASE_ITEM_INFUSED_HERB)
{
FloatingTextStringOnCreature("Not casting from an Infused Herb", oCaster);
return FALSE;
}
int nItemSpellLvl = GetCastSpellCasterLevelFromItem(oItem, nSpellID);
if (bPnPHerbs && nItemSpellLvl == -1)
{
FloatingTextStringOnCreature("Item has no spellcaster level.", oCaster);
return FALSE;
}
// **CRITICAL: Find the correct class that actually has the spell on its list**
int nClassCaster = FindSpellCastingClass(oCaster, nSpellID);
if(DEBUG) DoDebug("nClassCaster is: " + IntToString(nClassCaster) + ".");
// Check for valid class
if (nClassCaster == -1)
{
FloatingTextStringOnCreature("No valid class found for this spell.", oCaster);
return FALSE;
}
if(GetMaxDivineSpellLevel(oCaster, nClassCaster) < 1 )
{
FloatingTextStringOnCreature("You must be a divine spellcaster to activate an infusion.", oCaster);
return FALSE;
}
// Must have spell on class list - (This will also double-check via the class)
if (!GetHasSpellOnClassList(oCaster, nSpellID))
{
FloatingTextStringOnCreature("You must have a spell on one of your class spell lists to cast it from an infusion.", oCaster);
return FALSE;
}
// Must meet ability requirement: Ability score >= 10 + spell level
int nSpellLevel = PRCGetSpellLevelForClass(nSpellID, nClassCaster);
int nClassAbility = GetAbilityScoreForClass(nClassCaster, oCaster);
if(DEBUG) DoDebug("inc_infusion >> DoInfusionUseChecks: nClassCaster is "+IntToString(nClassCaster)+".");
if(DEBUG) DoDebug("inc_infusion >> DoInfusionUseChecks: Class nSpellLevel is "+IntToString(nSpellLevel)+".");
if(DEBUG) DoDebug("inc_infusion >> DoInfusionUseChecks: nClassAbility is "+IntToString(nClassAbility)+".");
if (nClassAbility < 10 + nSpellLevel)
{
FloatingTextStringOnCreature("You must meet ability score requirement to cast spell from infusion.", oCaster);
return FALSE;
}
// Must have a divine caster level at least equal to infusion's caster level
int nDivineLvl = GetPrCAdjustedCasterLevelByType(TYPE_DIVINE, oCaster);
if(DEBUG) DoDebug("inc_infusion >> DoInfusionUseChecks: nDivineLvl is "+IntToString(nDivineLvl)+".");
if (nDivineLvl < nItemSpellLvl)
{
FloatingTextStringOnCreature("Your divine caster level is too low to cast this spell from an infusion.", oCaster);
return FALSE;
}
return TRUE;
}
/* int DoInfusionUseChecks(object oCaster, object oItem, int nSpellID)
{
int bPnPHerbs = GetPRCSwitch(PRC_CREATE_INFUSION_OPTIONAL_HERBS);
if(GetBaseItemType(oItem) != BASE_ITEM_INFUSED_HERB)
{
FloatingTextStringOnCreature("Not casting from an Infused Herb", oCaster);
return FALSE;
}
int nItemSpellLvl = GetCastSpellCasterLevelFromItem(oItem, nSpellID);
if (bPnPHerbs && nItemSpellLvl == -1)
{
FloatingTextStringOnCreature("Item has no spellcaster level.", oCaster);
return FALSE;
}
// Find relevant class for the spell
int nClassCaster = FindSpellCastingClass(oCaster, nSpellID);
if(DEBUG) DoDebug("nClassCaster is: "+IntToString(nClassCaster)+".");
if(GetMaxDivineSpellLevel(oCaster, nClassCaster) < 1 )
{
FloatingTextStringOnCreature("You must be a divine spellcaster to activate an infusion.", oCaster);
return FALSE;
}
// Must have spell on class list
if (!GetHasSpellOnClassList(oCaster, nSpellID))
{
FloatingTextStringOnCreature("You must have a spell on one of your class spell lists to cast it from an infusion.", oCaster);
return FALSE;
}
// Must meet ability requirement: Ability score >= 10 + spell level
int nSpellLevel = PRCGetSpellLevelForClass(nSpellID, nClassCaster);
int nClassAbility = GetAbilityScoreForClass(nClassCaster, oCaster);
if(DEBUG) DoDebug("inc_infusion >> DoInfusionUseChecks: nClassCaster is "+IntToString(nClassCaster)+".");
if(DEBUG) DoDebug("inc_infusion >> DoInfusionUseChecks: Class nSpellLevel is "+IntToString(nSpellLevel)+".");
if(DEBUG) DoDebug("inc_infusion >> DoInfusionUseChecks: nClassAbility is "+IntToString(nClassAbility)+".");
if (nClassAbility < 10 + nSpellLevel)
{
FloatingTextStringOnCreature("You must meet ability score requirement to cast spell from infusion.", oCaster);
return FALSE;
}
// Must have a divine caster level at least equal to infusion's caster level
int nDivineLvl = GetPrCAdjustedCasterLevelByType(TYPE_DIVINE, oCaster);
if(DEBUG) DoDebug("inc_infusion >> DoInfusionUseChecks: nDivineLvl is "+IntToString(nDivineLvl)+".");
if (nDivineLvl < nItemSpellLvl)
{
FloatingTextStringOnCreature("Your divine caster level is too low to cast this spell from an infusion.", oCaster);
return FALSE;
}
return TRUE;
}
*/
/**
* @brief Retrieves the maximum divine spell level known by the caster for a given class.
*
* This function checks the caster's local integers named "PRC_DivSpell1" through "PRC_DivSpell9"
* in descending order to determine the highest divine spell level available.
* It returns the highest spell level for which the corresponding local int is false (zero).
*
* @param oCaster The creature whose divine spell levels are being checked.
* @param nClass The class index for which to check the divine spell level (currently unused).
*
* @return The highest divine spell level known by the caster (1 to 9).
*/
int GetMaxDivineSpellLevel(object oCaster, int nClass)
{
int i = 9;
for (i; i > 0; i--)
{
if(!GetLocalInt(oCaster, "PRC_DivSpell"+IntToString(i)))
return i;
}
return 1;
}
/**
* @brief Retrieves the spell school of an herb based on its resref by looking it up in the craft_infusion.2da file.
*
* This function searches the "craft_infusion" 2DA for a row matching the herb's resref.
* If found, it returns the corresponding spell school as an integer constant.
* If not found or the SpellSchool column is missing/invalid, it returns -1.
*
* @param oHerb The herb object to check.
*
* @return The spell school constant corresponding to the herb's infusion spell school,
* or -1 if the herb is invalid, not found, or the data is missing.
*/
int GetHerbsSpellSchool(object oHerb)
{
if (!GetIsObjectValid(oHerb)) return -1;
string sResref = GetResRef(oHerb);
int nRow = 0;
string sRowResref;
while (nRow < 200)
{
sRowResref = Get2DACache("craft_infusion", "Resref", nRow);
if (sRowResref == "") break;
if (sRowResref == sResref)
{
string sHerbSpellSchool = Get2DAString("craft_infusion", "SpellSchool", nRow);
if (sHerbSpellSchool == "A") return SPELL_SCHOOL_ABJURATION;
else if (sHerbSpellSchool == "C") return SPELL_SCHOOL_CONJURATION;
else if (sHerbSpellSchool == "D") return SPELL_SCHOOL_DIVINATION;
else if (sHerbSpellSchool == "E") return SPELL_SCHOOL_ENCHANTMENT;
else if (sHerbSpellSchool == "V") return SPELL_SCHOOL_EVOCATION;
else if (sHerbSpellSchool == "I") return SPELL_SCHOOL_ILLUSION;
else if (sHerbSpellSchool == "N") return SPELL_SCHOOL_NECROMANCY;
else if (sHerbSpellSchool == "T") return SPELL_SCHOOL_TRANSMUTATION;
else return SPELL_SCHOOL_GENERAL;
return -1;
}
nRow++;
}
return -1; // Not found
}
/**
* @brief Retrieves the infusion spell level of an herb by matching its resref in the craft_infusion.2da file.
*
* This function searches the "craft_infusion" 2DA for a row matching the herb's resref.
* If found, it returns the spell level from the SpellLevel column as an integer.
* If not found or the column is missing, it returns -1.
*
* @param oHerb The herb object whose infusion spell level is to be retrieved.
*
* @return The spell level as an integer if found, or -1 if the herb is invalid, not found, or the column is missing.
*/
int GetHerbsInfusionSpellLevel(object oHerb)
{
if (!GetIsObjectValid(oHerb)) return -1;
string sResref = GetResRef(oHerb);
int nRow = 0;
string sRowResref;
// Brute-force loop — adjust limit if your 2DA has more than 500 rows
while (nRow < 200)
{
sRowResref = Get2DACache("craft_infusion", "Resref", nRow);
if (sRowResref == "") break; // End of valid rows
if (sRowResref == sResref)
{
string sSpellLevelStr = Get2DAString("craft_infusion", "SpellLevel", nRow);
return StringToInt(sSpellLevelStr);
}
nRow++;
}
return -1; // Not found
}
/**
* @brief Retrieves the caster level of a specific cast-spell item property from an item.
*
* This function iterates through the item properties of the given item, searching for an
* ITEM_PROPERTY_CAST_SPELL_CASTER_LEVEL property that matches the specified spell ID.
* If found, it returns the caster level value stored in the item property.
*
* @param oItem The item object to check.
* @param nSpellID The spell ID to match against the item property.
*
* @return The caster level associated with the matching cast-spell item property,
* or -1 if no matching property is found.
*/
int GetCastSpellCasterLevelFromItem(object oItem, int nSpellID)
{
int nFoundCL = -1;
itemproperty ip = GetFirstItemProperty(oItem);
while (GetIsItemPropertyValid(ip))
{
int nType = GetItemPropertyType(ip);
// First preference: PRC's CASTER_LEVEL itemprop
if (nType == ITEM_PROPERTY_CAST_SPELL_CASTER_LEVEL)
{
int nSubType = GetItemPropertySubType(ip);
string sSpellIDStr = Get2DAString("iprp_spells", "SpellIndex", nSubType);
int nSubSpellID = StringToInt(sSpellIDStr);
if (nSubSpellID == nSpellID)
{
return GetItemPropertyCostTableValue(ip); // Found exact CL
}
}
// Fallback: vanilla CAST_SPELL property
if (nType == ITEM_PROPERTY_CAST_SPELL && nFoundCL == -1)
{
int nSubType = GetItemPropertySubType(ip);
string sSpellIDStr = Get2DAString("iprp_spells", "SpellIndex", nSubType);
int nSubSpellID = StringToInt(sSpellIDStr);
if (nSubSpellID == nSpellID)
{
// Vanilla uses CostTableValue for *number of uses*, not CL,
// so well assume default caster level = spell level * 2 - 1
int nSpellLevel = StringToInt(Get2DAString("spells", "Innate", nSubSpellID));
nFoundCL = nSpellLevel * 2 - 1; // default NWN caster level rule
}
}
ip = GetNextItemProperty(oItem);
}
return nFoundCL; // -1 if not found
}
/**
* @brief Checks if a given spell ID is present on the specified class's spell list for the caster.
*
* This function determines the spell level of the spell for the given class using PRCGetSpellLevelForClass.
* If the spell level is -1, the spell is not on the class's spell list.
* Otherwise, the spell is considered to be on the class spell list.
*
* @param oCaster The creature object casting or querying the spell.
* @param nSpellID The spell ID to check.
* @param nClass The class index to check the spell list against.
*
* @return TRUE if the spell is on the class's spell list; FALSE otherwise.
*/
int GetIsClassSpell(object oCaster, int nSpellID, int nClass)
{
if(DEBUG) DoDebug("inc_infusion >> GetIsClassSpell: nSpellID is: "+IntToString(nSpellID)+".");
if(DEBUG) DoDebug("inc_infusion >> GetIsClassSpell: nClass is: "+IntToString(nClass)+".");
int nSpellLevel = PRCGetSpellLevelForClass(nSpellID, nClass);
if (nSpellLevel == -1)
{
if(DEBUG) DoDebug("inc_infusion >> GetIsClassSpell: SpellLevel is "+IntToString(nSpellLevel)+".");
if(DEBUG) DoDebug("inc_infusion >> GetIsClassSpell: Spell "+IntToString(nSpellID)+" is not in spelllist of "+IntToString(nClass)+".");
return FALSE;
}
return TRUE;
}
/**
* @brief Checks if the caster has the specified spell on any of their class spell lists.
*
* This function iterates through all classes the caster has (up to position 8),
* and returns TRUE if the spell is found on any class's spell list.
*
* @param oCaster The creature object to check.
* @param nSpellID The spell ID to search for.
*
* @return TRUE if the spell is present on at least one of the caster's class spell lists;
* FALSE otherwise.
*/
int GetHasSpellOnClassList(object oCaster, int nSpellID)
{
int i;
for (i = 0; i <= 8; i++)
{
int nClass = GetClassByPosition(i, oCaster);
if (nClass == CLASS_TYPE_INVALID) continue;
if (GetIsClassSpell(oCaster, nSpellID, nClass))
{
if(DEBUG) DoDebug("inc_infusion >> GetHasSpellOnClassList: Class spell found.");
return TRUE;
}
}
if(DEBUG) DoDebug("inc_infusion >> GetHasSpellOnClassList: Class spell not found.");
return FALSE;
}
/**
* @brief Applies a poison nausea effect to the user when infusion use fails.
*
* This function performs an immediate Fortitude saving throw against poison DC based on infusion caster level.
* If the user fails and is not immune to poison, an infusion nausea effect is applied, replacing any existing one.
* A second saving throw is scheduled after 1 minute to attempt to remove the effect.
*
* @param oUser The creature who used the infusion and may be poisoned.
* @param nInfusionCL The caster level of the infusion used, affecting the DC of the saving throw.
*/
void ApplyInfusionPoison(object oUser, int nInfusionCL)
{
int nDC = 10 + (nInfusionCL / 2);
int bImmune = GetIsImmune(oUser, IMMUNITY_TYPE_POISON);
// First save immediately
if (!bImmune && !PRCMySavingThrow(SAVING_THROW_FORT, oUser, nDC, SAVING_THROW_TYPE_POISON))
{
// Remove existing infusion poison nausea effect before applying new
effect eOld = GetFirstEffect(oUser);
while (GetIsEffectValid(eOld))
{
if (GetEffectTag(eOld) == "INFUSION_POISON_TAG")
{
RemoveEffect(oUser, eOld);
break; // Assuming only one effect with this tag
}
eOld = GetNextEffect(oUser);
}
effect eNausea = EffectNausea(oUser, 60.0f);
TagEffect(eNausea, "INFUSION_POISON_TAG");
FloatingTextStringOnCreature("The infusion has made you nauseous.", oUser);
ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eNausea, oUser, RoundsToSeconds(10));
}
// Second save 1 minute later
if (!bImmune)
{
DelayCommand(60.0, InfusionSecondSave(oUser, nDC));
}
}
void InfusionSecondSave(object oUser, int nDC)
{
if (!PRCMySavingThrow(SAVING_THROW_FORT, oUser, nDC, SAVING_THROW_TYPE_POISON))
{
FloatingTextStringOnCreature("The infusion has made you nauseous.", oUser);
ApplyEffectToObject(DURATION_TYPE_TEMPORARY, EffectNausea(oUser, 60.0f), oUser, RoundsToSeconds(10));
}
}
//:: void main (){}