The chains of a cast share the enemies they have claimed and prefer unclaimed ones. Each chain keeps its heading and ranks candidates by distance and turn; a chain's first jump turns away from the directions the other chains set out in, so they keep fanning out. Jumps reach 75 % of the cast range. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
116 lines
6.1 KiB
Markdown
116 lines
6.1 KiB
Markdown
# 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.
|
||
|
||
Separate routes (2026-10-18). Request: the chains should not all follow the
|
||
same route. Before, each chain only avoided the enemies it had hit itself, and
|
||
only the extra chains' first targets differed. After that, parallel chains
|
||
picked the same nearest enemies and could swap onto each other's route. Now
|
||
the chains of one cast share a list of claimed enemies (the first target, and
|
||
every target as soon as a chain picks it). A jump prefers an enemy no chain
|
||
of the cast has claimed, then one this chain has not hit, then any other,
|
||
then the current one.
|
||
|
||
Follow-up (2026-10-18): "only on the first spread, then they most times merge
|
||
together again". The nearest free enemy of two chains often lies in the same
|
||
group. Each chain now keeps a heading (caster -> first target for the main
|
||
chain, then the direction of its last jump), and the candidates of a tier are
|
||
ranked by distance × (1 + 2 × turn), with turn = 0 straight ahead and 1
|
||
straight back. A chain's first jump has no heading yet; there the turn is the
|
||
largest closeness to the directions the cast's chains set out in, so the
|
||
branches fan away from the main chain and from each other.
|
||
|
||
First version (bolts copied for every jump, not using the game's chain)
|
||
replaced the same day: it doubled the jumps.
|
||
|
||
## Jump range = cast range (2026-10-16)
|
||
|
||
User request: a jump reaches as far as a cast. Blitzschlag's hit check is in
|
||
the art code (cCreature art switch `0x5222c0`, case 0x21): the ray from the
|
||
caster towards the target is laid out in segments of 30, at most 20, each on
|
||
passable ground (`0x636b60`); the bolt is created on the target only if the
|
||
ray reaches it, otherwise the visible ray just stops. So the cast range is
|
||
20 × 30 = 600. `CastRange()` in `chainlightning.cpp` returns it for every jump
|
||
(also the first hop of extra chains), instead of the game's jump search radius
|
||
(squared 100000, about 316). It is the one place to make the range dynamic
|
||
later. Jumps do not check the ground in between.
|
||
|
||
## Tooltip (2026-10-17)
|
||
|
||
Request: the tooltip should match the mod. The game's text (cCreatureArts::ArtTooltip
|
||
0x589270, measured for art 0x21 at level 5) ends each level section with
|
||
"Anzahl: X", the game's own hit count. The mod replaces that line with
|
||
"Treffer je Kette" (3 + L/5), "Ketten" (1 + L/10) and "Sprungweite" (the cast
|
||
range), with L = the hero's level (base + bonus) in the "Aktuelle Stufe" section
|
||
and L + 1 in "Nächste erlernte Stufe". The string is rewritten with the game's
|
||
append (0x592ef0), so the game's allocator owns the buffer.
|
||
|
||
## Range +50 % (2026-10-17)
|
||
|
||
Request: Kettenblitz range about 50 % larger. The ray's 20 segments are
|
||
bounded by the caster's segment id array (+0x263, 20 dwords), so the segments
|
||
get longer instead: 30 -> 45, cast and jump range 600 -> 900. Patched in the
|
||
art code: `fsub [30.0]` 0x526db6 and `fcomp [30.0]` 0x526e0b (redirected to
|
||
our 45.0; 0x88ef18 is shared by 72 instructions), `mov eax,30` 0x526e23 (the
|
||
segment length given to each segment object) and `push 30.0f` 0x526f3d (the
|
||
step of the ray point).
|
||
|
||
Follow-up (2026-10-17): jumps at 900 felt too far. The cast keeps 900; jumps
|
||
use `JumpRange()` = half the cast range (450). The tooltip shows both
|
||
("Reichweite", "Sprungweite").
|
||
|
||
Follow-up (2026-10-18): with several chains, half the cast range left too
|
||
few separate targets ("maybe chain range needs to increase to 75% of cast
|
||
range"). Jumps now reach 75 % of the cast range (675).
|