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")