Files
sacred_k/src/mods/aurahits.cpp
T
khallaandClaude Opus 5.5 1fd89524d3 Skills, auto-attack, aura and chain lightning rework, Esc, cache fix
- 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>
2026-10-16 00:11:38 +02:00

121 lines
4.8 KiB
C++

// Periodic aura hits strike every creature in range, not one.
//
// Effects of kind 0x12 (Blicke wie Klingen) are handled in the creature arts
// update (branch at 0x562f08, local player only): every second (timer at
// effect +8) it walks the creatures near the player (g_nearbyCreatures,
// 0xad1c60, 8 bytes each, count g_nearbyCreatureCount 0xad1b48), keeps one of
// those within (cCreatureArts +0x56 + 20) * 10 of the owner (the first,
// replaced by later ones with 1/3 chance) and sends it one hit (cEvent_creature
// 5, the effect's value as one random damage type) through its ReceiveOrder.
// The damage is made physical (user's choice: no random fire/magic/poison),
// and the hits name the owner as their attacker, so the game applies its life
// leech (with beam and sound) as for weapon hits.
//
// That call (`mov eax, [ecx]; call [eax+18h]` at 0x56323b) now goes to
// HitAllInRange: the chosen creature gets its hit, then every other creature
// of the list within the same range gets the same hit. Rebalancing request of
// 2026-10-15, see docs/aura-hits.md.
#include <cstdint>
#include <cstring>
#include "game/engine/arts.h"
#include "game/engine/objects.h"
#include "game/engine/orders.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::aurahits {
namespace {
constexpr uint32_t kHitCall = 0x0056323b; // mov eax, [ecx]; call [eax+18h]
constexpr uint32_t kEffectKindPeriodicHit = 0x12;
constexpr size_t kArtsHitRangeLevel = 0x56; // u16: range = (value + 20) * 10
// The hit's damage: four u16 slots at +0x20 (physical, fire, magic,
// poison); the game fills one of them at random.
constexpr size_t kHitDamage = 0x20;
constexpr int kDamageTypes = 4;
void MakePhysical(game::CreatureOrder& hit) {
auto* damage = reinterpret_cast<uint16_t*>(reinterpret_cast<uint8_t*>(&hit) + kHitDamage);
uint32_t total = 0;
for (int i = 0; i < kDamageTypes; ++i) {
total += damage[i];
damage[i] = 0;
}
damage[0] = static_cast<uint16_t>(total > 0xfffe ? 0xfffe : total);
}
void Send(void* creature, game::CreatureOrder* order) {
using ReceiveFn = void(__fastcall*)(void*, void*, void*);
reinterpret_cast<ReceiveFn>((*reinterpret_cast<void***>(creature))[0x18 / 4])(creature, nullptr, order);
}
// Sends the hit with the owner as its attacker (order +0x1c): the game leaves
// it 0 for aura hits, so the damage code (0x56aff0) does not apply the
// attacker's life leech, its beam and sound. With the attacker set, it does.
void Hit(void* owner, void* creature, game::CreatureOrder* order) {
order->b = game::CreatureField(owner, game::kCreatureId);
Send(creature, order);
}
} // namespace
// (ecx = the chosen creature, the hit, cCreatureArts in ebp, the effect's
// offset in its list in ebx)
void __fastcall HitAllInRange(void* chosen, void*, game::CreatureOrder* hit, uint8_t* arts, uint32_t effect) {
auto* effects = *reinterpret_cast<uint8_t**>(arts + game::kEffectsBegin);
void* owner = *reinterpret_cast<void**>(arts + game::kArtsCreature);
if (!effects || !owner ||
*reinterpret_cast<uint32_t*>(effects + effect + game::kEffectKind) != kEffectKindPeriodicHit) {
Send(chosen, hit);
return;
}
MakePhysical(*hit);
const game::CreatureOrder original = *hit; // the receiver may change it
Hit(owner, chosen, hit);
const uint32_t range = (*reinterpret_cast<uint16_t*>(arts + kArtsHitRangeLevel) + 20u) * 10u;
const auto chosenId = static_cast<uint32_t>(game::CreatureField(chosen, game::kCreatureId));
const int count = game::NearbyCreatureCount();
for (int i = 0; i < count; ++i) {
const uint32_t id = game::NearbyCreatureId(i);
// Player heroes need the game's PvP checks (0x4d4be0): left out.
if (id == chosenId || game::IsPlayerHeroId(id) || game::DistanceTo(owner, id) >= range) {
continue;
}
void* creature = game::GetCreature(id);
if (!game::IsHostileTo(owner, creature)) {
continue;
}
game::CreatureOrder copy = original;
Hit(owner, creature, &copy);
}
}
namespace {
__declspec(naked) void HitAllInRangeThunk() {
__asm {
push ebx // effect offset
push ebp // cCreatureArts
push dword ptr [esp+0Ch] // the hit (the call's argument)
call HitAllInRange
ret 4
}
}
void Startup() {
const auto target = reinterpret_cast<uintptr_t>(&HitAllInRangeThunk);
const int32_t rel = static_cast<int32_t>(target - (kHitCall + 5));
uint8_t to[5] = {0xe8};
std::memcpy(to + 1, &rel, 4);
const uint8_t from[5] = {0x8b, 0x01, 0xff, 0x50, 0x18};
const bool ok = re::ReplaceCode(kHitCall, from, to, sizeof(to));
re::Log("aurahits: periodic aura hits strike all creatures in range%s", ok ? "" : " (patch FAILED)");
}
RE_STARTUP(AuraHitsStartup, Startup);
} // namespace
} // namespace mods::aurahits