diff --git a/TemplePlus/action_sequence.cpp b/TemplePlus/action_sequence.cpp index 34cf595e9..8bf039931 100644 --- a/TemplePlus/action_sequence.cpp +++ b/TemplePlus/action_sequence.cpp @@ -1716,7 +1716,7 @@ int ActionSequenceSystem::TrimPathToRemainingMoveLength(D20Actn* d20a, float rem uint32_t ActionSequenceSystem::ActionCostNull(D20Actn* d20Actn, TurnBasedStatus* turnBasedStatus, ActionCostPacket* actionCostPacket) { actionCostPacket->hourglassCost = 0; - actionCostPacket->chargeAfterPicker = 0; + actionCostPacket->attackCost = 0; actionCostPacket->moveDistCost = 0; return 0; } @@ -3371,7 +3371,7 @@ BOOL ActionSequenceSystem::SimulsAdvance() int ActionSequenceSystem::ActionCostFullAttack(D20Actn* d20, TurnBasedStatus* tbStat, ActionCostPacket* acp) { - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; acp->hourglassCost = 4; int flags = d20->d20Caf; @@ -3512,13 +3512,13 @@ int ActionSequenceSystem::ActionCostProcess(TurnBasedStatus* tbStat, D20Actn* d2 if (tbStat->surplusMoveDistance >= actCost.moveDistCost) { tbStat->surplusMoveDistance -= actCost.moveDistCost; - if ( actCost.chargeAfterPicker <= 0 - || actCost.chargeAfterPicker + tbStat->attackModeCode <= tbStat->baseAttackNumCode + tbStat->numBonusAttacks) + if ( actCost.attackCost <= 0 + || actCost.attackCost + tbStat->attackModeCode <= tbStat->baseAttackNumCode + tbStat->numBonusAttacks) { - if ((int) tbStat->numBonusAttacks < actCost.chargeAfterPicker) - tbStat->attackModeCode += actCost.chargeAfterPicker; + if (actCost.attackCost > (int) tbStat->numBonusAttacks ) + tbStat->attackModeCode += actCost.attackCost; else - tbStat->numBonusAttacks -= actCost.chargeAfterPicker; + tbStat->numBonusAttacks -= actCost.attackCost; if (tbStat->attackModeCode == tbStat->baseAttackNumCode && !tbStat->numBonusAttacks) tbStat->tbsFlags &= ~TBSF_FullAttack; result = AEC_OK; @@ -3782,12 +3782,13 @@ int ActionSequenceSystem::StdAttackTurnBasedStatusCheck(D20Actn* d20a, TurnBased return AEC_TARGET_INVALID; } + const int STD_ATK_HG_COST= 2; if (hgState != -1) - hgState = turnBasedStatusTransitionMatrix[hgState][2]; + hgState = turnBasedStatusTransitionMatrix[hgState][STD_ATK_HG_COST]; tbStat->hourglassState = hgState; - if (inventory.ItemWornAt(d20a->d20APerformer, 3) || dispatch.DispatchD20ActionCheck(d20a, tbStat, dispTypeGetCritterNaturalAttacksNum) <= 0) - tbStat->attackModeCode = 0; + if (inventory.ItemWornAt(d20a->d20APerformer, EquipSlot::WeaponPrimary) || dispatch.DispatchD20ActionCheck(d20a, tbStat, dispTypeGetCritterNaturalAttacksNum) <= 0) + tbStat->attackModeCode = ATTACK_CODE_PRIMARY; else tbStat->attackModeCode = ATTACK_CODE_NATURAL_ATTACK; tbStat->baseAttackNumCode = tbStat->attackModeCode + 1; @@ -4308,12 +4309,15 @@ const char*actionErrorCodeStrings[] = "AEC_NEED_A_STRAIGHT_LINE", "AEC_NO_ACTIONS", "AEC_NOT_IN_COMBAT", - "AEC_AREA_NOT_SAFE" + "AEC_AREA_NOT_SAFE", + "AEC_ABILITY_ON_COOLDOWN", + "AEC_ALREADY_USED_THIS_TURN", + "AEC_ALREADY_ACTIVE" }; ostream & operator<<(ostream & str, ActionErrorCode aec) { size_t i = (size_t)aec; - if (i <= AEC_AREA_NOT_SAFE) { + if (i <= AEC_ALREADY_ACTIVE) { str << actionErrorCodeStrings[i]; } else { diff --git a/TemplePlus/action_sequence.h b/TemplePlus/action_sequence.h index 795bf8f97..5fbdc3894 100644 --- a/TemplePlus/action_sequence.h +++ b/TemplePlus/action_sequence.h @@ -48,7 +48,10 @@ enum ActionErrorCode : uint32_t AEC_NEED_A_STRAIGHT_LINE, AEC_NO_ACTIONS, AEC_NOT_IN_COMBAT, - AEC_AREA_NOT_SAFE + AEC_AREA_NOT_SAFE, + AEC_ABILITY_ON_COOLDOWN, + AEC_ALREADY_USED_THIS_TURN, + AEC_ALREADY_ACTIVE }; // Allows for direct use of ActionErrorCode in format() strings diff --git a/TemplePlus/condition.cpp b/TemplePlus/condition.cpp index fc8d42a9a..66373189b 100644 --- a/TemplePlus/condition.cpp +++ b/TemplePlus/condition.cpp @@ -338,6 +338,9 @@ class ConditionFunctionReplacement : public TempleFix { void apply() override { logger->info("Replacing Condition-related Functions"); + replaceFunction(0x100E19A0, []() { + conds.hashmethods.ConditionHashtableInit(conds.mCondStructHashtable); + }); //dispTypeConditionAddPre static int(__cdecl* orgTempAbilityLoss)(DispatcherCallbackArgs) = replaceFunction(0x100EA1F0, [](DispatcherCallbackArgs args) { Stat statDamaged = (Stat)args.GetCondArg(0); @@ -1863,16 +1866,16 @@ int __cdecl GlobalToHitBonus(DispatcherCallbackArgs args) && !d20Sys.d20Query(args.objHndCaller, DK_QUE_Polymorphed) ) { int attackIdx = dispIo->attackPacket.dispKey - (ATTACK_CODE_NATURAL_ATTACK + 1); - int bonValue = 0; // temporarily used as an index value for obj_f_attack_bonus_idx field + int atkBonIdx = 0; for (int i = 0, j=0; i < 4; i++) { j += objects.getArrayFieldInt32(args.objHndCaller, obj_f_critter_attacks_idx, i); // number of attacks if (attackIdx < j){ - bonValue = i; + atkBonIdx = i; break; } } - bonValue = objects.getArrayFieldInt32(args.objHndCaller, obj_f_attack_bonus_idx, bonValue); + int bonValue = objects.getArrayFieldInt32(args.objHndCaller, obj_f_attack_bonus_idx, atkBonIdx); bonusSys.bonusAddToBonusList(&dispIo->bonlist, bonValue, 1, 118); // base attack } @@ -7479,3 +7482,25 @@ void CondStructNew::AddAoESpellRemover() { AddHook(dispTypeD20Signal, DK_SIG_Spell_End, spCallbacks.AoeSpellRemove); } +uint32_t CondHashSystem::ConditionHashtableInit(ToEEHashtable* hashtable) +{ + const int VANILLA_COND_CAP = 1000; + return HashtableInit(hashtable, VANILLA_COND_CAP /*2023*/); +} + +uint32_t CondHashSystem::CondStructAddToHashtable(CondStruct* condStruct, bool overriding) +{ + + uint32_t key = StringHash(condStruct->condName); + CondStruct* condFound; + uint32_t result = HashtableSearch(condHashTable, key, &condFound); + if (result || overriding) + { + result = HashtableOverwriteItem(condHashTable, key, condStruct); + } + if (result == 3) { // over capacity + logger->error("Condition hashtable over capacity ({})! Trying to add {}", condHashTable->capacity, condStruct->condName); + } + return result; + +} diff --git a/TemplePlus/condition.h b/TemplePlus/condition.h index 8bc8ebab3..750c121ea 100644 --- a/TemplePlus/condition.h +++ b/TemplePlus/condition.h @@ -22,22 +22,9 @@ struct CondHashSystem : ToEEHashtableSystem < CondStruct > temple::Dll::RegisterAddressPtr((void**)&condHashTable); } - uint32_t ConditionHashtableInit(ToEEHashtable * hashtable) - { - return HashtableInit(hashtable, 1000); - } - - uint32_t CondStructAddToHashtable(CondStruct * condStruct, bool overriding = false) - { - uint32_t key = StringHash(condStruct->condName); - CondStruct * condFound; - uint32_t result = HashtableSearch(condHashTable, key, &condFound); - if (result || overriding) - { - result = HashtableOverwriteItem(condHashTable, key, condStruct); - } - return result; - } + uint32_t ConditionHashtableInit(ToEEHashtable* hashtable); + + uint32_t CondStructAddToHashtable(CondStruct* condStruct, bool overriding = false); int GetCondStructHashkey(CondStruct* condStruct) { diff --git a/TemplePlus/config/config.cpp b/TemplePlus/config/config.cpp index 9c735b7c1..c6f383529 100644 --- a/TemplePlus/config/config.cpp +++ b/TemplePlus/config/config.cpp @@ -129,6 +129,7 @@ static ConfigSetting configSettings[] = { CONF_BOOL(antialiasing), CONF_BOOL(softShadows), CONF_BOOL(noSound), + //CONF_BOOL(showFps), CONF_BOOL(featPrereqWarnings), CONF_BOOL(spellAlreadyKnownWarnings), CONF_BOOL(NPCsLevelLikePCs), diff --git a/TemplePlus/critter.cpp b/TemplePlus/critter.cpp index ac1a2e956..19322e1cd 100644 --- a/TemplePlus/critter.cpp +++ b/TemplePlus/critter.cpp @@ -248,8 +248,8 @@ uint32_t LegacyCritterSystem::HasMet(objHndl critter, objHndl otherCritter) { return addresses.HasMet(critter, otherCritter); } -uint32_t LegacyCritterSystem::AddFollower(objHndl npc, objHndl pc, int unkFlag, bool asAiFollower) { - return addresses.AddFollower(npc, pc, unkFlag, asAiFollower); +uint32_t LegacyCritterSystem::AddFollower(objHndl npc, objHndl pc, int forcedFollower, bool asAiFollower) { + return addresses.AddFollower(npc, pc, forcedFollower, asAiFollower); } bool LegacyCritterSystem::FollowerAtMax(){ diff --git a/TemplePlus/d20.cpp b/TemplePlus/d20.cpp index e1114b874..e8c0ba99a 100644 --- a/TemplePlus/d20.cpp +++ b/TemplePlus/d20.cpp @@ -2966,7 +2966,7 @@ ActionErrorCode D20ActionCallbacks::ActionCheckTripAttack(D20Actn* d20a, TurnBas ActionErrorCode D20ActionCallbacks::ActionCostCastSpell(D20Actn * d20a, TurnBasedStatus * tbStat, ActionCostPacket * acp){ acp->hourglassCost = 0; - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0.0f; auto flags = d20a->d20Caf; if ( (flags & D20CAF_FREE_ACTION) || !combatSys.isCombatActive()){ @@ -3030,7 +3030,7 @@ ActionErrorCode D20ActionCallbacks::ActionCostCastSpell(D20Actn * d20a, TurnBase } ActionErrorCode D20ActionCallbacks::ActionCostFullRound(D20Actn* d20a, TurnBasedStatus* tbStat, ActionCostPacket* acp){ - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; acp->hourglassCost = 4; if ( (d20a->d20Caf & D20CAF_FREE_ACTION) || !combatSys.isCombatActive()) @@ -3856,7 +3856,7 @@ ActionErrorCode D20ActionCallbacks::TurnBasedStatusCheckPython(D20Actn* d20a, Tu } ActionErrorCode D20ActionCallbacks::ActionCostFullAttack(D20Actn * d20a, TurnBasedStatus * tbStat, ActionCostPacket * acp){ - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; acp->hourglassCost = 4; //int flags = d20a->d20Caf; @@ -3873,7 +3873,7 @@ ActionErrorCode D20ActionCallbacks::ActionCostFullAttack(D20Actn * d20a, TurnBas } ActionErrorCode D20ActionCallbacks::ActionCostPartialCharge(D20Actn * d20a, TurnBasedStatus * tbStat, ActionCostPacket * acp){ - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; acp->hourglassCost = 3; if ((d20a->d20Caf & D20CAF_FREE_ACTION) || !combatSys.isCombatActive()) @@ -3887,6 +3887,10 @@ ActionErrorCode D20ActionCallbacks::ActionCostPython(D20Actn* d20a, TurnBasedSta return (ActionErrorCode) d20Sys.GetPyActionCost(d20a, tbStat, acp); } +/* +0x100910F0 +Used in: Standard attack, Standard Ranged attack, Trip Attack, Touch Attack, Throw Weapon, Throw Grenade +*/ ActionErrorCode D20ActionCallbacks::ActionCostStandardAttack(D20Actn* d20a, TurnBasedStatus* tbStat, ActionCostPacket* acp){ if ( d20Sys.d20Query(d20a->d20APerformer, DK_QUE_HoldingCharge) @@ -3897,12 +3901,12 @@ ActionErrorCode D20ActionCallbacks::ActionCostStandardAttack(D20Actn* d20a, Turn } acp->hourglassCost = 0; - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; if (!(d20a->d20Caf & D20CAF_FREE_ACTION) && combatSys.isCombatActive()) { - acp->chargeAfterPicker = 1; + acp->attackCost = 1; auto retainSurplusMoveDist = false; @@ -3925,7 +3929,7 @@ ActionErrorCode D20ActionCallbacks::ActionCostStandardAttack(D20Actn* d20a, Turn ActionErrorCode D20ActionCallbacks::ActionCostMoveAction(D20Actn *d20, TurnBasedStatus *tbStat, ActionCostPacket *acp) { acp->hourglassCost = 0; - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; if (!(d20->d20Caf & D20CAF_FREE_ACTION) && combatSys.isCombatActive()) { @@ -3941,7 +3945,7 @@ ActionErrorCode D20ActionCallbacks::ActionCostMoveAction(D20Actn *d20, TurnBased ActionErrorCode D20ActionCallbacks::ActionCostNull(D20Actn* d20a, TurnBasedStatus* tbStat, ActionCostPacket* acp){ acp->hourglassCost = 0; - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; return AEC_OK; } @@ -3953,7 +3957,7 @@ ActionErrorCode D20ActionCallbacks::ActionCostSwift(D20Actn* d20a, TurnBasedStat } acp->hourglassCost = 0; - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; tbStat->tbsFlags |= TBSF_SwiftActionPerformed; return AEC_OK; @@ -3961,13 +3965,13 @@ ActionErrorCode D20ActionCallbacks::ActionCostSwift(D20Actn* d20a, TurnBasedStat ActionErrorCode D20ActionCallbacks::ActionCostStandardAction(D20Actn*, TurnBasedStatus*, ActionCostPacket*acp){ acp->hourglassCost = 2; - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; return AEC_OK; }; ActionErrorCode D20ActionCallbacks::ActionCostWhirlwindAttack(D20Actn* d20a, TurnBasedStatus* tbStat, ActionCostPacket*acp) { - acp->chargeAfterPicker = 0; + acp->attackCost = 0; acp->moveDistCost = 0; acp->hourglassCost = 4; if ( ( d20a->d20Caf & D20CAF_FREE_ACTION) || !combatSys.isCombatActive()){ diff --git a/TemplePlus/d20_defs.h b/TemplePlus/d20_defs.h index 9378dd67a..828f543f4 100644 --- a/TemplePlus/d20_defs.h +++ b/TemplePlus/d20_defs.h @@ -109,10 +109,10 @@ enum class ActionCostType : int { struct ActionCostPacket { int hourglassCost; - int chargeAfterPicker; // flag I think; is only set at stuff that requires using the picker it seems + int attackCost; // how many attacks does this consume when doing Full Attack? (0 if not relevant; haven't seen a value > 1) float moveDistCost; - ActionCostPacket() { hourglassCost = 0; chargeAfterPicker = 0; moveDistCost = 0.0f; } + ActionCostPacket() { hourglassCost = 0; attackCost = 0; moveDistCost = 0.0f; } }; //const auto TestSizeOfActionCostPacket = sizeof(ActionCostPacket); // should be 12 (0xC) diff --git a/TemplePlus/damage.cpp b/TemplePlus/damage.cpp index 4706e58f5..8db22c89b 100644 --- a/TemplePlus/damage.cpp +++ b/TemplePlus/damage.cpp @@ -21,6 +21,8 @@ #include "pybind11/pybind11.h" #include "python/python_dice.h" +const int DAMAGE_MES_UNKNOWN = 103; + namespace py = pybind11; template <> class py::detail::type_caster { @@ -962,16 +964,78 @@ bool Damage::SavingThrowSpell(objHndl obj, objHndl attacker, int dc, SavingThrow // return addresses.SavingThrowSpell(obj, attacker, dc, type, flags, spellId); } -bool Damage::ReflexSaveAndDamage(objHndl obj, objHndl attacker, int dc, int reduction, int flags, const Dice& dice, DamageType damageType, int attackPower, D20ActionType actionType, int spellId) { - SpellPacketBody spPkt(spellId); - BonusList bonlist; +int GetTargetSpellDcBonus(objHndl attacker, objHndl obj, SpellPacketBody &spPkt) { + if (!spPkt.spellEnum) { + return 0; + } // Gets a DC bonus based on the target of the spell + BonusList bonlist; dispatch.DispatchTargetSpellDCBonus(attacker, obj, &bonlist, &spPkt); - int nDCBonus = bonlist.GetEffectiveBonusSum(); + return nDCBonus; +} + +/* 0x100B9500 */ +// Note: this is also used in traps, where spellId = 0 +bool Damage::ReflexSaveAndDamage(objHndl obj, objHndl attacker, int dc, int reduction, int flags, const Dice& dice, DamageType damageType, int attackPower, D20ActionType actionType, int spellId) { + //return addresses.ReflexSaveAndDamage(obj, attacker, dc, reduction, flags, dice.ToPacked(), damageType, attackPower, actionType, spellId); + + auto isSpellSave = actionType == D20A_CAST_SPELL; // todo: might require generalization for new action types? - return addresses.ReflexSaveAndDamage(obj, attacker, dc, reduction, flags, dice.ToPacked(), damageType, attackPower, actionType, spellId); + DispIoReflexThrow evtObj; + evtObj.reduction = (D20SavingThrowReduction)reduction; + evtObj.attackPower = (D20AttackPower)attackPower; + evtObj.damageMesLine = 105; // {105}{~Saving Throw~[TAG_SAVING_THROW_DESC]} + evtObj.attackType = (int)damageType; + evtObj.flags = (D20SavingThrowFlag)flags; + + if (isSpellSave) { // in vanilla, this always called SavingThrow, which would not apply the descriptor flags (and also the new DC bonus dispatch) + evtObj.throwResult = SavingThrowSpell(obj, attacker, dc, SavingThrowType::Reflex, flags, spellId); + } + else { + evtObj.throwResult = SavingThrow(obj, attacker, dc, SavingThrowType::Reflex, flags); + } + + auto caf = D20CAF_NONE; + if (evtObj.throwResult == 1) { + caf = D20CAF_SAVE_SUCCESSFUL; + if (reduction == D20SavingThrowReduction::D20_Save_Reduction_None) { + evtObj.effectiveReduction = 0; + } + else if (reduction == D20SavingThrowReduction::D20_Save_Reduction_Half) { + evtObj.effectiveReduction = 50; + } + else if (reduction == D20SavingThrowReduction::D20_Save_Reduction_Quarter) { + evtObj.effectiveReduction = 25; + } + else { + evtObj.effectiveReduction = 100; + } + } + + + dispatch.Dispatch49ReflexSaveReduction(obj, &evtObj); + if (evtObj.effectiveReduction == 100) { + if (isSpellSave) { + DealSpellDamage(obj, attacker, dice, (DamageType)evtObj.attackType, evtObj.attackPower, + 100, DAMAGE_MES_UNKNOWN, D20A_CAST_SPELL, spellId, caf); + } + else { + DealDamage(obj, attacker, dice, (DamageType)evtObj.attackType, evtObj.attackPower, + 100, DAMAGE_MES_UNKNOWN, actionType); + } + } + else if (isSpellSave) { + DealSpellDamage(obj, attacker, dice, (DamageType)evtObj.attackType, evtObj.attackPower, + evtObj.effectiveReduction, evtObj.damageMesLine, D20A_CAST_SPELL, spellId, caf); + } + else { + DealDamage(obj, attacker, dice, (DamageType)evtObj.attackType, evtObj.attackPower, + 100, evtObj.damageMesLine, actionType); + } + + return evtObj.throwResult; } void Damage::DamagePacketInit(DamagePacket* dmgPkt) diff --git a/TemplePlus/dispatcher.cpp b/TemplePlus/dispatcher.cpp index cbe35e39b..0d9669a18 100644 --- a/TemplePlus/dispatcher.cpp +++ b/TemplePlus/dispatcher.cpp @@ -349,6 +349,20 @@ int DispatcherSystem::Dispatch44FinalSaveThrow(objHndl handle, SavingThrowType s return DispatchSavingThrow(handle, evtObj, dispTypeCountersongSaveThrow, (D20DispatcherKey)((int)saveType + D20DispatcherKey::DK_SAVE_FORTITUDE)); } +int DispatcherSystem::Dispatch49ReflexSaveReduction(objHndl handle, DispIoReflexThrow* evtObj) +{ + auto obj = objSystem->GetObject(handle); + if (!obj) + return 0; + + auto dispatcher = obj->GetDispatcher(); + if (!dispatcherValid(dispatcher)) + return 0; + + dispatcher->Process(dispTypeReflexThrow, D20DispatcherKey::DK_NONE, evtObj); + return evtObj->effectiveReduction; +} + DispIoCondStruct* DispatcherSystem::DispIoCheckIoType1(DispIoCondStruct* dispIo) { @@ -1684,3 +1698,15 @@ DispIoSpellsPerDay::DispIoSpellsPerDay() unk = 3001; unk2 = 0; } + +DispIoReflexThrow::DispIoReflexThrow() +{ + dispIOType = dispIOTypeReflexThrow; + effectiveReduction = 100; + reduction = D20SavingThrowReduction::D20_Save_Reduction_None; // 0 + damageMesLine = 103; // Unknown + attackPower = D20AttackPower::D20DAP_UNSPECIFIED; // 1 + attackType = (int)DamageType::Unspecified; // -1 + throwResult = 0; + flags = D20SavingThrowFlag::D20STF_NONE; +} diff --git a/TemplePlus/dispatcher.h b/TemplePlus/dispatcher.h index e3fd8c5b6..f5ef3b11c 100644 --- a/TemplePlus/dispatcher.h +++ b/TemplePlus/dispatcher.h @@ -23,6 +23,7 @@ struct DispIoDispelCheck; // 11 struct DispIoD20ActionTurnBased; // 12 struct DispIoMoveSpeed; //13 struct DispIOBonusListAndSpellEntry; // 14 +struct DispIoReflexThrow; // 15 struct DispIoObjEvent; // 17 struct DispIoSpellsPerDay; // 18 struct DispIoAbilityLoss; // 19 @@ -72,7 +73,7 @@ struct DispatcherSystem : temple::AddressTable int Dispatch13SavingThrow(objHndl handle, SavingThrowType saveType, DispIoSavingThrow* evtObj); int Dispatch14SavingThrowMod(objHndl handle, SavingThrowType saveType, DispIoSavingThrow* evtObj); int Dispatch44FinalSaveThrow(objHndl handle, SavingThrowType saveType, DispIoSavingThrow* evtObj); - + int Dispatch49ReflexSaveReduction(objHndl handle, DispIoReflexThrow* evtObj); #pragma region event object checkers @@ -457,6 +458,7 @@ struct DispIoReflexThrow : DispIO { // DispIoType = 15 int attackType; int throwResult; D20SavingThrowFlag flags; + DispIoReflexThrow(); }; struct DispIoObjEvent : DispIO // type 17 diff --git a/TemplePlus/fixes/generalfixes.cpp b/TemplePlus/fixes/generalfixes.cpp index 0c09cd6df..4b34982f9 100644 --- a/TemplePlus/fixes/generalfixes.cpp +++ b/TemplePlus/fixes/generalfixes.cpp @@ -23,14 +23,19 @@ struct TigTextStyle; -// fixes size_colossal (was misspelled as size_clossal) -class SizeColossalFix : public TempleFix { + +class HexFixes : public TempleFix { public: void apply() override { + + // fixes size_colossal (was misspelled as size_clossal) writeHex(0x10278078, "73 69 7A 65 5F 63 6F 6C 6F 73 73 61 6C"); + + // fixes Orb of Golden Death infinite Earth Elementals + writeHex(0x101034D9, "C1 E0 10"); // was 08, a wrong shift (looks like a typo error, all the others are shifted by 0x10: radMenuEntry.d20ActionData1 = (invIdxFromCondArg2 << 16) | [spellIdx] } -} sizeColossalFix; +} hexFixes; // Makes Kukri Proficiency Martial class KukriFix : public TempleFix { diff --git a/TemplePlus/gamesystems/aas_hooks.cpp b/TemplePlus/gamesystems/aas_hooks.cpp index 6146c9f8b..3a195e274 100644 --- a/TemplePlus/gamesystems/aas_hooks.cpp +++ b/TemplePlus/gamesystems/aas_hooks.cpp @@ -268,7 +268,7 @@ void AasDebugHooks::apply() { memset(&animParams, 0, sizeof(animParams)); animParams.scale = 1.0f; - model->Advance(1.0f, 0.0f, 0.0f, animParams); // this fixes bad radius/height errors by forcing an update inside 0x102682a0 (itself called by GetSubmesh) + model->Advance(0.0f, 0.0f, 0.1f, animParams); // this fixes bad radius/height errors by forcing an update inside 0x102682a0 (itself called by GetSubmesh) auto maxRadiusSquared = -10000.0f; for (uint32_t i = 0; i < submeshes.size(); i++) { diff --git a/TemplePlus/hashtable.h b/TemplePlus/hashtable.h index c9bb81608..c78e3e573 100644 --- a/TemplePlus/hashtable.h +++ b/TemplePlus/hashtable.h @@ -8,11 +8,13 @@ struct ToEEHashtable : temple::TempleAlloc { uint32_t numItems; uint32_t capacity; - uint32_t powerOfTwo; - uint32_t * keyArray; + uint32_t powerOfTwo; // 2*capacity, rounded up to power of 2 + uint32_t * keyArray; // capacity: powerOfTwo T** dataArray; - uint32_t * idxArray; + uint32_t * idxArray; // capacity: capacity uint32_t pad; + + uint32_t Init(uint32_t capacity); }; template @@ -22,7 +24,8 @@ struct ToEEHashtableSystem : temple::AddressTable { hashtableOut->capacity = capacity; uint32_t powerOfTwo = 1; - for (hashtableOut->numItems = 0; powerOfTwo < 2 * capacity; powerOfTwo *= 2); + hashtableOut->numItems = 0; + for (; powerOfTwo < 2 * capacity; powerOfTwo *= 2); hashtableOut->powerOfTwo = powerOfTwo; hashtableOut->dataArray = _dataArrayNew(powerOfTwo); @@ -152,3 +155,9 @@ struct ToEEHashtableSystem : temple::AddressTable uint32_t(__cdecl* ELFhash)(char * stringIn); }; + +template +inline uint32_t ToEEHashtable::Init(uint32_t capacity) +{ + return uint32_t(); +} diff --git a/TemplePlus/inventory.cpp b/TemplePlus/inventory.cpp index ab7fb4263..b1eaf2b91 100644 --- a/TemplePlus/inventory.cpp +++ b/TemplePlus/inventory.cpp @@ -411,6 +411,7 @@ objHndl InventorySystem::FindMatchingStackableItem(objHndl receiver, objHndl ite auto itemSpell = itemObj->GetSpell(obj_f_item_spell_idx, 0); auto invenItemSpell = invenItemObj->GetSpell(obj_f_item_spell_idx, 0); if (itemSpell.spellLevel != invenItemSpell.spellLevel + || itemSpell.spellEnum != invenItemSpell.spellEnum // Temple+: added this for automated scribed scrolls (who all use the same proto) || (itemObj->type == obj_t_scroll && spellSys.IsArcaneSpellClass(itemSpell.classCode) != spellSys.IsArcaneSpellClass(invenItemSpell.classCode) )) diff --git a/TemplePlus/python/python_dispatcher.cpp b/TemplePlus/python/python_dispatcher.cpp index ff92f66fc..9bac384a7 100644 --- a/TemplePlus/python/python_dispatcher.cpp +++ b/TemplePlus/python/python_dispatcher.cpp @@ -1194,7 +1194,10 @@ PYBIND11_EMBEDDED_MODULE(tpdp, m) { py::class_(m, "EventObjSpellsPerDay", "Used for retrieving spells per day mods. Resurrected in Temple+!") .def_readwrite("bonus_list", &DispIoSpellsPerDay::bonList) - .def_readwrite("caster_class", &DispIoSpellsPerDay::classCode) + //.def_readwrite("caster_class", &DispIoSpellsPerDay::classCode) + .def("get_caster_class", [](DispIoSpellsPerDay& self)->int { + return (int)(self.classCode); + }) .def_readwrite("spell_level", &DispIoSpellsPerDay::spellLvl) .def_readwrite("base_caster_level", &DispIoSpellsPerDay::casterEffLvl) ; diff --git a/TemplePlus/python/python_integration_d20_action.cpp b/TemplePlus/python/python_integration_d20_action.cpp index f1cdc4646..71e94ca9a 100644 --- a/TemplePlus/python/python_integration_d20_action.cpp +++ b/TemplePlus/python/python_integration_d20_action.cpp @@ -133,9 +133,9 @@ PYBIND11_EMBEDDED_MODULE(tpactions, m) { return TRUE; }); - m.def("get_cur_seq", []()->ActnSeq &{ - return **actSeqSys.actSeqCur; - }); + m.def("get_cur_seq", []()->ActnSeq *{ + return *actSeqSys.actSeqCur; + }, py::return_value_policy::reference ); m.def("get_current_tb_actor", []()->objHndl { auto actor = tbSys.turnBasedGetCurrentActor(); diff --git a/TemplePlus/python/python_object.cpp b/TemplePlus/python/python_object.cpp index 9b2815fed..4599168a5 100644 --- a/TemplePlus/python/python_object.cpp +++ b/TemplePlus/python/python_object.cpp @@ -2838,6 +2838,24 @@ static PyObject* PyObjHandle_AnimGoalPushHitByWeapon(PyObject* obj, PyObject* ar return PyInt_FromLong(gameSystems->GetAnim().PushGoalHitByWeapon( attacker, self->handle)); } +static PyObject* PyObjHandle_AnimGoalThrowSpellWithCastAnim(PyObject* obj, PyObject* args) { + + auto self = GetSelf(obj); + if (!self->handle) { + return PyInt_FromLong(0); + } + + if (!PyArg_ParseTuple(args, ":objhndl.anim_goal_throw_spell_w_cast_anim")) { + return 0; + } + auto curSeq = *actSeqSys.actSeqCur; + if (!curSeq || curSeq->performer != self->handle) { + return PyInt_FromLong(0); + } + auto &pkt = curSeq->spellPktBody; + return PyInt_FromLong(gameSystems->GetAnim().PushSpellCast(pkt, objHndl::null)); +} + static PyObject* PyObjHandle_AnimGoalPushUseObject(PyObject* obj, PyObject* args) { auto self = GetSelf(obj); if (!self->handle) { @@ -4420,6 +4438,7 @@ static PyMethodDef PyObjHandleMethods[] = { { "anim_goal_push_attack", PyObjHandle_AnimGoalPushAttack, METH_VARARGS, NULL }, { "anim_goal_push_dodge", PyObjHandle_AnimGoalPushDodge, METH_VARARGS, NULL }, { "anim_goal_push_hit_by_weapon", PyObjHandle_AnimGoalPushHitByWeapon, METH_VARARGS, NULL }, + { "anim_goal_throw_spell_w_cast_anim", PyObjHandle_AnimGoalThrowSpellWithCastAnim, METH_VARARGS, NULL }, { "anim_goal_use_object", PyObjHandle_AnimGoalPushUseObject, METH_VARARGS, NULL }, { "anim_goal_get_new_id", PyObjHandle_AnimGoalGetNewId, METH_VARARGS, NULL }, { "apply_projectile_particles", PyObjHandle_ApplyProjectileParticles, METH_VARARGS, NULL }, diff --git a/TemplePlus/spell.cpp b/TemplePlus/spell.cpp index 783b7d763..bfcd8dcb2 100644 --- a/TemplePlus/spell.cpp +++ b/TemplePlus/spell.cpp @@ -867,7 +867,7 @@ uint32_t LegacySpellSystem::spellRegistryCopy(uint32_t spellEnum, SpellEntry* sp return spellEntryRegistry.copy(spellEnum, spellEntry); } -void LegacySpellSystem::DoForSpellEntries(void(*cb)(SpellEntry & spellEntry)){ +void LegacySpellSystem::DoForSpellEntries( const std::function &cb){ for (auto it : spellEntryRegistry) { cb(*it.data); } @@ -882,10 +882,8 @@ int LegacySpellSystem::CopyLearnableSpells(objHndl& handle, int spellClass, std: for (auto it : spellEntryRegistry) { auto spEntry = it.data; - // new spells from supplemental materials are generally added above enum 802 in Temple+ - if (spEntry->spellEnum > SPELL_ENUM_MAX_VANILLA) { - if (!config.nonCoreMaterials) - continue; + if (IsNonCore(spEntry->spellEnum) && !config.nonCoreMaterials) { + continue; } if (GetSpellLevelBySpellClass(spEntry->spellEnum, spellClass) >= 0) { entries.push_back(*spEntry); @@ -1414,7 +1412,7 @@ void LegacySpellSystem::SpellPacketSetCasterLevel(SpellPacketBody* spellPkt) con } // item spell - else if (spellPkt->invIdx != 255 && (spellPkt->spellEnum < NORMAL_SPELL_RANGE || spellPkt->spellEnum > SPELL_LIKE_ABILITY_RANGE)){ + else if (spellPkt->invIdx != 255 && !IsMonsterSpell(spellPkt->spellEnum)){ spellPkt->casterLevel = 0; logger->info("Critter {} is casting item spell {} at base caster_level {}.", casterName, spellName, 0); } @@ -1436,7 +1434,7 @@ void LegacySpellSystem::SpellPacketSetCasterLevel(SpellPacketBody* spellPkt) con } else{ // domain spell if (spellPkt->spellClass == Domain_Special){ // domain special (usually used for monsters) - if (spellPkt->invIdx != 255 && (spellPkt->spellEnum < NORMAL_SPELL_RANGE || spellPkt->spellEnum > SPELL_LIKE_ABILITY_RANGE)) { + if (spellPkt->invIdx != 255 && !IsMonsterSpell(spellPkt->spellEnum)) { spellPkt->casterLevel = 0; logger->info("Critter {} is casting item spell {} at base caster_level {}.", casterName, spellName, 0); } @@ -2902,6 +2900,12 @@ int LegacySpellSystem::GetSpellSchool(int spellEnum){ return spEntry.spellSchoolEnum; } +bool LegacySpellSystem::IsMonsterSpell(int spellEnum) +{ + return spellEnum >= NORMAL_SPELL_RANGE + && spellEnum <= SPELL_LIKE_ABILITY_RANGE; +} + bool LegacySpellSystem::IsSpellLike(int spellEnum){ return (spellEnum >= NORMAL_SPELL_RANGE && spellEnum <= SPELL_LIKE_ABILITY_RANGE) || spellEnum >= CLASS_SPELL_LIKE_ABILITY_START; @@ -2922,6 +2926,12 @@ bool LegacySpellSystem::IsNewSlotDesignator(int spellEnum) return false; } +// Non-Core spells will be added in the expanded range +bool LegacySpellSystem::IsNonCore(int spellEnum) +{ + return (spellEnum > SPELL_ENUM_MAX_VANILLA); +} + int LegacySpellSystem::GetSpellLevelBySpellClass(int spellEnum, int spellClass, objHndl handle){ if (IsLabel(spellEnum)) diff --git a/TemplePlus/spell.h b/TemplePlus/spell.h index 68e15c787..944c89485 100644 --- a/TemplePlus/spell.h +++ b/TemplePlus/spell.h @@ -170,7 +170,7 @@ struct LegacySpellSystem : temple::AddressTable uint32_t spellRegistryCopy(uint32_t spellEnum, SpellEntry* spellEntry); - void DoForSpellEntries( void(__cdecl*cb)(SpellEntry & spellEntry)); + void DoForSpellEntries( const std::function &cb); int CopyLearnableSpells(objHndl & handle, int spellClass, std::vector & entries); uint32_t ConfigSpellTargetting(PickerArgs* pickerArgs, SpellPacketBody* spellPacketBody); @@ -224,10 +224,11 @@ struct LegacySpellSystem : temple::AddressTable These are not interruptible via Ready vs. Spell, and do not automatically show up in the console as [ACTOR] casts [SPELL]! */ + static bool IsMonsterSpell(int spellEnum); static bool IsSpellLike(int spellEnum); static bool IsLabel(int spellEnum); // check if it is a hardcoded "label" enum (used in the GUI etc) static bool IsNewSlotDesignator(int spellEnum); // check if it is a hardcoded "new slot" designator (used for sorting) enums 1605-1614 - + static bool IsNonCore(int spellEnum); int GetSpellLevelBySpellClass(int spellEnum, int spellClass, objHndl handle = objHndl::null); // returns -1 if not available for spell class bool SpellHasMultiSelection(int spellEnum); diff --git a/TemplePlus/spell_condition.cpp b/TemplePlus/spell_condition.cpp index 13b09df6f..41b0be5bb 100644 --- a/TemplePlus/spell_condition.cpp +++ b/TemplePlus/spell_condition.cpp @@ -20,6 +20,9 @@ #include "float_line.h" #include "action_sequence.h" #include "ai.h" +#include +#include +#include void PyPerformTouchAttack_PatchedCallToHitProcessing(D20Actn * pd20A, D20Actn d20A, uint32_t savedesi, uint32_t retaddr, PyObject * pyObjCaller, PyObject * pyTupleArgs); @@ -76,8 +79,13 @@ class SpellConditionFixes : public TempleFix { static int SpellResistance_SpellResistanceMod(DispatcherCallbackArgs args); + static int SuggestionOnAdd(DispatcherCallbackArgs args); + void apply() override { + // Fix for when summoned Balor from skull casts suggestion + replaceFunction(0x100D01D0, SuggestionOnAdd); + // Magic Circle Taking Damage - didn't check that attacker is not null replaceFunction(0x100C8D60, MagicCirclePreventDamage); @@ -1468,3 +1476,31 @@ int SpellConditionFixes::SpellResistance_SpellResistanceMod(DispatcherCallbackAr return 0; } +int SpellConditionFixes::SuggestionOnAdd(DispatcherCallbackArgs args) +{ + auto spellId = args.GetCondArg(0); + auto duration = args.GetCondArg(1); + auto arg2 = args.GetCondArg(2); // is 0... + + if (!conds.AddTo(args.objHndCaller, "Charmed", { spellId, duration, arg2 })) { + logger->error("d20_mods_spells.c / _begin_spell_suggestion(): unable to add condition"); + } + floatSys.FloatSpellLine(args.objHndCaller, 20018, FloatLineColor::Red); // Charmed! + auto partyLeader = party.GetConsciousPartyLeader(); + SpellPacketBody pkt(spellId); + if (party.IsInParty(pkt.caster)) { + if (party.ObjIsAIFollower(pkt.caster)) { + critterSys.AddFollower(args.objHndCaller, /*partyLeader*/ pkt.caster, 1, 1); + } + else { + critterSys.AddFollower(args.objHndCaller, pkt.caster, 1, 0); + uiSystems->GetParty().Update(); + } + } + else { + critterSys.AddFollower(args.objHndCaller, pkt.caster, 1, 1); + } + args.SetCondArg(2, 1); + return 0; +} + diff --git a/TemplePlus/ui/ui_item_creation.cpp b/TemplePlus/ui/ui_item_creation.cpp index 3ad381d11..75cf05d53 100644 --- a/TemplePlus/ui/ui_item_creation.cpp +++ b/TemplePlus/ui/ui_item_creation.cpp @@ -259,6 +259,8 @@ UiItemCreation::UiItemCreation(const UiSystemConf &config) { memset(craftedItemHandles, 0, sizeof(craftedItemHandles)); craftedItemNamePos = 0; craftingWidgetId = -1; + scribeScrollSpells.clear(); + mScribedScrollSpell = 0; LoadMaaSpecs(); @@ -408,7 +410,7 @@ int UiItemCreation::CraftedWandCasterLevel(objHndl item) return casterLvl; } -bool UiItemCreation::CreateItemResourceCheck(objHndl crafter, objHndl objHndItem){ +bool UiItemCreation::CreateItemResourceCheck(objHndl crafter, objHndl objHndItem, int spellEnum){ bool canCraft = 1; bool xpCheck = 0; auto insuffXp = itemCreationAddresses.craftInsufficientXP; @@ -416,10 +418,10 @@ bool UiItemCreation::CreateItemResourceCheck(objHndl crafter, objHndl objHndItem auto insuffSkill = itemCreationAddresses.craftSkillReqNotMet; auto insuffPrereqs = itemCreationAddresses.insuffPrereqs; auto surplusXP = d20LevelSys.GetSurplusXp(crafter); - uint32_t craftingCostCP; + uint32_t craftingCostCP = 0; auto partyMoney = party.GetMoney(); - auto itemObj = objSystem->GetObject(objHndItem); + *insuffXp = 0; *insuffCp = 0; @@ -427,28 +429,31 @@ bool UiItemCreation::CreateItemResourceCheck(objHndl crafter, objHndl objHndItem *insuffPrereqs = 0; // Check GP Section - int itemWorth = itemObj->GetInt32(obj_f_item_worth); - - // Scrolls + int itemWorth = 0; if (itemCreationType == ItemCreationType::ScribeScroll){ - itemWorth = ScribedScrollWorth(objHndItem, ScribedScrollCasterLevel(objHndItem)); - craftingCostCP = itemWorth / 2; - } - // MAA - else if (itemCreationType == ItemCreationType::CraftMagicArmsAndArmor){ - craftingCostCP = MaaCpCost( CRAFT_EFFECT_INVALID ); - } - // Wands & Potions - else if (itemCreationType == ItemCreationType::CraftWand){ - itemWorth = CraftedWandWorth(objHndItem, CraftedWandCasterLevel(objHndItem)); //ItemWorthAdjustedForCasterLevel(objHndItem, CraftedWandCasterLevel(objHndItem)); + itemWorth = ScribedScrollWorth(spellEnum, ScribedScrollCasterLevel(spellEnum)); craftingCostCP = itemWorth / 2; } - // Potions else { - // current method for crafting stuff: - craftingCostCP = itemWorth / 2; - }; + auto itemObj = objSystem->GetObject(objHndItem); + itemWorth = itemObj->GetInt32(obj_f_item_worth); + // MAA + if (itemCreationType == ItemCreationType::CraftMagicArmsAndArmor) { + craftingCostCP = MaaCpCost(CRAFT_EFFECT_INVALID); + } + // Wands & Potions + else if (itemCreationType == ItemCreationType::CraftWand) { + itemWorth = CraftedWandWorth(objHndItem, CraftedWandCasterLevel(objHndItem)); //ItemWorthAdjustedForCasterLevel(objHndItem, CraftedWandCasterLevel(objHndItem)); + craftingCostCP = itemWorth / 2; + } + // Potions + else { + // current method for crafting stuff: + craftingCostCP = itemWorth / 2; + }; + } + if ( ( (uint32_t)partyMoney ) < craftingCostCP){ *insuffCp = 1; canCraft = 0; @@ -458,63 +463,27 @@ bool UiItemCreation::CreateItemResourceCheck(objHndl crafter, objHndl objHndItem // Check XP & prerequisites section // Scrolls, Wands and Potions: - if ( itemCreationType != CraftMagicArmsAndArmor){ - // check requirements from rules\\item_creation.mes - if (!ItemCreationParseMesfileEntry(crafter, objHndItem)){ + if (itemCreationType == ItemCreationType::ScribeScroll) { + if (!ScribeScrollCheck(crafter, spellEnum)) { *insuffPrereqs = 1; canCraft = 0; } - - // scrolls - ensure caster can also actually cast the spell - else if (itemCreationType == ItemCreationType::ScribeScroll ) { - - auto spData = itemObj->GetSpell(obj_f_item_spell_idx, 0); // scrolls should only have a single spell... - auto spEnum = spData.spellEnum; - - std::vector spellClasses, spellLevels; - - auto isLevelOk = false; - - if (!spellSys.SpellKnownQueryGetData(crafter, spEnum, spellClasses, spellLevels)) - *insuffPrereqs = 1; - - for (auto i=0u; i < spellLevels.size(); i++){ - auto maxSpellLvl = -1; - auto spClass = spellClasses[i]; - if (spellSys.isDomainSpell(spClass)) - { - maxSpellLvl = spellSys.GetMaxSpellLevel(crafter, stat_level_cleric); - - } else - { - maxSpellLvl = spellSys.GetMaxSpellLevel(crafter, spellSys.GetCastingClass(spClass)); - } - - - if (maxSpellLvl >= spellLevels[i]) - { - isLevelOk = true; - break; - } - } - - if (!isLevelOk){ - *insuffPrereqs = 1; - canCraft = 0; - } - - } - - // check XP + // check XP int itemXPCost = itemWorth / 2500; xpCheck = surplusXP >= itemXPCost; - } - // MAA - else { + } + else if (itemCreationType == CraftMagicArmsAndArmor) { int magicArmsAndArmorXPCost = MaaXpCost(CRAFT_EFFECT_INVALID); xpCheck = surplusXP >= magicArmsAndArmorXPCost; } + else { + // check requirements from rules\\item_creation.mes + if (!ItemCreationParseMesfileEntry(crafter, objHndItem)){ + *insuffPrereqs = 1; + canCraft = 0; + } + } if (xpCheck){ return canCraft; @@ -526,6 +495,30 @@ bool UiItemCreation::CreateItemResourceCheck(objHndl crafter, objHndl objHndItem } +bool UiItemCreation::ScribeScrollCheck(objHndl crafter, int spEnum) +{ + std::vector spellClasses, spellLevels; + if (!spellSys.SpellKnownQueryGetData(crafter, spEnum, spellClasses, spellLevels)) + return false; + + for (auto i = 0u; i < spellLevels.size(); i++) { + auto maxSpellLvl = -1; + auto spClass = spellClasses[i]; + if (spellSys.isDomainSpell(spClass)) { + maxSpellLvl = spellSys.GetMaxSpellLevel(crafter, stat_level_cleric); + } + else { + maxSpellLvl = spellSys.GetMaxSpellLevel(crafter, spellSys.GetCastingClass(spClass)); + } + + + if (maxSpellLvl >= spellLevels[i]) { + return true; + } + } + return false; +} + const char* UiItemCreation::GetItemCreationMesLine(int lineId){ MesLine line; line.key = lineId; @@ -953,15 +946,35 @@ void UiItemCreation::CraftScrollWandPotionSetItemSpellData(objHndl objHndItem, o return; } + if (itemCreationType == ScribeScroll){ auto scrollSpell = obj->GetSpell(obj_f_item_spell_idx, 0); - ScribedScrollSpellGet(objHndItem, scrollSpell); + ScribedScrollSpellGet(mScribedScrollSpell, scrollSpell); obj->SetSpell(obj_f_item_spell_idx, 0, scrollSpell); + auto invAid = GetScrollInventoryIconId(mScribedScrollSpell); + obj->SetInt32(obj_f_item_inv_aid, invAid); + + + auto baseDescr = description.GetDescriptionString(obj->GetInt32(obj_f_description) ); + auto spellName = spellSys.GetSpellName(mScribedScrollSpell); + int SPELL_ENUM_AID = 1; + auto aidSpellName = spellSys.GetSpellName(SPELL_ENUM_AID); + auto pos = std::strstr(baseDescr, aidSpellName); + auto endPos = pos + std::strlen(aidSpellName); + char newName[1024] = {0,}; + auto idx = 0; + for (idx = 0; baseDescr + idx < pos; ++idx) { + newName[idx] = baseDescr[idx]; + } + for (auto i=0; i < strlen(spellName); ++i) { + newName[idx+i] = spellName[i]; + } - /*int casterLevelFinal = scrollSpell.spellLevel * 2 - 1; - if (casterLevelFinal < 1) - casterLevelFinal = 1;*/ - + for (auto i = 0; endPos + i < baseDescr + strlen(baseDescr); ++i) { + newName[idx + strlen(spellName) + i] = endPos[i]; + } + auto newNameId = description.CustomNameNew(newName); + obj->SetInt32(obj_f_description, newNameId); return; }; @@ -1013,7 +1026,7 @@ void UiItemCreation::CraftScrollWandPotionSetItemSpellData(objHndl objHndItem, o }; -void UiItemCreation::CreateItemDebitXPGP(objHndl crafter, objHndl objHndItem){ +void UiItemCreation::CreateItemDebitXPGP(objHndl crafter, objHndl objHndItem, int spellEnum){ uint32_t crafterXP = objects.getInt32(crafter, obj_f_critter_experience); uint32_t craftingCostCP = 0; uint32_t craftingCostXP = 0; @@ -1028,7 +1041,7 @@ void UiItemCreation::CreateItemDebitXPGP(objHndl crafter, objHndl objHndItem){ if (itemCreationType == ItemCreationType::CraftWand) itemWorth = CraftedWandWorth(objHndItem, CraftedWandCasterLevel(objHndItem)); else if (itemCreationType == ItemCreationType::ScribeScroll){ - itemWorth = ScribedScrollWorth(objHndItem, ScribedScrollCasterLevel(objHndItem)); + itemWorth = ScribedScrollWorth(spellEnum, ScribedScrollCasterLevel(spellEnum)); } else itemWorth = objects.getInt32(objHndItem, obj_f_item_worth); @@ -1121,7 +1134,7 @@ bool UiItemCreation::CraftedWandSpellGet(objHndl item, SpellStoreData & spellDat } -void UiItemCreation::ItemCreationCraftingCostTexts(int widgetId, objHndl objHndItem){ +void UiItemCreation::ItemCreationCraftingCostTexts(int widgetId, objHndl objHndItem, int spellEnum){ // prolog int32_t * insuffXp; int32_t * insuffCp; @@ -1132,19 +1145,23 @@ void UiItemCreation::ItemCreationCraftingCostTexts(int widgetId, objHndl objHndI TigRect rect(212 + 108 * mUseCo8Ui, 157, 159, 10); char * prereqString; - int casterLevelNew = -1; // h4x! - auto obj = objSystem->GetObject(objHndItem); - auto itemWorth = obj->GetInt32(obj_f_item_worth); + int casterLevelNew = -1; + auto itemWorth = 0; - if (itemCreationType == CraftWand){ + if (itemCreationType == ScribeScroll) { + casterLevelNew = ScribedScrollCasterLevel(spellEnum); + itemWorth = ScribedScrollWorth(spellEnum, casterLevelNew); + } + else if (itemCreationType == CraftWand){ casterLevelNew = CraftedWandCasterLevel(objHndItem); itemWorth = CraftedWandWorth(objHndItem, casterLevelNew); } - else if (itemCreationType == ScribeScroll){ - casterLevelNew = ScribedScrollCasterLevel(objHndItem); - itemWorth = ScribedScrollWorth(objHndItem, casterLevelNew); + else { + auto obj = objSystem->GetObject(objHndItem); + itemWorth = obj->GetInt32(obj_f_item_worth); } + insuffXp = itemCreationAddresses.craftInsufficientXP; insuffCp = itemCreationAddresses.craftInsufficientFunds; @@ -1205,11 +1222,58 @@ void UiItemCreation::ItemCreationCraftingCostTexts(int widgetId, objHndl objHndI rect.y = 200; rect.width = 150; rect.height = 105; - prereqString = temple::GetRef(0x101525B0)(itemCreationCrafter, objHndItem); - if (prereqString){ - UiRenderer::DrawTextInWidget(widgetId, prereqString, rect, *itemCreationAddresses.itemCreationTextStyle); + if (GetItemCreationType() == ItemCreationType::ScribeScroll) { + std::string tmp; + + auto prereqStr = PrintPrereqToken("S"); + auto prereqMet = spellSys.IsSpellKnown(itemCreationCrafter, spellEnum); + + auto minCasterLevel = 0; + auto clPrereqMet = false; + if (prereqMet) { + SpellStoreData spellData; + ScribedScrollSpellGet(spellEnum, spellData); + + auto castingClass = !spellSys.isDomainSpell(spellData.classCode) ? spellSys.GetCastingClass(spellData.classCode) : stat_level_cleric; + minCasterLevel = d20ClassSys.GetMinCasterLevelForSpellLevel(castingClass, spellData.spellLevel); + clPrereqMet = critterSys.GetCasterLevelForClass(itemCreationCrafter, castingClass) >= minCasterLevel; + + } + + auto clPrereqStr = PrintPrereqToken( fmt::format("C{}", minCasterLevel).c_str() ); + + + static auto prereqFieldLabel = temple::GetRef(0x10BED98C); + if (*itemCreationAddresses.craftInsufficientXP + || *itemCreationAddresses.craftInsufficientFunds + || *itemCreationAddresses.craftSkillReqNotMet + || *itemCreationAddresses.insuffPrereqs) { + + //_snprintf(tmp, 2000, "@0%s @%d%s", prereqFieldLabel, prereqMet ? 1 : 2, prereqStr); + tmp.append(fmt::format("@0{} @{:d}{}\n", prereqFieldLabel, prereqMet ? 1 : 2, prereqStr)); + if (prereqMet) { + tmp.append(fmt::format("@0{} @{:d}{}\n", prereqFieldLabel, clPrereqMet ? 1 : 2, clPrereqStr)); + } + } + else { + //_snprintf(tmp, 2000, "@0%s @3%s", prereqFieldLabel, prereqStr); + tmp.append(fmt::format("@0{} @3{}\n", prereqFieldLabel, prereqStr)); + if (prereqMet) { + tmp.append(fmt::format("@0{} @3{}\n", prereqFieldLabel, clPrereqStr)); + } + } + if (tmp.size()) { + UiRenderer::DrawTextInWidget(widgetId, tmp, rect, *itemCreationAddresses.itemCreationTextStyle); + } + } + else { + prereqString = temple::GetRef(0x101525B0)(itemCreationCrafter, objHndItem); + if (prereqString) { + UiRenderer::DrawTextInWidget(widgetId, prereqString, rect, *itemCreationAddresses.itemCreationTextStyle); + } } + if (!*insuffPrereq && (itemCreationType == ItemCreationType::CraftWand || itemCreationType == ItemCreationType::ScribeScroll)) { @@ -1252,7 +1316,11 @@ BOOL UiItemCreation::ItemCreationEntryMsg(int widId, TigMsg* msg){ craftingItemIdx = itemIdx; auto itemHandle = craftedItemHandles[itemCreationType][itemIdx]; - if (CreateItemResourceCheck(itemCreationCrafter, itemHandle)) { + mScribedScrollSpell = 0; + if (itemCreationType == ItemCreationType::ScribeScroll) { + mScribedScrollSpell = scribeScrollSpells[itemIdx]; + } + if (CreateItemResourceCheck(itemCreationCrafter, itemHandle, mScribedScrollSpell)) { uiManager->SetButtonState(mItemCreationCreateBtnId, LgcyButtonState::Normal); } else { @@ -1576,15 +1644,11 @@ uint32_t UiItemCreation::CraftedWandWorth(objHndl item, int casterLevelNew){ } -bool UiItemCreation::ScribedScrollSpellGet(objHndl item, SpellStoreData & spellDataOut, int * spellLevelBaseOut){ - if (!item) - return false; - auto obj = objSystem->GetObject(item); - if (!obj->GetSpellArray(obj_f_item_spell_idx).GetSize()) - return false; - +bool UiItemCreation::ScribedScrollSpellGet(int spellEnum, SpellStoreData & spellDataOut, int * spellLevelBaseOut){ + // get spell - auto spellData = obj->GetSpell(obj_f_item_spell_idx, 0); + SpellStoreData spellData; + spellData.spellEnum = spellEnum; // default values (shouldn't really be used...) int spellLevelBasic = 999; @@ -1655,15 +1719,15 @@ bool UiItemCreation::ScribedScrollSpellGet(objHndl item, SpellStoreData & spellD return true; } -int UiItemCreation::ScribedScrollSpellLevel(objHndl item) +int UiItemCreation::ScribedScrollSpellLevel(int spellEnum) { SpellStoreData scrollSpell; - if (!ScribedScrollSpellGet(item, scrollSpell)) + if (!ScribedScrollSpellGet(spellEnum, scrollSpell)) scrollSpell.spellLevel = -1; return scrollSpell.spellLevel; } -int UiItemCreation::ScribedScrollCasterLevel(objHndl item) +int UiItemCreation::ScribedScrollCasterLevel(int spellEnum) { // int result = ScribedScrollSpellLevel(item); /*if (result <= 1) @@ -1672,7 +1736,7 @@ int UiItemCreation::ScribedScrollCasterLevel(objHndl item) */ SpellStoreData scrollSpell; - if (!ScribedScrollSpellGet(item, scrollSpell)) + if (!ScribedScrollSpellGet(spellEnum, scrollSpell)) scrollSpell.spellLevel = -1; if (scrollSpell.spellLevel <= 1) return 1; @@ -1691,21 +1755,18 @@ int UiItemCreation::ScribedScrollCasterLevel(objHndl item) return casterLvl; } -uint32_t UiItemCreation::ScribedScrollWorth(objHndl item, int casterLevelNew) +uint32_t UiItemCreation::ScribedScrollWorth(int spellEnum, int casterLevelNew) { auto baseWorth = 25; - auto obj = objSystem->GetObject(item); - - // which spell? - auto spellData = obj->GetSpell(obj_f_item_spell_idx, 0); - // Calculate cost - SpellEntry spEntry(spellData.spellEnum); + // Calculate cost + SpellEntry spEntry(spellEnum); int materialCost = spEntry.costGp; // retrieve Spell Known data (e.g. for Bards) and caster level (as modified by user selection) - int spellLevelBase = spellData.spellLevel; // default value - ScribedScrollSpellGet(item, spellData, &spellLevelBase); + int spellLevelBase = 0; // default value + SpellStoreData spellData; + ScribedScrollSpellGet(spellEnum, spellData, &spellLevelBase); auto casterLevelBase = max(1,spellLevelBase * 2 - 1); auto casterClass = (Stat)spellSys.GetCastingClass(spellData.classCode); auto minCasterLevel = (int)d20ClassSys.GetMinCasterLevelForSpellLevel(casterClass, spellLevelBase); @@ -1822,7 +1883,7 @@ UiItemCreation::UiItemCreation(){ memset(craftedItemHandles, 0, sizeof(craftedItemHandles)); craftedItemNamePos = 0; craftingWidgetId = -1; - + mScribedScrollSpell = 0; } int UiItemCreation::GetItemCreationType(){ @@ -1998,8 +2059,14 @@ std::string UiItemCreation::PrintPrereqToken(const char * reqTxt) result = fmt::format("{}", d20Stats.GetRaceName(d20RaceSys.GetRaceEnum(reqTxt + 1)) ); break; case 'S': + { + if (itemCreationType == ItemCreationType::ScribeScroll) { + result = spellSys.GetSpellName(mScribedScrollSpell); + return result; + } result = fmt::format("{}", spellSys.GetSpellName(spellSys.GetSpellEnum(reqTxt + 1))); break; + } case 'O': { StringTokenizer tok(reqTxt + 1); @@ -2025,6 +2092,35 @@ std::string UiItemCreation::PrintPrereqToken(const char * reqTxt) return result; } +int UiItemCreation::GetScrollInventoryIconId(int spellEnum) +{ + SpellEntry entry(mScribedScrollSpell); + if (!entry.spellEnum) { + return 0; + } + + switch ((SpellSchools)entry.spellSchoolEnum) { + case SpellSchools::School_Abjuration: + return 384; + case SpellSchools::School_Conjuration: + return 379; + case SpellSchools::School_Divination: + return 386; + case SpellSchools::School_Enchantment: + return 383; + case SpellSchools::School_Evocation: + return 154; + case SpellSchools::School_Illusion: + return 381; + case SpellSchools::School_Necromancy: + return 380; + case SpellSchools::School_Transmutation: + return 382; + default: + return 154; + } +} + ItemEnhancementSpec::ItemEnhancementSpec(const std::string &condName, uint32_t Flags, int EffcBonus, int enhBonus) :condName(condName),flags(Flags),effectiveBonus(EffcBonus){ @@ -2196,18 +2292,27 @@ void UiItemCreation::ItemCreationWndRender(int widId){ // draw info pertaining to selected item if (craftingItemIdx >= 0 && (uint32_t) craftingItemIdx < numItemsCrafting[itemCreationType]) { - auto itemHandle = craftedItemHandles[itemCreationType][craftingItemIdx]; - + objHndl itemHandle = craftedItemHandles[itemCreationType][craftingItemIdx]; + const char* itemName = nullptr; + auto invAid = 0; + if (itemCreationType == ItemCreationType::ScribeScroll) { + itemName = spellSys.GetSpellName(mScribedScrollSpell); + invAid = GetScrollInventoryIconId(mScribedScrollSpell); + } + else { + itemName = ItemCreationGetItemName(itemHandle); + invAid = gameSystems->GetObj().GetObject(itemHandle)->GetInt32(obj_f_item_inv_aid); + } + // draw icon - auto invAid = (UiGenericAsset)gameSystems->GetObj().GetObject(itemHandle)->GetInt32(obj_f_item_inv_aid); int textureId; - uiAssets->GetAsset(UiAssetType::Inventory, invAid, textureId); + uiAssets->GetAsset(UiAssetType::Inventory, (UiGenericAsset)invAid, textureId); rect = TigRect(temple::GetRef(0x102FAEC4)); rect.x += 108 * mUseCo8Ui; UiRenderer::DrawTexture(textureId, rect); - auto itemName = ItemCreationGetItemName(itemHandle); + measText = UiRenderer::MeasureTextSize(itemName, temple::GetRef(0x10BED938)); if (measText.width > 161) { measText.width = 161; @@ -2215,7 +2320,7 @@ void UiItemCreation::ItemCreationWndRender(int widId){ rect = TigRect((161 - measText.width )/2 + 208 + 108 * mUseCo8Ui, 132, 161, 24); UiRenderer::DrawTextInWidget(widId, itemName, rect, temple::GetRef(0x10BEDFE8)); - ItemCreationCraftingCostTexts(widId, itemHandle); + ItemCreationCraftingCostTexts(widId, itemHandle, mScribedScrollSpell); } @@ -2243,21 +2348,30 @@ void UiItemCreation::ItemCreationEntryRender(int widId){ UiRenderer::PushFont(PredefinedFont::ARIAL_10); - auto itemHandle = craftedItemHandles[itemCreationType][itemIdx]; - auto itemName = ItemCreationGetItemName(itemHandle); + // special case for scrolls + const char* text = nullptr; + if (itemCreationType == ItemCreationType::ScribeScroll) { + auto spellEnum = scribeScrollSpells[itemIdx]; + text = spellSys.GetSpellName(spellEnum); + } + else { + auto itemHandle = craftedItemHandles[itemCreationType][itemIdx]; + text = ItemCreationGetItemName(itemHandle); + } + TigRect rect(32, 12 * widIdx + 55, 155 + mUseCo8Ui * 108, 12); auto checkRes = itemCreationResourceCheckResults[itemIdx]; if (itemIdx == craftingItemIdx) { if (checkRes) - UiRenderer::DrawTextInWidget(mItemCreationWndId, itemName, rect, temple::GetRef(0x10BEDFE8)); + UiRenderer::DrawTextInWidget(mItemCreationWndId, text, rect, temple::GetRef(0x10BEDFE8)); else - UiRenderer::DrawTextInWidget(mItemCreationWndId, itemName, rect, temple::GetRef(0x10BECE90)); + UiRenderer::DrawTextInWidget(mItemCreationWndId, text, rect, temple::GetRef(0x10BECE90)); } else { if (checkRes) - UiRenderer::DrawTextInWidget(mItemCreationWndId, itemName, rect, temple::GetRef(0x10BED938)); + UiRenderer::DrawTextInWidget(mItemCreationWndId, text, rect, temple::GetRef(0x10BED938)); else - UiRenderer::DrawTextInWidget(mItemCreationWndId, itemName, rect, temple::GetRef(0x10BED6D8)); + UiRenderer::DrawTextInWidget(mItemCreationWndId, text, rect, temple::GetRef(0x10BED6D8)); } UiRenderer::PopFont(); @@ -2492,23 +2606,38 @@ void UiItemCreation::MaaEnhBonusUpRender(int widId){ void UiItemCreation::ButtonStateInit(int wndId){ uiManager->SetHidden(wndId, false); *mItemCreationWnd = *uiManager->GetWindow(wndId); - itemCreationResourceCheckResults = new bool[numItemsCrafting[itemCreationType]]; - for (int i = 0; i < (int)numItemsCrafting[itemCreationType];i++) - { - auto itemHandle = craftedItemHandles[itemCreationType][i]; - if (itemHandle) { - itemCreationResourceCheckResults[i] = CreateItemResourceCheck(itemCreationCrafter, itemHandle); + + auto itemCount = numItemsCrafting[itemCreationType]; + + itemCreationResourceCheckResults = new bool[itemCount]; + if (itemCreationType == ItemCreationType::ScribeScroll) { + for (int i = 0u; i < itemCount; ++i) { + auto spellEnum = scribeScrollSpells[i]; + itemCreationResourceCheckResults[i] = CreateItemResourceCheck(itemCreationCrafter, objHndl::null, spellEnum); } } - - if (craftingItemIdx >= 0 && craftingItemIdx < (int)numItemsCrafting[itemCreationType]){ - if (CreateItemResourceCheck(itemCreationCrafter, craftedItemHandles[itemCreationType][craftingItemIdx])) + else { + for (int i = 0u; i < itemCount; i++) + { + auto itemHandle = craftedItemHandles[itemCreationType][i]; + if (itemHandle) { + itemCreationResourceCheckResults[i] = CreateItemResourceCheck(itemCreationCrafter, itemHandle); + } + } + } + + mScribedScrollSpell = 0; + if (craftingItemIdx >= 0 && craftingItemIdx < itemCount){ + if (itemCreationType == ItemCreationType::ScribeScroll) { + mScribedScrollSpell = scribeScrollSpells[craftingItemIdx]; + } + if (CreateItemResourceCheck(itemCreationCrafter, craftedItemHandles[itemCreationType][craftingItemIdx], mScribedScrollSpell)) uiManager->SetButtonState(mItemCreationCreateBtnId, LgcyButtonState::Normal); else uiManager->SetButtonState(mItemCreationCreateBtnId, LgcyButtonState::Disabled); } - uiManager->ScrollbarSetYmax(mItemCreationScrollbarId, numItemsCrafting[itemCreationType] - NUM_DISPLAYED_CRAFTABLE_ITEMS_MAX < 0 ? 0 : numItemsCrafting[itemCreationType] - NUM_DISPLAYED_CRAFTABLE_ITEMS_MAX); + uiManager->ScrollbarSetYmax(mItemCreationScrollbarId, itemCount - NUM_DISPLAYED_CRAFTABLE_ITEMS_MAX < 0 ? 0 : itemCount - NUM_DISPLAYED_CRAFTABLE_ITEMS_MAX); uiManager->ScrollbarSetY(mItemCreationScrollbarId, 0); mItemCreationScrollbarY = 0; uiManager->BringToFront(wndId); @@ -2661,9 +2790,9 @@ BOOL UiItemCreation::CreateBtnMsg(int widId, TigMsg* msg) itemHandle = craftedItemHandles[itemCreationType][craftingItemIdx]; } - if ( CreateItemResourceCheck(itemCreationCrafter, itemHandle) ) + if ( CreateItemResourceCheck(itemCreationCrafter, itemHandle, mScribedScrollSpell) ) { - CreateItemDebitXPGP(itemCreationCrafter, itemHandle); + CreateItemDebitXPGP(itemCreationCrafter, itemHandle, mScribedScrollSpell); CreateItemFinalize(itemCreationCrafter, itemHandle); } } @@ -2849,28 +2978,32 @@ void UiItemCreation::CreateItemFinalize(objHndl crafter, objHndl item){ //infrastructure::gKeyboard.Update(); // if ALT is pressed, keep the window open for more crafting! - //auto& itemCreationResourceCheckResults = temple::GetRef(0x10BEE330); if (altPressed) { // refresh the resource checks - auto numItemsCrafting = temple::GetRef(0x11E76C7C); // array containing number of protos - static auto craftingHandles = temple::GetRef(0x11E76C3C); // proto handles - - for (int i = 0; i < numItemsCrafting[itemCreationType]; i++) { - auto protoHandle = craftingHandles[itemCreationType][i]; - if (protoHandle) - itemCreationResourceCheckResults[i] = CreateItemResourceCheck(crafter, protoHandle); + if (itemCreationType == ItemCreationType::ScribeScroll) { + for (int i = 0; i < numItemsCrafting[itemCreationType]; i++) { + auto protoHandle = craftedItemHandles[itemCreationType][i]; + auto spellEnum = scribeScrollSpells[i]; + if (protoHandle) { + itemCreationResourceCheckResults[i] = CreateItemResourceCheck(crafter, protoHandle, spellEnum); + } + } } - - auto icItemIdx = temple::GetRef(0x10BEE398); - if (icItemIdx >= 0 && icItemIdx < numItemsCrafting[itemCreationType]) { + else { + for (int i = 0; i < numItemsCrafting[itemCreationType]; i++) { + auto protoHandle = craftedItemHandles[itemCreationType][i]; + if (protoHandle) + itemCreationResourceCheckResults[i] = CreateItemResourceCheck(crafter, protoHandle); + } + } + + if (craftingItemIdx >= 0 && craftingItemIdx < numItemsCrafting[itemCreationType]) { auto createBtnId = mItemCreationCreateBtnId; //temple::GetRef(0x10BED8B0); - if (CreateItemResourceCheck(crafter, item)) - { + if (CreateItemResourceCheck(crafter, item, mScribedScrollSpell)) { uiManager->SetButtonState(createBtnId, LgcyButtonState::Normal); } - else - { + else { uiManager->SetButtonState(createBtnId, LgcyButtonState::Disabled); } } @@ -3825,43 +3958,109 @@ bool UiItemCreation::InitItemCreationRules(){ for (auto i = (int)ItemCreationType::IC_Alchemy; i < ItemCreationType::Inactive; i++){ if (i == ItemCreationType::CraftMagicArmsAndArmor){ mMaaCraftableItemList.clear(); + continue; } - else + if (i == ItemCreationType::ScribeScroll) { // special handling now + continue; + } + + MesLine line(i); + mesFuncs.GetLine_Safe(icrules, &line); + numItemsCrafting[i] = 0; { - MesLine line(i); - mesFuncs.GetLine_Safe(icrules, &line); - numItemsCrafting[i] = 0; - { - StringTokenizer craftableTok(line.value); - while (craftableTok.next()) { - if (craftableTok.token().type == StringTokenType::Number) - ++numItemsCrafting[i]; - } + StringTokenizer craftableTok(line.value); + while (craftableTok.next()) { + if (craftableTok.token().type == StringTokenType::Number) + ++numItemsCrafting[i]; } - craftedItemHandles[i] = new objHndl[numItemsCrafting[i]+1]; - memset(craftedItemHandles[i], 0, sizeof(objHndl) *(numItemsCrafting[i] + 1)); - if (numItemsCrafting[i] > 0){ - int j = 0; - StringTokenizer craftableTok(line.value); - while (craftableTok.next()) { - if (craftableTok.token().type == StringTokenType::Number){ - auto protoHandle = gameSystems->GetObj().GetProtoHandle(craftableTok.token().numberInt); - if (protoHandle) - craftedItemHandles[i][j++] = protoHandle; - } - + } + craftedItemHandles[i] = new objHndl[numItemsCrafting[i]+1]; + memset(craftedItemHandles[i], 0, sizeof(objHndl) *(numItemsCrafting[i] + 1)); + if (numItemsCrafting[i] > 0){ + int j = 0; + StringTokenizer craftableTok(line.value); + while (craftableTok.next()) { + if (craftableTok.token().type == StringTokenType::Number){ + auto protoHandle = gameSystems->GetObj().GetProtoHandle(craftableTok.token().numberInt); + if (protoHandle) + craftedItemHandles[i][j++] = protoHandle; } - numItemsCrafting[i] = j; // in case there are invalid protos - std::sort(craftedItemHandles[i], &craftedItemHandles[i][numItemsCrafting[i]], - [this](objHndl first, objHndl second){ - auto firstName = ItemCreationGetItemName(first); - auto secondName = ItemCreationGetItemName(second); - auto comRes = _stricmp(firstName, secondName); - return comRes < 0; - }); + } - + numItemsCrafting[i] = j; // in case there are invalid protos + std::sort(craftedItemHandles[i], &craftedItemHandles[i][numItemsCrafting[i]], + [this](objHndl first, objHndl second){ + auto firstName = ItemCreationGetItemName(first); + auto secondName = ItemCreationGetItemName(second); + auto comRes = _stricmp(firstName, secondName); + return comRes < 0; + }); + } + + } + // Automated scribe scroll entries: + { + int i = (int)ItemCreationType::ScribeScroll; + numItemsCrafting[i] = 0; + spellSys.DoForSpellEntries( [&](SpellEntry& entry)->void { + auto spellEnum = entry.spellEnum; + if (spellSys.IsNonCore(spellEnum) && !config.nonCoreMaterials) { + return; + } + if (spellSys.IsMonsterSpell(spellEnum) || spellSys.IsSpellLike(spellEnum)) { + return; + } + // Todo: warlock... + if (spellEnum >= 2300 && spellEnum <= 2400) { + return; + } + if (!entry.spellLvlsNum) { // some non-spells in the 700 range are like that + return; + } + if (spellEnum >= 700 && spellEnum <= 802) { // special case some snowflakes grrr + // 733 Scorching Ray, 740 Ray of Clumsiness, 775 Resonance, 794, 795, 796, 797, 798, 799 - vigor spells, scintillating sphere, etc + switch (spellEnum) { + case 733: + case 740: + case 775: + case 794: + case 795: + case 796: + case 797: + case 798: + case 799: + break; + default: + return; + } + } + scribeScrollSpells.push_back(spellEnum); + numItemsCrafting[i]++; + }); + std::sort(scribeScrollSpells.begin(), scribeScrollSpells.end(), + [this](int first, int second) { + auto firstName = spellSys.GetSpellName(first); + auto secondName = spellSys.GetSpellName(second); + auto comRes = _stricmp(firstName, secondName); + return comRes < 0; + }); + craftedItemHandles[i] = new objHndl[numItemsCrafting[i] + 1]; + memset(craftedItemHandles[i], 0, sizeof(objHndl) * (numItemsCrafting[i] + 1)); + + // Use Aid scroll as prototype + auto SCROLL_OF_AID_PROTO = 9002, SPELL_AID_ENUM = 1; + auto scrollProtoHandle = gameSystems->GetObj().GetProtoHandle(SCROLL_OF_AID_PROTO); + if (!scrollProtoHandle) { + throw TempleException("InitItemCreationRules: Cannot Find aid scroll proto!"); + } + auto &spell = objSystem->GetObject(scrollProtoHandle)->GetSpell(obj_f_item_spell_idx, 0); + if (spell.spellEnum != SPELL_AID_ENUM) { + throw TempleException("InitItemCreationRules: Unexpected scroll proto - expected aid spell scroll!"); + } + // just fill it with the same handle + for (auto j = 0u; j < numItemsCrafting[i]; ++j) { + craftedItemHandles[i][j] = scrollProtoHandle; } } diff --git a/TemplePlus/ui/ui_item_creation.h b/TemplePlus/ui/ui_item_creation.h index 6bca5202d..62d39edc5 100644 --- a/TemplePlus/ui/ui_item_creation.h +++ b/TemplePlus/ui/ui_item_creation.h @@ -93,7 +93,7 @@ class UiItemCreation : public UiSystem { void ItemCreationWndRender(int widId); void ItemCreationEntryRender(int widId); - void ItemCreationCraftingCostTexts(int widId, objHndl objHndItem); + void ItemCreationCraftingCostTexts(int widId, objHndl objHndItem, int spellEnum); BOOL ItemCreationEntryMsg(int widId, TigMsg* msg); void ItemCreationCreateBtnRender(int widId) const; void ItemCreationCancelBtnRender(int widId) const; @@ -156,21 +156,22 @@ class UiItemCreation : public UiSystem { int MaaCpCost(int appliedEffectIdx); int MaaXpCost(int effIdx); // calculates XP cost for crafting effects in MAA - bool CreateItemResourceCheck(objHndl crafter, objHndl item); + bool CreateItemResourceCheck(objHndl crafter, objHndl item, int spellEnum = 0); + bool ScribeScrollCheck(objHndl crafter, int spellEnum); const char* GetItemCreationMesLine(int lineId); char const* ItemCreationGetItemName(objHndl itemHandle) const; objHndl MaaGetItemHandle(); - void CreateItemDebitXPGP(objHndl crafter, objHndl item); + void CreateItemDebitXPGP(objHndl crafter, objHndl item, int spellEnum = 0); bool CraftedWandSpellGet(objHndl item, SpellStoreData& spellData, int * spellLevelBase = nullptr); int CraftedWandSpellLevel(objHndl item); int CraftedWandCasterLevel(objHndl item); uint32_t ItemWorthAdjustedForCasterLevel(objHndl objHndItem, uint32_t casterLevelNew); uint32_t CraftedWandWorth(objHndl item, int casterLevelNew); - bool ScribedScrollSpellGet(objHndl item, SpellStoreData& spellData, int * spellLevelBase = nullptr); - int ScribedScrollSpellLevel(objHndl item); - int ScribedScrollCasterLevel(objHndl item); - uint32_t ScribedScrollWorth(objHndl item, int casterLevelNew); + bool ScribedScrollSpellGet(int spellEnum, SpellStoreData& spellData, int * spellLevelBase = nullptr); + int ScribedScrollSpellLevel(int spellEnum); + int ScribedScrollCasterLevel(int spellEnum); + uint32_t ScribedScrollWorth(int spellEnum, int casterLevelNew); int GetMaxSpellLevelFromCasterLevel(int cl); bool IsWeaponBonus(int effIdx); @@ -211,9 +212,12 @@ class UiItemCreation : public UiSystem { bool ItemCreationRulesParseReqText(objHndl crafter, const char* reqTxt); std::string PrintPrereqToken(const char* str); + int GetScrollInventoryIconId(int spellEnum); + int mItemCreationType = 9; objHndl mItemCreationCrafter; int mCraftingItemIdx = -1; + int mScribedScrollSpell = 0; int craftingWidgetId; // denotes which item creation widget is currently active @@ -270,7 +274,7 @@ class UiItemCreation : public UiSystem { int& maaSelectedEffIdx = mMaaSelectedEffIdx; // currently selected craftable effect - bool* itemCreationResourceCheckResults = nullptr; + bool* itemCreationResourceCheckResults = nullptr; // 0x10BEE330 std::vector mMaaCraftableItemList; // handles to enchantable inventory items std::vector appliedBonusIndices; @@ -280,8 +284,10 @@ class UiItemCreation : public UiSystem { int craftedItemExtraGold; // stores the crafted item existing (pre-crafting) extra gold cost int& craftingItemIdx = mCraftingItemIdx; //temple::GetRef(0x10BEE398); - uint32_t numItemsCrafting[8]; // for each Item Creation type - objHndl* craftedItemHandles[8]; // proto handles for new items, and item to modifyt for MAA + uint32_t numItemsCrafting[8]; // for each Item Creation type (0x11E76C7C) + objHndl* craftedItemHandles[8]; // proto handles for new items, and item to modify for MAA (0x11E76C7C) + std::vector scribeScrollSpells; // New: special casing for automated scroll scribing + int& itemCreationType = mItemCreationType; objHndl& itemCreationCrafter = mItemCreationCrafter;//temple::GetRef(0x10BECEE0); std::string craftedItemName; diff --git a/tpdata/templeplus/lib/templeplus/constants.py b/tpdata/templeplus/lib/templeplus/constants.py index 4213a3e71..644e55afb 100644 --- a/tpdata/templeplus/lib/templeplus/constants.py +++ b/tpdata/templeplus/lib/templeplus/constants.py @@ -3358,6 +3358,105 @@ spell_new_slot_lvl_8 = 1613 spell_new_slot_lvl_9 = 1614 +# Python Action Spells +spell_epic_of_the_lost_king = 3080 +spell_ki_blast = 3120 + +#Dragon Disciple Breath Weapons +spell_dragon_diciple_cone_breath = 3231 +spell_dragon_diciple_line_breath = 3232 + +# Dragon Lineage Spell Like Abilities +spell_dragon_lineage_black = 3242 +spell_dragon_lineage_blue = 3243 +spell_dragon_lineage_brass = 3244 +spell_dragon_lineage_bronze = 3245 +spell_dragon_lineage_copper = 3246 +spell_dragon_lineage_gold = 3247 +spell_dragon_lineage_green = 3248 +spell_dragon_lineage_red = 3249 +spell_dragon_lineage_silver = 3250 +spell_dragon_lineage_white = 3251 + +# Marshal Spells +spell_marshal_minor_aura = 3831 +spell_marshal_major_aura = 3832 +spell_marshal_adrenaline_boost = 3833 + +# Dragon Shaman Spells +spell_draconic_aura = 3840 +spell_double_draconic_aura = 3841 + +# Dummy Spells for Python Actions +spell_dummy_spell_cone_small = 3900 +spell_dummy_spell_cone_medium = 3901 +spell_dummy_spell_cone_large = 3902 +spell_dummy_spell_line_small = 3903 +spell_dummy_spell_line_medium = 3904 +spell_dummy_spell_line_large = 3905 + +# Dragon Breath Shapes +dragon_breath_shape_cone = 1 +dragon_breath_shape_line = 2 + +# Heritage Types +heritage_draconic_black = 1 +heritage_draconic_blue = 2 +heritage_draconic_brass = 3 +heritage_draconic_bronze = 4 +heritage_draconic_copper = 5 +heritage_draconic_gold = 6 +heritage_draconic_green = 7 +heritage_draconic_red = 8 +heritage_draconic_silver = 9 +heritage_draconic_white = 10 +heritage_celestial_sorcerer = 11 +heritage_fey = 12 +heritage_fiendish = 13 +heritage_infernal_sorcerer = 14 + +# Aura Types +aura_type_minor = 1 +aura_type_major = 2 +aura_type_draconic = 3 +aura_type_double_draconic = 4 + +#Aura Enums +aura_accurate_strike = 1 +aura_art_of_war = 2 +aura_Demand_fortitude = 3 +aura_Determined_caster = 4 +aura_force_of_will = 5 +aura_master_of_opportunity = 6 +aura_master_of_tactics = 7 +aura_motivate_charisma = 8 +aura_motivate_constitution = 9 +aura_motivate_dexterity = 10 +aura_motivate_intelligence = 11 +aura_motivate_strength = 12 +aura_motivate_wisdom = 13 +aura_over_the_top = 14 +aura_watchful_eye = 15 +aura_hardy_soldiers = 16 +aura_motivate_ardor = 17 +aura_motivate_attack = 18 +aura_motivate_care = 19 +aura_motivate_urgency = 20 +aura_resilient_troops = 21 +aura_steady_hand = 22 +aura_energy_shield = 23 +aura_power = 24 +aura_presence = 25 +aura_resistance = 26 +aura_senses = 27 +aura_toughness = 28 +aura_vigor = 29 +aura_break_spell_resistance = 30 +aura_energy = 31 +aura_insight = 32 +aura_resolve = 33 +aura_stamina = 34 +aura_swiftness =35 stat_strength = 0 stat_dexterity = 1 @@ -3438,6 +3537,10 @@ stat_level_shadow_sun_ninja = 81 stat_level_fochlucan_lyrist = 82 stat_level_marshal = 83 +stat_level_dragon_shaman = 84 +stat_level_dragonheart_mage = 85 +stat_level_battle_howler_of_gruumsh = 86 +stat_level_fist_of_the_forest = 87 stat_level_ultimate_magus = 88 @@ -4089,6 +4192,9 @@ AEC_NO_ACTIONS = 24 AEC_NOT_IN_COMBAT = 25 AEC_AREA_NOT_SAFE = 26 +AEC_ABILITY_ON_COOLDOWN = 27 +AEC_ALREADY_USED_THIS_TURN = 28 +AEC_ALREADY_ACTIVE = 29 BM_INSPIRE_COURAGE = 1 diff --git a/tpdatasrc/tpgamefiles/mes/action_ext.mes b/tpdatasrc/tpgamefiles/mes/action_ext.mes new file mode 100644 index 000000000..6dc08f6fb --- /dev/null +++ b/tpdatasrc/tpgamefiles/mes/action_ext.mes @@ -0,0 +1,5 @@ +// New error codes added by Sagenlicht + +{1027}{Ability on Cooldown} +{1028}{Already used this turn} +{1029}{Already active!} diff --git a/tpdatasrc/tpgamefiles/mes/help/breath_weapon_feats_help.tab b/tpdatasrc/tpgamefiles/mes/help/breath_weapon_feats_help.tab new file mode 100644 index 000000000..dcbad8e5b --- /dev/null +++ b/tpdatasrc/tpgamefiles/mes/help/breath_weapon_feats_help.tab @@ -0,0 +1,2 @@ +TAG_BREATH_FEATS TAG_FEATS_DES Breath Channeling Feats Dragons and creatures of ~Draconic Heritage~[TAG_DRACONIC_HERITAGE] that have breath weapons can choose these feats, which channel the destructive energy of a breath weapon into some other magical or supernatural effect. Using a breath channeling feat requires a creature to activate its breath weapon and counts of a use of that breath weapon. Breath Feats: [CMD_CHILDREN_SORTED] +TAG_EXTRA_EXHALATION TAG_FEATS_DES TAG_BREATH_FEATS Extra Exhalation You can use your breath weapon one more time per day than normal. Prerequisite: ~Draconic Heritage~[TAG_DRACONIC_HERITAGE], Breath Weapon with limited uses per day Benefit: You can use your breath weapon one additional time per day. The interval you must wait between breaths is 1d4 rounds. Special: You can gain this feat multiple times. Each time you take it, you can breathe one additional time per day. diff --git a/tpdatasrc/tpgamefiles/mes/help/dragon_disciple_help.tab b/tpdatasrc/tpgamefiles/mes/help/dragon_disciple_help.tab new file mode 100644 index 000000000..0a420c2e6 --- /dev/null +++ b/tpdatasrc/tpgamefiles/mes/help/dragon_disciple_help.tab @@ -0,0 +1,12 @@ +TAG_DRAGON_DISCIPLE TAG_PRESTIGE_CLASSES Dragon Disciple The blood of dragons runs through the veins of many races. For some, this heritage manifests as a sorcerous bloodline and a predilection for magic. But a few embrace the dreams, recognizing their allure as a promise. These become dragon disciples, who use their magical power as a catalyst to ignite their dragon blood, realizing its fullest potential. Most Dragon Disciples are barbarian, fighters, or rangers who have dabbed as bards or sorcerers. Occasionally, a serious spellcaster explores the path to further a goal of finding out more about his draconic heritage. Hit Die: d12 Requirements: To qualify to become a Dragon Disciple, a character must fulfill all the following criteria. Race: Any non dragon (cannot be already a half-dragon). Skills: Knowledge(Arcana) 8 ranks (not implemented). Languages: Draconic (not implemented). Spellcasting: Ability to cast arcane spells without preparation. Special: The player chooses a dragon variety when taking the first level in this prestige class. Base Attack and Base Save Bonuses: see ~table~[TAG_CLASS_FEATURES_DRAGON_DISCIPLES_TABLES] Class Skills: ~Concentration(Con)~[TAG_CONCENTRATION], Craft(Int), ~Diplomacy(Cha)~[TAG_DIPLOMACY], Escape Artist(Dex), ~Gather Information(Cha)~[TAG_GATHER_INFORMATION], Knowledge(all skills), ~Listen(Wis)~[TAG_LISTEN], Profession(Wis), ~Search(Int)~[TAG_SEARCH], Speak Language(None), ~Spellcraft(Int)~[TAG_SPELLCRAFT], and ~Spot(Wis)~[TAG_SPOT]. Skill Points at Each Level: 2 + Int modifier Class Features: [CMD_CHILDREN] +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_TABLES TAG_DRAGON_DISCIPLE Dragon Disciple Base Attack & Base Save Bonuses Level ~BAB~[TAG_LEVEL_BONUSES] ~Fort.~[TAG_FORTITUDE] ~Reflex~[TAG_REFLEX] ~Will~[TAG_WILL] 1 +0 +2 +0 +2 2 +1 +3 +0 +3 3 +2 +3 +1 +3 4 +3 +4 +1 +4 5 +3 +4 +1 +4 6 +4 +5 +2 +5 7 +5 +5 +2 +5 8 +6 +6 +2 +6 9 +6 +6 +3 +6 10 +7 +7 +3 +7 +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_SPELLS_PER_DAY TAG_DRAGON_DISCIPLE Dragon Disciple Bonus Spells Dragon disciples gain bonus spells as they gain levels in this prestige class, as if from having a high ability score at level 1, 2, 4, 5, 6, 8 and 9. A bonus spell can be added to any level of spells the disciple already has the ability to cast. If a character has more than one spellcasting class, he must decide to which class he adds each bonus spell as it is gained. Once a bonus spell has been applied, it cannot be shifted. +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_GRANTED_PROFICIENCIES TAG_DRAGON_DISCIPLE Dragon Disciple Weapon and Armor Proficiency Dragon Disciples gain no weapon or armor proficiencies. +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_HERITAGE TAG_DRAGON_DISCIPLE Dragon Disciple Heritage The player chooses a dragon heritage when taking the first level in this prestige class. This determines the kind of ~Breath Weapon~[TAG_CLASS_FEATURES_DRAGON_DISCIPLES_BREATH_WEAPON] and ~Element Immunity~[TAG_CLASS_FEATURES_DRAGON_DISCIPLES_DRAGON_APOTHEOSIS] the Disciple will get as he progresses in levels. +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_NATURAL_ARMOR_INCREASE TAG_DRAGON_DISCIPLE Dragon Disciple Natural Armor Increase A dragon disciple gains an increase to the character's existing natural armor (if any). At 1st level +1, at 4th level +2, and at 7th level +3. As his skin thickens, a dragon disciple takes on more and more of his progenitor's physical aspect. +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_ABILITY_BOOST TAG_DRAGON_DISCIPLE Dragon Disciple Ability Boost As a dragon disciple gains levels in this prestige class, his ability scores increase. Strength +2 on Level 2 and 4. Constitution +2 on Level 6. Intelligence +2 on Level 8. These increases stack and are gained as if through level advancement. +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_CLAWS_AND_BITE TAG_DRAGON_DISCIPLE Dragon Disciple Claws and Bite At 2nd level, a dragon disciple gains claw and bite attacks if he does not already have them. Use the values below or the disciple’s base claw and bite damage values, whichever are greater. A dragon disciple is considered proficient with these attacks. When making a full attack, a dragon disciple uses his full base attack bonus with his bite attack but takes a -5 penalty on claw attacks. The Multiattack feat reduces this penalty to only -2. Size Bite Damage Claw Damage Small 1d4 1d3 Medium 1d6 1d4 Large 1d8 1d6 +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_BREATH_WEAPON TAG_DRAGON_DISCIPLE Dragon Disciple Breath Weapon At 3rd level, a dragon disciple gains a minor breath weapon. The type and shape depend on the dragon variety whose heritage he enjoys (see below). Regardless of the ancestor, the breath weapon deals 2d8 points of damage of the appropriate energy type. At 7th level, the damage increases to 4d8, and when a disciple attains dragon apotheosis at 10th level it reaches its full power at 6d8. Regardless of its strength, the breath weapon can be used only once per day. Use all the rules for dragon breath weapons except as specified here. Dragon Heritage Breath Weapon Black Line of acid Blue Line of lightning Green Cone of acid Red Cone of fire White Cone of Cold Brass Line of Fire Bronze Line of Lightning Copper Line of acid Gold Cone of Fire Silver Cone of cold The DC of the breath weapon is 10 + class level + Con modifier. A line-shaped breath weapon is 5 feet high, 5 feet wide, and 60 feet long. A cone-shaped breath weapon is 30 feet long. +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_BLINDSENSE TAG_DRAGON_DISCIPLE Dragon Disciple Blindsense At 5th level, the dragon disciple gains blindsense with a range of 30 feet. Using nonvisual senses the dragon disciple notices things it cannot see. He usually does not need to make Spot or Listen checks to notice and pinpoint the location of creatures within range of his blindsense ability, provided that he has line of effect to that creature. Any opponent the dragon disciple cannot see still has total concealment against him, and the dragon disciple still has the normal miss chance when attacking foes that have concealment. Visibility still affects the movement of a creature with blindsense. A creature with blindsense is still denied its Dexterity bonus to Armor Class against attacks from creatures it cannot see. At 10th level, the range of this ability increases to 60 feet. Not implemented in ToEE, has no real gameplay effect +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_WINGS TAG_DRAGON_DISCIPLE Dragon Disciple Wings At 9th level, a dragon disciple grows a set of draconic wings. He may now fly at a speed equal to his normal land speed, with average maneuverability. Not implemented in ToEE as there is flight in ToEE! +TAG_CLASS_FEATURES_DRAGON_DISCIPLES_DRAGON_APOTHEOSIS TAG_DRAGON_DISCIPLE Dragon Disciple Dragon Apotheosis At 10th level, a dragon disciple takes on the half-dragon template. His breath weapon reaches full strength (6d8), and he gains +4 to Strength and +2 to Charisma. His natural armor bonus increases to +4, and he acquires low-light vision, 60-foot darkvision, immunity to sleep and paralysis effects, and immunity to the energy type used by his breath weapon. Note: Not implemented: low-light vision, darkvision, half-dragon template. diff --git a/tpdatasrc/tpgamefiles/mes/help/spell_compendium_help.tab b/tpdatasrc/tpgamefiles/mes/help/spell_compendium_help.tab index e7d7f078c..9fd44ae70 100644 --- a/tpdatasrc/tpgamefiles/mes/help/spell_compendium_help.tab +++ b/tpdatasrc/tpgamefiles/mes/help/spell_compendium_help.tab @@ -1,5 +1,5 @@ TAG_WEAPON_SHIELD_PROFICIENCY TAG_COMBAT Weapon, Armor, and Shield Proficiency A character who uses a weapon with which he or she is not proficient takes a -4 penalty on ~attack rolls~[TAG_ATTACK_ROLL]. A character who wears armor and/or uses a shield with which he or she is not proficient takes the armor's (and/or shield's) armor check penalty on attack rolls and on all Strength-based and Dexterity-based ability and skill checks. The penalty for nonproficiency with armor stacks with the penalty for nonproficiency with shields. Weapon, armor, or shield proficiency may be granted by the character's race, class or by the following feats: ~Armor Proficiency (Light)~[TAG_ARMOR_LIGHT] ~Armor Proficiency (Medium)~[TAG_ARMOR_MEDIUM] ~Armor Proficiency (Heavy)~[TAG_ARMOR_HEAVY] ~Exotic Weapon Proficiency~[TAG_EXOTIC_PROF] ~Martial Weapon Proficiency~[TAG_MARTIAL_PROF] ~Simple Weapon Proficiency~[TAG_SIMPLE_PROF] ~Shield Proficiency~[TAG_SHIELD_PROF] Note: I am unsure about the state of simple weapon prof and shield prof and need to verify the help text as they differ from actual D&D. -TAG_SPELLS_ACID_FOG TAG_SPELLS TAG_SORCERER_ TAG_WIZARD_6 TAG_WATER_D Acid Fog ~Conjuration~[TAG_MAGIC_SCHOOLS_CONJURATION](Creation)[Acid] Level: ~Sorcerer~[TAG_SORCERERS]/~Wizard~[TAG_WIZARDS] 6, ~Domain: Water~[TAG_WATER_D] 7 Components: V, S Casting Time: 1 ~standard action~[TAG_STANDARD_ACTION] Range: Medium (100 ft. + 10 ft./level) Area: 20-ft. radius Duration: 1 round/level Saving Throw: None Spell Resistance: No Acid fog creates a billowing mass of misty vapors similar to that produced by a ~solid fog~[TAG_SPELLS_SOLID_FOG] spell. In addition to slowing creatures down and obscuring sight, this spell's vapors are highly acidic. Each round on your turn, starting when you cast the spell, the fog deals 2d6 points of acid damage to each creature and object within it. +TAG_SPELLS_ACID_FOG TAG_SPELLS TAG_SORCERER_6 TAG_WIZARD_6 TAG_WATER_D Acid Fog ~Conjuration~[TAG_MAGIC_SCHOOLS_CONJURATION](Creation)[Acid] Level: ~Sorcerer~[TAG_SORCERERS]/~Wizard~[TAG_WIZARDS] 6, ~Domain: Water~[TAG_WATER_D] 7 Components: V, S Casting Time: 1 ~standard action~[TAG_STANDARD_ACTION] Range: Medium (100 ft. + 10 ft./level) Area: 20-ft. radius Duration: 1 round/level Saving Throw: None Spell Resistance: No Acid fog creates a billowing mass of misty vapors similar to that produced by a ~solid fog~[TAG_SPELLS_SOLID_FOG] spell. In addition to slowing creatures down and obscuring sight, this spell's vapors are highly acidic. Each round on your turn, starting when you cast the spell, the fog deals 2d6 points of acid damage to each creature and object within it. TAG_SPELLS_AID_MASS TAG_SPELLS TAG_CLERIC_3 Aid, Mass ~Enchantment~[TAG_MAGIC_SCHOOLS_ENCHANTMENT](Compulsion)[Mind-Affecting] Level: ~Cleric~[TAG_CLERICS] 3 Components: V, S Casting Time: 1 ~standard action~[TAG_STANDARD_ACTION] Range: Close (25 ft. + 5 ft./2 levels) Target: One creature/level no two more than 30 ft. apart Duration: 1 min./level Saving Throw: None Spell Resistance: No You hold your holy symbol aloft and cast the spell. A silvery radiance dances from your hands, leaping over all the nearby party members and strengthening them. Aid, Mass grants the targets a +1 ~morale~[TAG_MODIFIER_MORALE] bonus on ~attack rolls~[TAG_ATTACK_ROLL] and saves against ~fear~[TAG_FEAR] effects, plus ~temporary hit points~[TAG_TEMPORARY_HIT_POINTS] equal to 1d8 + ~caster level~[TAG_CASTER_LEVEL] (to a maximum of 1d8+15 temporary hit points). TAG_SPELLS_ALIGN_WEAPON_MASS TAG_SPELLS TAG_CLERIC_3 Align Weapon, Mass ~Transmutation~[TAG_MAGIC_SCHOOLS_TRANSMUTATION](see text) Level: ~Cleric~[TAG_CLERICS] 3 Components: V, S Casting Time: 1 ~standard action~[TAG_STANDARD_ACTION] Range: Close (25 ft. + 5 ft./2 levels) Target: One creature/level no two more than 30 ft. apart Duration: 1 min./level Saving Throw: None Spell Resistance: No You hold your holy symbol high and speak old words of power. Your party's weapons take on a pale blue radiance. This spell functions like ~align weapon~[TAG_SPELLS_ALIGN_WEAPON], except that it affects multiple weapons or projectiles at a distance. Note: The projectiles part is not implemented. Warning: as a cleric you can't cast a spell that does have an opposed Alignment Descriptor. I can't change the descriptor on the fly, which means you can actually try to cast the spell but it will fizzle and you loose the spell if you try to cast it with the wrong alignment! TAG_SPELLS_ALLEGRO TAG_SPELLS TAG_BARD_3 Allegro ~Transmutation~[TAG_MAGIC_SCHOOLS_TRANSMUTATION] ~Bard~[TAG_BARDS] 3 Components: V, S Casting Time: 1 ~free action~[TAG_FREE_ACTION] Range: 20 ft. Area: 20 ft. radius burst centered on you Duration: 1 min/level With a quick wiggle of your fingers and a few arcane words, you release the feather in your hand to complete the spell. Suddenly, translucent blue motes burst outward from you and collect on yourself and your nearby allies before fading away. Each creature within the spell's area gains a 30-foot ~enhancement~[TAG_ENHANCEMENT_BONUS] bonus to its land ~speed~[TAG_MOVEMENT_RATE], up to a maximum of double the creature's land speed. Affected creatures retain these effects for the duration of the spell, even if they leave the original area. diff --git a/tpdatasrc/tpgamefiles/mes/help/stormlord_help.tab b/tpdatasrc/tpgamefiles/mes/help/stormlord_help.tab index b2df729c8..3e1dcb577 100644 --- a/tpdatasrc/tpgamefiles/mes/help/stormlord_help.tab +++ b/tpdatasrc/tpgamefiles/mes/help/stormlord_help.tab @@ -6,5 +6,5 @@ TAG_CLASS_FEATURES_STORMLORD_ENHANCED_JAVELINS TAG_STORMLORDS Stormlord Enhanc TAG_CLASS_FEATURES_STORMLORD_RESISTANCE_TO_ELECTRICITY TAG_STORMLORDS Stormlord Resistance to Electricity As a stormlord gains levels in this prestige class, he becomes increasingly ~resistant~[TAG_SPECIAL_ABILITIES_RESISTANCE_TO_ENERGY] to electrical energy, starting at 1st level with resistance to electricity 5. This bonus increases at 4th and 7th level by an additional 5. TAG_CLASS_FEATURES_STORMLORD_SHOCK_WEAPONS TAG_STORMLORDS Stormlord Shock Weapons Any spear or javelin used by a stormlord of 2nd level or higher is treated as a shock weapon (dealing an extra 1d6 points of electricity damage). The weapon loses this ability when leaving the hand of the stormlord. For a stormlord of 8th level or higher, any spear or javelin he uses is instead treated as a shocking burst weapon. TAG_CLASS_FEATURES_STORMLORD_THUNDERING_WEAPONS TAG_STORMLORDS Stormlords Thundering Weapons For a stormlord of 5th level or higher, any spear or javelin he uses is treated as a thundering weapon. The weapon loses this ability when leaving the hand of the stormlord. This effect stacks with that of the stormlord's shock weapon ability. -TAG_CLASS_FEATURES_STORMLORD_IMMUNITY_TO_ELECTRICITY_TAG_STORMLORDS Stormlords Immunity to Electricity At 9th level, a stormlord gains immunity to electricity. +TAG_CLASS_FEATURES_STORMLORD_IMMUNITY_TO_ELECTRICITY TAG_STORMLORDS Stormlords Immunity to Electricity At 9th level, a stormlord gains immunity to electricity. TAG_CLASS_FEATURES_STORMLORD_STORM_OF_ELEMENTAL_FURY TAG_STORMLORDS Stormlords Storm of Elemental Fury At 10th level, a stormlord can summon a storm of great magnitude and power. Once per day, a stormlord can use storm of elemental fury as if he were a 17th-level cleric. Deactivated until Spell Compendium is merged to temple+ diff --git a/tpdatasrc/tpgamefiles/mes/spell_ext/dragon_disciple_breath_weapons.mes b/tpdatasrc/tpgamefiles/mes/spell_ext/dragon_disciple_breath_weapons.mes new file mode 100644 index 000000000..da1c584ea --- /dev/null +++ b/tpdatasrc/tpgamefiles/mes/spell_ext/dragon_disciple_breath_weapons.mes @@ -0,0 +1,2 @@ +{3231} {Dragon Disciple Cone Breath} +{3232} {Dragon Disciple Line Breath} diff --git a/tpdatasrc/tpgamefiles/mes/stat_ext.mes b/tpdatasrc/tpgamefiles/mes/stat_ext.mes index 0b7fe936a..9c10af354 100644 --- a/tpdatasrc/tpgamefiles/mes/stat_ext.mes +++ b/tpdatasrc/tpgamefiles/mes/stat_ext.mes @@ -235,9 +235,7 @@ Feats: Cleave, Power Attack {13016}Dragon Disciple{-- COMING SOON --} A descendant of dragonkind who uses their innate magical power as a catalyst to ignite their dragon blood. -Requirements: -Level: 5 -Spellcasting: Ability to cast arcane spells without preparation. +Requirements: Level 5; Ability to cast arcane spells without preparation. {13017}Duelist{A nimble, intelligent fighter trained in making precise attacks with light weapons, such as the rapier. Also known as a swashbuckler. diff --git a/tpdatasrc/tpgamefiles/rules/char_class/class023_dragon_disciple.py b/tpdatasrc/tpgamefiles/rules/char_class/class023_dragon_disciple.py index 1ec2d9b82..b437e4e12 100644 --- a/tpdatasrc/tpgamefiles/rules/char_class/class023_dragon_disciple.py +++ b/tpdatasrc/tpgamefiles/rules/char_class/class023_dragon_disciple.py @@ -1,22 +1,24 @@ from toee import * import char_class_utils +import char_editor +from heritage_feat_utils import getDraconicHeritageColourString ################################################### def GetConditionName(): - return "Dragon Disciple" + return "Dragon Disciple" def GetSpellCasterConditionName(): - return "Dragon Disciple Spellcasting" + return "Dragon Disciple Spellcasting" def GetCategory(): - return "Core 3.5 Ed Prestige Classes" + return "Core 3.5 Ed Prestige Classes" def GetClassDefinitionFlags(): - return CDF_CoreClass + return CDF_CoreClass def GetClassHelpTopic(): - return "TAG_DRAGON_DISCIPLES" + return "TAG_DRAGON_DISCIPLES" classEnum = stat_level_dragon_disciple @@ -24,75 +26,115 @@ def GetClassHelpTopic(): class_feats = { +1: ("Dragon Disciple Natural Armor", "Dragon Disciple Bonus Spell per Day (1)",), +2: ("Dragon Disciple Claws and Bite", "Dragon Disciple Bonus Spell per Day (2)",), +3: ("Dragon Disciple Breath Weapon",), +4: ("Dragon Disciple Bonus Spell per Day (3)",), +5: ("Dragon Disciple Bonus Spell per Day (4)",), +6: ("Dragon Disciple Bonus Spell per Day (5)",), +8: ("Dragon Disciple Bonus Spell per Day (6)",), +9: ("Dragon Disciple Wings", "Dragon Disciple Bonus Spell per Day (7)",), +10: ("Dragon Disciple Dragon Apotheosis",) } class_skills = (skill_alchemy, skill_concentration, skill_craft, skill_diplomacy, skill_escape_artist, skill_gather_information, skill_knowledge_all, skill_listen, skill_profession, skill_search, skill_spellcraft, skill_spot) def IsEnabled(): - return 1 + return 1 def GetHitDieType(): - return 12 + return 12 def GetSkillPtsPerLevel(): - return 2 - + return 2 + def GetBabProgression(): - return base_attack_bonus_type_martial + return base_attack_bonus_type_martial def IsFortSaveFavored(): - return 1 + return 1 def IsRefSaveFavored(): - return 0 + return 0 def IsWillSaveFavored(): - return 1 + return 1 def GetSpellListType(): - return spell_list_type_none # dragon disciples only advance bonus spells + return spell_list_type_none # dragon disciples only advance bonus spells def IsClassSkill(skillEnum): - return char_class_utils.IsClassSkill(class_skills, skillEnum) + return char_class_utils.IsClassSkill(class_skills, skillEnum) def IsClassFeat(featEnum): - return char_class_utils.IsClassFeat(class_feats, featEnum) + return char_class_utils.IsClassFeat(class_feats, featEnum) def GetClassFeats(): - return class_feats + return class_feats def IsAlignmentCompatible( alignment): - return 1 + return 1 def CanCastInnateArcane(obj): #todo: generalize - if obj.stat_level_get(stat_level_bard) > 0: - return 1 - if obj.stat_level_get(stat_level_sorcerer) > 0: - return 1 - return 0 - -def SpeaksDraconic( obj ): - return 1 # languages not implemented in ToEE + if obj.stat_level_get(stat_level_bard) > 0: + return 1 + if obj.stat_level_get(stat_level_sorcerer) > 0: + return 1 + return 0 + +def SpeaksDraconic(obj): + return 1 # languages not implemented in ToEE def KnowledgeArcanaCheck( obj ): - # if (obj.skill_ranks_get(skill_knowledge_arcana) < 8): #knowledge skill not implemented in ToEE - # return 0 - if obj.stat_level_get(stat_level) < 5: # a replacement for checking knowledge arcana - return 0 - return 1 - -def ObjMeetsPrereqs( obj ): - return 0 # WIP - - if not KnowledgeArcanaCheck(obj): - return 0 - if (not obj.has_feat(feat_cleave) ): - return 0 - if (not obj.has_feat(feat_power_attack) ): - return 0 - if (not CanCastInnateArcane(obj)): - return 0 - # if (not obj.has_feat(feat_improved_sunder) ): # sunder not yet implemented - # return 0 - #if (not obj.d20_query('EVIL_OUTSIDER_CONTACTED')): - # return 0 - return 1 \ No newline at end of file + # if (obj.skill_ranks_get(skill_knowledge_arcana) < 8): #knowledge skill not implemented in ToEE + # return 0 + if obj.stat_level_get(stat_level) < 5: # a replacement for checking knowledge arcana + return 0 + return 1 + +def DragonRaceCheck(obj): + #Can't check if character is half-dragon; it does not exist in ToEE + if obj.is_category_type(mc_type_dragon): + return 0 + return 1 + +def ObjMeetsPrereqs(obj): + if not KnowledgeArcanaCheck(obj): + return 0 + elif not CanCastInnateArcane(obj): + return 0 + elif not SpeaksDraconic(obj): + return 0 + #elif DragonRaceCheck: + # return 0 + return 1 + + +# Levelup +def alreadyHasDraconicHeritage(obj): + hasDraconicHeritageFeat = False + for heritage in range(heritage_draconic_black, heritage_draconic_white + 1): + colourString = getDraconicHeritageColourString(heritage) + if char_editor.has_feat("Draconic Heritage {}".format(colourString)): + hasDraconicHeritageFeat = True + break + return True if hasDraconicHeritageFeat else False + +def IsSelectingFeatsOnLevelup(obj): + newLvl = char_editor.stat_level_get(classEnum) + if newLvl == 1 and not alreadyHasDraconicHeritage(obj): + return 1 + return 0 + +def LevelupGetBonusFeats(obj): + newLvl = char_editor.stat_level_get(classEnum) + bonus_feats = [] + if newLvl == 1: + bonus_feats.append("Draconic Heritage") + bonFeatInfo = [] + for ft in bonus_feats: + featInfo = char_editor.FeatInfo(ft) + featInfo.feat_status_flags |= 4 # always pickable + bonFeatInfo.append(featInfo) + char_editor.set_bonus_feats(bonFeatInfo) + return + diff --git a/tpdatasrc/tpgamefiles/rules/d20_actions/action02302_dragon_disciple_breath_attack.py b/tpdatasrc/tpgamefiles/rules/d20_actions/action02302_dragon_disciple_breath_attack.py new file mode 100644 index 000000000..5c099a95a --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/d20_actions/action02302_dragon_disciple_breath_attack.py @@ -0,0 +1,28 @@ +from toee import * +import tpactions +import tpdp + + +def GetActionName(): + return "Dragon Disciple Breath Attack" + + +def GetActionDefinitionFlags(): + return D20ADF_MagicEffectTargeting | D20ADF_QueryForAoO | D20ADF_TriggersCombat + + +def GetTargetingClassification(): + return D20TC_CastSpell + + +def GetActionCostType(): + return D20ACT_Standard_Action + + +def AddToSequence(d20action, action_seq, tb_status): + if d20action.performer.d20_query(Q_Prone): + d20aGetup = d20action + d20aGetup.action_type = tpdp.D20ActionType.StandUp + action_seq.add_action(d20aGetup) + action_seq.add_action(d20action) + return AEC_OK diff --git a/tpdatasrc/tpgamefiles/rules/d20_actions/action02303_dragon_disciple_toogle_fly.py b/tpdatasrc/tpgamefiles/rules/d20_actions/action02303_dragon_disciple_toogle_fly.py new file mode 100644 index 000000000..d4821d8fb --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/d20_actions/action02303_dragon_disciple_toogle_fly.py @@ -0,0 +1,18 @@ +from toee import * +import tpactions + +def GetActionName(): + return "Dragon Disciple Toogle Fly" + +def GetActionDefinitionFlags(): + return D20ADF_None + +def GetTargetingClassification(): + return D20TC_Target0 + +def GetActionCostType(): + return D20ACT_NULL + +def AddToSequence(d20action, action_seq, tb_status): + action_seq.add_action(d20action) + return AEC_OK \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/d20_ai/targeting.py b/tpdatasrc/tpgamefiles/rules/d20_ai/targeting.py index 423b8a65b..0383a6a4b 100644 --- a/tpdatasrc/tpgamefiles/rules/d20_ai/targeting.py +++ b/tpdatasrc/tpgamefiles/rules/d20_ai/targeting.py @@ -254,7 +254,7 @@ def will_kos(attacker, target, aiSearchingTgt): return 0 - if target.type == obj_t_npc: + if target.type != obj_t_npc: return 0 taifs = AiFightStatus(target) diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage black.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage black.txt new file mode 100644 index 000000000..13780ae13 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage black.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Black +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage blue.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage blue.txt new file mode 100644 index 000000000..f9896d6fa --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage blue.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Blue +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage brass.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage brass.txt new file mode 100644 index 000000000..32d1dd615 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage brass.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Brass +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage bronze.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage bronze.txt new file mode 100644 index 000000000..9ecc609a4 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage bronze.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Bronze +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage copper.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage copper.txt new file mode 100644 index 000000000..9851e3778 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage copper.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Copper +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage gold.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage gold.txt new file mode 100644 index 000000000..f11cef093 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage gold.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Gold +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage green.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage green.txt new file mode 100644 index 000000000..25d146ad0 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage green.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Green +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage red.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage red.txt new file mode 100644 index 000000000..31a0e5b1e --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage red.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Red +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage silver.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage silver.txt new file mode 100644 index 000000000..b9a6e8789 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage silver.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage Silver +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage white.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage white.txt new file mode 100644 index 000000000..7cb3e021d --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage white.txt @@ -0,0 +1,6 @@ +name: Draconic Heritage White +flags: 4194304 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1 +parent: Draconic Heritage \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/draconic heritage.txt b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage.txt new file mode 100644 index 000000000..3be52eac6 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/draconic heritage.txt @@ -0,0 +1,5 @@ +name: Draconic Heritage +flags: 13107200 +prereqs: +description: You have a greater connection with your draconic bloodline than others of your kind. +prereq descr: Sorcercer Level 1; automatically granted to Dragon Disciple and Dragon Shaman diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (1).txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (1).txt new file mode 100644 index 000000000..d9728e8ec --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (1).txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Bonus Spell per Day (1) +flags: 8 +prereqs: +description: Dragon Disciples gain bonus spells as they gain levels in this prestige class, as if from having a high ability score. The bonus spell is granted to the highest Arcane Casting Class and is applied to the highest spell level. +prereq descr: Dragon Disciple level 1. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (2).txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (2).txt new file mode 100644 index 000000000..458cda6d8 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (2).txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Bonus Spell per Day (2) +flags: 8 +prereqs: +description: Dragon Disciples gain bonus spells as they gain levels in this prestige class, as if from having a high ability score. The bonus spell is granted to the highest Arcane Casting Class and is applied to the highest spell level. +prereq descr: Dragon Disciple level 2. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (3).txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (3).txt new file mode 100644 index 000000000..61d10dd18 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (3).txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Bonus Spell per Day (3) +flags: 8 +prereqs: +description: Dragon Disciples gain bonus spells as they gain levels in this prestige class, as if from having a high ability score. The bonus spell is granted to the highest Arcane Casting Class and is applied to the highest spell level. +prereq descr: Dragon Disciple level 4. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (4).txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (4).txt new file mode 100644 index 000000000..3b1b02491 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (4).txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Bonus Spell per Day (4) +flags: 8 +prereqs: +description: Dragon Disciples gain bonus spells as they gain levels in this prestige class, as if from having a high ability score. The bonus spell is granted to the highest Arcane Casting Class and is applied to the highest spell level. +prereq descr: Dragon Disciple level 5. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (5).txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (5).txt new file mode 100644 index 000000000..e605fb0c5 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (5).txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Bonus Spell per Day (5) +flags: 8 +prereqs: +description: Dragon Disciples gain bonus spells as they gain levels in this prestige class, as if from having a high ability score. The bonus spell is granted to the highest Arcane Casting Class and is applied to the highest spell level. +prereq descr: Dragon Disciple level 6. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (6).txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (6).txt new file mode 100644 index 000000000..0bb3d6926 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (6).txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Bonus Spell per Day (6) +flags: 8 +prereqs: +description: Dragon Disciples gain bonus spells as they gain levels in this prestige class, as if from having a high ability score. The bonus spell is granted to the highest Arcane Casting Class and is applied to the highest spell level. +prereq descr: Dragon Disciple level 8. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (7).txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (7).txt new file mode 100644 index 000000000..5e08d3ece --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple bonus spell per day (7).txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Bonus Spell per Day (7) +flags: 8 +prereqs: +description: Dragon Disciples gain bonus spells as they gain levels in this prestige class, as if from having a high ability score. The bonus spell is granted to the highest Arcane Casting Class and is applied to the highest spell level. +prereq descr: Dragon Disciple level 9. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple breath weapon.txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple breath weapon.txt new file mode 100644 index 000000000..0dc16a094 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple breath weapon.txt @@ -0,0 +1,22 @@ +name: Dragon Disciple Breath Weapon +flags: 8 +prereqs: +description: At 3rd level, a dragon disciple gains a minor breath weapon. The type and shape depend on the dragon variety whose heritage he enjoys (see below). Regardless of the ancestor, the breath weapon deals 2d8 points of damage of the appropriate energy type. + +At 7th level, the damage increases to 4d8, and when a disciple attains dragon apotheosis at 10th level it reaches its full power at 6d8. Regardless of its strength, the breath weapon can be used only once per day. Use all the rules for dragon breath weapons except as specified here. + +The DC of the breath weapon is 10 + class level + Con modifier. + +Black Line of acid +Blue Line of lightning +Green Cone of corrosive gas (acid) +Red Cone of fire +White Cone of cold +Brass Line of fire +Bronze Line of lightning +Copper Line of acid +Gold Cone of fire +Silver Cone of cold + +A line-shaped breath weapon is 5 feet high, 5 feet wide, and 60 feet long. A cone-shaped breath weapon is 30 feet long. +prereq descr: Dragon Disciple level 3. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple claws and bite.txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple claws and bite.txt new file mode 100644 index 000000000..22251ea1e --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple claws and bite.txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Claws and Bite +flags: 8 +prereqs: +description: At 2nd level, a dragon disciple gains claw and bite attacks if he does not already have them. A dragon disciple is considered proficient with these attacks. When making a full attack, a dragon disciple uses his full base attack bonus with his bite attack but takes a -5 penalty on claw attacks. The Multiattack feat reduces this penalty to only -2. +prereq descr: Dragon Disciple level 2. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple dragon apotheosis.txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple dragon apotheosis.txt new file mode 100644 index 000000000..44dc4aa3c --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple dragon apotheosis.txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Dragon Apotheosis +flags: 8 +prereqs: +description: At 10th level, a dragon disciple takes on the half-dragon template. His breath weapon reaches full strength (as noted above), and he gains +4 to Strength and +2 to Charisma. His natural armor bonus increases to +4, and he acquires low-light vision, 60-foot darkvision, immunity to sleep and paralysis effects, and immunity to the energy type used by his breath weapon. +prereq descr: Dragon Disciple level 5. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple heritage.txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple heritage.txt new file mode 100644 index 000000000..9a5fef308 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple heritage.txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Heritage +flags: 8 +prereqs: +description: The player chooses a dragon variety when taking the first level in this prestige class. +prereq descr: Dragon Disciple level 1 \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple natural armor.txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple natural armor.txt index cd680fecb..c5e7d45a3 100644 --- a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple natural armor.txt +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple natural armor.txt @@ -1,5 +1,5 @@ name: Dragon Disciple Natural Armor flags: 8 prereqs: -description: At 1st, 4th, and 7th level, a dragon disciple gains a progressive +1 increase to the character's existing natural armor (if any). As his skin thickens, a dragon disciple takes on more and more of his progenitor’s physical aspect. +description: At 1st, 4th, and 7th level, a dragon disciple gains a progressive +1 increase to the character's existing natural armor (if any). As his skin thickens, a dragon disciple takes on more and more of his progenitor's physical aspect. prereq descr: Dragon Disciple level 1. \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/dragon disciple wings.txt b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple wings.txt new file mode 100644 index 000000000..5009f2cd5 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/dragon disciple wings.txt @@ -0,0 +1,5 @@ +name: Dragon Disciple Wings +flags: 8 +prereqs: +description: TBD! +prereq descr: Dragon Disciple level 9 \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/feats/extra exhalation.txt b/tpdatasrc/tpgamefiles/rules/feats/extra exhalation.txt new file mode 100644 index 000000000..6b6113083 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/extra exhalation.txt @@ -0,0 +1,5 @@ +name: Extra Exhalation +flags: 12582913 +prereqs: +description: You can use your Breath Weapon one more time per day than normal. Special: You can take this feat multiple times. Each time you take it, you can breath one additional time per day. +prereq descr: Limited Breath Weapon use per day diff --git a/tpdatasrc/tpgamefiles/rules/partsys/breath_weapons_partsys.tab b/tpdatasrc/tpgamefiles/rules/partsys/breath_weapons_partsys.tab new file mode 100644 index 000000000..c9c2b4a62 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/partsys/breath_weapons_partsys.tab @@ -0,0 +1,24 @@ +sp-Breath Weapon Cone Medium Acid smoke emit 10 20 600 Object YPR Sprite Same as Emitter Polar Cartesian fire-sprite 120 Blend 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0?30 0 -30?30 30?360 5?40 0 0 0?360 0 0 0,16,0 0 255 144 100 +sp-Breath Weapon Cone Medium Acid Fire Strike 30 75 Object YPR Disc Same as Emitter Polar flare 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 64,0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 20?60 0 0 0?720 0 0 0,255,0 0 255 144 20 +sp-Breath Weapon Cone Medium Acid Fire Strike 2 30 300 Object YPR Disc Same as Emitter Polar flare 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 0,10 0 0 0?720 0 0 0,255,0 0 255 144 30 +sp-Breath Weapon Cone Medium Cold smoke emit 10 20 600 Object YPR Sprite Same as Emitter Polar Cartesian fire-sprite 120 Blend 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0?30 0 -30?30 30?330 5?40 0 0 0?360 0 0 0,16,0 255 255 255 100 +sp-Breath Weapon Cone Medium Cold Fire Strike 30 100 Object YPR Sprite Same as Emitter Polar flare-big 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 64,0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 20?60 0 0 0?720 0 0 0 0 128 255 20 +sp-Breath Weapon Cone Medium Cold fire strike 3 sparklies 30 50 Object YPR Sprite Same as Emitter Polar flare-1 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 255,0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3, 0 0 0,360 0 0 0 255 255 255 100 +sp-Breath Weapon Cone Medium Electricity Fire Strike 30 100 Object YPR Sprite Same as Emitter Polar flare-big 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 64,0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 20?60 0 0 0?720 0 0 0 0 128 255 20 +sp-Breath Weapon Cone Medium Electricity fire strike 3 sparklies 30 50 Object YPR Sprite Same as Emitter Polar flare-1 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 255,0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3, 0 0 0,360 0 0 255,0 255,64,64,64,64 255,128,128,128,128 255,255 100 +sp-Breath Weapon Cone Medium Electricity water droplets 15 30 30 Object YPR bip01 head Sprite Same as Emitter Polar Cartesian flare-1 5 Add 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 3?15 0 0 0?360 0 0 255,0 255,64,64,64,64 255,128,128,128,128 255,255 5 +sp-Breath Weapon Cone Medium Fire smoke emit 10 20 600 Object YPR Sprite Same as Emitter Polar Cartesian fire-sprite 120 Blend 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0?30 0 -30?30 30?330 5?40 0 0 0?360 0 0 0,32,0 0 0 0 100 +sp-Breath Weapon Cone Medium Fire Fire Strike 30 75 Object YPR Disc Same as Emitter Polar flare 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 64,0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 20?60 0 0 0?720 0 0 0,255,255,0 255(6),255(10) 255(6),130(10) 0 20 +sp-Breath Weapon Cone Medium Fire Fire Strike 2 30 300 Object YPR Disc Same as Emitter Polar flare 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -30?30 0 0,360 0,10 0 0 0?720 0 0 0,255,255,0 255(6),255(10) 255(6),130(10) 0 30 +sp-Breath Weapon Line Medium Acid smoke emit 10 20 600 Object YPR Sprite Same as Emitter Polar Cartesian fire-sprite 120 Blend 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0?30 0 -3?3 30?600 5?40 0 0 0?360 0 0 0,16,0 0 255 144 100 +sp-Breath Weapon Line Medium Acid Fire Strike 30 75 Object YPR Disc Same as Emitter Polar flare 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 64,0 0 0 0 0 0 0 0 0 0 -3?3 0 0,600 20?60 0 0 0?720 0 0 0,255,0 0 255 144 20 +sp-Breath Weapon Line Medium Acid Fire Strike 2 30 300 Object YPR Disc Same as Emitter Polar flare 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -3?3 0 0,600 0,10 0 0 0?720 0 0 0,255,0 0 255 144 30 +sp-Breath Weapon Line Medium Cold smoke emit 10 20 600 Object YPR Sprite Same as Emitter Polar Cartesian fire-sprite 120 Blend 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0?30 0 -3?3 30?330 5?40 0 0 0?360 0 0 0,16,0 255 255 255 100 +sp-Breath Weapon Line Medium Cold Fire Strike 30 100 Object YPR Sprite Same as Emitter Polar flare-big 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 64,0 0 0 0 0 0 0 0 0 0 -3?3 0 0,360 20?60 0 0 0?720 0 0 0 0 128 255 20 +sp-Breath Weapon Line Medium Cold fire strike 3 sparklies 30 50 Object YPR Sprite Same as Emitter Polar flare-1 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 255,0 0 0 0 0 0 0 0 0 0 -3?3 0 0,360 1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3, 0 0 0,360 0 0 0 255 255 255 100 +sp-Breath Weapon Line Medium Electricity Fire Strike 30 100 Object YPR Sprite Same as Emitter Polar flare-big 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 64,0 0 0 0 0 0 0 0 0 0 -3?3 0 0,360 20?60 0 0 0?720 0 0 0 0 128 255 20 +sp-Breath Weapon Line Medium Electricity fire strike 3 sparklies 30 50 Object YPR Sprite Same as Emitter Polar flare-1 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 255,0 0 0 0 0 0 0 0 0 0 -3?3 0 0,360 1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3,1,30,20,25,6,26,15,2,28,3, 0 0 0,360 0 0 255,0 255,64,64,64,64 255,128,128,128,128 255,255 100 +sp-Breath Weapon Line Medium Electricity water droplets 15 30 30 Object YPR bip01 head Sprite Same as Emitter Polar Cartesian flare-1 5 Add 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -3?3 0 0,360 3?15 0 0 0?360 0 0 255,0 255,64,64,64,64 255,128,128,128,128 255,255 5 +sp-Breath Weapon Line Medium Fire smoke emit 10 20 400 Object YPR Sprite Same as Emitter Polar Cartesian fire-sprite 120 Blend 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0?30 0 -3?3 30?330 5?40 0 0 0?360 0 0 0,32,0 0 0 0 100 +sp-Breath Weapon Line Medium Fire Fire Strike 30 50 Object YPR Disc Same as Emitter Polar flare 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 64,0 0 0 0 0 0 0 0 0 0 -3?3 0 0,360 20?60 0 0 0?720 0 0 0,255,255,0 255(6),255(10) 255(6),130(10) 0 20 +sp-Breath Weapon Line Medium Fire Fire Strike 2 30 300 Object YPR Disc Same as Emitter Polar flare 30 Add 0 0 0 0 0 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -3?3 0 0,360 0,10 0 0 0?720 0 0 0,255,255,0 255(6),255(10) 255(6),130(10) 0 30 diff --git a/tpdatasrc/tpgamefiles/rules/spell_enums/dragon_disciple_spell_enum.mes b/tpdatasrc/tpgamefiles/rules/spell_enums/dragon_disciple_spell_enum.mes new file mode 100644 index 000000000..91a27524d --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/spell_enums/dragon_disciple_spell_enum.mes @@ -0,0 +1,7 @@ +// Dragon Disciple Breath Weapons + +{3231}{Dragon Disciple Cone Breath} +{3232}{Dragon Disciple Line Breath} + +{23231}{DRAGON_DISCIPLE_CONE_BREATH} +{23232}{DRAGON_DISCIPLE_LINE_BREATH} diff --git a/tpdatasrc/tpgamefiles/rules/spells/3231 - Dragon Disciple Cone Breath.txt b/tpdatasrc/tpgamefiles/rules/spells/3231 - Dragon Disciple Cone Breath.txt new file mode 100644 index 000000000..07c9d910a --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/spells/3231 - Dragon Disciple Cone Breath.txt @@ -0,0 +1,17 @@ +School: None +Casting Time: 1 action +Range: Specified 30 +Saving Throw: Reflex +Spell Resistance: No +Projectile: No +flags_Target: Range +flags_Target: Degrees +flags_Target: Fixed-radius +inc_flags_Target: Other +exc_flags_Target: Self +exc_flags_Target: Dead +exc_flags_Target: Non-critter +mode_Target: Cone +radius_Target: 60 +degrees_Target: 90 +ai_type: ai_action_offensive \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/rules/spells/3232 - Dragon Disciple Line Breath.txt b/tpdatasrc/tpgamefiles/rules/spells/3232 - Dragon Disciple Line Breath.txt new file mode 100644 index 000000000..1073d15d1 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/spells/3232 - Dragon Disciple Line Breath.txt @@ -0,0 +1,15 @@ +School: None +Casting Time: 1 action +Range: Specified 60 +Saving Throw: Reflex +Spell Resistance: No +Projectile: No +flags_Target: Range +flags_Target: Radius +inc_flags_Target: Other +exc_flags_Target: Self +exc_flags_Target: Dead +exc_flags_Target: Non-critter +mode_Target: Ray +radius_Target: 5 +ai_type: ai_action_offensive \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/scr/Spell3231 - Dragon Disciple Cone Breath.py b/tpdatasrc/tpgamefiles/scr/Spell3231 - Dragon Disciple Cone Breath.py new file mode 100644 index 000000000..f0d8cf6c6 --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/Spell3231 - Dragon Disciple Cone Breath.py @@ -0,0 +1,61 @@ +from toee import * +from heritage_feat_utils import getDraconicHeritageElement + +def OnBeginSpellCast(spell): + print "Dragon Disciple Cone Breath OnBeginSpellCast" + print "spell.target_list=", spell.target_list + print "spell.caster=", spell.caster, " caster.level= ", spell.caster_level + +def OnSpellEffect(spell): + print "Dragon Disciple Cone Breath OnSpellEffect" + + targetsToRemove = [] + spell.duration = 0 + spell.dc = 10 + spell.caster_level + ((spell.caster.stat_level_get(stat_constitution)- 10 ) / 2) + + spellDamageDice = dice_new('1d8') + if spell.caster_level < 7: + spellDamageDice.number = 2 + elif spell.caster_level < 10: + spellDamageDice.number = 4 + else: + spellDamageDice.number = 6 + saveType = D20_Save_Reduction_Half + + heritage = spell.caster.d20_query("PQ_Selected_Draconic_Heritage") + damageType = getDraconicHeritageElement(heritage) + if damageType == D20DT_ACID: + saveDescriptor = D20STD_F_SPELL_DESCRIPTOR_ACID + elementString = "Acid" + elif damageType == D20DT_COLD: + saveDescriptor = D20STD_F_SPELL_DESCRIPTOR_COLD + elementString = "Cold" + elif damageType == D20DT_ELECTRICITY: + saveDescriptor = D20STD_F_SPELL_DESCRIPTOR_ELECTRICITY + elementString = "Electricity" + elif damageType == D20DT_FIRE: + saveDescriptor = D20STD_F_SPELL_DESCRIPTOR_FIRE + elementString = "Fire" + else: #Fallback + saveDescriptor = D20STD_F_NONE + elementString = "Fire" + particleEffect = "sp-Breath Weapon Cone Medium {}".format(elementString) + + game.particles(particleEffect, spell.caster) + + for spellTarget in spell.target_list: + #Save for half damage: + if spellTarget.obj.reflex_save_and_damage(spell.caster, spell.dc, saveType, saveDescriptor, spellDamageDice, damageType, D20DAP_UNSPECIFIED, D20A_CAST_SPELL, spell.id): #success + spellTarget.obj.float_mesfile_line('mes\\spell.mes', 30001) + else: + spellTarget.obj.float_mesfile_line('mes\\spell.mes', 30002) + targetsToRemove.append(spellTarget.obj) + + spell.target_list.remove_list(targetsToRemove) + spell.spell_end(spell.id) + +def OnBeginRound(spell): + print "Dragon Disciple Cone Breath OnBeginRound" + +def OnEndSpellCast(spell): + print "Dragon Disciple Cone Breath OnEndSpellCast" \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/scr/Spell3232 - Dragon Disciple Line Breath.py b/tpdatasrc/tpgamefiles/scr/Spell3232 - Dragon Disciple Line Breath.py new file mode 100644 index 000000000..dd23cccdf --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/Spell3232 - Dragon Disciple Line Breath.py @@ -0,0 +1,61 @@ +from toee import * +from heritage_feat_utils import getDraconicHeritageElement + +def OnBeginSpellCast(spell): + print "Dragon Disciple Line Breath OnBeginSpellCast" + print "spell.target_list=", spell.target_list + print "spell.caster=", spell.caster, " caster.level= ", spell.caster_level + +def OnSpellEffect(spell): + print "Dragon Disciple Line Breath OnSpellEffect" + + targetsToRemove = [] + spell.duration = 0 + spell.dc = 10 + spell.caster_level + ((spell.caster.stat_level_get(stat_constitution) - 10) / 2) + + spellDamageDice = dice_new('1d8') + if spell.caster_level < 7: + spellDamageDice.number = 2 + elif spell.caster_level < 10: + spellDamageDice.number = 4 + else: + spellDamageDice.number = 6 + saveType = D20_Save_Reduction_Half + + heritage = spell.caster.d20_query("PQ_Selected_Draconic_Heritage") + damageType = getDraconicHeritageElement(heritage) + if damageType == D20DT_ACID: + saveDescriptor = D20STD_F_SPELL_DESCRIPTOR_ACID + elementString = "Acid" + elif damageType == D20DT_COLD: + saveDescriptor = D20STD_F_SPELL_DESCRIPTOR_COLD + elementString = "Cold" + elif damageType == D20DT_ELECTRICITY: + saveDescriptor = D20STD_F_SPELL_DESCRIPTOR_ELECTRICITY + elementString = "Electricity" + elif damageType == D20DT_FIRE: + saveDescriptor = D20STD_F_SPELL_DESCRIPTOR_FIRE + elementString = "Fire" + else: #Fallback + saveDescriptor = D20STD_F_NONE + elementString = "Fire" + particleEffect = "sp-Breath Weapon Line Medium {}".format(elementString) + + game.particles(particleEffect, spell.caster) + + for spellTarget in spell.target_list: + #Save for half damage: + if spellTarget.obj.reflex_save_and_damage(spell.caster, spell.dc, saveType, saveDescriptor, spellDamageDice, damageType, D20DAP_UNSPECIFIED, D20A_CAST_SPELL, spell.id): #success + spellTarget.obj.float_mesfile_line('mes\\spell.mes', 30001) + else: + spellTarget.obj.float_mesfile_line('mes\\spell.mes', 30002) + targetsToRemove.append(spellTarget.obj) + + spell.target_list.remove_list(targetsToRemove) + spell.spell_end(spell.id) + +def OnBeginRound(spell): + print "Dragon Disciple Line Breath OnBeginRound" + +def OnEndSpellCast(spell): + print "Dragon Disciple Line Breath OnEndSpellCast" \ No newline at end of file diff --git a/tpdatasrc/tpgamefiles/scr/feats/feat - Draconic Heritage.py b/tpdatasrc/tpgamefiles/scr/feats/feat - Draconic Heritage.py new file mode 100644 index 000000000..b7526c659 --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/feats/feat - Draconic Heritage.py @@ -0,0 +1,11 @@ +from toee import * +import char_editor +from heritage_feat_utils import hasDifferentHeritageFeat + +def CheckPrereq(attachee, classLevelled, abilityScoreRaised): + if char_editor.stat_level_get(stat_level_sorcerer) < 1: + return 0 + #Check if character already has any heritage feats + if hasDifferentHeritageFeat(char_editor, "Draconic Heritage"): + return 0 + return 1 diff --git a/tpdatasrc/tpgamefiles/scr/feats/feat - Extra Exhalation.py b/tpdatasrc/tpgamefiles/scr/feats/feat - Extra Exhalation.py new file mode 100644 index 000000000..2e9721d4e --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/feats/feat - Extra Exhalation.py @@ -0,0 +1,10 @@ +from templeplus.pymod import PythonModifier +from toee import * +import char_editor + +def CheckPrereq(attachee, classLevelled, abilityScoreRaised): + #Check if character has a limited usage Breath Weapon + #At the moment only Dragon Diciple has one: + if not char_editor.has_feat("Dragon Disciple Breath Weapon"): + return 0 + return 1 diff --git a/tpdatasrc/tpgamefiles/scr/heritage_feat_utils.py b/tpdatasrc/tpgamefiles/scr/heritage_feat_utils.py new file mode 100644 index 000000000..9b82de5ae --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/heritage_feat_utils.py @@ -0,0 +1,174 @@ +from templeplus.pymod import PythonModifier +from toee import * +import tpdp + +# This file handles heritage feats + +def heritageTypeList(): + typeList = [ + "Celestial Sorcerer Heritage", + "Draconic Heritage Black", + "Draconic Heritage Blue", + "Draconic Heritage Brass", + "Draconic Heritage Bronze", + "Draconic Heritage Copper", + "Draconic Heritage Gold", + "Draconic Heritage Green", + "Draconic Heritage Red", + "Draconic Heritage Silver", + "Draconic Heritage White", + "Fey Heritage", + "Fiendish Heritage", + "Infernal Sorcerer Heritage" + ] + return typeList + +# PHB II +def getCelestialSorcererHeritageFeatList(): + featList = [ + "Celestial Sorcerer Aura", + "Celestial Sorcerer Heritage", + "Celestial Sorcerer Lance", + "Celestial Sorcerer Lore", + "Celestial Sorcerer Wings" + ] + return featList + +# Draconic Feat List +# Note: Draconic Aura and Double Aura are not part of the draconic feats! +# Missing: Complete Arcane: Claw, Flight, Legacy(maybe a skip), Power, Presence +# Missing Dragon Magic: [Colour] Dragon Lineage (started), Draconic Knowledge (skip as there are no knowledge skills?), Draconic Senses +# Missing Dragon Magic Multiclass options: Dragonfire Assault (Power Attack), Dragonfire Channeling (Turn/Rebuke), +# Dragonfire Inspiration (Bardic Music), Dragonfire Strike (Sneak/Skirmish/Sudden Strike) +# Missing Races of the Dragon: Persuasion +def getDraconicHeritageFeatList(): + featList = [ + "Draconic Arcane Grace", + "Draconic Armor", + "Draconic Breath", + "Draconic Claw ", + "Draconic Flight" + "Draconic Heritage Black", + "Draconic Heritage Blue", + "Draconic Heritage Brass", + "Draconic Heritage Bronze", + "Draconic Heritage Copper", + "Draconic Heritage Gold", + "Draconic Heritage Green", + "Draconic Heritage Red", + "Draconic Heritage Silver", + "Draconic Heritage White", + "Draconic Knowledge", + "Draconic Legacy", + "Draconic Persuasion", + "Draconic Power", + "Draconic Presence", + "Draconic Resistance", + "Draconic Senses", + "Draconic Skin", + "Draconic Toughness", + "Draconic Vigor", + "Dragon Lineage Black", + "Dragon Lineage Blue", + "Dragon Lineage Brass", + "Dragon Lineage Bronze", + "Dragon Lineage Copper", + "Dragon Lineage Gold", + "Dragon Lineage Green", + "Dragon Lineage Red", + "Dragon Lineage Silver", + "Dragon Lineage White", + "Dragonfire Assault", + "Dragonfire Channeling", + "Dragonfire Inspiration", + "Dragonfire Strike" + ] + return featList + +# Complete Mage +def getFeyHeritageFeatList(): + featList = [ + "Fey Heritage", + "Fey Legacy", + "Fey Power", + "Fey Presence", + "Fey Skin" + ] + return featList + +# Complete Mage +def getFiendishHeritageFeatList(): + featList = [ + "Fiendish Heritage", + "Fiendish Legacy", + "Fiendish Power", + "Fiendish Presence", + "Fiendish Resistance" + ] + return featList + +# PHB II +def getInfernalSorcererHeritageFeatList(): + featList = [ + "Infernal Sorcerer Eyes", + "Infernal Sorcerer Heritage", + "Infernal Sorcerer Howl", + "Infernal Sorcerer Resistance", + ] + return featList + +def hasDifferentHeritageFeat(attachee, featToAquire): + for heritageFeat in heritageTypeList(): + if not featToAquire == heritageFeat: + if attachee.has_feat(heritageFeat): + return True + return False + +# Counts the number of heritage feats a character has +# Used by several feats +def countHeritageFeats(attachee, heritageType): + if heritageType == heritage_celestial_sorcerer: + featList = getCelestialSorcererHeritageFeatList() + elif heritageType in range(heritage_draconic_black, heritage_draconic_white + 1): + featList = getDraconicHeritageFeatList() + elif heritageType == heritage_fey: + featList = getFeyHeritageFeatList() + elif heritageType == heritage_fiendish: + featList = getFiendishHeritageFeatList() + elif heritageType == heritage_infernal_sorcerer: + featList = getInfernalSorcererHeritageFeatList() + numberOfFeats = 0 + for feat in featList: + if attachee.has_feat(feat): + numberOfFeats += 1 + return numberOfFeats + +# Draconic heritage mappings +# [colourString, elementType, breathWeaponShape] + +def getDictDraconicHeritage(): + dictDraconicHeritage = { + heritage_draconic_black: ["Black", D20DT_ACID, dragon_breath_shape_line], + heritage_draconic_blue: ["Blue", D20DT_ELECTRICITY, dragon_breath_shape_line], + heritage_draconic_brass: ["Brass", D20DT_FIRE, dragon_breath_shape_line], + heritage_draconic_bronze: ["Bronze", D20DT_ELECTRICITY, dragon_breath_shape_line], + heritage_draconic_copper: ["Copper", D20DT_ACID, dragon_breath_shape_line], + heritage_draconic_gold: ["Gold", D20DT_FIRE, dragon_breath_shape_cone], + heritage_draconic_green: ["Green", D20DT_ACID, dragon_breath_shape_cone], + heritage_draconic_red: ["Red", D20DT_FIRE, dragon_breath_shape_cone], + heritage_draconic_silver: ["Silver", D20DT_COLD, dragon_breath_shape_cone], + heritage_draconic_white: ["White", D20DT_COLD, dragon_breath_shape_cone] + } + return dictDraconicHeritage + +def getDraconicHeritageColourString(heritage): + dictDraconicHeritage = getDictDraconicHeritage() + return dictDraconicHeritage[heritage][0] + +def getDraconicHeritageElement(heritage): + dictDraconicHeritage = getDictDraconicHeritage() + return dictDraconicHeritage[heritage][1] + +def getDraconicHeritageBreathShape(heritage): + dictDraconicHeritage = getDictDraconicHeritage() + return dictDraconicHeritage[heritage][2] diff --git a/tpdatasrc/tpgamefiles/scr/tpModifiers/breath_weapon.py b/tpdatasrc/tpgamefiles/scr/tpModifiers/breath_weapon.py new file mode 100644 index 000000000..4818e4c55 --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/tpModifiers/breath_weapon.py @@ -0,0 +1,83 @@ +from templeplus.pymod import PythonModifier +from toee import * +import tpdp + +################################################### + +print "Registering Breath Weapon" + +################################################### + +def breathWeaponOnConditionAdd(attachee, args, evt_obj): + breathWeaponId = args.get_param(0) + baseCharges = args.get_param(1) + cooldown = 0 + args.set_arg(0, breathWeaponId) + args.set_arg(1, baseCharges) + args.set_arg(2, cooldown) + args.set_arg(3, baseCharges) + args.set_arg(4, 0) + return 0 + +# Handle Breath Weapon Charges +def getMaxCharges (attachee, args): + baseCharges = args.get_arg(3) + extraCharges = attachee.d20_query("PQ_Extra_Breath_Charges") + return baseCharges + extraCharges + +def resetBreathWeaponUses(attachee, args, evt_obj): + if not args.get_arg(1) == -1: + maxCharges = getMaxCharges(attachee, args) + args.set_arg(1, maxCharges) + return 0 + +# Handle Breath Weapon Cooldown +def getBreathWeaponCoolDown(): + cooldownDice = dice_new('1d4') + return cooldownDice.roll() + +def reduceBreathWeaponCooldown(attachee, args, evt_obj): + if args.get_arg(2) > -1: + cooldown = args.get_arg(2) + cooldown -= evt_obj.data1 + if cooldown < 0 and game.combat_is_active(): + attachee.float_text_line("Breath Weapon ready") + args.set_arg(2, cooldown) + return 0 + +# Trigger Breath Weapon Used +def signalBreathWeaponUsed(attachee, args, evt_obj): + signalId = evt_obj.data1 + breathWeaponId = args.get_arg(0) + if signalId == breathWeaponId: + charges = args.get_arg(1) + if not charges == 1: + cooldown = getBreathWeaponCoolDown() + args.set_arg(2, cooldown) + if not charges == -1: + charges -= 1 + args.set_arg(1, charges) + return 0 + +def queryLimitedBreathWeapon(attachee, args, evt_obj): + if args.get_arg(1) != -1: + evt_obj.return_val = 1 + return 0 + +class BreathWeaponModifier(PythonModifier): + # Breath Weapon modifiers have 5 arguments: + # 0: breathWeaponId, 1: charges, 2: Cooldown, 3: baseCharges, 4: empty + # Charges set to -1 indicates limitless breath weapon uses + # A Breath Weapon usage always triggers a 1d4 long cooldown + # before the Breath Weapon becomes availible again + def __init__(self, name): + PythonModifier.__init__(self, name, 5, True) + self.AddHook(ET_OnD20PythonSignal, "PS_Breath_Weapon_Used", signalBreathWeaponUsed, ()) + self.AddHook(ET_OnBeginRound, EK_NONE, reduceBreathWeaponCooldown, ()) + self.AddHook(ET_OnNewDay, EK_NEWDAY_REST, resetBreathWeaponUses, ()) + self.AddHook(ET_OnD20PythonQuery, "PQ_Has_Limited_Breath_Weapon", queryLimitedBreathWeapon, ()) + + # This hook needs to be added for every BreathWeaponModifier + def breathWeaponSetArgs(self, breathWeaponId, baseCharges): + self.AddHook(ET_OnConditionAdd, EK_NONE, breathWeaponOnConditionAdd, (breathWeaponId, baseCharges,)) + diff --git a/tpdatasrc/tpgamefiles/scr/tpModifiers/draconic_heritage.py b/tpdatasrc/tpgamefiles/scr/tpModifiers/draconic_heritage.py new file mode 100644 index 000000000..81dfe9bf2 --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/tpModifiers/draconic_heritage.py @@ -0,0 +1,78 @@ +from templeplus.pymod import PythonModifier +from toee import * +import tpdp +import char_class_utils +import heritage_feat_utils + +# Draconic Heritage: Complete Arcane, p. 77 + +print "Registering Draconic Heritage" + +def getDraconicHeritageClassSkill(heritage): + skillDict = { + heritage_draconic_black: skill_hide, + heritage_draconic_blue: skill_listen, + heritage_draconic_green: skill_move_silently, + heritage_draconic_red: skill_intimidate, + heritage_draconic_white: skill_balance, + heritage_draconic_brass: skill_gather_information, + heritage_draconic_bronze: skill_wilderness_lore, + heritage_draconic_copper: skill_hide, + heritage_draconic_gold: skill_heal, + heritage_draconic_silver: skill_disguise + } + return skillDict[heritage] + +def getSaveDescriptor(heritageElement): + if heritageElement == D20DT_ACID: + return D20STD_F_SPELL_DESCRIPTOR_ACID + elif heritageElement == D20DT_COLD: + return D20STD_F_SPELL_DESCRIPTOR_COLD + elif heritageElement == D20DT_ELECTRICITY: + return D20STD_F_SPELL_DESCRIPTOR_ELECTRICITY + elif heritageElement == D20DT_FIRE: + return D20STD_F_SPELL_DESCRIPTOR_FIRE + return 0 + +def addClassSkill(attachee, args, evt_obj): + heritage = args.get_arg(1) + skillEnum = getDraconicHeritageClassSkill(heritage) + if evt_obj.data1 == skillEnum: + evt_obj.return_val = 1 + return 0 + +def addSavingThrowBonus(attachee, args, evt_obj): + if attachee.stat_level_get(stat_level_sorcerer) > 0: + heritage = args.get_arg(1) + heritageElement = heritage_feat_utils.getDraconicHeritageElement(heritage) + saveDescriptor = getSaveDescriptor(heritageElement) + flags = evt_obj.flags + #This is not working properly, because as soon as the spell uses reflex_save_and_damage + #the Save Descriptor gets dropped :( + #I did not use reflex_save_and_damage for my damage spells in the spell compendium + #but instead the normal saving_throw_spell and then dealt damage afterwards with + #spell_damage_with_reduction/spell_damage which leads to the situation + #that the below save bonus works with the spell compendium spells + #but not with the core spells. + print "flags: {}".format(flags) + if (flags & (1 << (saveDescriptor-1))): + bonusValue = heritage_feat_utils.countHeritageFeats(attachee, heritage) + bonusType = 0 # ID 0 = Untyped (stacking) + evt_obj.bonus_list.add(bonusValue ,bonusType ,"~Draconic Heritage~[TAG_DRACONIC_HERITAGE]") + #elif #Sleep and Paralyze missing + #there are no slepp or paralyze flags/descriptors atm, which means, you can do immunities to both types + #but no save bonus effects + return 0 + +def querySelectedHeritage(attachee, args, evt_obj): + heritage = args.get_arg(1) + evt_obj.return_val = heritage + return 0 + +draconicHeritageFeat = PythonModifier("Draconic Heritage Feat", 3) #featEnum, heritage, empty +for heritage in range(heritage_draconic_black, heritage_draconic_white + 1): + colourString = heritage_feat_utils.getDraconicHeritageColourString(heritage) + draconicHeritageFeat.MapToFeat("Draconic Heritage {}".format(colourString), feat_cond_arg2 = heritage) +draconicHeritageFeat.AddHook(ET_OnD20PythonQuery, "PQ_Selected_Draconic_Heritage", querySelectedHeritage, ()) +draconicHeritageFeat.AddHook(ET_OnSaveThrowLevel, EK_NONE, addSavingThrowBonus, ()) +draconicHeritageFeat.AddHook(ET_OnD20PythonQuery, "Is Class Skill", addClassSkill, ()) diff --git a/tpdatasrc/tpgamefiles/scr/tpModifiers/dragon_disciple.py b/tpdatasrc/tpgamefiles/scr/tpModifiers/dragon_disciple.py index f80321d07..be2f74665 100644 --- a/tpdatasrc/tpgamefiles/scr/tpModifiers/dragon_disciple.py +++ b/tpdatasrc/tpgamefiles/scr/tpModifiers/dragon_disciple.py @@ -1,7 +1,10 @@ from templeplus.pymod import PythonModifier from toee import * import tpdp +import tpactions import char_class_utils +import heritage_feat_utils +import breath_weapon ################################################### @@ -13,8 +16,12 @@ def GetSpellCasterConditionName(): print "Registering " + GetConditionName() -classEnum = stat_level_eldritch_knight +classEnum = stat_level_dragon_disciple classSpecModule = __import__('class023_dragon_disciple') + +breathWeaponEnum = 2302 +toggleFlyingId = 2303 + ################################################### @@ -47,100 +54,231 @@ def OnGetSaveThrowWill(attachee, args, evt_obj): classSpecObj.AddHook(ET_OnSaveThrowLevel, EK_SAVE_REFLEX, OnGetSaveThrowReflex, ()) classSpecObj.AddHook(ET_OnSaveThrowLevel, EK_SAVE_WILL, OnGetSaveThrowWill, ()) +##### Dragon Disciple Class Features ##### -def OnGetAbilityScore(attachee, args, evt_obj): - #statType = args.get_param(0) - lvl = attachee.stat_level_get(classEnum) - statMod = args.get_param(1) - - newValue = statMod + evt_obj.bonus_list.get_sum() - if (newValue < 3): # ensure minimum stat of 3 - statMod = 3-newValue - evt_obj.bonus_list.add(statMod, 0, 139) +### Draconic Heritage is handle by the Draconic Heritage Feat now + +### Bonus Spells per Day +def setBonusSpellPerDaySlot(attachee, args, evt_obj): + bonusSpellClass = attachee.highest_arcane_class + spellLevel = attachee.arcane_spell_level_can_cast() + args.set_arg(1, bonusSpellClass) + args.set_arg(2, spellLevel) return 0 -classSpecObj.AddHook(ET_OnAbilityScoreLevel, EK_STAT_STRENGTH, OnGetAbilityScore, ()) +def applyExtraSpell(attachee, args, evt_obj): + bonusSpellClass = args.get_arg(1) + spellLevel = args.get_arg(2) + if evt_obj.get_caster_class() == bonusSpellClass and evt_obj.spell_level == spellLevel: + bonusValue = 1 + bonusType = 0 #ID 0 untyped (stacking) + evt_obj.bonus_list.add(bonusValue, bonusType, "Bonus Spell Slot") + return 0 -#region Spell casting +bonusSpellSlot = PythonModifier("Dragon Disciple Bonus Spell Slot", 4, False) #featEnum, bonusSpellClass, spellLevel, empty +for bonusSpellCount in range(1, 8): + bonusSpellSlot.MapToFeat("Dragon Disciple Bonus Spell per Day ({})".format(bonusSpellCount)) +bonusSpellSlot.AddHook(ET_OnGetSpellsPerDayMod, EK_NONE, applyExtraSpell, ()) +bonusSpellSlot.AddHook(ET_OnConditionAdd, EK_NONE, setBonusSpellPerDaySlot, ()) -# configure the spell casting condition to hold the highest Arcane classs -def OnAddSpellCasting(attachee, args, evt_obj): - #arg0 holds the arcane class - if (args.get_arg(0) == 0): - args.set_arg(0, char_class_utils.GetHighestArcaneClass(attachee)) - +### AC Bonus +def naturalArmorACBonus(attachee, args, evt_obj): + classLevel = attachee.stat_level_get(classEnum) + if classLevel == 1: + bonusValue = 1 + elif classLevel < 7: + bonusValue = 2 + elif classLevel < 10: + bonusValue = 3 + else: + bonusValue = 4 + bonusType = 0 #ID 0 = Stacking; Wrong Type as Touch Attacks should nullify this bonus + evt_obj.bonus_list.add(bonusValue, bonusType, "~Dragon Disciple Natural Armor~[TAG_CLASS_FEATURES_DRAGON_DISCIPLES_NATURAL_ARMOR_INCREASE]") return 0 -# Extend caster level for base casting class -def OnGetBaseCasterLevel(attachee, args, evt_obj): - class_extended_1 = args.get_arg(0) - class_code = evt_obj.arg0 - if (class_code != class_extended_1): - if (evt_obj.arg1 == 0): # arg1 != 0 means you're looking for this particular class's contribution - return 0 - classLvl = attachee.stat_level_get(classEnum) - if classLvl > 1: - evt_obj.bonus_list.add(classLvl - 1, 0, 137) +naturalArmorInc = PythonModifier("Dragon Disciple Natural Armor", 0) +naturalArmorInc.MapToFeat("Dragon Disciple Natural Armor") +naturalArmorInc.AddHook(ET_OnGetAC, EK_NONE, naturalArmorACBonus, ()) + +def onGetAbilityScoreStr(attachee, args, evt_obj): + level = attachee.stat_level_get(classEnum) + if level < 2: + return 0 + elif level < 4: + bonusValue = 2 + elif level < 10: + bonusValue = 4 + else: + bonusValue = 8 + bonusType = 0 #ID 0 = Untyped(stacking) + evt_obj.bonus_list.add(bonusValue, bonusType, "~Dragon Disciple Ability Boost~[TAG_CLASS_FEATURES_DRAGON_DISCIPLES_ABILITY_BOOST]") return 0 -def OnSpellListExtensionGet(attachee, args, evt_obj): - class_extended_1 = args.get_arg(0) - class_code = evt_obj.arg0 - if (class_code != class_extended_1): - if (evt_obj.arg1 == 0): # arg1 != 0 means you're looking for this particular class's contribution - return 0 - classLvl = attachee.stat_level_get(classEnum) - if classLvl > 1: - evt_obj.bonus_list.add(classLvl - 1, 0, 137) +def onGetAbilityScoreCon(attachee, args, evt_obj): + level = attachee.stat_level_get(classEnum) + if level >= 6: + bonusValue = 2 + bonusType = 0 #ID = 0 Untyped(stacking) + evt_obj.bonus_list.add(bonusValue, bonusType, "~Dragon Disciple Ability Boost~[TAG_CLASS_FEATURES_DRAGON_DISCIPLES_ABILITY_BOOST]") return 0 -def OnInitLevelupSpellSelection(attachee, args, evt_obj): - if (evt_obj.arg0 != classEnum): - return 0 - classLvl = attachee.stat_level_get(classEnum) - if (classLvl == 0): - return 0 - class_extended_1 = args.get_arg(0) - classSpecModule.InitSpellSelection(attachee, class_extended_1) +def onGetAbilityScoreInt(attachee, args, evt_obj): + level = attachee.stat_level_get(classEnum) + if level >= 8: + bonusValue = 2 + bonusType = 0 #ID = 0 Untyped(stacking) + evt_obj.bonus_list.add(bonusValue, bonusType, "~Dragon Disciple Ability Boost~[TAG_CLASS_FEATURES_DRAGON_DISCIPLES_ABILITY_BOOST]") return 0 -def OnLevelupSpellsCheckComplete(attachee, args, evt_obj): - if (evt_obj.arg0 != classEnum): - return 0 - class_extended_1 = args.get_arg(0) - if (not classSpecModule.LevelupCheckSpells(attachee, class_extended_1) ): - evt_obj.bonus_list.add(-1, 0, 137) # denotes incomplete spell selection - return 1 - -def OnLevelupSpellsFinalize(attachee, args, evt_obj): - if (evt_obj.arg0 != classEnum): - return 0 - classLvl = attachee.stat_level_get(classEnum) - if (classLvl == 0): - return 0 - class_extended_1 = args.get_arg(0) - classSpecModule.LevelupSpellsFinalize(attachee, class_extended_1) - return - -spellCasterSpecObj = PythonModifier(GetSpellCasterConditionName(), 8) -spellCasterSpecObj.AddHook(ET_OnConditionAdd, EK_NONE, OnAddSpellCasting, ()) -spellCasterSpecObj.AddHook(ET_OnGetBaseCasterLevel, EK_NONE, OnGetBaseCasterLevel, ()) -spellCasterSpecObj.AddHook(ET_OnSpellListExtensionGet, EK_NONE, OnSpellListExtensionGet, ()) -spellCasterSpecObj.AddHook(ET_OnLevelupSystemEvent, EK_LVL_Spells_Activate, OnInitLevelupSpellSelection, ()) -spellCasterSpecObj.AddHook(ET_OnLevelupSystemEvent, EK_LVL_Spells_Check_Complete, OnLevelupSpellsCheckComplete, ()) -spellCasterSpecObj.AddHook(ET_OnLevelupSystemEvent, EK_LVL_Spells_Finalize, OnLevelupSpellsFinalize, ()) - -#endregion - -def NaturalArmorACBonus(attachee, args, evt_obj): - type = args.get_arg(0) - if type != 3: - return 0 - bonus = args.get_arg(1) - evt_obj.bonus_list.add(bonus , 0, 137) +def onGetAbilityScoreCha(attachee, args, evt_obj): + level = attachee.stat_level_get(classEnum) + if level >= 10: + bonusValue = 2 + bonusType = 0 #ID = 0 Untyped(stacking) + evt_obj.bonus_list.add(bonusValue, bonusType, "~Dragon Disciple Ability Boost~[TAG_CLASS_FEATURES_DRAGON_DISCIPLES_ABILITY_BOOST]") return 0 -naturalArmorInc = PythonModifier("Dragon Disciple Natural Armor", 3) -naturalArmorInc.MapToFeat("Dragon Disciple Natural Armor") -naturalArmorInc.AddHook(ET_OnGetAC, EK_NONE, NaturalArmorACBonus, ()) +classSpecObj.AddHook(ET_OnAbilityScoreLevel, EK_STAT_STRENGTH, onGetAbilityScoreStr, ()) +classSpecObj.AddHook(ET_OnAbilityScoreLevel, EK_STAT_CONSTITUTION, onGetAbilityScoreCon, ()) +classSpecObj.AddHook(ET_OnAbilityScoreLevel, EK_STAT_INTELLIGENCE, onGetAbilityScoreInt, ()) +classSpecObj.AddHook(ET_OnAbilityScoreLevel, EK_STAT_CHARISMA, onGetAbilityScoreCha, ()) + +### Claws and Bite +# ToDo! + +### Breath Weapon + +def getBreathWeaponTag(): + return "TAG_CLASS_FEATURES_DRAGON_DISCIPLES_BREATH_WEAPON" + +def breathWeaponRadial(attachee, args, evt_obj): + print "breathWeaponRadial Hook" + breathWeaponCooldown = args.get_arg(2) + if breathWeaponCooldown > -1: + breathWeaponId = tpdp.RadialMenuEntryPythonAction("Breath Weapon Cooldown ({} round(s))".format(breathWeaponCooldown), D20A_PYTHON_ACTION, breathWeaponEnum, 0, "TAG_EXTRA_EXALATION") + else: + chargesLeft = args.get_arg(1) + maxCharges = breath_weapon.getMaxCharges(attachee, args) + heritage = attachee.d20_query("PQ_Selected_Draconic_Heritage") + breathWeaponShape = heritage_feat_utils.getDraconicHeritageBreathShape(heritage) + spellEnum = spell_dragon_diciple_cone_breath if breathWeaponShape == dragon_breath_shape_cone else spell_dragon_diciple_line_breath + breathWeaponTag = getBreathWeaponTag() + breathWeaponId = tpdp.RadialMenuEntryPythonAction("Breath Weapon ({}/{})".format(chargesLeft, maxCharges), D20A_PYTHON_ACTION, breathWeaponEnum, spellEnum, breathWeaponTag) + spellData = tpdp.D20SpellData(spellEnum) + spellData.set_spell_class(classEnum) + spellData.set_spell_level(9) #Setting this to 9 here so, it passes globes of invulnerability, as they should not protect against Breath Weapons + breathWeaponId.set_spell_data(spellData) + breathWeaponId.add_child_to_standard(attachee, tpdp.RadialMenuStandardNode.Class) + return 0 + +def checkBreathWeapon(attachee, args, evt_obj): + if args.get_arg(1) < 1: + evt_obj.return_val = AEC_OUT_OF_CHARGES + elif args.get_arg(2) > -1: + evt_obj.return_val = AEC_ABILITY_ON_COOLDOWN + return 0 + +def performBreathWeapon(attachee, args, evt_obj): + currentSequence = tpactions.get_cur_seq() + spellPacket = currentSequence.spell_packet + newSpellId = tpactions.get_new_spell_id() + spellPacket.caster_level += attachee.stat_level_get(classEnum) + tpactions.register_spell_cast(spellPacket, newSpellId) + currentSequence.spell_packet.spell_id = newSpellId + + if attachee.anim_goal_throw_spell_w_cast_anim(): # note: the animation goal has internal calls to trigger_spell_effect and the action frame + new_anim_id = attachee.anim_goal_get_new_id() + evt_obj.d20a.flags |= D20CAF_NEED_ANIM_COMPLETED + evt_obj.d20a.anim_id = new_anim_id + return 0 + +def frameBreathWeapon(attachee, args, evt_obj): + breathWeaponTag = getBreathWeaponTag() + genderString = "his" if attachee.stat_level_get(stat_gender) == 1 else "her" + game.create_history_freeform("{} uses {} ~Breath Weapon~[{}]\n\n".format(attachee.description, genderString, breathWeaponTag)) + #Send Breath Weapon Used Signal + breathWeaponId = args.get_arg(0) + attachee.d20_send_signal("PS_Breath_Weapon_Used", breathWeaponId) + return 0 + +dragonDiscipleBreathWeapon = breath_weapon.BreathWeaponModifier("Dragon Disciple Breath Weapon") +dragonDiscipleBreathWeapon.MapToFeat("Dragon Disciple Breath Weapon") +dragonDiscipleBreathWeapon.breathWeaponSetArgs(classEnum, 1) #1 = baseCharges of the Breath Weapon +dragonDiscipleBreathWeapon.AddHook(ET_OnBuildRadialMenuEntry, EK_NONE, breathWeaponRadial, ()) +dragonDiscipleBreathWeapon.AddHook(ET_OnD20PythonActionCheck, breathWeaponEnum, checkBreathWeapon, ()) +dragonDiscipleBreathWeapon.AddHook(ET_OnD20PythonActionPerform, breathWeaponEnum, performBreathWeapon, ()) +dragonDiscipleBreathWeapon.AddHook(ET_OnD20PythonActionFrame, breathWeaponEnum, frameBreathWeapon, ()) + + + +### Blindsense +#dropped + +###Wings +def addWings(attachee, args, evt_obj): + meshId = 14201 #Darley Wings + evt_obj.append(meshId) + return 0 + +def wingsRadial(attachee, args, evt_obj): + actionString = "Stop Flying" if attachee.d20_query("PQ_Is_Flying") else "Start Flying" + radialWingsId = tpdp.RadialMenuEntryPythonAction("{}".format(actionString), D20A_PYTHON_ACTION, toggleFlyingId, 0, "TAG_CLASS_FEATURES_DRAGON_DISCIPLES_WINGS") + radialWingsId.add_as_child(attachee, tpdp.RadialMenuStandardNode.Class) + return 0 + +def resetWings(attachee, args, evt_obj): + attachee.d20_signal("PS_Flying_End") + return 0 + +def checkWingsIndoors(attachee, args, evt_obj): + if not game.is_outdoor(): + evt_obj.return_val = AEC_ACTION_INVALID + return 0 + +def toggleWings(attachee, args, evt_obj): + isFlying = attachee.d20_query("PQ_Is_Flying") + if isFlying: + attachee.d20_send_signal("PS_Flying_End") + else: + attachee.condition_add_with_args("Flying Condition", 0, 0, 0) + return 0 + +dragonDiscipleWings = PythonModifier("Dragon Disciple Dragon Wings", 3) #empty, empty, empty +dragonDiscipleWings.MapToFeat("Dragon Disciple Wings") +dragonDiscipleWings.AddHook(ET_OnAddMesh, EK_NONE, addWings, ()) +dragonDiscipleWings.AddHook(ET_OnBuildRadialMenuEntry, EK_NONE, wingsRadial, ()) +dragonDiscipleWings.AddHook(ET_OnD20PythonActionCheck, toggleFlyingId, checkWingsIndoors, ()) +dragonDiscipleWings.AddHook(ET_OnD20PythonActionPerform, toggleFlyingId, toggleWings, ()) +dragonDiscipleWings.AddHook(ET_OnNewDay, EK_NEWDAY_REST, resetWings, ()) +dragonDiscipleWings.AddHook(ET_OnD20Signal, EK_S_Teleport_Reconnect, resetWings, ()) + + +#### Dragon Apotheosis +#Not implemented: +#Half-dragon template +#Low-light vision +#60-foot darkvision +def sleepParalyzeImmunity(attachee, args, evt_obj): + if evt_obj.is_modifier("sp-Sleep"): + evt_obj.return_val = 0 + combatMesLine = 5059 #ID 5059: "Sleep Immunity" + historyMesLine = 31 #ID 31: {[ACTOR] is immune to ~sleep~[TAG_SPELLS_SLEEP].} + attachee.float_mesfile_line('mes\\combat.mes', combatMesLine, tf_red) + game.create_history_from_pattern(historyMesLine, attachee, OBJ_HANDLE_NULL) + elif evt_obj.is_modifier("Paralyzed"): + evt_obj.return_val = 0 + attachee.float_text_line("Paralyze Immunity", tf_red) + game.create_history_freeform("{} is immune to ~paralyze~[TAG_PARALYZED] effects\n\n".format(attachee.description)) + return 0 + +def elementImmunity(attachee, args, evt_obj): + heritage = attachee.d20_query("PQ_Selected_Draconic_Heritage") + elementType = heritage_feat_utils.getDraconicHeritageElement(heritage) + damageMesLine = 132 #ID 132 in damage.mes is Immunity + evt_obj.damage_packet.add_mod_factor(0.0, elementType, damageMesLine) + return 0 +dragonDiscipleApotheosis = PythonModifier("Dragon Disciple Dragon Apotheosis", 3) #empty, empty, empty +dragonDiscipleApotheosis.MapToFeat("Dragon Disciple Dragon Apotheosis") +dragonDiscipleApotheosis.AddHook(ET_OnConditionAddPre, EK_NONE, sleepParalyzeImmunity, ()) +dragonDiscipleApotheosis.AddHook(ET_OnTakingDamage2, EK_NONE, elementImmunity, ()) diff --git a/tpdatasrc/tpgamefiles/scr/tpModifiers/flying_condition.py b/tpdatasrc/tpgamefiles/scr/tpModifiers/flying_condition.py new file mode 100644 index 000000000..3d9e9e10b --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/tpModifiers/flying_condition.py @@ -0,0 +1,41 @@ +from templeplus.pymod import PythonModifier +from __main__ import game +from toee import * +import tpdp + +def floatImmunity(attachee, immunityEffect, immunityTag): + attachee.float_text_line("Immune due to flying", tf_red) + game.create_history_freeform("{} is immune to ~{}~[{}] effects\n\n".format(attachee.description, immunityEffect, immunityTag)) + +def preventConditions(attachee, args, evt_obj): + if evt_obj.is_modifier("sp-Grease Hit"): + evt_obj.return_val = 0 + floatImmunity(attachee, "Grease", "TAG_SPELLS_GREASE") + elif evt_obj.is_modifier("sp-Entangle On"): + evt_obj.return_val = 0 + floatImmunity(attachee, "Entangle", "TAG_SPELLS_ENTANGLE") + return 0 + +def preventAoO(attachee, args, evt_obj): + attachee.float_text_line("Flying!") + evt_obj.return_val = 0 + return 0 + +def queryIsFlying(attachee, args, evt_obj): + evt_obj.return_val = 1 + return 0 + +def tooltipFlying(attachee, args, evt_obj): + evt_obj.append("Flying!") + return 0 + +def signalStopFlying(attachee, args, evt_obj): + args.condition_remove() + return 0 + +flyingCondition = PythonModifier("Flying Condition", 3) #empty, empty, empty +flyingCondition.AddHook(ET_OnConditionAddPre, EK_NONE, preventConditions, ()) +flyingCondition.AddHook(ET_OnD20Query, EK_Q_AOOIncurs, preventAoO,()) +flyingCondition.AddHook(ET_OnGetTooltip, EK_NONE, tooltipFlying, ()) +flyingCondition.AddHook(ET_OnD20PythonQuery, "PQ_Is_Flying", queryIsFlying, ()) +flyingCondition.AddHook(ET_OnD20PythonSignal, "PS_Flying_End", signalStopFlying, ())