Combos: cooldown labels on the skill bar, steps follow the arts' current levels

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-18 00:43:03 +02:00
1 parent 5a0db24b83
commit 075f9e2c8a
6 files changed
+199 -4

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
// Combos use the current levels of the arts they are made of (request of
// 2026-10-18).
//
// A combo (cCreatureArts +0x10e, layout in game/engine/arts.h) keeps the arts
// of its steps and a copy of their levels from when it was made at the
// Kombomeister; its regeneration total and the steps follow those levels, so
// raising an art later does not change the combo. Every engine tick the
// hero's combos are compared with the arts' current levels (cooldown entry
// level + bonus levels); a combo whose levels are out of date is set again
// with the game's own cCreatureArts::SetCombo (0x581290), which recomputes the
// total. The charge keeps its fraction of the total.
#include <cstdint>
#include <cstring>
#include "game/engine/arts.h"
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "re/log.h"
namespace game {
// cCreatureArts::SetCombo (0x581290, ret 0x10).
using SetComboFn = void(__fastcall*)(void* arts, void* edx, uint32_t index, const uint32_t* stepArts,
const uint16_t* levels, uint32_t id);
} // namespace game
namespace mods::combos {
namespace {
using namespace game; // combo and cooldown layout (arts.h)
// The art's level now (cooldown entry level + bonus levels), 0 if unknown.
uint16_t CurrentLevel(uint8_t* arts, uint32_t art) {
uint8_t* entry = art ? FindCooldownEntry(arts, static_cast<uint16_t>(art)) : nullptr;
return entry ? static_cast<uint16_t>(entry[kCooldownLevel] + entry[kCooldownBonusLevels]) : 0;
}
void SyncCombos(uint8_t* hero) {
uint8_t* arts = hero + kCreatureArts;
if (hero[kCreatureComboStep] != 0 || arts[kArtsComboRunning] != 0) {
return; // not while a combo is being executed
}
for (unsigned index = 0;; ++index) {
uint8_t* combo = ComboEntry(arts, index);
if (!combo) {
return;
}
const uint16_t id = *reinterpret_cast<uint16_t*>(combo + kComboId);
if (id == 0) {
continue;
}
uint32_t stepArts[kComboSteps];
uint16_t levels[kComboSteps];
std::memcpy(stepArts, combo + kComboArts, sizeof(stepArts));
std::memcpy(levels, combo + kComboLevels, sizeof(levels));
bool changed = false;
for (unsigned step = 0; step < kComboSteps; ++step) {
const uint16_t level = CurrentLevel(arts, stepArts[step]);
if (level != 0 && level != levels[step]) { // unknown arts keep their level
levels[step] = level;
changed = true;
}
}
if (!changed) {
continue;
}
const float oldTotal = Float(combo, kComboTotal);
const float fraction = oldTotal > 0.0f ? Float(combo, kComboCharge) / oldTotal : 1.0f;
reinterpret_cast<SetComboFn>(0x00581290)(arts, nullptr, index, stepArts, levels, id);
combo = ComboEntry(arts, index); // SetCombo does not reallocate, but stay safe
if (combo) {
Float(combo, kComboCharge) = fraction * Float(combo, kComboTotal);
re::Log("combos: combo %u set to the current levels %u/%u/%u/%u", index, levels[0], levels[1], levels[2],
levels[3]);
}
}
}
void OnTick(uint8_t*) {
if (auto* hero = static_cast<uint8_t*>(GetPlayerHero())) {
SyncCombos(hero);
}
}
struct Registration {
Registration() {
OnEngineTick(&OnTick);
}
} g_registration;
} // namespace
} // namespace mods::combos
+7 -4
View File
@@ -20,6 +20,7 @@
//
// Recharging arts show their remaining cooldown (seconds, one decimal) on the
// slot, drawn with the game's font after RenderArtSlots.
// Combos too, from their charge (game::GetComboCooldown).
//
// Also: 7 entries in the slot-count-by-level table (6 at level 45, 7 at 65),
// and cCreature::AssignArtToSlot accepts slots up to 6.
@@ -184,7 +185,8 @@ void UpdateLabelRects(uint8_t* const* skills) {
// The remaining cooldown in seconds (at the hero's current regeneration
// rate, game::GetArtCooldown), to one decimal, centred on each art slot that
// is recharging (combat moves and spells: slot types 2 / 3).
// is recharging (combat moves and spells: slot types 2 / 3; combos: 4,
// game::GetComboCooldown).
void RenderCooldowns(void* taskbar, void* renderer) {
auto* p = static_cast<uint8_t*>(taskbar);
uint8_t* inventory = skillslots::Inventory(*reinterpret_cast<const uint16_t*>(p + kInventoryIndex));
@@ -203,11 +205,12 @@ void RenderCooldowns(void* taskbar, void* renderer) {
for (unsigned slot = 0; slot < kSlots && slot < shown; ++slot) {
const uint32_t type = getType(inventory, nullptr, slot);
const uint32_t ref = getRef(inventory, nullptr, slot);
if (!skills[slot] || (type != 2 && type != 3) || !ref) {
if (!skills[slot] || (type != 2 && type != 3 && type != game::kArtSlotTypeCombo) || !ref) {
continue;
}
const uint16_t art = game::ResolveArtId(hero, ref);
const game::ArtCooldown cooldown = game::GetArtCooldown(hero, art);
const game::ArtCooldown cooldown = type == game::kArtSlotTypeCombo
? game::GetComboCooldown(hero, ref)
: game::GetArtCooldown(hero, game::ResolveArtId(hero, ref));
if (cooldown.remaining <= game::kArtReadyCooldown) {
continue; // ready: no label, the game's icon shows it
}