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