Auto-attack: next target, melee/ranged rule after arts; chain range = cast range
- autoattack: when the target dies, the nearest enemy within close range (contact distance + a step) takes over; after an art the hero auto-attacks if the art was melee (walked up / cast close by), or ranged with a ranged weapon, or ranged with an enemy already in melee range; measured action codes and weapon classes in game/engine/orders.h - chainlightning: jumps reach Blitzschlag's cast range (20 ray segments of 30, the art code's hit check) via the shared NearestHostile search - guards: burning bone render hook had the wrong signature (device plus a 4x4 matrix by value, ret 0x44) and corrupted the stack when fire arrows hit - crash reports follow the ebp chain before the raw stack scan Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5338527b49
commit
16c2844400
8 files changed
+275
-60
No files matched your search
+18
-42
@@ -11,7 +11,8 @@
|
||||
//
|
||||
// This mod keeps all of that and decides:
|
||||
// - hits per chain N = 3 + level / 5: the first bolt's [14];
|
||||
// - the next target: the nearest enemy within the game's range that this
|
||||
// - the next target: the nearest enemy within Blitzschlag's cast range (see
|
||||
// below; the game's own jump search used about 316) that this
|
||||
// chain has not hit yet; else another enemy; else the current target again
|
||||
// (a lone enemy takes all N hits);
|
||||
// - chains B = 1 + level / 10: B - 1 more chains start at the first target,
|
||||
@@ -43,7 +44,16 @@ namespace {
|
||||
constexpr uint32_t kBoltObjectType = 0x368; // 872
|
||||
constexpr uint32_t kJumpSearchCall = 0x005b469f; // in the bolt update
|
||||
constexpr uint32_t kFindJumpTarget = 0x006005d0; // cObjectManager::FindJumpTarget
|
||||
constexpr uint32_t kJumpRange = 100000; // the game's argument (a squared distance)
|
||||
// Blitzschlag's cast range: the art code (cCreature art switch 0x5222c0,
|
||||
// case 0x21) lays the ray from the caster towards the target in segments of
|
||||
// 30, at most 20, each on passable ground (0x636b60); only if the target is
|
||||
// reached is the bolt created on it. A jump may go as far as a cast. One place,
|
||||
// so it can become dynamic (levels, attributes) later.
|
||||
constexpr double kRaySegmentLength = 30.0;
|
||||
constexpr int kRaySegments = 20;
|
||||
double CastRange() {
|
||||
return kRaySegmentLength * kRaySegments;
|
||||
}
|
||||
constexpr size_t kObjectOwnerPlayer = 0x39; // u8, copied from the caster like the game does
|
||||
constexpr size_t kCreaturePosition = 0x18; // 4 dwords, as effects are placed
|
||||
constexpr size_t kEventHitsLeft = 14;
|
||||
@@ -84,44 +94,10 @@ unsigned ArtLevel(void* creature, uint16_t art) {
|
||||
return entry ? entry[game::kCooldownLevel] + entry[game::kCooldownBonusLevels] : 0;
|
||||
}
|
||||
|
||||
bool Contains(const std::vector<uint32_t>& ids, uint32_t id) {
|
||||
for (uint32_t i : ids) {
|
||||
if (i == id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsTarget(void* caster, uint32_t id) {
|
||||
return !game::IsPlayerHeroId(id) && game::IsHostileTo(caster, game::GetCreature(id));
|
||||
}
|
||||
|
||||
// The nearest living enemy of `caster` within `range` (squared) of `from`, not
|
||||
// `current`; one not in `avoid` if there is any. 0 if none.
|
||||
uint32_t Nearest(void* caster, void* from, uint32_t range, uint32_t current, const std::vector<uint32_t>& avoid) {
|
||||
uint32_t best = 0;
|
||||
double bestDistance = 0.0;
|
||||
bool bestFresh = false;
|
||||
const int count = game::NearbyCreatureCount();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint32_t id = game::NearbyCreatureId(i);
|
||||
if (id == current || !IsTarget(caster, id)) {
|
||||
continue;
|
||||
}
|
||||
const double d = static_cast<double>(game::DistanceTo(from, id));
|
||||
if (d * d > static_cast<double>(range)) {
|
||||
continue;
|
||||
}
|
||||
const bool fresh = !Contains(avoid, id);
|
||||
if (best && ((bestFresh && !fresh) || (bestFresh == fresh && d >= bestDistance))) {
|
||||
continue;
|
||||
}
|
||||
best = id;
|
||||
bestDistance = d;
|
||||
bestFresh = fresh;
|
||||
}
|
||||
return best;
|
||||
// The nearest living enemy of `caster` within the cast range of `from`, not
|
||||
// `current`; one not in `avoid` if there is any.
|
||||
uint32_t Nearest(void* caster, void* from, uint32_t current, const std::vector<uint32_t>& avoid) {
|
||||
return game::NearestHostile(caster, from, CastRange(), current, avoid.data(), avoid.size());
|
||||
}
|
||||
|
||||
void Forget() {
|
||||
@@ -194,7 +170,7 @@ void BeforeFxEvent(uint8_t* fx, uint32_t* event) {
|
||||
// More chains: each starts with a bolt at its own first jump target.
|
||||
std::vector<uint32_t> taken{target};
|
||||
for (unsigned c = 1; c < chains && hits > 1; ++c) {
|
||||
const uint32_t next = Nearest(caster, first, kJumpRange, target, taken);
|
||||
const uint32_t next = Nearest(caster, first, target, taken);
|
||||
const uint32_t branchTarget = next ? next : target;
|
||||
taken.push_back(branchTarget);
|
||||
auto branch = std::make_shared<Chain>();
|
||||
@@ -252,7 +228,7 @@ uint32_t __fastcall NextTarget(void* manager, void*, const int32_t* position, ui
|
||||
return 0;
|
||||
}
|
||||
const std::shared_ptr<Chain> chain = it->second.chain;
|
||||
uint32_t next = Nearest(casterCreature, from, range, current, chain->hit);
|
||||
uint32_t next = Nearest(casterCreature, from, current, chain->hit);
|
||||
if (!next) {
|
||||
next = current; // nobody else: hit the same enemy again
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user