- Skill bar: no art selection; slot 0 is the right-click art, keys 0-6 cast slots directly; cooldown labels in exact seconds; slot 0 set apart. - Arts regenerate on their own (casting no longer refills the group). - Auto-attack after targeting or casting at a hostile creature, at the speed of a held button. - Auras can be recast while active (renew, no second visual). - Blicke wie Klingen hits every enemy in range, physical, with life leech. - Blitzschlag steers the game's own chain: 3 + level/5 hits, 1 + level/10 chains, re-hits a lone enemy. - Weapon sets: click selects, drag only from the selected set; the leftmost set is clickable (signed hit tests). - Esc closes open windows before opening the game menu. - HUD: standalone buttons and texts are centred. - Record cache sized for widened views (town lag while walking). - Shared game code: orders, fx events, text drawing, cooldowns, effects. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
101 lines
3.4 KiB
C++
101 lines
3.4 KiB
C++
// Aura visuals after loading a save.
|
|
//
|
|
// Casting a lasting self aura (Heldenmut 0x3f7, Klingenblick 0x3f8) adds a
|
|
// buff to the hero's active effects and, separately, creates an aura effect
|
|
// object (cObjectFX type 842) that follows the hero: the art's cast effect
|
|
// (0x4feef0) creates it with cObjectManager::CreateAt at the hero and sends it
|
|
// a cEventFX 0x42 (owner, duration, art). Saving marks effect objects as dead;
|
|
// loading restores the buff but drops the effect, so the aura works but is
|
|
// invisible. A few ticks after a save game has loaded, the effect is created
|
|
// again for every such buff of a player hero, with the buff's remaining time.
|
|
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
#include "game/engine/arts.h"
|
|
#include "game/engine/events.h"
|
|
#include "game/engine/fx.h"
|
|
#include "game/engine/objects.h"
|
|
#include "game/engine/orders.h"
|
|
#include "re/log.h"
|
|
|
|
namespace mods::aura_restore {
|
|
namespace {
|
|
|
|
constexpr uint32_t kAuraEffectType = 842; // cObjectFX: aura following its owner
|
|
constexpr uint16_t kAuraArts[] = {game::kArtHeldenmut, game::kArtKlingenblick};
|
|
constexpr int kTicksAfterLoad = 5; // wait for the load to finish (0.5 s)
|
|
|
|
using namespace game; // effect layout (arts.h)
|
|
|
|
int g_ticksUntilRestore = -1;
|
|
|
|
void AttachAura(uint8_t* hero, uint16_t art, float remaining) {
|
|
const uint32_t heroId = *reinterpret_cast<uint32_t*>(hero + kCreatureId);
|
|
const uint32_t id = CreateObjectAt(kAuraEffectType, hero + 0x18); // the hero's position
|
|
auto* effect = static_cast<uint8_t*>(game::GetObjectById(id));
|
|
if (!effect) {
|
|
return;
|
|
}
|
|
uint32_t event[kFxEventDwords] = {};
|
|
event[0] = kFxEventVtable;
|
|
event[1] = kFxEventAttachAura;
|
|
event[2] = heroId;
|
|
std::memcpy(&event[11], &remaining, 4);
|
|
event[15] = art;
|
|
SendEvent(effect, event);
|
|
re::Log("aura: restored art %x on hero %u (%.1f s left)", art, heroId, remaining);
|
|
}
|
|
|
|
// Longest remaining time of the buffs (any kind but the lock) of `art`.
|
|
float RemainingBuff(uint8_t* hero, uint16_t art) {
|
|
uint8_t* arts = hero + kCreatureArts;
|
|
auto* entry = *reinterpret_cast<uint8_t**>(arts + kEffectsBegin);
|
|
auto* end = *reinterpret_cast<uint8_t**>(arts + kEffectsEnd);
|
|
float longest = 0.0f;
|
|
for (; entry && entry < end; entry += kEffectSize) {
|
|
float remaining;
|
|
std::memcpy(&remaining, entry + kEffectRemaining, 4);
|
|
if (*reinterpret_cast<uint32_t*>(entry + kEffectKind) != kEffectKindArtLock &&
|
|
*reinterpret_cast<uint16_t*>(entry + kEffectArt) == art && remaining > longest) {
|
|
longest = remaining;
|
|
}
|
|
}
|
|
return longest;
|
|
}
|
|
|
|
void RestoreAuras() {
|
|
for (uint32_t id = 1; id < kPlayerHeroIdEnd; ++id) {
|
|
auto* hero = static_cast<uint8_t*>(game::GetCreature(id));
|
|
if (!hero) {
|
|
continue;
|
|
}
|
|
for (uint16_t art : kAuraArts) {
|
|
const float remaining = RemainingBuff(hero, art);
|
|
if (remaining > 0.0f) {
|
|
AttachAura(hero, art, remaining);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnLoaded() {
|
|
g_ticksUntilRestore = kTicksAfterLoad;
|
|
}
|
|
|
|
void OnTick(uint8_t*) {
|
|
if (g_ticksUntilRestore >= 0 && g_ticksUntilRestore-- == 0) {
|
|
RestoreAuras();
|
|
}
|
|
}
|
|
|
|
struct Registration {
|
|
Registration() {
|
|
game::OnSaveGameLoaded(&OnLoaded);
|
|
game::OnEngineTick(&OnTick);
|
|
}
|
|
} g_registration;
|
|
|
|
} // namespace
|
|
} // namespace mods::aura_restore
|