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
@@ -38,6 +38,7 @@ add_library(sacred_re SHARED
|
|||||||
src/game/engine/items.cpp
|
src/game/engine/items.cpp
|
||||||
src/game/engine/stream.cpp
|
src/game/engine/stream.cpp
|
||||||
src/game/ui/texts.cpp
|
src/game/ui/texts.cpp
|
||||||
|
src/game/engine/art.cpp
|
||||||
src/mods/borderless.cpp
|
src/mods/borderless.cpp
|
||||||
src/mods/widescreen.cpp
|
src/mods/widescreen.cpp
|
||||||
src/mods/hud.cpp
|
src/mods/hud.cpp
|
||||||
@@ -58,6 +59,7 @@ add_library(sacred_re SHARED
|
|||||||
src/mods/combos.cpp
|
src/mods/combos.cpp
|
||||||
src/mods/aurahits.cpp
|
src/mods/aurahits.cpp
|
||||||
src/mods/chainlightning.cpp
|
src/mods/chainlightning.cpp
|
||||||
|
src/mods/arttooltip.cpp
|
||||||
src/mods/escape.cpp
|
src/mods/escape.cpp
|
||||||
src/mods/recordcache.cpp
|
src/mods/recordcache.cpp
|
||||||
src/mods/megamap.cpp
|
src/mods/megamap.cpp
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# What improves an art (mods/arttooltip.cpp)
|
||||||
|
|
||||||
|
Request of 2026-10-18: "edit ALL skill tooltips, to show which attributes and
|
||||||
|
abilities improve them in which way".
|
||||||
|
|
||||||
|
Follow-ups the same day: arts as objects carrying their effects, name and
|
||||||
|
tooltip, no art-id checks in code; a dense block, one line per Fertigkeit or
|
||||||
|
attribute ("Stärke -> Schaden +12%"); only skills and attributes that act on
|
||||||
|
the art; unlearned skills (of the hero's class) with 0% and greyed out;
|
||||||
|
lines sorted by impact. Then: only "Regenerationszeit" (faster
|
||||||
|
regeneration rates are shown as the time they save), no attribute flavor
|
||||||
|
texts; one line per effect (an attribute with two effects gets two lines,
|
||||||
|
each sorted by its own size); a wider tooltip so the lines don't wrap.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
- `game::Art` (game/engine/art.{h,cpp}): built on demand by `FindArt(id)`
|
||||||
|
from the game's definitions (name text SpellDef / CombatMoveDef +4, launch
|
||||||
|
type, spell or weapon damage values, regeneration group).
|
||||||
|
`Improvements(creature)` gives every (skill or attribute, stat, percent)
|
||||||
|
acting on the art for that hero; `Tooltip` = the game's text, the mods'
|
||||||
|
filters (`AddTooltipFilter`, e.g. Blitzschlag's values), then the block.
|
||||||
|
- Art ids appear only in data: the game's per-skill art lists and three
|
||||||
|
exceptions (Energieblitze regenerates as magic; 0x3fa and 0x38 take weapon
|
||||||
|
damage factors), both in art.cpp.
|
||||||
|
- Weapon skills count only with their weapon in hand (the game's checks
|
||||||
|
0x54a3e0 / 0x54a220 / 0x54a2f0 / 0x54a2e0 / 0x54a110, weapon class); only
|
||||||
|
the skills of the weapon in hand are listed. Their second curve (SP) is the attack speed
|
||||||
|
(+0x3c; the skill text labels it with text 0x44c, the same "speed" text the
|
||||||
|
magic schools use for +0x3e).
|
||||||
|
- mods/arttooltip.cpp hooks ArtTooltip and hands the text to the art.
|
||||||
|
- Width: tooltips with flag 0x80 wrap in cUI_Tooltip::FormatText
|
||||||
|
(`0x6e6630`): the first space after 45 characters of a line becomes a line
|
||||||
|
break (`cmp ecx, 5Ah` on the line's bytes at `0x6e687d`); the layout
|
||||||
|
(`0x6e7730`) then sizes the window to the longest line + 8. Colour codes
|
||||||
|
(`\cAARRGGBB`, 10 characters) counted there; the mod replaces the check so
|
||||||
|
they don't, and texts containing "Verbessert durch:" wrap after 56.
|
||||||
|
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
Texts: `scripts/us/global.res` holds the German texts (despite the folder):
|
||||||
|
`"SZ"`, then 16-byte entries {key hash, offset, 0, length}, the UTF-16 text at
|
||||||
|
offset + 4. Skill names are game text `0x24b7 + skill id` (character panel
|
||||||
|
`0x6abc8d`); attribute names `UI_ATTRIB_STRENGTH`, `_RESISTANCE`,
|
||||||
|
`_GESCHICK`, `_PHYREG`, `_MAGREG`, `_CHARISMA`.
|
||||||
|
|
||||||
|
Skill ids (0x565f30's switch, names in file order): 1 Himmelsmagie,
|
||||||
|
2 Waffenkunde, 3 Stangenwaffen, 4 Schwertkunde, 5 Axtkampf,
|
||||||
|
6 Zwei-Waffen-Kampf, 7 Fernkampf, 8 Wendigkeit, 9 Parade, 0xa Konstitution,
|
||||||
|
0xb Rüstung, 0xc Meditation, 0xd Klingenkampf, 0xe Magiekunde, 0xf
|
||||||
|
Feuermagie, 0x10 Wassermagie, 0x11 Erdmagie, 0x12 Luftmagie, 0x13 Mondmagie,
|
||||||
|
0x14 Vampirismus, 0x15 Handel, 0x16 Reiten, 0x17 Entwaffnung, 0x18
|
||||||
|
Faustkampf, 0x19 Konzentration, 0x1a Ballistik, 0x1b Fallenkunst, 0x1c
|
||||||
|
Blutrausch, 0x1d Waffentechnik, 0x1e Zweihandwaffen, 0x1f Zwergenkunde, 0x20
|
||||||
|
Höllenmacht, 0x21 Schmiedekunst.
|
||||||
|
|
||||||
|
Balance values (globals `0xad2400..`, parsed as `key=value` around `0x5eb4xx`;
|
||||||
|
no Balance.txt ships, so the defaults stand). Skill curves are 12-byte
|
||||||
|
{start `<k>off`, half `<k>_s`, limit `<k>_w`} at `0xad2450 + 12 n`: WK
|
||||||
|
Waffenkunde, MK Magiekunde, FK/SK/AK/KK/STK/BK/FEK weapon skills (AW / SP),
|
||||||
|
HM/FM/EM/WM/LM/MM schools, BA, BL, KO Konzentration, Med Meditation, W, HR,
|
||||||
|
HP, P, R (resistances), then the price tables; HOM `0xad368c`, WT `0xad3698`.
|
||||||
|
Value(level) = 0 below 1, else start + 2 (limit − start)(1 − 1/((level − 1)/half + 1))
|
||||||
|
percent (DiminishingCurve `0x565c30`).
|
||||||
|
|
||||||
|
## How the game applies them
|
||||||
|
|
||||||
|
cCreatureArts::Recalculate (`0x579560`), then ApplySkill (`0x565f30`) per
|
||||||
|
skill row with level = bought + item levels:
|
||||||
|
|
||||||
|
- Attributes (+0x10 u16[6]) = invested (+0x1c) + base (+0x4a) + base ×
|
||||||
|
(level − 1) / 10, plus items. Hero level +0x56.
|
||||||
|
- Damage factors, 4 types (physical, fire, magic, poison):
|
||||||
|
weapon / combat arts +0x96 + +0xa6, spells +0x96 + +0xb6 (ArtTooltip
|
||||||
|
multiplies spell values by these; arts 0x3fa and 0x38 by the combat ones).
|
||||||
|
+0x96 = 1 + items. Attribute shares with F = (156 − BalStatOff) × level / 156
|
||||||
|
+ BalStatOff + 9 and invested points p, base b, item points q:
|
||||||
|
- +0xa6 physical: (b F/10 + p + q) of Stärke, or Geschick when
|
||||||
|
HoldsRangedWeapon (`0x54a110`), / BalCharPD
|
||||||
|
- +0xaa fire: (2 p + b F/10 + q) of Stärke and Geschick / BalCharFD
|
||||||
|
- +0xae magic: (b F/10 + p + q) of Mentale Regeneration / BalCharMD
|
||||||
|
- +0xb2 poison: (2 p + b F/10 + q) of Widerstand and Charisma / BalCharGD
|
||||||
|
- +0xb6..+0xc2 spells: (b F/10 + p) of Mentale Regeneration / BalCharMD
|
||||||
|
- Waffenkunde adds its % to +0xa6..+0xb2, Magiekunde to +0xb6..+0xc2.
|
||||||
|
- Regeneration rates: +0xee magic = (1 + items + Meditation %) × (1 +
|
||||||
|
Mentale Regeneration / 100), +0xf2 combat = (1 + items) × (1 + Physische
|
||||||
|
Regeneration / 100). Creature type 8 (Zwerg) gets +0xee = +0xf2.
|
||||||
|
- Regeneration time: ApplySkillToArts (`0x565c80`) divides each listed art's
|
||||||
|
cooldown total by 1 + value / 100. With the attack flag it also adds curve
|
||||||
|
FEK..SP (`0xad2504`) to +0x3e while the art is the selected art slot. Lists
|
||||||
|
(art ids) per skill:
|
||||||
|
- Himmelsmagie (HM, attack): d f 11 12 13 14 15 16 3fb
|
||||||
|
- Feuermagie (FM, attack): 5 b 17 18; Wassermagie (WM): 1b 4 22 23;
|
||||||
|
Erdmagie (EM): 2 10 19 1a; Luftmagie (LM): 1 1c 1d 21;
|
||||||
|
Mondmagie (MM): 3 7 24..2a
|
||||||
|
- Vampirismus (KO): 3ff 401 403 405 40e..411 408 2b 406 407 409..40d
|
||||||
|
- Konzentration (KO): 3e8..3fa 3fc 421..434 41e 3fe 400 402 404 412..416
|
||||||
|
443 446 444 449 44e 44a 44d 44b 44f 44c 436 437 438 43a 43b 43c 441
|
||||||
|
- Ballistik (BA, attack) and Fallenkunst (KO): 417..41d
|
||||||
|
- Blutrausch (BL, attack): 3fe 2b 408 406 407 409..40d
|
||||||
|
- Waffentechnik (WT, attack) and Zwergenkunde (KO): 43f 43e 43d 37..3b
|
||||||
|
- Höllenmacht (HOM): 3c..43
|
||||||
|
- The weapon skills (Stangenwaffen, Schwertkunde, Axtkampf, ...) raise the
|
||||||
|
attack value (+0xe6, AW curve) and attack speed (+0x3c, SP curve) while
|
||||||
|
their weapon is held; listed for weapon arts with that weapon in hand.
|
||||||
|
- The held weapon's damage per type (physical, fire, magic, poison) is at
|
||||||
|
cCreatureArts +0x76 + 4 t (max) and +0x86 + 4 t (min); the damage range
|
||||||
|
functions (0x563e90 .. 0x564160) multiply them by +0x96 + +0xa6 + 4 t. The
|
||||||
|
attributes' damage lines are listed only for the types the weapon deals.
|
||||||
|
|
||||||
|
Measured 2026-10-18 (diag log): +0xa6..+0xb2 and +0xb6 equal the sums of the
|
||||||
|
listed shares exactly (e.g. 5.422 = Waffenkunde 300.96% + Stärke 241.21%),
|
||||||
|
Physische Regeneration matches +0xf2 / (1 + items).
|
||||||
|
|
||||||
|
Art definitions: ArtSpellDef (arts.h) as ArtTooltip finds the spell values
|
||||||
|
(own SpellDef, or the one CombatMoveDef::LinkedSpell `0x4d1c50` names);
|
||||||
|
damage values 4 × (base, per level) s16 at SpellDef +0x42.
|
||||||
@@ -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;
|
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
|
// cCreature::ResolveArtId (0x5521e0, (ref, 0) as the taskbar calls it): the
|
||||||
// art id of an art slot's reference (cInventoryEntry::GetArtSlotRef) for this
|
// art id of an art slot's reference (cInventoryEntry::GetArtSlotRef) for this
|
||||||
// creature.
|
// creature.
|
||||||
@@ -137,6 +153,13 @@ inline uint8_t* FindCooldownEntry(uint8_t* arts, uint16_t art) {
|
|||||||
return begin + index * kCooldownSize;
|
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 {
|
struct ArtCooldown {
|
||||||
float remaining = 0.0f; // in the game's units
|
float remaining = 0.0f; // in the game's units
|
||||||
float total = 0.0f;
|
float total = 0.0f;
|
||||||
@@ -148,10 +171,8 @@ inline ArtCooldown GetArtCooldown(void* creature, uint16_t art) {
|
|||||||
if (!entry) {
|
if (!entry) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const bool magic = *reinterpret_cast<uint32_t*>(entry + kCooldownGroup) == kRegenGroupMagic ||
|
|
||||||
*reinterpret_cast<uint16_t*>(entry + kCooldownArt) == kArtEnergieblitze;
|
|
||||||
const float perSecond =
|
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);
|
const float remaining = Float(entry, kCooldownRemaining);
|
||||||
return {remaining, Float(entry, kCooldownTotal), perSecond > 0.0f ? remaining / perSecond : 0.0f};
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
const void* __fastcall TextManager_GetText(void* self, void* edx, uint32_t id) {
|
||||||
if (id >= kFirstOwnText && id < 0x80000000) {
|
if (id >= kFirstOwnText && id < 0x80000000) {
|
||||||
std::lock_guard guard(g_lock);
|
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).
|
// The game's text `id` without our changes (empty if there is none).
|
||||||
std::wstring GameText(uint32_t id);
|
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
|
} // namespace game
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// Art tooltips name what improves the art (request of 2026-10-18: "edit ALL
|
||||||
|
// skill tooltips, to show which attributes and abilities improve them in
|
||||||
|
// which way"; then: arts as objects with their effects, name and tooltip,
|
||||||
|
// lines like "Damage improved by [Axe Lore, Strength, ...]").
|
||||||
|
//
|
||||||
|
// cCreatureArts::ArtTooltip (0x589270, thiscall on creature +0x3a8, (art,
|
||||||
|
// text, flag), ret 0xc) writes the game's description and values into a wide
|
||||||
|
// string; game::Art::Tooltip (game/engine/art.cpp) applies the mods' changes
|
||||||
|
// and adds "Verbessert durch:" with one line per skill or attribute acting on
|
||||||
|
// the art, one line per effect sorted by size, e.g. "Stärke -> Schaden +12%"
|
||||||
|
// (unlearned skills greyed out with 0%). Combos (0x200..0x204) get no text
|
||||||
|
// from the game. Art tooltips wrap at a wider line (below).
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "game/engine/art.h"
|
||||||
|
#include "game/engine/arts.h"
|
||||||
|
#include "game/ui/texts.h"
|
||||||
|
#include "re/hook.h"
|
||||||
|
#include "re/log.h"
|
||||||
|
#include "re/patch.h"
|
||||||
|
|
||||||
|
namespace mods::arttooltip {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag);
|
||||||
|
RE_HOOK(0x00589270, Arts_Tooltip);
|
||||||
|
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag) {
|
||||||
|
Arts_Tooltip_original(self, edx, art, text, flag);
|
||||||
|
const std::wstring gameText = game::WideStringText(text);
|
||||||
|
const game::Art* object = game::FindArt(static_cast<uint16_t>(art));
|
||||||
|
if (gameText.empty() || !object) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void* creature = static_cast<uint8_t*>(self) - game::kCreatureArts;
|
||||||
|
game::SetWideString(text, object->Tooltip(creature, gameText));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line wrap. With flag 0x80 (+0x154) cUI_Tooltip::FormatText (0x6e6630,
|
||||||
|
// thiscall (char), ret 4; called by SetText 0x6e6ae0) copies the text
|
||||||
|
// (+0x115c begin, +0x1160 end) and turns the first space after 45 characters
|
||||||
|
// of a line into a line break (the check `cmp ecx, 5Ah` on the line's bytes at
|
||||||
|
// 0x6e687d); the width then follows the longest line. Colour codes counted as
|
||||||
|
// characters there. Now they don't, and art tooltips wrap later, so the
|
||||||
|
// "Verbessert durch" lines stay whole.
|
||||||
|
constexpr int kGameWrapChars = 45;
|
||||||
|
constexpr int kArtWrapChars = 56;
|
||||||
|
constexpr size_t kTooltipText = 0x115c;
|
||||||
|
constexpr size_t kColourCodeLength = 10; // "\cAARRGGBB"
|
||||||
|
int g_wrapChars = kGameWrapChars;
|
||||||
|
|
||||||
|
void __fastcall Tooltip_FormatText(void* self, void* edx, char flag);
|
||||||
|
RE_HOOK(0x006e6630, Tooltip_FormatText);
|
||||||
|
void __fastcall Tooltip_FormatText(void* self, void* edx, char flag) {
|
||||||
|
auto* const* text = reinterpret_cast<const wchar_t* const*>(static_cast<uint8_t*>(self) + kTooltipText);
|
||||||
|
const bool art = text[0] && std::wstring_view(text[0], text[1] - text[0]).find(game::kImprovedByHeader) !=
|
||||||
|
std::wstring_view::npos;
|
||||||
|
g_wrapChars = art ? kArtWrapChars : kGameWrapChars;
|
||||||
|
Tooltip_FormatText_original(self, edx, flag);
|
||||||
|
g_wrapChars = kGameWrapChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Characters of the line [start, current) beyond the wrap width, colour codes not counted.
|
||||||
|
int __cdecl WrapExcess(const wchar_t* start, const wchar_t* current) {
|
||||||
|
int chars = 0;
|
||||||
|
for (const wchar_t* c = start; c < current; ++c, ++chars) {
|
||||||
|
if (c[0] == L'\\' && c[1] == L'c') {
|
||||||
|
c += kColourCodeLength - 1;
|
||||||
|
--chars;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chars - g_wrapChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replaces `mov ecx, edi; sub ecx, esi; and ecx, -2; cmp ecx, 5Ah` (line start
|
||||||
|
// in esi, current character in edi, the character in ax); the `jle` after it
|
||||||
|
// keeps the line going.
|
||||||
|
constexpr uint32_t kWrapCheck = 0x006e6876;
|
||||||
|
__declspec(naked) void WrapCheckThunk() {
|
||||||
|
__asm {
|
||||||
|
push eax
|
||||||
|
push edx
|
||||||
|
push edi
|
||||||
|
push esi
|
||||||
|
call WrapExcess
|
||||||
|
add esp, 8
|
||||||
|
mov ecx, eax
|
||||||
|
pop edx
|
||||||
|
pop eax
|
||||||
|
cmp ecx, 0
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Startup() {
|
||||||
|
const int32_t rel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(&WrapCheckThunk) - (kWrapCheck + 5));
|
||||||
|
uint8_t to[10] = {0xe8, 0, 0, 0, 0, 0x90, 0x90, 0x90, 0x90, 0x90};
|
||||||
|
std::memcpy(to + 1, &rel, 4);
|
||||||
|
const uint8_t from[10] = {0x8b, 0xcf, 0x2b, 0xce, 0x83, 0xe1, 0xfe, 0x83, 0xf9, 0x5a};
|
||||||
|
const bool ok = re::ReplaceCode(kWrapCheck, from, to, sizeof(to));
|
||||||
|
re::Log("arttooltip: tooltip wrap ignores colour codes, art tooltips %d characters wide%s", kArtWrapChars,
|
||||||
|
ok ? "" : " (patch FAILED)");
|
||||||
|
}
|
||||||
|
RE_STARTUP(ArtTooltipStartup, Startup);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace mods::arttooltip
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "game/engine/art.h"
|
||||||
#include "game/engine/arts.h"
|
#include "game/engine/arts.h"
|
||||||
#include "game/engine/fx.h"
|
#include "game/engine/fx.h"
|
||||||
#include "game/engine/objects.h"
|
#include "game/engine/objects.h"
|
||||||
@@ -373,8 +374,7 @@ uint32_t __fastcall NextTarget(void* manager, void*, const int32_t* position, ui
|
|||||||
// The tooltip: the game's "Anzahl: X" line (its hit count) becomes the mod's
|
// The tooltip: the game's "Anzahl: X" line (its hit count) becomes the mod's
|
||||||
// two numbers, hits per chain and chains, in both the current level's section
|
// two numbers, hits per chain and chains, in both the current level's section
|
||||||
// and the next level's (skill book). cCreatureArts::ArtTooltip (0x589270,
|
// and the next level's (skill book). cCreatureArts::ArtTooltip (0x589270,
|
||||||
// thiscall on creature +0x3a8, (art, text, flag), ret 0xc) builds the text
|
// mods/arttooltip.cpp, game::Art::Tooltip) builds the text; sections start with the level
|
||||||
// into a wide string {begin, end, capacity}; sections start with the level
|
|
||||||
// line, values are "Name: value" lines separated by '\n' (measured for art
|
// line, values are "Name: value" lines separated by '\n' (measured for art
|
||||||
// 0x21 at level 5: "...|Aktuelle Stufe: 5|Regeneration: 3.5s|Schaden: ...|
|
// 0x21 at level 5: "...|Aktuelle Stufe: 5|Regeneration: 3.5s|Schaden: ...|
|
||||||
// Anzahl: 7", with flag 1 also "Nächste erlernte Stufe: 6" and its values).
|
// Anzahl: 7", with flag 1 also "Nächste erlernte Stufe: 6" and its values).
|
||||||
@@ -386,22 +386,6 @@ constexpr const wchar_t* kCurrentLevelLine = L"Aktuelle Stufe:";
|
|||||||
constexpr const wchar_t* kNextLevelLine = L"Nächste erlernte Stufe:";
|
constexpr const wchar_t* kNextLevelLine = L"Nächste erlernte Stufe:";
|
||||||
constexpr const wchar_t* kCountLine = L"Anzahl:";
|
constexpr const wchar_t* kCountLine = L"Anzahl:";
|
||||||
|
|
||||||
// The game's wide string append (0x592ef0, thiscall (first, last, unused),
|
|
||||||
// ret 0xc); grows the buffer with the game's allocator.
|
|
||||||
void AppendText(void* text, const wchar_t* first, const wchar_t* last) {
|
|
||||||
using Fn = void*(__fastcall*)(void*, void*, const wchar_t*, const wchar_t*, uint32_t);
|
|
||||||
reinterpret_cast<Fn>(0x00592ef0)(text, nullptr, first, last, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetText(void* text, const std::wstring& value) {
|
|
||||||
auto** string = static_cast<wchar_t**>(text);
|
|
||||||
if (string[0]) {
|
|
||||||
string[1] = string[0];
|
|
||||||
*string[0] = L'\0';
|
|
||||||
}
|
|
||||||
AppendText(text, value.data(), value.data() + value.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
std::wstring Rewrite(const std::wstring& text, unsigned currentLevel) {
|
std::wstring Rewrite(const std::wstring& text, unsigned currentLevel) {
|
||||||
std::wstring out;
|
std::wstring out;
|
||||||
unsigned level = currentLevel;
|
unsigned level = currentLevel;
|
||||||
@@ -431,24 +415,13 @@ std::wstring Rewrite(const std::wstring& text, unsigned currentLevel) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag);
|
void RewriteTooltip(const game::Art& art, void* creature, std::wstring& text) {
|
||||||
RE_HOOK(0x00589270, Arts_Tooltip);
|
text = Rewrite(text, ArtLevel(creature, art.Id()));
|
||||||
void __fastcall Arts_Tooltip(void* self, void* edx, uint32_t art, void* text, uint32_t flag) {
|
|
||||||
Arts_Tooltip_original(self, edx, art, text, flag);
|
|
||||||
if (!text || static_cast<uint16_t>(art) != kArtBlitzschlag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto* const* string = static_cast<wchar_t* const*>(text);
|
|
||||||
if (!string[0] || string[1] <= string[0]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void* creature = static_cast<uint8_t*>(self) - game::kCreatureArts;
|
|
||||||
const std::wstring before(string[0], string[1]);
|
|
||||||
const std::wstring after = Rewrite(before, ArtLevel(creature, kArtBlitzschlag));
|
|
||||||
if (after != before) {
|
|
||||||
SetText(text, after);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TooltipRegistration {
|
||||||
|
TooltipRegistration() { game::AddTooltipFilter(kArtBlitzschlag, &RewriteTooltip); }
|
||||||
|
} g_tooltipRegistration;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace mods::chainlightning
|
} // namespace mods::chainlightning
|
||||||
+9
-8
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
#include "game/engine/arts.h"
|
#include "game/engine/arts.h"
|
||||||
#include "game/engine/objects.h"
|
#include "game/engine/objects.h"
|
||||||
|
#include "game/engine/skills.h"
|
||||||
#include "game/ui/events.h"
|
#include "game/ui/events.h"
|
||||||
#include "game/ui/text.h"
|
#include "game/ui/text.h"
|
||||||
#include "mods/hud.h"
|
#include "mods/hud.h"
|
||||||
@@ -46,7 +47,7 @@ constexpr size_t kPlusButtons = 0x2cc;
|
|||||||
constexpr size_t kMinusButtons = 0x8ac; // unused by the game
|
constexpr size_t kMinusButtons = 0x8ac; // unused by the game
|
||||||
constexpr size_t kButtonSize = 0xbc;
|
constexpr size_t kButtonSize = 0xbc;
|
||||||
constexpr int kRows = 8;
|
constexpr int kRows = 8;
|
||||||
constexpr int kAttributeRows = 6;
|
constexpr int kAttributeRows = game::kAttributeCount;
|
||||||
|
|
||||||
// cUI_Button (vtable 0x895308).
|
// cUI_Button (vtable 0x895308).
|
||||||
constexpr size_t kControlFlags = 0x10;
|
constexpr size_t kControlFlags = 0x10;
|
||||||
@@ -85,13 +86,13 @@ constexpr float kIconSize = 16.0f;
|
|||||||
// The − sits left of the value column (values end at the + at x 232).
|
// The − sits left of the value column (values end at the + at x 232).
|
||||||
constexpr int kMinusX = 184;
|
constexpr int kMinusX = 184;
|
||||||
|
|
||||||
// cCreatureArts fields (see the top).
|
// cCreatureArts fields (see the top, game/engine/skills.h).
|
||||||
constexpr size_t kAttributes = 0x1c;
|
using game::kFreeAttributesIndex;
|
||||||
constexpr int kFreeAttributesIndex = 7;
|
constexpr size_t kAttributes = game::kArtsInvested;
|
||||||
constexpr size_t kSkillIds = 0x24;
|
constexpr size_t kSkillIds = game::kArtsSkillIds;
|
||||||
constexpr size_t kSkillLevels = 0x2c;
|
constexpr size_t kSkillLevels = game::kArtsSkillLevels;
|
||||||
constexpr size_t kSkillBonus = 0x34;
|
constexpr size_t kSkillBonus = game::kArtsSkillBonus;
|
||||||
constexpr size_t kSkillChoices = 0x44;
|
constexpr size_t kSkillChoices = game::kArtsSkillChoices;
|
||||||
|
|
||||||
using AddAttributeFn = void(__fastcall*)(void* arts, void*, int index, int delta);
|
using AddAttributeFn = void(__fastcall*)(void* arts, void*, int index, int delta);
|
||||||
using AddSkillLevelsFn = void(__fastcall*)(void* arts, void*, int row, int delta, int spendPoints);
|
using AddSkillLevelsFn = void(__fastcall*)(void* arts, void*, int row, int delta, int spendPoints);
|
||||||
|
|||||||
@@ -292,3 +292,12 @@
|
|||||||
0x00605900 ItemDataMgr::loadHero
|
0x00605900 ItemDataMgr::loadHero
|
||||||
0x00672fd0 TextManager::GetTextByKey # (key string): id = hash 0x80e010
|
0x00672fd0 TextManager::GetTextByKey # (key string): id = hash 0x80e010
|
||||||
0x0080e010 TextKeyHash # h = (h*113 + toupper(c)) % 999999991, & 0x7fffffff
|
0x0080e010 TextKeyHash # h = (h*113 + toupper(c)) % 999999991, & 0x7fffffff
|
||||||
|
0x00565f30 cCreatureArts::ApplySkill # (skill, level, describe out, ...), ret 0x18: skill effects, or their text
|
||||||
|
0x00565c80 cCreatureArts::ApplySkillToArts # (arts[], start, level, half, limit, attack, out, nameText), ret 0x20
|
||||||
|
0x0054a110 cCreature::HoldsRangedWeapon # Geschick instead of Stärke for physical damage
|
||||||
|
0x004d1c50 CombatMoveDef::LinkedSpell # spell id whose values the combat move uses, 0 if none
|
||||||
|
0x0043ac50 TypeTable::CreatureRecord # (type), ret 4: class record (+0x16: 16 skill ids)
|
||||||
|
0x006e6630 cUI_Tooltip::FormatText # (char), ret 4: copies +0x115c to +0x2168, wraps at 45 chars with flag 0x80
|
||||||
|
0x006e6ae0 cUI_Tooltip::SetText # (textId, text*, fixedWidth), ret 0xc
|
||||||
|
0x006e7730 cUI_Tooltip::Layout # measures the text, width = longest line + 8 unless flag 0x100
|
||||||
|
0x0075ac70 cUI_Manager::ShowTooltip # (x, y, text, ?, flags, show)
|
||||||
Reference in new issue
Block a user