// 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 #include #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(reinterpret_cast(&hit) + kHitDamage); uint32_t total = 0; for (int i = 0; i < kDamageTypes; ++i) { total += damage[i]; damage[i] = 0; } damage[0] = static_cast(total > 0xfffe ? 0xfffe : total); } void Send(void* creature, game::CreatureOrder* order) { using ReceiveFn = void(__fastcall*)(void*, void*, void*); reinterpret_cast((*reinterpret_cast(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(arts + game::kEffectsBegin); void* owner = *reinterpret_cast(arts + game::kArtsCreature); if (!effects || !owner || *reinterpret_cast(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(arts + kArtsHitRangeLevel) + 20u) * 10u; const auto chosenId = static_cast(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, ©); } } 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(&HitAllInRangeThunk); const int32_t rel = static_cast(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