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.
481 lines
17 KiB
Plaintext
481 lines
17 KiB
Plaintext
//:://////////////////////////////////////////////
|
||
//:: ;-. ,-. ,-. ,-.
|
||
//:: | ) | ) / ( )
|
||
//:: |-' |-< | ;-:
|
||
//:: | | \ \ ( )
|
||
//:: ' ' ' `-' `-'
|
||
//::///////////////////////////////////////////////
|
||
//::
|
||
/*
|
||
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 (){} |