Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
TemplePlus.ini
.claude/

# Source reference material (copyrighted) kept locally, never committed
/Divine O.pdf

# C++ objects and libs

Expand Down
127 changes: 127 additions & 0 deletions DIVINE_ORACLE_INSTALL.txt
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions PackRelease.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions TemplePlus.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<file src="Release\FontAwesome.WPF.dll" target="lib\net45" />
<file src="Release\DeltaCompressionDotNet*.dll" target="lib\net45" />
<file src="Release\TemplePlus.exe" target="lib\net45" />
<file src="DIVINE_ORACLE_INSTALL.txt" target="lib\net45" />
<file src="tpdata\**\*" target="lib\net45\tpdata" />
<file src="dependencies\python-lib\**\*" target="lib\net45\tpdata\python-lib" />
<file src="C:\Program Files (x86)\Windows Kits\10\Redist\D3D\x86\d3dcompiler_47.dll" target="lib\net45" />
Expand Down
6 changes: 5 additions & 1 deletion TemplePlus/d20_status.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions TemplePlus/gamesystems/d20/d20stats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
43 changes: 42 additions & 1 deletion TemplePlus/spell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,11 @@ class SpellFuncReplacements : public TempleFix {
// SpellEntriesInit
replaceFunction<BOOL(__cdecl)(const char*)>(0x1007B5B0, [](const char* spellRulesFolder)->BOOL
{
return spellSys.SpellEntriesInit(spellRulesFolder);
BOOL result = spellSys.SpellEntriesInit(spellRulesFolder);
if (result) {
spellSys.AddOracleDomainSpells();
}
return result;
});


Expand Down Expand Up @@ -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<int, int> 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 <typename T>
Expand Down
5 changes: 5 additions & 0 deletions TemplePlus/spell.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, std::vector<SpellMultiOption>> mMultiOptions;
void GetSpellEntryExtFromClassSpec(std::map<int, int>& mapping, int classEnum);

Expand Down
4 changes: 3 additions & 1 deletion TemplePlus/temple_enums.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion TemplePlus/turn_based.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,15 @@ void TurnBasedSys::AddToInitiative(objHndl handle)
conds.AddTo(handle, "Flatfooted", {});
auto isSurpriseRound = temple::GetRef<BOOL>(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<void(__cdecl)(objHndl)>(0x100DF1E0);
Expand Down
1 change: 1 addition & 0 deletions tpdata/templeplus/lib/templeplus/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tpdata/tprules/class_specs.tab
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ PLACEHOLDER 46
PLACEHOLDER 47
PLACEHOLDER 48
PLACEHOLDER 49
PLACEHOLDER 50
Divine Oracle 50
PLACEHOLDER 51
PLACEHOLDER 52
PLACEHOLDER 53
Expand Down
11 changes: 11 additions & 0 deletions tpdatasrc/tpgamefiles/mes/help/divine_oracle_help.tab
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions tpdatasrc/tpgamefiles/mes/spell_ext.mes
Original file line number Diff line number Diff line change
@@ -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}
Expand Down
Loading
Loading