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>
This commit is contained in:
1 parent
f721d2a486
commit
1fd89524d3
41 files changed
+2347
-214
No files matched your search
@@ -0,0 +1,120 @@
|
||||
// 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
|
||||
Reference in new issue
Block a user