Eingraben: aimed escape to a point in sight, 3 s barrier, no lock

Eingraben becomes ground-targeted; after its own case the dwarf moves to
the end of Phasenverschiebung's walk towards the point (stops before walls
and cliffs). The barrier's bonus lasts 3 s and its lock in place
(effect kind 0x3a) is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-28 01:01:43 +01:00
1 parent 55e48dd302
commit 198bbab6f9
6 files changed
+182 -1

No files matched your search

+38 -1
View File
@@ -1,4 +1,4 @@
# Dwarf arts: Kanonenschuss and Mörsergranate # Dwarf arts: Kanonenschuss, Mörsergranate, Eingraben
## Requests (2026-10-25 / 2026-10-26) ## Requests (2026-10-25 / 2026-10-26)
@@ -87,3 +87,40 @@ with life >= 256 or row >= 3 are switched off before drawing. Logged in the
next session: 5 particles, all row 3, life 255, garbage sizes (e.g. 190555, next session: 5 particles, all row 3, life 255, garbage sizes (e.g. 190555,
4.6e-40): the explosion emits into a fourth row whose emitter parameters it 4.6e-40): the explosion emits into a fourth row whose emitter parameters it
never sets (vf10 0x7b6980 fills three rows at +0x30d0). No crash since. never sets (vf10 0x7b6980 fills three rows at +0x30d0). No crash since.
## Eingraben: the dwarf's escape (request 2026-10-28)
"I want it to be the Dwarfs option to Teleport/Escape. The Skill will cast a
protective barrier around the Dwarf (current visual effect), clear him of all
movement impending effects and then teleport to the position marked by the
cursor (if possible). The Teleport is visually shown by the dwarf going
underground and coming out again at target position." Range: the visible
area; the barrier is the skill's current defensive bonus for 3 s; digging in
and out 1 s each; "he should be able to see the target location", compared
to the battlemage's Phasenverschiebung; the original lock in place goes.
Found in the code:
- Spells run in `cCreature::CastSpell` (0x5222c0, thiscall (cast), ret 4;
cast +0x08 / +0x0c the ground point, +0x20 the spell id). Eingraben is
spell 0x39 (`gui_moves_eingraben`, launch type 5 around the caster).
- Case 0x39 creates object type 0x480 at the caster with cEventFX 0x9d (owner
and target the caster) and plays sound 0x840: the barrier, which follows
the dwarf.
- Case 1 (Phasenverschiebung, launch type 3) walks from the caster towards the
target in steps of 10 while the cell is passable (`cWorld::IsPassable`
0x636b60) and keeps the last point a creature can stand on
(`cWorld::CanStand` 0x636eb0); a wall or cliff in the way ends the walk.
Logged (2026-10-28): the barrier adds, all with art 0x39, kind 0x3a value 0
from its particles (0x7b9602), the lock in place (the dwarf was stuck 2-3 s),
and kind 2 value 0.5 plus kinds 7..0xa value 30 from its update (0x5b5862..),
the defensive bonus (resistances up while it lasts).
Mod (`mods/dwarfarts.cpp`): Eingraben's definition +0x3c becomes 3 (a point);
after the original case the dwarf is teleported to the end of
Phasenverschiebung's walk towards the point (the cursor keeps it within the
visible area). The effect hook (`aurarecast.cpp`, `KeepEffect`) drops kind
0x3a for art 0x39 and cuts the art's other effects to 3 s. Confirmed
2026-10-28: aimed, stops before walls and cliffs, casts at once at the screen
edge, the barrier moves with him, no lock, bonus for 3 s. Open: clearing
slows and stuns, 1 s digging in and out.
+1
View File
@@ -88,6 +88,7 @@ constexpr uint16_t kArtEnergieblitze = 0x3fb;
constexpr uint16_t kArtFlammenwerfer = 0x43d; constexpr uint16_t kArtFlammenwerfer = 0x43d;
constexpr uint16_t kArtMoerser = 0x43e; // Mörsergranate (gui_spells_moerser) constexpr uint16_t kArtMoerser = 0x43e; // Mörsergranate (gui_spells_moerser)
constexpr uint16_t kArtKanone = 0x43f; // Kanonenschuss (gui_spells_kanone) constexpr uint16_t kArtKanone = 0x43f; // Kanonenschuss (gui_spells_kanone)
constexpr uint16_t kArtEingraben = 0x39; // spell (gui_moves_eingraben)
// cCreatureArts fields. // cCreatureArts fields.
constexpr size_t kArtsCreature = 0x4; // cCreature*: the owner constexpr size_t kArtsCreature = 0x4; // cCreature*: the owner
+4
View File
@@ -19,6 +19,7 @@
#include "game/engine/fx.h" #include "game/engine/fx.h"
#include "game/engine/objects.h" #include "game/engine/objects.h"
#include "game/engine/orders.h" #include "game/engine/orders.h"
#include "mods/dwarfarts.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/log.h" #include "re/log.h"
#include "re/patch.h" #include "re/patch.h"
@@ -126,6 +127,9 @@ uint32_t __fastcall Arts_AddEffect(void* self, void* edx, uint32_t kind, uint32_
RE_HOOK(0x00582c10, Arts_AddEffect); RE_HOOK(0x00582c10, Arts_AddEffect);
uint32_t __fastcall Arts_AddEffect(void* self, void* edx, uint32_t kind, uint32_t a2, float value, float duration, uint32_t __fastcall Arts_AddEffect(void* self, void* edx, uint32_t kind, uint32_t a2, float value, float duration,
uint32_t art, uint32_t a6, uint32_t a7, uint32_t a8) { uint32_t art, uint32_t a6, uint32_t a7, uint32_t a8) {
if (!mods::dwarfarts::KeepEffect(kind, art, duration)) {
return 1;
}
if (self && mods::aurarecast::Renew(static_cast<uint8_t*>(self), kind, duration, art)) { if (self && mods::aurarecast::Renew(static_cast<uint8_t*>(self), kind, duration, art)) {
return 1; return 1;
} }
+123
View File
@@ -34,6 +34,27 @@
// the point; the circle is drawn after the world view's ground (0x62d530, // the point; the circle is drawn after the world view's ground (0x62d530,
// before the objects) until the impact gathers its targets (the grenade // before the objects) until the impact gathers its targets (the grenade
// object itself stays about 30 s longer). // object itself stays about 30 s longer).
//
// Eingraben (spell 0x39), the dwarf's escape (request of 2026-10-28): "cast a
// protective barrier around the Dwarf (current visual effect), clear him of
// all movement impeding effects and then teleport to the position marked by
// the cursor (if possible)", shown as the dwarf going underground and coming
// out at the target, 1 s each; range the visible area; the barrier is the
// skill's current defensive bonus for 3 s; "he should be able to see the
// target location"; like the battlemage's Phasenverschiebung. The spells run
// in cCreature::CastSpell (0x5222c0, thiscall (cast), ret 4; cast +0x08 /
// +0x0c the ground point, +0x20 the spell). Case 0x39 creates object type
// 0x480 at the caster with cEventFX 0x9d (owner and target the caster) and
// plays sound 0x840: the barrier, which gives the bonus. Case 1
// (Phasenverschiebung) walks from the caster towards the target in steps of
// 10 while the cell is passable (world 0x636b60) and keeps the last point a
// creature can stand on (0x636eb0); a wall in the way ends the walk, so the
// target has to be in sight. The mod aims Eingraben at a point (definition
// +0x3c: 5 around the caster -> 3 a point), runs the original case (the bonus
// cut to kBarrierSeconds) and moves the dwarf to the end of that walk; the
// cursor limits it to the visible area.
#include "mods/dwarfarts.h"
#include <windows.h> #include <windows.h>
@@ -46,6 +67,7 @@
#include "game/engine/art.h" #include "game/engine/art.h"
#include "game/engine/arts.h" #include "game/engine/arts.h"
#include "game/engine/events.h"
#include "game/engine/objects.h" #include "game/engine/objects.h"
#include "game/engine/orders.h" #include "game/engine/orders.h"
#include "game/engine/worldview.h" #include "game/engine/worldview.h"
@@ -406,7 +428,87 @@ void DrawMarker(const uint8_t* view, void* device, const Grenade& grenade) {
game::FillTriangles(device, vertices.data(), vertices.size()); game::FillTriangles(device, vertices.data(), vertices.size());
} }
// --- Eingraben ----------------------------------------------------------------
constexpr float kBarrierSeconds = 3.0f;
// The barrier's effects (logged 2026-10-28, all with art 0x39): kind 0x3a
// value 0 from its particles (0x7b9602), which holds the dwarf in place, and
// kind 2 value 0.5 plus kinds 7..0xa value 30 from its update (0x5b5862..),
// the defensive bonus.
constexpr uint32_t kEffectDugIn = 0x3a;
constexpr int32_t kWalkStep = 10; // Phasenverschiebung's step (0x52288a: 10.0)
constexpr size_t kSpellLaunchType = 0x3c;
// The world (cWorld, the object manager's first field), as the art code
// passes it to its cell checks.
void* World() {
void* manager = game::ObjectManager();
return manager ? *static_cast<void**>(manager) : nullptr;
}
// cWorld 0x636b60 (thiscall (position), ret 4): the cell is passable.
bool Passable(void* world, game::MapPosition& at) {
using Fn = bool(__fastcall*)(void*, void*, game::MapPosition*);
return reinterpret_cast<Fn>(0x00636b60)(world, nullptr, &at);
}
// cWorld 0x636eb0 (thiscall (creature id, position), ret 8): the creature can
// stand there (may correct the position's area).
bool Standable(void* world, uint32_t creature, game::MapPosition& at) {
using Fn = bool(__fastcall*)(void*, void*, uint32_t, game::MapPosition*);
return reinterpret_cast<Fn>(0x00636eb0)(world, nullptr, creature, &at);
}
// Phasenverschiebung's walk (0x5227a6..): from the creature towards (x, y),
// the last point it can stand on before a cell that is not passable.
game::MapPosition DigDestination(uint8_t* creature, int32_t x, int32_t y) {
const auto& from = *reinterpret_cast<const game::MapPosition*>(creature + game::kObjectPosition);
game::MapPosition best = from;
void* world = World();
const float dx = static_cast<float>(x - from.x), dy = static_cast<float>(y - from.y);
const float length = std::sqrt(dx * dx + dy * dy);
if (!world || length < kWalkStep) {
return best;
}
const auto stepX = static_cast<int32_t>(dx / length * kWalkStep);
const auto stepY = static_cast<int32_t>(dy / length * kWalkStep);
const auto steps = static_cast<int32_t>(length / kWalkStep);
const uint32_t id = *reinterpret_cast<const uint32_t*>(creature + game::kCreatureId);
game::MapPosition at = from;
for (int32_t i = 0; i <= steps && Passable(world, at); ++i) {
game::MapPosition stand = at;
if (Standable(world, id, stand)) {
best = stand;
}
at.x += stepX;
at.y += stepY;
}
return best;
}
void Dig(uint8_t* creature, int32_t x, int32_t y) {
const auto& from = *reinterpret_cast<const game::MapPosition*>(creature + game::kObjectPosition);
const game::MapPosition to = DigDestination(creature, x, y);
re::Log("dwarfarts: Eingraben %d,%d towards %d,%d -> %d,%d", from.x, from.y, x, y, to.x, to.y);
if (to.x == from.x && to.y == from.y) {
return;
}
game::Teleport(creature, static_cast<int32_t>(to.x / game::kMapCellSize),
static_cast<int32_t>(to.y / game::kMapCellSize));
}
bool AimEingraben() {
auto* def = const_cast<uint8_t*>(game::FindSpellDef(game::kArtEingraben));
if (!def || def[kSpellLaunchType] != static_cast<uint8_t>(game::LaunchType::AroundSelf)) {
return false;
}
def[kSpellLaunchType] = static_cast<uint8_t>(game::LaunchType::Ground);
return true;
}
void Startup() { void Startup() {
const bool aim = AimEingraben();
re::Log("dwarfarts: Eingraben aimed at a point, barrier %g s%s", kBarrierSeconds, aim ? "" : " (patch FAILED)");
const bool impact =re::RedirectCall(kImpactCreate, kCreateAt, reinterpret_cast<const void*>(&CreateImpactThunk)); const bool impact =re::RedirectCall(kImpactCreate, kCreateAt, reinterpret_cast<const void*>(&CreateImpactThunk));
re::Log("dwarfarts: Kanonenschuss impact only on stuns%s", impact ? "" : " (patch FAILED)"); re::Log("dwarfarts: Kanonenschuss impact only on stuns%s", impact ? "" : " (patch FAILED)");
uint8_t call[sizeof(kPushStepCode)]; uint8_t call[sizeof(kPushStepCode)];
@@ -438,6 +540,14 @@ struct Registration {
} // namespace } // namespace
bool KeepEffect(uint32_t kind, uint32_t art, float& duration) {
if (art != game::kArtEingraben) {
return true;
}
duration = std::fmin(duration, kBarrierSeconds);
return kind != kEffectDugIn;
}
uint32_t* __fastcall GatherTargets(void* manager, void*, uint32_t* out, const void* position, uint32_t type, uint32_t* __fastcall GatherTargets(void* manager, void*, uint32_t* out, const void* position, uint32_t type,
uint32_t only, int32_t radius, const uint8_t* fx) { uint32_t only, int32_t radius, const uint8_t* fx) {
if (type == kGrenadeImpactType && radius < 0 && fx) { if (type == kGrenadeImpactType && radius < 0 && fx) {
@@ -481,4 +591,17 @@ void __fastcall WorldView_DrawGroundQuads(uint8_t* self, void* edx, void* render
} }
} }
// cCreature::CastSpell (0x5222c0, thiscall (cast), ret 4): the spells; after
// Eingraben's own case the dwarf digs to the point (cast +0x08 / +0x0c).
void __fastcall Creature_CastSpell(uint8_t* self, void* edx, uint32_t* cast);
RE_HOOK(0x005222c0, Creature_CastSpell);
void __fastcall Creature_CastSpell(uint8_t* self, void* edx, uint32_t* cast) {
const bool dig = self && cast && cast[8] == kArtEingraben;
const int32_t x = dig ? static_cast<int32_t>(cast[2]) : 0, y = dig ? static_cast<int32_t>(cast[3]) : 0;
Creature_CastSpell_original(self, edx, cast);
if (dig) {
mods::dwarfarts::Dig(self, x, y);
}
}
} // namespace game } // namespace game
+13
View File
@@ -0,0 +1,13 @@
#pragma once
// The dwarf's arts (dwarfarts.cpp).
#include <cstdint>
namespace mods::dwarfarts {
// Whether cCreatureArts::AddEffect adds an effect of `kind` for art `art`,
// and its duration: Eingraben no longer holds the dwarf in place, and its
// defensive bonus lasts only while the barrier does.
bool KeepEffect(uint32_t kind, uint32_t art, float& duration);
} // namespace mods::dwarfarts
+3
View File
@@ -448,3 +448,6 @@
0x0049bb30 QuestManager_SetVarBit # script op 0x44: variable |= 1 << n 0x0049bb30 QuestManager_SetVarBit # script op 0x44: variable |= 1 << n
0x0049c450 QuestManager_ClearVarBit # script op 0x45: variable &= ~(1 << n) 0x0049c450 QuestManager_ClearVarBit # script op 0x45: variable &= ~(1 << n)
0x0049cc20 QuestManager_AddToVar # script op 0x4b: variable += n 0x0049cc20 QuestManager_AddToVar # script op 0x4b: variable += n
0x005222c0 cCreature::CastSpell # thiscall (cast), ret 4: switch on the spell (cast +0x20); +0x08/+0x0c ground point; case 1 Phasenverschiebung, 0x39 Eingraben
0x00636b60 cWorld::IsPassable # thiscall (position), ret 4: world = *ObjectManager
0x00636eb0 cWorld::CanStand # thiscall (creature id, position), ret 8: may correct the position's area