You are basically correct in your assessment of what is happening: the two events are occurring in the wrong order. After reviewing the call stack with many prints, here is a solution that I tested and found to be working without any issues:
As you pointed out, the area effect from the spell is passed into this function:
C++:
void Combat::combatTileEffects(const SpectatorVec& list, Creature* caster, Tile* tile, const CombatParams& params)
And actually drawn out in the very last 'if' statement of that function:
C++:
if (params.impactEffect != CONST_ME_NONE) {
Game::addMagicEffect(list, tile->getPosition(), params.impactEffect);
}
combatTileEffects is called toward the end of this function:
C++:
void Combat::CombatFunc(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params, COMBATFUNC func, CombatDamage* data)
It's called inside a 'for' loop that iterates through all area tiles in whatever spell is being cast. As you already discovered, by the time this happens, the damage has already been done to the target, and the "hit effect" (as it's called in game.cpp) has already ocurred.
From this, we realize that when we move the order of which effect comes first, we cant just throw 'Game::addMagicEffect' anywhere. We have to grab all tiles for the spell and iterate through.
So, let's get to the actual changes:
Create a new function, name it whatever you want, here is mine:
C++:
void Combat::drawEffects(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params)
{
std::forward_list<Tile*> tileList;
if (caster) {
getCombatArea(caster->getPosition(), pos, area, tileList);
} else {
getCombatArea(pos, pos, area, tileList);
}
SpectatorVec list;
uint32_t maxX = 0;
uint32_t maxY = 0;
// calculate the max viewable range
for (Tile* tile : tileList) {
const Position& tilePos = tile->getPosition();
uint32_t diff = Position::getDistanceX(tilePos, pos);
if (diff > maxX) {
maxX = diff;
}
diff = Position::getDistanceY(tilePos, pos);
if (diff > maxY) {
maxY = diff;
}
}
const int32_t rangeX = maxX + Map::maxViewportX;
const int32_t rangeY = maxY + Map::maxViewportY;
g_game.map.getSpectators(list, pos, true, true, rangeX, rangeX, rangeY, rangeY);
for (Tile* tile : tileList) {
if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) {
continue;
}
if (params.impactEffect != CONST_ME_NONE) {
Game::addMagicEffect(list, tile->getPosition(), params.impactEffect);
}
combatTileEffects(list, caster, tile, params);
}
}
I placed it right after 'void Combat::CombatFunc' and before 'void Combat::doCombat'. You will need to declare this function in combat.h:
C++:
static void drawEffects(Creature* caster, const Position& pos, const AreaCombat* area, const CombatParams& params);
I placed it after 'void circleShapeSpell'.
Next, comment out the call to combatTileEffects toward the 'bottom of void Combat::CombatFunc', and call your new function before the very last curly bracket:
C++:
for (Tile* tile : tileList) {
if (canDoCombat(caster, tile, params.aggressive) != RETURNVALUE_NOERROR) {
continue;
}
// combatTileEffects(list, caster, tile, params); originally area effects were drawn here, but this is after the target takes damage
if (CreatureVector* creatures = tile->getCreatures()) {
const Creature* topCreature = tile->getTopCreature();
for (Creature* creature : *creatures) {
if (params.targetCasterOrTopMost) {
if (caster && caster->getTile() == tile) {
if (creature != caster) {
continue;
}
} else if (creature != topCreature) {
continue;
}
}
if (!params.aggressive || (caster != creature && Combat::canDoCombat(caster, creature) == RETURNVALUE_NOERROR)) {
func(caster, creature, params, data);
if (params.targetCallback) {
params.targetCallback->onTargetCombat(caster, creature);
}
if (params.targetCasterOrTopMost) {
break;
}
}
}
}
}
drawEffects(caster, pos, area, params); //new function called here, this runs before the target takes damage
}
This is only half of the battle. If you do this fix, area spells like energy strike, energy beam, monster AoE spells will have the correct effect order, but
targeted attacks, like LMM, HMM, SD, etc, will still have the wrong order.
For targeted attacks, the magic effect is passed in to this function here:
C++:
bool Combat::doCombatHealth(Creature* caster, Creature* target, CombatDamage& damage, const CombatParams& params)
{
bool canCombat = !params.aggressive || (caster != target && Combat::canDoCombat(caster, target) == RETURNVALUE_NOERROR);
if ((caster == target || canCombat) && params.impactEffect != CONST_ME_NONE) {
g_game.addMagicEffect(target->getPosition(), params.impactEffect); //<--distance effect originally drawn right here, this is too late
}
if (canCombat) {
if (caster && params.distanceEffect != CONST_ANI_NONE) {
addDistanceEffect(caster, caster->getPosition(), target->getPosition(), params.distanceEffect);
}
canCombat = CombatHealthFunc(caster, target, params, &damage);
if (params.targetCallback) {
params.targetCallback->onTargetCombat(caster, target);
}
}
return canCombat;
}
Comment out the call to draw the effect in that function: g_game.addMagicEffect(target->getPosition(), params.impactEffect);
And place it instead here in 2 places, within the if statement (we only want to send the magic effect if there is a magic effect, or the Cip client will crash when a player uses a wand/rod for example, the server sends effect 0 to the client, we also want to send the effect even if the attack is blocked by armor):
C++:
bool Combat::CombatHealthFunc(Creature* caster, Creature* target, const CombatParams& params, CombatDamage* data)
{
assert(data);
CombatDamage damage = *data;
if (damage.value == 0) {
damage.value = normal_random(damage.min, damage.max);
}
if (damage.value < 0 && caster) {
Player* targetPlayer = target->getPlayer();
if (targetPlayer && caster->getPlayer()) {
damage.value /= 2;
}
}
if (g_game.combatBlockHit(damage, caster, target, params.blockedByShield, params.blockedByArmor, params.itemId != 0)) {
if (params.impactEffect > 0) {
g_game.addMagicEffect(target->getPosition(), params.impactEffect); //we also want to still draw the effect even if the attack doesnt do damage, i.e. a beholder using their weak SD attack, it is often blocked by the player's shield
}
return false;
}
if (g_game.combatChangeHealth(caster, target, damage)) { //this is the exact spot in game.cpp where it does damage and makes the effect
if (params.impactEffect > 0) {
g_game.addMagicEffect(target->getPosition(), params.impactEffect);//move drawing the end effect here so that it happens BEFORE the damage, but only if it receives an impact effect
}
CombatConditionFunc(caster, target, params, &damage);
CombatDispelFunc(caster, target, params, nullptr);
}
return true;
}
That's it. Compile and run. Screenshots of before/after:
View attachment 80685