- 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>
121 lines
4.8 KiB
C++
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, ©);
|
|
}
|
|
}
|
|
|
|
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
|