2026/02/14 Updates

Updated PRC8 version.
Hathran can now select an ethran as a cohort.
Preliminary Circle Magic work done.
Added Choke Hold, Pain Touch, Ki Shout, Great Ki Shout, Freezing the Lifeblood, Falling Star Strikea nd Unbalancing Strike feats.
Warforged get Immunity Energy Drain, not Immunity: Ability Drain.
Forsakers can use alchemical items.
Added VectorToPerpendicular().
Added GetIsAlchemical().
Added GenerateRandomName().
Added _DoChokeHold().
Updated Shaman bonus feat list.
Updated fighter bonus feat lists.
Added Favored of the Companions to the Vow of Poverty bonus feat list.
Ur-Priest can't enter RKV, BFZ or Thrall of Orcus.
This commit is contained in:
Jaysyn904
2026-02-14 19:53:55 -05:00
parent 066590fe88
commit 41a3c945f9
94 changed files with 51583 additions and 49651 deletions

View File

@@ -0,0 +1,58 @@
//:://////////////////////////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//:://////////////////////////////////////////////////////////////////
//;;
//:: ft_choke_hold.nss
//;:
//:://////////////////////////////////////////////////////////////////
//:
/*
Choke Hold
(Oriental Adventures, p. 61)
[General]
You have learned the correct way to apply pressure to render an
opponent unconscious.
Prerequisite
Improved Unarmed Strike, Improved Grapple, Stunning Fist
Required for
Mighty Works Mastery I
Benefit
If you pin your opponent while grappling and maintain the pin for
1 full round, at the end of the round your opponent must make a
Fortitude saving throw (DC 10 + 1/2 your level + your Wisdom
modifier). If the saving throw fails, your opponent falls
unconscious for 1d3 rounds.
*/
//:
//:://////////////////////////////////////////////////////////////////
//::
//:: Created by: Jaysyn
//:: Created on: 2026-02-13 20:54:17
//::
//:://////////////////////////////////////////////////////////////////
#include "prc_inc_combmove"
void main()
{
object oPC = OBJECT_SELF;
object oTarget = GetSpellTargetObject();
// First, initiate a grapple. If this fails, attack normally.
if (!DoGrapple(oPC, oTarget, 0, TRUE, FALSE))
{
AssignCommand(oPC, ActionAttack(oTarget));
return;
}
DoGrappleOptions(oPC, oTarget, 0, GRAPPLE_PIN);
}

View File

@@ -0,0 +1,105 @@
//:://////////////////////////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//:://////////////////////////////////////////////////////////////////
//;;
//:: ft_fall_star_stk.nss
//;:
//:://////////////////////////////////////////////////////////////////
//:
/*
Falling Star Strike
(Oriental Adventures, p. 62)
[General]
You have mastered the art of striking a nerve that blinds a
humanoid opponent.
Prerequisite
Improved Unarmed Strike (PH) , Stunning Fist (PH) (or monk's
stunning attack) , Base attack bonus +4, WIS 17
Required for
Meditation of War Mastery (OA)
Benefit
Against a humanoid opponent, you can make an unarmed attack that
has a chance of blinding your target. If your attack is successful,
your target must attempt a Fortitude saving throw (DC 10 + 1/2 your
level + your Wisdom modifier). If the target fails this saving throw,
he is blinded for 1 round per level you possess. In addition to the
obvious effects, a blinded creature suffers a 50% miss chance in
combat (all opponents have full concealment), loses any Dexterity
bonus to AC, grants a +2 bonus on attackers' attack rolls (they
are effectively invisible), moves at half speed, and suffers a <20>4
penalty on most Strength- and Dexterity-based skills.
Using this feat uses up one of your stunning attacks for the day (
either a monk stunning attack or a use of the Stunning Fist feat).
*/
//:
//:://////////////////////////////////////////////////////////////////
//::
//:: Created by: Jaysyn
//:: Created on: 2026-02-13 07:58:24
//::
//:://////////////////////////////////////////////////////////////////
#include "prc_inc_spells"
#include "prc_inc_combat"
#include "prc_inc_stunfist"
void DoFallingStarStrike(object oTarget, object oInitiator)
{
if (!PRCAmIAHumanoid(oTarget))
{
SendMessageToPC(oInitiator, "Target is not humanoid.");
AssignCommand(oInitiator, ActionAttack(oTarget)); // fallback
return;
}
if (!GetIsUnarmed(oInitiator))
{
SendMessageToPC(oInitiator, "You must be unarmed to use this ability.");
AssignCommand(oInitiator, ActionAttack(oTarget)); // fallback
return;
}
if (!ExpendStunfistUses(oInitiator, 1))
{
SendMessageToPC(oInitiator, "You have no stunning fist uses remaining.");
AssignCommand(oInitiator, ActionAttack(oTarget)); // fallback
return;
}
int iLevel = GetHitDice(oInitiator);
effect eHit = EffectVisualEffect(VFX_COM_HIT_SONIC);
PerformAttack(oTarget, oInitiator, eHit, 0.0, 0, 0, 0,
"Falling Star Strike Hit", "Falling Star Strike Miss");
if (GetLocalInt(oTarget, "PRCCombat_StruckByAttack"))
{
int nDC = 10 + (iLevel / 2) + GetAbilityModifier(ABILITY_WISDOM, oInitiator);
if (!PRCMySavingThrow(SAVING_THROW_FORT, oTarget, nDC, SAVING_THROW_TYPE_NONE))
{
effect eBlind = EffectBlindness();
SPApplyEffectToObject(DURATION_TYPE_TEMPORARY, eBlind, oTarget,
RoundsToSeconds(iLevel));
}
}
else
{
//:: To prevent being flatfooted.
AssignCommand(oInitiator, ActionAttack(oTarget));
}
}
void main()
{
object oInitiator = OBJECT_SELF;
object oTarget = PRCGetSpellTargetObject();
DoFallingStarStrike(oTarget, oInitiator);
}

View File

@@ -0,0 +1,112 @@
//:://////////////////////////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//:://////////////////////////////////////////////////////////////////
//;;
//:: ft_frz_lifeblood.nss
//;:
//:://////////////////////////////////////////////////////////////////
//:
/*
Freezing The Lifeblood
(Complete Warrior, p. 99)
[Fighter Bonus Feat, General]
You can paralyze a humanoid opponent with an unarmed attack.
Prerequisite
Improved Unarmed Strike (PH) , Stunning Fist (PH) , WIS 17,
base attack bonus +10,
Benefit
Declare that you are using this feat before you make your attack
roll (thus, a missed attack roll ruins the attempt). Against a
humanoid opponent, you can make an unarmed attack that deals no
damage but has a chance of paralyzing your target. If your attack
is successful, your target must attempt a Fortitude saving throw
(DC 10 + 1/2 your character level + your Wis modifier). If the
target fails this saving throw, it is paralyzed for 1d4+1 rounds.
Each attempt to paralyze an opponent counts as one of your uses of
the Stunning Fist feat for the day. Creatures immune to stunning
cannot be paralyzed in this manner.
Special
A fighter may select Freezing the Lifeblood as one of his fighter
bonus feats.
*/
//:
//:://////////////////////////////////////////////////////////////////
//::
//:: Created by: Jaysyn
//:: Created on: 2026-02-13 18:31:43
//::
//:://////////////////////////////////////////////////////////////////
#include "prc_inc_spells"
#include "prc_inc_combat"
#include "prc_inc_stunfist"
void DoFreezingTheLifeblood(object oTarget, object oInitiator)
{
if (!PRCAmIAHumanoid(oTarget))
{
SendMessageToPC(oInitiator, "Target is not humanoid.");
AssignCommand(oInitiator, ActionAttack(oTarget)); // fallback
return;
}
if (!GetIsUnarmed(oInitiator))
{
SendMessageToPC(oInitiator, "You must be unarmed to use this ability.");
AssignCommand(oInitiator, ActionAttack(oTarget)); // fallback
return;
}
if (!ExpendStunfistUses(oInitiator, 1))
{
SendMessageToPC(oInitiator, "You have no stunning fist uses remaining.");
AssignCommand(oInitiator, ActionAttack(oTarget)); // fallback
return;
}
// Simulate unarmed attack without damage
object oWeap = GetUnarmedWeapon(oInitiator);
int nHit = GetAttackRoll(oTarget, oInitiator, oWeap);
if (nHit > 0) // Hit
{
int nDC = 10 + (GetHitDice(oInitiator) / 2) + GetAbilityModifier(ABILITY_WISDOM, oInitiator);
if (!PRCMySavingThrow(SAVING_THROW_FORT, oTarget, nDC, SAVING_THROW_TYPE_NONE))
{
if (GetIsImmune(oTarget, IMMUNITY_TYPE_STUN))
{
SendMessageToPC(oInitiator, "Target is immune to stunning.");
return;
}
float fDuration = RoundsToSeconds(d4() + 1);
effect eParal = EffectParalyze();
effect eDur = EffectVisualEffect(VFX_DUR_CESSATE_NEGATIVE);
effect eDur2 = EffectVisualEffect(VFX_DUR_PARALYZED);
effect eDur3 = EffectVisualEffect(VFX_DUR_PARALYZE_HOLD);
effect eLink = EffectLinkEffects(eDur2, eDur);
eLink = EffectLinkEffects(eLink, eParal);
eLink = EffectLinkEffects(eLink, eDur3);
SPApplyEffectToObject(DURATION_TYPE_TEMPORARY, eLink, oTarget, fDuration, TRUE, -1, GetHitDice(oInitiator));
}
}
else
{
//:: To prevent being flatfooted.
AssignCommand(oInitiator, ActionAttack(oTarget));
}
}
void main()
{
object oInitiator = OBJECT_SELF;
object oTarget = PRCGetSpellTargetObject();
DoFreezingTheLifeblood(oTarget, oInitiator);
}

View File

@@ -0,0 +1,182 @@
//:://////////////////////////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//:://////////////////////////////////////////////////////////////////
//;;
//:: ft_oa_ki_shout.nss
//;:
//:://////////////////////////////////////////////////////////////////
//:
/*
Ki Shout
(Oriental Adventures, p. 64)
[General]
You can bellow forth a ki-empowered shout that strikes terror into
your enemies.
Prerequisite
Base attack bonus +1, CHA 13
Required for
Empty Hand Mastery (OA) , Great Ki Shout (OA) , Mighty Works
Mastery II (OA)
Benefit
Making a ki shout is a standard action. Opponents who can hear
your shout and who are within 30 feet of you may become shaken for
1d6 rounds. The ki shout affects only opponents with fewer Hit
Dice or levels than you have. An affected opponent can resist the
effects with a successful Will save against a DC of 10 + 1/2 your
character level + your Charisma modifier. You can use Ki Shout
once per day.
Shaken characters suffer a -2 morale penalty on attack rolls,
saves, and checks.
Great Ki Shout
(Oriental Adventures, p. 63)
[General]
Your ki shout can panic your opponents.
Prerequisite
Ki Shout (OA) , CHA 13, Base attack bonus +9
Benefit
When you make a ki shout, your opponents are panicked for 2d6
rounds unless they succeed at their Will saves (DC 10 + 1/2 your
character level + your Charisma modifier). Panicked characters
suffer a <20>2 morale penalty on attack rolls, saves, and checks,
they have a 50% chance to drop what they are holding, and they
run away from you as quickly as they can. The effects of being
panicked supersede the effects of being shaken.
*/
//:
//:://////////////////////////////////////////////////////////////////
//::
//:: Created by: Jaysyn
//:: Created on: 2026-02-13 18:31:43
//::
//:://////////////////////////////////////////////////////////////////
#include "prc_inc_spells"
// Copy non-plot item from a slot to the ground, confirm, then destroy original; notify victim
void ForceDropSlot(object oTarget, int nSlot)
{
object oItem = GetItemInSlot(nSlot, oTarget);
if (!GetIsObjectValid(oItem) || GetPlotFlag(oItem)) return;
// Ensure droppable and not stolen
SetDroppableFlag(oItem, TRUE);
SetStolenFlag(oItem, FALSE);
// Copy to the ground at target's location
location lLoc = GetLocation(oTarget);
object oCopy = CopyObject(oItem, lLoc, OBJECT_INVALID);
if (GetIsObjectValid(oCopy))
{
DestroyObject(oItem);
SendMessageToPC(oTarget, "You dropped " + GetName(oItem) + ".");
}
}
void main()
{
object oPC = OBJECT_SELF;
int bGreat = GetHasFeat(FEAT_GREAT_KI_SHOUT, oPC);
if (!bGreat && !GetHasFeat(FEAT_KI_SHOUT, oPC, TRUE)) return;
// Effects
effect eShaken = ExtraordinaryEffect(EffectShaken());
effect ePanicked = ExtraordinaryEffect(EffectLinkEffects(eShaken, EffectFrightened()));
ePanicked = EffectLinkEffects(ePanicked, EffectVisualEffect(VFX_DUR_MIND_AFFECTING_FEAR));
// Radius and duration
float fRadius = FeetToMeters(30.0f);
float fDuration = RoundsToSeconds(bGreat ? d6(2) : d6());
// DC
int nDC = 10 + (GetHitDice(oPC) / 2) + GetAbilityModifier(ABILITY_CHARISMA, oPC);
int nPCHD = GetHitDice(oPC);
string sPCID = ObjectToString(oPC);
int bDoVFX = FALSE;
// Loop targets
location lLoc = GetLocation(oPC);
object oTarget = GetFirstObjectInShape(SHAPE_SPHERE, fRadius, lLoc, TRUE);
while (GetIsObjectValid(oTarget))
{
if(DEBUG) DoDebug("Evaluating: " + GetName(oTarget) + " HD=" + IntToString(GetHitDice(oTarget))
+ " ImmFear=" + IntToString(GetIsImmune(oTarget, IMMUNITY_TYPE_FEAR))
+ " Saved=" + IntToString(GetLocalInt(oTarget, "KiShout_SavedVs" + sPCID)));
if (oTarget != oPC &&
!GetLocalInt(oTarget, "KiShout_SavedVs" + sPCID) &&
spellsIsTarget(oTarget, SPELL_TARGET_SELECTIVEHOSTILE, oPC) &&
nPCHD > GetHitDice(oTarget))
{
bDoVFX = TRUE;
SignalEvent(oTarget, EventSpellCastAt(oPC, -1, TRUE));
if (!PRCMySavingThrow(SAVING_THROW_WILL, oTarget, nDC, SAVING_THROW_TYPE_FEAR, oPC) &&
!GetIsImmune(oTarget, IMMUNITY_TYPE_FEAR, oPC))
{
if (bGreat)
{
object oItemL = GetItemInSlot(INVENTORY_SLOT_LEFTHAND, oTarget);
object oItemR = GetItemInSlot(INVENTORY_SLOT_RIGHTHAND, oTarget);
if (d100() < 50)
{
if (GetIsObjectValid(oItemL))
{
//SendMessageToPC(oPC, GetName(oTarget)+" is dropping "+GetName(oItemL)+".");
AssignCommand(oTarget, ForceDropSlot(oTarget, INVENTORY_SLOT_LEFTHAND));
}
}
if (d100() < 50)
{
if (GetIsObjectValid(oItemR))
{
//SendMessageToPC(oPC, GetName(oTarget)+" is dropping "+GetName(oItemR)+".");
AssignCommand(oTarget, ForceDropSlot(oTarget, INVENTORY_SLOT_RIGHTHAND));
}
}
// Apply panicked effect after drops
DelayCommand(0.1f, ApplyEffectToObject(DURATION_TYPE_TEMPORARY, ePanicked, oTarget, fDuration));
}
else
{
// Apply shaken
SPApplyEffectToObject(DURATION_TYPE_TEMPORARY, eShaken, oTarget, fDuration);
}
}
else
{
// 24h immunity
SetLocalInt(oTarget, "KiShout_SavedVs" + sPCID, TRUE);
AssignCommand(oTarget, DelayCommand(HoursToSeconds(24),
DeleteLocalInt(oTarget, "KiShout_SavedVs" + sPCID)));
}
}
oTarget = GetNextObjectInShape(SHAPE_SPHERE, fRadius, lLoc, TRUE);
}
// VFX
if (bDoVFX)
ApplyEffectToObject(DURATION_TYPE_INSTANT,
EffectVisualEffect(GetGender(oPC) == GENDER_FEMALE ? VFX_FNF_HOWL_WAR_CRY_FEMALE : VFX_FNF_HOWL_WAR_CRY),
oPC);
}

View File

@@ -0,0 +1,106 @@
//:://////////////////////////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//:://////////////////////////////////////////////////////////////////
//;;
//:: ft_pain_touch.nss
//;:
//:://////////////////////////////////////////////////////////////////
//:
/*
Pain Touch
(Complete Warrior, p. 103)
[General]
You cause intense pain in an opponent with a successful stunning attack.
Prerequisite
Stunning Fist, WIS 15, base attack bonus +2
Benefit
Victims of a successful stunning attack are subject to such
debilitating pain that they are nauseated for 1 round following
the round they are stunned. Creatures that are immune to stunning
attacks are also immune to the effect of this feat, as are any
creatures that are more than one size category larger than the
feat user.
*/
//:
//:://////////////////////////////////////////////////////////////////
//::
//:: Created by: Jaysyn
//:: Created on: 2026-02-14 19:37:06
//::
//:://////////////////////////////////////////////////////////////////
#include "prc_inc_combat"
#include "prc_inc_stunfist"
#include "prc_effect_inc"
void main()
{
object oPC = OBJECT_SELF;
object oTarget = PRCGetSpellTargetObject();
// Must target a humanoid
if (!PRCAmIAHumanoid(oTarget))
{
SendMessageToPC(oPC, "Target is not humanoid.");
AssignCommand(oPC, ActionAttack(oTarget));
return;
}
// Must be unarmed
if (!GetIsUnarmed(oPC))
{
SendMessageToPC(oPC, "You must be unarmed to use this ability.");
AssignCommand(oPC, ActionAttack(oTarget));
return;
}
// Check size restriction
int nAttackerSize = PRCGetCreatureSize(oPC);
int nTargetSize = PRCGetCreatureSize(oTarget);
if(nTargetSize > nAttackerSize + 1)
{
SendMessageToPC(oPC, "Target is too large for Pain Touch");
AssignCommand(oPC, ActionAttack(oTarget));
return;
}
// Check stunning fist uses
if(!ExpendStunfistUses(oPC, 1))
{
SendMessageToPC(oPC, "No stunning fist uses remaining");
AssignCommand(oPC, ActionAttack(oTarget));
return;
}
// Prepare stun effect
effect eStun = EffectStunned();
effect eVis = EffectVisualEffect(VFX_DUR_MIND_AFFECTING_DISABLED);
eStun = EffectLinkEffects(eStun, eVis);
eStun = SupernaturalEffect(eStun);
// Perform attack
string sSuccess = "*Pain Touch Hit*";
string sMiss = "*Pain Touch Miss*";
PerformAttackRound(oTarget, oPC, eStun, RoundsToSeconds(1), 0, 0, 0, FALSE, sSuccess, sMiss);
// Check if attack hit and apply delayed nausea
if(GetLocalInt(oTarget, "PRCCombat_StruckByAttack"))
{
// Apply nausea 1 round after stun (6 second delay)
DelayCommand(6.0f, ApplyEffectToObject(DURATION_TYPE_TEMPORARY,
EffectNausea(oTarget, 6.0f), oTarget, RoundsToSeconds(1)));
}
else
{
//:: To prevent being flatfooted.
AssignCommand(oPC, ActionAttack(oTarget));
}
}

View File

@@ -0,0 +1,102 @@
//:://////////////////////////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//:://////////////////////////////////////////////////////////////////
//;;
//:: ft_unbal_strike.nss
//;:
//:://////////////////////////////////////////////////////////////////
//:
/*
Unbalancing Strike
(Oriental Adventures, p. 66)
[General]
You can strike a humanoid opponent's joints to knock your target
off balance. This feat is called kuzushi in Rokugan.
Prerequisite
Improved Unarmed Strike (PH) , Stunning Fist (PH) (or monk's
stunning attack) , WIS 15
Benefit
Against a humanoid opponent, you can make an unarmed attack that
has a chance of unbalancing your target. If your attack is
successful, you deal normal damage and your target must attempt
a Reflex saving throw (DC 10 + 1/2 your level + your Wisdom
modifier). If the target fails this saving throw, he is thrown off
balance for 1 round, losing any Dexterity bonus to AC and giving
attackers a +2 bonus on their attack rolls.
*/
//:
//:://////////////////////////////////////////////////////////////////
//::
//:: Created by: Jaysyn
//:: Created on: 2026-02-13 20:54:17
//::
//:://////////////////////////////////////////////////////////////////
#include "prc_inc_spells"
#include "prc_inc_combat"
#include "prc_inc_stunfist"
void DoUnbalancingStrike(object oTarget, object oInitiator)
{
// Must target a humanoid
if (!PRCAmIAHumanoid(oTarget))
{
SendMessageToPC(oInitiator, "Target is not humanoid.");
AssignCommand(oInitiator, ActionAttack(oTarget));
return;
}
// Must be unarmed
if (!GetIsUnarmed(oInitiator))
{
SendMessageToPC(oInitiator, "You must be unarmed to use this ability.");
AssignCommand(oInitiator, ActionAttack(oTarget));
return;
}
// Optional: expend a Stunning Fist use if you want it to consume one
if (!ExpendStunfistUses(oInitiator, 1))
{
SendMessageToPC(oInitiator, "You have no stunning fist uses remaining.");
AssignCommand(oInitiator, ActionAttack(oTarget));
return;
}
// Perform a normal unarmed attack
effect eNone;
PerformAttack(oTarget, oInitiator, eNone, 0.0, 0, 0, 0,
"Unbalancing Strike Hit", "Unbalancing Strike Miss");
if (GetLocalInt(oTarget, "PRCCombat_StruckByAttack"))
{
int nDC = 10 + (GetHitDice(oInitiator) / 2) + GetAbilityModifier(ABILITY_WISDOM, oInitiator);
if (!PRCMySavingThrow(SAVING_THROW_REFLEX, oTarget, nDC, SAVING_THROW_TYPE_NONE))
{
// Lose Dex bonus to AC: apply an AC penalty equal to the target<65>s Dex modifier
int nDexPenalty = GetAbilityModifier(ABILITY_DEXTERITY, oTarget);
effect eLink = EffectLinkEffects(eLink, EffectACDecrease(2+nDexPenalty, AC_DODGE_BONUS));
eLink = ExtraordinaryEffect(eLink);
SPApplyEffectToObject(DURATION_TYPE_TEMPORARY, eLink, oTarget, RoundsToSeconds(1));
}
}
else
{
//:: To prevent being flatfooted.
AssignCommand(oInitiator, ActionAttack(oTarget));
}
}
void main()
{
object oInitiator = OBJECT_SELF;
object oTarget = PRCGetSpellTargetObject();
DoUnbalancingStrike(oTarget, oInitiator);
}

View File

@@ -0,0 +1,178 @@
//:://////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//:://////////////////////////////////////////////
//::
/*
Circle Leader
Type of Feat: Class Specific
Prerequisite: Red Wizard level 5 or Hathran 5.
Specifics: Allows caster to initiate Circle
Magic. Two to four participants can sacrifice
spells to augment the Circle Leader's spell
casting abilities for one rest cycle.
Use: Activate Feat.
*/
//::
//:://////////////////////////////////////////////
//:: Script: prc_circle_lead.nss
//:: Author: Jaysyn
//:: Created: 2026-02-10 12:19:50
//:://////////////////////////////////////////////
#include "prc_inc_spells"
#include "prc_inc_burn"
#include "x2_inc_spellhook"
// Helper to apply delayed visual transform
void DelayedApplyTransform(object oTarget)
{
if (DEBUG) DoDebug("Starting to float!");
SetObjectVisualTransform(oTarget, OBJECT_VISUAL_TRANSFORM_TRANSLATE_Z, 1.0f,
OBJECT_VISUAL_TRANSFORM_LERP_SMOOTHERSTEP, 3.0f);
}
// Helper to clean up all circle effects and events
void CleanupCircle(object oLeader)
{
if (DEBUG) DoDebug("Cleaning up circle for " + GetName(oLeader));
// Clear animation
if (GetLocalInt(oLeader, "CircleMagicAnimating"))
{
AssignCommand(oLeader, ClearAllActions());
DeleteLocalInt(oLeader, "CircleMagicAnimating");
}
// Clear VFX by tag
effect eVFX = GetFirstEffect(oLeader);
while (GetIsEffectValid(eVFX))
{
if (GetEffectTag(eVFX) == "CircleMagicVFX")
DelayCommand(2.5f, RemoveEffect(oLeader, eVFX));
eVFX = GetNextEffect(oLeader);
}
// Reset floating transform with lerp
if (GetLocalInt(oLeader, "CircleMagicFloating"))
{
SetObjectVisualTransform(oLeader, OBJECT_VISUAL_TRANSFORM_TRANSLATE_Z, 0.0f,
OBJECT_VISUAL_TRANSFORM_LERP_SMOOTHERSTEP, 3.0f);
DeleteLocalInt(oLeader, "CircleMagicFloating");
}
// Clear state variables
DeleteLocalInt(oLeader, "CircleMagicActive");
DeleteLocalInt(oLeader, "CircleMagicTotal");
DeleteLocalString(oLeader, "CircleMagicClass");
DeleteLocalInt(oLeader, "CircleMagicMaxParticipants");
DeleteLocalInt(oLeader, "CircleMagicParticipants");
DeleteLocalLocation(oLeader, "CircleMagicStartLoc");
// Remove events
if (DEBUG) DoDebug("Removing HB and OnDamaged events for " + GetName(oLeader));
RemoveEventScript(oLeader, EVENT_ONHEARTBEAT, "prc_circle_lead");
RemoveEventScript(oLeader, EVENT_VIRTUAL_ONDAMAGED, "prc_od_conc");
}
void main()
{
if (DEBUG) DoDebug("prc_circle_lead script executed for " + GetName(OBJECT_SELF));
int nEvent = GetRunningEvent();
object oLeader = OBJECT_SELF;
// Initial feat activation
if (nEvent == FALSE)
{
if (DEBUG) DoDebug("Circle Leader feat activation running");
// Toggle off if already active
if (GetLocalInt(oLeader, "CircleMagicActive"))
{
FloatingTextStringOnCreature("You stop leading the circle.", oLeader);
CleanupCircle(oLeader);
return;
}
// Determine max participants and class tag
int bIsGreat = GetHasFeat(FEAT_GREAT_CIRCLE_LEADER_THAYAN, oLeader) ||
GetHasFeat(FEAT_GREAT_CIRCLE_LEADER_RASHEMAN, oLeader);
int nMaxParticipants = bIsGreat ? 9 : 4;
string sClassTag;
if (GetHasFeat(FEAT_CIRCLE_LEADER_THAYAN, oLeader) ||
GetHasFeat(FEAT_GREAT_CIRCLE_LEADER_THAYAN, oLeader))
sClassTag = "RED_WIZARD";
else if (GetHasFeat(FEAT_CIRCLE_LEADER_RASHEMAN, oLeader) ||
GetHasFeat(FEAT_GREAT_CIRCLE_LEADER_RASHEMAN, oLeader))
sClassTag = "HATHRAN";
// Class gating for Great Circle Leader variants
if ((sClassTag == "RED_WIZARD" && GetLevelByClass(CLASS_TYPE_RED_WIZARD, oLeader) == 0) ||
(sClassTag == "HATHRAN" && GetLevelByClass(CLASS_TYPE_HATHRAN, oLeader) == 0))
{
FloatingTextStringOnCreature("You do not qualify to lead this circle.", oLeader);
return;
}
// Initialize circle state
SetLocalInt(oLeader, "CircleMagicActive", TRUE);
SetLocalInt(oLeader, "CircleMagicTotal", 0);
SetLocalInt(oLeader, "CircleMagicParticipants", 0);
SetLocalInt(oLeader, "CircleMagicMaxParticipants", nMaxParticipants);
SetLocalString(oLeader, "CircleMagicClass", sClassTag);
SetLocalLocation(oLeader, "CircleMagicStartLoc", GetLocation(oLeader));
// Start animation and mark
AssignCommand(oLeader, ActionPlayAnimation(ANIMATION_LOOPING_MEDITATE, 1.0, HoursToSeconds(24)));
SetLocalInt(oLeader, "CircleMagicAnimating", TRUE);
// Apply VFX (tagged for removal)
effect eVFX1 = EffectVisualEffect(VFX_DUR_GLOW_PURPLE);
effect eVFX2 = EffectVisualEffect(VFX_DUR_BLUESHIELDPROTECT);
effect eVFX3 = EffectVisualEffect(PSI_DUR_BURST);
// Dummy valid effect so the link isn<73>t VFX-only
effect eDummy = EffectCutsceneGhost();
effect eLink = EffectLinkEffects(eDummy, eVFX1);
eLink = EffectLinkEffects(eLink, eVFX2);
eLink = EffectLinkEffects(eLink, eVFX3);
eLink = TagEffect(eLink, "CircleMagicVFX");
ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eLink, oLeader, HoursToSeconds(24));
// Apply floating transform after a short delay
DelayCommand(0.1f, DelayedApplyTransform(oLeader));
SetLocalInt(oLeader, "CircleMagicFloating", TRUE);
// Register heartbeat and OnDamaged for concentration
AddEventScript(oLeader, EVENT_ONHEARTBEAT, "prc_circle_lead", TRUE, FALSE);
AddEventScript(oLeader, EVENT_VIRTUAL_ONDAMAGED, "prc_od_conc", TRUE, FALSE);
// Command eligible henchmen to use Circle Magic feat
int i = 1;
object oAssoc = GetAssociate(ASSOCIATE_TYPE_HENCHMAN, oLeader, i);
while (GetIsObjectValid(oAssoc))
{
if (GetHasFeat(FEAT_CIRCLE_MAGIC, oAssoc))
{
if ((sClassTag == "RED_WIZARD" && GetLevelByClass(CLASS_TYPE_RED_WIZARD, oAssoc) > 0) ||
(sClassTag == "HATHRAN" && GetLevelByClass(CLASS_TYPE_HATHRAN, oAssoc) > 0))
{
AssignCommand(oAssoc, ActionUseFeat(FEAT_CIRCLE_MAGIC, oLeader));
}
}
i++;
oAssoc = GetAssociate(ASSOCIATE_TYPE_HENCHMAN, oLeader, i);
}
FloatingTextStringOnCreature("Circle opened (" + IntToString(nMaxParticipants) + " participants max).", oLeader);
}
else if (nEvent == EVENT_ONHEARTBEAT)
{
if (DEBUG) DoDebug("Circle Leader HB running");
// Concentration break check
if (X2GetBreakConcentrationCondition(oLeader))
{
FloatingTextStringOnCreature("Your concentration is broken; the circle collapses.", oLeader);
CleanupCircle(oLeader);
return;
}
// Movement check
location lStart = GetLocalLocation(oLeader, "CircleMagicStartLoc");
float fDist = GetDistanceBetweenLocations(lStart, GetLocation(oLeader));
if (DEBUG) DoDebug("Distance from start: " + FloatToString(fDist));
if (fDist > 2.0f)
{
FloatingTextStringOnCreature("You moved too far; the circle collapses.", oLeader);
CleanupCircle(oLeader);
return;
}
}
}

View File

@@ -0,0 +1,150 @@
//:://////////////////////////////////////////////
//:: ;-. ,-. ,-. ,-.
//:: | ) | ) / ( )
//:: |-' |-< | ;-:
//:: | | \ \ ( )
//:: ' ' ' `-' `-'
//:://////////////////////////////////////////////
//::
/*
Circle Magic
Type of Feat: Class Specific
Prerequisite: None
Specifics: Allows caster to participate in
Circle Magic. Caster sacrifices a spell to
augment the casting power of the Circle Leader.
Use: Activate feat, target circle leader,
select and cast spell.
*/
//::
//:://////////////////////////////////////////////
//:: Script: prc_circle_magic.nss
//:: Author: Jaysyn
//:: Created: 2026-02-10 12:19:50
//:://////////////////////////////////////////////
#include "prc_inc_spells"
#include "x2_inc_spellhook"
void main()
{
int nEvent = GetRunningEvent();
object oPC = OBJECT_SELF;
// Initial feat activation
if (nEvent == FALSE)
{
object oLeader = GetSpellTargetObject();
// Prevent leader from joining their own circle
if (oLeader == oPC)
{
FloatingTextStringOnCreature("You cannot join a circle you lead as a channeler.", oPC);
return;
}
// Validate target is a Circle Leader with active circle
if (!GetHasFeat(FEAT_CIRCLE_LEADER_RASHEMAN, oLeader) &&
!GetHasFeat(FEAT_CIRCLE_LEADER_THAYAN, oLeader) &&
!GetHasFeat(FEAT_GREAT_CIRCLE_LEADER_THAYAN, oLeader) &&
!GetHasFeat(FEAT_GREAT_CIRCLE_LEADER_RASHEMAN, oLeader))
{
FloatingTextStringOnCreature("Target is not a Circle Leader.", oPC);
return;
}
if (!GetLocalInt(oLeader, "CircleMagicActive"))
{
FloatingTextStringOnCreature("Circle is not active.", oPC);
return;
}
// Start channeling for 1 in-game hour; concentration/range checks in event script
SetLocalInt(oPC, "CircleMagicChanneling", TRUE);
SetLocalObject(oPC, "CircleMagicLeader", oLeader);
SetLocalFloat(oPC, "CircleMagicTimeLeft", HoursToSeconds(1));
// Only make associates non-commandable
if (!GetIsPC(oPC))
SetCommandable(FALSE, oPC);
AssignCommand(oPC, ClearAllActions());
AssignCommand(oPC, ActionPlayAnimation(ANIMATION_LOOPING_SPASM, 1.0, HoursToSeconds(1)));
AddEventScript(oPC, EVENT_ONHEARTBEAT, "prc_circle_magic", TRUE, FALSE);
AddEventScript(oPC, EVENT_VIRTUAL_ONDAMAGED, "prc_od_conc", TRUE, FALSE);
FloatingTextStringOnCreature("Channeling Circle Magic for 1 hour...", oPC);
}
else if (nEvent == EVENT_ONHEARTBEAT)
{
if (!GetLocalInt(oPC, "CircleMagicChanneling")) return;
object oLeader = GetLocalObject(oPC, "CircleMagicLeader");
float fTimeLeft = GetLocalFloat(oPC, "CircleMagicTimeLeft") - 6.0f;
// Concentration break check
if (GetLocalInt(oPC, "CONC_BROKEN"))
{
FloatingTextStringOnCreature("Concentration broken. Channeling failed.", oPC);
RemoveEventScript(oPC, EVENT_ONHEARTBEAT, "prc_circle_magic");
RemoveEventScript(oPC, EVENT_VIRTUAL_ONDAMAGED, "prc_od_conc");
DeleteLocalInt(oPC, "CircleMagicChanneling");
DeleteLocalFloat(oPC, "CircleMagicTimeLeft");
if (!GetIsPC(oPC))
SetCommandable(TRUE, oPC);
AssignCommand(oPC, ClearAllActions());
return;
}
// Range check
if (GetIsObjectValid(oLeader) && GetDistanceBetween(oPC, oLeader) > 4.0f)
{
FloatingTextStringOnCreature("Too far from leader. Channeling failed.", oPC);
RemoveEventScript(oPC, EVENT_ONHEARTBEAT, "prc_circle_magic");
RemoveEventScript(oPC, EVENT_VIRTUAL_ONDAMAGED, "prc_od_conc");
DeleteLocalInt(oPC, "CircleMagicChanneling");
DeleteLocalFloat(oPC, "CircleMagicTimeLeft");
if (!GetIsPC(oPC))
SetCommandable(TRUE, oPC);
AssignCommand(oPC, ClearAllActions());
return;
}
if (fTimeLeft <= 0.0f)
{
// Channeling complete: find highest spell and cast it at the leader
int nHighestSpell = GetBestL9Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL8Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL7Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL6Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL5Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL4Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL3Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL2Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL1Spell(oPC, -1);
if (nHighestSpell == -1) nHighestSpell = GetBestL0Spell(oPC, -1);
if (nHighestSpell != -1)
{
// Set flags for spellhook interception, then cast
SetLocalInt(oPC, "CircleMagicSacrifice", TRUE);
SetLocalObject(oPC, "CircleMagicLeader", oLeader);
AssignCommand(oPC, ClearAllActions());
AssignCommand(oPC, ActionCastSpellAtObject(nHighestSpell, oLeader));
}
else
{
FloatingTextStringOnCreature("No spells available to sacrifice.", oPC);
}
// Cleanup channeling state
RemoveEventScript(oPC, EVENT_ONHEARTBEAT, "prc_circle_magic");
RemoveEventScript(oPC, EVENT_VIRTUAL_ONDAMAGED, "prc_od_conc");
DeleteLocalInt(oPC, "CircleMagicChanneling");
DeleteLocalFloat(oPC, "CircleMagicTimeLeft");
if (!GetIsPC(oPC))
SetCommandable(TRUE, oPC);
}
else
{
SetLocalFloat(oPC, "CircleMagicTimeLeft", fTimeLeft);
}
}
}

View File

@@ -10,46 +10,63 @@
//:: Created On: January 3 , 2004
//:: Modified By: Stratovarius, bugfixes.
//:://////////////////////////////////////////////
#include "prc_class_const"
void main()
{
effect eVis = EffectVisualEffect(VFX_FNF_SUMMON_GATE);
string sSummon;
int i = 1;
object oHench = GetAssociate(ASSOCIATE_TYPE_HENCHMAN, OBJECT_SELF, i);
while (GetIsObjectValid(oHench))
{
if (GetStringLeft(GetResRef(oHench), 8) == "prc_hath_")
{
FloatingTextStringOnCreature("You already have a Rashemi Cohort", OBJECT_SELF, FALSE);
return;
}
i += 1;
oHench = GetAssociate(ASSOCIATE_TYPE_HENCHMAN, OBJECT_SELF, i);
}
int nClass = GetLevelByClass(CLASS_TYPE_HATHRAN, OBJECT_SELF);
if (nClass > 27) sSummon = "prc_hath_rash10";
else if (nClass > 24) sSummon = "prc_hath_rash9";
else if (nClass > 21) sSummon = "prc_hath_rash8";
else if (nClass > 18) sSummon = "prc_hath_rash7";
else if (nClass > 15) sSummon = "prc_hath_rash6";
else if (nClass > 12) sSummon = "prc_hath_rash5";
else if (nClass > 9) sSummon = "prc_hath_rash4";
else if (nClass > 6) sSummon = "prc_hath_rash3";
else if (nClass > 3) sSummon = "prc_hath_rash2";
else if (nClass > 0) sSummon = "prc_hath_rash";
object oCreature = CreateObject(OBJECT_TYPE_CREATURE, sSummon, GetSpellTargetLocation());
int nMaxHenchmen = GetMaxHenchmen();
SetMaxHenchmen(99);
AddHenchman(OBJECT_SELF, oCreature);
SetMaxHenchmen(nMaxHenchmen);
ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eVis, GetSpellTargetLocation());
#include "inc_persist_loca"
#include "prc_class_const"
void main()
{
effect eVis = EffectVisualEffect(VFX_FNF_SUMMON_GATE);
string sSummon;
int i = 1;
object oHench = GetAssociate(ASSOCIATE_TYPE_HENCHMAN, OBJECT_SELF, i);
while (GetIsObjectValid(oHench))
{
if (GetStringLeft(GetResRef(oHench), 8) == "prc_hath_")
{
FloatingTextStringOnCreature("You already have a Rashemi Cohort", OBJECT_SELF, FALSE);
return;
}
i += 1;
oHench = GetAssociate(ASSOCIATE_TYPE_HENCHMAN, OBJECT_SELF, i);
}
int nClass = GetLevelByClass(CLASS_TYPE_HATHRAN, OBJECT_SELF);
int nCohortType = GetPersistantLocalInt(OBJECT_SELF, "PRC_HATHRAN_COHORT_TYPE");
if(nCohortType == 1) // Ethran
{
if (nClass > 27) sSummon = "prc_hath_ethrn10";
else if (nClass > 24) sSummon = "prc_hath_ethrn09";
else if (nClass > 21) sSummon = "prc_hath_ethrn08";
else if (nClass > 18) sSummon = "prc_hath_ethrn07";
else if (nClass > 15) sSummon = "prc_hath_ethrn06";
else if (nClass > 12) sSummon = "prc_hath_ethrn05";
else if (nClass > 9) sSummon = "prc_hath_ethrn04";
else if (nClass > 6) sSummon = "prc_hath_ethrn03";
else if (nClass > 3) sSummon = "prc_hath_ethrn02";
else if (nClass > 0) sSummon = "prc_hath_ethrn01";
}
else // Default Rashemi Barbarian
{
if (nClass > 27) sSummon = "prc_hath_rash10";
else if (nClass > 24) sSummon = "prc_hath_rash9";
else if (nClass > 21) sSummon = "prc_hath_rash8";
else if (nClass > 18) sSummon = "prc_hath_rash7";
else if (nClass > 15) sSummon = "prc_hath_rash6";
else if (nClass > 12) sSummon = "prc_hath_rash5";
else if (nClass > 9) sSummon = "prc_hath_rash4";
else if (nClass > 6) sSummon = "prc_hath_rash3";
else if (nClass > 3) sSummon = "prc_hath_rash2";
else if (nClass > 0) sSummon = "prc_hath_rash";
}
object oCreature = CreateObject(OBJECT_TYPE_CREATURE, sSummon, GetSpellTargetLocation());
int nMaxHenchmen = GetMaxHenchmen();
SetMaxHenchmen(99);
AddHenchman(OBJECT_SELF, oCreature);
SetMaxHenchmen(nMaxHenchmen);
ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eVis, GetSpellTargetLocation());
}

View File

@@ -9,6 +9,14 @@ void main()
{
object oDead = OBJECT_SELF;
ExecuteScript("prc_ondeath", oDead);
// Clear Circle Magic state on death
DeleteLocalInt(OBJECT_SELF, "CircleMagicActive");
DeleteLocalInt(OBJECT_SELF, "CircleMagicTotal");
DeleteLocalString(OBJECT_SELF, "CircleMagicClass");
DeleteLocalInt(OBJECT_SELF, "CircleMagicMaxParticipants");
DeleteLocalInt(OBJECT_SELF, PRC_CASTERLEVEL_ADJUSTMENT);
DeleteLocalInt(OBJECT_SELF, "CircleMagicAnimating");
if (GetIsObjectValid(GetLocalObject(GetModule(), "Necrocarnate")))
{

View File

@@ -63,6 +63,14 @@ void main()
// Clear a damage tracking variable. Oni's stuff uses this
SetLocalInt(oDead, "PC_Damage", 0);
// Clear Circle Magic state on death
DeleteLocalInt(OBJECT_SELF, "CircleMagicActive");
DeleteLocalInt(OBJECT_SELF, "CircleMagicTotal");
DeleteLocalString(OBJECT_SELF, "CircleMagicClass");
DeleteLocalInt(OBJECT_SELF, "CircleMagicMaxParticipants");
DeleteLocalInt(OBJECT_SELF, PRC_CASTERLEVEL_ADJUSTMENT);
DeleteLocalInt(OBJECT_SELF, "CircleMagicAnimating");
// Do Lolth's Meat for the killer
if(GetAbilityScore(oDead, ABILITY_INTELLIGENCE) >= 4

View File

@@ -78,7 +78,8 @@ void PrcFeats(object oPC)
DelayCommand(0.3, DeletePRCLocalIntsT(oPC));
DelayCommand(0.4, EvalPRCFeats(oPC));
DelayCommand(0.4, DoWeaponsEquip(oPC));
DelayCommand(1.0, DeleteLocalInt(oPC,"ONREST"));
DelayCommand(0.5, DeleteLocalInt(oPC, "CircleMagicAnimating"));
DelayCommand(1.0, DeleteLocalInt(oPC,"ONREST"));
}
void RestCancelled(object oPC)

View File

@@ -81,6 +81,7 @@ const int STAGE_WIELDING = 52;
const int STAGE_WIELDING_ONE = 53;
const int STAGE_WIELDING_TWO = 54;
const int STAGE_WIELDING_POLEARM = 55;
const int STAGE_HATHRAN_COHORT = 56;
// Confirmation stage for registering cohort
const int STAGE_REGISTER_CONFIRM = 200;
@@ -485,6 +486,7 @@ void main()
AddChoice("Miscellaneous options.", 11);
if(DEBUG)//TO DO: add separate switch
AddChoice("Wipe PRC Spellbooks", 12);
AddChoice("Hathran Cohort Selection", 56);
if((!bRanged && oWeaponR != OBJECT_INVALID) && (PRCGetCreatureSize(oPC) > 1 && PRCGetCreatureSize(oPC) < 5))
{
AddChoice("Set weapon wielding.", 52);
@@ -1642,7 +1644,17 @@ void main()
AddChoice("Back", CHOICE_RETURN_TO_PREVIOUS);
MarkStageSetUp(nStage, oPC);
}
}
else if(nStage == STAGE_HATHRAN_COHORT)
{
SetHeader("Select your Hathran cohort type:");
AddChoice("Barbarian", 0);
AddChoice("Ethran", 1);
AddChoice("Back", STAGE_ENTRY);
MarkStageSetUp(nStage, oPC);
SetDefaultTokens();
}
}
@@ -1736,6 +1748,8 @@ void main()
DelayCommand(1.0, ExecuteScript("prc_wipeNSB", oPC));
AllowExit(DYNCONV_EXIT_FORCE_EXIT);
}
else if(nChoice == 56)
nStage = STAGE_HATHRAN_COHORT;
// Mark the target stage to need building if it was changed (ie, selection was other than ID all)
if(nStage != STAGE_ENTRY)
@@ -2431,14 +2445,28 @@ void main()
MyDestroyObject(GetItemPossessedBy(oPC, "prc_pnp_familiar"));
}
}
else if (nStage == STAGE_CDKEY_ADD)
{
else if (nStage == STAGE_CDKEY_ADD)
{
if(nChoice == 1)
AddNewCDKey(oPC);
nStage = STAGE_ENTRY;
MarkStageNotSetUp(nStage, oPC);
}
else if (nStage == STAGE_HATHRAN_COHORT)
{
if(nChoice == STAGE_ENTRY)
{
nStage = STAGE_ENTRY;
MarkStageNotSetUp(nStage, oPC);
}
else
{
SetPersistantLocalInt(oPC, "PRC_HATHRAN_COHORT_TYPE", nChoice);
nStage = STAGE_ENTRY;
MarkStageNotSetUp(nStage, oPC);
}
}
else if (nStage == STAGE_WIELDING)
{
if(nChoice == CHOICE_RETURN_TO_PREVIOUS)
@@ -2556,7 +2584,7 @@ void main()
//AddChoice("Back", CHOICE_RETURN_TO_PREVIOUS);
MarkStageSetUp(nStage, oPC);
}
}
}
// Store the stage value. If it has been changed, this clears out the choices
SetStage(nStage, oPC);