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

+32
View File
@@ -0,0 +1,32 @@
# Art regeneration (`src/mods/artregen.cpp`, `game::GetArtCooldown`)
Goal (user, 2026-10-15): casting an art no longer affects the cooldowns of
other arts; the cooldown labels show exact seconds.
## How the game regenerates arts (measured 2026-10-15)
Cooldown entries: see `src/game/engine/arts.h` (cCreatureArts +0x102,
0x16 bytes each: group, art, level, bonus levels, total, speed factor,
remaining). Found with a hardware write watch on one entry's remaining time:
- countdown, every creature update (`0x562998`):
`remaining -= speed factor * rate * seconds`, rate = cCreatureArts +0xee
(magic group, Energieblitze) or +0xf2; clamped to 0..total;
- casting an art: the cast code computes the art's regeneration
(`cCreatureArts::RegenFor` `0x582710`) and adds it with
`cCreatureArts::AddGroupRegen` (`0x562330`, (amount, group, filter)) to
**every** entry of the group (combat arts / magic), capped at each total:
one cast puts the whole group into regeneration. Flags +0x148 & 8 / & 4 cut
the amount to 80 % for the magic / combat group; Flammenwerfer, Meteor,
Kanone and Energieblitze always count as magic. A combo (creature +0x2b4,
arts +0x59) or group 3 takes another branch (combo slots at +0x10e);
- the speed factor (+0xe) is reset by the stats recalculation (`0x579b7d`,
`0x570fad`), the level byte +7 raised by item bonuses (`0x581e40`, which
adds the change of the total to the remaining time).
## The mod
For player heroes, an AddGroupRegen call right after RegenFor (same thread)
adds the amount to the cast art's entry only. Other calls (negative amounts,
filters, combos, other callers) go to the game. Labels: seconds =
remaining / (speed factor * rate).
+44
View File
@@ -0,0 +1,44 @@
# Periodic aura hits (`src/mods/aurahits.cpp`)
Rebalancing request (user, 2026-10-15): "Blicke wie Klingen" (art 0x3f8,
`GUI_MOVE_KLINGENBLICK`) hit only one enemy per tick; it should hit all
enemies in range.
## What the game does (measured with a hit log, 2026-10-15)
The aura gives the hero an effect of kind 0x12 (value 0x445f in the test).
Its hits reach the enemies as `cEvent_creature` 5 (attacker id at +8, the
damage at +0x18.. in one of four damage-type slots, chosen at random), sent
from `0x56323d`, one enemy about once per second.
Code (creature arts update, branch `0x562f08`, only for effects of kind 0x12
of the local player's creature):
- timer at effect +8 (a vector holding one int, ms); every 1000 ms:
- walk the creatures near the player (`0xad1c60`, 8 bytes each, count
`0xad1b48`); a creature within `(cCreatureArts +0x56 + 20) * 10` of the
owner (`cCreature::DistanceTo` `0x536f20`) becomes the target if there is
none yet, otherwise replaces it with 1/3 chance;
- player heroes as targets go through PvP checks (`0x4d4be0`);
- the hit: `cEvent_creature` 5 built on the stack, sent with the target's
`ReceiveOrder` (vf18).
## The mod
`mov eax, [ecx]; call [eax+18h]` at `0x56323b` becomes a call of
`HitAllInRange` (via a thunk passing ebp = cCreatureArts and ebx = the
effect's offset): the chosen enemy gets its hit, then every other living
creature of the nearby list within the same range gets a copy of the same
hit. Player heroes are skipped (they need the PvP checks). Applies to every
effect of kind 0x12, i.e. by behaviour, not by art id.
## Damage type and life leech (2026-10-15)
- The game puts the aura's value into one of the four damage slots at
random (physical, fire, magic, poison), so a quarter of the ticks poisoned;
the mod makes every aura hit physical (user's choice).
- Life leech, its beam and sound are applied by the damage code (`0x56aff0`,
leech block `0x5707a4`..`0x570a88`: property 0x349 x difficulty factor x
damage x distance falloff, capped at max health) for the hit's attacker,
order +0x1c. Weapon hits carry the hero's id there, enemy hits the enemy's,
aura hits 0, so they never leeched. The mod sets it to the aura's owner.
+36
View File
@@ -0,0 +1,36 @@
# Recasting active auras (`src/mods/aurarecast.cpp`)
Goal (user, 2026-10-15): auras / buffs can be cast again while active; the
new cast renews the remaining time, it does not add to it.
## What blocks it (measured 2026-10-15)
Heldenmut (0x3f7) on the hero: effects kind 1 and 2 (the buff) and kind 0x22
(the lock `cCreature::StartArtCooldown` 0x539ce0 adds), all with the aura's
duration (66 s); its art cooldown (cArtCooldowns, 301.9 units) ran out while
the aura still had 30 s left, and a cast order (0x105, art 0x3f7) was still
ignored.
`cCreature::IsValidArtTarget` (`0x51d680`) switches on the art: 0x28, 0x29,
0x41, 0x48, 0x3ed, 0x3f7, 0x3f8 and table entries for 0x3fa.. go to
`0x51d8b1`, which rejects the target if it has any effect of the art
(`cCreatureArts::HasEffectOfArt` `0x5829c0`, effect +0x1c). 0x3fa / 0x3fb
and 0x446 have their own variants of the test right after it (not changed).
## The mod
- The `je` after that test (`0x51d8bd`) becomes a `jmp`: the target is valid
whether or not the effect is active. The art's cooldown still applies.
- `cCreatureArts::AddEffect` (`0x582c10`, (kind, ?, value, duration, art,
network, ?, ?), ret 0x20) is hooked: for a buff art (launch type
SelfOrAlly) on a player hero, if an effect of the same kind and art exists,
its remaining time is set to the new duration and nothing is added. The
value of the old effect stays (a recast after raising the art's level keeps
the old strength until it runs out).
- The aura visual is an effect object (cObjectFX, vtable `0x88f99c`) each
cast creates and attaches with a cEventFX 0x42 (owner +8, art +0x3c; the
object keeps the owner at +0x68). `cObjectFX::ReceiveEvent` (`0x5a37b0`)
is hooked: when a new one attaches to a player hero for a buff art, the
previous one for the same hero and art is flagged dead (object +0x14 bit
0x10, as saving does). Untested whether the object manager removes it
promptly.
+54
View File
@@ -0,0 +1,54 @@
# Auto-attack (`src/mods/autoattack.cpp`)
Goal (user, 2026-10-15): once an enemy was targeted, by a left click or by
casting an art at it, the hero keeps attacking it with the weapon between
casts. Left click stays the normal attack; right click casts slot 0.
## What the game does (order log, 2026-10-15)
Orders logged in `cCreature::ReceiveOrder` (`0x52e400`) for the hero, with
the hero's fields per tick:
| Order | Caller | Fields |
|---|---|---|
| left click on an enemy: `cmd 7` | `0x617abb` | +0x18 target id, +0x1c / +0x20 its position |
| art: `cmd 0x105` | `0x6172f9` (`SendCreatureOrder`) | +0x10 target id, +0x18 / +0x1c ground point, +0x28 0x64, +0x2c 1, +0x30 art id |
| walk: `cmd 4` | `0x6172f9` | see docs/wasd.md |
| hit by an enemy: `cmd 5` | `0x4ea478` | +0x8 attacker id, +0x10 the hero, +0x2c 1.0f (every ~0.5 s) |
One left click is one swing: hero +0x150 goes 0x10000 (idle) -> 0x10003
(attack) -> 0x20003 -> 0x10000 within about a second; +0xfc 0 -> 4 ->
0x20004 -> 0 at the start. An art: 0x10001 -> 0x10008 -> 0x20008 -> 0 ->
0x10000. Low word of +0x150: current action (0 none, 1 art, 3 attack, 6
dying), high word its phase. `cCreature::IsAlive` (`0x548ef0`): +0xfc != 9,
+0x150 != 6, health +0x4d8 != 0.
Holding the left button on an enemy (measured): one attack order, then +0x150
stays 0x10003 for as long as the button is held (9 s in the test), no further
orders. At the end of each swing (after the hit, `0x4e6584`) the attack code
calls `cMouseCursor::IsButtonHeld` (`0x655ab0`, cursor word bit 8) at
`0x4e65c6` (attacker in esi, target creature in ebp; measured: the only
caller answered "held" during a held attack): held -> the next swing starts
right away, otherwise recovery (+0x152 = 2) and idle. (`0x4f665a`, a similar
check in another creature update, is not reached by the hero.) Re-sending
the attack order after the hero went idle took ~1.45 s per swing (0.36 s
wind-up, 0.64 s attack, 0.43 s recovery).
## The mod
- An attack order or an art order whose target is hostile
(`cCreature::IsHostileTo` `0x548dd0`, faction relation) sets the combat
target. (First version: "not an ally", which included NPCs: clicking a
trader made auto-attack talk to it again and again, reopening its window
with new stock.) any walk order (mouse, WASD, stop) clears it, as
does the target's death.
- Each engine tick, if the hero is idle (+0x150 low word 0), no mouse button
is held (engine +0x54 & 0x10), input is not blocked (0x4000) and no skill
key is held, it sends the attack order a left click sends. After sending it
waits up to 5 ticks for the swing to start.
- The end-of-swing button check (`call` at `0x4e65c6`) is redirected: it
also answers "held" when the hero hits its auto-attack target, the target
is alive and no skill key is held. Swings then chain like
with a held button; the tick only restarts the attack after casts.
- Self / ally arts pointed at an enemy lose their target
(`src/mods/arttarget.cpp`) and therefore do not start auto-attack.
+57
View File
@@ -0,0 +1,57 @@
# Chain lightning (`src/mods/chainlightning.cpp`)
Rebalancing request (user, 2026-10-15) for the Battlemage's Blitzschlag (art
0x21, icon `GUI_SPELL_BLITZSCHLAG`; 0x0e has the same icon):
- hits per chain N = 3 + level / 5 (the first target included);
- chains B = 1 + level / 10, all starting at the first target
("hero -> T1 -> A1 -> A2 ..., T1 -> B1 -> B2 ...");
- a chain that finds no new target hits the previous one again (a lone enemy
takes N hits).
## How the game casts it (measured with an object / hit log, 2026-10-15)
About 0.3 s after the cast order (0x105, art 0x21) the caster's art code
(`0x5222c0`, Blitzschlag part around `0x526c91`..`0x527102`):
- builds the visual path: up to 20 segment objects of type 799 (0x31f),
each created linked to the previous one (`CreateAt` third argument), ids
kept at caster +0x263;
- creates the bolt object, type 872 (0x368), at the target, linked to the
last segment, copies the caster's +0x2b5 to its +0x39, and sends it a
cEventFX 0x60 (`0x41e0a0`: [2] caster id, [3] target id, [4..7] position,
[8..13] two strings, [14] damage, [16..29] damage block);
- plays sound 0x7db.
The bolt object then sends the hit (`cEvent_creature` 5 from `0x5b52d3`,
attacker = caster, +0x14 = 0x368, +0x18 = the bolt object's id).
## The game's own chain (measured 2026-10-15)
The game's Blitzschlag already chains. The 0x60 event's [14] is the number of
hits left in the chain (3 at level 1, from the art level, `0x4d24f0`). After
its hit, a bolt with hits left looks for the next target in its update
(`cObjectManager::FindJumpTarget` `0x6005d0`, called at `0x5b469f` with
(position, 100000 = squared range, caster, current target): the nearest
creature, excluding only caster and current target), builds the arc (segment
objects 799 created at `0x5b4aff`, each set up with a cEventFX), and creates
the next bolt at the target (`0x5b4d7c`) with [14] - 1. No target: the chain
ends (a lone enemy is hit once). Two enemies: it bounces between them.
## The mod
- The first bolt of a hero's cast (the first 0x60 bolt setup after the cast
order) gets [14] = N = 3 + level / 5 (level = cooldown entry level + bonus
levels of the art cast).
- The jump search call (`0x5b469f`) goes to `NextTarget` (thunk passing the
bolt in ebp): for bolts of our chains (bolt id -> chain, filled when a bolt
of a chain is set up at the target the chain said it jumps to) the next
target is the nearest enemy within the game's range not hit by this chain
yet, else another enemy, else the current one again (a lone enemy takes all
N hits). Other bolts use the game's search.
- B = 1 + level / 10 chains: while the first bolt is set up, B - 1 more bolts
are created at the first jump targets of the other chains (copies of the
event with [14] = N - 1); their first hop has no arc.
First version (bolts copied for every jump, not using the game's chain)
replaced the same day: it doubled the jumps.
+33
View File
@@ -0,0 +1,33 @@
# Esc closes windows first (`src/mods/escape.cpp`)
Asked 2026-10-15: with a dialog open, Esc should close it instead of opening
the game menu.
## The game (`cUI_Manager::HandleEsc` 0x756ba0, measured 2026-10-15)
On an Esc key event the UI manager (`0x757544`) calls HandleEsc. It knows its
windows by slot (cUI_Manager dwords): 0x20 taskbar, 0x21 inventory, 0x22
equipment, 0x23 blacksmith, 0x24 merchant, 0x25 megamap, 0x26 overview map,
0x27 mercenary (party panel), 0x28 stats, 0x29 console, 0x2a diary (quest
book), 0x2b savegame, 0x2c ESC menu, 0x2d purchase, 0x2e master, 0x2f options,
0x30 chest, 0x31 horse, 0x32 network info, 0x33 net portrait, 0x34
character, 0x35 cube. It closes the ESC menu, the megamap, the quest book, or
(with 0x21/0x23/0x24/0x2e/0x30 open) the inventory; hides 0x2b / 0x2f;
otherwise opens the ESC menu. With a trader open only the inventory closed,
and the trader opened it again.
Closing a window: a cEventUI2 (vtable 0x88edd0, 0x80 bytes: +4 code 0x12,
+8 subtype 5 = hide, names via 0x5904e0: sender at +0x28, window at +0x48 as
measured in the commands the game sends when a window is closed by its
button or hotkey; a string at +0x68, 0x423920 / 0x416660) sent to the UI
manager's event handler (vf00, 0x757120), which routes it by name. Sent to
the window's own vf10 it was not understood (first version).
## The mod
HandleEsc is hooked: unless the ESC menu is open, every visible window of a
table (slot, name) gets that hide command, and Esc does nothing else. If none
is open, the game handles Esc as before.
Not covered: dialogs that are not one of these windows, e.g. quest dialogs
(standalone buttons and texts in the UI manager, docs/hud.md).
+12
View File
@@ -58,3 +58,15 @@ copying and writing back hold the real object's lock).
A primitive drawn inside a moved window that covers the whole 1024x768 layout
(e.g. the dimming behind the exit confirmation) is stretched across the render
width instead of shifted.
## Standalone controls (quest dialog)
Some dialogs (e.g. accepting a quest) are not one of the hooked window
classes: their buttons and texts sit directly in the UI manager as top-level
controls (measured: the clicked button's root window was the cUI_Button2
itself). They were drawn unshifted or with whichever window's scope happened
to be active, so the dialog jumped between the 1024 layout and the centre and
clicks only hit at the unshifted position. `cUI_Button2`, `cUI_Static2`,
`cUI_StaticText64`, `cUI_StaticText64FX` (render and event) and
`cUI_TextLines` (render) are hooked as centred windows too; inside other
windows they are in that window's scope already (outermost wins).
+20
View File
@@ -0,0 +1,20 @@
# Performance
## Record cache in widened views (`src/mods/recordcache.cpp`)
Measured 2026-10-15 (Battlemage save in a town, frame time at Flip, sampling
profiler over all threads): walking took 90-130 ms per frame in the widened
view, 17 ms with widescreen off; idle 17 ms in both.
The render thread spent most samples in a std::map walk (0x832b80 /
0x832ba0, called from 0x636222) inside FUN_006360e0: a record-by-id cache
(map at +0xde5c, size at +0xde60, 16-byte value at node +0x14, last-used
stamp at node +0x24 from 0xcd59d0; misses read the record from the file at
+0x23c). With 4096 entries or more, every insert walks the whole map to
erase the single least recently used entry. The widened view needs more
records than that, so walking evicted and reloaded constantly.
Fix: the limit (`cmp ecx, 1000h` at 0x636201) is raised to 0x8000 so the
per-insert walk never runs; before a lookup with 0x6000 entries the mod
erases the less recently used half in one pass (map::erase 0x63da10). Walking
in town: 17 ms per frame again (confirmed by the user).
+47 -1
View File
@@ -115,6 +115,51 @@ the 1024 frame (66-78 px each side). In the widened view the centred bar has
room for that (16:9: 1365 wide, 170 px each side). Decision 2026-10-14: keep
the original spacing; 4:3 is not a target (cut off there).
### Art slots without selection (2026-10-15)
Decision: art slots are no longer selected. Slot 0 (leftmost) is always the
right mouse button's art; left click stays the normal attack. Keys 0-6 cast
slots 0-6 directly (`src/mods/skillkeys.cpp`): key 0 presses the right
button, keys 1-6 select their slot for as long as the key is held and press
the right button. Without a held skill key the selection returns to slot 0
(engine tick). `cUI_Taskbar2::SelectSkillSlot` turns every other request
(clicks on the bar, wheel, COMBINE_SLOTS, slot count check) into slot 0;
slots on the bar are only filled by drag & drop. Weapon sets are still
selected (F1-F7, clicks).
How a slot is drawn (`RenderArtSlots` `0x6e31e0`): each slot is a
`UI_STATIC` control drawn by its vf14; the function swaps the control's atlas
entry and rect, draws, and restores them. An art has 3 icons (indices into
`g_uiAtlasEntries` `0x9eaee0`, 0x54 each): ready + selected slot uses the
third, otherwise the first; while recharging, the slot is drawn twice, split
vertically by the ready fraction (first icon above, second below). Icons
0x276-0x278 replace them when the art cannot be used mounted and
`0x54fa90` holds (fill from creature +0x4e8 / +0x4ec). Over the icon a frame
is drawn: `g_uiTaskbarArtFrameSelected` `0x9f4be4` / `g_uiTaskbarArtFrame`
`0x9f7f14`, `0x9f5d9c` as a drop target, `0x9f696c` with creature flag
0x2000. `UpdateSlotHighlight` (`0x6df9a0`) sets the slot backgrounds
(`0x9ed084` selected / `0x9ed030`). Patched (`kTaskbarSkillSelectionLook`):
frame and background test "slot 0" instead of "selected slot", and every
ready art uses its third icon. Slots 1-6 are moved right by half a slot
(0x21) after the layout, and the decorative strip right of the centre starts
that much further right (`add ecx, 23Ah` at `0x6e01cc`).
Weapon set clicks (2026-10-15): with an empty cursor, the taskbar's mouse
handling jumps to `0x6e10d8` (`je` at `0x6e0ca7`; taskbar in esi, inventory
in ebp, set at [esp+0B8h], hand at [esp+0BCh]), which takes the set's weapon
onto the cursor (`TakeWeaponSetItem`) and re-selects the selected set. That
jump now goes through `WeaponSetClickThunk`: a click on a set that is not
selected only selects it (exit `0x6e124d`, al = 1); dragging starts from the
selected set only.
Negative x (2026-10-15): with 7 slots the leftmost weapon set (set 1, F1)
lies at x = -68..-2 of the layout. The taskbar's own hit test accepts it,
but every place that asks the slot controls (`vf1c`, signed compares) passes
the position as `and reg, 0FFFFh`, so -43 became 65493 and the set could
not be clicked. `kTaskbarSignedHitTests` sign-extends them (slot lookups
`0x6dfcfe`/`0x6dfd5e`, the click loops `0x6e1a0c`/`0x6e1a5c`, drop and
drag `0x6e1e8e`.. `0x6e20f2`).
The two 5 x 0xbc blocks at +0x258 / +0x604 are the belt (3D item views), not
slots. Still to map: rendering of the slot contents (icons, cooldowns,
`Render` `0x6e3ac0`), drag & drop onto slots (`HandleEvent` mouse branch,
@@ -142,4 +187,5 @@ only (or disabled in multiplayer).
(agreed 2026-10-14).
5. Taskbar: 7 controls each (arrays inside the object are fixed at 5: own
table for the extra controls), layout, rendering, hit tests, drag & drop.
6. Keys: 1-7 select and cast art slots, F1-F7 select weapon sets.
6. Keys: 0-6 cast art slots (since 2026-10-15; before: 1-7 select and
cast), F1-F7 select weapon sets.
+89
View File
@@ -8,6 +8,95 @@ recreates it after a load for Heldenmut (0x3f7) and Klingenblick (0x3f8). Spell
auras (Steinhaut, Lichtschild, ...) use other effects and are not covered
yet; check them if their visuals go missing too.
## Data-driven arts instead of hardcoded art ids
Asked 2026-10-15: naming single arts as constants (`kArtHeldenmut`,
`kArtFlammenwerfer`, ... in `src/game/engine/arts.h`) does not scale. Mods
should decide by an art's properties, read from the game's own data:
- the definition tables `g_spellDefs` (`0x950a38`, 71 x 0x94) and
`g_combatMoveDefs` (`0x94ddb0`, 95 x 0x78): id, icon name, launch type,
usable mounted, ... (`tools/arttable.mjs` dumps them; most fields unnamed);
- map the remaining fields (regeneration group, lasting effect / aura,
duration, ...) and expose them as an `ArtInfo` lookup in `src/game/`;
- where the game itself hardcodes ids (e.g. the arts that always count as
magic in `cCreatureArts::AddGroupRegen`, the "already active" switch in
`IsValidArtTarget`), keep one table of those ids with the reason, instead of
named constants spread over the mods;
- optionally an own data file (JSON next to the DLL) for mod-side settings per
art, if we need values the game does not have.
## Art ranges: scaling and indicators
Asked 2026-10-15 (after making periodic aura hits strike everything in
range, docs/aura-hits.md):
- art ranges should scale with the art's level, and possibly with new
attributes;
- possibly show a range indicator (e.g. a ring around the hero for auras, or
at the cursor for aimed arts).
Known so far: the periodic-hit aura (effect kind 0x12) uses
`(cCreatureArts +0x56 + 20) * 10` world units, measured from the owner with
`cCreature::DistanceTo` (`0x536f20`); what +0x56 is (level? a stat?) is not
mapped yet. Other arts' ranges are not mapped. Fits the data-driven arts
item above (range as a property of an art, one place to compute it).
## Rename life leech to "omnivamp"
Asked 2026-10-15: life leech (property 0x349, "Lebensentzug") becomes
"omnivamp". To do: the in-game texts (item property, tooltips, character
window). The leech code is the game's (`0x5707a4`.., property 0x349, in the
damage code `0x56aff0`); it credits the hit's attacker (order +0x1c). Omnivamp usually means healing from all damage,
not only weapon hits: aura hits (docs/aura-hits.md) now name their attacker
so the game applies it;
other damage sources (spells, combat arts' extra hits, damage over time) may
follow.
## Map and map zoom in widened views
Reported 2026-10-15: the map (and zooming it) does not support the widened
render sizes yet (docs/widescreen.md, docs/hud.md: windows are laid out for
1024 and centred). To check: the map window's draw area and the zoom's
hardcoded 1024/768 (or 512/384 centre) values.
## Quest dialog glitches in widened views
Reported 2026-10-15: accepting a quest, the dialog window "kept glitching"
and button clicks were not detected correctly. Probably the HUD layer
(src/mods/hud.cpp: windows shifted while they render / handle events): the
dialog may not be covered, or shifted twice. Details asked from the user.
## WASD walks around walls (caves)
Reported 2026-10-15: in caves WASD movement behaves "as if the player tries
to find another way to the target location if a wall comes up". WASD sends a
walk order to a point 300 world units ahead (docs/wasd.md); when that point is
behind a wall the pathfinder plans a detour. Idea: find the game's straight
line / walkability check and shorten the target when the line is blocked (or
stop / slide along the wall). Related to the pathfinder item above.
## Esc closes an open dialog first
Asked 2026-10-15: with any dialog open (quest, trader, inventory, ...), Esc
should close that dialog instead of opening the game menu (ESC menu,
`cUI_EscMenu`). Esc is handled in the engine / UI manager hotkeys
(docs/wasd.md: engine `0x618130`, UI manager `0x757120`).
## In-DLL smoke tests (later)
A test mode inside the mod DLL, started by an ini setting or command-line flag,
so builds can be checked without play-testing and without driving the real
mouse/keyboard like `tools/autotest-load.ps1`:
1. load a fixed test save;
2. play scripted input through the existing hooks (keys, casting arts, weapon
set switches, save + reload);
3. take back-buffer screenshots at checkpoints and log checks to
`sacred_re.log` (aura effect present after reload, slot 6-7 inventories
survive a save, ...);
4. exit the game.
## Better pathfinder (mouse clicks and monsters)
Replace the game's pathfinding with a more reliable and efficient one, for