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:
khallaandClaude Opus 5.5 committed 2026-10-16 00:11:38 +02:00
1 parent f721d2a486
commit 1fd89524d3
41 files changed
+2347 -214

No files matched your search

+49 -77
View File
@@ -1,24 +1,24 @@
// Skill keys cast the skill, not only select it.
// Skill keys cast their combat-art slot.
//
// The taskbar's combat-art slots are selected with a key (cUI_Taskbar2::
// HandleEvent -> SelectSkillSlot 0x6e17d0); the selected art is then used with
// the right mouse button. When a skill key (src/mods/input.cpp) selects a slot,
// this mod also presses the right mouse button: a WM_RBUTTONDOWN is posted to
// the game window, so the game handles it exactly like a real right-click at
// the cursor (UI hit tests, target under the cursor, cEvent_creature 0x105).
// The button is released with the key, so holding the key is holding the
// button. Slots selected by clicking them with the mouse are not cast.
// The game casts one art with the right mouse button: the one in the
// "selected" art slot, which is always slot 0 now (src/mods/skillbar.cpp).
// Slot 0's key presses the right mouse button; the keys of slots 1-6 first
// make their slot the selected one. The button is pressed by posting
// WM_RBUTTONDOWN to the game window, so the game handles it exactly like a
// real right-click at the cursor (UI hit tests, target under the cursor,
// cEvent_creature 0x105), and released with the key: holding the key is
// holding the button. While no skill key is held, slot 0 is selected again
// (checked every engine tick, which also covers saves loaded with another
// selection).
#include "mods/skillkeys.h"
#include "mods/skillbar.h"
#include <cstdint>
#include <mutex>
#include <atomic>
#include <windows.h>
#include "re/hook.h"
#include "game/engine/events.h"
#include "mods/skillbar.h"
namespace mods::skillkeys {
namespace {
@@ -40,77 +40,49 @@ LPARAM CursorClientPos(HWND window) {
return MAKELPARAM(cursor.x, cursor.y);
}
// The key that is to cast slot `slot` when it gets selected, then the key
// holding the right mouse button down. Set on the window thread, used on the
// thread that handles UI events.
struct State {
std::mutex lock;
int pendingKey = 0;
unsigned pendingSlot = 0;
bool keyDown = false;
int heldKey = 0; // right button is down for this key
HWND window = nullptr;
};
State g_state;
// The key holding the right mouse button down, and the window it was pressed
// in. Set on the window thread, read by the engine tick.
std::atomic<int> g_heldKey{0};
HWND g_window = nullptr;
void OnTick(uint8_t*) {
if (!g_heldKey.load() && mods::skillbar::SelectedSkillSlot() != 0) {
mods::skillbar::SelectSkillSlot(0);
}
}
struct Registration {
Registration() { game::OnEngineTick(&OnTick); }
} g_registration;
} // namespace
void CastOnSelect(unsigned slot, int vk) {
std::lock_guard guard(g_state.lock);
g_state.pendingKey = vk;
g_state.pendingSlot = slot;
g_state.keyDown = true;
void KeyPressed(unsigned slot, int vk) {
if (g_heldKey.load() || !mods::skillbar::SkillSlotHasArt(slot)) {
return;
}
HWND window = GameWindow();
if (!window) {
return;
}
g_window = window;
g_heldKey = vk; // before selecting: the tick leaves the selection alone
if (slot != 0) {
mods::skillbar::SelectSkillSlot(slot);
}
PostMessageW(window, WM_RBUTTONDOWN, MK_RBUTTON, CursorClientPos(window));
}
bool KeyHeld() {
return g_heldKey.load() != 0;
}
void KeyReleased(int vk) {
std::lock_guard guard(g_state.lock);
if (g_state.pendingKey == vk) {
g_state.keyDown = false; // released before the slot was selected
}
if (g_state.heldKey == vk) {
g_state.heldKey = 0;
PostMessageW(g_state.window, WM_RBUTTONUP, 0, CursorClientPos(g_state.window));
}
}
// After a slot was selected: cast if a skill key asked for it.
void OnSlotSelected(void* taskbar, unsigned slot) {
std::lock_guard guard(g_state.lock);
if (!g_state.pendingKey || g_state.pendingSlot != slot) {
if (g_heldKey.load() != vk) {
return;
}
const int key = g_state.pendingKey;
g_state.pendingKey = 0;
if (!mods::skillbar::HasSkillSlot(taskbar, slot)) {
return; // empty slot: nothing was selected
}
HWND window = GameWindow();
if (!window || g_state.heldKey) {
return;
}
PostMessageW(window, WM_RBUTTONDOWN, MK_RBUTTON, CursorClientPos(window));
if (g_state.keyDown) {
g_state.heldKey = key;
g_state.window = window;
} else {
PostMessageW(window, WM_RBUTTONUP, 0, CursorClientPos(window));
}
PostMessageW(g_window, WM_RBUTTONUP, 0, CursorClientPos(g_window));
g_heldKey = 0;
}
} // namespace mods::skillkeys
namespace game {
// cUI_Taskbar2::SelectSkillSlot (0x6e17d0): makes combat-art slot `slot` the
// right mouse button's art (keys, clicks on the slot).
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, unsigned slot, int notify);
RE_HOOK(0x006e17d0, UiTaskbar_SelectSkillSlot);
void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, unsigned slot, int notify) {
UiTaskbar_SelectSkillSlot_original(self, edx, slot, notify);
if (self) {
mods::skillkeys::OnSlotSelected(self, slot & 0xffff);
}
}
} // namespace game