If there was a UnitDied event ...

whoward69

DLL Minion
Joined
May 30, 2011
Messages
8,725
Location
Near Portsmouth, UK
... what would trigger it and what data would be required?

Firaxis use "kill" to mean when a unit is removed from play for any reason. This is not just combat, but also includes upgrades, special abilities (GP builds, religious actions, etc), founding a city, building a work boat, etc, etc, etc

Modders tend to use "kill" to mean "died in combat".

So if there was a "UnitDied" event for when a unit dies from direct or indirect combat a) under what situations does this need to trigger, and b) what parameters would be needed for such an event?

I think there are the following ways for a unit to die during combat

While attacking
  • another unit (melee, naval)
  • air/missile strike
  • a city

While defending
  • a melee attack
  • a ranged attack (bows, siege, naval, air, missile)
  • a city bombardment

3rd party
  • air interception
  • attrition from terrain/features
  • attrition from improvements (eg citadels)
  • via Lua (specifically pUnit:ChangeDamage())

Ignoring the (not insignificant) problem that by the time the Lua event is trigger the unit has probably been removed from the game core, what parameters would need to be passed to such an event

iPlayer - the player owning the deceased unit
iUnit - the id of the deceased unit
iPlotX, iPlotY - the plot the unit died on
iOtherPlayer - the player doing the killing
iOtherUnit - the id of the killing unit (may be nil/-1)
iOtherCity - the id of the killing city (may be nil/-1)
iOtherPlotX, iOtherPlotY - the plot the killing unit/city was on

Thoughts?
 
I can't think of anything else that would be needed as parameters passed because everything I can think of ought to be extractable out of the info you've outlined.

Would it fire as "died in combat" when a Civilian unit is killed off ? For example, if the Barbs destroy a Great General as opposed to a Great General being used to citadel-bomb? Obviously there are the hassles with Workers and Settlers and Missionaries and such that get captured, destroyed, resurrected, recaptured, re-destroyed, re-resurrected that I'm assuming you would want to avoid by excluding those types of units.
 
Would it fire as "died in combat" when a Civilian unit is killed off?

Good question.

Another one is "What about any defender/air units in a captured city?"

I'm thinking that a suite of events will probably fill the "infinitely expandable" requirement

BattleStarted()
BattleJoined(iPlayer, iUnitOrCity, iRole) (iRole is defender, attacker, interceptor, city, etc)
BattleDamage(iPlayer, iUnitOrCity, iRole, iDamage, bKilled)
BattleFinished()

That way the modder can cache any values they will need as units/cities join the fray, and will have the info on hand when units are killed, without the events having to second guess what every modder may want.

So a typical sequence would then become

BattleStarted()
BattleJoined(iPlayer, iPlayerUnit, ROLE_ATTACKER)
BattleJoined(iEnemy, iEnemyUnit, ROLE_DEFENDER)
BattleDamage(iPlayer, iPlayerUnit, ROLE_ATTACKER, 23, false)
BattleDamage(iEnemy, iEnemyUnit, ROLE_DEFENDER, 78, true)
BattleFinished()

Capturing a garrisoned city would then become
BattleStarted()
BattleJoined(iPlayer, iPlayerUnit, ROLE_ATTACKER)
BattleJoined(iEnemy, iEnemyCity, ROLE_CITY)
BattleDamage(iPlayer, iPlayerUnit, ROLE_ATTACKER, 86, false)
BattleDamage(iEnemy, iEnemyCity, ROLE_CITY, 18, true)
BattleJoined(iEnemy, iGarisonUnit, ROLE_GARRISON)
BattleDamage(iEnemy, iGarisonUnit, ROLE_GARRISON, 100, true)
BattleFinished()
 
I would also add that anytime pUnit:ChangeDamage() is called, and enough damage is done to the unit to kill it, that it should count as death for this purpose. Or perhaps it could do so only if the second argument of ChangeDamage (a player ID) is defined and greater than -1. Attrition damage could also count for this, as well as damage from enemy Improvements (i.e. Citadels).


A bit unrelated, but since you've mentioned functions like this...

BattleStarted()
BattleJoined(iPlayer, iUnitOrCity, iRole) (iRole is defender, attacker, interceptor, city, etc)
BattleDamage(iPlayer, iUnitOrCity, iRole, iDamage, bKilled)
BattleFinished()

What are the odds we could get a GameEvent, perhaps something like GameEvents.CombatDamage, which can return two values to override the damage dealt? The first value could be the damage the attacker receives, the second, the damage the defender receives. Here's a quick little concept of what I mean:

Spoiler :
Code:
local ARCHERY_IMMUNE = GameInfoTypes.PROMOTION_VV_IMMUNE_TO_ARCHERY_ATTACKS
local ARCHER_TYPE = GameInfoTypes.UNITCOMBAT_ARCHER
function OnCombatDamage(iAttackerPlayer, iAttackerUnitID, iAttackerDamage, iDefenderPlayer, iDefenderUnitID, iDefenderDamage)
	local pAttackerPlayer = Players[iAttackerPlayer]
	local pAttackerUnit = pAttackerPlayer:GetUnitByID(iAttackerUnitID)
	if pAttackerUnit:GetUnitCombatType() == ARCHER_TYPE then
		local pDefenderPlayer = Players[iDefenderPlayer]
		local pDefenderUnit = pDefenderPlayer:GetUnitByID(iDefenderUnitID)
		if pDefenderUnit:IsHasPromotion(ARCHERY_IMMUNE) then
			return iAttackerDamage, 0
		end
	end
	return iAttackerDamage, iDefenderDamage
end
GameEvents.CombatDamage.Add(OnCombatDamage)

Something like this is currently "possible" with the RED combat events, but it requires me to heal the unit for the damage it received. While it ends up working from a mechanical standpoint, you still see visual ugliness with the damage and healing popups on the unit's tile, and the combat animations assume the original amount of damage prior to the healing.
 
but since you've mentioned functions like this...

Those are potential events ... I just got lazy adding all the GameEvents....Add() stuff

perhaps something like GameEvents.CombatDamage, which can return two values to override the damage dealt?

Lua events can only return one value, so you'd need to pack the values in Lua and unpack them in the C++ code (eg return iAttackerDamage * 1000 + iDefenderDamage)
 
I would also add that anytime pUnit:ChangeDamage() is called, and enough damage is done to the unit to kill it, that it should count as death for this purpose. Or perhaps it could do so only if the second argument of ChangeDamage (a player ID) is defined and greater than -1. Attrition damage could also count for this, as well as damage from enemy Improvements (i.e. Citadels).

OP updated
 
perhaps something like GameEvents.CombatDamage, which can return two values to override the damage dealt?

Thinking about this a bit more, you only need to return one result, but call the event twice. So if player A's unit X attacks player B's unit Y and does 45 damage while receiving 18, you'd get

BattleDamage(pA, uX, pB, uY, 18)
BattleDamage(pB, uY, pA, uX, 45)

The default would be to return the received (via parameter) damage, but if you wanted to make tanks, planes, etc immune to archery you could check for those conditions and return 0

Then the BattleDamage event in post #3 becomes BattleOutcome, and you no longer need the BattleJoined event (as you can use the BattleDamage event as the cache controller), so a typical melee sequence becomes

Code:
BattleStarted()
BattleDamage(iPlayer, iPlayerUnit, ROLE_ATTACKER, iEnemy, iEnemyUnit, ROLE_DEFENDER, 23) - assume this returns 23
BattleDamage(iEnemy, iEnemyUnit, ROLE_DEFENDER, iPlayer, iPlayerUnit, ROLE_ATTACKER, 78, true) - assume this returns 39
BattleOutcome(iPlayer, iPlayerUnit, ROLE_ATTACKER, 23, false)
BattleOutcome(iEnemy, iEnemyUnit, ROLE_DEFENDER, 39, true)
BattleFinished()
 
That sounds perfectly fine; wasn't actually aware that there was a single-value limitation on Lua returns to the DLL. Hopefully we can see this implemented at some point; it would help me out tremendously!
 
Testers will be needed at some point, as there are a large number of combinations for battles ... ranged on melee, ranged on city, melee on city, city+ranged on melee, missile on naval, air on land with nearby AA, air on naval with nearby anti-air naval, nuke on city, nuke on empty plot including collateral damage, etc, etc, etc

I think I have the basis of the events in place - there are four of them

Code:
-- Battle roles
--   there is ALWAYS an attacker
--   there is USUALLY a defender (but not in the case of a nuke strike against a plot)
--   there MAY be an interceptor
--   there may also be many bystanders (collateral damage from nukes), but the system doesn't handle those at the moment
local iBattleRoleAttacker    = GameInfoTypes.BATTLEROLE_ATTACKER
local iBattleRoleDefender    = GameInfoTypes.BATTLEROLE_DEFENDER
local iBattleRoleInterceptor = GameInfoTypes.BATTLEROLE_INTERCEPTOR


-- Battles can be nested, that is
--   a battle between City X and Unit A starts
--   a battle between Unit Y and Unit A starts
--   the battle between Y and A finishes
--   the battle between X and A finishes
-- Because of this we need to maintain a stack of battles,
local allBattles = {}
-- and to simplify things a reference to the active (top of stack) battle
local activeBattle = nil


-- Event received as a battle starts at the given plot
function OnBattleStarted(iPlotX, iPlotY)
  print(string.format("Battle started at (%i, %i)", iPlotX, iPlotY))

  -- Maintenance stuff, do not edit
  activeBattle = {
    iX=iPlotX, iY=iPlotY,
	attacker=nil,
	defender=nil,
	interceptor=nil
  }
  table.insert(allBattles, activeBattle)
  -- Maintenance stuff ends
  
  -- Insert any code here that needs to run at the very start of the active battle
end
GameEvents.BattleStarted.Add(OnBattleStarted)


-- Event received as a unit/city join the fray as an attacker, defender or interceptor
function OnBattleJoined(iPlayer, iID, iRole, bIsCity)
  local pPlayer = Players[iPlayer]
  local pCombatant = bIsCity and pPlayer:GetCityByID(iID) or pPlayer:GetUnitByID(iID)
  
  print(string.format("Battle joined by %s (%s) as %s for player %i with id %i", (bIsCity and "city" or "unit"), pCombatant:GetName(), GameInfo.BattleRoles[iRole].Type, iPlayer, iID))
  
  if (not activeBattle) then
    print("ERROR: No active battle!!!")
    return
  end
  
  local combatant = {
    iPlayer=iPlayer, iID = iID,
	bIsCity = bIsCity
	-- Stash any other required info about the unit/city,
	-- BUT under NO circumstances store the pointer to the unit/city
  }
  
  -- At this point the unit/city will be alive,
  -- BUT you can make no assumptions about other units in the active battle
  
  if (iRole == iBattleRoleAttacker) then
    activeBattle.attacker = combatant
  elseif (iRole == iBattleRoleDefender) then
    activeBattle.defender = combatant
  else
    activeBattle.interceptor = combatant
  end
end
GameEvents.BattleJoined.Add(OnBattleJoined)


-- Event received to request any change to the calculated damage inflicted BY the combatant with the given role
function OnBattleDamageDelta(iRole, iBaseDamage)
  print(string.format("Battle damage delta requested for %s, base damage is %i", GameInfo.BattleRoles[iRole].Type, iBaseDamage))

  if (not activeBattle) then
    print("ERROR: No active battle!!!")
    return 0
  end
  
  local iDeltaDamage = 0
  
  -- At this point the unit/city with the given role will still be alive,
  -- BUT you can make no assumptions about other units in the active battle

  -- The unit/city with the given role CAUSED iBaseDamage
  -- Calculate any changes here
  -- For example, if the attacker is an archer and the defender is a tank, we would check for
  --   1) iRole == iBattleRoleAttacker
  --   2) the unit class of activeBattle.attacker being an archer
  --   3) the unit class of activeBattle.defender being a tank
  -- and if all conditions are met, we would set iDeltaDamage to -iBaseDamage
  
  return iDeltaDamage
end
GameEvents.BattleDamageDelta.Add(OnBattleDamageDelta)


-- Event received at the end of the battle
function OnBattleFinished()
  if (not activeBattle) then
    print("ERROR: No active battle!!!")
    return 0
  end
  
  print(string.format("Battle finished at (%i, %i)", activeBattle.iX, activeBattle.iY))
  
  -- At this point you can make no assumptions about any of the units that participated in the battle
  -- Insert any code here that needs to run at the end of the active battle
  
  -- We can work out if a combatant died as
  --   1) activeBattle.combatant is not nil
  --   2) activeBattle.combatant.bIsCity is false
  --  3a) Players[activeBattle.combatant.iPlayer]:GetUnitByID(activeBattle.combatant.iID) is nil
  --  3b) Players[activeBattle.combatant.iPlayer]:GetUnitByID(activeBattle.combatant.iID):IsDead()
  --  3c) Players[activeBattle.combatant.iPlayer]:GetUnitByID(activeBattle.combatant.iID):IsDelayedDeath()
  
  -- Maintenance stuff, do not edit
  table.remove(allBattles)
  activeBattle = allBattles[#allBattles]
  -- Maintenance stuff ends
end
GameEvents.BattleFinished.Add(OnBattleFinished)

which produce sequences such as

Spoiler :
Code:
My archer attacked a barbie warrior
  Battle started at (31, 25)
  Battle joined by unit (Archer) as BATTLEROLE_ATTACKER for player 0 with id 24576
  Battle joined by unit (Warrior) as BATTLEROLE_DEFENDER for player 63 with id 49157
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 31
  Battle finished at (31, 25)

My warrior attacked the same barbie warrior
  Battle started at (31, 25)
  Battle joined by unit (Warrior) as BATTLEROLE_ATTACKER for player 0 with id 16385
  Battle joined by unit (Warrior) as BATTLEROLE_DEFENDER for player 63 with id 49157
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 35
  Battle damage delta requested for BATTLEROLE_DEFENDER, base damage is 20
  Battle finished at (31, 25)

My city attacked a different barbie warrior
  Battle started at (33, 26)
  Battle joined by city (Edinburgh) as BATTLEROLE_ATTACKER for player 0 with id 8192
  Battle joined by unit (Warrior) as BATTLEROLE_DEFENDER for player 63 with id 57350
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 28
  Battle finished at (33, 26)

Both barbie warriors attack my warrior
  Battle started at (32, 26)
  Battle joined by unit (Warrior) as BATTLEROLE_ATTACKER for player 63 with id 57350
  Battle joined by unit (Warrior) as BATTLEROLE_DEFENDER for player 0 with id 16385
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 19
  Battle damage delta requested for BATTLEROLE_DEFENDER, base damage is 29
  Battle started at (32, 26)
  Battle joined by unit (Warrior) as BATTLEROLE_ATTACKER for player 63 with id 49157
  Battle joined by unit (Warrior) as BATTLEROLE_DEFENDER for player 0 with id 16385
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 25
  Battle damage delta requested for BATTLEROLE_DEFENDER, base damage is 24
  Battle finished at (32, 26)
  Battle finished at (32, 26)

My archer kills the first barbie warrior
  Battle started at (31, 25)
  Battle joined by unit (Archer) as BATTLEROLE_ATTACKER for player 0 with id 24576
  Battle joined by unit (Warrior) as BATTLEROLE_DEFENDER for player 63 with id 49157
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 10
  Battle finished at (31, 25)

My city splats the second barbie warrior
  Battle started at (33, 26)
  Battle joined by city (Edinburgh) as BATTLEROLE_ATTACKER for player 0 with id 8192
  Battle joined by unit (Warrior) as BATTLEROLE_DEFENDER for player 63 with id 57350
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 28
  Battle finished at (33, 26)

My warrior finishes off the second barbie warrior
  Battle started at (33, 26)
  Battle joined by unit (Warrior) as BATTLEROLE_ATTACKER for player 0 with id 16385
  Battle joined by unit (Warrior) as BATTLEROLE_DEFENDER for player 63 with id 57350
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 21
  Battle damage delta requested for BATTLEROLE_DEFENDER, base damage is 19
  Battle finished at (33, 26)

Spoiler :
Code:
My Comp Bows take pot-shots at Genoa
  Battle started at (27, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 0 with id 49152
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 31
  Battle finished at (27, 21)

  Battle started at (27, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 0 with id 57346
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 28
  Battle finished at (27, 21)

Genoa retaliates against one of my spearmen
  Battle started at (26, 21)
  Battle joined by city (Genoa) as BATTLEROLE_ATTACKER for player 28 with id 8192
  Battle joined by unit (Spearman) as BATTLEROLE_DEFENDER for player 0 with id 73732
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 18
  Battle started at (26, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 28 with id 16384
  Battle joined by unit (Spearman) as BATTLEROLE_DEFENDER for player 0 with id 73732
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 27
  Battle finished at (26, 21)
  Battle finished at (26, 21)
  
More action by my Comp Bows
  Battle started at (27, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 0 with id 49152
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 31
  Battle finished at (27, 21)

  Battle started at (27, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 0 with id 57346
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 25
  Battle finished at (27, 21)

Genoa is hurting my spearman
  Battle started at (26, 21)
  Battle joined by city (Genoa) as BATTLEROLE_ATTACKER for player 28 with id 8192
  Battle joined by unit (Spearman) as BATTLEROLE_DEFENDER for player 0 with id 73732
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 13
  Battle started at (26, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 28 with id 16384
  Battle joined by unit (Spearman) as BATTLEROLE_DEFENDER for player 0 with id 73732
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 22
  Battle finished at (26, 21)
  Battle finished at (26, 21)
  
Reinforcements arrive and capture Genoa
  Battle started at (27, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 0 with id 49152
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 22
  Battle finished at (27, 21)

  Battle started at (27, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 0 with id 57346
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 26
  Battle finished at (27, 21)

  Battle started at (27, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 0 with id 98311
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 29
  Battle finished at (27, 21)

  Battle started at (27, 21)
  Battle joined by unit (Composite Bowman) as BATTLEROLE_ATTACKER for player 0 with id 90118
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 30
  Battle finished at (27, 21)

  Battle started at (27, 21)
  Battle joined by unit (Spearman) as BATTLEROLE_ATTACKER for player 0 with id 65539
  Battle joined by city (Genoa) as BATTLEROLE_DEFENDER for player 28 with id 8192
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 25
  Battle damage delta requested for BATTLEROLE_DEFENDER, base damage is 33
  Battle finished at (27, 21)

Spoiler :
Code:
Nuking a city
  Battle started at (34, 16) of type BATTLETYPE_NUKE
  Battle joined by unit (Nuclear Missile) as BATTLEROLE_ATTACKER for player 0 with id 155658
  Battle joined by city (Delhi) as BATTLEROLE_DEFENDER for player 2 with id 8192
  Battle joined by unit (Warrior) as BATTLEROLE_BYSTANDER for player 2 with id 16385
  Battle joined by unit (Warrior) as BATTLEROLE_BYSTANDER for player 2 with id 24578
  Battle finished at (34, 16)

Nuking a unit
  Battle started at (31, 15) of type BATTLETYPE_NUKE
  Battle joined by unit (Nuclear Missile) as BATTLEROLE_ATTACKER for player 0 with id 163851
  Battle joined by unit (Pikeman) as BATTLEROLE_DEFENDER for player 2 with id 40962
  Battle finished at (31, 15)

Nuking an empty plot
  Battle started at (31, 16) of type BATTLETYPE_NUKE
  Battle joined by unit (Nuclear Missile) as BATTLEROLE_ATTACKER for player 0 with id 172044
  Battle finished at (31, 16)

Triple nesting!
Spoiler :
Code:
  Battle started at (59, 36) of type BATTLETYPE_MELEE
  Battle joined by unit (Warrior) as BATTLEROLE_ATTACKER for player 5 with id 16385
  Battle joined by unit (Brute) as BATTLEROLE_DEFENDER for player 63 with id 49157
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 33
  Battle damage delta requested for BATTLEROLE_DEFENDER, base damage is 21
  Battle started at (59, 36) of type BATTLETYPE_MELEE
  Battle joined by unit (Warrior) as BATTLEROLE_ATTACKER for player 5 with id 65540
  Battle joined by unit (Brute) as BATTLEROLE_DEFENDER for player 63 with id 49157
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 30
  Battle damage delta requested for BATTLEROLE_DEFENDER, base damage is 24
  Battle started at (59, 36) of type BATTLETYPE_MELEE
  Battle joined by unit (Warrior) as BATTLEROLE_ATTACKER for player 5 with id 24578
  Battle joined by unit (Brute) as BATTLEROLE_DEFENDER for player 63 with id 49157
  Battle damage delta requested for BATTLEROLE_ATTACKER, base damage is 30
  Battle damage delta requested for BATTLEROLE_DEFENDER, base damage is 17
  Battle finished at (59, 36)
  Battle finished at (59, 36)
  Battle finished at (59, 36)

The obvious omission at the moment is the Comp Bow in Genoa that goes "poof" when the city is captured
 
These events are in v68 of my DLL which I've just uploaded to my web-site and GitHub

The final test code for them is

Spoiler :
Code:
<GameData>
  <CustomModOptions>
	<Update>
	  <Where Name="EVENTS_BATTLES"/>
	  <Set Value="1"/>
	</Update>
	<Update>
	  <Where Name="EVENTS_BATTLES_DAMAGE"/>
	  <Set Value="1"/>
	</Update>
  </CustomModOptions>
</GameData>

Code:
-- Battle roles
--   there is ALWAYS an attacker
--   there is USUALLY a defender (but not in the case of a nuke strike against an empty plot)
--   there MAY be an interceptor
--   there MAY also be many bystanders (collateral damage from nukes)
local iBattleRoleAttacker    = GameInfoTypes.BATTLEROLE_ATTACKER
local iBattleRoleDefender    = GameInfoTypes.BATTLEROLE_DEFENDER
local iBattleRoleInterceptor = GameInfoTypes.BATTLEROLE_INTERCEPTOR
local iBattleRoleBystander   = GameInfoTypes.BATTLEROLE_BYSTANDER


-- Battles can be nested, that is
--   a battle between City X and Unit A starts
--   a battle between Unit Y and Unit A starts
--   the battle between Y and A finishes
--   the battle between X and A finishes
-- Because of this we need to maintain a stack of battles,
local allBattles = {}
-- and to simplify things a reference to the active (top of stack) battle
local activeBattle = nil


-- Event received as a battle starts at the given plot
function OnBattleStarted(iType, iPlotX, iPlotY)
  print(string.format("Battle started at (%i, %i) of type %s", iPlotX, iPlotY, GameInfo.BattleTypes[iType].Type))

  -- Maintenance stuff, do not edit
  activeBattle = {
    iX = iPlotX, iY = iPlotY,
	attacker = nil,
	defender = nil,
	interceptor = nil,
	bystanders = {}
  }
  table.insert(allBattles, activeBattle)
  -- Maintenance stuff ends
  
  -- Insert any code here that needs to run at the very start of the active battle
end
GameEvents.BattleStarted.Add(OnBattleStarted)


-- Event received as a unit/city join the fray as an attacker, defender, interceptor or bystander
function OnBattleJoined(iPlayer, iID, iRole, bIsCity)
  local pPlayer = Players[iPlayer]
  local pCombatant = bIsCity and pPlayer:GetCityByID(iID) or pPlayer:GetUnitByID(iID)
  
  print(string.format("Battle joined by %s (%s) as %s for player %i with id %i", (bIsCity and "city" or "unit"), pCombatant:GetName(), GameInfo.BattleRoles[iRole].Type, iPlayer, iID))
  
  if (not activeBattle) then
    print("ERROR: No active battle!!!")
    return
  end
  
  local combatant = {
    iPlayer = iPlayer, iID = iID,
    bIsCity = bIsCity
    -- Stash any other required info about the unit/city,
    -- BUT under NO circumstances store the pointer to the unit/city
  }
  
  -- At this point the unit/city will be alive,
  -- BUT you can make no assumptions about other units in the active battle
  
   -- Maintenance stuff, do not edit
   if (iRole == iBattleRoleAttacker) then
    activeBattle.attacker = combatant
  elseif (iRole == iBattleRoleDefender) then
    activeBattle.defender = combatant
  elseif (iRole == iBattleRoleInterceptor) then
    activeBattle.interceptor = combatant
  else
    table.insert(activeBattle.bystanders, combatant)
  end
  -- Maintenance stuff ends
end
GameEvents.BattleJoined.Add(OnBattleJoined)


-- Event received to request any change to the calculated damage inflicted BY the combatant with the given role
function OnBattleDamageDelta(iRole, iBaseDamage)
  print(string.format("Battle damage delta requested for %s, base damage is %i", GameInfo.BattleRoles[iRole].Type, iBaseDamage))

  if (not activeBattle) then
    print("ERROR: No active battle!!!")
    return 0
  end
  
  local iDeltaDamage = 0
  
  -- At this point the unit/city with the given role will still be alive,
  -- BUT you can make no assumptions about other units in the active battle

  -- The unit/city with the given role CAUSED iBaseDamage
  -- Calculate any changes here
  -- For example, if the attacker is an archer and the defender is a tank, we would check for
  --   1) iRole == iBattleRoleAttacker
  --   2) the unit class of activeBattle.attacker being an archer
  --   3) the unit class of activeBattle.defender being a tank
  -- and if all conditions are met, we would set iDeltaDamage to -iBaseDamage
  
  return iDeltaDamage
end
GameEvents.BattleDamageDelta.Add(OnBattleDamageDelta)


-- Event received at the end of the battle
function OnBattleFinished()
  if (not activeBattle) then
    print("ERROR: No active battle!!!")
    return 0
  end
  
  print(string.format("Battle finished at (%i, %i)", activeBattle.iX, activeBattle.iY))
  
  -- At this point you can make no assumptions about any of the units that participated in the battle
  -- Insert any code here that needs to run at the end of the active battle
  
  -- We can work out if a combatant died as
  --   1) activeBattle.combatant is not nil
  --   2) activeBattle.combatant.bIsCity is false
  --  3a) Players[activeBattle.combatant.iPlayer]:GetUnitByID(activeBattle.combatant.iID) is nil
  --  3b) Players[activeBattle.combatant.iPlayer]:GetUnitByID(activeBattle.combatant.iID):IsDead()
  --  3c) Players[activeBattle.combatant.iPlayer]:GetUnitByID(activeBattle.combatant.iID):IsDelayedDeath()
  -- Similar logic can be used to ascertain if a city was captured
  
  -- Maintenance stuff, do not edit
  table.remove(allBattles)
  activeBattle = allBattles[#allBattles]
  -- Maintenance stuff ends
end
GameEvents.BattleFinished.Add(OnBattleFinished)

A word of warning about enabling the battle events ... they can seriously overload the game.

It's best to enable them via Lua and not XML. Enabling them via XML, eg
Code:
  <CustomModOptions>
    <Update>
      <Where Name="EVENTS_BATTLES"/>
      <Set Value="1"/>
    </Update>
    <Update>
      <Where Name="EVENTS_BATTLES_DAMAGE"/>
      <Set Value="1"/>
    </Update>
  </CustomModOptions>
means the events are being sent, even if the Civ requiring them is not in play

To enable them via Lua
Code:
if (theCivIsInPlay()) then
  for _ in DB.Query("UPDATE CustomModOptions SET Value=1 WHERE NAME='EVENTS_BATTLES'") do end
  for _ in DB.Query("UPDATE CustomModOptions SET Value=1 WHERE NAME='EVENTS_BATTLES_DAMAGE'") do end
  Game.ReloadCustomModOptions()
end

I've learnt a lot about combat during this process ... mainly why there are no Firaxis supplied universal combat events!

Some situations are still not covered, eg civilians captured "underneath" combat units, and units destroyed as cities are captured - but at least you'll get a UnitPrekill event for those and you'll now be able to tie up their plot with an active battle.

Edit: "Some situations are still not covered, eg civilians captured "underneath" combat units, and units destroyed as cities are captured" - both of these, in fact all possible situations, are easily covered within Lua. When the battle is joined by the defender (city or unit), just add all the other units on the battle tile into the bystanders table.

Code:
  elseif (iRole == iBattleRoleDefender) then
    activeBattle.defender = combatant

    [COLOR="blue"]-- Loop all units on the battle tile, and add any others to the bystanders table
    local iMe = bIsCity and -1 or iID
    local pPlot = bIsCity and pCombatant:Plot() or pCombatant:GetPlot()
    for i = 0, pPlot:GetNumUnits()-1, 1 do
      local pUnit = pPlot:GetUnit(i)
      if (pUnit:GetID() ~= iMe) then
        table.insert(activeBattle.bystanders, {iPlayer = iPlayer, iID = pUnit:GetID(), bIsCity = false})
      end
    end[/COLOR]
  elseif (iRole == iBattleRoleInterceptor) then
 
Code using the events to create a UnitDied event (see very end of the Lua below)

Code:
<GameData>
  <CustomModOptions>
	<Update>
	  <Where Name="EVENTS_BATTLES"/>
	  <Set Value="1"/>
	</Update>
  </CustomModOptions>
</GameData>

Code:
-- Battle roles
--   there is ALWAYS an attacker
--   there is USUALLY a defender (but not in the case of a nuke strike against an empty plot)
--   there MAY be an interceptor
--   there MAY also be many bystanders (collateral damage from nukes, captured citizens, defenders in cities, etc)
local iBattleRoleAttacker    = GameInfoTypes.BATTLEROLE_ATTACKER
local iBattleRoleDefender    = GameInfoTypes.BATTLEROLE_DEFENDER
local iBattleRoleInterceptor = GameInfoTypes.BATTLEROLE_INTERCEPTOR
local iBattleRoleBystander   = GameInfoTypes.BATTLEROLE_BYSTANDER
 
-- Battles can be nested, that is
--   a battle between City X and Unit A starts
--   a battle between Unit Y and Unit A starts
--   the battle between Y and A finishes
--   the battle between X and A finishes
-- Because of this we need to maintain a stack of battles,
local allBattles = {}
-- and to simplify things a reference to the active (top of stack) battle
local activeBattle = nil
 
-- As battles can be nested, it is possible for a unit to die more than once!
-- So keep a list of units that the event has been sent for
local unitEvents = {}
 
 
function OnBattleStarted(iType, iPlotX, iPlotY)
  -- print(string.format("Battle started at (%i, %i) of type %s", iPlotX, iPlotY, GameInfo.BattleTypes[iType].Type))
 
  -- Maintenance stuff, do not edit
  activeBattle = {
    iX = iPlotX, iY = iPlotY,
    combatants = {}
  }
  table.insert(allBattles, activeBattle)
  -- Maintenance stuff ends
end
GameEvents.BattleStarted.Add(OnBattleStarted)
 
-- Event received as a unit/city join the fray as an attacker, defender, interceptor or bystander
function OnBattleJoined(iPlayer, iID, iRole, bIsCity)
  local pPlayer = Players[iPlayer]
  local pCombatant = bIsCity and pPlayer:GetCityByID(iID) or pPlayer:GetUnitByID(iID)
  -- print(string.format("Battle joined by %s (%s) as %s for player %i with id %i", (bIsCity and "city" or "unit"), pCombatant:GetName(), GameInfo.BattleRoles[iRole].Type, iPlayer, iID))

  if (not activeBattle) then
    -- print("ERROR: No active battle!!!")
    return
  end

  local combatant = {
    iPlayer = iPlayer, iID = iID,
    bIsCity = bIsCity,
    iRole = iRole,
    iX = pCombatant:GetX(), iY = pCombatant:GetY()
    -- Stash any other required info about the unit/city,
    -- BUT under NO circumstances store the pointer to the unit/city
  }

  table.insert(activeBattle.combatants, combatant)
  if (iRole == iBattleRoleDefender) then
    -- Loop all units on the battle tile, and add any others to the combatants table
    local iMe = bIsCity and -1 or iID
    local pPlot = bIsCity and pCombatant:Plot() or pCombatant:GetPlot()
    for i = 0, pPlot:GetNumUnits()-1, 1 do
      local pUnit = pPlot:GetUnit(i)
      if (pUnit:GetID() ~= iMe) then
        OnBattleJoined(iPlayer, pUnit:GetID(), iBattleRoleBystander, false)
      end
    end
  end
end
GameEvents.BattleJoined.Add(OnBattleJoined)
 
function OnBattleFinished()
  if (not activeBattle) then
    -- print("ERROR: No active battle!!!")
    return 0
  end

  -- print(string.format("Battle finished at (%i, %i)", activeBattle.iX, activeBattle.iY))
  -- Test all the partcipants to see if they died, if so, send an event
  for _, combatant in ipairs(activeBattle.combatants) do
    if (IsDead(combatant)) then
      SendUnitDiedEvent(activeBattle.iX, activeBattle.iY, combatant)
    end
  end

  -- Maintenance stuff, do not edit
  table.remove(allBattles)
  activeBattle = allBattles[#allBattles]
  -- Maintenance stuff ends
end
GameEvents.BattleFinished.Add(OnBattleFinished)
 
function IsDead(combatant)
  if (combatant and not combatant.bIsCity) then
    local pUnit = Players[combatant.iPlayer]:GetUnitByID(combatant.iID)
    
    if (not pUnit or pUnit:IsDead() or pUnit:IsDelayedDeath()) then
      return true
    end
  end

  return false
end
 
function SendUnitDiedEvent(iBattlePlotX, iBattlePlotY, combatant)
  local iPlayer = combatant.iPlayer
  local iID = combatant.iID

  if (not unitEvents[iPlayer]) then
    unitEvents[iPlayer] = {}
  end

  if (not unitEvents[iPlayer][iID]) then
    LuaEvents.UnitDied(iPlayer, iID, iBattlePlotX, iBattlePlotY, combatant)
  end

  unitEvents[iPlayer][iID] = true
end
 
function OnPlayerDoTurn(iPlayer)
  unitEvents = {}
end
GameEvents.PlayerDoTurn.Add(OnPlayerDoTurn)
 
function OnUnitDied(iPlayer, iID, iBattlePlotX, iBattlePlotY, combatant)
  print(string.format("The unit with ID %i belonging to player %i died during a battle at (%i, %i), they were at (%i, %i)", iID, iPlayer, iBattlePlotX, iBattlePlotY, combatant.iX, combatant.iY))
  -- If you need more info about the unit that died, you'll need to stash it into the combatant table above and access it via the 5th parameter
end
LuaEvents.UnitDied.Add(OnUnitDied)
 
Which I suppose means I should retitle the thread "Now there is a UnitDied event ..." :D
 
It's best to enable them (the battle events) via Lua and not XML.

Just to clarify, this is the same reasoning as only hooking standard events in the civ is actually in play - see here

Game.ReloadCustomModOptions() should only be called once per mod as the game starts and not every turn - it basically trashes the memory cache of the <CustomModOptions> database table and reloads it.

(Note: There is also a Game.ReloadGameDataDefines() which does the same for the <Defines> database table.)
 
Back
Top Bottom