diff --git a/TemplePlus/bonusspells.cpp b/TemplePlus/bonusspells.cpp index f2e73876f..47c38990e 100644 --- a/TemplePlus/bonusspells.cpp +++ b/TemplePlus/bonusspells.cpp @@ -5,6 +5,7 @@ #include "temple_functions.h" #include "obj.h" #include "gamesystems/objects/objsystem.h" +#include void removeSurplusSpells(int surplus, objHndl objHnd, uint32_t classCode, int slotLvl) @@ -45,18 +46,17 @@ int getMemorizedSpells(objHndl objHnd, uint32_t classCode, int slotLvl) int getMaxSpells(objHndl objHnd, uint32_t classCode, int slotLvl, uint32_t classLvl) { - uint32_t maxSpells = spellSys.getBaseSpellCountByClassLvl(classCode, classLvl, slotLvl, 0); - if (maxSpells) - { - if ( spellSys.getWizSchool(objHnd) ) - { + int maxSpells = spellSys.GetNumSpellsPerDay(objHnd, (Stat)classCode, slotLvl); // spellSys.getBaseSpellCountByClassLvl(classCode, classLvl, slotLvl, 0); + + if (maxSpells >=0 ){ + if ( spellSys.getWizSchool(objHnd) ){ maxSpells++; } - maxSpells += spellSys.getStatModBonusSpellCount(objHnd, classCode, slotLvl); + // maxSpells += spellSys.getStatModBonusSpellCount(objHnd, classCode, slotLvl); return maxSpells; } - else if ( d20ClassSys.IsLateCastingClass((Stat)classCode) + /*else if ( d20ClassSys.IsLateCastingClass((Stat)classCode) && (classLvl >= 4 && slotLvl == 1 || classLvl >= 8 && slotLvl == 2 || classLvl >= 11 && slotLvl == 3 @@ -64,7 +64,7 @@ int getMaxSpells(objHndl objHnd, uint32_t classCode, int slotLvl, uint32_t class { maxSpells += spellSys.getStatModBonusSpellCount(objHnd, classCode, slotLvl); return maxSpells; - } + }*/ return 0; diff --git a/TemplePlus/d20.cpp b/TemplePlus/d20.cpp index e9df16284..a76ee7ab6 100644 --- a/TemplePlus/d20.cpp +++ b/TemplePlus/d20.cpp @@ -2218,7 +2218,7 @@ ActionErrorCode D20ActionCallbacks::ActionCheckCastSpell(D20Actn* d20a, TurnBase if (d20ClassSys.IsNaturalCastingClass(classCode)) while (true) { - auto spellsPerDay = spellSys.GetSpellsPerDay(d20a->d20APerformer, classCode, spellLvl); + auto spellsPerDay = spellSys.GetNumSpellsPerDay(d20a->d20APerformer, classCode, spellLvl); auto spellsCastNum = spellSys.NumSpellsInLevel(d20a->d20APerformer, obj_f_critter_spells_cast_idx, spellClass, spellLvl); if (spellsCastNum < spellsPerDay) { diff --git a/TemplePlus/d20_class.cpp b/TemplePlus/d20_class.cpp index a580faa3a..e2db9da45 100644 --- a/TemplePlus/d20_class.cpp +++ b/TemplePlus/d20_class.cpp @@ -170,6 +170,23 @@ bool D20ClassSystem::IsArcaneCastingClass(Stat classCode, objHndl handle){ return false; } +bool D20ClassSystem::IsDivineCastingClass(Stat classCode, objHndl handle) +{ + auto classSpec = classSpecs.find(classCode); + if (classSpec == classSpecs.end()) + return false; + + if (classSpec->second.spellListType == SpellListType::Divine + || classSpec->second.spellListType == SpellListType::Clerical + || classSpec->second.spellListType == SpellListType::Druidic + || classSpec->second.spellListType == SpellListType::Ranger + || classSpec->second.spellListType == SpellListType::Paladin + ) + return true; + + return false; +} + bool D20ClassSystem::HasDomainSpells(Stat classEnum){ if (classEnum == stat_level_cleric) return true; @@ -436,8 +453,13 @@ BOOL D20ClassSystem::IsClassSkill(SkillEnum skillEnum, Stat classCode){ } bool D20ClassSystem::LevelupSpellsCheckComplete(objHndl handle, Stat classEnum){ - - return pythonClassIntegration.LevelupSpellsCheckComplete(handle, classEnum); + if (objects.StatLevelGet(handle, classEnum)){ + auto result = dispatch.DispatchLevelupSystemEvent(handle, classEnum, DK_LVL_Spells_Check_Complete); + return result >= 0; + } + + else + return pythonClassIntegration.LevelupSpellsCheckComplete(handle, classEnum); } void D20ClassSystem::LevelupSpellsFinalize(objHndl handle, Stat classEnum){ diff --git a/TemplePlus/d20_class.h b/TemplePlus/d20_class.h index 25d78b86c..5bf3a6d4a 100644 --- a/TemplePlus/d20_class.h +++ b/TemplePlus/d20_class.h @@ -88,6 +88,7 @@ struct D20ClassSystem : temple::AddressTable bool IsCastingClass(Stat classEnum); bool IsLateCastingClass(Stat classEnum); // for classes like Ranger / Paladin that start casting on level 4 bool IsArcaneCastingClass(Stat stat, objHndl handle = objHndl::null); + bool IsDivineCastingClass(Stat stat, objHndl handle = objHndl::null); static bool HasDomainSpells(Stat classEnum); Stat GetSpellStat(Stat classEnum); // default - wisdom int GetMaxSpellLevel(Stat classEnum, int characterLvl); @@ -143,7 +144,7 @@ struct D20ClassSystem : temple::AddressTable int ClericMaxSpellLvl(uint32_t clericLvl) const; int NumDomainSpellsKnownFromClass(objHndl dude, Stat classCode); - int GetNumSpellsFromClass(objHndl obj, Stat classCode, int spellLvl, uint32_t classLvl, bool getFromStatMod = true); + int GetNumSpellsFromClass(objHndl obj, Stat classCode, int spellLvl, uint32_t classLvl, bool getFromStatMod = true); // returns -1 if none; must have >=0 spells per day before taking into account the stat mod // skills BOOL IsClassSkill(SkillEnum skillEnum, Stat classCode); diff --git a/TemplePlus/feat.cpp b/TemplePlus/feat.cpp index e44cfd3eb..5e34a04ac 100644 --- a/TemplePlus/feat.cpp +++ b/TemplePlus/feat.cpp @@ -346,7 +346,7 @@ int LegacyFeatSystem::IsMagicFeat(feat_enums feat) int LegacyFeatSystem::IsFeatPartOfMultiselect(feat_enums feat) { - return ( m_featPropertiesTable[feat] & 0x100 ) != 0; + return ( m_featPropertiesTable[feat] & FPF_MULTI_SELECT_ITEM ) != 0; } int LegacyFeatSystem::IsFeatRacialOrClassAutomatic(feat_enums feat) @@ -380,7 +380,7 @@ int LegacyFeatSystem::IsFeatPropertySet(feat_enums feat, int featProp) } bool LegacyFeatSystem::IsFeatMultiSelectMaster(feat_enums feat){ - return IsFeatPropertySet(feat, 0x80000) != 0; + return IsFeatPropertySet(feat, FPF_MULTI_MASTER) != 0; }; #pragma endregion @@ -678,53 +678,30 @@ uint32_t _FeatPrereqsCheck(objHndl objHnd, feat_enums featIdx, feat_enums * feat FeatPrereqRow * featPrereqs = feats.m_featPreReqTable; const uint8_t numCasterClasses = 7; uint32_t casterClassCodes[numCasterClasses] = { stat_level_bard, stat_level_cleric, stat_level_druid, stat_level_paladin, stat_level_ranger, stat_level_sorcerer, stat_level_wizard }; - - if (featProps & 2 && !(featProps & 524290)) - { + + if (!feats.IsFeatEnabled(featIdx) && !feats.IsFeatMultiSelectMaster(featIdx)){ return 0; } - //return 1; // h4x :) - -#pragma region checking feats in the character editor - SpellSlinger hack for special Rogue feats for level > 10 - if ( ui.CharEditorIsActive() && *feats.charEditorClassCode != 0 && *feats.charEditorObjHnd) - { - auto newClassLvl = objects.StatLevelGet(*feats.charEditorObjHnd, *feats.charEditorClassCode) + 1; - if (classCodeBeingLevelledUp == stat_level_rogue) - { - if (newClassLvl == 10 || newClassLvl == 13 || newClassLvl == 16 || newClassLvl == 19) - { - if (featProps & featPropRogueFeat) - { - return 1; - } - } + // checking feats in the character editor - SpellSlinger hack for special Rogue feats for level > 10 + if ( classCodeBeingLevelledUp == stat_level_rogue && objHnd && feats.IsFeatPropertySet(featIdx, FPF_ROGUE_BONUS)){ + auto newClassLvl = objects.StatLevelGet(objHnd, stat_level_rogue) + 1; + if (newClassLvl == 10 || newClassLvl == 13 || newClassLvl == 16 || newClassLvl == 19){ + return 1; } } -#pragma endregion - uint32_t initOffset = featIdx * 16; - if (featIdx == FEAT_GREATER_TWO_WEAPON_FIGHTING) - { - int bpDummy = 1; - } + if (featPrereqs[featIdx].featPrereqs[0].featPrereqCode == featReqCodeTerminator){ return 1; }; - for (uint32_t i = 0; featPrereqs[featIdx].featPrereqs[i].featPrereqCode != featReqCodeTerminator; i += 1) - { - //disassm notes: - // eax is classCodeBeingLevelledUp - // esi is featReqCode - // ecx is featIdx + for (uint32_t i = 0; featPrereqs[featIdx].featPrereqs[i].featPrereqCode != featReqCodeTerminator; i += 1){ int32_t featReqCode = featPrereqs[featIdx].featPrereqs[i].featPrereqCode; auto featReqCodeArg = featPrereqs[featIdx].featPrereqs[i].featPrereqCodeArg; - uint32_t var_2C = 0; - - + uint32_t var_2C = 0; if (featReqCode == featReqCodeMinCasterLevel) { diff --git a/TemplePlus/feat.h b/TemplePlus/feat.h index bdb85d2f9..907f7109e 100644 --- a/TemplePlus/feat.h +++ b/TemplePlus/feat.h @@ -5,6 +5,31 @@ #define NUM_FEATS 750 // vanilla was 649 (and Moebius hack increased this to 664 I think) #include "tig/tig_mes.h" +enum FeatPropertyFlag : uint32_t { + FPF_CAN_GAIN_MULTIPLE_TIMES = 0x1, + FPF_DISABLED = 0x2, + FPF_RACE_AUTOMATIC = 0x4, + FPF_CLASS_AUTMATIC = 0x8 , + FPF_FIGHTER_BONUS = 0x10, + FPF_MONK_BONUS_1st = 0x20, + FPF_MONK_BONUS_2nd = 0x40, + FPF_MONK_BONUS_6th = 0x80, + FPF_MULTI_SELECT_ITEM = 0x100, + FPF_EXOTIC_WEAP_ITEM = 0x300, + FPF_IMPR_CRIT_ITEM = 0x500, + FPF_MARTIAL_WEAP_ITEM = 0x900, + FPF_SKILL_FOCUS_ITEM = 0x1100, + FPF_WEAP_FINESSE_ITEM = 0x2100, + FPF_WEAP_FOCUS_ITEM = 0x4100, + FPF_WEAP_SPEC_ITEM = 0x8100, + FPF_GREATER_WEAP_FOCUS_ITEM = 0x10100, + FPF_WIZARD_BONUS = 0x20000, + FPF_ROGUE_BONUS = 0x40000, // rogue bonus at 10th level + + FPF_MULTI_MASTER = 0x80000, // head of multiselect class of feats (NEW) + FPF_GREAT_WEAP_SPEC_ITEM = 0x100100, // NEW +}; + struct FeatPrereqRow; struct ClassFeatTableEntry diff --git a/TemplePlus/fonts/fonts_layout.cpp b/TemplePlus/fonts/fonts_layout.cpp index bc985c955..2f19fff05 100644 --- a/TemplePlus/fonts/fonts_layout.cpp +++ b/TemplePlus/fonts/fonts_layout.cpp @@ -643,12 +643,14 @@ uint32_t TextLayouter::CountLinesVanilla(uint32_t maxWidth, uint32_t maxLines, c // Measure the length of the current word for (; i < length; i++) { ch = text[i]; - + if (ch == '’') // fix for this character that sometimes appears in vanilla + ch = '\''; // Skip @[0-9] if (ch == '@' && i + 1 < length && text[i + 1] >= '0' && text[i + 1] <= '9') { i++; continue; } + if (isspace(ch)) { break; diff --git a/TemplePlus/python/python_object.cpp b/TemplePlus/python/python_object.cpp index a42ec3c94..092d877a6 100644 --- a/TemplePlus/python/python_object.cpp +++ b/TemplePlus/python/python_object.cpp @@ -821,21 +821,16 @@ static PyObject* PyObjHandle_ArcaneSpellLevelCanCast(PyObject* obj, PyObject* ar auto arcaneSpellLvlMax = 0; - auto wizLvl = objects.StatLevelGet(self->handle, stat_level_wizard); - if (wizLvl > 0){ - arcaneSpellLvlMax = (1 + wizLvl) / 2; - } + critterSys.GetSpellLvlCanCast(self->handle, SpellSourceType::Arcane, SpellReadyingType::Any); - auto sorcLvl = objects.StatLevelGet(self->handle, stat_level_sorcerer); - if (sorcLvl > 0){ - auto sorcSpellLvlMax = max(1, sorcLvl / 2); - if (sorcSpellLvlMax > arcaneSpellLvlMax) - arcaneSpellLvlMax = sorcSpellLvlMax; + for (auto it: d20ClassSys.classEnums){ + auto classEnum = (Stat)it; + if (d20ClassSys.IsArcaneCastingClass(classEnum)){ + arcaneSpellLvlMax = max(arcaneSpellLvlMax , spellSys.GetMaxSpellLevel(self->handle, classEnum, 0)); + } } + - auto bardLvl = objects.StatLevelGet(self->handle, stat_level_bard); - critterSys.GetSpellLvlCanCast(self->handle, SpellSourceType::Arcane, SpellReadyingType::Any); - // todo: generalize return PyInt_FromLong(arcaneSpellLvlMax); } @@ -848,20 +843,15 @@ static PyObject* PyObjHandle_DivineSpellLevelCanCast(PyObject* obj, PyObject* ar auto divineSpellLvlMax = 0; - auto clrLvl = objects.StatLevelGet(self->handle, stat_level_cleric); - if (clrLvl > 0) { - divineSpellLvlMax = (1 + clrLvl) / 2; - } - - auto drdLvl = objects.StatLevelGet(self->handle, stat_level_druid); - if (drdLvl > 0) { - if (drdLvl > clrLvl) - divineSpellLvlMax = (1 + drdLvl) / 2; + critterSys.GetSpellLvlCanCast(self->handle, SpellSourceType::Divine, SpellReadyingType::Any); + + for (auto it : d20ClassSys.classEnums) { + auto classEnum = (Stat)it; + if (d20ClassSys.IsDivineCastingClass(classEnum)) { + divineSpellLvlMax = max(divineSpellLvlMax, spellSys.GetMaxSpellLevel(self->handle, classEnum, 0)); + } } - auto palLvl = objects.StatLevelGet(self->handle, stat_level_paladin); - critterSys.GetSpellLvlCanCast(self->handle, SpellSourceType::Divine, SpellReadyingType::Any); - // todo: generalize return PyInt_FromLong(divineSpellLvlMax); } diff --git a/TemplePlus/radialmenu.cpp b/TemplePlus/radialmenu.cpp index 276279162..e6cfe51ce 100644 --- a/TemplePlus/radialmenu.cpp +++ b/TemplePlus/radialmenu.cpp @@ -144,7 +144,7 @@ class RadialMenuReplacements : public TempleFix } } auto spellClass = spellSys.GetSpellClass(classCode); - auto numSpellsPerDay = spellSys.GetSpellsPerDay(handle, classCode, spLvl); + auto numSpellsPerDay = spellSys.GetNumSpellsPerDay(handle, classCode, spLvl); if (numSpellsPerDay < 0) numSpellsPerDay = 0; diff --git a/TemplePlus/spell.cpp b/TemplePlus/spell.cpp index 174079cde..8ad680366 100644 --- a/TemplePlus/spell.cpp +++ b/TemplePlus/spell.cpp @@ -459,7 +459,7 @@ int LegacySpellSystem::GetMaxSpellLevel(objHndl objHnd, Stat classCode, int char //return addresses.GetMaxSpellSlotLevel(objHnd, classCode, characterLvl); } -int LegacySpellSystem::GetSpellsPerDay(objHndl handle, Stat classCode, int spellLvl){ +int LegacySpellSystem::GetNumSpellsPerDay(objHndl handle, Stat classCode, int spellLvl){ auto spellClass = spellSys.GetSpellClass(classCode); auto effLvl = critterSys.GetSpellListLevelExtension(handle, classCode) + objects.StatLevelGet(handle, classCode); return d20ClassSys.GetNumSpellsFromClass(handle, classCode, spellLvl, effLvl); diff --git a/TemplePlus/spell.h b/TemplePlus/spell.h index 0b02f65f3..16dff886f 100644 --- a/TemplePlus/spell.h +++ b/TemplePlus/spell.h @@ -163,8 +163,8 @@ struct LegacySpellSystem : temple::AddressTable uint32_t spellRegistryCopy(uint32_t spellEnum, SpellEntry* spellEntry); int CopyLearnableSpells(objHndl & handle, int spellClass, std::vector & entries); uint32_t ConfigSpellTargetting(PickerArgs* pickerArgs, SpellPacketBody* spellPacketBody); - int GetMaxSpellLevel(objHndl objHnd, Stat classCode, int characterLvl); - int GetSpellsPerDay(objHndl handle, Stat classCode, int spellLvl); + int GetMaxSpellLevel(objHndl objHnd, Stat classCode, int characterLvl = 0); // if characterLvl is 0 it will fetch the actual level; it also takes into account spell list extension by PrC's and such + int GetNumSpellsPerDay(objHndl handle, Stat classCode, int spellLvl); // including from spell list extension int ParseSpellSpecString(SpellStoreData* spell, char* spellString); const char* GetSpellMesline(uint32_t line) const; diff --git a/TemplePlus/ui/ui_char.cpp b/TemplePlus/ui/ui_char.cpp index 0fce9409b..ec63cac50 100644 --- a/TemplePlus/ui/ui_char.cpp +++ b/TemplePlus/ui/ui_char.cpp @@ -330,7 +330,7 @@ void CharUiSystem::SpellsShow(objHndl obj) // find which caster class tabs should appear for (int i = 0; i < VANILLA_NUM_CLASSES ; i++){ auto classCode = d20ClassSys.vanillaClassEnums[i]; - auto spellClassCode = (classCode & 0x7F) | 0x80; + auto spellClassCode = spellSys.GetSpellClass(classCode); if (!d20ClassSys.IsCastingClass(classCode)) continue; @@ -349,10 +349,10 @@ void CharUiSystem::SpellsShow(objHndl obj) ui.WidgetSetHidden(navTabBtn->widgetId, 0); navClassPackets[uiCharSpellTabsCount].spellClassCode = spellClassCode; - + int n1 = 0; for (int spellLvl = 0; spellLvl < 10; spellLvl++){ - int numSpells = d20ClassSys.GetNumSpellsFromClass(dude, classCode, spellLvl, classLvl); + int numSpells = spellSys.GetNumSpellsPerDay(dude, classCode, spellLvl); //d20ClassSys.GetNumSpellsFromClass(dude, classCode, spellLvl, classLvl); if (numSpells > 0) n1 += numSpells; } @@ -439,12 +439,12 @@ void CharUiSystem::SpellsShow(objHndl obj) if (!spellSys.isDomainSpell(spellClassCode)){ //normal casting class //LevelPacket lvlPkt; auto classCode = spellSys.GetCastingClass(spellClassCode); - auto classLvl = objects.StatLevelGet(dude, classCode); - classLvl += critterSys.GetSpellListLevelExtension(dude, classCode); + //auto classLvl = objects.StatLevelGet(dude, classCode); + //classLvl += critterSys.GetSpellListLevelExtension(dude, classCode); //lvlPkt.GetLevelPacket(classCode, dude, 0, classLvl); auto numSpellsForLvl = navClassPackets[i].numSpellsForLvl; for (int j = 0; j < NUM_SPELL_LEVELS; j++){ - auto numSp = d20ClassSys.GetNumSpellsFromClass(dude, classCode, j, classLvl); + auto numSp = spellSys.GetNumSpellsPerDay(dude, classCode, j); //d20ClassSys.GetNumSpellsFromClass(dude, classCode, j, classLvl); if (numSp >= 0){ //(lvlPkt.spellCountFromClass[j] >= 0){ numSpellsForLvl[j] = numSp; //lvlPkt.spellCountBonusFromStatMod[j] + lvlPkt.spellCountFromClass[j]; if (numSpellsForLvl[j] > 0 && classCode == stat_level_wizard) { diff --git a/TemplePlus/ui/ui_char_editor.cpp b/TemplePlus/ui/ui_char_editor.cpp index 8384f975c..f05bc9850 100644 --- a/TemplePlus/ui/ui_char_editor.cpp +++ b/TemplePlus/ui/ui_char_editor.cpp @@ -33,6 +33,7 @@ #include #include #include +#include namespace py = pybind11; using namespace pybind11; @@ -200,10 +201,10 @@ class UiCharEditor { WidgetType3 featsScrollbar, featsExistingScrollbar, featsMultiSelectScrollbar; eastl::vector featsAvailBtnIds, featsExistingBtnIds, featsMultiSelectBtnIds; int featsMultiOkBtnId = 0, featsMultiCancelBtnId = 0; - const int FEATS_AVAIL_BTN_COUNT = 18; - const int FEATS_AVAIL_BTN_HEIGHT = 11; - const int FEATS_EXISTING_BTN_COUNT = 11; - const int FEATS_EXISTING_BTN_HEIGHT = 12; + const int FEATS_AVAIL_BTN_COUNT = 17; // vanilla 18 + const int FEATS_AVAIL_BTN_HEIGHT = 12; // vanilla 11 + const int FEATS_EXISTING_BTN_COUNT = 10; // vanilla 11 + const int FEATS_EXISTING_BTN_HEIGHT = 13; // vanilla 12 const int FEATS_MULTI_BTN_COUNT = 15; const int FEATS_MULTI_BTN_HEIGHT = 12; std::string featsAvailTitleString, featsExistingTitleString; @@ -287,7 +288,7 @@ class UiCharEditor { bool mFeatsActivated = false; bool mIsSelectingBonusFeat = false; bool mBonusFeatOk = false; - feat_enums featsMultiSelected = FEAT_NONE; + feat_enums featsMultiSelected = FEAT_NONE, mFeatsMultiMasterFeat = FEAT_NONE; std::unique_ptr mClass; std::vector mSpellInfo; @@ -430,6 +431,12 @@ PYBIND11_PLUGIN(tp_char_editor){ { d20ClassSys.LevelupSpellsFinalize(handle, (Stat)classEnum); }) + .def("spells_check_complete", [](objHndl handle, int classEnum)->int{ + if (d20ClassSys.IsSelectingSpellsOnLevelup(handle, (Stat)classEnum)) + return d20ClassSys.LevelupSpellsCheckComplete(handle, (Stat)classEnum); + else + return 1; + }) .def("get_max_spell_level", [](const objHndl & handle, int classEnum, int characterLvl) { return spellSys.GetMaxSpellLevel(handle, (Stat)classEnum, characterLvl); @@ -787,7 +794,7 @@ BOOL UiCharEditor::FeatsSystemInit(GameSystemConf & conf){ featsTitleString.append(mesline.value); // Class Bonus title - mesline.key = 19001; + mesline.key = 19003; mesFuncs.GetLine_Safe(pcCreationMes, &mesline); featsClassBonusTitleString.append(mesline.value); @@ -1016,9 +1023,9 @@ BOOL UiCharEditor::FeatsWidgetsInit(int w, int h) { featsMultiBtnRects.push_back(TigRect(featMultiBtn.x, featMultiBtn.y, featMultiBtn.width, featMultiBtn.height)); } - featsAvailTitleRect = TigRect(7,24,185, 10 ); + featsAvailTitleRect = TigRect(7,22,185, 10 ); featsTitleRect = TigRect(206, 27, 185, 10); - featsExistingTitleRect =TigRect(206, 105, 185, 10); + featsExistingTitleRect =TigRect(206, 103, 185, 10); featsClassBonusRect = TigRect(206, 65, 185, 10); // Selectable feats @@ -1026,7 +1033,7 @@ BOOL UiCharEditor::FeatsWidgetsInit(int w, int h) { featsBtnRects.clear(); for (auto i = 0; i < FEATS_AVAIL_BTN_COUNT; i++) { int newId = 0; - WidgetType2 featsAvailBtn("Feats Available btn", featsMainWndId, 6, 40 + i*(FEATS_AVAIL_BTN_HEIGHT + 1), 169, FEATS_AVAIL_BTN_HEIGHT); + WidgetType2 featsAvailBtn("Feats Available btn", featsMainWndId, 7, 38 + i*(FEATS_AVAIL_BTN_HEIGHT + 1), 169, FEATS_AVAIL_BTN_HEIGHT); featsAvailBtn.x += featsMainWnd.x; featsAvailBtn.y += featsMainWnd.y; featsAvailBtn.render = [](int id) {uiCharEditor.FeatsEntryBtnRender(id); }; @@ -1175,9 +1182,10 @@ void UiCharEditor::FeatsActivate(){ ui.WidgetSet(featsExistingScrollbarId, &featsExistingScrollbar); // Available feats + mSelectableFeats.clear(); for (auto i = 0u; i < NUM_FEATS; i++){ auto feat = (feat_enums)i; - if (!feats.IsFeatEnabled(feat)) + if (!feats.IsFeatEnabled(feat) && !feats.IsFeatMultiSelectMaster(feat)) continue; if (feats.IsFeatRacialOrClassAutomatic(feat)) continue; @@ -1620,6 +1628,9 @@ void UiCharEditor::ClassPrevBtnRender(int widId){ UiRenderer::PopFont(); } + +#pragma region Feats + void UiCharEditor::FeatsWndRender(int widId){ auto &selPkt = GetCharEditorSelPacket(); @@ -1679,6 +1690,7 @@ BOOL UiCharEditor::FeatsWndMsg(int widId, TigMsg * msg) if (msgM->buttonStateFlags & MouseStateFlags::MSF_RMB_RELEASED && ui.IsWidgetHidden(featsMultiSelectWndId)) { + bool dumy = 1; auto putFeat = false; feat_enums feat; @@ -1699,25 +1711,30 @@ BOOL UiCharEditor::FeatsWndMsg(int widId, TigMsg * msg) putFeat = true; break; } - else if (IsSelectingBonusFeat() && selPkt.feat2 == FEAT_NONE) + else if (IsSelectingBonusFeat() && IsClassBonusFeat(feat) && selPkt.feat2 == FEAT_NONE) { selPkt.feat2 = feat; putFeat = true; break; } } + if (putFeat){ if (feats.IsFeatMultiSelectMaster(feat)){ FeatsMultiSelectActivate(feat); } FeatsSanitize(); + if (feat == FEAT_SKILL_MASTERY && selPkt.feat2 == feat){ + auto skillMasteryActivate = temple::GetRef(0x1016C2B0); + skillMasteryActivate(GetEditedChar(), temple::GetRef(0x101A86D0)); + } } - if (featsSelectedBorderRect.ContainsPoint(msgM->x, msgM->y)){ + else if (featsSelectedBorderRect.ContainsPoint(msgM->x, msgM->y)){ selPkt.feat0 = FEAT_NONE; } - else if (featsClassBonusRect.ContainsPoint(msgM->x, msgM->y) && IsSelectingBonusFeat()){ + else if (featsClassBonusBorderRect.ContainsPoint(msgM->x, msgM->y) && IsSelectingBonusFeat()){ selPkt.feat2 = FEAT_NONE; } @@ -1802,6 +1819,10 @@ BOOL UiCharEditor::FeatsEntryBtnMsg(int widId, TigMsg * msg){ selPkt.feat2 = feat; if (feats.IsFeatMultiSelectMaster(feat)) FeatsMultiSelectActivate(feat); + else if (feat == FEAT_SKILL_MASTERY){ + auto skillMasteryActivate = temple::GetRef(0x1016C2B0); + skillMasteryActivate(GetEditedChar(), temple::GetRef(0x101A86D0)); + } } FeatsSanitize(); return TRUE; @@ -1820,9 +1841,6 @@ BOOL UiCharEditor::FeatsEntryBtnMsg(int widId, TigMsg * msg){ } - - - void UiCharEditor::FeatsEntryBtnRender(int widId){ auto widIdx = ui.WidgetlistIndexof(widId, &featsAvailBtnIds[0], FEATS_AVAIL_BTN_COUNT); @@ -1842,229 +1860,808 @@ void UiCharEditor::FeatsEntryBtnRender(int widId){ BOOL UiCharEditor::FeatsExistingBtnMsg(int widId, TigMsg* msg) { - return 0; -} + if (msg->type != TigMsgType::WIDGET) + return 0; + auto msgW = (TigMsgWidget*)msg; -void UiCharEditor::FeatsExistingBtnRender(int widId){ + auto widIdx = ui.WidgetlistIndexof(widId, &featsExistingBtnIds[0], FEATS_EXISTING_BTN_COUNT); + auto featIdx = widIdx + featsExistingScrollbarY; + if (widIdx == -1 || featIdx >= (int)mExistingFeats.size()) + return FALSE; -} + auto featInfo = mExistingFeats[featIdx]; + auto feat = (feat_enums)featInfo.featEnum; -void UiCharEditor::FeatsMultiSelectWndRender(int widId) -{ -} + auto &selPkt = GetCharEditorSelPacket(); + auto btn = ui.GetButton(widId); -BOOL UiCharEditor::FeatsMultiSelectWndMsg(int widId, TigMsg * msg) -{ - return 0; -} + switch (msgW->widgetEventType) { + case TigMsgWidgetEvent::Entered: + temple::GetRef(0x10162A10)(FeatsMultiGetFirst(feat), temple::GetRef(0x10C76B48), 1024u); // UiTooltipSetForFeat + temple::GetRef(0x10162C00)(temple::GetRef(0x10C76B48)); // UiCharTextboxSet + return TRUE; + case TigMsgWidgetEvent::Exited: + temple::GetRef(0x10162C00)(""); // UiCharTextboxSet + return TRUE; + default: + return FALSE; -void UiCharEditor::FeatsMultiOkBtnRender(int widId) -{ + } + return TRUE; } -BOOL UiCharEditor::FeatsMultiOkBtnMsg(int widId, TigMsg * msg) -{ - return 0; -} +void UiCharEditor::FeatsExistingBtnRender(int widId){ + auto widIdx = ui.WidgetlistIndexof(widId, &featsExistingBtnIds[0], FEATS_EXISTING_BTN_COUNT); + auto featIdx = widIdx + featsExistingScrollbarY; + if (widIdx == -1 || featIdx >= (int)mExistingFeats.size()) + return; -void UiCharEditor::FeatsMultiCancelBtnRender(int widId) -{ -} + auto featInfo = mExistingFeats[featIdx]; + auto feat = (feat_enums)featInfo.featEnum; -BOOL UiCharEditor::FeatsMultiCancelBtnMsg(int widId, TigMsg * msg) -{ - return 0; -} + UiRenderer::PushFont(PredefinedFont::PRIORY_12); -void UiCharEditor::FeatsMultiBtnRender(int widId) -{ -} + UiRenderer::DrawTextInWidget(featsMainWndId, GetFeatName(feat), featsExistingBtnRects[widIdx], featsClassStyle); -BOOL UiCharEditor::FeatsMultiBtnMsg(int widId, TigMsg* msg) -{ - return 0; + UiRenderer::PopFont(); } -void UiCharEditor::SpellsWndRender(int widId){ - UiRenderer::PushFont(PredefinedFont::PRIORY_12); - UiRenderer::DrawTextInWidget(widId, spellsAvailLabel, spellsAvailTitleRect, spellsTitleStyle); - UiRenderer::DrawTextInWidget(widId, spellsChosenLabel, spellsChosenTitleRect, spellsTitleStyle); - - - levelupSpellbar->SetX(spellsWnd.x); - levelupSpellbar->SetY(spellsWnd.y + 275); - levelupSpellbar->Render(); +void UiCharEditor::FeatsMultiSelectActivate(feat_enums feat) { - // RenderSpellsPerDay - UiRenderer::DrawTextInWidget(widId, spellsPerDayLabel, spellsPerDayTitleRect, spellsTextStyle); - for (auto i = 0; i < SPELLS_PER_DAY_BOXES_COUNT; i++){ - UiRenderer::DrawTextInWidget(widId, spellsPerDayTexts[i], spellsPerDayTextRects[i], spellsPerDayStyle); - } + if (!FeatCanPick(feat)) + return; - UiRenderer::PopFont(); + auto &selPkt = GetCharEditorSelPacket(); + if (feat == FEAT_WEAPON_FINESSE) { + if (selPkt.feat0 == FEAT_WEAPON_FINESSE) + selPkt.feat0 = FEAT_WEAPON_FINESSE_DAGGER; + if (selPkt.feat1 == FEAT_WEAPON_FINESSE) + selPkt.feat1 = FEAT_WEAPON_FINESSE_DAGGER; + if (selPkt.feat2 == FEAT_WEAPON_FINESSE) + selPkt.feat2 = FEAT_WEAPON_FINESSE_DAGGER; + return; + } - // Rects - RenderHooks::RenderRectInt(spellsWnd.x , spellsWnd.y + 38, 195, 227, 0xFF5D5D5D); - RenderHooks::RenderRectInt(spellsWnd.x + 201, spellsWnd.y + 38, 195, 227, 0xFF5D5D5D); + mFeatsMultiMasterFeat = feat; + featsMultiSelected = FEAT_NONE; - StateTitleRender(widId); -} + // populate list + mMultiSelectFeats.clear(); -BOOL UiCharEditor::SpellsWndMsg(int widId, TigMsg * msg){ - - if (msg->type == TigMsgType::WIDGET){ - auto msgW = (TigMsgWidget*)msg; - if (msgW->widgetEventType == TigMsgWidgetEvent::Scrolled){ - ui.ScrollbarGetY(spellsScrollbarId, &spellsScrollbarY); - ui.ScrollbarGetY(spellsScrollbar2Id, &spellsScrollbar2Y); - SpellsPerDayUpdate(); - return 1; - } - return 0; + auto featIt = FEAT_ACROBATIC; + auto featProp = 0x100; + switch(feat){ + case FEAT_EXOTIC_WEAPON_PROFICIENCY: + featProp = FPF_EXOTIC_WEAP_ITEM; + break; + case FEAT_IMPROVED_CRITICAL: + featProp = FPF_IMPR_CRIT_ITEM; + break; + case FEAT_MARTIAL_WEAPON_PROFICIENCY: + featProp = FPF_MARTIAL_WEAP_ITEM; + break; + case FEAT_SKILL_FOCUS: + featProp = FPF_SKILL_FOCUS_ITEM; + break; + case FEAT_WEAPON_FINESSE: + featProp = FPF_WEAP_FINESSE_ITEM; + break; + case FEAT_WEAPON_FOCUS: + featProp = FPF_WEAP_FOCUS_ITEM; + break; + case FEAT_WEAPON_SPECIALIZATION: + featProp = FPF_WEAP_SPEC_ITEM; + break; + case FEAT_GREATER_WEAPON_FOCUS: + featProp = FPF_GREATER_WEAP_FOCUS_ITEM; + break; + case FEAT_GREATER_WEAPON_SPECIALIZATION: + featProp = FPF_GREAT_WEAP_SPEC_ITEM; + break; + default: + break; } - if (msg->type == TigMsgType::MOUSE){ - auto msgM = (TigMsgMouse*)msg; - if ((msgM->buttonStateFlags & MouseStateFlags::MSF_LMB_RELEASED) && helpSys.IsClickForHelpActive()){ - // LMB handler - present help for spell - for (auto i = 0; i < SPELLS_BTN_COUNT; i++){ - // check if mouse within button - if (!ui.WidgetContainsPoint(spellsChosenBtnIds[i], msgM->x, msgM->y)) - continue; - - auto spellIdx = i + spellsScrollbar2Y; - if ((uint32_t)spellIdx >= mSpellInfo.size()) - break; - - auto spEnum = mSpellInfo[spellIdx].spEnum; - // ensure is not label - if (spellSys.IsLabel(spEnum)) - break; - - helpSys.PresentWikiHelp(860 + spEnum); - return 1; - } + for (auto ft = 0; ft < NUM_FEATS; ft++) { + featIt = (feat_enums)ft; + if (feats.IsFeatPropertySet(featIt, featProp) && feats.IsFeatEnabled(featIt)) { + mMultiSelectFeats.push_back(FeatInfo(ft)); } - if (msgM->buttonStateFlags & MouseStateFlags::MSF_RMB_RELEASED) { - // RMB handler - add to known spells - for (auto i = 0; i < SPELLS_BTN_COUNT; i++) { - // get spell btn - if (!ui.WidgetContainsPoint(spellsAvailBtnIds[i], msgM->x, msgM->y)) - continue; - auto spellAvailIdx = i + spellsScrollbarY; - if ((uint32_t)spellAvailIdx >= mAvailableSpells.size()) - break; + } - // got the avail btn, now search for suitable vacant slot - auto spEnum = mAvailableSpells[spellAvailIdx].spEnum; - auto spClass = mAvailableSpells[spellAvailIdx].spellClass; - auto spLevel = mAvailableSpells[spellAvailIdx].spellLevel; + ui.WidgetCopy(featsMultiSelectScrollbarId, &featsMultiSelectScrollbar); + featsMultiSelectScrollbar.scrollbarY = 0; + featsMultiSelectScrollbarY = 0; + featsMultiSelectScrollbar.yMax = max(0, (int)mMultiSelectFeats.size() - FEATS_MULTI_BTN_COUNT); + ui.WidgetSet(featsMultiSelectScrollbarId, &featsMultiSelectScrollbar); + ui.ButtonSetButtonState(featsMultiOkBtnId, UBS_DISABLED); - if (spellSys.IsLabel(spEnum)) - break; + ui.WidgetSetHidden(featsMultiSelectWndId, 0); + ui.WidgetBringToFront(featsMultiSelectWndId); +} - if (SpellIsAlreadyKnown(spEnum, spClass) || SpellIsForbidden(spEnum)) - break; +void UiCharEditor::FeatsMultiSelectWndRender(int widId){ + featsbackdrop->SetX(featsMultiSelectWnd.x + featsMultiCenterX); + featsbackdrop->SetY(featsMultiSelectWnd.y + featsMultiCenterY); + featsbackdrop->Render(); - auto curSpellLvl = -1; - auto foundSlot = false; - for (auto j = 0u; j < mSpellInfo.size(); j++){ - auto spInfo = mSpellInfo[j]; - if (spInfo.spellClass != spClass) - continue; - if (spellSys.IsLabel(spInfo.spEnum)){ - curSpellLvl = spInfo.spellLevel; - if (curSpellLvl > spLevel) - break; - continue; - } + UiRenderer::PushFont(PredefinedFont::PRIORY_12); - if (spInfo.spEnum != SPELL_ENUM_VACANT) - continue; + UiRenderer::DrawTextInWidget(widId, GetFeatName(mFeatsMultiMasterFeat), featMultiTitleRect, featsCenteredStyle); - // ensure spell slot is of correct level - if (spInfo.spellLevel == -1 // for "wildcard" empty slots (e.g. Wizard) - || curSpellLvl == spLevel ){ - mSpellInfo[j].spEnum = spEnum; // spell level might still be -1 so be careful when adding to spellbook later on! - break; - } - } - } - } - if (!(msgM->buttonStateFlags & MouseStateFlags::MSF_SCROLLWHEEL_CHANGE)) - return 1; + UiRenderer::PopFont(); - TigMsgMouse msgCopy = *msgM; - msgCopy.buttonStateFlags = MouseStateFlags::MSF_SCROLLWHEEL_CHANGE; +} - if ((int)msgM->x >= spellsWnd.x + 4 && (int)msgM->x <= spellsWnd.x + 184 - && (int)msgM->y >= spellsWnd.y && (int)msgM->y <= spellsWnd.y + 259){ - ui.WidgetCopy(spellsScrollbarId, &spellsScrollbar); - if (spellsScrollbar.handleMessage) - return spellsScrollbar.handleMessage(spellsScrollbarId, (TigMsg*)&msgCopy); - } +BOOL UiCharEditor::FeatsMultiSelectWndMsg(int widId, TigMsg * msg){ + if (msg->type != TigMsgType::WIDGET && msg->type != TigMsgType::KEYSTATECHANGE) + return FALSE; + + ui.ScrollbarGetY(featsMultiSelectScrollbarId, &featsMultiSelectScrollbarY); + + return TRUE; +} - if ((int)msgM->x >= spellsWnd.x +206 && (int)msgM->x <= spellsWnd.x + 376 - && (int)msgM->y >= spellsWnd.y && (int)msgM->y <= spellsWnd.y + 259) { - ui.WidgetCopy(spellsScrollbar2Id, &spellsScrollbar2); - if (spellsScrollbar2.handleMessage) - return spellsScrollbar2.handleMessage(spellsScrollbar2Id, (TigMsg*)&msgCopy); - } - return 1; +void UiCharEditor::FeatsMultiOkBtnRender(int widId){ + UiButtonState buttonState; + if (ui.GetButtonState(widId, buttonState)) + return; + int texId; + switch(buttonState){ + case UBS_NORMAL: + ui.GetAsset(UiAssetType::Generic, UiGenericAsset::AcceptNormal, texId); + break; + case UBS_HOVERED: + ui.GetAsset(UiAssetType::Generic, UiGenericAsset::AcceptHover, texId); + break; + case UBS_DOWN: + ui.GetAsset(UiAssetType::Generic, UiGenericAsset::AcceptPressed, texId); + break; + case UBS_DISABLED: + ui.GetAsset(UiAssetType::Generic, UiGenericAsset::DisabledNormal, texId); + break; + default: + break; } + + static TigRect srcRect(1,1,110,22); + UiRenderer::DrawTextureInWidget(featsMultiSelectWndId, texId, featMultiOkRect, srcRect); + + + UiRenderer::PushFont(PredefinedFont::PRIORY_12); + UiRenderer::DrawTextInWidget(featsMultiSelectWndId, combatSys.GetCombatMesLine(6009), featMultiOkTextRect, featsCenteredStyle); + UiRenderer::PopFont(); - return 0; } -void UiCharEditor::SpellsPerDayUpdate(){ - UiRenderer::PushFont(PredefinedFont::ARIAL_BOLD_24); - auto &selPkt = GetCharEditorSelPacket(); +BOOL UiCharEditor::FeatsMultiOkBtnMsg(int widId, TigMsg * msg){ - spellsPerDayTexts.clear(); - for (auto i = 0; i < SPELLS_PER_DAY_BOXES_COUNT; i++){ - auto &handle = GetEditedChar(); - auto casterLvl = objects.StatLevelGet(handle, selPkt.classCode); - auto numSpells = d20ClassSys.GetNumSpellsFromClass(handle, selPkt.classCode, i, casterLvl); - if (numSpells < 0) - numSpells = 0; - std::string text(fmt::format("{}", numSpells)); - spellsPerDayTexts.push_back(text); + if (msg->type != TigMsgType::WIDGET) + return FALSE; + auto msgW = (TigMsgWidget*)msg; + if (msgW->widgetEventType != TigMsgWidgetEvent::MouseReleased) + return FALSE; - auto textMeas = UiRenderer::MeasureTextSize(text, spellsPerDayStyle); - spellsPerDayTextRects[i].x = spellsNewSpellsBoxRects[i].x + - (spellsNewSpellsBoxRects[i].width - textMeas.width)/2; - spellsPerDayTextRects[i].y = spellsNewSpellsBoxRects[i].y + - (spellsNewSpellsBoxRects[i].height - textMeas.height) / 2; - spellsPerDayTextRects[i].width = textMeas.width; - spellsPerDayTextRects[i].height = textMeas.height; - } - UiRenderer::PopFont(); -} + auto &selPkt = GetCharEditorSelPacket(); -BOOL UiCharEditor::SpellsEntryBtnMsg(int widId, TigMsg * msg) -{ - if (msg->type != TigMsgType::WIDGET) - return 0; + if (featsMultiSelected == FEAT_NONE) { + if (selPkt.feat0 == mFeatsMultiMasterFeat) { + selPkt.feat0 = FEAT_NONE; + } + if (selPkt.feat1 == mFeatsMultiMasterFeat) { + selPkt.feat1 = FEAT_NONE; + } + if (selPkt.feat2 == mFeatsMultiMasterFeat) { + selPkt.feat2 = FEAT_NONE; + } + } + else + { + if (selPkt.feat2 == mFeatsMultiMasterFeat) { + selPkt.feat2 = featsMultiSelected; + } + else if (selPkt.feat0 == mFeatsMultiMasterFeat) { + selPkt.feat0 = featsMultiSelected; + } + else if (selPkt.feat1 == mFeatsMultiMasterFeat) { + selPkt.feat1 = featsMultiSelected; + } + } + + mFeatsMultiMasterFeat = FEAT_NONE; + featsMultiSelected = FEAT_NONE; + ui.WidgetSetHidden(featsMultiSelectWndId, 1); - // oops, looks like this is handled in the SpellsWndMsg function + return TRUE; +} - /*auto widIdx = ui.WidgetlistIndexof(widId, &spellsChosenBtnIds[0], SPELLS_BTN_COUNT); - if (widIdx == -1) - return 0; +void UiCharEditor::FeatsMultiCancelBtnRender(int widId){ + UiButtonState buttonState; + if (ui.GetButtonState(widId, buttonState)) + return; - auto spellIdx = widIdx + spellsScrollbar2Y; - if (spellIdx >= (int)mSpellInfo.size()) - return 0; + int texId; + switch (buttonState) { + case UBS_NORMAL: + ui.GetAsset(UiAssetType::Generic, UiGenericAsset::DeclineNormal, texId); + break; + case UBS_HOVERED: + ui.GetAsset(UiAssetType::Generic, UiGenericAsset::DeclineHover, texId); + break; + case UBS_DOWN: + ui.GetAsset(UiAssetType::Generic, UiGenericAsset::DeclinePressed, texId); + break; + case UBS_DISABLED: + ui.GetAsset(UiAssetType::Generic, UiGenericAsset::DisabledNormal, texId); + break; + default: + break; + } - auto spInfo = mSpellInfo[spellIdx]; - auto spFlag = spInfo.spFlag; - auto spEnum = spInfo.spEnum; - auto spLvl = spInfo.spellLevel;*/ + static TigRect srcRect(1, 1, 110, 22); + UiRenderer::DrawTextureInWidget(featsMultiSelectWndId, texId, featMultiCancelRect, srcRect); - return 0; + UiRenderer::PushFont(PredefinedFont::PRIORY_12); + UiRenderer::DrawTextInWidget(featsMultiSelectWndId, combatSys.GetCombatMesLine(6010), featMultiCancelTextRect, featsCenteredStyle); + UiRenderer::PopFont(); +} + +BOOL UiCharEditor::FeatsMultiCancelBtnMsg(int widId, TigMsg * msg){ + if (msg->type != TigMsgType::WIDGET) + return FALSE; + auto msgW = (TigMsgWidget*)msg; + if (msgW->widgetEventType != TigMsgWidgetEvent::MouseReleased) + return FALSE; + + auto &selPkt = GetCharEditorSelPacket(); + + if (selPkt.feat0 == mFeatsMultiMasterFeat){ + selPkt.feat0 = FEAT_NONE; + } + if (selPkt.feat1 == mFeatsMultiMasterFeat) { + selPkt.feat1 = FEAT_NONE; + } + if (selPkt.feat2 == mFeatsMultiMasterFeat) { + selPkt.feat2 = FEAT_NONE; + } + + mFeatsMultiMasterFeat = FEAT_NONE; + featsMultiSelected = FEAT_NONE; + ui.WidgetSetHidden(featsMultiSelectWndId, 1); + + return TRUE; +} + +void UiCharEditor::FeatsMultiBtnRender(int widId){ + auto widIdx = ui.WidgetlistIndexof(widId, &featsMultiSelectBtnIds[0], FEATS_MULTI_BTN_COUNT); + auto featIdx = widIdx + featsMultiSelectScrollbarY; + if (widIdx == -1 || featIdx >= (int)mMultiSelectFeats.size()) + return; + + auto featInfo = mMultiSelectFeats[featIdx]; + auto feat = (feat_enums)featInfo.featEnum; + + + + auto getFeatShortName = [](feat_enums ft){ + if (feats.IsFeatMultiSelectMaster(ft)) + return uiCharEditor.GetFeatName(ft); + + + auto mesKey = 50000 + ft; + + if (feats.IsFeatPropertySet(ft, FPF_GREAT_WEAP_SPEC_ITEM)){ + mesKey = 50000 + (ft - FEAT_GREATER_WEAPON_SPECIALIZATION_GAUNTLET + FEAT_WEAPON_SPECIALIZATION_GAUNTLET); + } + + MesLine line(mesKey); + auto pcCreationMes = temple::GetRef(0x11E72EF0); + auto text = mesFuncs.GetLineById(pcCreationMes,mesKey); + if (!text) + text = uiCharEditor.GetFeatName(ft).c_str(); + return std::string(text); + }; + + auto ftName = getFeatShortName(feat); + + UiRenderer::PushFont(PredefinedFont::PRIORY_12); + UiRenderer::DrawTextInWidget(featsMultiSelectWndId, ftName, featsMultiBtnRects[widIdx], GetFeatStyle(feat, false)); + + UiRenderer::PopFont(); +} + +BOOL UiCharEditor::FeatsMultiBtnMsg(int widId, TigMsg* msg){ + + if (msg->type == TigMsgType::MOUSE) + return TRUE; + + if (msg->type != TigMsgType::WIDGET) + return FALSE; + + + auto msgW = (TigMsgWidget*)msg; + + auto widIdx = ui.WidgetlistIndexof(widId, &featsMultiSelectBtnIds[0], FEATS_MULTI_BTN_COUNT); + auto featIdx = widIdx + featsMultiSelectScrollbarY; + if (widIdx == -1 || featIdx >= (int)mMultiSelectFeats.size()) + return FALSE; + + auto featInfo = mMultiSelectFeats[featIdx]; + auto feat = (feat_enums)featInfo.featEnum; + + auto &selPkt = GetCharEditorSelPacket(); + auto btn = ui.GetButton(widId); + + switch (msgW->widgetEventType) { + case TigMsgWidgetEvent::MouseReleased: + if (helpSys.IsClickForHelpActive()) { + helpSys.PresentWikiHelp(109 + feat); + return TRUE; + } + if (FeatCanPick(feat)){ + featsMultiSelected = feat; + ui.ButtonSetButtonState(featsMultiOkBtnId, UBS_NORMAL); + } else + { + featsMultiSelected = FEAT_NONE; + ui.ButtonSetButtonState(featsMultiOkBtnId, UBS_DISABLED); + } + return TRUE; + default: + return FALSE; + + } + + return FALSE; +} + + + +bool UiCharEditor::IsSelectingNormalFeat() { + auto handle = GetEditedChar(); + auto newLvl = GetNewLvl(); + return (newLvl % 3) == 0; +} + +bool UiCharEditor::IsSelectingBonusFeat() { + return mIsSelectingBonusFeat; +} + + +std::string UiCharEditor::GetFeatName(feat_enums feat) { + + if (feat >= FEAT_EXOTIC_WEAPON_PROFICIENCY && feat <= FEAT_GREATER_WEAPON_FOCUS) + return featsMasterFeatStrings[feat]; + + return std::string(feats.GetFeatName(feat)); + +} + +TigTextStyle & UiCharEditor::GetFeatStyle(feat_enums feat, bool allowMultiple) { + auto &selPkt = GetCharEditorSelPacket(); + auto newLvl = uiCharEditor.GetNewLvl(selPkt.classCode); + + if ((allowMultiple || !uiCharEditor.FeatAlreadyPicked(feat)) + && uiCharEditor.FeatCanPick(feat)) + { + if (uiCharEditor.featsMultiSelected == feat) { + return uiCharEditor.blueTextStyle; + } + if (feats.IsClassFeat(feat)) { // class Specific feat + return uiCharEditor.featsClassStyle; + } + else if (uiCharEditor.IsClassBonusFeat(feat)) // is choosing class bonus right now + { + return uiCharEditor.featsGoldenStyle; + } + else + return uiCharEditor.featsBonusTextStyle; + } + + return uiCharEditor.featsGreyedStyle; + +} + +bool UiCharEditor::FeatAlreadyPicked(feat_enums feat) { + if (feats.IsFeatPropertySet(feat, 0x1) // can be gained multiple times + || feats.IsFeatMultiSelectMaster(feat)) + return false; + auto &selPkt = GetCharEditorSelPacket(); + if (selPkt.feat0 == feat || selPkt.feat1 == feat || selPkt.feat2 == feat) + return true; + + auto handle = GetEditedChar(); + + auto isRangerSpecial = IsSelectingRangerSpec(); + return feats.HasFeatCountByClass(handle, feat, selPkt.classCode, isRangerSpecial ? selPkt.feat2 : FEAT_ACROBATIC) != 0; +} + +bool UiCharEditor::FeatCanPick(feat_enums feat) { + std::vector featsPicked; + auto &selPkt = GetCharEditorSelPacket(); + auto handle = GetEditedChar(); + + if (selPkt.feat0 != FEAT_NONE) { + featsPicked.push_back(selPkt.feat0); + } + if (selPkt.feat1 != FEAT_NONE) { + featsPicked.push_back(selPkt.feat1); + } + if (selPkt.feat2 != FEAT_NONE) { + featsPicked.push_back(selPkt.feat2); + } + + // TODO extend the specials + if (feat == FEAT_IMPROVED_TRIP || feat == FEAT_IMPROVED_DISARM) { + if (selPkt.classCode == stat_level_monk && GetNewLvl(stat_level_monk) == 6) + return true; + } + + if (IsSelectingBonusFeat() && IsClassBonusFeat(feat) && feats.IsFeatPropertySet(feat, FPF_ROGUE_BONUS)){ + return true; + } + + + if (!feats.IsFeatMultiSelectMaster(feat)) { + return feats.FeatPrereqsCheck(handle, feat, featsPicked.size() > 0 ? &featsPicked[0] : nullptr, featsPicked.size(), selPkt.classCode, selPkt.statBeingRaised); + } + + + // Multiselect Master feats + + auto ftrLvl = objects.StatLevelGet(handle, stat_level_fighter); + if (selPkt.classCode == stat_level_fighter) + ftrLvl++; + bool hasFocus = false; + switch (feat) { + case FEAT_EXOTIC_WEAPON_PROFICIENCY: + return critterSys.GetBaseAttackBonus(handle, selPkt.classCode) >= 1; + case FEAT_IMPROVED_CRITICAL: + return critterSys.GetBaseAttackBonus(handle, selPkt.classCode) >= 8; + + case FEAT_MARTIAL_WEAPON_PROFICIENCY: + case FEAT_SKILL_FOCUS: + return true; + + case FEAT_WEAPON_FINESSE: + if (critterSys.GetBaseAttackBonus(handle, selPkt.classCode) < 1) + return false; + for (auto i = (int)FEAT_WEAPON_FINESSE_GAUNTLET; i <= FEAT_WEAPON_FINESSE_NET; i++) { + if (feats.HasFeatCountByClass(handle, (feat_enums)i, (Stat)0, 0)) + return false; + } + for (auto it : featsPicked) { + if (feats.IsFeatPropertySet(it, FPF_WEAP_FINESSE_ITEM)) + return false; + } + return true; + + case FEAT_WEAPON_FOCUS: + return critterSys.GetBaseAttackBonus(handle, selPkt.classCode) >= 1; + + case FEAT_WEAPON_SPECIALIZATION: + + if (ftrLvl < 8) + return false; + // check if has weapon focus + + for (auto i = (int)FEAT_WEAPON_FOCUS_GAUNTLET; i <= FEAT_WEAPON_FOCUS_RAY; i++) { + if (feats.HasFeatCountByClass(handle, (feat_enums)i, (Stat)0, 0)) { + return true; + } + // if not, check if it's one of the picked ones + for (auto it : featsPicked) { + if (it == (feat_enums)i) + return true; + } + } + return false; + + case FEAT_GREATER_WEAPON_FOCUS: + return ftrLvl >= 4; + + case FEAT_GREATER_WEAPON_SPECIALIZATION: + if (ftrLvl < 12) + return false; + + for (auto i = (int)FEAT_GREATER_WEAPON_FOCUS_GAUNTLET; i <= FEAT_GREATER_WEAPON_FOCUS_RAY; i++) { + hasFocus = false; + if (feats.HasFeatCountByClass(handle, (feat_enums)i, (Stat)0, 0)) { + hasFocus = true; + } + // if not, check if it's one of the picked ones + for (auto it : featsPicked) { + if (it == (feat_enums)i) + hasFocus = true; + break; + } + // if has Greater Weapon Focus, check for Weapon Specialization + if (hasFocus) { + + for (auto j = (int)FEAT_WEAPON_SPECIALIZATION_GAUNTLET; j <= FEAT_WEAPON_SPECIALIZATION_GRAPPLE; j++) { + if (feats.HasFeatCountByClass(handle, (feat_enums)j, (Stat)0, 0)) + return true; + } + } + } + + default: + return true; + } + + +} + +bool UiCharEditor::IsSelectingRangerSpec() +{ + auto &selPkt = GetCharEditorSelPacket(); + auto handle = GetEditedChar(); + auto isRangerSpecial = selPkt.classCode == stat_level_ranger && (objects.StatLevelGet(handle, stat_level_ranger) + 1) == 2; + return isRangerSpecial; +} + +bool UiCharEditor::IsClassBonusFeat(feat_enums feat) { + // mBonusFeats is delivered via the python class API + for (auto it : mBonusFeats) { + if (it.featEnum == feat) + return true; + } + + // the old stuff + auto &selPkt = GetCharEditorSelPacket(); + auto newLvl = GetNewLvl(selPkt.classCode); + + + switch (selPkt.classCode) { + case stat_level_fighter: + return feats.IsFighterFeat(feat); + case stat_level_monk: + if (feats.IsFeatPropertySet(feat, 0x20) && newLvl == 1) + return true; + if (feats.IsFeatPropertySet(feat, 0x40) && newLvl == 2) + return true; + if (feats.IsFeatPropertySet(feat, 0x80) && newLvl == 6) + return true; + return false; + case stat_level_ranger: + return (newLvl == 2 && (feat == FEAT_RANGER_TWO_WEAPON_STYLE || feat == FEAT_RANGER_ARCHERY_STYLE)); + case stat_level_rogue: + return (feat < FEAT_NONE && newLvl >= 10 && !( (newLvl-10) % 3)); + case stat_level_wizard: + return feats.IsMagicFeat(feat); + default: + return false; + } +} + +void UiCharEditor::SetBonusFeats(std::vector& fti) { + mBonusFeats.clear(); + for (auto it : fti) { + uiCharEditor.mBonusFeats.push_back(it); + } +} + +void UiCharEditor::FeatsSanitize() { + auto &selPkt = GetCharEditorSelPacket(); + + for (auto i = 0; i < 3; i++) { // check if any of the feat now lack the prereq (due to user removal). loop three times to ensure up-to-date state. + if (selPkt.feat0 != FEAT_NONE && !FeatCanPick(selPkt.feat0)) + selPkt.feat0 = FEAT_NONE; + if (selPkt.feat1 != FEAT_NONE && !FeatCanPick(selPkt.feat1)) { + selPkt.feat1 = FEAT_NONE; + } + if (selPkt.feat2 != FEAT_NONE && !FeatCanPick(selPkt.feat2) && !IsSelectingRangerSpec()) + selPkt.feat2 = FEAT_NONE; + } + + +} + +feat_enums UiCharEditor::FeatsMultiGetFirst(feat_enums feat) { + switch (feat) + { + case FEAT_EXOTIC_WEAPON_PROFICIENCY: + return FEAT_EXOTIC_WEAPON_PROFICIENCY_BASTARD_SWORD; + + case FEAT_IMPROVED_CRITICAL: + return FEAT_IMPROVED_CRITICAL_BASTARD_SWORD; + + case FEAT_MARTIAL_WEAPON_PROFICIENCY: + return FEAT_MARTIAL_WEAPON_PROFICIENCY_BATTLEAXE; + + case FEAT_SKILL_FOCUS: + return FEAT_SKILL_FOCUS_APPRAISE; + + case FEAT_WEAPON_FINESSE: + return FEAT_WEAPON_FINESSE_BASTARD_SWORD; + + case FEAT_WEAPON_FOCUS: + return FEAT_WEAPON_FOCUS_BASTARD_SWORD; + + case FEAT_GREATER_WEAPON_FOCUS: + return FEAT_GREATER_WEAPON_FOCUS_BASTARD_SWORD; + + case FEAT_WEAPON_SPECIALIZATION: + return FEAT_WEAPON_SPECIALIZATION_BASTARD_SWORD; + + case FEAT_GREATER_WEAPON_SPECIALIZATION: + return FEAT_GREATER_WEAPON_SPECIALIZATION_BASTARD_SWORD; + default: + return feat; + } +} + + +#pragma endregion + +#pragma region Spells + +void UiCharEditor::SpellsWndRender(int widId){ + UiRenderer::PushFont(PredefinedFont::PRIORY_12); + UiRenderer::DrawTextInWidget(widId, spellsAvailLabel, spellsAvailTitleRect, spellsTitleStyle); + UiRenderer::DrawTextInWidget(widId, spellsChosenLabel, spellsChosenTitleRect, spellsTitleStyle); + + + levelupSpellbar->SetX(spellsWnd.x); + levelupSpellbar->SetY(spellsWnd.y + 275); + levelupSpellbar->Render(); + + + // RenderSpellsPerDay + UiRenderer::DrawTextInWidget(widId, spellsPerDayLabel, spellsPerDayTitleRect, spellsTextStyle); + for (auto i = 0; i < SPELLS_PER_DAY_BOXES_COUNT; i++){ + UiRenderer::DrawTextInWidget(widId, spellsPerDayTexts[i], spellsPerDayTextRects[i], spellsPerDayStyle); + } + + UiRenderer::PopFont(); + + // Rects + RenderHooks::RenderRectInt(spellsWnd.x , spellsWnd.y + 38, 195, 227, 0xFF5D5D5D); + RenderHooks::RenderRectInt(spellsWnd.x + 201, spellsWnd.y + 38, 195, 227, 0xFF5D5D5D); + + StateTitleRender(widId); +} + +BOOL UiCharEditor::SpellsWndMsg(int widId, TigMsg * msg){ + + if (msg->type == TigMsgType::WIDGET){ + auto msgW = (TigMsgWidget*)msg; + if (msgW->widgetEventType == TigMsgWidgetEvent::Scrolled){ + ui.ScrollbarGetY(spellsScrollbarId, &spellsScrollbarY); + ui.ScrollbarGetY(spellsScrollbar2Id, &spellsScrollbar2Y); + SpellsPerDayUpdate(); + return 1; + } + return 0; + } + + if (msg->type == TigMsgType::MOUSE){ + auto msgM = (TigMsgMouse*)msg; + if ((msgM->buttonStateFlags & MouseStateFlags::MSF_LMB_RELEASED) && helpSys.IsClickForHelpActive()){ + // LMB handler - present help for spell + for (auto i = 0; i < SPELLS_BTN_COUNT; i++){ + // check if mouse within button + if (!ui.WidgetContainsPoint(spellsChosenBtnIds[i], msgM->x, msgM->y)) + continue; + + auto spellIdx = i + spellsScrollbar2Y; + if ((uint32_t)spellIdx >= mSpellInfo.size()) + break; + + auto spEnum = mSpellInfo[spellIdx].spEnum; + // ensure is not label + if (spellSys.IsLabel(spEnum)) + break; + + helpSys.PresentWikiHelp(860 + spEnum); + return 1; + } + } + if (msgM->buttonStateFlags & MouseStateFlags::MSF_RMB_RELEASED) { + // RMB handler - add to known spells + for (auto i = 0; i < SPELLS_BTN_COUNT; i++) { + // get spell btn + if (!ui.WidgetContainsPoint(spellsAvailBtnIds[i], msgM->x, msgM->y)) + continue; + auto spellAvailIdx = i + spellsScrollbarY; + if ((uint32_t)spellAvailIdx >= mAvailableSpells.size()) + break; + + // got the avail btn, now search for suitable vacant slot + auto spEnum = mAvailableSpells[spellAvailIdx].spEnum; + auto spClass = mAvailableSpells[spellAvailIdx].spellClass; + auto spLevel = mAvailableSpells[spellAvailIdx].spellLevel; + + if (spellSys.IsLabel(spEnum)) + break; + + if (SpellIsAlreadyKnown(spEnum, spClass) || SpellIsForbidden(spEnum)) + break; + + auto curSpellLvl = -1; + auto foundSlot = false; + for (auto j = 0u; j < mSpellInfo.size(); j++){ + auto spInfo = mSpellInfo[j]; + if (spInfo.spellClass != spClass) + continue; + if (spellSys.IsLabel(spInfo.spEnum)){ + curSpellLvl = spInfo.spellLevel; + if (curSpellLvl > spLevel) + break; + continue; + } + + if (spInfo.spEnum != SPELL_ENUM_VACANT) + continue; + + // ensure spell slot is of correct level + if (spInfo.spellLevel == -1 // for "wildcard" empty slots (e.g. Wizard) + || curSpellLvl == spLevel ){ + mSpellInfo[j].spEnum = spEnum; // spell level might still be -1 so be careful when adding to spellbook later on! + break; + } + } + } + } + if (!(msgM->buttonStateFlags & MouseStateFlags::MSF_SCROLLWHEEL_CHANGE)) + return 1; + + TigMsgMouse msgCopy = *msgM; + msgCopy.buttonStateFlags = MouseStateFlags::MSF_SCROLLWHEEL_CHANGE; + + if ((int)msgM->x >= spellsWnd.x + 4 && (int)msgM->x <= spellsWnd.x + 184 + && (int)msgM->y >= spellsWnd.y && (int)msgM->y <= spellsWnd.y + 259){ + ui.WidgetCopy(spellsScrollbarId, &spellsScrollbar); + if (spellsScrollbar.handleMessage) + return spellsScrollbar.handleMessage(spellsScrollbarId, (TigMsg*)&msgCopy); + } + + if ((int)msgM->x >= spellsWnd.x +206 && (int)msgM->x <= spellsWnd.x + 376 + && (int)msgM->y >= spellsWnd.y && (int)msgM->y <= spellsWnd.y + 259) { + ui.WidgetCopy(spellsScrollbar2Id, &spellsScrollbar2); + if (spellsScrollbar2.handleMessage) + return spellsScrollbar2.handleMessage(spellsScrollbar2Id, (TigMsg*)&msgCopy); + } + return 1; + + } + + return 0; +} + +void UiCharEditor::SpellsPerDayUpdate(){ + UiRenderer::PushFont(PredefinedFont::ARIAL_BOLD_24); + auto &selPkt = GetCharEditorSelPacket(); + + spellsPerDayTexts.clear(); + for (auto i = 0; i < SPELLS_PER_DAY_BOXES_COUNT; i++){ + auto &handle = GetEditedChar(); + auto casterLvl = objects.StatLevelGet(handle, selPkt.classCode); + auto numSpells = d20ClassSys.GetNumSpellsFromClass(handle, selPkt.classCode, i, casterLvl); + if (numSpells < 0) + numSpells = 0; + std::string text(fmt::format("{}", numSpells)); + spellsPerDayTexts.push_back(text); + + auto textMeas = UiRenderer::MeasureTextSize(text, spellsPerDayStyle); + spellsPerDayTextRects[i].x = spellsNewSpellsBoxRects[i].x + + (spellsNewSpellsBoxRects[i].width - textMeas.width)/2; + spellsPerDayTextRects[i].y = spellsNewSpellsBoxRects[i].y + + (spellsNewSpellsBoxRects[i].height - textMeas.height) / 2; + spellsPerDayTextRects[i].width = textMeas.width; + spellsPerDayTextRects[i].height = textMeas.height; + } + UiRenderer::PopFont(); +} + +BOOL UiCharEditor::SpellsEntryBtnMsg(int widId, TigMsg * msg) +{ + if (msg->type != TigMsgType::WIDGET) + return 0; + return 0; } void UiCharEditor::SpellsEntryBtnRender(int widId) @@ -2271,10 +2868,45 @@ void UiCharEditor::SpellsAvailableEntryBtnRender(int widId){ else UiRenderer::DrawTextInWidget(spellsWndId, text, rect, spellsTextStyle); } - UiRenderer::PopFont(); - + UiRenderer::PopFont(); + +} + +bool UiCharEditor::SpellIsForbidden(int spEnum) +{ + auto &selPkt = GetCharEditorSelPacket(); + auto handle = GetEditedChar(); + SpellEntry spEntry(spEnum); + auto spSchool = spEntry.spellSchoolEnum; + + if (spSchool == selPkt.forbiddenSchool1 + || spSchool == selPkt.forbiddenSchool2) + return true; + if (spellSys.IsForbiddenSchool(handle, spSchool)) + return true; + return false; +} + +bool UiCharEditor::SpellIsAlreadyKnown(int spEnum, int spellClass) { + for (auto i = 0u; i < mSpellInfo.size(); i++) { + if (mSpellInfo[i].spEnum == spEnum + && mSpellInfo[i].spellClass == spellClass) + return true; + } + + auto &selPkt = GetCharEditorSelPacket(); + if (spellSys.IsSpellKnown(GetEditedChar(), spEnum, spellClass)) { + if (selPkt.spellEnumToRemove == spEnum) + return false; // Oh god... TODO! (need to record class too..) + return true; + } + + return false; } + +#pragma endregion + int UiCharEditor::GetClassWndPage(){ return classWndPage; } @@ -2327,15 +2959,6 @@ void UiCharEditor::ClassSetPermissibles(){ ui.ButtonSetButtonState(classNextBtn, UBS_DISABLED); } -bool UiCharEditor::IsSelectingNormalFeat(){ - auto handle = GetEditedChar(); - auto newLvl = GetNewLvl(); - return (newLvl % 3) == 0; -} - -bool UiCharEditor::IsSelectingBonusFeat(){ - return mIsSelectingBonusFeat; -} int UiCharEditor::GetNewLvl(Stat classEnum){ // default is classEnum = stat_level i.e. get the overall new level auto handle = GetEditedChar(); @@ -2423,270 +3046,8 @@ void UiCharEditor::SpellsPopulateAvailableEntries(Stat classEnum, int maxSpellLv temple::GetRef(0x10C75A60) = numSpells; } -bool UiCharEditor::SpellIsForbidden(int spEnum) -{ - auto &selPkt = GetCharEditorSelPacket(); - auto handle = GetEditedChar(); - SpellEntry spEntry(spEnum); - auto spSchool = spEntry.spellSchoolEnum; - - if (spSchool == selPkt.forbiddenSchool1 - || spSchool == selPkt.forbiddenSchool2) - return true; - if (spellSys.IsForbiddenSchool(handle, spSchool)) - return true; - return false; -} - -bool UiCharEditor::SpellIsAlreadyKnown(int spEnum, int spellClass){ - for (auto i = 0u; i < mSpellInfo.size(); i++){ - if (mSpellInfo[i].spEnum == spEnum - && mSpellInfo[i].spellClass == spellClass) - return true; - } - - auto &selPkt = GetCharEditorSelPacket(); - if (spellSys.IsSpellKnown(GetEditedChar(), spEnum, spellClass)){ - if (selPkt.spellEnumToRemove == spEnum) - return false; // Oh god... TODO! (need to record class too..) - return true; - } - - return false; -} - -std::string UiCharEditor::GetFeatName(feat_enums feat){ - - if (feat >= FEAT_EXOTIC_WEAPON_PROFICIENCY && feat <= FEAT_GREATER_WEAPON_FOCUS) - return featsMasterFeatStrings[feat]; - - return std::string(feats.GetFeatName(feat)); - -} - -TigTextStyle & UiCharEditor::GetFeatStyle(feat_enums feat, bool allowMultiple){ - auto &selPkt = GetCharEditorSelPacket(); - auto newLvl = uiCharEditor.GetNewLvl(selPkt.classCode); - - if ( (allowMultiple || !uiCharEditor.FeatAlreadyPicked(feat)) - && uiCharEditor.FeatCanPick(feat)) - { - if (uiCharEditor.featsMultiSelected == feat) { - return uiCharEditor.blueTextStyle; - } - if (feats.IsClassFeat(feat)) { // class Specific feat - return uiCharEditor.featsClassStyle; - } - else if (uiCharEditor.IsClassBonusFeat(feat)) // is choosing class bonus right now - { - return uiCharEditor.featsGoldenStyle; - } - else - return uiCharEditor.featsBonusTextStyle; - } - - return uiCharEditor.featsGreyedStyle; - -} - -bool UiCharEditor::FeatAlreadyPicked(feat_enums feat){ - if (feats.IsFeatPropertySet(feat, 0x1) // can be gained multiple times - || feats.IsFeatMultiSelectMaster(feat)) - return false; - auto &selPkt = GetCharEditorSelPacket(); - if (selPkt.feat0 == feat || selPkt.feat1 == feat || selPkt.feat2 == feat) - return true; - - auto handle = GetEditedChar(); - - auto isRangerSpecial = IsSelectingRangerSpec(); - return feats.HasFeatCountByClass(handle, feat, selPkt.classCode, isRangerSpecial? selPkt.feat2 : FEAT_ACROBATIC) != 0; -} - -bool UiCharEditor::FeatCanPick(feat_enums feat){ - std::vector featsPicked; - auto &selPkt = GetCharEditorSelPacket(); - auto handle = GetEditedChar(); - - if (selPkt.feat0 != FEAT_NONE){ - featsPicked.push_back(selPkt.feat0); - } - if (selPkt.feat1 != FEAT_NONE) { - featsPicked.push_back(selPkt.feat1); - } - if (selPkt.feat2 != FEAT_NONE) { - featsPicked.push_back(selPkt.feat2); - } - - // TODO extend the specials - if (feat == FEAT_IMPROVED_TRIP || feat == FEAT_IMPROVED_DISARM){ - if (selPkt.classCode == stat_level_monk && GetNewLvl(stat_level_monk) == 6) - return true; - } - if (!feats.IsFeatPartOfMultiselect(feat)){ - return feats.FeatPrereqsCheck(handle, feat, featsPicked.size() > 0 ? &featsPicked[0] : nullptr, featsPicked.size(), selPkt.classCode, selPkt.statBeingRaised); - } - // Multiselect feats - - auto ftrLvl = objects.StatLevelGet(handle, stat_level_fighter); - if (selPkt.classCode == stat_level_fighter) - ftrLvl++; - bool hasFocus = false; - switch (feat){ - case FEAT_EXOTIC_WEAPON_PROFICIENCY: - return critterSys.GetBaseAttackBonus(handle, selPkt.classCode) >= 1; - case FEAT_IMPROVED_CRITICAL: - return critterSys.GetBaseAttackBonus(handle, selPkt.classCode) >= 8; - - case FEAT_MARTIAL_WEAPON_PROFICIENCY: - case FEAT_SKILL_FOCUS: - return true; - - case FEAT_WEAPON_FINESSE: - if (critterSys.GetBaseAttackBonus(handle, selPkt.classCode) < 1) - return false; - for (auto i = (int)FEAT_WEAPON_FINESSE_GAUNTLET; i <= FEAT_WEAPON_FINESSE_NET; i++){ - if (feats.HasFeatCountByClass(handle, (feat_enums)i, (Stat)0, 0)) - return false; - } - for (auto it: featsPicked){ - if (feats.IsFeatPropertySet(it, 0x2100)) - return false; - } - return true; - - case FEAT_WEAPON_FOCUS: - return critterSys.GetBaseAttackBonus(handle, selPkt.classCode) >= 1; - - case FEAT_WEAPON_SPECIALIZATION: - - if (ftrLvl < 8) - return false; - // check if has weapon focus - - for (auto i = (int)FEAT_WEAPON_FOCUS_GAUNTLET; i <= FEAT_WEAPON_FOCUS_RAY; i++) { - if (feats.HasFeatCountByClass(handle, (feat_enums)i, (Stat)0, 0)){ - return true; - } - // if not, check if it's one of the picked ones - for (auto it : featsPicked) { - if (it == (feat_enums)i) - return true; - } - } - return false; - - case FEAT_GREATER_WEAPON_FOCUS: - return ftrLvl >= 4; - default: - return true; - } - - -} - -bool UiCharEditor::IsSelectingRangerSpec() -{ - auto &selPkt = GetCharEditorSelPacket(); - auto handle = GetEditedChar(); - auto isRangerSpecial = selPkt.classCode == stat_level_ranger && (objects.StatLevelGet(handle, stat_level_ranger) + 1) == 2; - return isRangerSpecial; -} - -bool UiCharEditor::IsClassBonusFeat(feat_enums feat){ - // mBonusFeats is delivered via the python class API - for (auto it : mBonusFeats) { - if (it.featEnum == feat) - return true; - } - - // the old stuff - auto &selPkt = GetCharEditorSelPacket(); - auto newLvl = GetNewLvl(selPkt.classCode); - - - switch(selPkt.classCode){ - case stat_level_fighter: - return feats.IsFighterFeat(feat); - case stat_level_monk: - if (feats.IsFeatPropertySet(feat, 0x20) && newLvl == 1) - return true; - if (feats.IsFeatPropertySet(feat, 0x40) && newLvl == 2) - return true; - if (feats.IsFeatPropertySet(feat, 0x80) && newLvl == 6) - return true; - return false; - case stat_level_ranger: - return (newLvl == 2 && (feat == FEAT_RANGER_TWO_WEAPON_STYLE || feat == FEAT_RANGER_ARCHERY_STYLE)); - case stat_level_rogue: - return (feat < FEAT_NONE && newLvl >= 10 && !(newLvl % 3)); - case stat_level_wizard: - return feats.IsMagicFeat(feat); - default: - return false; - } -} - -void UiCharEditor::SetBonusFeats(std::vector& fti){ - mBonusFeats.clear(); - for (auto it : fti) { - uiCharEditor.mBonusFeats.push_back(it); - } -} - -void UiCharEditor::FeatsSanitize(){ - auto &selPkt = GetCharEditorSelPacket(); - - for (auto i=0; i < 3; i++){ // check if any of the feat now lack the prereq (due to user removal). loop three times to ensure up-to-date state. - if (selPkt.feat0 != FEAT_NONE && !FeatCanPick(selPkt.feat0)) - selPkt.feat0 = FEAT_NONE; - if (selPkt.feat1 != FEAT_NONE && !FeatCanPick(selPkt.feat1)) { - selPkt.feat1 = FEAT_NONE; - } - if (selPkt.feat2 != FEAT_NONE && !FeatCanPick(selPkt.feat2) && !IsSelectingRangerSpec()) - selPkt.feat2 = FEAT_NONE; - } - - -} - -void UiCharEditor::FeatsMultiSelectActivate(feat_enums feat){ - - auto &selPkt = GetCharEditorSelPacket(); - if (feat == FEAT_WEAPON_FINESSE){ - if (selPkt.feat0 == FEAT_WEAPON_FINESSE) - selPkt.feat0 = FEAT_WEAPON_FINESSE_DAGGER; - if (selPkt.feat1 == FEAT_WEAPON_FINESSE) - selPkt.feat1 = FEAT_WEAPON_FINESSE_DAGGER; - if (selPkt.feat2 == FEAT_WEAPON_FINESSE) - selPkt.feat2 = FEAT_WEAPON_FINESSE_DAGGER; - return; - } - ui.WidgetBringToFront(featsMultiSelectWndId); -} -feat_enums UiCharEditor::FeatsMultiGetFirst(feat_enums feat){ - switch(feat) - { - case FEAT_EXOTIC_WEAPON_PROFICIENCY: - return FEAT_EXOTIC_WEAPON_PROFICIENCY_BASTARD_SWORD; - case FEAT_IMPROVED_CRITICAL: - return FEAT_IMPROVED_CRITICAL_BASTARD_SWORD; - case FEAT_MARTIAL_WEAPON_PROFICIENCY: - return FEAT_MARTIAL_WEAPON_PROFICIENCY_BATTLEAXE; - case FEAT_SKILL_FOCUS: - return FEAT_SKILL_FOCUS_APPRAISE; - case FEAT_GREATER_WEAPON_FOCUS: - return FEAT_GREATER_WEAPON_FOCUS_BASTARD_SWORD; - case FEAT_WEAPON_SPECIALIZATION: - return FEAT_WEAPON_SPECIALIZATION_BASTARD_SWORD; - case FEAT_GREATER_WEAPON_SPECIALIZATION: - return FEAT_GREATER_WEAPON_SPECIALIZATION_BASTARD_SWORD; - default: - return feat; - } -} class UiCharEditorHooks : public TempleFix { diff --git a/TemplePlus/ui/ui_char_editor_feats.cpp b/TemplePlus/ui/ui_char_editor_feats.cpp index 4c032b030..23a4857b0 100644 --- a/TemplePlus/ui/ui_char_editor_feats.cpp +++ b/TemplePlus/ui/ui_char_editor_feats.cpp @@ -76,7 +76,7 @@ int __declspec(naked) HookedUsercallFeatMultiselectSub_101A8080() }; -int HookedFeatMultiselectSub_101A8080(feat_enums feat) +int HookedFeatMultiselectSub_101A8080(feat_enums feat) // redundant now { if ( (feat >= FEAT_EXOTIC_WEAPON_PROFICIENCY && feat <= FEAT_WEAPON_SPECIALIZATION) || feat == FEAT_WEAPON_FINESSE_DAGGER ) { diff --git a/tpdata/tpgamefiles.dat b/tpdata/tpgamefiles.dat index 8e922073a..e45df865f 100644 Binary files a/tpdata/tpgamefiles.dat and b/tpdata/tpgamefiles.dat differ