File size: 9,497 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
if FirstLoad then
	g_Units = {}
	g_Teams = {}
	g_CurrentTeam = false
	g_CurrentSquad = false
end

function ResetZuluStateGlobals()
	local igi = GetInGameInterface()
	if igi then igi:Close() end
	
	g_Units = {}
	g_Teams = {}
	g_CurrentTeam = false
	g_CurrentSquad = false
	Msg("CurrentSquadChanged")
end

OnMsg.ChangeMap = ResetZuluStateGlobals
OnMsg.NewGame = ResetZuluStateGlobals
OnMsg.NewGameSessionStart = ResetZuluStateGlobals

if FirstLoad then
	SideDefs = false
	Sides = false
end

DefineClass.CampaignSide = {
	__parents = { "PropertyObject", },
	properties = {
		{ id = "Id", editor = "text", default = false, },
		{ id = "DisplayName", name = "Display Name", editor = "text", default = false, translate = true, },
		{ id = "Player", editor = "bool", default = false, },
		{ id = "Enemy", editor = "bool", default = false, },
	},
}

function OnMsg.ClassesBuilt()
	SideDefs = {
		CampaignSide:new{ Id = "neutral",      DisplayName = T(521973101724, "Neutral") },
		CampaignSide:new{ Id = "player1",      DisplayName = T(222892508302, "Player 1"), Player = true },
		CampaignSide:new{ Id = "player2",      DisplayName = T(942355779355, "Player 2"), Player = true },
		CampaignSide:new{ Id = "enemy1",       DisplayName = T(692913892455, "Enemy 1"), Enemy = true },
		CampaignSide:new{ Id = "enemy2",       DisplayName = T(456135028453, "Enemy 2"), Enemy = true },
		CampaignSide:new{ Id = "enemyNeutral", DisplayName = T(607169506860, "Enemy Neutral"), },
		CampaignSide:new{ Id = "ally",         DisplayName = T(346100175449, "Ally"), },
	}
	Sides = table.map(SideDefs, "Id")
end

function SetupDummyTeams()
	if not g_Teams then g_Teams = {} end

	local side_to_team = {}
	for _, side in ipairs(SideDefs) do
		local team = table.find_value(g_Teams, "side", side.Id)
		if not team then
			team = CombatTeam:new {
				side = side.Id,
				control = (side.Id == "player1" or side.Id == "player2") and "UI" or "AI",
				team_color = (side.Id == "player1" or side.Id == "player2") and RGB(0, 0, 200) or RGB(200, 0, 0),
			}
			g_Teams[#g_Teams+1] = team
		end
		team.units = {}
		side_to_team[team.side] = team
	end
	
	return side_to_team
end

local function filter_unit(u)
	return IsValid(u) and (not u.team or #u.team.units == 0 or not u.team:IsDefeated()) 
end

function SetupTeamsFromMap(reset_teams)
	local units = MapGet("map", "Unit", filter_unit) or {}
	local detached_units = MapGet("detached", "Unit", filter_unit) or {}
	g_Units = table.union(units, detached_units)
	
	-- Create a team for all campaign sides - you don't know who's gonna show up :)
	local side_to_team = SetupDummyTeams()
	
	SuppressTeamUpdate = true
	for _, unit in ipairs(units) do
		if CheckUniqueSessionId(unit) then
			g_Units[unit.session_id] = unit
		end
		local side = unit:GetSide(reset_teams)
		local team = side_to_team[side]
		table.insert_unique(team.units, unit)
		unit:SetTeam(team)
	end
	for _, unit in ipairs(g_Units) do
		if not unit.team then
			unit:SetTeam(side_to_team.neutral)
		end
	end
	
	-- for player teams only: check for wounded units and start on -1 morale if there are any
	for _, team in ipairs(g_Teams) do
		if team.player_team then
			local wounded
			for _, unit in ipairs(team.units) do
				wounded = wounded or unit:HasStatusEffect("Wounded")
			end
			if wounded then 
				team.morale = -1
			end
		end
	end
	
	-- Is this ever used? Seems like no.
	for i, u in ipairs(g_Units) do
		assert(not u.Group)
	end
	
	SuppressTeamUpdate = false
	Msg("TeamsUpdated")
end

function SendUnitToTeam(unit, team)
	local hasTeam = not not unit.team

	assert(team)
	if hasTeam then
		table.remove_value(unit.team.units, unit)
	end	
	table.insert(team.units, unit)
	unit:SetTeam(team)
	AddToGlobalUnits(unit)	
	Msg("UnitSideChanged", unit, team)
end

function OnMsg.ChangeMapDone(map)
	if map ~= "" and mapdata.GameLogic then
		g_Units = MapGet("map", "Unit") or {}
	end
end

function OnMsg.CloseSatelliteView()
	EnsureCurrentSquad()
	ObjModified("hud_squads")
	
	local team = GetCurrentTeam()
	if team then
		for i, u in ipairs(team.units or empty_table) do
			ObjModified(u)
		end
	end
end

-- 1. Ensures that there is a currently selected unit
-- 2. Ensures that the currently selected squad is correct (squad of selected units)

-- Cases in which there might not be a selected unit:
-- 1. Selected units left the sector
-- 2. Squad was destroyed
function EnsureCurrentSquad()
	if #(Selection or "") == 0 then
		local squadsOnMap, team = GetSquadsOnMap()
		local selectedSquadIdx = table.find(squadsOnMap, g_CurrentSquad)
		
		if not selectedSquadIdx and team then
			ResetCurrentSquad(team)
			selectedSquadIdx = table.find(squadsOnMap, g_CurrentSquad)
		end
		
		if selectedSquadIdx then -- first try to select a unit from the current squad
			for _, unit in ipairs(g_Units) do
				if unit.Squad == g_CurrentSquad and not unit:IsDead() and not unit:IsDowned() and unit:IsLocalPlayerControlled() then
					SuppressNextSelectionChangeVR = true
					DelayedCall(0, SelectObj, unit) -- this will trigger selection logic which will bring us back here
					return
				end
			end
		end
	else	
		-- check if current selection is fine first (only unit selection changed or w/e)
		local allFromSelectedSquad = true
		for i, u in ipairs(Selection) do
			if u.Squad ~= g_CurrentSquad then
				allFromSelectedSquad = false
				break
			end
		end
		if allFromSelectedSquad then return end
		
		-- Set g_CurrentSquad to be the squad with the most units selected.
		local unitsPerSquad = {}
		for i, u in ipairs(Selection) do
			local squad = u:GetSatelliteSquad()
			if squad then
				unitsPerSquad[squad.UniqueId] = (unitsPerSquad[squad.UniqueId] or 0) + 1
			end
		end
	
		local maxCount, maxCountId = 0, 0
		for sqId, unitCount in pairs(unitsPerSquad) do
			if unitCount > maxCount then
				maxCount = unitCount
				maxCountId = sqId
			end
		end
		
		if g_CurrentSquad == maxCountId then return end
		
		g_CurrentSquad = maxCountId
		Msg("CurrentSquadChanged")
	end
end

function ResetCurrentSquad(currentTeam)
	local firstUnit
	if Selection and #Selection > 0 then
		firstUnit = Selection[1]
	else
		firstUnit = currentTeam.units[1]
	end
	local squad = firstUnit and firstUnit:GetSatelliteSquad()
	g_CurrentSquad = squad and squad.UniqueId or false
	Msg("CurrentSquadChanged")
	return firstUnit
end

function CheckUniqueSessionId(unit)
	local session_id = unit.session_id
	local same_id_unit = g_Units[session_id] and g_Units[session_id] ~= unit
	if same_id_unit then
		assert(false, string.format("Two units with the same session_id %s?", session_id))
		return false
	end
	return true
end

function AddToGlobalUnits(unit)
	if not CheckUniqueSessionId(unit) then
		return
	end
	table.insert_unique(g_Units, unit)
	g_Units[unit.session_id] = unit
end

function GetAllPlayerUnitsOnMap()
	local team = table.find_value(g_Teams, "side", "player1")
	return team and team.units
end

function GetAllPlayerUnitsOnMapSessionId()
	local units = GetAllPlayerUnitsOnMap()
	return table.map(units, "session_id")
end

function GetCurrentTeam()
	return GetPoVTeam()
end

function GetPoVTeam()
	if g_Combat then
		-- The current team can be an enemy team, return the first allied team instead.
		local active_team = g_Teams[g_CurrentTeam or 1]
		if active_team and (active_team.control ~= "UI" or not active_team.player_ally) then
			for _, team in ipairs(g_Teams) do
				if team.control == "UI" and team.player_ally then
					return team
				end
			end
		end
		return active_team
	end
	
	if not Selection or #Selection == 0 then
		for _, team in ipairs(g_Teams) do
			if team.side == "player1" then
				return team
			end
		end
	elseif IsKindOf(Selection[1], "Unit") then
		return Selection[1].team
	end
end



function WholeTeamSelected()
	if g_Combat then return false end -- No multiselect
	local team = GetFilteredCurrentTeam()
	
	local unitsCanControl = {}
	for i, u in ipairs(team and team.units) do
		if u:IsLocalPlayerControlled() then
			unitsCanControl[#unitsCanControl + 1] = u
		end
	end
	
	if #unitsCanControl ~= #Selection then return false end
	for i, s in ipairs(Selection) do
		if not table.find(unitsCanControl, s) then
			return false
		end
	end
	return true
end

function GetFilteredCurrentTeam(team)
	team = team or GetCurrentTeam()

	-- If there is a current squad filter units from it only.
	if team and team.units and g_CurrentSquad then
		local teamFiltered = {
			DisplayName = false,
			side = false,
			control = team.control
		}
		
		local units = {}
		local squad = gv_Squads[g_CurrentSquad]
		if not squad then 
			ResetCurrentSquad(team)
			squad = gv_Squads[g_CurrentSquad]
			if not squad then return team end
		end
		
		teamFiltered.DisplayName = Untranslated(squad.Name)
		teamFiltered.side = squad.Side

		for i, u in ipairs(squad.units) do
			local unit = g_Units[u]
			if IsValid(unit) then --and not unit:IsDead() then
				units[#units + 1] = unit
			end
		end
		
		if #units == 0 then
			ResetCurrentSquad(team)
			return team
		end
	
		teamFiltered.units = units
		team = teamFiltered
	end
	
	return team
end

function GetMapUnitsInSquad(squadId)
	local squad = gv_Squads[squadId]
	if not squad then return {} end
	
	local units = {}
	for i, u in ipairs(squad.units) do
		local unitOnMap = g_Units[u]
		if unitOnMap then
			units[#units + 1] = unitOnMap
		end
	end
	return units
end

function GetCampaignPlayerTeam()
	if IsHotSeatGame() or IsCompetitiveGame() then return end

	for i, team in ipairs(g_Teams) do
		if team.side == "player1" then
			return team
		end
	end
end