ff_u_poisonwp

Poison Weapon: replacement of the x2_s2_poisonwp spell script.

When enabled, the unit overrides x2_s2_poisonwp with itself, so using a poison item (Cast Spell: Poison Weapon) runs this script. It behaves like the original script, with the options below.

The poison item coats a melee weapon, ammunition, shuriken, darts or throwing axes with a temporary on-hit poison. Bludgeoning weapons and weapons that are already poisoned are refused.

The poison is read from des_crft_poison.2da (X2_IP_POISONWEAPON_2DA), at the row given by the last three characters of the poison item’s tag. For example, an item whose tag ends with 007 uses row 7. Rows 0 to 99 are reserved by Bioware. The row gives the save DC, the poison type, the duration of the coating in seconds, and the DC of the application check.

If the module switch MODULE_SWITCH_RESTRICT_USE_POISON_TO_FEAT is set:

  • creatures with the Use Poison feat always succeed;

  • the others cannot apply poison, and poison items are shown as unusable (red).

Otherwise:

  • creatures with the Use Poison feat always succeed, and get “Auto success”;

  • the others stop their actions (provoking attacks of opportunity) and roll d10 + 10 + Dexterity modifier against the ApplyCheckDC column of the row. On a failure, the poison is lost.

Variables

ENABLE__FF_U_POISONWP: (string) (opt-in) Set to "Y" on module to enable the unit.

POISON_DURATION: (int) Duration of the coating, in seconds. Read from the creature applying the poison, then its area, then the module. 0 or unset uses the Duration column of the 2da.

POISON_USE_IN_COMBAT: (string) Set to "N" on module to refuse applying poison while in combat. Defaults to "Y".

Source code

// @code

#include "ff_i_core"
#include "x2_inc_itemprop"
#include "x2_inc_switches"


const string ENABLE__FF_U_POISONWP = "ENABLE__FF_U_POISONWP";

const string POISON_DURATION = "POISON_DURATION";

const string POISON_USE_IN_COMBAT = "POISON_USE_IN_COMBAT";

//! @brief Spell script replaced by this unit.
const string POISONWP_SCRIPT = "x2_s2_poisonwp";

//! @brief iprp_spells.2da row of the Poison Weapon cast spell property.
const int IPRP_SPELL_POISON_WEAPON = 497;

//! @brief On-hit property subtype of an item poison.
const int ONHIT_ITEMPOISON = 19;


// -----------------------------------------------------------------------------

//! @brief Return TRUE if oTarget can be poisoned, or send the reason to oPC.
//! @param oPC The creature using the poison.
//! @param oTarget The item to poison.
int GetCanBePoisoned(object oPC, object oTarget);
int GetCanBePoisoned(object oPC, object oTarget)
{
  if (!GetIsObjectValid(oTarget) || GetObjectType(oTarget) != OBJECT_TYPE_ITEM)
  {
    FloatingTextStrRefOnCreature(83359, oPC); // "Invalid target"
    return FALSE;
  }

  int nType = GetBaseItemType(oTarget);
  if (!IPGetIsMeleeWeapon(oTarget) && !IPGetIsProjectile(oTarget)
      && nType != BASE_ITEM_SHURIKEN && nType != BASE_ITEM_DART && nType != BASE_ITEM_THROWINGAXE)
  {
    FloatingTextStrRefOnCreature(83359, oPC); // "Invalid target"
    return FALSE;
  }

  if (IPGetIsBludgeoningWeapon(oTarget))
  {
    FloatingTextStrRefOnCreature(83367, oPC); // "Weapon does not do slashing or piercing damage"
    return FALSE;
  }

  if (IPGetItemHasItemOnHitPropertySubType(oTarget, ONHIT_ITEMPOISON))
  {
    FloatingTextStrRefOnCreature(83407, oPC); // "Weapon already poisoned"
    return FALSE;
  }

  return TRUE;
}


// -----------------------------------------------------------------------------

//! @brief Return TRUE if oItem is a poison item: it has the Poison Weapon cast spell property.
//! @param oItem An item.
int GetIsPoisonItem(object oItem);
int GetIsPoisonItem(object oItem)
{
  itemproperty ip = GetFirstItemProperty(oItem);
  while (GetIsItemPropertyValid(ip))
  {
    if (GetItemPropertyType(ip) == ITEM_PROPERTY_CAST_SPELL
        && GetItemPropertySubType(ip) == IPRP_SPELL_POISON_WEAPON)
      return TRUE;
    ip = GetNextItemProperty(oItem);
  }
  return FALSE;
}


// -----------------------------------------------------------------------------

//! @brief Make poison items unusable (red in the inventory) for creatures without the Use Poison feat,
//! when the module restricts poison to the feat.
//! @param oCreature The creature the item is validated for.
void OnValidateUseItem(object oCreature);
void OnValidateUseItem(object oCreature)
{
  if (!GetModuleSwitchValue(MODULE_SWITCH_RESTRICT_USE_POISON_TO_FEAT))
    return;

  if (GetHasFeat(FEAT_USE_POISON, oCreature))
    return;

  object oItem = StringToObject(NWNX_Events_GetEventData("ITEM_OBJECT_ID"));
  if (!GetIsPoisonItem(oItem))
    return;

  NWNX_Events_SkipEvent();
  NWNX_Events_SetEventResult("0");
}


// -----------------------------------------------------------------------------

//! @brief Return TRUE if oPC manages to apply the poison, and send the feedback to oPC.
//! Refused in combat if POISON_USE_IN_COMBAT is "N". With the Use Poison feat, always
//! succeeds. Without it, stops oPC's actions (provoking attacks of opportunity), then
//! fails if the module restricts poison to the feat, or rolls d10 + 10 + Dexterity
//! modifier against nApplyDC.
//! @param oPC The creature using the poison.
//! @param nApplyDC DC of the application check.
int GetCanApplyPoison(object oPC, int nApplyDC);
int GetCanApplyPoison(object oPC, int nApplyDC)
{
  if (!GetModuleFlag(POISON_USE_IN_COMBAT, TRUE) && GetIsInCombat(oPC))
  {
    FloatingTextStrRefOnCreature(84420, oPC); // "You can not use this item!"
    return FALSE;
  }

  int bRestricted = GetModuleSwitchValue(MODULE_SWITCH_RESTRICT_USE_POISON_TO_FEAT);

  if (GetHasFeat(FEAT_USE_POISON, oPC))
  {
    if (!bRestricted)
      FloatingTextStrRefOnCreature(83369, oPC); // "Auto success"
    return TRUE;
  }

  // Force attacks of opportunity
  AssignCommand(oPC, ClearAllActions(TRUE));

  if (bRestricted)
  {
    FloatingTextStrRefOnCreature(84420, oPC); // "You can not use this item!"
    return FALSE;
  }

  int nCheck = d10() + 10 + GetAbilityModifier(ABILITY_DEXTERITY, oPC);
  if (nCheck < nApplyDC)
  {
    FloatingTextStrRefOnCreature(83368, oPC); // "Failed"
    return FALSE;
  }

  FloatingTextStrRefOnCreature(83370, oPC); // "Success"
  return TRUE;
}


// -----------------------------------------------------------------------------

//! @brief Poison the target item of the Poison Weapon spell cast by oPC.
//! @param oPC The creature using the poison item.
void OnPoisonWeapon(object oPC);
void OnPoisonWeapon(object oPC)
{
  object oItem = GetSpellCastItem();
  object oTarget = GetSpellTargetObject();

  if (!GetCanBePoisoned(oPC, oTarget))
    return;

  // The 2da row is given by the last three characters of the tag
  int nRow = StringToInt(GetStringRight(GetTag(oItem), 3));
  if (nRow == 0)
  {
    FloatingTextStrRefOnCreature(83360, oPC); // "Nothing happens"
    LogError(__FILE__ + ": item " + GetTag(oItem) + " has the Poison Weapon spell, but its tag does not end with a 3 digit row");
    return;
  }

  int nSaveDC     = StringToInt(Get2DAString(X2_IP_POISONWEAPON_2DA, "SaveDC", nRow));
  int nDuration   = StringToInt(Get2DAString(X2_IP_POISONWEAPON_2DA, "Duration", nRow));
  int nPoisonType = StringToInt(Get2DAString(X2_IP_POISONWEAPON_2DA, "PoisonType", nRow));
  int nApplyDC    = StringToInt(Get2DAString(X2_IP_POISONWEAPON_2DA, "ApplyCheckDC", nRow));

  if (!GetCanApplyPoison(oPC, nApplyDC))
    return;

  // POISON_DURATION overrides the 2da duration
  nDuration = GetRecursiveInt(oPC, POISON_DURATION, nDuration);

  float fDuration = IntToFloat(nDuration);
  itemproperty ip = ItemPropertyOnHitProps(IP_CONST_ONHIT_ITEMPOISON, nSaveDC, nPoisonType);
  IPSafeAddItemProperty(oTarget, ip, fDuration, X2_IP_ADDPROP_POLICY_KEEP_EXISTING, TRUE, TRUE);

  // The item poison sub properties cannot be read back, so this is the best check available
  if (!IPGetItemHasItemOnHitPropertySubType(oTarget, ONHIT_ITEMPOISON))
  {
    FloatingTextStrRefOnCreature(83360, oPC); // "Nothing happens"
    return;
  }

  FloatingTextStrRefOnCreature(83361, oPC); // "Weapon is coated with poison"
  IPSafeAddItemProperty(oTarget, ItemPropertyVisualEffect(ITEM_VISUAL_ACID), fDuration,
    X2_IP_ADDPROP_POLICY_KEEP_EXISTING, TRUE, FALSE);
  ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectVisualEffect(VFX_IMP_PULSE_NATURE), GetItemPossessor(oTarget));
}


// -----------------------------------------------------------------------------

void main()
{

  string sEvent = NWNX_Events_GetCurrentEvent();
  if (sEvent == ON_REGISTER)
  {
    if (!GetModuleFlag(ENABLE__FF_U_POISONWP, FALSE))
      return;

    // Run this script instead of the spell script
    NWNX_Util_SetResourceOverride(RESTYPE_NCS, POISONWP_SCRIPT, RemoveExtension(__FILE__));
    LogNotice(__FILE__ + ": " + POISONWP_SCRIPT + " is overridden");
    SubscribeToEvent(NWNX_ON_VALIDATE_USE_ITEM_BEFORE, __FILE__);
  }
  else if (sEvent == NWNX_ON_VALIDATE_USE_ITEM_BEFORE)
    OnValidateUseItem(OBJECT_SELF);
  // Not an event: run as the overridden spell script
  else if (sEvent == "")
    OnPoisonWeapon(OBJECT_SELF);
}