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,179 @@
|
||||
// Auto-attack: once the hero attacked or cast an art at an enemy, it keeps
|
||||
// attacking that enemy with its weapon whenever it would stand idle.
|
||||
//
|
||||
// A left click on a creature sends the hero one attack order (cEvent_creature
|
||||
// 7, target id at +0x18, its position at +0x1c / +0x20); the hero swings once
|
||||
// and stands again (+0x150 back to 0x10000). Arts are orders 0x105 with the
|
||||
// target at +0x10. Both are seen in cCreature::ReceiveOrder; their target
|
||||
// becomes the hero's combat target if it is a living creature and not an ally.
|
||||
// Every engine tick in which the hero is idle, no mouse button and no skill
|
||||
// key is held, the attack order a left click sends goes to the target again.
|
||||
// A walk order (mouse move, WASD, stop) or the target's death ends it.
|
||||
// Between swings the hero does not go idle at all: the end-of-swing check for
|
||||
// a held mouse button is answered "held" for the auto-attack target.
|
||||
// Measured with an order log, see docs/auto-attack.md.
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
#include "game/engine/arts.h"
|
||||
#include "game/engine/events.h"
|
||||
#include "game/engine/objects.h"
|
||||
#include "game/engine/orders.h"
|
||||
#include "mods/skillkeys.h"
|
||||
#include "re/hook.h"
|
||||
#include "re/patch.h"
|
||||
|
||||
namespace mods::autoattack {
|
||||
namespace {
|
||||
|
||||
// After an attack order, wait this many ticks for the hero to start it before
|
||||
// sending another one (it takes about 2 ticks).
|
||||
constexpr int kStartTicks = 5;
|
||||
constexpr int32_t kActionAttack = 3; // +0x150 low word
|
||||
|
||||
std::atomic<uint32_t> g_target{0};
|
||||
int g_waitTicks = 0;
|
||||
|
||||
bool IsEnemy(void* hero, uint32_t id) {
|
||||
if (id == 0 || id == static_cast<uint32_t>(game::CreatureField(hero, game::kCreatureId))) {
|
||||
return false;
|
||||
}
|
||||
return game::IsHostileTo(hero, game::GetCreature(id));
|
||||
}
|
||||
|
||||
bool Idle(void* creature) {
|
||||
return (game::CreatureField(creature, game::kCreatureAction) & 0xffff) == 0;
|
||||
}
|
||||
|
||||
void OnTick(uint8_t* engine) {
|
||||
const uint32_t id = g_target.load();
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
void* hero = game::ControlledCreature(engine);
|
||||
void* target = game::GetCreature(id);
|
||||
if (!hero || !target || !game::IsAlive(target)) {
|
||||
g_target = 0;
|
||||
return;
|
||||
}
|
||||
if (!Idle(hero)) {
|
||||
g_waitTicks = 0;
|
||||
return;
|
||||
}
|
||||
const uint32_t flags = *reinterpret_cast<uint32_t*>(engine + game::kEngineFlags);
|
||||
if (g_waitTicks > 0 && --g_waitTicks > 0) {
|
||||
return;
|
||||
}
|
||||
if ((flags & 0x4010) != 0 || mods::skillkeys::KeyHeld()) {
|
||||
return; // mouse button held / input blocked / a skill key casts
|
||||
}
|
||||
game::CreatureOrder order;
|
||||
order.command = game::kOrderAttack;
|
||||
order.a = static_cast<int32_t>(id);
|
||||
order.b = game::CreatureField(target, game::kCreatureX);
|
||||
order.c = game::CreatureField(target, game::kCreatureY);
|
||||
game::SendCreatureOrder(engine, hero, order);
|
||||
g_waitTicks = kStartTicks;
|
||||
}
|
||||
|
||||
struct Registration {
|
||||
Registration() { game::OnEngineTick(&OnTick); }
|
||||
} g_registration;
|
||||
|
||||
// The end of a swing (0x4e6584, after the hit) asks the mouse cursor whether
|
||||
// the left button is still held (cMouseCursor::IsButtonHeld, call at
|
||||
// 0x4e65c6; attacker in esi, target creature in ebp): if so the attacker
|
||||
// swings again right away, otherwise it recovers (+0x152 = 2) and stands.
|
||||
// While the hero auto-attacks its target, the answer is yes, so swings follow
|
||||
// each other at the speed of a held button.
|
||||
constexpr uint32_t kSwingEndButtonCheck = 0x004e65c6;
|
||||
constexpr uint32_t kIsButtonHeld = 0x00655ab0;
|
||||
|
||||
char __fastcall SwingContinues(void* cursor, void*, void* attacker, void* target) {
|
||||
using IsButtonHeldFn = char(__fastcall*)(void*, void*);
|
||||
if (reinterpret_cast<IsButtonHeldFn>(kIsButtonHeld)(cursor, nullptr)) {
|
||||
return 1;
|
||||
}
|
||||
const uint32_t id = g_target.load();
|
||||
void* hero = game::GetPlayerHero();
|
||||
const uint32_t targetId = target ? static_cast<uint32_t>(game::CreatureField(target, game::kCreatureId)) : 0;
|
||||
if (!id || !target || attacker != hero || mods::skillkeys::KeyHeld() || targetId != id) {
|
||||
return 0;
|
||||
}
|
||||
return game::IsAlive(target) ? 1 : 0;
|
||||
}
|
||||
|
||||
// Called instead of IsButtonHeld (ecx = the cursor, attacker in esi, target
|
||||
// in ebp).
|
||||
__declspec(naked) void SwingContinuesThunk() {
|
||||
__asm {
|
||||
push ebp
|
||||
push esi
|
||||
call SwingContinues
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// During a swing the attack code also checks the button (call at 0x4e7101)
|
||||
// and clears the global "attack held" flag (0xab6030) when it is up; the swing
|
||||
// end only chains while that flag is set (0x4e6641). The flag exists for the
|
||||
// local player only, so the answer is "held" whenever the hero auto-attacks.
|
||||
constexpr uint32_t kSwingButtonCheck = 0x004e7101;
|
||||
|
||||
char __fastcall AttackHeld(void* cursor, void*) {
|
||||
using IsButtonHeldFn = char(__fastcall*)(void*, void*);
|
||||
if (reinterpret_cast<IsButtonHeldFn>(kIsButtonHeld)(cursor, nullptr)) {
|
||||
return 1;
|
||||
}
|
||||
const uint32_t id = g_target.load();
|
||||
void* hero = game::GetPlayerHero();
|
||||
if (!id || !hero || mods::skillkeys::KeyHeld() ||
|
||||
(game::CreatureField(hero, game::kCreatureAction) & 0xffff) != kActionAttack) {
|
||||
return 0;
|
||||
}
|
||||
void* target = game::GetCreature(id);
|
||||
return target && game::IsAlive(target) ? 1 : 0;
|
||||
}
|
||||
|
||||
void Startup() {
|
||||
re::RedirectCall(kSwingEndButtonCheck, kIsButtonHeld, reinterpret_cast<const void*>(&SwingContinuesThunk));
|
||||
re::RedirectCall(kSwingButtonCheck, kIsButtonHeld, reinterpret_cast<const void*>(&AttackHeld));
|
||||
}
|
||||
RE_STARTUP(AutoAttackStartup, Startup);
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
|
||||
// An order for the player's hero.
|
||||
void OnOrder(void* creature, const game::CreatureOrder& order, void*) {
|
||||
void* hero = game::GetPlayerHero();
|
||||
if (creature != hero) {
|
||||
return;
|
||||
}
|
||||
switch (order.command) {
|
||||
case game::kOrderWalk:
|
||||
g_target = 0;
|
||||
break;
|
||||
case game::kOrderAttack:
|
||||
if (IsEnemy(hero, static_cast<uint32_t>(order.a))) {
|
||||
g_target = static_cast<uint32_t>(order.a);
|
||||
}
|
||||
break;
|
||||
case game::kOrderUseArt:
|
||||
if (IsEnemy(hero, order.target)) {
|
||||
g_target = order.target;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
struct OrderRegistration {
|
||||
OrderRegistration() { game::OnCreatureOrder(&OnOrder); }
|
||||
} g_orderRegistration;
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace mods::autoattack
|
||||
|
||||
Reference in new issue
Block a user