diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbe1b66ff..d285fd32d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,10 @@ on: jobs: build: name: Build TemplePlus - runs-on: windows-latest + # Pin to windows-2022: windows-latest has migrated to a VS 2026 image + # that ships without ATL (atlcomcli.h missing) and without the .NET 4.5.2 + # reference assemblies the C# projects target. + runs-on: windows-2022 outputs: version: ${{ steps.get_version.outputs.version }} release: ${{ steps.get_version.outputs.release }} @@ -50,14 +53,30 @@ jobs: nuget install Squirrel.Windows -OutputDirectory Squirrel -Verbosity Detailed nuget restore - name: Build + # Discover the VS install path via vswhere instead of hardcoding the + # Enterprise edition path; the windows-latest runner image's VS edition + # and path can change without warning. run: | - call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars32.bat" + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set VSPATH=%%i + if not defined VSPATH ( + echo Failed to locate Visual Studio installation via vswhere + exit /b 1 + ) + echo Using VS install: %VSPATH% + call "%VSPATH%\VC\Auxiliary\Build\vcvars32.bat" msbuild TemplePlus.sln /p:Configuration=Release /p:Platform=Win32 /p:MSBuildDefines="_TP_VERSION=%TEMPLEPLUS_VERSION%;_TP_COMMIT=%COMMIT_ID%;TP_RELEASE_BUILD" /verbosity:minimal - # Need to use cmd because of the vcvars32 batch file shell: cmd env: TEMPLEPLUS_VERSION: ${{ steps.get_version.outputs.version }} COMMIT_ID: ${GITHUB_SHA::8} + - name: Pack tpdatasrc into .dat files + # PackFiles.bat regenerates tpgamefiles.dat (and the other .dat files) + # from tpdatasrc/, so the release artifact reflects any data-file mod + # changes. datpack.exe comes from the dependencies tarball above. + run: | + cd tpdatasrc + call PackFiles.bat + shell: cmd - name: Pack Release run: .\PackRelease.ps1 shell: pwsh @@ -67,7 +86,8 @@ jobs: shell: cmd # Package the symbol files, dump_syms needs the dev tools to be on the PATH run: | - set PATH=%PATH%;C:\Program Files\Microsoft Visual Studio\2022\Enterprise\DIA SDK\bin + for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath`) do set VSPATH=%%i + set PATH=%PATH%;%VSPATH%\DIA SDK\bin dependencies\bin\dump_syms.exe Release\TemplePlus.pdb > TemplePlus.sym 7z a TemplePlusSymbols-${{ steps.get_version.outputs.version }}.zip TemplePlus.sym Release\TemplePlus.pdb - name: Pack Tools @@ -130,7 +150,7 @@ jobs: package-release: name: Package Release - runs-on: windows-latest + runs-on: windows-2022 needs: build steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 727f81bc4..adcd24d75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ TemplePlus.ini +.claude/ + +# Source reference material (copyrighted) kept locally, never committed +/Divine O.pdf # C++ objects and libs diff --git a/DIVINE_ORACLE_INSTALL.txt b/DIVINE_ORACLE_INSTALL.txt new file mode 100644 index 000000000..83fde0c30 --- /dev/null +++ b/DIVINE_ORACLE_INSTALL.txt @@ -0,0 +1,127 @@ +Divine Oracle Prestige Class — Install Instructions +===================================================== + +This mod adds the Divine Oracle prestige class (from D&D 3.5e Complete Divine) +to TemplePlus. This version includes C++ engine changes for the Oracle domain +and Immune to Surprise, so it ships as a custom TemplePlus build — you +will replace TemplePlus.exe along with the data files. + + +Files to copy into your TemplePlus install +------------------------------------------ + +This ZIP is a full TemplePlus distribution built from the Divine Oracle fork. +Easiest install: back up your existing TemplePlus folder, then extract this +ZIP over the top so it overwrites: + + TemplePlus.exe + tpdata/tpgamefiles.dat + tpdata/tprules/class_specs.tab + (plus the usual TemplePlus DLLs and supporting files) + +Launch TemplePlus normally. Your existing saves should continue to work -- +the new class enum slot is appended at the end and doesn't shift any +existing class IDs. + +(Strongly recommended: back up your old TemplePlus folder before overwriting.) + + +Picking up the class in-game +---------------------------- + +Divine Oracle is a prestige class. To take it, your character needs to meet +the requirements at level-up: + + * Knowledge (religion): 8 ranks + * Feat: Skill Focus (Knowledge) <- ToEE only has the generic + "Skill Focus (Knowledge)" feat, + not per-knowledge variants + * Spellcasting: able to cast at least 2 divination spells + (clerics and druids qualify automatically once they + have 1st-level divine spells) + +Typical build: cleric 5 -> divine oracle 1+ on level up. + + +Class summary +------------- + +Hit Die: d6 Skill points: 2 + Int +BAB: poor (wizard) Saves: Will favored; Fort/Ref poor + +Spellcasting: each Divine Oracle level adds +1 caster level to your existing +divine class's spellcasting (like Mystic Theurge / Stormlord). + +Level features: + 1 Oracle Domain (+2 caster level on all Divination spells) + 1 Scry Bonus (+1 sacred to DC of Divination [Scrying] spells) + 2 Prescient Sense (granted as engine "Evasion") + 3 Divination Enhancement (marker -- see limitations) + 4 Uncanny Dodge + 6 Improved Uncanny Dodge + 10 Immune to Surprise (Divine Oracle 10+ characters skip the Surprised + condition entirely on entering combat during a surprise round) + + +Oracle domain +------------- + +The Oracle domain is now a real domain enum in the engine. Its spell list, +matching the Complete Divine source: + + 1st: Identify 6th: Legend Lore + 2nd: Augury 7th: Greater Scrying + 3rd: Divination 8th: Discern Location + 4th: Scrying 9th: Foresight + 5th: Commune + +These spell-to-domain registrations are added at engine startup so the +engine recognizes the Oracle domain. Note, however, that this list is not +selectable as daily domain spells in normal play (see limitation 4 below); +the practical benefit of the Oracle domain is the +2 caster level granted +power. Some of these spells (Augury, Commune, etc.) are also stubs in the +vanilla ToEE engine -- they exist as spell enums but don't do much +mechanically. + + +Known limitations +----------------- + +1. Trap Sense (PDF L2/+1, L5/+2, L8/+3) is NOT implemented. The engine + doesn't distinguish trap-vs-non-trap reflex saves, so adding it would + either over-apply (universal reflex bonus) or require non-trivial + refactor of the save system. Skipped to avoid silent overage. + +2. Divination Enhancement (L3 -- roll twice, take better on divinations + like augury/divination) is a marker feat only. The relevant spells + don't have functioning result-mechanics to re-roll in the first place. + +3. Prescient Sense (L2) uses the engine's standard Evasion. The PDF says + it works in any armor; the engine inherits whatever armor restriction + it applies to monk/rogue Evasion. Fixing this would require finding + and patching the vanilla evasion implementation, which lives in + ToEE.exe and isn't currently hooked by TemplePlus. + +4. Of the Oracle Domain feature, only the granted power (+2 caster level on + Divination spells) is functional in-game -- it is wired to the Divine + Oracle class feature and works automatically. The other half of the + feature ("choose the spells in that domain as her daily domain spells") + is NOT available: the engine only grants domain spells when the domain is + stored in the character's cleric domain slots, and no deity offers the + Oracle domain in the domain picker, so it can never be selected there. + The Oracle spell-to-domain registration exists in the engine but is not + reachable through normal play; treat the domain as the source of the + +2 caster level granted power only. + +5. Immune to Surprise gives the Divine Oracle 10+ character full normal + actions during a surprise round (instead of strictly "a standard + action" per the PDF). This is more generous than RAW but consistent + with how other capstones translate to ToEE's combat model. + + +Uninstalling +------------ + +Restore your backed-up TemplePlus folder. Characters that already took +Divine Oracle levels won't load cleanly with stock TemplePlus -- keep a +save from before you took the class. diff --git a/PackRelease.ps1 b/PackRelease.ps1 index c201f7f1c..8204a1db9 100644 --- a/PackRelease.ps1 +++ b/PackRelease.ps1 @@ -26,6 +26,7 @@ copy Release\ICSharpCode.SharpZipLib.dll dist copy Release\FontAwesome.WPF.dll dist copy Release\DeltaCompressionDotNet*.dll dist copy Release\TemplePlus.exe dist +copy DIVINE_ORACLE_INSTALL.txt dist copy -Recurse tpdata dist\tpdata copy -Recurse dependencies\python-lib dist\tpdata\python-lib copy "C:\Program Files (x86)\Windows Kits\10\Redist\D3D\x86\d3dcompiler_47.dll" dist diff --git a/TemplePlus.nuspec b/TemplePlus.nuspec index ca7ac3ba9..52053e862 100644 --- a/TemplePlus.nuspec +++ b/TemplePlus.nuspec @@ -23,6 +23,7 @@ + diff --git a/TemplePlus/d20_status.cpp b/TemplePlus/d20_status.cpp index ace711a41..d645eae02 100644 --- a/TemplePlus/d20_status.cpp +++ b/TemplePlus/d20_status.cpp @@ -214,7 +214,11 @@ void D20StatusSystem::initDomains(objHndl objHnd) void D20StatusSystem::initDomain(Dispatcher * dispatcher, uint32_t domain) { - if (domain) { + // ConditionArrayDomains is the vanilla ToEE table, populated only for the + // vanilla domains (0..Domain_Special). Modded domains such as Domain_Oracle + // have no entry there, so indexing with them would read past the table. + // They carry no vanilla domain condition anyway, so skip them. + if (domain && domain < Domain_Oracle) { CondStruct * condStructDomain = *(conds.ConditionArrayDomains + 3 * domain); uint32_t arg1 = *(conds.ConditionArrayDomainsArg1 + 3 * domain); uint32_t arg2 = *(conds.ConditionArrayDomainsArg2 + 3 * domain); diff --git a/TemplePlus/gamesystems/d20/d20stats.cpp b/TemplePlus/gamesystems/d20/d20stats.cpp index 05aecb403..913aa5bba 100644 --- a/TemplePlus/gamesystems/d20/d20stats.cpp +++ b/TemplePlus/gamesystems/d20/d20stats.cpp @@ -548,6 +548,7 @@ StatType D20StatsSystem::GetType(Stat stat) { case stat_level_warmage: case stat_level_beguilers: case stat_level_swashbuckler: + case stat_level_divine_oracle: case stat_level_psion: case stat_level_psychic_warrior: diff --git a/TemplePlus/spell.cpp b/TemplePlus/spell.cpp index b3f17aa55..a60c2af5f 100644 --- a/TemplePlus/spell.cpp +++ b/TemplePlus/spell.cpp @@ -267,7 +267,11 @@ class SpellFuncReplacements : public TempleFix { // SpellEntriesInit replaceFunction(0x1007B5B0, [](const char* spellRulesFolder)->BOOL { - return spellSys.SpellEntriesInit(spellRulesFolder); + BOOL result = spellSys.SpellEntriesInit(spellRulesFolder); + if (result) { + spellSys.AddOracleDomainSpells(); + } + return result; }); @@ -2427,6 +2431,43 @@ BOOL LegacySpellSystem::SpellEntriesInit(const char * spellRulesFolder){ return TRUE; } +void LegacySpellSystem::AddOracleDomainSpells() +{ + // Oracle domain spell list (Complete Divine, also at realmshelps.net): + // 1st: Identify, 2nd: Augury, 3rd: Divination, 4th: Scrying, + // 5th: Commune, 6th: Legend Lore, 7th: Greater Scrying, + // 8th: Discern Location, 9th: Foresight. + // Granted power (+2 caster level on Divination) is applied separately via + // the Divine Oracle class feature; this method only registers the + // spell-by-level mapping the engine needs for domain spell selection. + static const std::pair oracleSpells[] = { + {238, 1}, // Identify + { 23, 2}, // Augury + {136, 3}, // Divination + {410, 4}, // Scrying + { 69, 5}, // Commune + {264, 6}, // Legend Lore + {209, 7}, // Greater Scrying + {126, 8}, // Discern Location + {186, 9}, // Foresight + }; + + for (auto& spec : oracleSpells) { + auto spEntry = spellEntryRegistry.get(spec.first); + if (!spEntry) { + logger->warn("AddOracleDomainSpells: spell {} not in registry", spec.first); + continue; + } + if (spEntry->spellLvlsNum >= 10) { + logger->warn("AddOracleDomainSpells: spell {} has no room for Oracle entry", spec.first); + continue; + } + spEntry->spellLvls[spEntry->spellLvlsNum].spellClass = Domain_Oracle; + spEntry->spellLvls[spEntry->spellLvlsNum].slotLevel = spec.second; + spEntry->spellLvlsNum++; + } +} + // Factored out logic. Tries to look up a lowercased string key in the map, and // otherwise searches for a prefix match. template diff --git a/TemplePlus/spell.h b/TemplePlus/spell.h index 9f7cb39c6..5b8a5da0a 100644 --- a/TemplePlus/spell.h +++ b/TemplePlus/spell.h @@ -338,6 +338,11 @@ struct LegacySpellSystem : temple::AddressTable BOOL SpellEntriesInit(const char* spellRulesFodler); bool SpellEntryFileParse(SpellEntry & spEntry, TioFile *tf); + // Injects the Oracle domain (Complete Divine) into the existing divination + // spells listed in its 1st-9th level granted spell list. Run once after + // SpellEntriesInit so the engine treats Oracle just like any other domain. + void AddOracleDomainSpells(); + std::map> mMultiOptions; void GetSpellEntryExtFromClassSpec(std::map& mapping, int classEnum); diff --git a/TemplePlus/temple_enums.h b/TemplePlus/temple_enums.h index 7f6c860d8..f1ca77dfb 100644 --- a/TemplePlus/temple_enums.h +++ b/TemplePlus/temple_enums.h @@ -1952,7 +1952,8 @@ enum Domain Domain_War = 21, Domain_Water = 22, Domain_Special = 23, - Domain_Count = 24 + Domain_Oracle = 24, // Complete Divine; granted power +2 caster level on Divination spells + Domain_Count = 25 }; #pragma endregion @@ -2011,6 +2012,7 @@ enum Stat : uint32_t { stat_level_warmage = 47, stat_level_beguilers = 48, stat_level_swashbuckler = 49, + stat_level_divine_oracle = 50, stat_level_psion = 58, stat_level_psychic_warrior = 59, diff --git a/TemplePlus/turn_based.cpp b/TemplePlus/turn_based.cpp index 3e02eb4b7..7a93d23fe 100644 --- a/TemplePlus/turn_based.cpp +++ b/TemplePlus/turn_based.cpp @@ -225,7 +225,15 @@ void TurnBasedSys::AddToInitiative(objHndl handle) conds.AddTo(handle, "Flatfooted", {}); auto isSurpriseRound = temple::GetRef(0x10BCAD90); if (isSurpriseRound){ - conds.AddTo(handle, "Surprised", {}); + // Divine Oracle 10+: Immune to Surprise. PDF says the oracle can always + // take a standard action during a surprise round; the cleanest engine + // expression of that is to skip the Surprised condition entirely + // (which means full normal turn instead of strictly a standard action). + // Erring on the player's favor for a 10th-level capstone is consistent + // with how other capstones are translated to ToEE's combat model. + if (objects.StatLevelGet(handle, stat_level_divine_oracle) < 10) { + conds.AddTo(handle, "Surprised", {}); + } } /*auto addToInit = temple::GetRef(0x100DF1E0); diff --git a/tpdata/templeplus/lib/templeplus/constants.py b/tpdata/templeplus/lib/templeplus/constants.py index d424ecda7..91cc618b8 100644 --- a/tpdata/templeplus/lib/templeplus/constants.py +++ b/tpdata/templeplus/lib/templeplus/constants.py @@ -3610,6 +3610,7 @@ stat_level_warmage = 47 stat_level_beguiler = 48 stat_level_swashbuckler = 49 +stat_level_divine_oracle = 50 stat_level_psion = 58 stat_level_psychic_warrior = 59 diff --git a/tpdata/tprules/class_specs.tab b/tpdata/tprules/class_specs.tab index 5a0bc82c6..6ff814df4 100644 --- a/tpdata/tprules/class_specs.tab +++ b/tpdata/tprules/class_specs.tab @@ -44,7 +44,7 @@ PLACEHOLDER 46 PLACEHOLDER 47 PLACEHOLDER 48 PLACEHOLDER 49 -PLACEHOLDER 50 +Divine Oracle 50 PLACEHOLDER 51 PLACEHOLDER 52 PLACEHOLDER 53 diff --git a/tpdatasrc/tpgamefiles/mes/help/divine_oracle_help.tab b/tpdatasrc/tpgamefiles/mes/help/divine_oracle_help.tab new file mode 100644 index 000000000..e1d3ec3f5 --- /dev/null +++ b/tpdatasrc/tpgamefiles/mes/help/divine_oracle_help.tab @@ -0,0 +1,11 @@ +TAG_DIVINE_ORACLES TAG_PRESTIGE_CLASSES Divine Oracles All divine oracles are spellcasters, and most were clerics or druids before adopting the divine oracle prestige class. Whatever their other classes, all divine oracles share a particular devotion to the Divination school of magic, having mastered all available means to catch glimpses of the future.Hit Die: d6Requirements:To qualify to become a divine oracle, a character must fulfill all the following criteria. Skill: ~Knowledge (religion)~[TAG_KNOWLEDGE] 8 ranks. Feat: ~Skill Focus~[TAG_SKILL_FOCUS] (Knowledge [religion]). Spells: Able to cast at least 2 divination spells.Base Attack and Base Save Bonuses: see ~table~[TAG_CLASS_FEATURES_DIVINE_ORACLE_TABLES]Class Skills: ~Concentration(Con)~[TAG_CONCENTRATION], Craft(Int), ~Heal(Wis)~[TAG_HEAL], ~Intimidate(Cha)~[TAG_INTIMIDATE], Knowledge (arcana)(Int), Knowledge (religion)(Int), Profession(Wis), ~Spellcraft(Int)~[TAG_SPELLCRAFT]Skill Points at Each Level: 2 + Int modifierClass Features:[CMD_CHILDREN] +TAG_CLASS_FEATURES_DIVINE_ORACLE_TABLES TAG_DIVINE_ORACLES Divine Oracle Base Attack & Base Save Bonuses Level ~BAB~[TAG_LEVEL_BONUSES] ~Fort.~[TAG_FORTITUDE] ~Reflex~[TAG_REFLEX] ~Will~[TAG_WILL] 1 +0 +0 +0 +2 2 +1 +0 +0 +3 3 +1 +1 +1 +3 4 +2 +1 +1 +4 5 +2 +1 +1 +4 6 +3 +2 +2 +5 7 +3 +2 +2 +5 8 +4 +2 +2 +6 9 +4 +3 +3 +6 10 +5 +3 +3 +7 +TAG_CLASS_FEATURES_DIVINE_ORACLE_SPELLS_PER_DAY TAG_DIVINE_ORACLES Divine Oracle Spells per Day/Spells Known A divine oracle continues advancing in spellcasting ability as well as gaining the abilities of her new class. Thus, when a new divine oracle level is gained, the character gains new spells per day (and spells known, if applicable) as if she had also gained a level in whatever spellcasting class she belonged to before she added the prestige class level. She does not, however, gain any other benefit a character of that class would have gained. +TAG_CLASS_FEATURES_DIVINE_ORACLE_GRANTED_PROFICIENCIES TAG_DIVINE_ORACLES Divine Oracle Weapon and Armor Proficiency Divine oracles gain no weapon or armor proficiencies. +TAG_CLASS_FEATURES_DIVINE_ORACLE_ORACLE_DOMAIN TAG_DIVINE_ORACLES Divine Oracle Oracle Domain Upon adopting the divine oracle class, the character gains the Oracle domain's granted power: she casts divination spells at +2 caster level. (Choosing Oracle-domain spells as daily domain spells is not available in this implementation.) +TAG_CLASS_FEATURES_DIVINE_ORACLE_SCRY_BONUS TAG_DIVINE_ORACLES Divine Oracle Scry Bonus A divine oracle adds a +1 sacred bonus to the save DC of all her divination (scrying) spells. +TAG_CLASS_FEATURES_DIVINE_ORACLE_PRESCIENT_SENSE TAG_DIVINE_ORACLES Divine Oracle Prescient Sense Beginning at 2nd level, if a divine oracle makes a successful Reflex saving throw against an attack that normally inflicts half damage on a successful save, she instead takes no damage. This form of evasion works no matter what armor the divine oracle wears. +TAG_CLASS_FEATURES_DIVINE_ORACLE_DIVINATION_ENHANCEMENT TAG_DIVINE_ORACLES Divine Oracle Divination Enhancement Beginning at 3rd level, a divine oracle may roll twice and take the better result when using divination spells such as augury or divination. +TAG_CLASS_FEATURES_DIVINE_ORACLE_UNCANNY_DODGE TAG_DIVINE_ORACLES Divine Oracle Uncanny Dodge Starting at 4th level, a divine oracle gains the ability to react to danger before her senses would normally allow her to do so. +TAG_CLASS_FEATURES_DIVINE_ORACLE_IMPROVED_UNCANNY_DODGE TAG_DIVINE_ORACLES Divine Oracle Improved Uncanny Dodge At 6th level and higher, the divine oracle can no longer be flanked. This defense denies a rogue the ability to use flank attacks to sneak attack the divine oracle. +TAG_CLASS_FEATURES_DIVINE_ORACLE_IMMUNE_TO_SURPRISE TAG_DIVINE_ORACLES Divine Oracle Immune to Surprise At 10th level, the divine oracle's sensitivity to danger is so great that she is never surprised. She can always take a standard action during a surprise round, unless she is physically restrained from doing so. diff --git a/tpdatasrc/tpgamefiles/mes/spell_ext.mes b/tpdatasrc/tpgamefiles/mes/spell_ext.mes index 651214ebd..bbf5ac116 100644 --- a/tpdatasrc/tpgamefiles/mes/spell_ext.mes +++ b/tpdatasrc/tpgamefiles/mes/spell_ext.mes @@ -1,3 +1,6 @@ +// Domain name; line ID = 4000 + Domain_Oracle (24). +{4024}{Oracle} + {1000}{Moment of Prescience} {1001}{Touch of Fatigue} {1002}{Ray of Exhaustion} diff --git a/tpdatasrc/tpgamefiles/mes/stat_ext.mes b/tpdatasrc/tpgamefiles/mes/stat_ext.mes index 3794500c6..7a957ed99 100644 --- a/tpdatasrc/tpgamefiles/mes/stat_ext.mes +++ b/tpdatasrc/tpgamefiles/mes/stat_ext.mes @@ -34,6 +34,7 @@ {47}{Warmage} {48}{Beguiler} {49}{Swashbuckler} +{50}{Divine Oracle} // Psionic Classes {58}{Psion} @@ -105,7 +106,7 @@ {1047}{Wrmg} {1048}{Bglr} {1049}{Swbk} -{1050}{} +{1050}{DvOr} {1051}{} {1052}{} {1053}{} @@ -320,6 +321,12 @@ Special: Can cast 1st-level arcane spells and knows 1 abjuration spell of first {13040}Warmage{A warmage is a militant spellcaster whose training focuses on battlefield magic.} {13041}Beguiler{A beguiler is a spellcaster who delights in manipulating others, either to their disadvantage or for their own good.} {13042}Swashbuckler{The swashbuckler embodies the concepts of daring and panache.} + +{13043}Divine Oracle{All divine oracles are spellcasters sharing a particular devotion to the Divination school of magic, having mastered all available means to catch glimpses of the future. + +Requirements: Knowledge (religion) 8 ranks, Skill Focus (Knowledge), able to cast at least 2 divination spells. +} + {13075}Fochlucan Lyrist{The Fochlucan lyrist is a Warrior, thief, spy, poet and woodland champion. Requirements: Alignment NG/TN/CN/NE. diff --git a/tpdatasrc/tpgamefiles/rules/char_class/class050_divine_oracle.py b/tpdatasrc/tpgamefiles/rules/char_class/class050_divine_oracle.py new file mode 100644 index 000000000..04bd0120c --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/char_class/class050_divine_oracle.py @@ -0,0 +1,157 @@ +from toee import * +import tpdp +import char_class_utils +import char_editor +################################################### + +def GetConditionName(): + return "Divine Oracle" + +def GetSpellCasterConditionName(): + return "Divine Oracle Spellcasting" + +def GetCategory(): + return "Complete Divine Prestige Classes" + +def GetClassDefinitionFlags(): + return CDF_None + +def GetClassHelpTopic(): + return "TAG_DIVINE_ORACLES" + +classEnum = stat_level_divine_oracle + +################################################### + + +class_feats = { +1: ("Divine Oracle Oracle Domain", "Divine Oracle Scry Bonus",), +2: ("Divine Oracle Prescient Sense", feat_evasion,), +3: ("Divine Oracle Divination Enhancement",), +4: (feat_uncanny_dodge,), +6: (feat_improved_uncanny_dodge,), +10: ("Divine Oracle Immune to Surprise",), +} + +class_skills = (skill_concentration, skill_craft, skill_heal, skill_intimidate, skill_knowledge_arcana, skill_knowledge_religion, skill_profession, skill_spellcraft) + + +def IsEnabled(): + return 1 + +def GetHitDieType(): + return 6 + +def GetSkillPtsPerLevel(): + return 2 + +def GetBabProgression(): + return base_attack_bonus_type_non_martial + +def IsFortSaveFavored(): + return 0 + +def IsRefSaveFavored(): + return 0 + +def IsWillSaveFavored(): + return 1 + +# Spell casting +def GetSpellListType(): + return spell_list_type_extender + +def GetSpellSourceType(): + return spell_source_type_divine + +def IsClassSkill(skillEnum): + return char_class_utils.IsClassSkill(class_skills, skillEnum) + +def IsClassFeat(featEnum): + return char_class_utils.IsClassFeat(class_feats, featEnum) + +def GetClassFeats(): + return class_feats + +def IsAlignmentCompatible(alignment): + return 1 + +def ObjMeetsPrereqs(obj): + # Knowledge (religion) 8 ranks + if obj.skill_ranks_get(skill_knowledge_religion) < 8: + return 0 + # Skill Focus (Knowledge) - ToEE only has the generic Skill Focus (Knowledge) feat + if not obj.has_feat(feat_skill_focus_knowledge): + return 0 + # The class advances DIVINE spellcasting (all progression hooks bind to + # GetHighestDivineClass), so require divine casting -- an arcane-only caster + # could otherwise meet the prereq but gain no spellcasting progression. + if obj.divine_spell_level_can_cast() < 1: + return 0 + # "Able to cast at least 2 divination spells." The requirement is character + # -wide, so count distinct DIVINE divinations the character can actually + # cast across ALL of their divine casting classes (a multiclass caster may + # satisfy it from any one of them). + divine_classes = (stat_level_cleric, stat_level_druid, stat_level_paladin, + stat_level_ranger, stat_level_blackguard, stat_level_favored_soul) + divinations = set() + for cls in divine_classes: + cls_lvl = obj.stat_level_get(cls) + if cls_lvl < 1: + continue + # A class only counts if it can actually cast spells at this level (e.g. + # a too-low casting stat, or a non-casting paladin/ranger level, yields + # no castable spells). + max_lvl = char_editor.get_max_spell_level(obj, cls, cls_lvl) + if max_lvl < 1: + continue + # Cleric/druid prepare from the full divine list, which always contains + # well over two divinations -- they qualify outright. + if cls == stat_level_cleric or cls == stat_level_druid: + return 1 + # Spontaneous casters (Favored Soul) cast only what they KNOW for this + # class; prepared limited-list casters (paladin/ranger/blackguard) cast + # their whole available class list. + if cls == stat_level_favored_soul: + class_spells = [sp for sp in obj.spells_known if sp.caster_class == cls] + else: + class_spells = char_editor.get_learnable_spells(obj, cls, max_lvl) + for sp in class_spells: + if sp.spell_level > 0 and tpdp.SpellEntry(sp.spell_enum).spell_school_enum == Divination: + divinations.add(sp.spell_enum) + if len(divinations) >= 2: + return 1 + return 0 + + +# Levelup + +def IsSelectingSpellsOnLevelup(obj, class_extended_1=0): + if class_extended_1 <= 0: + class_extended_1 = char_class_utils.GetHighestDivineClass(obj) + if char_editor.is_selecting_spells(obj, class_extended_1): + return 1 + return 0 + + +def LevelupCheckSpells(obj, class_extended_1=0): + if class_extended_1 <= 0: + class_extended_1 = char_class_utils.GetHighestDivineClass(obj) + if not char_editor.spells_check_complete(obj, class_extended_1): + return 0 + return 1 + + +def InitSpellSelection(obj, class_extended_1=0): + newLvl = obj.stat_level_get(classEnum) + 1 + if newLvl == 1 or class_extended_1 <= 0: + class_extended_1 = char_class_utils.GetHighestDivineClass(obj) + char_editor.init_spell_selection(obj, class_extended_1) + return 0 + + +def LevelupSpellsFinalize(obj, class_extended_1=0): + if class_extended_1 <= 0: + class_extended_1 = char_class_utils.GetHighestDivineClass(obj) + char_editor.spells_finalize(obj, class_extended_1) + return 0 diff --git a/tpdatasrc/tpgamefiles/rules/feats/divine oracle divination enhancement.txt b/tpdatasrc/tpgamefiles/rules/feats/divine oracle divination enhancement.txt new file mode 100644 index 000000000..f995879cd --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/divine oracle divination enhancement.txt @@ -0,0 +1,5 @@ +name: Divine Oracle Divination Enhancement +flags: 8 +prereqs: +description: Beginning at 3rd level, a divine oracle may roll twice and take the better result when using divination spells such as augury or divination. +prereq descr: Divine Oracle level 3. diff --git a/tpdatasrc/tpgamefiles/rules/feats/divine oracle immune to surprise.txt b/tpdatasrc/tpgamefiles/rules/feats/divine oracle immune to surprise.txt new file mode 100644 index 000000000..756983dbb --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/divine oracle immune to surprise.txt @@ -0,0 +1,5 @@ +name: Divine Oracle Immune to Surprise +flags: 8 +prereqs: +description: At 10th level, the divine oracle's sensitivity to danger is so great that she is never surprised. She can always take a standard action during a surprise round, unless she is physically restrained from doing so. +prereq descr: Divine Oracle level 10. diff --git a/tpdatasrc/tpgamefiles/rules/feats/divine oracle oracle domain.txt b/tpdatasrc/tpgamefiles/rules/feats/divine oracle oracle domain.txt new file mode 100644 index 000000000..22c9758e6 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/divine oracle oracle domain.txt @@ -0,0 +1,5 @@ +name: Divine Oracle Oracle Domain +flags: 8 +prereqs: +description: Upon adopting the divine oracle class, the character gains the Oracle domain's granted power: she casts divination spells at +2 caster level. (Choosing Oracle-domain spells as daily domain spells is not available in this implementation.) +prereq descr: Divine Oracle level 1. diff --git a/tpdatasrc/tpgamefiles/rules/feats/divine oracle prescient sense.txt b/tpdatasrc/tpgamefiles/rules/feats/divine oracle prescient sense.txt new file mode 100644 index 000000000..bfa57bc11 --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/divine oracle prescient sense.txt @@ -0,0 +1,5 @@ +name: Divine Oracle Prescient Sense +flags: 8 +prereqs: +description: Beginning at 2nd level, if a divine oracle makes a successful Reflex saving throw against an attack that normally inflicts half damage on a successful save (such as a red dragon's fiery breath or a fireball spell), she instead takes no damage. This form of evasion works no matter what armor the divine oracle wears. +prereq descr: Divine Oracle level 2. diff --git a/tpdatasrc/tpgamefiles/rules/feats/divine oracle scry bonus.txt b/tpdatasrc/tpgamefiles/rules/feats/divine oracle scry bonus.txt new file mode 100644 index 000000000..c3305aeee --- /dev/null +++ b/tpdatasrc/tpgamefiles/rules/feats/divine oracle scry bonus.txt @@ -0,0 +1,5 @@ +name: Divine Oracle Scry Bonus +flags: 8 +prereqs: +description: A divine oracle adds a +1 sacred bonus to the save DC of all her divination (scrying) spells. +prereq descr: Divine Oracle level 1. diff --git a/tpdatasrc/tpgamefiles/rules/stat_ext.mes b/tpdatasrc/tpgamefiles/rules/stat_ext.mes index c121dd1fd..eab58c66a 100644 --- a/tpdatasrc/tpgamefiles/rules/stat_ext.mes +++ b/tpdatasrc/tpgamefiles/rules/stat_ext.mes @@ -29,6 +29,7 @@ {43}{Frost Mage} {44}{Artificer} {45}{Abjurant Champion} +{50}{Divine Oracle} // Psionic Classes {58}{Psion} diff --git a/tpdatasrc/tpgamefiles/scr/tpModifiers/divine_oracle.py b/tpdatasrc/tpgamefiles/scr/tpModifiers/divine_oracle.py new file mode 100644 index 000000000..b5bc8bc5a --- /dev/null +++ b/tpdatasrc/tpgamefiles/scr/tpModifiers/divine_oracle.py @@ -0,0 +1,172 @@ +from templeplus.pymod import PythonModifier +from toee import * +import tpdp +import char_class_utils + +################################################### + +def GetConditionName(): + return "Divine Oracle" + +def GetSpellCasterConditionName(): + return "Divine Oracle Spellcasting" + +print "Registering " + GetConditionName() + +classEnum = stat_level_divine_oracle +classSpecModule = __import__('class050_divine_oracle') + +################################################### + + +#### standard callbacks - BAB and Save values +def OnGetToHitBonusBase(attachee, args, evt_obj): + classLvl = attachee.stat_level_get(classEnum) + babvalue = game.get_bab_for_class(classEnum, classLvl) + evt_obj.bonus_list.add(babvalue, 0, 137) # untyped, description: "Class" + return 0 + +def OnGetSaveThrowFort(attachee, args, evt_obj): + value = char_class_utils.SavingThrowLevel(classEnum, attachee, D20_Save_Fortitude) + evt_obj.bonus_list.add(value, 0, 137) + return 0 + +def OnGetSaveThrowReflex(attachee, args, evt_obj): + value = char_class_utils.SavingThrowLevel(classEnum, attachee, D20_Save_Reflex) + evt_obj.bonus_list.add(value, 0, 137) + return 0 + +def OnGetSaveThrowWill(attachee, args, evt_obj): + value = char_class_utils.SavingThrowLevel(classEnum, attachee, D20_Save_Will) + evt_obj.bonus_list.add(value, 0, 137) + return 0 + +classSpecObj = PythonModifier(GetConditionName(), 0) +classSpecObj.AddHook(ET_OnToHitBonusBase, EK_NONE, OnGetToHitBonusBase, ()) +classSpecObj.AddHook(ET_OnSaveThrowLevel, EK_SAVE_FORTITUDE, OnGetSaveThrowFort, ()) +classSpecObj.AddHook(ET_OnSaveThrowLevel, EK_SAVE_REFLEX, OnGetSaveThrowReflex, ()) +classSpecObj.AddHook(ET_OnSaveThrowLevel, EK_SAVE_WILL, OnGetSaveThrowWill, ()) + + +##### Spell casting +# Divine Oracle raises the caster level for the divine base class specified in Modifier arg 0 + +def OnAddSpellCasting(attachee, args, evt_obj): + if args.get_arg(0) == 0: + args.set_arg(0, char_class_utils.GetHighestDivineClass(attachee)) + return 0 + +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: + return 0 + classLvl = attachee.stat_level_get(classEnum) + if classLvl == 0: + return 0 + evt_obj.bonus_list.add(classLvl, 0, 137) + 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: + return 0 + classLvl = attachee.stat_level_get(classEnum) + if classLvl == 0: + return 0 + evt_obj.bonus_list.add(classLvl, 0, 137) + return 0 + +def OnInitLevelupSpellSelection(attachee, args, evt_obj): + if evt_obj.arg0 != classEnum: + return 0 + class_extended_1 = args.get_arg(0) + classSpecModule.InitSpellSelection(attachee, class_extended_1) + 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) + return 1 + +def OnLevelupSpellsFinalize(attachee, args, evt_obj): + if evt_obj.arg0 != classEnum: + 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, ()) + + +#### Divine Oracle Class Features #### + +## Scry Bonus (Su) ## +# +1 sacred bonus to the save DC of all Divination (Scrying) spells. +def ScryBonusOnGetSpellDcMod(attachee, args, evt_obj): + spell_entry = evt_obj.spell_entry + if spell_entry.spell_school_enum != Divination: + return 0 + if spell_entry.spell_subschool_enum != Scrying: + return 0 + evt_obj.bonus_list.add(1, 17, "Divine Oracle Scry Bonus") # Sacred bonus + return 0 + +scryBonus = PythonModifier("Divine Oracle Scry Bonus", 0) +scryBonus.MapToFeat("Divine Oracle Scry Bonus") +scryBonus.AddHook(ET_OnGetSpellDcMod, EK_NONE, ScryBonusOnGetSpellDcMod, ()) + + +## Oracle Domain ## +# Grants the Oracle domain granted power: cast Divination spells at +2 caster level. +def OracleDomainCasterLevelMod(attachee, args, evt_obj): + spellPkt = evt_obj.get_spell_packet() + if spellPkt is None: + return 0 + spell_enum = spellPkt.spell_enum + if spell_enum <= 0: + return 0 + spell_entry = tpdp.SpellEntry(spell_enum) + if spell_entry.spell_school_enum != Divination: + return 0 + evt_obj.return_val += 2 + return 0 + +oracleDomain = PythonModifier("Divine Oracle Oracle Domain", 0) +oracleDomain.MapToFeat("Divine Oracle Oracle Domain") +oracleDomain.AddHook(ET_OnGetCasterLevelMod, EK_NONE, OracleDomainCasterLevelMod, ()) + + +## Prescient Sense (Ex) ## +# Marker feat. Mechanical effect is granted via feat_evasion in the class spec +# class_feats dict at level 2. Listed here so the feature appears on the sheet. +prescientSense = PythonModifier("Divine Oracle Prescient Sense", 0) +prescientSense.MapToFeat("Divine Oracle Prescient Sense") + + +## Divination Enhancement (Ex) ## +# Roll twice and take the better result when casting divination spells such as +# augury or divination. ToEE has no engine support for re-rolling such spell +# outcomes, so the feature is registered as a marker. +divinationEnhancement = PythonModifier("Divine Oracle Divination Enhancement", 0) +divinationEnhancement.MapToFeat("Divine Oracle Divination Enhancement") + + +## Immune to Surprise (Ex) ## +# Marker feat for the character sheet. Mechanical effect is implemented in +# C++ at TurnBasedSys add-to-initiative time: Divine Oracle 10+ characters +# skip the engine "Surprised" condition entirely (turn_based.cpp). +immuneSurprise = PythonModifier("Divine Oracle Immune to Surprise", 0) +immuneSurprise.MapToFeat("Divine Oracle Immune to Surprise")