
script_version_num=5

ticks_per_sec = 25
round_num = 0 -- current round number
live_players = {false,false,false,false} -- mask of players who are still playing the current round
player_scores = {0,0,0,0}
island_centre = CPos.New(38,38) -- not necessarily the exact centre of the map

-- For each player: {waypoint at hovercraft spawn location, {list of waypoints to be used as landing locations}}
hovercraft_waypoints={{WayNW,{WayLZ4,WayLZ2,WayLZ18,WayLZ3,WayLZ16}},
{WayNE,{WayLZ6,WayLZ0,WayLZ19,WayLZ7,WayLZ5}},
{WaySE,{WayLZ9,WayLZ8,WayLZ20,WayLZ10,WayLZ14}},
{WaySW,{WayLZ15,WayLZ12,WayLZ1,WayLZ11,WayLZ13}}}

-- initialized in WorldLoaded
-- Can be used to get a player from his index (1,2,3,4)
-- Also maps the index back to the player
-- Inactive players are nil
PlayerLookup={}
PlayerNicks={} -- maps an index or a player to a nickname
NeutralPlayer=nil

-- If nothing happens for a while then a round ends automatically (see: bot vs bot)
-- These values must be integers (or ==0 comparison fails and there is no timeout)
round_end_timeout_initial=8 -- Specified as dekaseconds
round_end_timeout_danger=3 -- Warnings start when timer goes below this value
round_end_timeout=-1 -- Timeout counter. Negative value disables timeout

-- Concatenates 2 tables (and removes gaps)
Concat = function(a,b)
	local c={}
	for k,v in ipairs(a) do c[#c+1]=v end
	for k,v in ipairs(b) do c[#c+1]=v end
	return c
end
-- Returns true if (string) s ends with e
EndsWith = function(s,e)
	return #s >= #e and string.sub(s,1+#s-#e) == e
end

-- Spam DebugBeacons everywhere should there be a cryptic exception without any information
DebugBeacon = function(where)
	if false then print(".. "..where) end
end

-- Some wrapper functions should the lua API change
DoLater = function(seconds,func) Trigger.AfterDelay(ticks_per_sec*seconds,func) end
Rand = function(min,max) return Utils.RandomInteger(min,max) end
AfterEveryoneIsDead = function(everyone,callb) return Trigger.OnAllKilled(everyone,callb) end
NewUnit = function(actor_class,add_to_world,attribs) return Actor.Create(actor_class,add_to_world,attribs) end
ChatMsg = function(msg) print(msg); Media.DisplayMessage(msg) end
ObtainPlayer = function(s) return Player.GetPlayer(s) end

NearestCornerPos = function(pos)
	local nw=WayNW.Location
	local se=WaySE.Location
	local x=se.X
	local y=se.Y
	if pos.X < island_centre.X then x=nw.X end
	if pos.Y < island_centre.Y then y=nw.Y end
	return CPos.New(x,y)
end

-- Each player gets an invisible actor that provides the airstrike power (AIRSTENABLE in map.yaml)
airstrike_enablers={nil,nil,nil,nil}

AirstrikeOn = function(i)
	local pl=PlayerLookup[i]
	if pl and not airstrike_enablers[i] then
		if round_num == 3 then
			airstrike_enablers[i]=NewUnit("airstenable",true,{Location=CPos.New(10,10),Owner=pl})
		end
	end
	local pl=PlayerLookup[i]
	if pl and not airstrike_enablers[i] then
		if round_num == 4 then
			airstrike_enablers[i]=NewUnit("airstenable4",true,{Location=CPos.New(10,10),Owner=pl})
		end
	end
	local pl=PlayerLookup[i]
	if pl and not airstrike_enablers[i] then
		if round_num == 5 then
			airstrike_enablers[i]=NewUnit("airstenable5",true,{Location=CPos.New(10,10),Owner=pl})
		end
	end
	local pl=PlayerLookup[i]
	if pl and not airstrike_enablers[i] then
		if round_num == 6 then
			airstrike_enablers[i]=NewUnit("airstenable6",true,{Location=CPos.New(10,10),Owner=pl})
		end
	end
	local pl=PlayerLookup[i]
	if pl and not airstrike_enablers[i] then
		if round_num == 7 then
			airstrike_enablers[i]=NewUnit("airstenable7",true,{Location=CPos.New(10,10),Owner=pl})
		end
	end
end








AirstrikeOff = function(i)
	local enb=airstrike_enablers[i]
	if enb then
		local o=enb.Owner
		--The game only checks prerequisites if the owner changes
		--Therefore the owner must be changed for the player to lose the airstrike (even if enb gets destroyed)
		enb.Owner=NeutralPlayer
		enb.Destroy()
		airstrike_enablers[i]=nil
	end
end

EnableAirstrikes = function() for i=1,4 do AirstrikeOn(i) end end
DisableAirstrikes = function() for i=1,4 do AirstrikeOff(i) end end

GetLanderPath = function(player)
	DebugBeacon("GetLanderPath")
	local wp=hovercraft_waypoints[PlayerLookup[player]]
	if ( not hovercraft_exit_counter ) or ( hovercraft_exit_counter >= #wp[2] ) then
		hovercraft_exit_counter = 1
	else
		hovercraft_exit_counter = hovercraft_exit_counter + 1
	end
	local from=wp[1]
	local to=wp[2][hovercraft_exit_counter]
	assert(from)
	assert(to)
	return {from.Location,to.Location}
end

LoadLander = function(player,troops)
	DebugBeacon("LoadLander")
	local from_to=GetLanderPath(player)
	if true then
		-- ReinforceWithTransport returns {theTransportActor,passengerList}
		--crash: return Reinforcements.ReinforceWithTransport(player, "lst", troops, from_to, {from_to[1]})[2]
		return Reinforcements.ReinforceWithTransport(player, "tran", troops, from_to, {from_to[1]})[2]
	else
		local stuff={}
		for i,kind in ipairs(troops) do
			local a=Actor.Create(kind,true,{Owner=player,Location=from_to[2]})
			a.Scatter()
			stuff[#stuff+1]=a
		end
		return stuff
	end
end

loadouts={{{"bike","bggy","apc","jeep"},{"e1","e1","e1","e1","e1","e1","e1","e1","e1","e1"}},
{{"apc2","apc","apc","arty","arty"}},
{{"3tnk","bike","mrpg","e3","e3"}},
{{"msam","bike","ltnk","mlrs","arty"},{"htnk"}},
{{"mrpg","mrpg","arty","arty","farty"},{"mrpg","mrpg","arty","arty","farty"}},
{{"ftnk","bike","bggy","3tnk"},{"arty","mtnk","ftnk","e3","e3"},{"ltnk","ltnk","apc2","mrpg","mrpg"}},
{{"msam","v2rl","v2rl","arty","arty"},{"mlrs","mlrs","apc2","farty","farty"},{"htnk"},{"htnk"},{"mrpg","mrpg","msam"}}}

-- 1 table per round, which contains a table of tables which in turn contain the units brought within each transport
--[[ max 5 units per transport (could mod LST to hold more)
loadouts = {{{"bike","bike","bike"}},
{{"mrpg","mrpg","arty","arty","farty"},{"mrpg","mrpg","arty","arty","farty"}},
{{"bike","bggy","ftnk","bike"},{"e3","e3","e3","e3","arty"}},
{{"bike","bggy","bggy","apc","arty"}},
{{"apc","apc","apc","e3","e3"},{"bike","bike","bike","bike"},{"3tnk","arty","arty"}},
{{"ftnk","bike","bggy","3tnk"},{"arty","mtnk","ftnk","e3","e3"},{"ltnk","ltnk","apc","mrpg","mrpg"}},
--{{"3tnk","3tnk","jeep","arty","arty"},{"e3","e3","e3","e3","e3"},{"e1","e1","e1","rmbo","rmbo"},{"msam","3tnk"}},
{{"msam","msam","arty","arty","arty"},{"mlrs","mlrs","mlrs","farty","farty"},{"htnk"},{"htnk"},{"mrpg","mrpg","msam"}}}
]]

-- Number of the round when the match ends
final_round = #loadouts

GiveArmy = function(player)
	DebugBeacon("GiveArmy")
	if false then
		-- make rounds be real quick for testing
		--final_round=3
		local units=LoadLander(player,{"bggy"})
		if false then
			-- Send everyone's buggy to middle to find out if rush distance to island_centre is equal for everyone
			local MoveToMid=function(a) a.Stance="HoldFire"; a.Move(island_centre,2) end
			Utils.Do(units,function(u) Trigger.OnIdle(u,MoveToMid) end)
		end
		return units
	end
	local army={}
	for i,group in ipairs(loadouts[round_num]) do
		DebugBeacon("GiveArmy round="..round_num..", group="..i)
		army=Concat(army,LoadLander(player,group))
	end
	return army
end

GameOver = function()
	DebugBeacon("GameOver")
	local best_score=0
	local winners={}
	local winners_p={}
	local losers_p={}
	for p,s in ipairs(player_scores) do
		if s > best_score then best_score=s end
	end
	-- It is possible for 2 players to have equal score (so 2 winners)
	for p,s in ipairs(player_scores) do
		player=PlayerLookup[p]
		if player then
			if s == best_score then
				winners[#winners+1]=p
				winners_p[#winners_p+1]=player
			else
				losers_p[#losers_p+1]=player
			end
		end
	end
	ChatMsg("The match is over")
	if #winners == 1 then
		ChatMsg(PlayerNicks[winners[1]].." won the match with "..best_score.." points!")
	elseif #winners > 1 then
		local str=""
		for i,p in ipairs(winners) do str=str..", "..PlayerNicks[p] end
		ChatMsg(string.sub(str,3).." all won the match with tied "..best_score.." points!")
	end
	-- End the mission. Show endgame menu.
	DoLater(5,function()
		for i,p in ipairs(winners_p) do
			p.MarkCompletedObjective(p.AddPrimaryObjective("win"))
		end
		for i,p in ipairs(losers_p) do
			p.MarkFailedObjective(p.AddPrimaryObjective("fail"))
		end
	end)
end

PrintScores = function()
	DebugBeacon("PrintScores")
	local s=""
	for i=1,4 do
		nick=PlayerNicks[i]
		if nick then
			s=s..", "..nick.." "..player_scores[i]
		end
	end
	s=string.sub(s,2)
	ChatMsg("Points:"..s)
end

EndRound = function(p)
	DebugBeacon("EndRound")
	round_end_timeout=-1
	ChatMsg("<<< Round "..round_num.." has ended <<<")
	if p then
		-- give a point to the winner
		player_scores[p] = player_scores[p] + 1
		ChatMsg(PlayerNicks[p].." gets the point!")
	end
	PrintScores()
	if round_num < final_round then
		DisableAirstrikes()
		DoLater(5,BeginRound)
	else
		GameOver()
	end
end

LastLivePlayer = function()
	DebugBeacon("LastLivePlayer")
	local num_live=0
	local last_live=nil
	for index,is_live in ipairs(live_players) do
		if is_live then
			num_live=num_live+1
			last_live=index
		end
	end
	return num_live,last_live
end

PlayerHasLost = function(p,r)
	if not live_players[p] then
		-- Called twice (from both CheckTimeout and OnAllKilled?)
		return
	end
	if round_num ~= r then
		-- Sometimes a round can end twice if ClearArena() fails to clear the arena of units
		-- This is the quick fix
		DebugBeacon("PlayerHasLost: wrong round")
		return
	end
	DebugBeacon("PlayerHasLost")
	live_players[p]=false
	AirstrikeOff(p)
	local num_live,the_one = LastLivePlayer()
	if num_live == 1 then
		EndRound(the_one)
	end
end

-- Supposed to instantly remove the actor (yet sometimes the actor sticks fingers in his ears and keeps on walking!?!?)
DestroyActor = function(actor)
	if not actor.IsDead then
		actor.Owner=NeutralPlayer--keep player from spamming move commands (they can prevent Destroy?)
		actor.Stop()
		actor.Destroy()
		Trigger.OnIdle( actor, function(a) a.Destroy() end )
	end
end


IsHusk = function(actor)
	return EndsWith(string.lower(actor.Type),".husk") -- actor.HasProperty("Husk") returns always false
end
IsCombatant = function(actor)
	-- There are no creeps. Just neutral & player units. And the only player units are supposed to be those from GiveArmy	
	return actor.Owner ~= NeutralPlayer
end
-- Returns true for any actors that need to disappear when a round ends
MustBeDestroyed = function(actor)
	return IsHusk(actor) or IsCombatant(actor) or actor.HasProperty("Crate")
end

ClearArena = function()
	DebugBeacon("ClearArena")
	Utils.Do(Map.ActorsInBox(Map.TopLeft,Map.BottomRight,MustBeDestroyed),DestroyActor)
end

CheckTimeout = function()
	DebugBeacon("CheckTimeout t="..round_end_timeout)
	round_end_timeout = round_end_timeout - 1
	if ( round_end_timeout > 0 ) and ( round_end_timeout <= round_end_timeout_danger ) then
		ChatMsg("Round times out in "..round_end_timeout*10 .." s unless a unit is attacked")
	end
	if round_end_timeout == 0 then
		ChatMsg("Time limit reached")
		ClearArena()
		EndRound(nil)
	end
	DoLater(10,CheckTimeout)
end

ResetTimeout = function()
	DebugBeacon("ResetTimeout")
	if round_end_timeout > 0 then
		if round_end_timeout <= round_end_timeout_danger then
			-- Only show this warning if time was close to running out 
			ChatMsg("Timeout reset")
		end
		round_end_timeout = round_end_timeout_initial
	end
end

OnDamagedResetTimeout = function(my,his)
	if my.Owner ~= his.Owner then
		ResetTimeout()
	end
end

SpawnCrates = function()
	local r=2 -- Max displacement
	for i=1,2 do
		NewUnit("dowant1",true,{Owner=NeutralPlayer,Location=island_centre+CVec.New(Rand(-r,r),Rand(-r,r))})
	end
end

BeginRound = function()
	DebugBeacon("BeginRound: begin")
	assert( round_num < final_round, "Can't BeginRound after the final round" )
	round_num=round_num+1
	round_end_timeout=round_end_timeout_initial
	ChatMsg(">>> Round "..round_num.." commencing >>>")
	if round_num == 1 then
		ChatMsg([[Last player with a unit alive wins the round and gains 1 point.
 Most points => victory. There are ]]..final_round..[[ rounds in total. Thats it. Now fight]])
	end
	if round_num >= 3 then
		if round_num == 3 then
			ChatMsg("Airstrike unlocked")
		end
		DoLater(1,EnableAirstrikes) -- airstrikes are disabled when round ends and re-enabled here (to prevent spawn killing)
	end
	ClearArena()
	SpawnCrates()
	for pnum=1,4 do
		local player = PlayerLookup[pnum]
		if player then
			--Media.PlaySoundNotification("AnnounceRoundBegin")
			local army = GiveArmy(player)
			DebugBeacon("BeginRound: callbacks")
			Utils.Do(army,function(actor) Trigger.OnDamaged(actor,OnDamagedResetTimeout) end)
			AfterEveryoneIsDead(army,function() PlayerHasLost(pnum,round_num) end)
			live_players[pnum]=true
		end
	end
end

InitPlayers = function()
	DebugBeacon("Initializing player list")
	PlayerLookup={}
	for i=1,4 do
		local s="Multi"..i-1
		local p=ObtainPlayer(s)
		if p then
			local nick="Unnamed"..i
			if p.Name then nick=p.Name end
			PlayerLookup[i]=p
			PlayerLookup[p]=i
			PlayerNicks[i]=nick
			DebugBeacon("Player registered: "..s)
			--prim_objectives[n]=p.AddPrimaryObjective("Destroy all enemy units")
		end
	end
	NeutralPlayer=ObtainPlayer("Neutral")
end

WorldLoaded = function()
	ChatMsg("Script version: "..script_version_num)
	InitPlayers()
	DoLater(2,BeginRound)
	DoLater(10,CheckTimeout)
end

