myspace / Lua /Satellite /SatelliteConflict.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
69.8 kB
if FirstLoad then
g_ConflictSectors = false
end
function OnMsg.PreLoadSessionData()
g_ConflictSectors = {}
for id, sector in pairs(gv_Sectors) do
if sector.conflict then
g_ConflictSectors[#g_ConflictSectors + 1] = id
end
end
table.sort(g_ConflictSectors)
end
function OnMsg.NewGame(game)
g_ConflictSectors = {}
end
function IsConflictMode(sector_id)
if sector_id then
return not not table.find(g_ConflictSectors, sector_id), gv_Sectors[sector_id]
else
return #g_ConflictSectors > 0, gv_Sectors[g_ConflictSectors[1]]
end
end
function AnyNonWaitingConflict()
for i, sectorId in ipairs(g_ConflictSectors) do
local sector = gv_Sectors[sectorId]
if not sector.conflict.waiting then
return sector
end
end
return false
end
function GetConflictCustomDescr(sector)
if not sector then return end
local conflict = sector.conflict
local preset = conflict and ConflictDescriptionDefs[conflict.descr_id or false]
local custom = preset and preset.description
if custom then return custom end
if conflict and conflict.spawn_mode == "defend" then
return T{129191214872, ConflictDescriptionDefs.DefaultDefend.description, sector}
else
return T{292704915698, ConflictDescriptionDefs.DefaultAttack.description, sector}
end
end
function GetConflictCustomTitle(sector)
if not sector then return end
local conflict = sector.conflict
local preset = conflict and ConflictDescriptionDefs[conflict.descr_id or false]
local custom = preset and preset.title
if custom then return custom end
return T(829620197199, "ENEMY PRESENCE")
end
TFormat.SectorConflictCustomDescr = GetConflictCustomDescr
TFormat.GetConflictCustomTitle = GetConflictCustomTitle
function SatelliteRetreat(sector_id, sides_to_retreat)
NetUpdateHash("SatelliteRetreat", sector_id)
local sector = gv_Sectors[sector_id]
if not sector.conflict then return end
sides_to_retreat = sides_to_retreat or {"player1"}
local previousSector = false
local squadsToRetreat = {}
for i, squad in ipairs(g_SquadsArray) do
if squad.militia then goto continue end
if squad.CurrentSector ~= sector_id then goto continue end
if not IsSquadInConflict(squad) then goto continue end
if not table.find(sides_to_retreat, squad.Side) then goto continue end
squadsToRetreat[#squadsToRetreat + 1] = squad
-- How?!?
if squad.PreviousSector == sector_id then
squad.PreviousSector = false
end
if squad.PreviousSector then
previousSector = squad.PreviousSector
end
::continue::
end
-- Make sure all squads have a previous sector (inherit from other squads here)
-- If none of them have one auto resolve shouldn't have been allowed in the first place.
-- See IsAutoResolveEnabled
if previousSector then
for i, squad in ipairs(squadsToRetreat) do
if not squad.PreviousSector then
squad.PreviousSector = previousSector
end
end
end
for i, squad in ipairs(squadsToRetreat) do
local prev_sector_id = (sector.conflict.player_attacking and sector.conflict.prev_sector_id) or squad.PreviousSector
if IsWaterSector(prev_sector_id) and squad.PreviousLandSector then
prev_sector_id = squad.PreviousLandSector
end
if not prev_sector_id then goto continue end
if (IsSectorUnderground(sector_id) and not IsSectorUnderground(prev_sector_id)) or
(IsSectorUnderground(prev_sector_id) and not IsSectorUnderground(sector_id)) then
SetSatelliteSquadCurrentSector(squad, prev_sector_id, true, true)
else
-- Find best retreat sector if previous sector is a bad idea.
local badRetreat = false
local otherSideSquads = (squad.Side == "enemy1" or squad.Side == "enemy2") and g_PlayerAndMilitiaSquads or g_EnemySquads
-- Check if any of the other side squads on my sector are travelling towards the one I want to retreat to.
for i, os in ipairs(otherSideSquads) do
if os.CurrentSector == sector_id and os.route then
local nextDest = os.route[1] and os.route[1][1]
if nextDest == prev_sector_id then
badRetreat = true
break
end
end
end
-- Check if retreating into water.
local prevSector = gv_Sectors[prev_sector_id]
local illegalRetreat = prevSector.Passability == "Water" or prevSector.Passability == "Blocked"
badRetreat = badRetreat or illegalRetreat
-- Check if retreating into enemies.
if not badRetreat then badRetreat = not not table.find(otherSideSquads, "CurrentSector", prev_sector_id) end
-- Try to find a better retreat position, such as one without other side squads.
if badRetreat then
local illegalRetreatFallback, foundSector = false, false
ForEachSectorCardinal(sector_id, function(otherSecId)
local considerThisSector = false
local otherSec = gv_Sectors[otherSecId]
if SideIsAlly(otherSec.Side, squad.Side) then -- If my side, then there aren't any baddies there.
considerThisSector = true
elseif not table.find(otherSideSquads, "CurrentSector", otherSecId) then
considerThisSector = true
end
local forbiddenRoute = IsRouteForbidden({{otherSecId}}, squad)
-- If illegal retreat consider the first non-forbidden route sector, regardless of the
-- "other-side" conditions above.
if illegalRetreat and not illegalRetreatFallback and not forbiddenRoute then
illegalRetreatFallback = otherSecId
end
if considerThisSector and not forbiddenRoute then
foundSector = true
prev_sector_id = otherSecId
return "break"
end
end)
if illegalRetreat and not foundSector and illegalRetreatFallback then
prev_sector_id = illegalRetreatFallback
end
end
local retreatRoute = GenerateRouteDijkstra(squad.CurrentSector, prev_sector_id, false, squad.units, "retreat", squad.CurrentSector, squad.side)
if not retreatRoute then
-- Ehh, what?
assert(false) -- Retreat route is invalid
retreatRoute = { prev_sector_id } -- Fallback to just get out of this sector.
end
-- Try to retain the joining squad if it will retreat in the same direction it was going anyway
local keepJoining = false
if squad.joining_squad then
local squadToJoin = gv_Squads[squad.joining_squad]
keepJoining = squadToJoin and squadToJoin.CurrentSector == prev_sector_id
end
SetSatelliteSquadRetreatRoute(squad, { retreatRoute }, keepJoining)
end
::continue::
end
ResolveConflict(sector, "no voice", false, "retreat")
ResumeCampaignTime("UI")
end
function NetSyncEvents.UISatelliteRetreat(sector_id, sides_to_retreat)
SatelliteRetreat(sector_id, sides_to_retreat)
local satCon = GetDialog("SatelliteConflict")
if satCon then CloseDialog("SatelliteConflict") end
end
GameVar("ForceReloadSectorMap", false)
function EnterConflict(sector, prev_sector_id, spawn_mode, disable_travel, locked, descr_id, force, from_map)
-- Forced conflicts are resolved via quest events and such,
sector = sector or gv_Sectors[gv_CurrentSectorId]
local sector_id = sector and sector.Id
if not sector then return end
-- Separate enemy attack from player attack to set player_attacking
local playerInvading = spawn_mode == "attack"
if spawn_mode == "enemy_attack" then
spawn_mode = "attack"
end
-- Already in conflict, and waiting
if IsConflictMode(sector_id) then
if locked ~= nil then sector.conflict.locked = locked end
if locked ~= nil then sector.conflict.descr_id = descr_id end
if disable_travel ~= nil then sector.conflict.disable_travel = disable_travel end
--sector.conflict.spawn_mode = spawn_mode or sector.conflict.spawn_mode
sector.conflict.prev_sector_id = prev_sector_id or sector.conflict.prev_sector_id
if sector.conflict.waiting then
sector.conflict.player_attacking = playerInvading
sector.conflict.waiting = playerInvading or EnemyWantsToWait(sector_id)
if sector.conflict.waiting then
ResumeCampaignTime("SatelliteConflict")
else
PauseCampaignTime("SatelliteConflict")
end
if gv_SatelliteView then
ObjModified(SelectedObj)
ObjModified(Game)
OpenSatelliteConflictDlg(sector)
RequestAutosave{ autosave_id = "satelliteConflict", save_state = "CombatStart", display_name = T{285747878633, "Satellite_Conflict_<u(sector)>", sector = sector.name}, mode = "delayed" }
end
end
return
end
local no_exploration_resolve = false
if force == "force-exploration-only" then
no_exploration_resolve = true
elseif force then
sector.ForceConflict = true
end
table.insert(g_ConflictSectors, sector_id)
table.sort(g_ConflictSectors)
sector.conflict = {
prev_sector_id = prev_sector_id or nil,
spawn_mode = spawn_mode or nil,
player_attacking = playerInvading,
disable_travel = disable_travel or nil,
locked = locked,
descr_id = descr_id or sector.CustomConflictDescr or (not playerInvading and "SectorAttacked"),
waiting = playerInvading or EnemyWantsToWait(sector_id),
from_map = from_map,
no_exploration_resolve = no_exploration_resolve
}
if InteractionSeeds then
local prediction, player_power, enemy_power = GetAutoResolveOutcome(sector)
sector.conflict.predicted_autoresolve = prediction
sector.conflict.player_power = player_power
sector.conflict.enemy_power = enemy_power
end
-- Check if we gameovered in a conflict here then copy special properties.
if sector.conflict_backup then
local backupConflict = sector.conflict_backup
local newConflict = sector.conflict
newConflict.locked = backupConflict.locked
newConflict.disable_travel = backupConflict.disable_travel
newConflict.descr_id = backupConflict.descr_id
newConflict.initial_sector = backupConflict.initial_sector
sector.conflict_backup = false
end
Msg("ConflictStart", sector_id)
local squads = GetSquadsInSectorCombined(sector_id, false, true)
for i, squad in ipairs(squads) do
SatelliteSquadWaitInSector(squad, false)
if squad.route then squad.route.satellite_tick_passed = false end
ObjModified(squad)
end
if gv_SatelliteView then
OpenSatelliteConflictDlg(sector, "auto-open")
RequestAutosave{ autosave_id = "satelliteConflict", save_state = "CombatStart", display_name = T{285747878633, "Satellite_Conflict_<u(sector)>", sector = sector.name}, mode = "delayed" }
if spawn_mode and sector_id == gv_CurrentSectorId then -- if an enemy squad attacks the squad on the current sector, reload sector map
ForceReloadSectorMap = true
ObjModified("gv_SatelliteView")
end
ObjModified(gv_Squads)
end
if not sector.conflict.waiting then PauseCampaignTime("SatelliteConflict") end
UpdateEntranceAreasVisibility()
ObjModified(SelectedObj)
ObjModified(Game)
ExecuteSectorEvents("SE_OnConflictStarted", sector_id)
end
function EnemyWantsToWait(sector, get_squads)
local enemySquadsEnroute = GetSquadsEnroute(sector, "enemy1")
if #enemySquadsEnroute == 0 then return false end
local squads = {}
local waitTime = const.Satellite.EnemySquadWaitTime
for i, s in ipairs(enemySquadsEnroute) do
local estimatedTravelTime = GetTotalRouteTravelTime(s.CurrentSector, s.route, s)
if estimatedTravelTime < waitTime then
if not get_squads then
return true
else
squads[#squads+1] = {s, arriving = estimatedTravelTime>0 and estimatedTravelTime or 0}
end
end
end
if not next(squads) then
return false
else
return true, squads
end
end
function CanGoInMap(sector)
sector = gv_Sectors[sector]
if not sector.Map then return false end
if not sector.conflict then return true end
if not sector.conflict.waiting then return true end
if sector.conflict.waiting then
if sector.conflict.player_attacking then
return true
end
return false, "enemy waiting"
end
return false
end
local function lTravellingTowardsSectorCenter(sq, sector_id)
return sq.route and sq.route[1] and #sq.route[1] > 0 and sq.route[1][1] == sector_id
end
-- return value is from player perspective
function GetConflictSide(squad, sector_id)
local player_squad = squad.Side == "player1" or squad.Side == "player2"
local owningSide = gv_Sectors[sector_id].Side
local sectorOwnedByPlayer = owningSide == "player1" or owningSide == "player2" or owningSide == "ally"
if player_squad and not sectorOwnedByPlayer then
return "attack"
elseif not player_squad and not sectorOwnedByPlayer then
-- the player is attacking, but an enemy reinforcement has arrived
return "enemy_attack"
end
return "defend"
end
local function lGetSquadsForConflict(squad)
local sector_id = squad.CurrentSector
local sector = gv_Sectors[sector_id]
local allySquads, enemySquads = GetSquadsInSector(sector_id, false, true, true)
-- note: retreating squads are filtered below conditionally
-- Filter out neutral squads
local enemiesForReal = {}
for i, s in ipairs(enemySquads) do
if s.Side == "enemy1" or s.Side == "enemy2" then
enemiesForReal[#enemiesForReal + 1] = s
end
end
enemySquads = enemiesForReal
if #allySquads > 0 and #enemySquads > 0 then
local travellingPlayer = false
local travellingEnemy = false
local nonTravellingPlayer = false
local nonTravellingEnemy = false
local nonRetreatingAlly = {}
local nonRetreatingEnemy = {}
for i, squad in ipairs(allySquads) do
if (not squad.Retreat or SquadReachedDest(squad)) and not IsTraversingShortcut(squad) then
nonRetreatingAlly[#nonRetreatingAlly + 1] = squad
end
end
for i, squad in ipairs(enemySquads) do
if (not squad.Retreat or SquadReachedDest(squad)) and not IsTraversingShortcut(squad) then
nonRetreatingEnemy[#nonRetreatingEnemy + 1] = squad
end
end
for i, squad in ipairs(nonRetreatingAlly) do
local travelling = not IsSquadInSectorVisually(squad, sector_id)
if travelling then
travellingPlayer = true
else
nonTravellingPlayer = true
end
end
for i, squad in ipairs(nonRetreatingEnemy) do
local travelling = not IsSquadInSectorVisually(squad, sector_id)
if travelling then
travellingEnemy = true
else
nonTravellingEnemy = true
end
end
local bothSidesTraveling = travellingEnemy and travellingPlayer
local bothSidesInCenter = nonTravellingPlayer and nonTravellingEnemy
local conflictWillHappen = bothSidesTraveling or bothSidesInCenter
-- If there is an existing conflict (a waiting conflict) retriggering it will end the wait,
-- however we want that to happen only when a squad reaches the center.
-- To avoid teleporting squads traveling towards that waiting conflict as soon as
-- they pass the sector boundary we need the check below.
--
-- Note that squads that are past the sector boundary will all get teleported into the conflict
-- regardless whether the conflict produced will be waiting and they are travelling towards it.
if sector.conflict then
conflictWillHappen = conflictWillHappen and IsSquadInSectorVisually(squad, sector_id)
end
return conflictWillHappen, nonRetreatingAlly, nonRetreatingEnemy
end
end
function CheckAndEnterConflict(sector, squad, prev_sector_id)
local sector_id = sector.Id
local conflictSide = GetConflictSide(squad, sector_id)
if sector.Passability ~= "Water" then
local conflictStart, playerSquads, enemySquads = lGetSquadsForConflict(squad)
if conflictStart then
-- Teleport all travelling squads to the sector center
for i = 1, #playerSquads + #enemySquads do
local squad = i <= #playerSquads and playerSquads[i] or enemySquads[i - #playerSquads]
if IsSquadTravelling(squad) then
-- Reset travel status to as if plotting a route from conflict.
squad.route.satellite_tick_passed = false
local movingToConflict = lTravellingTowardsSectorCenter(squad, sector_id)
SatelliteReachSectorCenter(squad.UniqueId, sector_id, prev_sector_id, not movingToConflict, true)
end
end
EnterConflict(sector, prev_sector_id, conflictSide)
end
end
return conflictSide
end
-- Similar code to above but used for SectorEnterConflict effect.
-- The squads returned are the same as the ones returned from PlayerPresentInSector
-- as the two effects are often used together
function ForceEnterConflictEffect(sector, spawnMode, ...)
local sector_id = sector.Id
local playerSquads, enemySquads = GetSquadsInSector(sector_id, false, false, true)
-- Dont allow even a forced conflict in this case as it will break the game.
if #playerSquads == 0 then return end
-- Teleport all travelling squads to the sector center
for i = 1, #playerSquads + #enemySquads do
local squad = i <= #playerSquads and playerSquads[i] or enemySquads[i - #playerSquads]
if IsSquadTravelling(squad) then
-- Reset travel status to as if plotting a route from conflict.
squad.route.satellite_tick_passed = false
local movingToConflict = lTravellingTowardsSectorCenter(squad, sector_id)
SatelliteReachSectorCenter(squad.UniqueId, sector_id, squad.PreviousSector, not movingToConflict, true)
end
end
-- Ensure the conflict has a proper spawn mode
if not spawnMode then
spawnMode = sector.Side == "player1" and "defend" or "attack"
end
EnterConflict(sector, nil, spawnMode, ...)
end
function OnMsg.EnterSector(gameStart, gameLoaded)
if gameLoaded then return end -- Trust the saved ForceReloadSectorMap
ForceReloadSectorMap = false
end
GameVar("SatQueuedResolveConflict", function() return {} end)
function OnMsg.StartSatelliteGameplay()
if not SatQueuedResolveConflict or #SatQueuedResolveConflict == 0 then return end
assert(g_SatelliteUI)
for i, sId in ipairs(SatQueuedResolveConflict) do
local sector = gv_Sectors[sId]
if sector.conflict then
AutoResolveConflict(sector)
end
end
table.clear(SatQueuedResolveConflict)
end
-- bNoVoice is unused
function ResolveConflict(sector, bNoVoice, isAutoResolve, isRetreat)
gv_ActiveCombat = false
sector = sector or gv_Sectors[gv_CurrentSectorId]
-- If there is both a militia and enemy squad remaining
-- autoresolve the conflict between them to decide the outcome.
local mercSquads, enemySquads = GetSquadsInSector(sector.Id, "no_travel", not "include_militia", "no_arriving", "no_retreat")
local militiaLeft = GetMilitiaSquads(sector)
if #militiaLeft > 0 and #enemySquads > 0 then
if isAutoResolve then
assert(false) -- Auto resolve is causing another auto resolve, infinite loop!
table.remove_value(g_ConflictSectors, sector.Id)
sector.conflict = false
if not AnyNonWaitingConflict() then
ResumeCampaignTime("SatelliteConflict")
end
return
end
if g_SatelliteUI then
AutoResolveConflict(sector)
elseif not table.find(SatQueuedResolveConflict, sector.Id) then
SatQueuedResolveConflict[#SatQueuedResolveConflict + 1] = sector.Id
end
return
end
local playerAttacking = sector.conflict and sector.conflict.player_attacking
local fromMap = sector.conflict and sector.conflict.from_map
if sector then -- when going into combat without new game / proper campaign setup
table.remove_value(g_ConflictSectors, sector.Id)
sector.conflict = false
if not g_Combat then
ShowTacticalNotification("conflictResolved")
PlayFX("NoEnemiesLeft", "start")
end
end
if not AnyNonWaitingConflict() then
ResumeCampaignTime("SatelliteConflict")
end
UpdateEntranceAreasVisibility()
-- UI update and restoring travel state from before conflict
local squads = GetSquadsInSector(sector.Id)
for i, squad in ipairs(squads) do
ObjModified(squad)
end
ObjModified(SelectedObj)
ObjModified("gv_SatelliteView")
UpdateSectorControl(sector.Id)
-- Check if player died in tactical view from enemies that dont have squads.
if (sector.Side == "player1" or sector.Side == "player2") and not gv_SatelliteView and #mercSquads == 0 then
local playerUnitsOnMap = GetCurrentMapUnits("player")
local enemyUnitsOnMap = GetCurrentMapUnits("enemy")
if #playerUnitsOnMap == 0 and #enemyUnitsOnMap > 0 then
local first = enemyUnitsOnMap[1]
SatelliteSectorSetSide(sector.Id, "enemy1")
end
end
local playerWon = not isRetreat and (sector.Side == "player1" or sector.Side == "player2")
if playerWon then
sector.CustomConflictDescr = false
RollForMilitiaPromotion(sector)
end
Msg("ConflictEnd", sector, bNoVoice, playerAttacking, playerWon, isAutoResolve, isRetreat, fromMap)
end
function OnMsg.SquadDespawned(squad_id, sector_id)
-- Prevent sector takeover on arriving squad despawning on a force conflict sector as
-- this logic will run before the "squad in sector" logic that will trigger the conflict
if gv_Sectors[sector_id].ForceConflict then return end
UpdateSectorControl(sector_id)
end
function UpdateSectorControl(sector_id)
if not sector_id then
return
end
local allySquads, enemySquads = GetSquadsInSector(sector_id, "excludeTravel", "includeMilitia", "excludeArrive", "excludeRetreat")
local playerHere = #allySquads > 0
local enemiesHere = false
for _, squad in ipairs(enemySquads) do
if squad.Side == "neutral" then goto continue end
local all_dead = true
for _, unit_id in ipairs(squad.units) do
local unit = gv_SatelliteView and gv_UnitData[unit_id] or g_Units[unit_id]
if unit and not unit:IsDead() then
all_dead = false
break
end
end
enemiesHere = not all_dead and squad.Side
::continue::
end
-- If both sides are present dont update the sector side.
-- It should be updated by the resolution of the conflict.
if enemiesHere and playerHere then return end
if enemiesHere then
SatelliteSectorSetSide(sector_id, enemiesHere)
elseif playerHere then
SatelliteSectorSetSide(sector_id, "player1")
end
end
function NetEvents.ResolveConflict(sector, bNoVoice) -- left only for testing from the console
ResolveConflict(gv_Sectors[sector], bNoVoice)
end
function GetSectorConflict(sector_id)
local sector = gv_Sectors and gv_Sectors[sector_id or gv_CurrentSectorId]
return sector and sector.conflict
end
function SatelliteConflict_IsSquadDefeated(squad)
local is_squad_defeated = true
for _, unit_id in ipairs(squad.units) do
if gv_UnitData[unit_id] and not gv_UnitData[unit_id]:IsDead() then
is_squad_defeated = false
break
end
end
return is_squad_defeated
end
function SatelliteConflict_SurviveDefeatedText(squads, lost_text)
local defeated = 0
for _, s in ipairs(squads) do
if SatelliteConflict_IsSquadDefeated(s) then
defeated = defeated + 1
end
end
local survived = #squads - defeated
local surv = T{221106911816, "<survive> survived", survive = survived==1 and T(792634263677, "1 squad") or T{811215070300, "<count> squads", count = survived}}
local lost = T{949500986907, "<lost> lost", lost = defeated==1 and T(792634263677, "1 squad") or T{811215070300, "<count> squads", count = defeated}}
if lost_text then
lost = (defeated==1 and T(792634263677, "1 squad") or T{811215070300, "<count> squads", count = defeated}) .." "..lost_text
end
if defeated<=0 then return surv end
if survived<=0 then return lost end
return Untranslated(surv.." / "..lost)
end
function CheckMapConflictResolved(no_voice)
if GameState.entering_sector then return end
local sector = gv_Sectors[gv_CurrentSectorId]
local playerUnits = GetCurrentMapUnits("player")
local enemy_win = #playerUnits == 0
local enemy_units = GetCurrentMapUnits("enemy")
local player_win = true
for _, unit in ipairs(enemy_units) do
-- conflict can be resolved if all the remaining enemy units are both non-human and not aware
player_win = player_win and not unit.Squad and not unit:IsAware()
end
if sector and sector.conflict and not sector.conflict.locked and (player_win or enemy_win) then
sector.ForceConflict = false
local isRetreat = not player_win and enemy_win
-- Assert that this is correct
if isRetreat then
for i, u in ipairs(playerUnits) do
assert(u.retreat_to_sector)
end
end
ResolveConflict(sector, no_voice, false, isRetreat)
end
end
function OnMsg.CombatEnd(combat)
if not combat.test_combat then
CheckMapConflictResolved()
end
end
function OnMsg.UnitDied()
if not g_Combat then
CheckMapConflictResolved()
end
end
function OnMsg.VillainDefeated(unit)
if not g_Combat then
CheckMapConflictResolved()
end
end
function OnMsg.ExplorationTick()
if g_TestCombat then return end
local sector = gv_Sectors[gv_CurrentSectorId]
if not sector then return end
if not sector.conflict then return end
if sector.conflict.locked then return end
if sector.conflict.no_exploration_resolve then return end
if sector.ForceConflict then return end
CheckMapConflictResolved()
end
local function lTacticalModeCheckEnterConflict()
if GameState.disable_tactical_conflict then return end
if GameState.Conflict or not gv_ActiveCombat then return end
-- start conflict if there are aware units of opposing sides
for _, unit in ipairs(g_Units) do
if not unit:IsAware("pending") then goto continue end
local unitSquad = unit.Squad
unitSquad = unitSquad and gv_Squads[unitSquad]
if unitSquad and unitSquad.Retreat then goto continue end
if unitSquad and unitSquad.CurrentSector ~= gv_CurrentSectorId then goto continue end
local enemies = GetAllEnemyUnits(unit)
for _, enemy in ipairs(enemies) do
if not enemy:IsAware("pending") then goto continue end
local enemySquad = enemy.Squad
enemySquad = enemySquad and gv_Squads[enemySquad]
if enemySquad and enemySquad.Retreat then goto continue end
if enemySquad and enemySquad.CurrentSector ~= gv_CurrentSectorId then goto continue end
local unitTeam = unit.team
local enemyTeam = enemy.team
if unitTeam and enemyTeam then
local unitTeamSide, enemyTeamSide = unitTeam.side, enemyTeam.side
local playerPresent = unitTeamSide == "player1" or unitTeamSide == "player2" or enemyTeamSide == "player1" or enemyTeamSide == "player2"
local enemyPresent = unitTeamSide == "enemy1" or unitTeamSide == "enemy2" or enemyTeamSide == "enemy1" or enemyTeamSide == "enemy2"
if playerPresent and enemyPresent then
EnterConflict(nil, nil, nil, nil, nil, nil, nil, "from_map")
end
end
::continue::
end
::continue::
end
end
OnMsg.CombatStart = lTacticalModeCheckEnterConflict
OnMsg.UnitAwarenessChanged = lTacticalModeCheckEnterConflict
function SatelliteConflictAppliedOnSector(sector)
return gv_CurrentSectorId == (sector and sector.Id) and CanCloseSatelliteView()
end
-- auto-resolve, autoresolve, auto resolve
GameVar("gv_AutoResolveUseOrdnance", false)
function GetPowerOfUnit(unit, noMods)
if not unit then return 0 end
local power
if IsMerc(unit) then
power = const.AutoResolve.BaseMercPower * unit:GetLevel()
elseif unit.Squad and gv_Squads[unit.Squad].militia or unit.militia then
power = const.AutoResolve.BaseMilitiaPower * unit:GetLevel("baseLevel")
else -- enemy
power = const.AutoResolve.BaseEnemyPower * unit:GetLevel("baseLevel")
end
power = MulDivRound(power, unit:GetProperty("unitPowerModifier"), 100)
if noMods then
return power
end
-- in Percents
local modifier = 100
-- Health and Wounds mod (power reduced by direct proportion to the units HitPoints/MaxHitPoints)
local mod = MulDivRound(100, unit.HitPoints or unit.Health, unit:GetInitialMaxHitPoints()) - 100
modifier = modifier + mod
if IsMerc(unit) then
-- Status Effects mod
if unit:HasStatusEffect("Tired") then
modifier = modifier + const.AutoResolve.TiredMod
end
if unit:HasStatusEffect("Exhausted") then
modifier = modifier + const.AutoResolve.ExhaustedMod
end
if unit:HasStatusEffect("WellRested") then
modifier = modifier + const.AutoResolve.WellRestedMod
end
-- Armor mod
modifier = modifier + GetCombinedArmorPowerMod(unit)
-- Weapon mod
modifier = modifier + GetBestWeaponPowerMod(unit)
-- Ordnance mod
if gv_AutoResolveUseOrdnance and CanUseOrdnancePower(unit) then
modifier = modifier + const.AutoResolve.OrdnanceMod
end
end
power = MulDivRound(power, modifier, 100)
return power
end
-- Max mod per Armor piece <maxMod>%. Total 3*<maxMod>%.
-- Based on Direct Proportion of its Cost and <costCap>, also on its condition
function GetCombinedArmorPowerMod(unit)
local mod = 0
mod = mod + GetArmorPowerMod(unit:GetItemAtPos("Head", 1, 1))
mod = mod + GetArmorPowerMod(unit:GetItemAtPos("Torso", 1, 1))
mod = mod + GetArmorPowerMod(unit:GetItemAtPos("Legs", 1, 1))
return mod
end
function GetArmorPowerMod(armor)
if not armor then return 0 end
local maxMod = const.AutoResolve.MaxArmorMod -- Maximum +% mod per Armor piece
local costCap = const.AutoResolve.MaxArmorModCost
local mod = Min(maxMod, MulDivRound(maxMod, armor.Cost, costCap))
mod = MulDivRound(mod, armor.Condition, 100)
return mod
end
-- Max mod <maxMod>% from the best equiped Weapon.
-- Based on Direct Proportion of its Cost and <costCap>, also on its condition
function GetBestWeaponPowerMod(unit)
local items = unit:GetHandheldItems()
local mods = {}
for _, item in ipairs(items) do
mods[#mods+1] = GetWeaponPowerMod(unit, item)
end
table.sort(mods)
return mods[#mods] or 0
end
function GetWeaponPowerMod(unit, weapon)
if not weapon or not IsKindOfClasses(weapon, "Firearm", "MeleeWeapon") then return 0 end
if IsKindOf(weapon, "MeleeWeapon") and unit.Strength + Unit.Dexterity < const.AutoResolve.MeleeRequiredStats then return 0 end
if IsKindOf(weapon, "Firearm") and #unit:GetAvailableAmmos(weapon) < 1 then return 0 end -- doesn't have any ammo
local maxMod = const.AutoResolve.MaxWeaponMod -- Maximum +% mod a Weapon can give
local costCap = const.AutoResolve.MaxWeaponModCost
local mod = Min(maxMod, MulDivRound(maxMod, weapon.Cost, costCap))
mod = MulDivRound(mod, weapon.Condition, 100)
return mod
end
-- if has equiped Grenades or Heavy Weapon and Ordnance
function CanUseOrdnancePower(unit)
local items = unit:GetHandheldItems()
for _, item in ipairs(items) do
if IsKindOf(item, "Grenade") then
return true
elseif IsKindOf(item, "HeavyWeapon") and item.ammo and item.ammo.Amount > 0 then
return true
end
end
return false
end
-- 0% at 50 Leadership, 20% at 100 Leadership
function GetSideLeaderMod(units)
local mod = 0
local maxMod = const.AutoResolve.MaxLeaderMod
local minLeadership = const.AutoResolve.MinLeadershipRequired
local highestLeadership = 0
for _, unit in ipairs(units) do
if unit.Leadership > highestLeadership then
highestLeadership = unit.Leadership
end
end
local mod = Max(highestLeadership - minLeadership, 0)
mod = MulDivRound(maxMod, mod, minLeadership)
return mod
end
function GetSideMedicMod(units)
local mod = 0
local minMedical = const.AutoResolve.MinMedicalRequired
local medics = 0
for _, unit in ipairs(units) do
if unit.Medical >= minMedical and GetUnitEquippedMedicine(unit) then
medics = medics + 1
end
end
if medics == 0 then
mod = const.AutoResolve.NoMedicsMod
elseif medics == 1 then
mod = 0
else
mod = const.AutoResolve.EnoughMedicsMod
end
return mod
end
function GetSquadPower(squad)
local power = 0
if squad.units then
for _, id in ipairs(squad.units) do
local unit = gv_UnitData[id]
power = power + GetPowerOfUnit(unit)
end
end
return power
end
-- Excluding side modifiers
function GetMultipleSquadsPower(squads)
local power = 0
for _, squad in ipairs(squads) do
power = power + GetSquadPower(squad)
end
return power
end
-- Including side modifiers
function GetSectorPowersInConflict(sector, playerSquads, enemySquads, disableRandomMod)
-- Combined power of individual units
if not playerSquads or not enemySquads then
local playerSquads, enemySquads = GetSquadsInSector(sector.Id, "excludeTravelling", "includeMilitia", "excludeArriving")
end
local playerPower = GetMultipleSquadsPower(playerSquads)
local enemyPower = GetMultipleSquadsPower(enemySquads)
local playerUnits = GetUnitsFromSquads(playerSquads, "getUnitData")
local enemyUnits = GetUnitsFromSquads(enemySquads, "getUnitData")
local playerMod = 100 -- percent
local enemyMod = 100
local militiaOnlyTeam = true
for _, unit in ipairs(playerUnits) do
if IsMerc(unit) then
militiaOnlyTeam = false
break
end
end
if not militiaOnlyTeam then
-- Leader mod
playerMod = playerMod + GetSideLeaderMod(playerUnits)
-- Medic mod
playerMod = playerMod + GetSideMedicMod(playerUnits)
-- Numerical Advantage mod
if #playerUnits >= #enemyUnits * 2 then
playerMod = playerMod + const.AutoResolve.NumericalAdvantageMod
elseif #enemyUnits >= #playerUnits * 2 then
playerMod = playerMod - const.AutoResolve.NumericalAdvantageMod
end
end
-- Randomization mod: -<value>% to +<value>% (Applied to attacker)
if sector.conflict and not disableRandomMod then
local attackerMod = const.AutoResolve.AttackerRandomMod
attackerMod = InteractionRandRange(-attackerMod, attackerMod, "AutoResolve")
if sector.conflict.spawn_mode == "attack" then -- player is attacker
playerMod = playerMod + attackerMod
else
enemyMod = enemyMod + attackerMod
end
end
playerPower = MulDivRound(playerPower, playerMod, 100)
enemyPower = MulDivRound(enemyPower, enemyMod, 100)
-- Deffender bonus
if sector.conflict then
local bonus = sector.AutoResolveDefenderBonus
if sector.conflict.spawn_mode == "attack" then -- player is attacker
playerPower = playerPower + bonus
else
enemyPower = enemyPower + bonus
end
end
return playerPower, enemyPower, playerMod
end
function GetAutoResolveOutcome(sector, disableRandomMod)
local playerSquads, enemySquads = GetSquadsInSector(sector.Id, "excludeTravelling", "includeMilitia", "excludeArriving", "excludeRetreat")
local playerPower, enemyPower, playerMod = GetSectorPowersInConflict(sector, playerSquads, enemySquads, disableRandomMod)
-- Decide Outcome
if CheatEnabled("AutoResolve") then
return "decisive_win", playerPower, enemyPower, playerMod
end
if playerPower > 2*enemyPower then
return "decisive_win", playerPower, enemyPower, playerMod
elseif playerPower >= enemyPower then
return "win", playerPower, enemyPower, playerMod
elseif enemyPower > 2*playerPower then
return "crushing_defeat", playerPower, enemyPower, playerMod
else
return "defeat", playerPower, enemyPower, playerMod
end
end
MapVar("g_AccumulatedTeamXP", false)
function LogAccumulatedTeamXP(actor)
if g_AccumulatedTeamXP then
local log_msg
for _, unit in ipairs(table.keys(g_AccumulatedTeamXP, "sorted")) do
log_msg = log_msg or { T(280141508210, "Gained XP:") }
log_msg[#log_msg + 1] = T{547096297080, " <unit>(<em><gain></em>)", unit = unit, gain = g_AccumulatedTeamXP[unit] }
end
if next(log_msg) then CombatLog(actor, table.concat(log_msg)) end
end
g_AccumulatedTeamXP = false
end
function CalculateAutoResolveUnitDamage(unit, outcome, side)
local injuryChances = {
decisive_win = { seriousInjury = const.AutoResolveDamage.DecisiveWinSeriousInjuryChance, injury = const.AutoResolveDamage.DecisiveWinInjuryChance }, -- 5% Serious Injury chance, 30% Injury chance
win = { seriousInjury = const.AutoResolveDamage.WinSeriousInjuryChance, injury = const.AutoResolveDamage.WinInjuryChance }, -- 15% Serious Injury chance, 50% Injury chance
defeat = { seriousInjury = const.AutoResolveDamage.DefeatSeriousInjuryChance, injury = const.AutoResolveDamage.DefeatInjuryChance }, -- 10% Serious Injury chance, 90% Injury chance
crushing_defeat = { seriousInjury = const.AutoResolveDamage.CrushingDefeatSeriousInjuryChance, injury = const.AutoResolveDamage.CrushingDefeatInjuryChance}, -- 50% Serious Injury chance, 50% Injury chance
}
local militiaInjuryChanceMod = 0
--adjust base and random dmg values and chances based on difficulty
if side == "enemy" then
local bonus = (GameDifficulties[Game.game_difficulty]:ResolveValue("autoResolveInjuryChanceEnemyBonus") or 0)
injuryChances.decisive_win.injury = injuryChances.decisive_win.injury + bonus
injuryChances.win.seriousInjury = injuryChances.win.seriousInjury + bonus
injuryChances.win.injury = injuryChances.win.injury + bonus
injuryChances.defeat.seriousInjury = injuryChances.defeat.seriousInjury + bonus
injuryChances.crushing_defeat.seriousInjury = injuryChances.crushing_defeat.seriousInjury + bonus
elseif side == "militia" then
militiaInjuryChanceMod = const.AutoResolveDamage.MilitiaInjuryAdditiveMod
end
local percChangePerDiff = 100
if side == "enemy" then
--percChangePerDiff = PercentModifyByDifficulty(GameDifficulties[Game.game_difficulty]:ResolveValue("autoResolveEnemyDmgBonus"))
elseif side == "player" then
--percChangePerDiff = PercentModifyByDifficulty(GameDifficulties[Game.game_difficulty]:ResolveValue("autoResolvePlayerDmgBonus"))
end
local injuryDamage = MulDivRound(const.AutoResolveDamage.InjuryBaseDamage, percChangePerDiff, 100)
local injuryRandomDamage = MulDivRound(const.AutoResolveDamage.InjuryRandomDamage, percChangePerDiff, 100)
local seriousInjuryDamage = MulDivRound(const.AutoResolveDamage.SeriousInjuryBaseDamage, percChangePerDiff, 100)
local seriousInjuryRandomDamage = MulDivRound(const.AutoResolveDamage.SeriousInjuryRandomDamage, percChangePerDiff, 100) -- applies 2 times
local damage = 0
local injury = false
local injuryRoll = InteractionRand(100, "DamageOnAutoResolve") + 1
if injuryRoll <= (injuryChances[outcome].seriousInjury + militiaInjuryChanceMod) then -- Unit got SeriouslyInjured
damage = seriousInjuryDamage + InteractionRand(seriousInjuryRandomDamage, "DamageOnAutoResolve") + InteractionRand(seriousInjuryRandomDamage, "DamageOnAutoResolve")
injury = "seriousInjury"
elseif injuryRoll <= (injuryChances[outcome].injury + militiaInjuryChanceMod) then -- Unit got Injured
damage = injuryDamage + InteractionRand(injuryRandomDamage, "DamageOnAutoResolve")
injury = "injury"
end
return damage, injury
end
function AutoResolveUseMeds(playerSquads)
local bestMedic = false
local medkit = false
for _, squad in ipairs(playerSquads) do
for _, id in ipairs(squad.units) do
local unit = gv_UnitData[id]
local umedkit = GetUnitEquippedMedicine(unit)
if umedkit and (not bestMedic or bestMedic.Medical < unit.Medical) then
bestMedic = unit
medkit = umedkit
end
end
end
if bestMedic then
for _, squad in ipairs(playerSquads) do
for _, id in ipairs(squad.units) do
local unit = gv_UnitData[id]
unit:GetBandaged(medkit, bestMedic)
end
end
end
end
function AutoResolveUseAmmo(playerSquads, damageDone)
local damageToUseAmmo = const.AutoResolveResources.DamageToAmmo
local playerUnitsCount = CountUnitsInSquads(playerSquads)
local baseAmmoUsagePerUnit = DivRound(damageDone, damageToUseAmmo * playerUnitsCount)
local function TakeItemAmount(item, amount, container, slot)
local used = Min(item.Amount, amount)
item.Amount = item.Amount - used
if item.Amount <= 0 then
if slot then
container:RemoveItem(slot, item, "no_update")
elseif container then --presumably, squad bag which is an array..
table.remove_entry(container, item)
end
DoneObject(item)
end
return used
end
for _, squad in ipairs(playerSquads) do
for _, id in ipairs(squad.units) do
local unit = gv_UnitData[id]
local ammoRandMult = InteractionRand(50, "AutoResolveAmmo")
local ammoToUse = MulDivRound(baseAmmoUsagePerUnit, 100 + ammoRandMult, 100)
local handeldItems, handeldItemsSlots = unit:GetHandheldItems()
if gv_AutoResolveUseOrdnance then
local allowedOrdnance = 1 + InteractionRand(const.AutoResolveResources.MaxOrdnanceUsed, "AutoResolveAmmo")
local ordnanceToAmmoMult = 3
for i, item in ipairs(handeldItems) do
if ammoToUse <= 0 or allowedOrdnance <= 0 then break end
if IsKindOf(item, "Grenade") then
local used = TakeItemAmount(item, allowedOrdnance, unit, handeldItemsSlots[i])
allowedOrdnance = allowedOrdnance - used
ammoToUse = ammoToUse - used * ordnanceToAmmoMult
elseif IsKindOf(item, "HeavyWeapon") then
local degrade = -item:GetBaseDegradePerShot()
local ammos, containers, slots = unit:GetAvailableAmmos(item)
for j, ammo in ipairs(ammos) do -- Use from AmmoPack and UnitInventory
local used = TakeItemAmount(ammo, allowedOrdnance, containers[j], slots[j])
allowedOrdnance = allowedOrdnance - used
ammoToUse = ammoToUse - used * ordnanceToAmmoMult
unit:ItemModifyCondition(item, degrade * used)
end
if allowedOrdnance > 0 and ammoToUse > 0 and item.ammo then -- Use from Weapon itself
local used = TakeItemAmount(item.ammo, allowedOrdnance)
allowedOrdnance = allowedOrdnance - used
ammoToUse = ammoToUse - used * ordnanceToAmmoMult
unit:ItemModifyCondition(item, degrade * used)
end
end
end
end
for _, item in ipairs(handeldItems) do
if ammoToUse <= 0 then break end
if IsKindOf(item, "Firearm") then
local degrade = -item:GetBaseDegradePerShot()
local ammos, containers, slots = unit:GetAvailableAmmos(item)
for j, ammo in ipairs(ammos) do -- Use from AmmoPack and UnitInventory
local used = TakeItemAmount(ammo, ammoToUse, containers[j], slots[j])
ammoToUse = ammoToUse - used
unit:ItemModifyCondition(item, degrade * used)
end
if ammoToUse > 0 and item.ammo then -- Use from Weapon itself
local used = TakeItemAmount(item.ammo, ammoToUse)
ammoToUse = ammoToUse - used
unit:ItemModifyCondition(item, degrade * used)
end
end
end
end
end
end
function AutoResolveArmorDegradation(unit, injury)
if not unit or not injury then return end
local armorPieces = {}
armorPieces[#armorPieces+1] = unit:GetItemAtPos("Head", 1, 1)
armorPieces[#armorPieces+1] = unit:GetItemAtPos("Torso", 1, 1)
armorPieces[#armorPieces+1] = unit:GetItemAtPos("Legs", 1, 1)
local times = injury == "seriousInjury" and const.AutoResolveResources.ArmorDegradationTimesSeriousInjury or const.AutoResolveResources.ArmorDegradationTimesInjury
for i = 1, times do
if #armorPieces == 0 then return end
local idx = InteractionRand(#armorPieces, "AutoResolveArmor") + 1
local item = armorPieces[idx]
unit:ItemModifyCondition(item, -item.Degradation)
if item.Condition <= 0 then
table.remove(armorPieces, idx)
end
end
end
function ApplyAutoResolveOutcome(sector, playerOutcome)
local playerWins = IsOutcomeWin(playerOutcome)
local enemyOutcome = GetOppositeOutcome(playerOutcome)
local playerSquads, enemySquads = GetSquadsInSector(sector.Id, "excludeTravelling", not "includeMilitia", "excludeArriving", "excludeRetreating")
local items = {}
-- Militia outcome
local militiaSquads = GetMilitiaSquads(sector)
local militiaUnitsCount = CountUnitsInSquads(militiaSquads)
local militiaKilled = 0
for i = #militiaSquads, 1, -1 do
local squad = militiaSquads[i]
for j = #squad.units, 1, -1 do
local id = squad.units[j]
local unit = gv_UnitData[id]
local damage = 0
-- When the unit appears twice in a squad (due to another bug), this will error.
if not unit then goto continue end
if not playerWins then -- all militia die if the player loses;
damage = unit.HitPoints
else
local deathRoll = InteractionRand(100, "AutoResolve")
local deathChance = playerOutcome == "decisive_win" and const.AutoResolveDamage.NPCDeathChanceOnDecisiveWin or const.AutoResolveDamage.NPCDeathChanceOnWin
if deathRoll < deathChance then
damage = unit.HitPoints
else
damage = CalculateAutoResolveUnitDamage(unit, playerOutcome, "militia")
local militiaDamageMultiplier = const.AutoResolveDamage.MilitiaDamageTakenMod -- percent
damage = MulDivRound(damage, 100 + militiaDamageMultiplier, 100)
end
end
unit.HitPoints = Max(unit.HitPoints - damage, 0)
unit:AccumulateDamageTaken(damage)
if playerWins and #playerSquads <= 0 and (militiaKilled == militiaUnitsCount - 1) then -- let the last militia survive when defending with no mercs
unit.HitPoints = 1
end
if unit.HitPoints <= 0 then
militiaKilled = militiaKilled + 1
unit:Die()
Unit.DropLoot(unit)
if playerWins then
unit:ForEachItem(function(item, slot_name, left, top, items)
items[#items + 1] = item
unit:RemoveItem(slot_name, item)
end, items)
end
end
::continue::
end
end
-- Enemies outcome
g_AccumulatedTeamXP = {}
local totalDamageToEnemy = 0
for i = #enemySquads, 1, -1 do
for j = #enemySquads[i].units, 1, -1 do
local id = enemySquads[i].units[j]
local unit = gv_UnitData[id]
local damage = 0
if playerWins then -- all enemies die if the player wins;
damage = unit.HitPoints
else
local deathRoll = InteractionRand(100, "AutoResolve")
local deathChance = enemyOutcome == "decisive_win" and const.AutoResolveDamage.NPCDeathChanceOnDecisiveWin or const.AutoResolveDamage.NPCDeathChanceOnWin
if deathRoll < deathChance then
damage = unit.HitPoints
else
damage = CalculateAutoResolveUnitDamage(unit, enemyOutcome, "enemy")
end
end
unit.HitPoints = Max(unit.HitPoints - damage, 0)
--unit:AccumulateDamageTaken(damage)
if unit.villain and not playerWins then -- don't kill the villain
unit.HitPoints = 1
end
if unit.HitPoints <= 0 then
unit:Die()
end
totalDamageToEnemy = totalDamageToEnemy + damage
if playerWins then -- Generate loot
Unit.DropLoot(unit)
unit:ForEachItem(function(item, slot_name, left, top, items)
items[#items + 1] = item
unit:RemoveItem(slot_name, item)
end, items)
end
end
end
-- Player outcome
-- Consume Resources
if #playerSquads > 0 then -- Militia only autoresolve
AutoResolveUseAmmo(playerSquads, totalDamageToEnemy)
end
-- Damage Units
local playerUnitsCount = CountUnitsInSquads(playerSquads)
local mercsKilled = 0
for i = #playerSquads, 1, -1 do
local squad = playerSquads[i]
for j = #squad.units, 1, -1 do
local id = squad.units[j]
local unit = gv_UnitData[id]
local damage = 0
local injury
damage, injury = CalculateAutoResolveUnitDamage(unit, playerOutcome, "player")
if injury then
AutoResolveArmorDegradation(unit, injury)
end
if injury == "seriousInjury" and unit.Tiredness < 1 then
unit:SetTired(unit.Tiredness + 1)
end
unit.HitPoints = Max(unit.HitPoints - damage, 0)
unit:AccumulateDamageTaken(damage)
if playerWins and (mercsKilled == playerUnitsCount - 1) then -- let the last Merc survive
unit.HitPoints = 1
end
if unit.HitPoints <= 0 then
mercsKilled = mercsKilled + 1
unit:Die()
end
end
end
--AutoResolveUseMeds(playerSquads)
if #items > 0 then
SortItemsArray(items)
end
LogAccumulatedTeamXP("short")
return items
end
function IsOutcomeWin(outcome)
return outcome == "decisive_win" or outcome == "win"
end
function GetOppositeOutcome(outcome)
if outcome == "decisive_win" then return "crushing_defeat" end
if outcome == "win" then return "defeat" end
if outcome == "defeat" then return "win" end
if outcome == "crushing_defeat" then return "decisive_win" end
end
function NetEvents.CloseOtherGuysAutoResolveResultsUI()
local dlg = GetDialog("SatelliteConflict")
if dlg then
dlg:Close()
end
end
local function RecalcNames(sector, oldAllySquads)
for _, squad in ipairs(oldAllySquads) do
local squadUnits = table.copy(squad.units)
for id, unitId in ipairs(squadUnits) do
if not gv_UnitData[unitId] then
table.remove(squad.units, table.find(squad.units, unitId))
end
end
end
local allySquads = GetGroupedSquads(sector.Id, true, false, "no_retreating")
for i, s in ipairs(allySquads) do
for i, u in ipairs(s.units) do
local unitData = gv_UnitData[u]
local squad = table.find_value(oldAllySquads, "UniqueId", s.UniqueId)
if squad and not table.find(squad.units, unitData.session_id) then
table.insert(squad.units, unitData.session_id)
end
end
end
return oldAllySquads
end
function lCopySquadsBeforeAutoResolve(squadList)
local newList = {}
for i, s in ipairs(squadList) do
local copy = {
units = table.copy(s.units),
Name = s.Name,
CurrentSector = s.CurrentSector,
UniqueId = s.UniqueId,
image = s.image,
Retreat = s.Retreat,
militia = s.militia
}
newList[#newList + 1] = copy
end
return newList
end
function AutoResolveConflict(sector)
local player_outcome = GetAutoResolveOutcome(sector)
local player_wins = IsOutcomeWin(player_outcome)
--save the needed data for the auto-resolve screen
--todo: maybe GetAutoResolveOutcome should return squads
local allySquads = GetGroupedSquads(sector.Id, "includeMilitia", not "get_enemies", "no_retreating", "exclude_travelling")
local enemySquads = GetGroupedSquads(sector.Id, not "includeMilitia", "get_enemies", "no_retreating", "exclude_travelling")
enemySquads = enemySquads or {}
-- Auto resolve will cause units to be ejected from the squads,
-- so we need to copy the data.
allySquads = lCopySquadsBeforeAutoResolve(allySquads)
enemySquads = lCopySquadsBeforeAutoResolve(enemySquads)
local loot = ApplyAutoResolveOutcome(sector, player_outcome)
-- Sync to prevent ongoing combat hang if retreat->autoresolve due to militia
if sector.Id == gv_CurrentSectorId and not ForceReloadSectorMap then
LocalCheckUnitsMapPresence()
SyncUnitProperties("session")
end
-- todo: can this reuse the information from above once it is refactored to get it from GetOutcome?
local playerSquads = GetSquadsInSector(sector.Id, "excludeTravelling", not "includeMilitia", "excludeArriving", "excludeRetreating")
local first_alive_merc
for _, squad in ipairs(playerSquads) do
for _, id in ipairs(squad.units) do
local unit = gv_UnitData[id]
if unit.Squad and unit.HireStatus ~= "Dead" then
first_alive_merc = unit
break
end
end
if first_alive_merc then
break
end
end
local playerPower, enemyPower, playerMod = GetSectorPowersInConflict(sector, allySquads, enemySquads, "disableRandomMod")
allySquads.power = playerPower
allySquads.playerMod = playerMod
enemySquads.power = enemyPower
local items = table.copy(loot)
local sectorStash = GetSectorInventory(sector.Id)
assert(sectorStash)
if sectorStash then
AddItemsToInventory(sectorStash, items)
end
PauseCampaignTime("SatelliteConflictOutcome")
if player_wins then
sector.ForceConflict = false
ResolveConflict(sector, nil, "auto-resolve", nil)
elseif first_alive_merc then -- retreat mercs
SatelliteRetreat(sector.Id)
else -- no mercs left alive
ResolveConflict(sector, "no voice", "auto-resolve", nil)
ResumeCampaignTime("UI")
end
-- recalc squads to handle promoted militia
allySquads = RecalcNames(sector, allySquads)
OpenSatelliteConflictDlg(
{
player_outcome = player_outcome,
allySquads = allySquads,
enemySquads = enemySquads,
sector = sector,
loot = loot,
first_alive_merc = first_alive_merc,
autoResolve = true
})
ResumeCampaignTime("SatelliteConflictOutcome")
ObjModified("sector_selection_changed")
ObjModified("sector_selection_changed_actions")
Msg("AutoResolvedConflict", sector.Id, player_outcome)
end
function NetSyncEvents.UIAutoResolveConflict(sector_id, ordenance)
local sector = gv_Sectors[sector_id]
if not sector.conflict then return end
local old = gv_AutoResolveUseOrdnance
gv_AutoResolveUseOrdnance = ordenance --quick and dirty fix for autoresolve using ui button state, this will only work if there is no sleeps/threads n such
AutoResolveConflict(sector)
gv_AutoResolveUseOrdnance = old
local dlg = GetDialog("SatelliteConflict")
if dlg then
dlg:Close()
end
end
function TFormat.AutoResolveOutcomeText(context_obj, status)
if status == "decisive_win" then
return T(907277131281, "DECISIVE WIN")
elseif status == "win" then
return T(561227589007, "VICTORY")
elseif status == "defeat" then
return T(979864159307, "DEFEAT")
elseif status == "crushing_defeat" then
return T(912438837574, "CRUSHING DEFEAT")
end
end
function RollForMilitiaPromotion(sector)
local squads = GetMilitiaSquads(sector)
local promotedCount = 0
for _, squad in ipairs(squads) do
local unitIds = table.copy(squad.units)
for _, id in ipairs(unitIds) do
--local unit = g_Units[id]
local unitData = gv_UnitData[id]
local chance = 30
local roll = InteractionRand(100, "MilitiaPromotion")
if roll < chance then
if unitData.class == "MilitiaRookie" then
CreateMilitiaUnitData("MilitiaVeteran", sector, squad)
DeleteMilitiaUnitData(unitData.session_id, squad)
promotedCount = promotedCount + 1
elseif unitData.class == "MilitiaVeteran" then
CreateMilitiaUnitData("MilitiaElite", sector, squad)
DeleteMilitiaUnitData(unitData.session_id, squad)
promotedCount = promotedCount + 1
end
end
end
end
if promotedCount > 0 then
if promotedCount > 1 then
CombatLog("important", T{293615811082, "<promotedCount> militia got promoted in <SectorName(sectorId)>", promotedCount = promotedCount, sectorId = sector.Id})
else
CombatLog("important", T{488327770041, "A militia unit got promoted in <SectorName(sectorId)>", promotedCount = promotedCount, sectorId = sector.Id})
end
end
end
-- remove enemy units/squads from a sector
-- value is an signed number, denoting whether and how many units to add or remove
-- valueType is a string, can either "count" which means value is a specific count or "percent"
function ModifySectorEnemySquads(sector_id, value, valueType, class)
if value == 0 then
return
end
valueType = valueType or "percent"
local squads = {}
local mercs, mercsPerSquad = {}, {}
local enemySquads = GetSectorSquadsFromSide(sector_id, "enemy1", "enemy2")
for i, squad in ipairs(enemySquads) do
if not squad.villain and not squad.guardpost then
for _, merc in ipairs(squad.units) do
local unit = gv_UnitData[merc]
if (not class and not unit.villain and not HasAnyShipmentItem(unit)) or unit.class == class then
mercs[#mercs+1] = merc
if not mercsPerSquad[squad] then mercsPerSquad[squad] = {} end
mercsPerSquad[squad][#mercsPerSquad + 1] = merc
end
end
if EnemySquadDefs[squad.enemy_squad_def] then
squads[#squads + 1] = squad
end
end
end
if value < 0 then -- Remove units
value = abs(value)
local removeAll = (valueType == "percent" and value == 100) or (valueType == "count" and value == #mercs)
if removeAll then
for _, merc in ipairs(mercs) do
RemoveUnitFromSquad(gv_UnitData[merc], "despawn")
end
else
local count = valueType == "count" and value or MulDivRound(value, #mercs, 100)
count = Max(count, 1)
for i = 1, Min(count, #mercs) do
local merc, idx = table.interaction_rand(mercs, "SectorEnemySquads")
table.remove(mercs, idx)
RemoveUnitFromSquad(gv_UnitData[merc], "despawn")
end
end
else -- Add units
for _, squad in ipairs(squads) do
-- Note: If the value is a percent it is relative to the current number of units of the same type in the sector.
local count = valueType == "count" and value or MulDivRound(value, #mercsPerSquad[squad], 100)
count = Max(count, 1)
local units_to_create = {}
if class then
for i = 1, count do
units_to_create[i] = class
end
else
while count > 0 do
local unit_template_ids = GenerateRandEnemySquadUnits(squad.enemy_squad_def)
if #unit_template_ids == 0 then
assert(false, "We risk infinite loop here, something is wrong with the enemy squad definition. Not all enemy units will be created from this ModifySectorEnemySquads effect.")
break
end
local all = #unit_template_ids
for i = 1, all do
local id, idx = table.interaction_rand(unit_template_ids, "SectorEnemySquads")
units_to_create[#units_to_create + 1] = id
table.remove(unit_template_ids, idx)
count = count - 1
if count == 0 then
break
end
end
end
end
local new_units = GenerateUnitsFromTemplates(squad.CurrentSector, units_to_create, "ModifyEffect")
AddUnitsToSquad(squad, new_units)
end
end
if not gv_SatelliteView and sector_id == gv_CurrentSectorId then
LocalCheckUnitsMapPresence()
end
end
DefineClass.SatelliteConflictUIMercsDisplay = {
__parents = { "XContextWindow" },
GridWidth = 2,
LayoutMethod = "Vlist",
LayoutVSpacing = 10,
MinHeight = 330,
MaxHeight = 450,
properties = {
{ category = "MercDisplay", id = "headerText", name = "Header Name", editor = "text", default = "", translate = true },
{ category = "MercDisplay", id = "subheaderText", name = "Subheader", editor = "text", default = "", translate = true },
{ category = "MercDisplay", id = "showEnroute", name = "ShowEnroute", editor = "bool", default = "" },
{ category = "MercDisplay", id = "align", name = "Align", editor = "choice", items = { "left", "right" }, default = "left" },
}
}
function SatelliteConflictUIMercsDisplay:Open()
self.idHeaderTitle:SetText(self.headerText)
self.idSubHeaderText:SetText(self.subheaderText)
XContextWindow.Open(self)
if self.align == "right" then
self.idHeader:SetHAlign("left")
self.idHeaderTitle:SetHAlign("left")
self.idHeaderLine:SetHAlign("left")
self.idSubHeader:SetHAlign("left")
local margins = self.idHeader.Margins
self.idHeader:SetMargins(box(margins:maxx(), margins:miny(), margins:minx(), margins:maxy()))
local paddings = self.idMercs.Padding
self.idMercs:SetPadding(box(paddings:maxx(), paddings:miny(), paddings:minx(), paddings:maxy()))
for i, s in ipairs(self.idMercs) do
s:SetHAlign("left")
if rawget(s, "idMercContainer") then
s.idMercContainer:SetHAlign("left")
end
s:SetMargins(box(20, 0, 0, 0))
end
end
end
function SatelliteConflictUIMercsDisplay:GetMercUnitData(context)
if context.UniqueId then
return GetMercArrayUnitData(context.units) or {}
else
if not context.units or #context.units == 0 then
return false
end
return table.imap(context.units, function(o)
local propObj = ResolvePropObj(o) or o
if IsKindOf(propObj, "UnitData") then return o end
return SubContext(o.template, o)
end)
end
end
function SatelliteConflictUIMercsDisplay:SplitMercsIntoSquads(context)
if not context.units then return {context} end
local maxPeopleInSquad = const.Satellite.MercSquadMaxPeople
local squadCount = #context.units > maxPeopleInSquad and MulDivRound(#context.units, 1000, maxPeopleInSquad * 1000) or 1
local squads = {}
for i = 0, squadCount - 1 do
local units = {}
local startIdx = (maxPeopleInSquad * i) + 1
for m = startIdx, Min(startIdx + maxPeopleInSquad - 1, #context.units) do
units[#units + 1] = context.units[m]
end
squads[i + 1] = {
units = units
}
end
return squads
end
GameVar("LostLoyaltyWithSectorsThisTick", false) -- Used to prevent double penalties
function OnMsg.SatelliteTick()
LostLoyaltyWithSectorsThisTick = false
end
function GetLoyaltyCityNearby(sector, filter)
if filter == "center_only" then
local s = gv_Sectors[sector.Id]
local city = s and s.City
if city and city ~= "none" then
return city
else
return
end
end
local city = gv_Sectors[sector.Id].City
if not filter and city and city ~= "none" then
return city
end
city = nil
ForEachSectorAround(sector.Id, 1, function(sector_id)
if filter == "adjacent_only" and sector_id == sector.Id then return end
-- If any of the adjacent sectors is part of a city
local s = gv_Sectors[sector_id]
if s and s.City and s.City ~= "none" then
city = s.City
return "break"
end
end)
return city
end
function OnMsg.SectorSideChanged(sector_id, oldSide, newSide)
-- Lost control of a sector that belongs to a city
if oldSide == "player1" and newSide == "enemy1" then
if LostLoyaltyWithSectorsThisTick and LostLoyaltyWithSectorsThisTick[sector_id] then return end
local sector = gv_Sectors[sector_id]
if sector.conflict then return end -- Conflict End will handle loyalty change
local city = GetLoyaltyCityNearby(sector, "center_only")
CityModifyLoyalty(city, const.Loyalty.CitySectorEnemyTakeOverLoyaltyLoss, T(171133072609, "City Lost"))
end
end
function CivilianDeathPenalty()
local penaltyAmount = const.Loyalty.CivilianDeathPenalty
local penaltyCap = const.Loyalty.CivilianDeathPenaltyCityCap
local currentSector = gv_Sectors[gv_CurrentSectorId]
local cityId = currentSector and GetLoyaltyCityNearby(currentSector)
local city = gv_Cities[cityId]
if not city then return end
if city.currentCivilianDeathPenalty + penaltyAmount > penaltyCap then penaltyAmount = penaltyCap - city.currentCivilianDeathPenalty end
if city.Loyalty - penaltyAmount < 0 then penaltyAmount = city.Loyalty end
local oldLoyalty = city.Loyalty
CityModifyLoyalty(cityId, -penaltyAmount, T(938505306538, "Civilian death penalty"))
if oldLoyalty > city.Loyalty then
city.currentCivilianDeathPenalty = city.currentCivilianDeathPenalty + penaltyAmount
end
end
function OnMsg.SectorSideChanged(sector_id, oldSide, newSide)
-- reset conflictLoyaltyGained
if oldSide == "player1" and newSide == "enemy1" then
local sector = gv_Sectors[sector_id]
sector.conflictLoyaltyGained = false
end
end
function OnMsg.ConflictEnd(sector, _, playerAttacked, playerWon, autoResolve, isRetreat, startedFromMap)
-- If you win in a conflict in a sector adjacent to a city sector that you own
-- you get loyalty for that city.
local allySquads = GetGroupedSquads(sector.Id, true, false, "no_retreating", "non_travelling")
if playerWon then
if sector.RunLoyaltyLogic then
if (not playerAttacked and not startedFromMap) or not sector.conflictLoyaltyGained then
local city = GetLoyaltyCityNearby(sector)
assert(allySquads and #allySquads > 0)
local nonMilitiaSquad = false
for i, sq in ipairs(allySquads) do
if not sq.militia then
nonMilitiaSquad = true
end
end
-- Militia won alone!
if not nonMilitiaSquad then
assert(autoResolve)
CityModifyLoyalty(city, const.Loyalty.ConflictMilitiaOnlyWinBonus, T(469271409848, "Enemies cleared by militia"))
sector.conflictLoyaltyGained = true
else
CityModifyLoyalty(city, const.Loyalty.ConflictWinBonus, T(133483288436, "Enemies cleared"))
sector.conflictLoyaltyGained = true
end
end
--world flip that has caused disable autoresolve should be cleared
sector.autoresolve_disabled = false
end
-- Cancel units that have retreated, now that we've won. (219850)
for i, squad in ipairs(allySquads) do
for i, uId in ipairs(squad.units) do
local ud = gv_UnitData[uId]
if ud and ud.retreat_to_sector then
CancelUnitRetreat(ud)
end
end
end
LocalCheckUnitsMapPresence()
-- If you retreat (this also happens when losing an auto resolve)
elseif isRetreat and sector.RunLoyaltyLogic then
local city = GetLoyaltyCityNearby(sector)
CityModifyLoyalty(city, const.Loyalty.ConflictRetreatPenalty, T(186425120178, "Retreat"))
if not LostLoyaltyWithSectorsThisTick then LostLoyaltyWithSectorsThisTick = {} end
LostLoyaltyWithSectorsThisTick[sector.Id] = true
-- If you get defeated (regardless of whether militia got defeated or mercs)
elseif not playerWon and (not allySquads or #allySquads == 0) and sector.RunLoyaltyLogic then
local city = GetLoyaltyCityNearby(sector, "adjacent_only")
CityModifyLoyalty(city, const.Loyalty.ConflictDefeatedLoyaltyLoss, T(703208874704, "Defeat"))
end
end
function OpenSatelliteConflictDlg(context, openedBy)
CreateRealTimeThread(function()
WaitPlayingSetpiece()
local satCon = GetDialog("SatelliteConflict")
if satCon then
-- This shouldn't happen, but better to handle it. Reopen the dialog to ensure we have the latest.
-- One way this can happen is when triggering a conflict from a shortcut exit
-- since reaching the sector and the sector center will happen back to back.
-- Another way this happens is when going out of an underground sector into an overground conflict through sat view.
if satCon.context.Id == context.Id then
print("double conflict", context.Id)
satCon:Close()
end
WaitMsg(satCon)
end
local popupHost = GetDialog("PDADialogSatellite")
popupHost = popupHost and popupHost:ResolveId("idDisplayPopupHost")
OpenDialog("SatelliteConflict", popupHost or GetInGameInterface(), context)
-- Play the cool sound only when the conflict is
-- initiated by the code, and not when it is opened by UI.
if openedBy == "auto-open" then
PlayFX("ConflictPanelOpen")
else
PlayFX("ConflictPanelOpenByPlayer")
end
end)
end
function OnMsg.UnitDieStart(unit, attacker)
if unit:IsCivilian() and unit.Affiliation == "Civilian" and attacker then
local attackerSide = IsKindOf(attacker, "DynamicSpawnLandmine") and attacker.team_side or attacker.team.side
local playerSide = NetPlayerSide()
local attackerIsPlayer = attackerSide == playerSide
local attackerIsPlayerEnemy = SideIsEnemy(playerSide, attackerSide)
if attackerIsPlayer or attackerIsPlayerEnemy then
CivilianDeathPenalty()
end
end
end
GameVar("gv_CiviliansKilled", 0)
function OnMsg.OnKill(attacker, killedUnits)
if IsMerc(attacker) then
for _, unit in ipairs(killedUnits) do
if unit:IsCivilian() and not unit.immortal then
gv_CiviliansKilled = gv_CiviliansKilled + 1
end
end
end
end
function DespawnUnitData(sectorId, class, despawnUnitToo)
local found = table.filter(gv_UnitData, function(i, o)
local squad = o.Squad
squad = squad and gv_Squads[squad]
local sectorFilter = squad and squad.CurrentSector == sectorId
if not squad then
sectorFilter = g_Units[o.session_id] and gv_CurrentSectorId == sectorId
end
return o.class == class and sectorFilter
end)
local firstIdx = found and next(found)
if not firstIdx then return end
RemoveUnitFromSquad(found[firstIdx], despawnUnitToo and "despawn")
if despawnUnitToo then
LocalCheckUnitsMapPresence()
end
end
function IsAutoResolveEnabled(sector)
if sector.never_autoresolve then
return false
end
if not sector.conflict then
return false
end
if not sector.Map then
return true
end
-- These are the squads that would be part of the conflict. (SatelliteConflict.lua)
-- Auto resolve is always enabled if only militia squads will be part of the conflict.
local alliesInConflict, enemySquads = GetSquadsInSector(sector.Id, "excludeTravelling", "includeMilitia", "excludeArriving")
if not alliesInConflict or #alliesInConflict == 0 then
return false
end
local onlyMilitia = true
for i, s in ipairs(alliesInConflict) do
if not s.militia then
onlyMilitia = false
break
end
end
if onlyMilitia then
return true
end
-- Losing auto resolve will retreat the squad
-- so we allow auto resolve only when at least of the squads
-- has a sector to retreat to (then all squads which dont have
-- a valid retreat sector will inherit it from them in SatelliteRetreat)
local anyHavePreviousSector = false
for i, squad in ipairs(alliesInConflict) do
anyHavePreviousSector = not squad.militia and squad.PreviousSector
-- How?!?
anyHavePreviousSector = anyHavePreviousSector and squad.PreviousSector ~= sector.Id
if anyHavePreviousSector then break end
end
if not anyHavePreviousSector then
return false
end
if sector.autoresolve_disabled then
return false
end
if not enemySquads or #enemySquads == 0 then
return false
end
return CanGoInMap(sector.Id) and not sector.ForceConflict
end
function OnMsg.ConflictEnd(sector)
-- Check if player won
if sector.Side ~= "player1" then return end
-- Check if militia present
local militia_squad_id = sector.militia_squad_id
local militia_squad = gv_Squads[militia_squad_id]
if not militia_squad or #(militia_squad.units or "") == 0 then return end
local quest = gv_Quests["05_TakeDownMajor"] and QuestGetState("05_TakeDownMajor")
if not quest then return end
SetQuestVar(quest, "LegionBeatenByMilitia", true)
end
function GetSatelliteConflictWarnings(squads)
local woundedCount, tiredCount = 0, 0
for _, squad in ipairs(squads) do
for _, id in ipairs(squad.units) do
local unit = gv_UnitData[id]
if unit.Tiredness >= 1 then
tiredCount = tiredCount + 1
end
if unit.HitPoints < MulDivRound(unit:GetInitialMaxHitPoints(), 50, 100) then
woundedCount = woundedCount + 1
end
end
end
return woundedCount, tiredCount
end
function OnMsg.StartSatelliteGameplay()
if ZuluAppliedSessionDataFixups.RemoveInvalidConflicts and not
ZuluAppliedSessionDataFixups.RemoveInvalidConflicts_2 then
if CampaignPauseReasons.SatelliteConflict and not AnyNonWaitingConflict() then
ResumeCampaignTime("SatelliteConflict")
end
ZuluAppliedSessionDataFixups.RemoveInvalidConflicts_2 = true
end
end
function SavegameSessionDataFixups.RemoveInvalidConflicts(data)
-- Manually get squads in the sector as the data is not filled in yet at this point.
-- Uses same logic as AddSquadToSectorList
local function lSquadsInSector(sector)
local ally, enemy, militia = {}, {}, {}
local squads = GetGameVarFromSession(data, "gv_Squads")
for _, squad in sorted_pairs(squads) do
if squad.CurrentSector == sector.Id then
if (squad.Side == "player1" or squad.Side == "ally") then
if not squad.militia then
ally[#ally + 1] = squad
else
militia[#militia + 1] = squad
end
else
enemy[#enemy + 1] = squad
end
end
end
return ally, enemy, militia
end
local sectors = GetGameVarFromSession(data, "gv_Sectors")
local anyConflict
for id, sector in pairs(sectors) do
if sector.conflict then
local ally, enemy, militia = lSquadsInSector(sector)
if not next(ally) and (not next(militia) or not next(enemy)) then
sector.conflict = false
else
anyConflict = true
end
end
end
if not anyConflict then
data.game.PersistableCampaignPauseReasons["SatelliteConflict"] = nil
end
end