From 198bbab6f9371a2fb87a4edcd1cdda0861066f32 Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Wed, 28 Oct 2026 01:01:43 +0100 Subject: [PATCH] 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 --- docs/dwarf-arts.md | 39 ++++++++++++- src/game/engine/arts.h | 1 + src/mods/aurarecast.cpp | 4 ++ src/mods/dwarfarts.cpp | 125 +++++++++++++++++++++++++++++++++++++++- src/mods/dwarfarts.h | 13 +++++ tools/names.txt | 3 + 6 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 src/mods/dwarfarts.h diff --git a/docs/dwarf-arts.md b/docs/dwarf-arts.md index 10f8741..2cde91a 100644 --- a/docs/dwarf-arts.md +++ b/docs/dwarf-arts.md @@ -1,4 +1,4 @@ -# Dwarf arts: Kanonenschuss and Mörsergranate +# Dwarf arts: Kanonenschuss, Mörsergranate, Eingraben ## 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, 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. + +## 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. diff --git a/src/game/engine/arts.h b/src/game/engine/arts.h index e9c6cc4..d0b1fae 100644 --- a/src/game/engine/arts.h +++ b/src/game/engine/arts.h @@ -88,6 +88,7 @@ constexpr uint16_t kArtEnergieblitze = 0x3fb; constexpr uint16_t kArtFlammenwerfer = 0x43d; constexpr uint16_t kArtMoerser = 0x43e; // Mörsergranate (gui_spells_moerser) constexpr uint16_t kArtKanone = 0x43f; // Kanonenschuss (gui_spells_kanone) +constexpr uint16_t kArtEingraben = 0x39; // spell (gui_moves_eingraben) // cCreatureArts fields. constexpr size_t kArtsCreature = 0x4; // cCreature*: the owner diff --git a/src/mods/aurarecast.cpp b/src/mods/aurarecast.cpp index 85242d3..d2a9da2 100644 --- a/src/mods/aurarecast.cpp +++ b/src/mods/aurarecast.cpp @@ -19,6 +19,7 @@ #include "game/engine/fx.h" #include "game/engine/objects.h" #include "game/engine/orders.h" +#include "mods/dwarfarts.h" #include "re/hook.h" #include "re/log.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); 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) { + if (!mods::dwarfarts::KeepEffect(kind, art, duration)) { + return 1; + } if (self && mods::aurarecast::Renew(static_cast(self), kind, duration, art)) { return 1; } diff --git a/src/mods/dwarfarts.cpp b/src/mods/dwarfarts.cpp index 630731e..0df8ec4 100644 --- a/src/mods/dwarfarts.cpp +++ b/src/mods/dwarfarts.cpp @@ -34,6 +34,27 @@ // the point; the circle is drawn after the world view's ground (0x62d530, // before the objects) until the impact gathers its targets (the grenade // 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 @@ -46,6 +67,7 @@ #include "game/engine/art.h" #include "game/engine/arts.h" +#include "game/engine/events.h" #include "game/engine/objects.h" #include "game/engine/orders.h" #include "game/engine/worldview.h" @@ -406,8 +428,88 @@ void DrawMarker(const uint8_t* view, void* device, const Grenade& grenade) { 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(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(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(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(creature + game::kObjectPosition); + game::MapPosition best = from; + void* world = World(); + const float dx = static_cast(x - from.x), dy = static_cast(y - from.y); + const float length = std::sqrt(dx * dx + dy * dy); + if (!world || length < kWalkStep) { + return best; + } + const auto stepX = static_cast(dx / length * kWalkStep); + const auto stepY = static_cast(dy / length * kWalkStep); + const auto steps = static_cast(length / kWalkStep); + const uint32_t id = *reinterpret_cast(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(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(to.x / game::kMapCellSize), + static_cast(to.y / game::kMapCellSize)); +} + +bool AimEingraben() { + auto* def = const_cast(game::FindSpellDef(game::kArtEingraben)); + if (!def || def[kSpellLaunchType] != static_cast(game::LaunchType::AroundSelf)) { + return false; + } + def[kSpellLaunchType] = static_cast(game::LaunchType::Ground); + return true; +} + void Startup() { - const bool impact = re::RedirectCall(kImpactCreate, kCreateAt, reinterpret_cast(&CreateImpactThunk)); + 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(&CreateImpactThunk)); re::Log("dwarfarts: Kanonenschuss impact only on stuns%s", impact ? "" : " (patch FAILED)"); uint8_t call[sizeof(kPushStepCode)]; std::memset(call, 0x90, sizeof(call)); @@ -438,6 +540,14 @@ struct Registration { } // 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 only, int32_t radius, const uint8_t* 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(cast[2]) : 0, y = dig ? static_cast(cast[3]) : 0; + Creature_CastSpell_original(self, edx, cast); + if (dig) { + mods::dwarfarts::Dig(self, x, y); + } +} + } // namespace game diff --git a/src/mods/dwarfarts.h b/src/mods/dwarfarts.h new file mode 100644 index 0000000..40a7437 --- /dev/null +++ b/src/mods/dwarfarts.h @@ -0,0 +1,13 @@ +#pragma once +// The dwarf's arts (dwarfarts.cpp). + +#include + +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 diff --git a/tools/names.txt b/tools/names.txt index 3bd462e..a2f7246 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -448,3 +448,6 @@ 0x0049bb30 QuestManager_SetVarBit # script op 0x44: variable |= 1 << n 0x0049c450 QuestManager_ClearVarBit # script op 0x45: variable &= ~(1 << 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