All pastes #2016387 Raw Edit

Untitled

public text v1 · immutable
#2016387 ·published 2010-12-11 13:09 UTC
rendered paste body
function widget:GetInfo()
  return {
	name 	= "Tactical Unit AI",
	desc	= "Implements tactial AI for some units",
	author	= "Google Frog",
	date	= "April 20 2010",
	license	= "GNU GPL, v2 or later",
	layer	= 0,
	enabled = true,
  }
end

--------------------------------------------------------------------------------
-- Speedups

local spInsertUnitCmdDesc   = Spring.InsertUnitCmdDesc
local spGetCommandQueue		= Spring.GetCommandQueue
local spGetUnitDefID		= Spring.GetUnitDefID	
local spGetUnitPosition		= Spring.GetUnitPosition
local spGiveOrderToUnit		= Spring.GiveOrderToUnit
local spGetUnitNearestEnemy = Spring.GetUnitNearestEnemy
local spGetUnitSeparation 	= Spring.GetUnitSeparation
local spEditUnitCmdDesc 	= Spring.EditUnitCmdDesc
local spFindUnitCmdDesc		= Spring.FindUnitCmdDesc
local spGetUnitAllyTeam		= Spring.GetUnitAllyTeam
local spGetUnitLosState		= Spring.GetUnitLosState
local spGetUnitStates		= Spring.GetUnitStates
local spValidUnitID			= Spring.ValidUnitID
local random 				= math.random
local sqrt 					= math.sqrt

--------------------------------------------------------------------------------
-- Globals

local unit = {}
local unitAIBehaviour = {}

--------------------------------------------------------------------------------
-- Commands

local CMD_MOVE			= CMD.MOVE
local CMD_ATTACK		= CMD.ATTACK
local CMD_FIGHT			= CMD.FIGHT
local CMD_OPT_INTERNAL 	= CMD.OPT_INTERNAL
local CMD_OPT_RIGHT 	= CMD.OPT_RIGHT
local CMD_INSERT 		= CMD.INSERT
local CMD_REMOVE 		= CMD.REMOVE

--------------------------
---- Unit AI
--------------------------

local function distance(x1,y1,x2,y2)
	return math.sqrt((x1-x2)^2 + (y1-y2)^2)
end

local function getUnitState(unitID,data,cQueue)
	-- returns enemy ID or -1
	-- returns if I have previous movement
	
	if not cQueue or #cQueue == 0 then
		local movestate = spGetUnitStates(unitID).movestate
		if movestate ~= 0 then
			return -1, false -- could still skirm from static or flee
		end
		return false -- no queue, return
	end
	
	local movestate = spGetUnitStates(unitID).movestate
	if (#cQueue == 1 and movestate == 0 and cQueue[1].id == CMD_ATTACK and cQueue[1].options.internal) then
		return false -- set to hold position and not given a user command
	end
	
	if (cQueue[1].id == CMD_ATTACK) or (cQueue[1].id == CMD_FIGHT) then -- if I attack 
		
		local target,check = cQueue[1].params[1],cQueue[1].params[2]
		if not check then -- if I target a unit
			local los = spGetUnitLosState(target,data.allyTeam,false)
			if los then
				los = los.los
			end
			if los and not (cQueue[1].id == CMD_FIGHT or cQueue[1].options.internal) then -- only skirm single target when given the order manually
				return target,false
			else
				return -1,false
			end
		elseif (cQueue[1].id == 16) then --  if I target the ground and have fight or patrol command
			return -1,false
		end
	  
    elseif (cQueue[1].id == CMD_MOVE) and #cQueue > 1 then -- if I am moving
	
		local cx,cy,cz = cQueue[1].params[1],cQueue[1].params[2],cQueue[1].params[3]
		if (cx == data.cx) and (cy == data.cy) and (cz == data.cz) then -- if I was given this move command by this gadget
			if (cQueue[2].id == CMD_ATTACK) or (cQueue[2].id == CMD_FIGHT) then -- if the next command is attack, patrol or fight
				local target,check = cQueue[2].params[1],cQueue[2].params[2]
				if not check then -- if I target a unit
					local los = spGetUnitLosState(target,data.allyTeam,false)
					if los then 
						los = los.los
					end
					if los and not (cQueue[2].id == CMD_FIGHT or cQueue[2].options.internal) then -- only skirm single target when given the order manually
						return target,true
					else
						return -1,true
					end
				elseif (cQueue[2].id == 16) then -- if I target the ground and have fight or patrol command
					return -1,true
				end
			end
		end

	end

	return false
end

local function clearOrder(unitID,data,cQueue)
	-- removes move order
	if receivedOrder then

		if (#cQueue >= 1 and cQueue[1].id == CMD_MOVE) then -- if I am moving
			local cx,cy,cz = cQueue[1].params[1],cQueue[1].params[2],cQueue[1].params[3]
			if (cx == data.cx) and (cy == data.cy) and (cz == data.cz) then -- if I was given this move command by this gadget
				spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} )
			end
		end
		receivedOrder = false
	end
	
end

local function swarmEnemy(unitID, enemy, enemyUnitDef, los, move, cQueue,n)

	local data = unit[unitID]
	local behaviour = unitAIBehaviour[data.udID]
	
	if enemy and los then
	
		local pointDis = spGetUnitSeparation (enemy,unitID,true)
		
		if pointDis then

			if behaviour.maxSwarmRange < pointDis then -- if I cannot shoot at the enemy
				
				local enemyRange = behaviour.swarmEnemyDefaultRange
				if enemyUnitDef and los then
					enemyRange = UnitDefs[enemyUnitDef].maxWeaponRange
				end
				
				if pointDis < enemyRange+behaviour.swarmLeeway then -- if I am within enemy range

					local ex,ey,ez = spGetUnitPosition(enemy) -- enemy position
					local ux,uy,uz = spGetUnitPosition(unitID) -- my position
					local cx,cy,cz -- command position
					
					-- insert move commands to jink towards enemy
					data.jinkDir = data.jinkDir*-1
					
					-- jink towards the enemy
					cx = ux+(-(ux-ex)*behaviour.jinkParallelLength-(uz-ez)*data.jinkDir*behaviour.jinkTangentLength)/pointDis
					cy = uy
					cz = uz+(-(uz-ez)*behaviour.jinkParallelLength+(ux-ex)*data.jinkDir*behaviour.jinkTangentLength)/pointDis
					if move then
						spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} )
						spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
					else
						spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
					end
					data.cx,data.cy,data.cz = cx,cy,cz
					
					data.receivedOrder = true
					return true
				end
			else -- if I can shoot at the enemy

				local ex,ey,ez = spGetUnitPosition(enemy) -- enemy position
				local ux,uy,uz = spGetUnitPosition(unitID) -- my position
				local cx,cy,cz -- command position
				
				if behaviour.circleStrafe then
					
					-- jink around the enemy
					local up = 0
					local ep = 1
					if pointDis < behaviour.minCircleStrafeDistance then
						up = 1
						ep = 0
					end
					
					cx = ux*up+ex*ep+data.rot*(uz-ez)*behaviour.strafeOrderLength/pointDis
					cy = uy
					cz = uz*up+ez*ep-data.rot*(ux-ex)*behaviour.strafeOrderLength/pointDis
					
				else
					if pointDis > behaviour.minSwarmRange then
						-- jink at max range
						cx = ux+data.rot*(uz-ez)*behaviour.strafeOrderLength/pointDis
						cy = uy
						cz = uz-data.rot*(ux-ex)*behaviour.strafeOrderLength/pointDis
						data.rot = data.rot*-1
					else
						data.jinkDir = data.jinkDir*-1					-- jink away
						cx = ux-(-(ux-ex)*behaviour.jinkParallelLength-(uz-ez)*data.jinkDir*behaviour.jinkTangentLength)/pointDis
						cy = uy
						cz = uz-(-(uz-ez)*behaviour.jinkParallelLength+(ux-ex)*data.jinkDir*behaviour.jinkTangentLength)/pointDis
					end
				end
				
				if move then
					spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} )
					spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
				else
					spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
				end
				data.cx,data.cy,data.cz = cx,cy,cz
				data.receivedOrder = true
				return true
			end
			
		end
		
	else
	
		if (#cQueue > 0 and cQueue[1].id == CMD_FIGHT) or move then
			local ex,ey,ez -- enemy position
			if move and #cQueue > 1 then
				ex,ey,ez = cQueue[2].params[1],cQueue[2].params[2],cQueue[2].params[3]
			else
				ex,ey,ez = cQueue[1].params[1],cQueue[1].params[2],cQueue[1].params[3] 
			end
			local ux,uy,uz = spGetUnitPosition(unitID) -- my position
			local cx,cy,cz -- command position
			
			local pointDis = distance(ex,ez,ux,uz)
			
			-- insert move commands to jink towards enemy
			data.jinkDir = data.jinkDir*-1
			
			-- jink towards the enemy
			cx = ux+(-(ux-ex)*behaviour.jinkParallelLength-(uz-ez)*data.jinkDir*behaviour.jinkTangentLength)/pointDis
			cy = uy
			cz = uz+(-(uz-ez)*behaviour.jinkParallelLength+(ux-ex)*data.jinkDir*behaviour.jinkTangentLength)/pointDis
			if move then
				spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} )
				spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
			else
				spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
			end
			data.cx,data.cy,data.cz = cx,cy,cz
				
			data.receivedOrder = true
			return true
		end
	end
	
	return false
	
end


local function skirmEnemy(unitID, enemy, move, cQueue,n)

	local data = unit[unitID]
	local behaviour = unitAIBehaviour[data.udID]
	
	local pointDis = spGetUnitSeparation (enemy,unitID,true)

	if pointDis then
		if behaviour.skirmRange > pointDis then -- if I can shoot at the enemy
		
			local ex,ey,ez = spGetUnitPosition(enemy) -- enemy position
			local ux,uy,uz = spGetUnitPosition(unitID) -- my position
			local cx,cy,cz -- command position
			
			local dis = behaviour.skirmOrderDis 
			local f = dis/pointDis
			if (pointDis+dis > behaviour.skirmRange-behaviour.stoppingDistance) then
				f = (behaviour.skirmRange-behaviour.stoppingDistance-pointDis)/pointDis
			end
			local cx = ux+(ux-ex)*f
			local cy = uy
			local cz = uz+(uz-ez)*f
		
			if move then
				spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} )
				spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
			else
				spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
			end
			
			data.cx,data.cy,data.cz = cx,cy,cz
			data.receivedOrder = true
			return true
			
		end
	end

	return false
end

local function fleeEnemy(unitID, enemy, enemyUnitDef, los, move, cQueue,n)

	local data = unit[unitID]
	local behaviour = unitAIBehaviour[data.udID]

	if (not enemy) or (#cQueue > 0 and cQueue[1].id == CMD_ATTACK and not cQueue[1].options.internal) or -- if I have been given attack order manually do not flee
	not ( (los and (behaviour.flees[enemyUnitDef] or (behaviour.fleeCombat and not UnitDefs[enemyUnitDef].modCategories["unarmed"]))) 
	-- if I have los and the unit is a fleeable or a unit is unarmed and I flee combat - flee
	or (not los and behaviour.fleeRadar)) then -- if I do not have los and flee radar dot, flee
		return false
	end

	local enemyRange = behaviour.minFleeRange
	
	if enemyUnitDef and los then
		local range = UnitDefs[enemyUnitDef].maxWeaponRange 
		if range > enemyRange then
			enemyRange = range
		end
	end

	local pointDis = spGetUnitSeparation (enemy,unitID,true)
	
	if pointDis then
	
		if pointDis < enemyRange + behaviour.fleeLeeway then -- if the enemy can shoot at me
			local ex,ey,ez = spGetUnitPosition(enemy) -- enemy position
			local ux,uy,uz = spGetUnitPosition(unitID) -- my position
			local cx,cy,cz -- command position
			
			local dis = behaviour.fleeOrderDis
			
			local f = dis/pointDis
			if (pointDis+dis > enemyRange+behaviour.fleeDistance) then
				f = (enemyRange+behaviour.fleeDistance-pointDis)/pointDis
			end
			
			local cx = ux+(ux-ex)*f
			local cy = uy
			local cz = uz+(uz-ez)*f
		
			if #cQueue > 0 then
				if move then
					spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} )
					spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
				else
					spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, cx,cy,cz }, {"alt"} )
				end
			else
				spGiveOrderToUnit(unitID, CMD_FIGHT, {cx,cy,cz }, CMD_OPT_RIGHT )
			end
			
			data.cx,data.cy,data.cz = cx,cy,cz
			data.receivedOrder = true
			return true
			
		end
	end

	return false
end

local function updateUnits(n)
	for unitID, data in pairs(unit) do
		
		while true do
			
			if not spValidUnitID(unitID) then
				unit[unitID] = nil
				break
			end
		
			--Spring.Echo("unit parsed")
			if not data.active then
				if data.receivedOrder then
					local cQueue = spGetCommandQueue(unitID)
					clearOrder(unitID,data,cQueue)
				end
				break
			end
			local cQueue = spGetCommandQueue(unitID)
			local enemy,move = getUnitState(unitID,data,cQueue) -- returns target enemy and movement state
			--local ux,uy,uz = spGetUnitPosition(unitID)
			--Spring.MarkerAddPoint(ux,uy,uz,"unit active")
			if (enemy) then -- if I am fighting/patroling ground or targeting an enemy
				--Spring.Echo("enemy spotted 1")
				if enemy == -1 then -- if I am fighting/patroling ground get nearest enemy
					enemy = (spGetUnitNearestEnemy(unitID,unitAIBehaviour[data.udID].searchRange,true) or false)
				end
				--Spring.Echo("enemy spotted 2")
				-- don't get info on out of los units
				--Spring.Echo("enemy in los")
				-- use AI on target
				local enemyUnitDef = false
				local los = false
				if enemy then
					los = spGetUnitLosState(enemy,data.allyTeam,false)
					if los then
						los = los.los
					end
					enemyUnitDef = spGetUnitDefID(enemy)
				end
				
				if (enemy and los and unitAIBehaviour[data.udID].swarms[enemyUnitDef]) or (((#cQueue > 0 and cQueue[1].id == CMD_FIGHT) or move) and unitAIBehaviour[data.udID].alwaysJinkFight ) then
					--Spring.Echo("unit checking swarm")
					if not swarmEnemy(unitID, enemy, enemyUnitDef, los, move, cQueue,n) then
						clearOrder(unitID,data,cQueue)
					end
				elseif enemy and ((los and unitAIBehaviour[data.udID].skirms[enemyUnitDef]) or ((not los) and unitAIBehaviour[data.udID].skirmRadar) or unitAIBehaviour[data.udID].skirmEverything) then
					--Spring.Echo("unit checking skirm")
					if not skirmEnemy(unitID, enemy, move, cQueue,n) then
						clearOrder(unitID,data,cQueue)
					end
				else
					--Spring.Echo("unit checking flee")
					if not fleeEnemy(unitID, enemy, enemyUnitDef, los,move, cQueue,n) then
						clearOrder(unitID,data,cQueue)
					end
				end
				
			end
			break
		end
		
		--Spring.Echo("")
	end
	
end

function widget:GameFrame(n)
 
	-- update orders
	if (n%20<1) then 
		updateUnits(n)
	end
  
end

------------------------------------------------------
-- Load Ai behaviour

local function LoadBehaviour(unitConfigArray)

	for unitDef, behaviourData in pairs(unitConfigArray) do
		local ud = UnitDefNames[unitDef]
		
		if ud then
			unitAIBehaviour[ud.id] = {
				skirms = {}, 
				swarms = {}, 
				flees  = {},
				circleStrafe = (behaviourData.circleStrafe or false), 
				skirmRadar = (behaviourData.skirmRadar or false), 
				skirmEverything = (behaviourData.skirmEverything or false), 
				maxSwarmRange = ud.maxWeaponRange - (behaviourData.maxSwarmLeeway or 0), 
				minSwarmRange = ud.maxWeaponRange - (behaviourData.minSwarmLeeway or ud.maxWeaponRange/2),
				minCircleStrafeDistance = ud.maxWeaponRange - (behaviourData.minCircleStrafeDistance or unitConfigArray.defaultMinCircleStrafeDistance),
				skirmRange = ud.maxWeaponRange - (behaviourData.skirmLeeway or 0),
				jinkTangentLength = (behaviourData.jinkTangentLength or unitConfigArray.defaultJinkTangentLength),
				jinkParallelLength =  (behaviourData.jinkParallelLength or unitConfigArray.defaultJinkParallelLength),
				alwaysJinkFight = (behaviourData.alwaysJinkFight or false),
				stoppingDistance = (behaviourData.stoppingDistance or 0),
				strafeOrderLength = (behaviourData.strafeOrderLength or unitConfigArray.defaultStrafeOrderLength),
				fleeCombat = (behaviourData.fleeCombat or false), 
				fleeLeeway = (behaviourData.fleeLeeway or 100), 
				fleeDistance = (behaviourData.fleeDistance or 100), 
				fleeRadar = (behaviourData.fleeRadar or false), 
				minFleeRange = (behaviourData.minFleeRange or 0), 
				swarmLeeway = (behaviourData.swarmLeeway or 50), 
				skirmOrderDis = (behaviourData.skirmOrderDis or 120),
				searchRange = (behaviourData.searchRange or 800),
				defaultAIState = (behaviourData.defaultAIState or 1),
				fleeOrderDis = (behaviourData.fleeOrderDis or 120)
			}
			
			unitAIBehaviour[ud.id].minFleeRange = unitAIBehaviour[ud.id].minFleeRange - unitAIBehaviour[ud.id].fleeLeeway
			
			if behaviourData.skirms then
				for _,skirmDef  in pairs(behaviourData.skirms) do
					local skirmName = UnitDefNames[skirmDef]
					if skirmName then
						unitAIBehaviour[ud.id].skirms[skirmName.id] = true
					end
				end
			end
			
			if behaviourData.swarms then
				for _,swarmDef  in pairs(behaviourData.swarms) do
					local swarmName = UnitDefNames[swarmDef]
					if swarmName then
						unitAIBehaviour[ud.id].swarms[swarmName.id] = true
					end
				end
			end
			
			if behaviourData.flees then
				for _,fleeDef  in pairs(behaviourData.flees) do
					local fleeName = UnitDefNames[fleeDef]
					if fleeName then
						unitAIBehaviour[ud.id].flees[fleeName.id] = true
					end
				end
			end
			
		end
	end
end

--------------------------------------------------------------------------------
-- Unit adding/removal

function widget:Initialize()

	-- import config
	behaviourDefs = include("Configs/tactical_ai_defs.lua")
	if not behaviourDefs then 
		Spring.Echo("Configs/tactical_ai_defs.lua not found")
		gadgetHandler:RemoveGadget()
		return 
	end
	LoadBehaviour(behaviourDefs)
	
	-- load active units
	for _, unitID in ipairs(Spring.GetAllUnits()) do
		local unitDefID = Spring.GetUnitDefID(unitID)
		local teamID = Spring.GetUnitTeam(unitID)
		widget:UnitCreated(unitID, unitDefID, teamID)
	end
	
end

function widget:UnitCreated(unitID, unitDefID, unitTeam, builderID) 

	local ud = UnitDefs[unitDefID]
	-- add swarmers
	if unitAIBehaviour[unitDefID] and unitTeam == Spring.GetMyTeamID() then
		behaviour = unitAIBehaviour[unitDefID]
		--Spring.Echo("unit added")
		unit[unitID] = {
			cx = 0, cy = 0, cz = 0,
			udID = unitDefID,
			jinkDir = random(0,1)*2-1, 
			rot = random(0,1)*2-1,
			active = true,
			receivedOrder = false,
			allyTeam = spGetUnitAllyTeam(unitID),
		}
		
	end
	
end

function widget:UnitDestroyed(unitID, unitDefID, unitTeam)
	if (unit[unitID]) then
		unit[unitID] = nil
	end
end