Art tooltips: "Verbessert durch" lists what improves each art
Arts are objects (game::Art) built from the game's definitions: name, launch type, damage source, regeneration group and the skills' art lists. The tooltip gets one line per effect of a skill or attribute, sorted by size; unlearned class skills are greyed out at 0%. Weapon arts list only the damage types the held weapon deals and the held weapon's skill. Tooltip line wrap ignores colour codes; art tooltips wrap wider. Blitzschlag's tooltip rewrite becomes a tooltip filter. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5d80a33e9b
commit
c875d7d6b9
12 files changed
+933
-46
No files matched your search
@@ -0,0 +1,339 @@
|
||||
#include "game/engine/art.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
#include "game/engine/orders.h"
|
||||
#include "game/engine/skills.h"
|
||||
#include "game/ui/texts.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
// The game's per-skill art lists (cCreatureArts::ApplySkill 0x565f30 passes
|
||||
// them to ApplySkillToArts 0x565c80): the regeneration time of each listed art
|
||||
// is divided by 1 + curve / 100; skills with the speed flag also add curve
|
||||
// FEK..SP to the execution speed +0x3e of the selected art. Ballistik raises
|
||||
// the damage of its traps too (ArtTooltip's damage factor for them).
|
||||
struct LinkedStat {
|
||||
ArtStat stat;
|
||||
uint32_t curve;
|
||||
};
|
||||
struct SkillArts {
|
||||
uint8_t skill;
|
||||
std::vector<LinkedStat> stats;
|
||||
std::vector<uint16_t> arts;
|
||||
};
|
||||
const std::vector<SkillArts>& SkillArtLists() {
|
||||
using S = ArtStat;
|
||||
auto school = [](uint32_t curve) {
|
||||
return std::vector<LinkedStat>{{S::RegenerationTime, curve}, {S::ExecutionSpeed, kCurveExecutionSpeed}};
|
||||
};
|
||||
static const std::vector<SkillArts> lists = {
|
||||
{kHimmelsmagie, school(kCurveHimmelsmagie), {0x0d, 0x0f, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x3fb}},
|
||||
{kFeuermagie, school(kCurveFeuermagie), {0x05, 0x0b, 0x17, 0x18}},
|
||||
{kWassermagie, school(kCurveWassermagie), {0x1b, 0x04, 0x22, 0x23}},
|
||||
{kErdmagie, school(kCurveErdmagie), {0x02, 0x10, 0x19, 0x1a}},
|
||||
{kLuftmagie, school(kCurveLuftmagie), {0x01, 0x1c, 0x1d, 0x21}},
|
||||
{kMondmagie, school(kCurveMondmagie), {0x03, 0x07, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a}},
|
||||
{kVampirismus,
|
||||
{{S::RegenerationTime, kCurveKonzentration}},
|
||||
{0x3ff, 0x401, 0x403, 0x405, 0x40e, 0x40f, 0x410, 0x411, 0x408, 0x2b, 0x406, 0x407, 0x409, 0x40a, 0x40b,
|
||||
0x40c, 0x40d}},
|
||||
{kKonzentration,
|
||||
{{S::RegenerationTime, kCurveKonzentration}},
|
||||
{0x3e8, 0x3e9, 0x3ea, 0x3eb, 0x3ec, 0x3ed, 0x3ee, 0x3ef, 0x3f0, 0x3f1, 0x3f2, 0x3f3, 0x3f4, 0x3f5, 0x3f6,
|
||||
0x3f7, 0x3f8, 0x3f9, 0x3fa, 0x3fc, 0x421, 0x422, 0x423, 0x424, 0x425, 0x426, 0x427, 0x428, 0x429, 0x42a,
|
||||
0x42b, 0x42c, 0x42d, 0x42e, 0x42f, 0x430, 0x431, 0x432, 0x433, 0x434, 0x41e, 0x3fe, 0x400, 0x402, 0x404,
|
||||
0x412, 0x413, 0x414, 0x415, 0x416, 0x443, 0x446, 0x444, 0x449, 0x44e, 0x44a, 0x44d, 0x44b, 0x44f, 0x44c,
|
||||
0x436, 0x437, 0x438, 0x43a, 0x43b, 0x43c, 0x441}},
|
||||
{kBallistik,
|
||||
{{S::RegenerationTime, kCurveBallistik},
|
||||
{S::ExecutionSpeed, kCurveExecutionSpeed},
|
||||
{S::Damage, kCurveBallistik}},
|
||||
{0x417, 0x418, 0x419, 0x41a, 0x41b, 0x41c, 0x41d}},
|
||||
{kFallenkunst, {{S::RegenerationTime, kCurveKonzentration}}, {0x417, 0x418, 0x419, 0x41a, 0x41b, 0x41c, 0x41d}},
|
||||
{kBlutrausch, school(kCurveBlutrausch), {0x3fe, 0x2b, 0x408, 0x406, 0x407, 0x409, 0x40a, 0x40b, 0x40c, 0x40d}},
|
||||
{kWaffentechnik, school(kCurveWaffentechnik), {0x43f, 0x43e, 0x43d, 0x37, 0x38, 0x39, 0x3a, 0x3b}},
|
||||
{kZwergenkunde,
|
||||
{{S::RegenerationTime, kCurveKonzentration}},
|
||||
{0x43f, 0x43e, 0x43d, 0x37, 0x38, 0x39, 0x3a, 0x3b}},
|
||||
{kHoellenmacht, {{S::RegenerationTime, kCurveHoellenmacht}}, {0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43}},
|
||||
};
|
||||
return lists;
|
||||
}
|
||||
|
||||
// Arts the game treats unlike their definitions say: Energieblitze counts down
|
||||
// at the magic rate (+0xee) although it is a combat move (0x562998,
|
||||
// ArtTooltip); 0x3fa and 0x38 take the weapon's damage factors although they
|
||||
// have spell values (ArtTooltip).
|
||||
constexpr uint16_t kMagicRegenerationMoves[] = {kArtEnergieblitze};
|
||||
constexpr uint16_t kWeaponDamageSpells[] = {0x3fa, 0x38};
|
||||
|
||||
template <size_t N>
|
||||
bool Contains(const uint16_t (&ids)[N], uint16_t id) {
|
||||
return std::find(std::begin(ids), std::end(ids), id) != std::end(ids);
|
||||
}
|
||||
|
||||
// cCreature weapon checks (plain thiscall, as ApplySkill calls them).
|
||||
bool CreatureCheck(void* creature, uint32_t address) {
|
||||
using Fn = bool(__fastcall*)(void*, void*);
|
||||
return reinterpret_cast<Fn>(address)(creature, nullptr);
|
||||
}
|
||||
bool HoldsTwoWeapons(void* creature) {
|
||||
return CreatureCheck(creature, 0x0054a2e0); // weapon class 8
|
||||
}
|
||||
constexpr size_t kCreatureSecondHand = 0x1d8;
|
||||
constexpr int kWeaponClassBareHands = 0;
|
||||
constexpr int kWeaponClassBlade = 10;
|
||||
|
||||
// The weapon skills: attack value (+0xe6 += AW curve %) and attack speed
|
||||
// (+0x3c += SP curve, the next curve) while the matching weapon is held
|
||||
// (ApplySkill cases 3..7, 0xd, 0x18).
|
||||
struct WeaponSkill {
|
||||
uint8_t skill;
|
||||
uint32_t curve;
|
||||
bool (*applies)(void* creature);
|
||||
};
|
||||
const WeaponSkill kWeaponSkills[] = {
|
||||
{kStangenwaffen, kCurveStangenwaffenValue,
|
||||
[](void* c) { return CreatureCheck(c, 0x0054a3e0) && !HoldsTwoWeapons(c); }},
|
||||
{kSchwertkunde, kCurveSchwertkundeValue,
|
||||
[](void* c) { return CreatureCheck(c, 0x0054a220) && !HoldsTwoWeapons(c); }},
|
||||
{kAxtkampf, kCurveAxtkampfValue, [](void* c) { return CreatureCheck(c, 0x0054a2f0) && !HoldsTwoWeapons(c); }},
|
||||
{kZweiWaffenKampf, kCurveZweiWaffenValue,
|
||||
[](void* c) {
|
||||
return HoldsTwoWeapons(c) ||
|
||||
(WeaponClass(c) == kWeaponClassBareHands &&
|
||||
*reinterpret_cast<const uint32_t*>(static_cast<uint8_t*>(c) + kCreatureSecondHand) != 0);
|
||||
}},
|
||||
{kFernkampf, kCurveFernkampfValue, [](void* c) { return HoldsRangedWeapon(c); }},
|
||||
// The game also counts a blade in the second hand; not covered here.
|
||||
{kKlingenkampf, kCurveKlingenkampfValue, [](void* c) { return WeaponClass(c) == kWeaponClassBlade; }},
|
||||
{kFaustkampf, kCurveFaustkampfValue, [](void* c) { return WeaponClass(c) == kWeaponClassBareHands; }},
|
||||
};
|
||||
|
||||
// The held weapon's damage per type (physical, fire, magic, poison):
|
||||
// cCreatureArts +0x76 + 4 t max, +0x86 + 4 t min (floats; the damage range
|
||||
// functions 0x563e90 .. 0x564160 multiply them by +0x96 + +0xa6 + 4 t).
|
||||
constexpr size_t kArtsWeaponDamageMax = 0x76;
|
||||
constexpr size_t kArtsWeaponDamageMin = 0x86;
|
||||
bool WeaponDealsDamage(const uint8_t* arts, int damageType) {
|
||||
auto value = [&](size_t offset) { return *reinterpret_cast<const float*>(arts + offset + 4 * damageType); };
|
||||
return value(kArtsWeaponDamageMax) > 0.0f || value(kArtsWeaponDamageMin) > 0.0f;
|
||||
}
|
||||
|
||||
// Recalculate (0x579560) gives creature type 8 (Zwerg) the combat rate for
|
||||
// magic arts too.
|
||||
constexpr size_t kCreatureType = 0x10;
|
||||
constexpr uint32_t kTypeZwerg = 8;
|
||||
|
||||
// The attributes' shares of the damage factors (Recalculate), in percent:
|
||||
// (base * F / 10 + invested * w [+ item points]) / BalChar*D with F = (156 -
|
||||
// BalStatOff) * level / 156 + BalStatOff + 9.
|
||||
double AttributeDamage(const uint8_t* arts, int attribute, int damageType, double investedWeight, bool items) {
|
||||
const float off = BalanceValue(kBalStatOff);
|
||||
const double f = (156.0 - off) * HeroLevel(arts) / 156.0 + off + 9.0;
|
||||
double value = AttributeBase(arts, attribute) * f * 0.1 + investedWeight * arts[kArtsInvested + attribute];
|
||||
if (items) {
|
||||
value += AttributeFromItems(arts, attribute);
|
||||
}
|
||||
return 100.0 * value / BalanceValue(kBalCharDamage[damageType]);
|
||||
}
|
||||
|
||||
double RegenerationTimePercent(float bonus) {
|
||||
return -100.0 * (1.0 - 1.0 / (1.0 + bonus / 100.0));
|
||||
}
|
||||
|
||||
bool Offensive(LaunchType launch) {
|
||||
return launch != LaunchType::SelfOrAlly && launch != LaunchType::Corpse && launch != LaunchType::Unknown;
|
||||
}
|
||||
|
||||
// Damage values of a spell definition: 4 types x (base, per level), s16 at +0x42.
|
||||
bool HasDamageValues(const uint8_t* spell) {
|
||||
const auto* values = reinterpret_cast<const int16_t*>(spell + 0x42);
|
||||
return std::any_of(values, values + 8, [](int16_t v) { return v != 0; });
|
||||
}
|
||||
|
||||
const wchar_t* StatName(ArtStat stat) {
|
||||
switch (stat) {
|
||||
case ArtStat::Damage:
|
||||
return L"Schaden";
|
||||
case ArtStat::FireDamage:
|
||||
return L"Feuerschaden";
|
||||
case ArtStat::MagicDamage:
|
||||
return L"Magieschaden";
|
||||
case ArtStat::PoisonDamage:
|
||||
return L"Giftschaden";
|
||||
case ArtStat::AttackValue:
|
||||
return L"Angriffswert";
|
||||
case ArtStat::AttackSpeed:
|
||||
return L"Angriffstempo";
|
||||
case ArtStat::ExecutionSpeed:
|
||||
return L"Ausführungstempo";
|
||||
case ArtStat::RegenerationTime:
|
||||
return L"Regenerationszeit";
|
||||
}
|
||||
return L"";
|
||||
}
|
||||
|
||||
// Tooltip colours (ARGB, "\c" codes as the game's tooltips use them).
|
||||
constexpr const wchar_t* kHeaderColour = L"\\cFFFFFF80";
|
||||
constexpr const wchar_t* kTextColour = L"\\cFFFFFFFF";
|
||||
constexpr const wchar_t* kInactiveColour = L"\\cFFAFAFAF";
|
||||
|
||||
std::wstring ImproverName(const Improver& by) {
|
||||
return by.kind == Improver::Skill ? SkillName(by.id) : AttributeName(by.id);
|
||||
}
|
||||
|
||||
std::map<uint16_t, std::vector<Art::TooltipFilter>>& Filters() {
|
||||
static std::map<uint16_t, std::vector<Art::TooltipFilter>> filters;
|
||||
return filters;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Art::Art(uint16_t id) : id_(id) {
|
||||
const uint8_t* spell = FindSpellDef(id);
|
||||
const uint8_t* def = spell ? spell : FindCombatMoveDef(id);
|
||||
nameText_ = def ? *reinterpret_cast<const uint32_t*>(def + 4) : 0; // SpellDef / CombatMoveDef +4 (0x4d24d0, 0x4d1b40)
|
||||
launch_ = ArtLaunchType(id);
|
||||
const uint8_t* values = ArtSpellDef(id);
|
||||
spellValues_ = values && !Contains(kWeaponDamageSpells, id);
|
||||
magicRegeneration_ = spell || Contains(kMagicRegenerationMoves, id);
|
||||
damage_ = Offensive(launch_) && (!values || HasDamageValues(values));
|
||||
for (const SkillArts& list : SkillArtLists()) {
|
||||
if (std::find(list.arts.begin(), list.arts.end(), id) == list.arts.end()) {
|
||||
continue;
|
||||
}
|
||||
for (const LinkedStat& linked : list.stats) {
|
||||
if (linked.stat != ArtStat::Damage || damage_) {
|
||||
links_.push_back({list.skill, linked.curve, linked.stat});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring Art::Name() const {
|
||||
return nameText_ ? GameText(nameText_) : std::wstring();
|
||||
}
|
||||
|
||||
std::vector<Improvement> Art::Improvements(void* creature) const {
|
||||
auto* arts = static_cast<uint8_t*>(creature) + kCreatureArts;
|
||||
const uint8_t* classSkills = ClassSkills(arts);
|
||||
std::vector<Improvement> out;
|
||||
auto add = [&](Improver by, ArtStat stat, double percent) {
|
||||
out.push_back({by, stat, static_cast<float>(percent)});
|
||||
};
|
||||
auto skill = [&](uint8_t id, uint32_t curve, ArtStat stat) {
|
||||
const unsigned level = SkillLevel(arts, id);
|
||||
const bool ofClass =
|
||||
level > 0 || (classSkills && std::find(classSkills, classSkills + kClassSkills, id) != classSkills + kClassSkills);
|
||||
if (!ofClass) {
|
||||
return;
|
||||
}
|
||||
const float value = CurveValue(curve, level);
|
||||
add({Improver::Skill, id}, stat, stat == ArtStat::RegenerationTime ? RegenerationTimePercent(value) : value);
|
||||
};
|
||||
// A faster regeneration rate (+x %) as the time it saves.
|
||||
auto rate = [&](Improver by, double percent) {
|
||||
add(by, ArtStat::RegenerationTime, RegenerationTimePercent(static_cast<float>(percent)));
|
||||
};
|
||||
auto attribute = [&](uint8_t id, ArtStat stat, double percent) { add({Improver::Attribute, id}, stat, percent); };
|
||||
|
||||
if (damage_ && spellValues_) {
|
||||
// Spell values times +0x96 + +0xb6: Magiekunde, Mentale Regeneration.
|
||||
skill(kMagiekunde, kCurveMagiekunde, ArtStat::Damage);
|
||||
attribute(kMentaleRegeneration, ArtStat::Damage, AttributeDamage(arts, kMentaleRegeneration, 2, 1.0, false));
|
||||
} else if (damage_) {
|
||||
// Weapon damage times +0x96 + +0xa6: Waffenkunde for all types; the
|
||||
// attributes for the types the held weapon deals.
|
||||
skill(kWaffenkunde, kCurveWaffenkunde, ArtStat::Damage);
|
||||
if (WeaponDealsDamage(arts, 0)) {
|
||||
const uint8_t physical = HoldsRangedWeapon(creature) ? kGeschick : kStaerke;
|
||||
attribute(physical, ArtStat::Damage, AttributeDamage(arts, physical, 0, 1.0, true));
|
||||
}
|
||||
if (WeaponDealsDamage(arts, 1)) {
|
||||
attribute(kStaerke, ArtStat::FireDamage, AttributeDamage(arts, kStaerke, 1, 2.0, true));
|
||||
attribute(kGeschick, ArtStat::FireDamage, AttributeDamage(arts, kGeschick, 1, 2.0, true));
|
||||
}
|
||||
if (WeaponDealsDamage(arts, 2)) {
|
||||
attribute(kMentaleRegeneration, ArtStat::MagicDamage,
|
||||
AttributeDamage(arts, kMentaleRegeneration, 2, 1.0, true));
|
||||
}
|
||||
if (WeaponDealsDamage(arts, 3)) {
|
||||
attribute(kWiderstand, ArtStat::PoisonDamage, AttributeDamage(arts, kWiderstand, 3, 2.0, true));
|
||||
attribute(kCharisma, ArtStat::PoisonDamage, AttributeDamage(arts, kCharisma, 3, 2.0, true));
|
||||
}
|
||||
// Only the skills of the weapon in hand.
|
||||
for (const WeaponSkill& weapon : kWeaponSkills) {
|
||||
if (weapon.applies(creature)) {
|
||||
skill(weapon.skill, weapon.curve, ArtStat::AttackValue);
|
||||
skill(weapon.skill, weapon.curve + kCurveSpeedOffset, ArtStat::AttackSpeed);
|
||||
}
|
||||
}
|
||||
skill(kWendigkeit, kCurveWendigkeitValue, ArtStat::AttackValue);
|
||||
}
|
||||
for (const SkillLink& link : links_) {
|
||||
skill(link.skill, link.curve, link.stat);
|
||||
}
|
||||
// Rates: +0xee = (1 + Meditation %) * (1 + Mentale Regeneration / 100),
|
||||
// +0xf2 = 1 + Physische Regeneration / 100.
|
||||
const bool zwerg = *reinterpret_cast<const uint32_t*>(static_cast<uint8_t*>(creature) + kCreatureType) == kTypeZwerg;
|
||||
if (magicRegeneration_ && !zwerg) {
|
||||
skill(kMeditation, kCurveMeditation, ArtStat::RegenerationTime); // a rate, divides the time too
|
||||
rate({Improver::Attribute, kMentaleRegeneration}, AttributeValue(arts, kMentaleRegeneration));
|
||||
} else {
|
||||
rate({Improver::Attribute, kPhysischeRegeneration}, AttributeValue(arts, kPhysischeRegeneration));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::wstring Art::Tooltip(void* creature, std::wstring gameText) const {
|
||||
const auto filters = Filters().find(id_);
|
||||
if (filters != Filters().end()) {
|
||||
for (TooltipFilter filter : filters->second) {
|
||||
filter(*this, creature, gameText);
|
||||
}
|
||||
}
|
||||
std::vector<Improvement> improvements = Improvements(creature);
|
||||
if (improvements.empty()) {
|
||||
return gameText;
|
||||
}
|
||||
// One line per effect, biggest first; an improver with several effects
|
||||
// appears once per effect.
|
||||
std::stable_sort(improvements.begin(), improvements.end(), [](const Improvement& a, const Improvement& b) {
|
||||
return std::fabs(a.percent) > std::fabs(b.percent);
|
||||
});
|
||||
std::wstring text = gameText + L"\n\n" + kHeaderColour + kImprovedByHeader;
|
||||
for (const Improvement& i : improvements) {
|
||||
// Greyed out while it changes nothing (not learned).
|
||||
const long value = std::lround(i.percent);
|
||||
text += L"\n";
|
||||
text += value != 0 ? kTextColour : kInactiveColour;
|
||||
text += ImproverName(i.by) + L" -> " + StatName(i.stat);
|
||||
text += (value < 0 ? L" " : L" +") + std::to_wstring(value) + L"%";
|
||||
}
|
||||
return text + kTextColour;
|
||||
}
|
||||
|
||||
const Art* FindArt(uint16_t id) {
|
||||
static std::map<uint16_t, std::unique_ptr<Art>> arts;
|
||||
auto it = arts.find(id);
|
||||
if (it == arts.end()) {
|
||||
std::unique_ptr<Art> art;
|
||||
if (FindSpellDef(id) || FindCombatMoveDef(id)) {
|
||||
art.reset(new Art(id));
|
||||
}
|
||||
it = arts.emplace(id, std::move(art)).first;
|
||||
}
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
void AddTooltipFilter(uint16_t id, Art::TooltipFilter filter) {
|
||||
Filters()[id].push_back(filter);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
// Combat arts as objects: name, what improves them and by how much, and their
|
||||
// tooltip (docs/art-tooltip.md).
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "game/engine/arts.h"
|
||||
|
||||
namespace game {
|
||||
|
||||
// The heading of the block Art::Tooltip adds; also marks a text as an art tooltip.
|
||||
constexpr const wchar_t* kImprovedByHeader = L"Verbessert durch:";
|
||||
|
||||
// What about an art can be improved.
|
||||
enum class ArtStat : uint8_t {
|
||||
Damage, // all damage of a spell; the physical part of a weapon hit
|
||||
FireDamage,
|
||||
MagicDamage,
|
||||
PoisonDamage,
|
||||
AttackValue,
|
||||
AttackSpeed,
|
||||
ExecutionSpeed, // cast / execution speed while the art is selected (+0x3e)
|
||||
RegenerationTime, // also faster regeneration rates, as the shorter time
|
||||
};
|
||||
|
||||
// A skill or attribute that improves an art.
|
||||
struct Improver {
|
||||
enum Kind : uint8_t { Skill, Attribute } kind;
|
||||
uint8_t id; // game::Skill / game::Attribute
|
||||
bool operator==(const Improver& other) const { return kind == other.kind && id == other.id; }
|
||||
};
|
||||
|
||||
// One stat an improver changes, in percent (regeneration time: negative); 0
|
||||
// for a skill of the hero's class that it has not learned.
|
||||
struct Improvement {
|
||||
Improver by;
|
||||
ArtStat stat;
|
||||
float percent;
|
||||
};
|
||||
|
||||
class Art {
|
||||
public:
|
||||
// Changes a mod makes to an art's tooltip text (the game's description and
|
||||
// values), e.g. values the mod changed.
|
||||
using TooltipFilter = void (*)(const Art& art, void* creature, std::wstring& text);
|
||||
|
||||
uint16_t Id() const { return id_; }
|
||||
std::wstring Name() const;
|
||||
LaunchType Launch() const { return launch_; }
|
||||
bool DealsDamage() const { return damage_; }
|
||||
|
||||
// What improves the art for `creature`: the attributes and the skills of
|
||||
// its class that act on this art, with their current effect.
|
||||
std::vector<Improvement> Improvements(void* creature) const;
|
||||
|
||||
// The full tooltip: `gameText` (cCreatureArts::ArtTooltip's) with the
|
||||
// mods' filters applied, then kImprovedByHeader with one line per effect
|
||||
// of a skill or attribute, biggest first (greyed out while it changes
|
||||
// nothing).
|
||||
std::wstring Tooltip(void* creature, std::wstring gameText) const;
|
||||
|
||||
private:
|
||||
friend const Art* FindArt(uint16_t id);
|
||||
explicit Art(uint16_t id);
|
||||
|
||||
// A skill that improves this art beyond the general rules (the game's
|
||||
// per-skill art lists): its curve and the stat.
|
||||
struct SkillLink {
|
||||
uint8_t skill;
|
||||
uint32_t curve;
|
||||
ArtStat stat;
|
||||
};
|
||||
|
||||
uint16_t id_;
|
||||
uint32_t nameText_ = 0;
|
||||
LaunchType launch_ = LaunchType::Unknown;
|
||||
bool spellValues_ = false; // damage from spell values (+0xb6 factors), else the weapon's (+0xa6)
|
||||
bool magicRegeneration_ = false; // counts down at the magic rate (+0xee)
|
||||
bool damage_ = false;
|
||||
std::vector<SkillLink> links_;
|
||||
};
|
||||
|
||||
// The art `id`, or null if the game has no definition for it.
|
||||
const Art* FindArt(uint16_t id);
|
||||
|
||||
// Registers a mod's tooltip change for art `id`.
|
||||
void AddTooltipFilter(uint16_t id, Art::TooltipFilter filter);
|
||||
|
||||
} // namespace game
|
||||
+24
-3
@@ -40,6 +40,22 @@ inline LaunchType ArtLaunchType(uint16_t id) {
|
||||
return LaunchType::Unknown;
|
||||
}
|
||||
|
||||
// The spell definition an art's damage values come from: its own, else the
|
||||
// spell its combat move names (0x4d1c50 on the CombatMoveDef, plain ret), as
|
||||
// cCreatureArts::ArtTooltip looks it up. Null for pure combat moves.
|
||||
inline const uint8_t* ArtSpellDef(uint16_t id) {
|
||||
if (const uint8_t* spell = FindSpellDef(id)) {
|
||||
return spell;
|
||||
}
|
||||
const uint8_t* move = FindCombatMoveDef(id);
|
||||
if (!move) {
|
||||
return nullptr;
|
||||
}
|
||||
using LinkFn = uint16_t(__fastcall*)(const void*, void*);
|
||||
const uint16_t linked = reinterpret_cast<LinkFn>(0x004d1c50)(move, nullptr);
|
||||
return linked ? FindSpellDef(linked) : nullptr;
|
||||
}
|
||||
|
||||
// cCreature::ResolveArtId (0x5521e0, (ref, 0) as the taskbar calls it): the
|
||||
// art id of an art slot's reference (cInventoryEntry::GetArtSlotRef) for this
|
||||
// creature.
|
||||
@@ -137,6 +153,13 @@ inline uint8_t* FindCooldownEntry(uint8_t* arts, uint16_t art) {
|
||||
return begin + index * kCooldownSize;
|
||||
}
|
||||
|
||||
// Whether cooldown entry `entry` counts down at the magic rate (+0xee): magic
|
||||
// group, or Energieblitze.
|
||||
inline bool EntryAtMagicRate(const uint8_t* entry) {
|
||||
return *reinterpret_cast<const uint32_t*>(entry + kCooldownGroup) == kRegenGroupMagic ||
|
||||
*reinterpret_cast<const uint16_t*>(entry + kCooldownArt) == kArtEnergieblitze;
|
||||
}
|
||||
|
||||
struct ArtCooldown {
|
||||
float remaining = 0.0f; // in the game's units
|
||||
float total = 0.0f;
|
||||
@@ -148,10 +171,8 @@ inline ArtCooldown GetArtCooldown(void* creature, uint16_t art) {
|
||||
if (!entry) {
|
||||
return {};
|
||||
}
|
||||
const bool magic = *reinterpret_cast<uint32_t*>(entry + kCooldownGroup) == kRegenGroupMagic ||
|
||||
*reinterpret_cast<uint16_t*>(entry + kCooldownArt) == kArtEnergieblitze;
|
||||
const float perSecond =
|
||||
Float(entry, kCooldownSpeed) * Float(arts, magic ? kArtsRegenRateMagic : kArtsRegenRateCombat);
|
||||
Float(entry, kCooldownSpeed) * Float(arts, EntryAtMagicRate(entry) ? kArtsRegenRateMagic : kArtsRegenRateCombat);
|
||||
const float remaining = Float(entry, kCooldownRemaining);
|
||||
return {remaining, Float(entry, kCooldownTotal), perSecond > 0.0f ? remaining / perSecond : 0.0f};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#pragma once
|
||||
// A creature's attributes and skills (Fertigkeiten) in cCreatureArts (creature
|
||||
// +0x3a8). See docs/art-tooltip.md for what each of them improves.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "game/ui/texts.h"
|
||||
|
||||
namespace game {
|
||||
|
||||
// Attributes, in the character panel's order.
|
||||
enum Attribute : uint8_t {
|
||||
kStaerke = 0,
|
||||
kWiderstand = 1,
|
||||
kGeschick = 2,
|
||||
kPhysischeRegeneration = 3,
|
||||
kMentaleRegeneration = 4,
|
||||
kCharisma = 5,
|
||||
};
|
||||
constexpr int kAttributeCount = 6;
|
||||
|
||||
// The attribute's bare name: the texts with key hashes 0x440470a.. in
|
||||
// global.res ("Stärke" .. "Charisma"), looked up by hash as GetTextByKey does
|
||||
// (bit 31). The UI_ATTRIB_* key texts also explain the attribute; their first
|
||||
// line is the fallback.
|
||||
constexpr uint32_t kAttributeNameHash = 0x0440470a;
|
||||
inline std::wstring AttributeName(uint8_t attribute) {
|
||||
static constexpr const char* kTextKeys[kAttributeCount] = {
|
||||
"UI_ATTRIB_STRENGTH", "UI_ATTRIB_RESISTANCE", "UI_ATTRIB_GESCHICK",
|
||||
"UI_ATTRIB_PHYREG", "UI_ATTRIB_MAGREG", "UI_ATTRIB_CHARISMA",
|
||||
};
|
||||
if (attribute >= kAttributeCount) {
|
||||
return {};
|
||||
}
|
||||
std::wstring name = GameText(0x80000000 | (kAttributeNameHash + attribute));
|
||||
if (name.empty()) {
|
||||
name = GameTextByKey(kTextKeys[attribute]);
|
||||
name = name.substr(0, name.find_first_of(L"\n:"));
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// cCreatureArts fields. cCreatureArts::Recalculate (0x579560) sets each shown
|
||||
// attribute to invested + base + base * (level - 1) / 10, then adds items.
|
||||
constexpr size_t kArtsAttributes = 0x10; // u16[6]: shown values
|
||||
constexpr size_t kArtsInvested = 0x1c; // u8[6]: attribute points invested
|
||||
constexpr int kFreeAttributesIndex = 7; // u8 +0x23: free points
|
||||
constexpr size_t kArtsSkillIds = 0x24; // u8[8]: skill id by row (0: empty)
|
||||
constexpr size_t kArtsSkillLevels = 0x2c; // u8[8]: bought levels
|
||||
constexpr size_t kArtsSkillBonus = 0x34; // u8[8]: levels from items
|
||||
constexpr size_t kArtsSkillChoices = 0x44; // u16: new skills still to choose
|
||||
constexpr size_t kArtsCreatureType = 0x8; // u32: the creature (hero class) type
|
||||
constexpr size_t kArtsAttributeBase = 0x4a; // u16[6]: the class's base values
|
||||
constexpr size_t kArtsLevel = 0x56; // u16: hero level
|
||||
constexpr int kSkillRows = 8;
|
||||
|
||||
inline uint16_t AttributeValue(const uint8_t* arts, int attribute) {
|
||||
return reinterpret_cast<const uint16_t*>(arts + kArtsAttributes)[attribute];
|
||||
}
|
||||
inline uint16_t AttributeBase(const uint8_t* arts, int attribute) {
|
||||
return reinterpret_cast<const uint16_t*>(arts + kArtsAttributeBase)[attribute];
|
||||
}
|
||||
inline uint16_t HeroLevel(const uint8_t* arts) {
|
||||
return *reinterpret_cast<const uint16_t*>(arts + kArtsLevel);
|
||||
}
|
||||
// The part of an attribute's shown value that comes from items.
|
||||
inline int AttributeFromItems(const uint8_t* arts, int attribute) {
|
||||
const int base = AttributeBase(arts, attribute);
|
||||
const int grown = arts[kArtsInvested + attribute] + base + base * (HeroLevel(arts) - 1) / 10;
|
||||
return AttributeValue(arts, attribute) - grown;
|
||||
}
|
||||
|
||||
// Skill ids (cCreatureArts::ApplySkill 0x565f30 switches on them).
|
||||
enum Skill : uint8_t {
|
||||
kHimmelsmagie = 0x01,
|
||||
kWaffenkunde = 0x02,
|
||||
kStangenwaffen = 0x03,
|
||||
kSchwertkunde = 0x04,
|
||||
kAxtkampf = 0x05,
|
||||
kZweiWaffenKampf = 0x06,
|
||||
kFernkampf = 0x07,
|
||||
kWendigkeit = 0x08,
|
||||
kMeditation = 0x0c,
|
||||
kKlingenkampf = 0x0d,
|
||||
kMagiekunde = 0x0e,
|
||||
kFeuermagie = 0x0f,
|
||||
kWassermagie = 0x10,
|
||||
kErdmagie = 0x11,
|
||||
kLuftmagie = 0x12,
|
||||
kMondmagie = 0x13,
|
||||
kVampirismus = 0x14,
|
||||
kFaustkampf = 0x18,
|
||||
kKonzentration = 0x19,
|
||||
kBallistik = 0x1a,
|
||||
kFallenkunst = 0x1b,
|
||||
kBlutrausch = 0x1c,
|
||||
kWaffentechnik = 0x1d,
|
||||
kZwergenkunde = 0x1f,
|
||||
kHoellenmacht = 0x20,
|
||||
};
|
||||
|
||||
// The level of skill `skill` (bought + from items, as ApplySkill uses it), 0
|
||||
// if the creature has not learned it.
|
||||
inline unsigned SkillLevel(const uint8_t* arts, uint8_t skill) {
|
||||
for (int row = 0; row < kSkillRows; ++row) {
|
||||
if (arts[kArtsSkillIds + row] == skill) {
|
||||
return arts[kArtsSkillLevels + row] + arts[kArtsSkillBonus + row];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The name the character panel shows (game text 0x24b7 + id, 0x6abc8d).
|
||||
inline std::wstring SkillName(uint8_t skill) {
|
||||
return GameText(0x24b7 + skill);
|
||||
}
|
||||
|
||||
// The skills of the creature's class: 16 ids at +0x16 of its type record
|
||||
// (0x43ac50 on the type table *0xaa9564, (type), ret 4; read by
|
||||
// cCreatureArts::ChoosableSkills 0x580f30). Null without a record.
|
||||
constexpr int kClassSkills = 16;
|
||||
inline const uint8_t* ClassSkills(const uint8_t* arts) {
|
||||
using Fn = const uint8_t*(__fastcall*)(void*, void*, uint32_t);
|
||||
void* table = *reinterpret_cast<void**>(0x00aa9564);
|
||||
const uint8_t* record =
|
||||
table ? reinterpret_cast<Fn>(0x0043ac50)(table, nullptr, *reinterpret_cast<const uint32_t*>(arts + kArtsCreatureType))
|
||||
: nullptr;
|
||||
return record ? record + 0x16 : nullptr;
|
||||
}
|
||||
|
||||
// A skill curve of the balance settings (globals parsed from "key=value"
|
||||
// around 0x5eb4xx: "<k>off" start, "<k>_s" half, "<k>_w" limit): 0 below level
|
||||
// 1, else start + 2 (limit - start) (1 - 1 / ((level - 1) / half + 1)), in
|
||||
// percent (DiminishingCurve 0x565c30).
|
||||
struct SkillCurve {
|
||||
float start;
|
||||
float half;
|
||||
float limit;
|
||||
};
|
||||
inline float CurveValue(uint32_t curveAddress, unsigned level) {
|
||||
const auto& curve = *reinterpret_cast<const SkillCurve*>(curveAddress);
|
||||
if (level < 1) {
|
||||
return 0.0f;
|
||||
}
|
||||
const float t = 1.0f - 1.0f / ((static_cast<float>(level) - 1.0f) / curve.half + 1.0f);
|
||||
return 2.0f * (curve.limit - curve.start) * t + curve.start;
|
||||
}
|
||||
constexpr uint32_t kCurveWaffenkunde = 0x00ad2450; // WK
|
||||
constexpr uint32_t kCurveMagiekunde = 0x00ad245c; // MK
|
||||
constexpr uint32_t kCurveFaustkampfValue = 0x00ad2468; // FK..AW, then ..SP
|
||||
constexpr uint32_t kCurveSchwertkundeValue = 0x00ad2480; // SK
|
||||
constexpr uint32_t kCurveAxtkampfValue = 0x00ad2498; // AK
|
||||
constexpr uint32_t kCurveKlingenkampfValue = 0x00ad24b0; // KK
|
||||
constexpr uint32_t kCurveStangenwaffenValue = 0x00ad24c8; // STK
|
||||
constexpr uint32_t kCurveZweiWaffenValue = 0x00ad24e0; // BK
|
||||
constexpr uint32_t kCurveFernkampfValue = 0x00ad24f8; // FEK
|
||||
constexpr uint32_t kCurveExecutionSpeed = 0x00ad2504; // FEK..SP: also the magic schools' speed bonus
|
||||
constexpr uint32_t kCurveHimmelsmagie = 0x00ad2510; // HM
|
||||
constexpr uint32_t kCurveFeuermagie = 0x00ad251c; // FM
|
||||
constexpr uint32_t kCurveErdmagie = 0x00ad2528; // EM
|
||||
constexpr uint32_t kCurveWassermagie = 0x00ad2534; // WM
|
||||
constexpr uint32_t kCurveLuftmagie = 0x00ad2540; // LM
|
||||
constexpr uint32_t kCurveMondmagie = 0x00ad254c; // MM
|
||||
constexpr uint32_t kCurveBallistik = 0x00ad2558; // BA
|
||||
constexpr uint32_t kCurveBlutrausch = 0x00ad2564; // BL
|
||||
constexpr uint32_t kCurveKonzentration = 0x00ad2570; // KO
|
||||
constexpr uint32_t kCurveMeditation = 0x00ad257c; // Med
|
||||
constexpr uint32_t kCurveWendigkeitValue = 0x00ad2588; // W..AW
|
||||
constexpr uint32_t kCurveHoellenmacht = 0x00ad368c; // HOM
|
||||
constexpr uint32_t kCurveWaffentechnik = 0x00ad3698; // WT
|
||||
// Weapon skills have two curves in a row: attack value (AW), then speed (SP).
|
||||
constexpr uint32_t kCurveSpeedOffset = 0xc;
|
||||
|
||||
// Balance settings for the attributes' share of the damage factors
|
||||
// (Recalculate): BalStatOff and the divisors BalCharPD / FD / MD / GD.
|
||||
constexpr uint32_t kBalStatOff = 0x00ad242c;
|
||||
constexpr uint32_t kBalCharDamage[4] = {0x00ad2430, 0x00ad2434, 0x00ad2438, 0x00ad243c};
|
||||
inline float BalanceValue(uint32_t address) {
|
||||
return *reinterpret_cast<const float*>(address);
|
||||
}
|
||||
|
||||
// cCreature::HoldsRangedWeapon (0x54a110, plain ret): Recalculate takes
|
||||
// Geschick instead of Stärke for the physical damage then.
|
||||
inline bool HoldsRangedWeapon(void* creature) {
|
||||
using Fn = bool(__fastcall*)(void*, void*);
|
||||
return reinterpret_cast<Fn>(0x0054a110)(creature, nullptr);
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -126,6 +126,29 @@ std::wstring GameText(uint32_t id) {
|
||||
: std::wstring();
|
||||
}
|
||||
|
||||
std::wstring GameTextByKey(const char* key) {
|
||||
// The key's hash (0x80e010, cdecl), looked up "by key" as GetTextByKey does.
|
||||
using HashFn = uint32_t(__cdecl*)(const char*);
|
||||
const uint32_t hash = reinterpret_cast<HashFn>(0x0080e010)(key);
|
||||
return hash ? GameText(hash | 0x80000000) : std::wstring();
|
||||
}
|
||||
|
||||
std::wstring WideStringText(const void* string) {
|
||||
const auto* s = static_cast<const GameString*>(string);
|
||||
return s && s->begin && s->end > s->begin ? std::wstring(s->begin, s->end) : std::wstring();
|
||||
}
|
||||
|
||||
void SetWideString(void* string, const std::wstring& value) {
|
||||
auto* s = static_cast<GameString*>(string);
|
||||
if (s->begin) {
|
||||
s->end = s->begin;
|
||||
*const_cast<wchar_t*>(s->begin) = L'\0';
|
||||
}
|
||||
// The game's wide string append (0x592ef0, thiscall (first, last, unused), ret 0xc).
|
||||
using AppendFn = void*(__fastcall*)(void*, void*, const wchar_t*, const wchar_t*, uint32_t);
|
||||
reinterpret_cast<AppendFn>(0x00592ef0)(string, nullptr, value.data(), value.data() + value.size(), 0);
|
||||
}
|
||||
|
||||
const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) {
|
||||
if (id >= kFirstOwnText && id < 0x80000000) {
|
||||
std::lock_guard guard(g_lock);
|
||||
|
||||
@@ -23,4 +23,13 @@ void ReplaceInTexts(const std::wstring& from, const std::wstring& to);
|
||||
// The game's text `id` without our changes (empty if there is none).
|
||||
std::wstring GameText(uint32_t id);
|
||||
|
||||
// The game's text under key `key` (TextManager::GetTextByKey 0x672fd0), e.g.
|
||||
// "UI_ATTRIB_STRENGTH"; empty if there is none.
|
||||
std::wstring GameTextByKey(const char* key);
|
||||
|
||||
// A game wide string ({begin, end, capacity end}, e.g. a tooltip's text) as
|
||||
// text, and replaced by `value` (grown with the game's allocator).
|
||||
std::wstring WideStringText(const void* string);
|
||||
void SetWideString(void* string, const std::wstring& value);
|
||||
|
||||
} // namespace game
|
||||
Reference in new issue
Block a user