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

//:://////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//::///////////////////////////////////////////////
//::
/*
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 <20> 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 we<77>ll 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 (){}