diff --git a/TemplePlus/action_sequence.cpp b/TemplePlus/action_sequence.cpp index ca0110cba..b3f14bfd5 100644 --- a/TemplePlus/action_sequence.cpp +++ b/TemplePlus/action_sequence.cpp @@ -2138,6 +2138,20 @@ int32_t ActionSequenceSystem::DoAoosByAdjcentEnemies(objHndl obj) // return _AOOSthg2_100981C0(obj); } +int32_t ActionSequenceSystem::ProvokeAooFrom(objHndl provoker, objHndl enemy) +{ + if (objects.GetFlags(enemy) & OF_INVULNERABLE) // see above + return 0; + + if (!combatSys.CanMeleeTarget(enemy, provoker)) return 0; + if (critterSys.IsFriendly(provoker, enemy)) return 0; + if (!d20Sys.d20QueryWithData(enemy, DK_QUE_AOOPossible, provoker)) return 0; + if (!d20Sys.d20QueryWithData(enemy, DK_QUE_AOOWillTake, provoker)) return 0; + + DoAoo(enemy, provoker); + return 1; +} + bool ActionSequenceSystem::SpellTargetsFilterInvalid(D20Actn& d20a){ auto valid = true; @@ -2810,13 +2824,25 @@ void ActionSequenceSystem::ActionPerform() } else{ + bool preempted = false; + switch (d20->D20ActionTriggersAoO(d20a, &tbStatus)) + { + case 0: + break; + case 2: + preempted = ProvokeAooFrom(d20a->d20APerformer, d20a->d20ATarget); + break; + case 1: + default: + preempted = DoAoosByAdjcentEnemies(d20a->d20APerformer); + break; + } - if ( d20->D20ActionTriggersAoO(d20a, &tbStatus) && DoAoosByAdjcentEnemies(d20a->d20APerformer)) { + if (preempted) { logger->debug("ActionPerform: \t Sequence Preempted {}", d20a->d20APerformer); --*(curIdx); sequencePerform(); - } - else{ + } else { curSeq->tbStatus = tbStatus; *(uint32_t*)(&curSeq->tbStatus.tbsFlags) |= TBSF_HasActedThisRound; InterruptCounterspell(d20a); diff --git a/TemplePlus/action_sequence.h b/TemplePlus/action_sequence.h index 63bb3d231..f4dcd8051 100644 --- a/TemplePlus/action_sequence.h +++ b/TemplePlus/action_sequence.h @@ -180,6 +180,7 @@ struct ActionSequenceSystem : temple::AddressTable uint32_t seqCheckFuncs(TurnBasedStatus *tbStatus); void DoAoo(objHndl, objHndl); int32_t DoAoosByAdjcentEnemies(objHndl); + int32_t ProvokeAooFrom(objHndl provoker, objHndl enemy); bool SpellTargetsFilterInvalid(D20Actn &d20a); void HandleInterruptSequence(); diff --git a/TemplePlus/animgoals/anim.cpp b/TemplePlus/animgoals/anim.cpp index 159f5f56c..02a2b4511 100644 --- a/TemplePlus/animgoals/anim.cpp +++ b/TemplePlus/animgoals/anim.cpp @@ -457,62 +457,83 @@ int AnimSystem::PushAnimate(objHndl obj, int anim) { return addresses.PushAnimate(obj, anim); } -BOOL AnimSystem::PushSpellCast(SpellPacketBody & spellPkt, objHndl item) +void AnimSystem::SetGoalDataForSpellPacket(SpellPacketBody & pkt, AnimSlotGoalStackEntry & goalData, bool wand, bool conjure) { - - // note: the original included the spell ID generation & registration, this is separated here. - auto caster = spellPkt.caster; + auto caster = pkt.caster; auto casterObj = objSystem->GetObject(caster); - AnimSlotGoalStackEntry goalData; - if (!goalData.InitWithInterrupt(caster, ag_throw_spell_w_cast_anim)) - return FALSE; + SpellEntry ent(pkt.spellEnum); - goalData.skillData.number = spellPkt.spellId; - - SpellEntry spEntry(spellPkt.spellEnum); + goalData.skillData.number = pkt.spellId; // if self-targeted spell - if (spEntry.IsBaseModeTarget(UiPickerType::Single) && spellPkt.targetCount == 0 ){ - goalData.target.obj = spellPkt.caster; - - if (spellPkt.aoeCenter.location.location == 0){ + if (ent.IsBaseModeTarget(UiPickerType::Single) && pkt.targetCount == 0) { + goalData.target.obj = caster; + if (pkt.aoeCenter.location.location == 0) goalData.targetTile.location = casterObj->GetLocationFull(); - } - else{ - goalData.targetTile.location = spellPkt.aoeCenter.location; - } - } + else + goalData.targetTile.location = pkt.aoeCenter.location; - else{ - auto tgt = spellPkt.targetListHandles[0]; + } else { + auto tgt = pkt.targetListHandles[0]; goalData.target.obj = tgt; - if (tgt && spellPkt.aoeCenter.location.location == 0 ){ + if (tgt && pkt.aoeCenter.location.location == 0) { goalData.targetTile.location = objSystem->GetObject(tgt)->GetLocationFull(); - } - else { - goalData.targetTile.location = spellPkt.aoeCenter.location; + } else { + goalData.targetTile.location = pkt.aoeCenter.location; } } - if (inventory.UsesWandAnim(item)){ - goalData.animIdPrevious.number = temple::GetRef(0x100757C0)(spEntry.spellSchoolEnum); // GetAnimIdWand + if (wand){ + goalData.animIdPrevious.number = GetWandAnimId(ent.spellSchoolEnum, conjure); } else{ - goalData.animIdPrevious.number = temple::GetRef(0x100757B0)(spEntry.spellSchoolEnum); // GetSpellSchoolAnimId + goalData.animIdPrevious.number = GetCastingAnimId(ent.spellSchoolEnum, conjure); } +} + +BOOL AnimSystem::PushSpellCast(SpellPacketBody & spellPkt, objHndl item) +{ + // note: the original included the spell ID generation & registration, this is separated here. + AnimSlotGoalStackEntry goalData; + if (!goalData.InitWithInterrupt(spellPkt.caster, ag_throw_spell_w_cast_anim)) + return FALSE; + + SetGoalDataForSpellPacket(spellPkt, goalData, inventory.UsesWandAnim(item), false); return goalData.Push(nullptr); } +BOOL AnimSystem::PushSpellDismiss(SpellPacketBody & pkt) +{ + AnimSlotGoalStackEntry goalData; + if (!goalData.InitWithInterrupt(pkt.caster, ag_throw_spell_w_cast_anim)) + return FALSE; + + SetGoalDataForSpellPacket(pkt, goalData, true, true); + + return goalData.Push(nullptr); +} + +int AnimSystem::GetWandAnimId(int school, bool conjure) { + auto dec = conjure ? 1 : 0; + return temple::GetRef(0x100757C0)(school) - dec; +} + +int AnimSystem::GetCastingAnimId(int school, bool conjure) { + auto dec = conjure ? 1 : 0; + return temple::GetRef(0x100757B0)(school) - dec; +} + + BOOL AnimSystem::PushSpellInterrupt(const objHndl& caster, objHndl item, AnimGoalType animGoalType, int spellSchool){ AnimSlotGoalStackEntry goalData; goalData.InitWithInterrupt(caster, animGoalType); goalData.target.obj = (*actSeqSys.actSeqCur)->spellPktBody.caster; goalData.skillData.number = 0; if (inventory.UsesWandAnim(item)) - goalData.animIdPrevious.number = temple::GetRef(0x100757C0)(spellSchool); // GetAnimIdWand + goalData.animIdPrevious.number = GetWandAnimId(spellSchool); else - goalData.animIdPrevious.number = temple::GetRef(0x100757B0)(spellSchool); // GetSpellSchoolAnimId + goalData.animIdPrevious.number = GetCastingAnimId(spellSchool); return goalData.Push(nullptr); } diff --git a/TemplePlus/animgoals/anim.h b/TemplePlus/animgoals/anim.h index d67a19c9d..43ffc2519 100644 --- a/TemplePlus/animgoals/anim.h +++ b/TemplePlus/animgoals/anim.h @@ -182,9 +182,16 @@ friend struct AnimSlotGoalStackEntry; pushes spell animation, including wand animation if relevant */ BOOL PushSpellCast(SpellPacketBody &spellPkt, objHndl item); - BOOL PushSpellInterrupt(const objHndl& caster, objHndl item, AnimGoalType animGoalType, int spellSchool); + /* + pushes an animation for dismissing an active spell + */ + BOOL PushSpellDismiss(SpellPacketBody &spellPkt); + int GetWandAnimId(int school, bool conjure = false); + int GetCastingAnimId(int school, bool conjure = false); + void SetGoalDataForSpellPacket(SpellPacketBody &pkt, AnimSlotGoalStackEntry &goalData, bool wand, bool conjure=false); + /* used by the general out-of-combat mouse LMB click handler */ diff --git a/TemplePlus/condition.cpp b/TemplePlus/condition.cpp index b35d631e5..6287bbf7f 100644 --- a/TemplePlus/condition.cpp +++ b/TemplePlus/condition.cpp @@ -261,6 +261,7 @@ class ClassAbilityCallbacks // Aura Of Courage static int __cdecl CouragedAuraSavingThrow(DispatcherCallbackArgs args); + static int FailedCopyScroll(DispatcherCallbackArgs args); static int FeatBrewPotionRadialMenu(DispatcherCallbackArgs args); static int FeatScribeScrollRadialMenu(DispatcherCallbackArgs args); @@ -568,6 +569,9 @@ class ConditionFunctionReplacement : public TempleFix { SubDispDefNew sdd(dispTypeDealingDamageWeaponlikeSpell, 0, classAbilityCallbacks.SneakAttackDamage, 0u,0u); // Weapon-like spell damage hook write(0x102ED2A8, &sdd, sizeof(SubDispDefNew)); + // Wizard + replaceFunction(0x10102AE0, classAbilityCallbacks.FailedCopyScroll); + // D20Mods countdown handler replaceFunction(0x100EC9B0, genericCallbacks.D20ModCountdownHandler); replaceFunction(0x100E98B0, genericCallbacks.D20ModCountdownEndHandler); @@ -1153,7 +1157,8 @@ int GenericCallbacks::TripAooQuery(DispatcherCallbackArgs args) int GenericCallbacks::ImprovedTripBonus(DispatcherCallbackArgs args) { auto dispIo = dispatch.DispIoCheckIoType10(args.dispIO); - if (dispIo->flags & 1){ + // 2 seems to be defender + if (dispIo->flags & 1 && !(dispIo->flags & 2)) { dispIo->bonOut->AddBonusWithDesc(4, 0, 114, feats.GetFeatName(FEAT_IMPROVED_TRIP)); } return 0; @@ -1943,7 +1948,8 @@ int __cdecl GlobalToHitBonus(DispatcherCallbackArgs args) // helplessness bonus if (dispIo->attackPacket.victim && d20Sys.d20Query(dispIo->attackPacket.victim, DK_QUE_Helpless) - && !d20Sys.d20Query(dispIo->attackPacket.victim, DK_QUE_Critter_Is_Stunned)) + && !d20Sys.d20Query(dispIo->attackPacket.victim, DK_QUE_Critter_Is_Stunned) + && !(dispIo->attackPacket.flags & D20CAF_RANGED)) bonusSys.bonusAddToBonusList(&dispIo->bonlist, 4, 30, 136); // flanking bonus @@ -3256,6 +3262,24 @@ void ConditionSystem::RegisterNewConditions() DispatcherHookInit(cond, 1, dispTypeRadialMenuEntry, 0, AidAnotherRadialMenu, 0, 0); // + { + static CondStructNew condHeld; + condHeld.ExtendExisting("Held"); + condHeld.subDispDefs[11].dispCallback = [](DispatcherCallbackArgs args) { + static auto orig = temple::GetRef(0x100EDF10); + // disable effect tooltip if freedom of movement + if (!d20Sys.d20Query(args.objHndCaller, DK_QUE_Critter_Has_Freedom_of_Movement)) + return orig(args); + return 0; + }; + condHeld.AddHook(dispTypeAbilityScoreLevel, DK_STAT_STRENGTH, HeldCapStatBonus); + condHeld.AddHook(dispTypeAbilityScoreLevel, DK_STAT_DEXTERITY, HeldCapStatBonus); + + static CondStructNew condSleeping; + condSleeping.ExtendExisting("Sleeping"); + condSleeping.AddHook(dispTypeAbilityScoreLevel, DK_STAT_DEXTERITY, HelplessCapStatBonus); + } + /* char mCondIndomitableWillName[100]; CondStructNew *mCondIndomitableWill; @@ -3649,6 +3673,31 @@ int TacticalOptionAbusePrevention(DispatcherCallbackArgs args) return temple::GetRef(0x100F7ED0)(args); // replaced in ability_fixes.cpp } +// Port of 0x100E7F80. Was used in Unconscious but missing in similar +// conditions. Helpless critters should have 0 effective dexterity, and +// paralyzed creatures should have 0 effective strength. +int HelplessCapStatBonus(DispatcherCallbackArgs args) +{ + DispIoBonusList *dispIo = dispatch.DispIoCheckIoType2(args.dispIO); + + dispIo->bonlist.AddCap(0, 0, 109); + + return 0; +} + +// As above, but check for freedom of movement, since it doesn't remove the held +// condition. +int HeldCapStatBonus(DispatcherCallbackArgs args) +{ + DispIoBonusList *dispIo = dispatch.DispIoCheckIoType2(args.dispIO); + + if (!d20Sys.d20Query(args.objHndCaller, DK_QUE_Critter_Has_Freedom_of_Movement)) + dispIo->bonlist.AddCap(0, 0, 109); + + return 0; +} + + #pragma region Barbarian Stuff int __cdecl CombatExpertiseRadialMenu(DispatcherCallbackArgs args) @@ -3994,7 +4043,11 @@ int RendOnDamage(DispatcherCallbackArgs args) auto previousTarget = args.GetCondArgObjHndl(2); if (hasDeliveredDamage && attackDescr == previousAttackDescr && previousTarget == dispIo->attackPacket.victim) { - Dice dice(2, 6, 9); + int strScore = objects.StatLevelGet(args.objHndCaller, stat_strength); + int bonus = objects.GetModFromStatLevel(strScore); + if (bonus > 0) bonus += bonus/2; + + Dice dice(2, 6, bonus); dispIo->damage.AddDamageDice(dice.ToPacked(), DamageType::PiercingAndSlashing, 133); //damage.AddDamageDice(&dispIo->damage, dice.ToPacked(), DamageType::PiercingAndSlashing, 133); floatSys.FloatCombatLine(args.objHndCaller, 203); @@ -6279,6 +6332,24 @@ int ClassAbilityCallbacks::CouragedAuraSavingThrow(DispatcherCallbackArgs args) return 0; } +int ClassAbilityCallbacks::FailedCopyScroll(DispatcherCallbackArgs args) { + auto dispIo = dispatch.DispIoCheckIoType7(args.dispIO); + + auto failedScRanks = args.GetCondArg(1); + auto curScRanks = critterSys.SkillBaseGet(args.objHndCaller, SkillEnum::skill_spellcraft); + auto failedSpellEnum = args.GetCondArg(0); + + // if we've gained spellcraft ranks, get rid of the condition + if (curScRanks > failedScRanks) { + conds.ConditionRemove(args.objHndCaller, args.subDispNode->condNode); + // if the scroll being copied matches, set return value + } else if (failedSpellEnum == dispIo->data1) { + dispIo->return_val = 1; + } + + return 0; +} + int ClassAbilityCallbacks::FeatBrewPotionRadialMenu(DispatcherCallbackArgs args){ return ItemCreationBuildRadialMenuEntry(args, BrewPotion, "TAG_BREW_POTION", 5066); } @@ -7539,6 +7610,16 @@ void Conditions::AddConditionsToTable(){ } } + { + static CondStructNew condColorSprayStun; + condColorSprayStun.ExtendExisting("sp-Color Spray Stun"); + condColorSprayStun.AddHook(dispTypeD20Query, DK_QUE_SneakAttack, genericCallbacks.QuerySetReturnVal1); + + static CondStructNew condColorSprayBlind; + condColorSprayBlind.ExtendExisting("sp-Color Spray Blind"); + condColorSprayBlind.AddHook(dispTypeTurnBasedStatusInit, DK_NONE, TurnBasedStatusInitNoActions); + } + // New Conditions! conds.hashmethods.CondStructAddToHashtable((CondStruct*)conds.mConditionDisableAoO); conds.hashmethods.CondStructAddToHashtable((CondStruct*)conds.mCondGreaterTwoWeaponFighting); diff --git a/TemplePlus/condition.h b/TemplePlus/condition.h index 85bdde2df..c2264ce88 100644 --- a/TemplePlus/condition.h +++ b/TemplePlus/condition.h @@ -320,6 +320,9 @@ int RecklessOffenseAcPenalty(DispatcherCallbackArgs args); int RecklessOffenseToHitBonus(DispatcherCallbackArgs args); int TacticalOptionAbusePrevention(DispatcherCallbackArgs args); +int HeldCapStatBonus(DispatcherCallbackArgs args); +int HelplessCapStatBonus(DispatcherCallbackArgs args); + int CombatExpertiseRadialMenu(DispatcherCallbackArgs args); int CombatExpertiseSet(DispatcherCallbackArgs args); diff --git a/TemplePlus/d20.cpp b/TemplePlus/d20.cpp index a1d2b7985..343642cb6 100644 --- a/TemplePlus/d20.cpp +++ b/TemplePlus/d20.cpp @@ -75,6 +75,7 @@ class D20ActionCallbacks { // Action Checks static ActionErrorCode ActionCheckAidAnotherWakeUp(D20Actn* d20a, TurnBasedStatus* tbStat); static ActionErrorCode ActionCheckCastSpell(D20Actn* d20a, TurnBasedStatus* tbStat); + static ActionErrorCode ActionCheckCopyScroll(D20Actn* d20a, TurnBasedStatus* tbStat); static ActionErrorCode ActionCheckDisarm(D20Actn* d20a, TurnBasedStatus* tbStat); static ActionErrorCode ActionCheckDisarmedWeaponRetrieve(D20Actn* d20a, TurnBasedStatus* tbStat); static ActionErrorCode ActionCheckDivineMight(D20Actn* d20a, TurnBasedStatus* tbStat); @@ -88,6 +89,9 @@ class D20ActionCallbacks { static ActionErrorCode ActionCheckSunder(D20Actn* d20a, TurnBasedStatus* tbStat); static ActionErrorCode ActionCheckTripAttack(D20Actn* d20a, TurnBasedStatus* tbStat); + // Was part of an action check, but seems to be called directly from the + // Perform function. + static ActionErrorCode CanCopyScroll(D20Actn* d20a); // Target Check //static ActionErrorCode TargetCheckStandardAttack(D20Actn* d20a, TurnBasedStatus* tbStat); @@ -136,6 +140,7 @@ class D20ActionCallbacks { static BOOL ActionFrameAidAnotherWakeUp(D20Actn* d20a); static BOOL ActionFrameAoo(D20Actn* d20a); static BOOL ActionFrameCharge(D20Actn* d20a); + static BOOL ActionFrameCoupDeGrace(D20Actn* d20a); static BOOL ActionFrameDisarm(D20Actn* d20a); static BOOL ActionFramePython(D20Actn* d20a); static BOOL ActionFrameQuiveringPalm(D20Actn* d20a); @@ -496,6 +501,9 @@ void LegacyD20System::NewD20ActionsInit() d20Defs[d20Type].performFunc = d20Callbacks.PerformCharge; d20Defs[d20Type].actionFrameFunc = d20Callbacks.ActionFrameCharge; + d20Type = D20A_COUP_DE_GRACE; + d20Defs[d20Type].actionFrameFunc = d20Callbacks.ActionFrameCoupDeGrace; + d20Type = D20A_DEATH_TOUCH; d20Defs[d20Type].performFunc = d20Callbacks.PerformTouchAttack; @@ -521,10 +529,15 @@ void LegacyD20System::NewD20ActionsInit() d20Defs[d20Type].performFunc = d20Callbacks.PerformUseItem; d20Type = D20A_COPY_SCROLL; + d20Defs[d20Type].actionCheckFunc = d20Callbacks.ActionCheckCopyScroll; d20Defs[d20Type].performFunc = d20Callbacks.PerformCopyScroll; d20Type = D20A_DISMISS_SPELLS; d20Defs[d20Type].performFunc = d20Callbacks.PerformDismissSpell; + // Dismissing a spell is supposed to take a standard action + if (config.stricterRulesEnforcement) { + d20Defs[d20Type].actionCost = d20Callbacks.ActionCostStandardAction; + } d20Type = D20A_USE_POTION; d20Defs[d20Type].addToSeqFunc = d20Callbacks.AddToSeqSpellCast; @@ -1086,7 +1099,10 @@ int32_t LegacyD20System::D20ActionTriggersAoO(D20Actn* d20a, TurnBasedStatus* tb && d20QueryWithData(d20a->d20APerformer, DK_QUE_ActionTriggersAOO, (int)d20a, 0)) { if (d20a->d20ActType == D20A_DISARM) - return feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_DISARM) == 0; + if (!feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_DISARM)) + return 2; + else + return 0; return 1; } @@ -1100,21 +1116,27 @@ int32_t LegacyD20System::D20ActionTriggersAoO(D20Actn* d20a, TurnBasedStatus* tb return FALSE; if (feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_UNARMED_STRIKE)) return FALSE; - return feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_TRIP) == 0; + if (feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_TRIP)) + return FALSE; + return 2; } - - - - if (d20a->d20ActType == D20A_SUNDER) - return feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_SUNDER) == 0; + if (d20a->d20ActType == D20A_SUNDER) { + if (feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_SUNDER)) + return FALSE; + else + return 2; + } if (d20a->d20Caf & D20CAF_TOUCH_ATTACK || d20Sys.GetAttackWeapon(d20a->d20APerformer, d20a->data1, (D20CAF)d20a->d20Caf) || dispatch.DispatchD20ActionCheck(d20a, tbStat, dispTypeGetCritterNaturalAttacksNum)) return 0; - return feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_UNARMED_STRIKE) == 0; + if (feats.HasFeatCountByClass(d20a->d20APerformer, FEAT_IMPROVED_UNARMED_STRIKE)) + return 0; + else + return 2; /* __asm{ @@ -2572,12 +2594,17 @@ ActionErrorCode D20ActionCallbacks::PerformCharge(D20Actn* d20a){ ActionErrorCode D20ActionCallbacks::PerformCopyScroll(D20Actn * d20a){ auto performer = d20a->d20APerformer; - auto check = temple::GetRef(0x10091B80)(d20a); - if (check == AEC_INVALID_ACTION){ + switch (CanCopyScroll(d20a)) + { + case AEC_OK: + break; + case AEC_INVALID_ACTION: skillSys.FloatError(performer, 17); return AEC_OK; - } - else if (check!= AEC_OK){ + case AEC_CANNOT_CAST_NOT_ENOUGH_GP: + skillSys.FloatError(performer, 3); + return AEC_OK; + default: return AEC_OK; } @@ -2601,6 +2628,12 @@ ActionErrorCode D20ActionCallbacks::PerformCopyScroll(D20Actn * d20a){ return AEC_OK; } + if (config.stricterRulesEnforcement) { + if (party.IsInParty(performer)){ + party.DebitMoney(0, spLvl == 0 ? 100 : 100*spLvl, 0, 0); + } + } + spellSys.SpellKnownAdd(performer, spEnum, spellSys.GetSpellClass(stat_level_wizard), spLvl, 1, 0); auto scrollObj = inventory.GetItemAtInvIdx(performer, d20a->data1); if (scrollObj){ @@ -2633,6 +2666,31 @@ BOOL D20ActionCallbacks::ActionFrameCharge(D20Actn* d20a){ return FALSE; } +// version of 0x10090C80 that actually performs a fortitude save +// TODO: only allow regenerators to be killed by lethal damage? +BOOL D20ActionCallbacks::ActionFrameCoupDeGrace(D20Actn* d20a) { + auto performer = d20a->d20APerformer; + auto target = d20a->d20ATarget; + auto caf = static_cast(d20a->d20Caf); + auto dmg = damage.DealAttackDamage(performer, target, d20a->data1, caf, d20a->d20ActType); + + if (dmg <= 0 || critterSys.IsDeadNullDestroyed(target)) + return 0; + + auto dc = 10 + dmg; + + if (damage.SavingThrow(target, performer, dc, SavingThrowType::Fortitude, D20STF_NONE)) + return 0; + + auto leader = party.GetConsciousPartyLeader(); + auto desc = description.getDisplayName(target); + auto msg = fmt::format("{} {}\n", desc, combatSys.GetCombatMesLine(0xAE)).c_str(); + histSys.CreateFromFreeText(msg); + critterSys.Kill(target, performer); + + return 0; +} + BOOL D20ActionCallbacks::ActionFrameDisarm(D20Actn* d20a){ objHndl performer = d20a->d20APerformer; int failedOnce = 0; @@ -2902,12 +2960,23 @@ ActionErrorCode D20ActionCallbacks::PerformDisarmedWeaponRetrieve(D20Actn* d20a) d20Sys.d20SendSignal(d20a->d20APerformer, DK_SIG_Disarmed_Weapon_Retrieve, (int)d20a, 0); return AEC_OK; } + ActionErrorCode D20ActionCallbacks::PerformDismissSpell(D20Actn * d20a){ auto spellId = d20a->data1; - d20Sys.d20SendSignal(d20a->d20APerformer, DK_SIG_Dismiss_Spells, spellId, 0); SpellPacketBody spPkt(spellId); - if (!spPkt.spellEnum) + + // in case of bad spell + if (!spPkt.spellEnum) { + d20Sys.d20SendSignal(d20a->d20APerformer, DK_SIG_Dismiss_Spells, spellId, 0); return AEC_OK; + } + + if (gameSystems->GetAnim().PushSpellDismiss(spPkt)) { + d20a->animID = gameSystems->GetAnim().GetActionAnimId(d20a->d20APerformer); + d20a->d20Caf |= D20CAF_NEED_ANIM_COMPLETED; + } + + d20Sys.d20SendSignal(d20a->d20APerformer, DK_SIG_Dismiss_Spells, spellId, 0); if (spPkt.caster) d20Sys.d20SendSignal(spPkt.caster, DK_SIG_Dismiss_Spells, spellId, 0); @@ -3626,6 +3695,38 @@ ActionErrorCode D20ActionCallbacks::ActionCheckCastSpell(D20Actn* d20a, TurnBase return d20Sys.TargetCheck(d20a) != 0 ? AEC_OK : AEC_TARGET_INVALID; } +// Broke up and enhanced 0x10091B80 for better floats. +// Action check only checks for whether you have 'time' to copy the scroll. +ActionErrorCode D20ActionCallbacks::ActionCheckCopyScroll(D20Actn* d20a, TurnBasedStatus* tbStat) +{ + if (combatSys.isCombatActive()) + return AEC_OUT_OF_COMBAT_ONLY; + else + return AEC_OK; +} + +// Checks whether you have the resources, and haven't already failed to copy the +// scroll with the given spellcraft ranks. +ActionErrorCode D20ActionCallbacks::CanCopyScroll(D20Actn* d20a) { + auto performer = d20a->d20APerformer; + int spEnum = 0; + d20a->d20SpellData.Extract(&spEnum, nullptr, nullptr, nullptr, nullptr, nullptr); + + if (config.stricterRulesEnforcement && party.IsInParty(performer)) { + int spClass = spellSys.GetSpellClass(stat_level_wizard); + auto spLvl = spellSys.GetSpellLevelBySpellClass(spEnum, spClass); + auto gpCost = spLvl == 0 ? 100 : 100 * spLvl; + auto money = party.GetMoney(); + if (money >= 0 && money < gpCost*100) + return AEC_CANNOT_CAST_NOT_ENOUGH_GP; + } + + auto failed = d20Sys.d20QueryWithData(performer, DK_QUE_Failed_Copy_Scroll, spEnum, 0); + + if (failed) return AEC_INVALID_ACTION; + else return AEC_OK; +} + ActionErrorCode D20ActionCallbacks::AddToSeqCharge(D20Actn* d20a, ActnSeq* actSeq, TurnBasedStatus* tbStat){ auto tgt = d20a->d20ATarget; diff --git a/TemplePlus/d20_class.cpp b/TemplePlus/d20_class.cpp index 4dc3df5eb..44ae8f3f4 100644 --- a/TemplePlus/d20_class.cpp +++ b/TemplePlus/d20_class.cpp @@ -735,7 +735,7 @@ void D20ClassSystem::LevelupInitSpellSelection(objHndl handle, Stat classEnum, i int D20ClassHooks::HookedLvl1SkillPts(int intStatLvl){ - auto result = (intStatLvl - 10) / 2; + auto result = (intStatLvl - 10) >> 1; auto &selPkt = chargen.GetCharEditorSelPacket(); auto classLevelled = selPkt.classCode; result += d20ClassSys.GetSkillPts(classLevelled); diff --git a/TemplePlus/python/python_dispatcher.cpp b/TemplePlus/python/python_dispatcher.cpp index 434b59da6..6017d3580 100644 --- a/TemplePlus/python/python_dispatcher.cpp +++ b/TemplePlus/python/python_dispatcher.cpp @@ -755,7 +755,7 @@ PYBIND11_EMBEDDED_MODULE(tpdp, m) { .value("Alchemy", Alchemy) .value("Movement", Movement) .value("Offense", Offense) - .value("Tacitical", Tactical) + .value("Tactical", Tactical) .value("Options", Options) .value("Potions", Potions) .value("Wands", Wands) diff --git a/TemplePlus/python/python_object.cpp b/TemplePlus/python/python_object.cpp index 233ff7090..460672e7d 100644 --- a/TemplePlus/python/python_object.cpp +++ b/TemplePlus/python/python_object.cpp @@ -1840,6 +1840,15 @@ static PyObject* PyObjHandle_ConditionsGet(PyObject* obj, PyObject* args) { } } + // default to all conditions by being distinct from 0 and 1 + uint32_t active = 0xff; + if (PyTuple_GET_SIZE(args) > 1) { + PyObject* arg = PyTuple_GET_ITEM(args, 1); + if (PyInt_Check(arg) || PyLong_Check(arg)) { + active = PyInt_AsLong(arg); + } + } + CondNode* node = dispatcher->conditions; if (kind == 1) { node = dispatcher->permanentMods; @@ -1852,6 +1861,14 @@ static PyObject* PyObjHandle_ConditionsGet(PyObject* obj, PyObject* args) { auto list = PyList_New(0); while (node) { + // if active == 1 and inactive flag is set, skip + // if active == 0 and inactive flag isn't set, skip + // if active is anything else, don't skip + if ((node->flags & 1) == active) { + node = node->nextCondNode; + continue; + } + auto cname = PyString_FromString(node->condStruct->condName); auto tuple = PyTuple_New(2); PyTuple_SET_ITEM(tuple, 0, cname); diff --git a/TemplePlus/python/python_spell.cpp b/TemplePlus/python/python_spell.cpp index ca0c5e59c..19a9c510e 100644 --- a/TemplePlus/python/python_spell.cpp +++ b/TemplePlus/python/python_spell.cpp @@ -592,7 +592,9 @@ static PyObject* PySpell_GetTargetList(PyObject* obj, void*) { static PyObject* PySpell_GetSpellRadius(PyObject* obj, void*) { auto self = (PySpell*)obj; auto spellEntry = spellEntryRegistry.get(self->spellEnum); - return PyInt_FromLong(spellEntry->radiusTarget); + auto baseRadius = spellEntry->radiusTarget; + auto scale = 1 + self->metaMagic.metaMagicWidenSpellCount; + return PyInt_FromLong(baseRadius * scale); } static PyObject* PySpell_GetSpell(PyObject* obj, void*) { diff --git a/TemplePlus/spell_condition.cpp b/TemplePlus/spell_condition.cpp index 41b0be5bb..fdb80a8ef 100644 --- a/TemplePlus/spell_condition.cpp +++ b/TemplePlus/spell_condition.cpp @@ -318,6 +318,16 @@ class SpellConditionFixes : public TempleFix { // Grease fix for Freedom of Movement replaceFunction(0x100C8270, GreaseSlippage); + // Don't bother trying to 'break free' of Hold X while freedom of movement + // is active; it wastes your turn if you succeed. + static int (*origHoldBreakFree)(DispatcherCallbackArgs) = + replaceFunction(0x100C3FE0, + [](DispatcherCallbackArgs args) { + if (!d20Sys.d20Query(args.objHndCaller, DK_QUE_Critter_Has_Freedom_of_Movement)) + return origHoldBreakFree(args); + return 0; + }); + static int (*orgImmunityCheckHandler )(DispatcherCallbackArgs)= replaceFunction(0x100ED650, [](DispatcherCallbackArgs args) { if (!ImmunityCheckHandler(args)) diff --git a/TemplePlus/temple_enums.h b/TemplePlus/temple_enums.h index 571204b03..1acc27f86 100644 --- a/TemplePlus/temple_enums.h +++ b/TemplePlus/temple_enums.h @@ -2739,7 +2739,7 @@ enum SpellFlags : uint32_t SF_2000 = 0x2000, SF_4000 = 0x4000, SF_8000 = 0x8000, - SF_10000 = 0x10000, // Used in goalstatefunc_124 + SF_10000 = 0x10000, // Used in goalstatefunc_124, might be 'held' or similar SF_20000 = 0x20000, SF_40000 = 0x40000, SF_80000 = 0x80000, diff --git a/tpdata/tpmes/help_extensions.tab b/tpdata/tpmes/help_extensions.tab index f52a37112..c4c1716f6 100644 --- a/tpdata/tpmes/help_extensions.tab +++ b/tpdata/tpmes/help_extensions.tab @@ -109,4 +109,5 @@ TAG_SPELLS_ENERVATION TAG_SPELLS TAG_SORCERER_4 TAG_WIZARD_4 Enervation ~Necrom TAG_SPELLS_ENERGY_DRAIN TAG_SPELLS TAG_CLERIC_9 TAG_SORCERER_9 TAG_WIZARD_9 Energy Drain ~Necromancy~[TAG_MAGIC_SCHOOLS_NECROMANCY] ~Cleric~[TAG_CLERICS] 9, ~Sorcerer~[TAG_SORCERERS] 9, ~Wizard~[TAG_WIZARDS] 9 Components: V, S Casting Time: 1 ~standard action~[TAG_STANDARD_ACTION] Range: Close (25 ft. + 5 ft./2 levels) Duration: Instantaneous This spell functions like ~enervation~[TAG_SPELLS_ENERVATION], except that the creature gains 2d4 ~negative levels~[TAG_SPECIAL_ABILITIES_ENERGY_DRAIN_AND_NEGATIVE_LEVELS] (double on a critical hit), and the negative levels last longer. There is no ~saving throw~[TAG_SAVING_THROW_DESC] to avoid gaining the negative levels, but 24 hours after gaining them, the subject must make a ~Fortitude~[TAG_FORTITUDE] saving throw (DC = the spell's save DC) for each negative level. If the save succeeds, that negative level is removed. If it fails, the negative level instead becomes a permanent negative level. A separate save must be made for each negative level. Like enervation, an undead creature struck by the ray instead gains 5 ~temporary hit points~[TAG_TEMPORARY_HIT_POINTS] for each negative level that would be bestowed. TAG_SPELLS_DISRUPTING_WEAPON TAG_SPELLS TAG_CLERIC_5 Disrupting Weapon ~Transmutation~[TAG_MAGIC_SCHOOLS_TRANSMUTATION] ~Cleric~[TAG_CLERICS] 5 Components: V, S Casting Time: 1 ~standard action~[TAG_STANDARD_ACTION] Range: Touch Target: One melee weapon Duration: 1 round/level Saving Throw: No Spell Resistance: No This spell makes a melee weapon deadly to undead. When struck with the weapon, an undead creature with ~hit dice~[TAG_HIT_DICE] equal to or less than the caster level must succeed on a ~Will~[TAG_WILL] save or be destroyed. Spell resistance does not apply against the destruction effect. TAG_SPELLS_HOLD_PERSON TAG_SPELLS TAG_BARD_2 TAG_CLERIC_2 TAG_SORCERER_3 TAG_WIZARD_3 Hold Person ~Enchantment~[TAG_MAGIC_SCHOOLS_ENCHANTMENT] (Compulsion) Mind-Affecting Level: ~Bard~[TAG_BARDS] 2, ~Cleric~[TAG_CLERICS] 2, ~Sorcerer~[TAG_SORCERERS]/~Wizard~[TAG_WIZARDS] 3 Components: V, S Casting Time: 1 ~standard action~[TAG_STANDARD_ACTION] Range: Medium (100 ft. + 10 ft./level) Target: One humanoid creature Duration: 1 round/level (D); see text Saving Throw: ~Will~[TAG_WILL] negates; see text Spell Resistance: Yes The subject becomes ~paralyzed~[TAG_PARALYZED] and freezes in place. It is aware and breathes normally but cannot take any actions, even speech. Each round on its turn, the subject may attempt a new ~saving throw~[TAG_SAVING_THROW_DESC] to end the effect. (In ToEE, this is a ~full-round action~[TAG_FULL_ROUND_ACTION] that does not provoke ~attacks of opportunity~[TAG_AOO].) +TAG_CLASS_FEATURES_WIZARD_SPELLBOOK TAG_WIZARDS Spellbook A wizard must study his spellbook each day to prepare his spells. A wizard begins play with a spellbook containing all 0-level wizard spells (except those from his prohibited schools, if any) plus two 1st-level spells of your choice. For each point of ~Intelligence~[TAG_INTELLIGENCE] bonus the wizard has, the spellbook holds one additional 1st-level spell of your choice. At each new wizard level, he gains two new spells of any spell level or levels that he can cast (based on his new wizard level) for his spellbook. A wizard can also copy spells into his spellbook from scrolls. To start, first the wizard needs at least 1 ~skill point~[TAG_ACQUIRE_SKILLS] in the ~Spellcraft~[TAG_SPELLCRAFT] skill for training. Then you select a scroll that the wizard has in his ~inventory~[TAG_HMU_CHAR_INVENTORY_UI] by using the ~radial menu~[TAG_RADIAL_MENU] and under the Person icon, select Copy Scroll. The wizard must make a Spellcraft check against a DC of: 15 + the spell's level If successful, the spell is entered into his spellbook and the scroll is destroyed. If unsuccessful, the scroll is unharmed, but the wizard may not try again until he gains at least one more rank in Spellcraft. ~Strict Rules~[TAG_STRICT_RULES] Scribing a spell uses up materials costing 100GP per level of the spell. Thus, a scribing a 5th level spell requires spending 500GP on materials to do so. This cost is in addition to the cost of the scroll being copied. The spells learned when a wizard levels up do not incur this cost. diff --git a/tpdatasrc/co8infra/scr/Spell170 - Finger of Death.py b/tpdatasrc/co8infra/scr/Spell170 - Finger of Death.py new file mode 100644 index 000000000..9ea9c3a2c --- /dev/null +++ b/tpdatasrc/co8infra/scr/Spell170 - Finger of Death.py @@ -0,0 +1,44 @@ +from toee import * +from utilities import * + +def OnBeginSpellCast( spell ): + print "Finger of Death OnBeginSpellCast" + print "spell.target_list=", spell.target_list + print "spell.caster=", spell.caster, " caster.level= ", spell.caster_level + game.particles( "sp-necromancy-conjure", spell.caster ) + +def OnSpellEffect ( spell ): + print "Finger of Death OnSpellEffect" + + damage_dice = dice_new( "3d6" ) + damage_dice.bonus = min( 25, spell.caster.stat_level_get( spell.caster_class ) ) + + target = spell.target_list[0] + + game.particles( 'sp-Slay Living', target.obj ) + + # damage target + if target.obj.saving_throw_spell( spell.dc, D20_Save_Fortitude, D20STD_F_NONE, spell.caster, spell.id ): + target.obj.float_mesfile_line( 'mes\\spell.mes', 30001 ) + + # saving throw succesful, damage target + target.obj.spell_damage( spell.caster, D20DT_NEGATIVE_ENERGY, damage_dice, D20DAP_UNSPECIFIED, D20A_CAST_SPELL, spell.id ) + else: + target.obj.float_mesfile_line( 'mes\\spell.mes', 30002 ) + + # saving throw unsuccesful, kill target + + # set attribute for proper XP award + if target.obj.type == obj_t_npc: + target.obj.obj_set_obj(obj_f_last_hit_by, spell.caster) + + target.obj.critter_kill_by_effect() + + spell.target_list.remove_target( target.obj ) + spell.spell_end(spell.id) + +def OnBeginRound( spell ): + print "Finger of Death OnBeginRound" + +def OnEndSpellCast( spell ): + print "Finger of Death OnEndSpellCast" diff --git a/tpdatasrc/co8infra/tpmes/help_extensions.tab b/tpdatasrc/co8infra/tpmes/help_extensions.tab index 8c22e4fd6..153ab218d 100644 --- a/tpdatasrc/co8infra/tpmes/help_extensions.tab +++ b/tpdatasrc/co8infra/tpmes/help_extensions.tab @@ -121,4 +121,5 @@ TAG_ULTIMATE_MAGUS TAG_PRESTIGE_CLASSES Ultimate Magus Sorcerers channel unkno TAG_CLASS_FEATURES_ULTIMATE_MAGUS TAG_ULTIMATE_MAGUS Ultimate Magus Base Attack & Base Save Bonuses Level ~Base Attack Bonus~[TAG_LEVEL_BONUSES] ~Fortitude~[TAG_FORTITUDE] ~Save~[TAG_LEVEL_BONUSES] ~Reflex~[TAG_REFLEX] ~Save~[TAG_LEVEL_BONUSES] ~Will~[TAG_WILL] ~Save~[TAG_LEVEL_BONUSES] 1 @t+0 @t+0 @t+0 @t+2 2 @t+1 @t+0 @t+0 @t+3 3 @t+1 @t+1 @t+1 @t+3 4 @t+2 @t+1 @t+1 @t+4 5 @t+2 @t+1 @t+1 @t+4 6 @t+3 @t+2 @t+2 @t+5 7 @t+3 @t+2 @t+2 @t+5 8 @t+4 @t+2 @t+2 @t+6 9 @t+4 @t+3 @t+3 @t+6 10 @t+5 @t+3 @t+3 @t+7 TAG_UNSEEN_SEER TAG_PRESTIGE_CLASSES Unseen Seer Mysterious and elusive, the unseen seer trades in secrets. Subterfuge is his business and he uses his magic to help his gather other people's secrets while keeping his own. Requirements: Skills: Hide 8 ranks, Search 8 ranks, Sense Motive 4 ranks, Spellcraft 4 ranks, Spot 8 ranks Spellcasting: Ability to cast 1st-level arcane spells, including at least two divination spells. Hit Die: d4 Base Attack and Base Save Bonuses: see ~table~[TAG_CLASS_FEATURES_UNSEEN_SEER]. Class Skills: ~Bluff~[TAG_BLUFF], ~Concentration~[TAG_CONCENTRATION], ~Diplomacy~[TAG_DIPLOMACY], ~Disable Device~[TAG_DISABLE_DEVICE], ~Gather Information~[TAG_GATHER_INFORMATION], ~Hide~[TAG_HIDE], ~Listen~[TAG_LISTEN], ~Move Silently~[TAG_MOVE_SILENTLY], ~Open Lock~[TAG_OPEN_LOCK], ~Search~[TAG_SEARCH], ~Sense Motive~[TAG_SENSE_MOTIVE], ~Spot~[TAG_SPOT], ~Spellcraft~[TAG_SPELLCRAFT], ~Spot~[TAG_SPOT]. Skill Points at Each Level: 6 + Int modifier Class Features: Weapon and Armor Proficiency: Unseen Seers gain no proficiency with ayn weapon or armor. Spells per Day: When a new unseen seer level is gained, the character gains new spells per day as if he had also gained a level in a spellcasting class he belonged to before adding the prestige class. He does not, however, gain any other benefit a character of that class would have gained (improved chance of controlling or rebuking undead, metamagic or item creation feats, and so on), except for an increased effective level of spellcasting. If a character had more than one spellcasting class before becoming an arcane trickster, the highest one is chosen for adding the new level for the purposes of determining spells per day. Note: Spells that require an attack roll, such as Chill Touch, are considered weapon-like spells and as such benefit from Sneak Attack or Skirmish damage. Damage Bonus: At 1st level, the extra damage you deal with your sneak attack, skirmish, or sudden strike ability increases by 1d6. If you have more than one of these abilities, only one ability gains this increase (choose each time you gain this benefit).Your sneak attack, skirmish, or sudden strike damage increases by another 1d6 at 4th level, 7th level, and 10th level. Advanced Learning (Ex): At 2nd, 5th, and 8th level, you can add a new spell to your spellbook or list of spells known, representing the result of personal study and experimentation. The spell must be a divination spell of a level no higher than that of the highest-level arcane spell you already know. The spell can be from any class's spell list (arcane or divine). Once a new spell is selected, it is forever added to your spell list and can be cast just like any other spell on your list. Select the spell after levelup through the radial menu. Silent Spell: At 2nd level, you gain Silent Spell as a bonus feat. Divination Spell Power (Ex): At 3rd level, you gain a +1 bonus to your caster level when casting an arcane divination spell. This bonus improves to +2 at 6th level, and to +3 at 9th level.This benefit comes at a cost: Your caster level for all other arcane spells is reduced by 1 at 3rd level. This reduction becomes 2 at 6th level and becomes 3 at 9th level. For example, a 4th-level rogue/1st-level sorcerer/6th-level unseen seer would have a caster level of 9th for his arcane divination spells, but only 5th for his nondivination arcane spells. Guarded Mind (Su): Any successful unseen seer must learn to protect himself from magic that would reveal his identity. At 5th level, you become protected by nondetection (as the spell, but with a permanent duration). For the purpose of divinations attempted against you, your caster level equals your character level (no in game effect). TAG_CLASS_FEATURES_UNSEEN_SEER TAG_DUMMY Unseen Seer Base Attack & Base Save Bonuses Level ~Base Attack Bonus~[TAG_LEVEL_BONUSES] ~Fortitude~[TAG_FORTITUDE] ~Save~[TAG_LEVEL_BONUSES] ~Reflex~[TAG_REFLEX] ~Save~[TAG_LEVEL_BONUSES] ~Will~[TAG_WILL] ~Save~[TAG_LEVEL_BONUSES] 1 @t+0 @t+0 @t+0 @t+2 2 @t+1 @t+0 @t+0 @t+3 3 @t+2 @t+1 @t+1 @t+3 4 @t+3 @t+1 @t+1 @t+4 5 @t+3 @t+1 @t+1 @t+4 6 @t+4 @t+2 @t+2 @t+5 7 @t+5 @t+2 @t+2 @t+5 8 @t+6 @t+2 @t+2 @t+6 9 @t+6 @t+3 @t+3 @t+6 10 @t+7 @t+3 @t+3 @t+7 +TAG_CLASS_FEATURES_WIZARD_SPELLBOOK TAG_WIZARDS Spellbook A wizard must study his spellbook each day to prepare his spells. A wizard begins play with a spellbook containing all 0-level wizard spells (except those from his prohibited schools, if any) plus two 1st-level spells of your choice. For each point of ~Intelligence~[TAG_INTELLIGENCE] bonus the wizard has, the spellbook holds one additional 1st-level spell of your choice. At each new wizard level, he gains two new spells of any spell level or levels that he can cast (based on his new wizard level) for his spellbook. A wizard can also copy spells into his spellbook from scrolls. To start, first the wizard needs at least 1 ~skill point~[TAG_ACQUIRE_SKILLS] in the ~Spellcraft~[TAG_SPELLCRAFT] skill for training. Then you select a scroll that the wizard has in his ~inventory~[TAG_HMU_CHAR_INVENTORY_UI] by using the ~radial menu~[TAG_RADIAL_MENU] and under the Person icon, select Copy Scroll. The wizard must make a Spellcraft check against a DC of: 15 + the spell's level If successful, the spell is entered into his spellbook and the scroll is destroyed. If unsuccessful, the scroll is unharmed, but the wizard may not try again until he gains at least one more rank in Spellcraft. ~Strict Rules~[TAG_STRICT_RULES] Scribing a spell uses up materials costing 100GP per level of the spell. Thus, a scribing a 5th level spell requires spending 500GP on materials to do so. This cost is in addition to the cost of the scroll being copied. The spells learned when a wizard levels up do not incur this cost. diff --git a/tpdatasrc/kotbfixes/tpmes/help_extensions.tab b/tpdatasrc/kotbfixes/tpmes/help_extensions.tab index 8c22e4fd6..153ab218d 100644 --- a/tpdatasrc/kotbfixes/tpmes/help_extensions.tab +++ b/tpdatasrc/kotbfixes/tpmes/help_extensions.tab @@ -121,4 +121,5 @@ TAG_ULTIMATE_MAGUS TAG_PRESTIGE_CLASSES Ultimate Magus Sorcerers channel unkno TAG_CLASS_FEATURES_ULTIMATE_MAGUS TAG_ULTIMATE_MAGUS Ultimate Magus Base Attack & Base Save Bonuses Level ~Base Attack Bonus~[TAG_LEVEL_BONUSES] ~Fortitude~[TAG_FORTITUDE] ~Save~[TAG_LEVEL_BONUSES] ~Reflex~[TAG_REFLEX] ~Save~[TAG_LEVEL_BONUSES] ~Will~[TAG_WILL] ~Save~[TAG_LEVEL_BONUSES] 1 @t+0 @t+0 @t+0 @t+2 2 @t+1 @t+0 @t+0 @t+3 3 @t+1 @t+1 @t+1 @t+3 4 @t+2 @t+1 @t+1 @t+4 5 @t+2 @t+1 @t+1 @t+4 6 @t+3 @t+2 @t+2 @t+5 7 @t+3 @t+2 @t+2 @t+5 8 @t+4 @t+2 @t+2 @t+6 9 @t+4 @t+3 @t+3 @t+6 10 @t+5 @t+3 @t+3 @t+7 TAG_UNSEEN_SEER TAG_PRESTIGE_CLASSES Unseen Seer Mysterious and elusive, the unseen seer trades in secrets. Subterfuge is his business and he uses his magic to help his gather other people's secrets while keeping his own. Requirements: Skills: Hide 8 ranks, Search 8 ranks, Sense Motive 4 ranks, Spellcraft 4 ranks, Spot 8 ranks Spellcasting: Ability to cast 1st-level arcane spells, including at least two divination spells. Hit Die: d4 Base Attack and Base Save Bonuses: see ~table~[TAG_CLASS_FEATURES_UNSEEN_SEER]. Class Skills: ~Bluff~[TAG_BLUFF], ~Concentration~[TAG_CONCENTRATION], ~Diplomacy~[TAG_DIPLOMACY], ~Disable Device~[TAG_DISABLE_DEVICE], ~Gather Information~[TAG_GATHER_INFORMATION], ~Hide~[TAG_HIDE], ~Listen~[TAG_LISTEN], ~Move Silently~[TAG_MOVE_SILENTLY], ~Open Lock~[TAG_OPEN_LOCK], ~Search~[TAG_SEARCH], ~Sense Motive~[TAG_SENSE_MOTIVE], ~Spot~[TAG_SPOT], ~Spellcraft~[TAG_SPELLCRAFT], ~Spot~[TAG_SPOT]. Skill Points at Each Level: 6 + Int modifier Class Features: Weapon and Armor Proficiency: Unseen Seers gain no proficiency with ayn weapon or armor. Spells per Day: When a new unseen seer level is gained, the character gains new spells per day as if he had also gained a level in a spellcasting class he belonged to before adding the prestige class. He does not, however, gain any other benefit a character of that class would have gained (improved chance of controlling or rebuking undead, metamagic or item creation feats, and so on), except for an increased effective level of spellcasting. If a character had more than one spellcasting class before becoming an arcane trickster, the highest one is chosen for adding the new level for the purposes of determining spells per day. Note: Spells that require an attack roll, such as Chill Touch, are considered weapon-like spells and as such benefit from Sneak Attack or Skirmish damage. Damage Bonus: At 1st level, the extra damage you deal with your sneak attack, skirmish, or sudden strike ability increases by 1d6. If you have more than one of these abilities, only one ability gains this increase (choose each time you gain this benefit).Your sneak attack, skirmish, or sudden strike damage increases by another 1d6 at 4th level, 7th level, and 10th level. Advanced Learning (Ex): At 2nd, 5th, and 8th level, you can add a new spell to your spellbook or list of spells known, representing the result of personal study and experimentation. The spell must be a divination spell of a level no higher than that of the highest-level arcane spell you already know. The spell can be from any class's spell list (arcane or divine). Once a new spell is selected, it is forever added to your spell list and can be cast just like any other spell on your list. Select the spell after levelup through the radial menu. Silent Spell: At 2nd level, you gain Silent Spell as a bonus feat. Divination Spell Power (Ex): At 3rd level, you gain a +1 bonus to your caster level when casting an arcane divination spell. This bonus improves to +2 at 6th level, and to +3 at 9th level.This benefit comes at a cost: Your caster level for all other arcane spells is reduced by 1 at 3rd level. This reduction becomes 2 at 6th level and becomes 3 at 9th level. For example, a 4th-level rogue/1st-level sorcerer/6th-level unseen seer would have a caster level of 9th for his arcane divination spells, but only 5th for his nondivination arcane spells. Guarded Mind (Su): Any successful unseen seer must learn to protect himself from magic that would reveal his identity. At 5th level, you become protected by nondetection (as the spell, but with a permanent duration). For the purpose of divinations attempted against you, your caster level equals your character level (no in game effect). TAG_CLASS_FEATURES_UNSEEN_SEER TAG_DUMMY Unseen Seer Base Attack & Base Save Bonuses Level ~Base Attack Bonus~[TAG_LEVEL_BONUSES] ~Fortitude~[TAG_FORTITUDE] ~Save~[TAG_LEVEL_BONUSES] ~Reflex~[TAG_REFLEX] ~Save~[TAG_LEVEL_BONUSES] ~Will~[TAG_WILL] ~Save~[TAG_LEVEL_BONUSES] 1 @t+0 @t+0 @t+0 @t+2 2 @t+1 @t+0 @t+0 @t+3 3 @t+2 @t+1 @t+1 @t+3 4 @t+3 @t+1 @t+1 @t+4 5 @t+3 @t+1 @t+1 @t+4 6 @t+4 @t+2 @t+2 @t+5 7 @t+5 @t+2 @t+2 @t+5 8 @t+6 @t+2 @t+2 @t+6 9 @t+6 @t+3 @t+3 @t+6 10 @t+7 @t+3 @t+3 @t+7 +TAG_CLASS_FEATURES_WIZARD_SPELLBOOK TAG_WIZARDS Spellbook A wizard must study his spellbook each day to prepare his spells. A wizard begins play with a spellbook containing all 0-level wizard spells (except those from his prohibited schools, if any) plus two 1st-level spells of your choice. For each point of ~Intelligence~[TAG_INTELLIGENCE] bonus the wizard has, the spellbook holds one additional 1st-level spell of your choice. At each new wizard level, he gains two new spells of any spell level or levels that he can cast (based on his new wizard level) for his spellbook. A wizard can also copy spells into his spellbook from scrolls. To start, first the wizard needs at least 1 ~skill point~[TAG_ACQUIRE_SKILLS] in the ~Spellcraft~[TAG_SPELLCRAFT] skill for training. Then you select a scroll that the wizard has in his ~inventory~[TAG_HMU_CHAR_INVENTORY_UI] by using the ~radial menu~[TAG_RADIAL_MENU] and under the Person icon, select Copy Scroll. The wizard must make a Spellcraft check against a DC of: 15 + the spell's level If successful, the spell is entered into his spellbook and the scroll is destroyed. If unsuccessful, the scroll is unharmed, but the wizard may not try again until he gains at least one more rank in Spellcraft. ~Strict Rules~[TAG_STRICT_RULES] Scribing a spell uses up materials costing 100GP per level of the spell. Thus, a scribing a 5th level spell requires spending 500GP on materials to do so. This cost is in addition to the cost of the scroll being copied. The spells learned when a wizard levels up do not incur this cost.