summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--disengage.lua365
-rw-r--r--disengage_damaged_units.lua53
2 files changed, 418 insertions, 0 deletions
diff --git a/disengage.lua b/disengage.lua
new file mode 100644
index 0000000..edbdf7a
--- /dev/null
+++ b/disengage.lua
@@ -0,0 +1,365 @@
+-- This widget allows units to disengage from combat. Specifically this is
+-- useful for disengaging damaged units and sending them back to a safe location
+-- where they can be repaired and returned to combat.
+--
+-- When a unit is disengaged, it will be deselected, removed from its unit
+-- group, and (with UnicodeSnowman's patched smart_select) will not be
+-- selectable with smart_select. This is to avoid accidentally re-engaging the
+-- unit prematurely when executing sweaty micro. Once a unit is re-engaged
+-- (reaches the fallback points or is given a new command or stopped) it will be
+-- added back to the unit group and become selectable again.
+
+function widget:GetInfo()
+ return {
+ name = "Disengage",
+ desc = "Allows units to disengage from combat.",
+ author = "UnicodeSnowman",
+ date = "May 10, 2023",
+ license = "GNU GPL, v2 or later",
+ handler = true,
+ layer = 5,
+ enabled = true -- loaded by default?
+ }
+end
+
+widgetHandler = assert(widgetHandler)
+widget = assert(widget)
+Spring = assert(Spring)
+WG = assert(WG)
+CMD = assert(CMD)
+CMDTYPE = assert(CMDTYPE)
+gl = assert(gl)
+GL = assert(GL)
+UnitDefs = assert(UnitDefs)
+
+local spacePerUnit = 70
+
+local spSetUnitGroup = Spring.SetUnitGroup
+local spGetUnitGroup = Spring.GetUnitGroup
+local spGetCameraPosition = Spring.GetCameraPosition
+local spGetViewGeometry = Spring.GetViewGeometry
+local spTraceScreenRay = Spring.TraceScreenRay
+local GL_LINE_STRIP = GL.LINE_STRIP
+local glVertex = gl.Vertex
+local glLineStipple = gl.LineStipple
+local glLineWidth = gl.LineWidth
+local glColor = gl.Color
+local glBeginEnd = gl.BeginEnd
+
+local ceil = math.ceil
+local sqrt = math.sqrt
+local max = math.max
+
+
+local CMD_MOVE = CMD.MOVE
+local CMD_DISENGAGE = 1133
+local CMD_SET_FALLBACK_POSITION = 1134
+
+local spGetUnitPosition = Spring.GetUnitPosition
+local spGiveOrderToUnit = Spring.GiveOrderToUnit
+local spGetSelectedUnits = Spring.GetSelectedUnits
+local spGetGroundHeight = Spring.GetGroundHeight
+local spGetTeamStartPosition = Spring.GetTeamStartPosition
+
+local unitFallbackPosition = {}
+
+local function tVerts(verts)
+ for i = 1, #verts do
+ local v = verts[i]
+ glVertex(v[1], v[2], v[3])
+ end
+end
+
+local function disengageUnit(uid)
+ local pos = unitFallbackPosition[uid]
+ if not pos then
+ -- By default, run back to base.
+ pos = spGetTeamStartPosition(0)
+ if not pos then
+ return false
+ end
+ end
+
+ spGiveOrderToUnit(uid, CMD_MOVE, { pos.x, pos.y, pos.z }, 0)
+
+ local group = spGetUnitGroup(uid)
+ spSetUnitGroup(uid, -1)
+
+ WG.disengaged[uid] = { originalGroup = group }
+ return true
+end
+
+local function reengageUnit(uid)
+ local unitInfo = WG.disengaged[uid]
+ if unitInfo then
+ WG.disengaged[uid] = nil
+ spSetUnitGroup(uid, unitInfo.originalGroup or -1)
+ end
+end
+
+local function disengageUnits()
+ for _, u in pairs(spGetSelectedUnits()) do
+ disengageUnit(u)
+ end
+end
+
+function widget:UnitDestroyed(uid)
+ unitFallbackPosition[uid] = nil
+end
+
+-- local function dump(o)
+-- if type(o) == 'table' then
+-- local s = '{ '
+-- for k,v in pairs(o) do
+-- if type(k) ~= 'number' then k = '"'..k..'"' end
+-- s = s .. '['..k..'] = ' .. dump(v) .. ','
+-- end
+-- return s .. '} '
+-- else
+-- return tostring(o)
+-- end
+-- end
+
+local function getLocationsForFront(n, center, right)
+ -- The command give the center point and the rightmost point. We'll normalize
+ -- this so the leftmost point is at the origin and this normalized vector is
+ -- the rightmost position. It makes the math MUCH easier.
+ local normalized = {
+ x = (right.x - center.x) * 2,
+ y = (right.y - center.y) * 2,
+ z = (right.z - center.z) * 2 }
+
+ -- Takes a vector relative to the normalized vector and denormalizes it back
+ -- to the world coordinates.
+ local function denorm(pos)
+ return { x = (pos.x - normalized.x / 2) + center.x,
+ y = (pos.y - normalized.y / 2) + center.y,
+ z = (pos.z - normalized.z / 2) + center.z }
+ end
+
+ -- Get the length battle-front line.
+ local xsq = normalized.x
+ xsq = xsq * xsq
+ local zsq = normalized.z
+ zsq = zsq * zsq
+ local llen2d = sqrt(xsq + zsq)
+
+ local max_units_in_rank = llen2d / spacePerUnit
+ local n_ranks = ceil(n / max_units_in_rank)
+
+ local xstep = normalized.x / n
+ local pos
+
+ local function getY(x)
+ return { x = x, y = normalized.y, z = (normalized.z / normalized.x) * x }
+ end
+
+ local x = 0
+ if n_ranks <= 1 then
+ -- If there is only one rank, spread the units along that front.
+ pos = {}
+ x = xstep / 2
+ for i = 1, n do
+ local p = denorm(getY(x))
+ p.y = spGetGroundHeight(p.x, p.z)
+ p.u = i
+ table.insert(pos, p)
+ x = x + xstep
+ end
+ else
+ pos = {}
+
+ -- Vector perpendicular to the drawn front vector. Used to create the ranks.
+ local perp_vec = { x = - normalized.z, 0, z = normalized.x }
+
+ -- Normalize the perpendicuar vector to be of length "spacePerUnit"
+ -- This vector * rank will be added to each unit.
+ local perp_vec_len = sqrt(
+ perp_vec.x * perp_vec.x +
+ perp_vec.z * perp_vec.z)
+ perp_vec.x = perp_vec.x / perp_vec_len
+ perp_vec.z = perp_vec.z / perp_vec_len
+ perp_vec.x = perp_vec.x * spacePerUnit
+ perp_vec.z = perp_vec.z * spacePerUnit
+
+ xstep = normalized.x / max_units_in_rank
+ for r = 1, n_ranks do
+ x = 0
+ for j = 1, max_units_in_rank do
+ if #pos <= n then
+ local point = getY(x)
+ point.x = point.x + perp_vec.x * r
+ point.z = point.z + perp_vec.z * r
+ local p = denorm(point)
+ p.u = #pos
+ p.y = spGetGroundHeight(p.x, p.z)
+ table.insert(pos, p)
+ x = x + xstep
+ end
+ end
+ end
+ end
+ return pos
+end
+
+-- When the Unit is done executing a command, the unit is then considered
+-- re-engaged.
+function widget:UnitCmdDone(uid)
+ reengageUnit(uid)
+end
+
+function widget:UnitCommand(uid, _, _, cid)
+ Spring.Echo("Unit Command: " .. uid .. ", " .. cid)
+
+ if cid == CMD_DISENGAGE then
+ disengageUnit(uid)
+ return true
+ end
+end
+
+function widget:CommandNotify(id, params, options)
+ Spring.Echo("Command Notify: " .. id)
+
+ if id == CMD_SET_FALLBACK_POSITION then
+ -- Sets the unit's fallback point.
+ --
+ -- The command doesn't actually change what the unit is doing, it just sets
+ -- the fallback point for disengaged units.
+ local selectedUnits = Spring.GetSelectedUnits()
+ if #params < 6 then
+ for _, uid in selectedUnits do
+ unitFallbackPosition[uid] = { x = params[1], y = params[2], z = params[3] }
+ end
+ else
+ local locs = getLocationsForFront(
+ #selectedUnits,
+ { x = params[1], y = params[2], z = params[3] },
+ { x = params[4], y = params[5], z = params[6] })
+
+ for i = 1, #selectedUnits do
+ unitFallbackPosition[selectedUnits[i]] = locs[i]
+ end
+ end
+
+ return true
+ elseif id == CMD_DISENGAGE then
+ -- Tells the units to disengage from combat. Disengaged units will start
+ -- moving to the fallback point and will no longer be selectable using smart
+ -- select or the unit's group.
+ --
+ -- Once the unit is re-engaged, it will se selectable again.
+ disengageUnits()
+ return true
+ else
+ -- If the command is neither FALLBACK or SET_FALLBACK, re-engage the unit.
+ for _, u in pairs(spGetSelectedUnits()) do
+ reengageUnit(u)
+ end
+ return false
+ end
+end
+
+local function drawGroundQuad(x,y,z,size)
+ gl.TexCoord(0,0)
+ gl.Vertex(x-size,y,z-size)
+ gl.TexCoord(0,1)
+ gl.Vertex(x-size,y,z+size)
+ gl.TexCoord(1,1)
+ gl.Vertex(x+size,y,z+size)
+ gl.TexCoord(1,0)
+ gl.Vertex(x+size,y,z-size)
+end
+
+local dotImage = "LuaUI/Images/formationDot.dds"
+
+local function drawCircle(pos, size, disengaged)
+ if disengaged == true then
+ glColor(1, 0.6, 0.6, 0.85)
+ else
+ glColor(0.75, 0.75, 0.75, 0.75)
+ end
+ gl.Texture(dotImage)
+ gl.BeginEnd(GL.QUADS,drawGroundQuad, pos[1], pos[2], pos[3], size)
+ gl.Texture(false)
+end
+
+local Xs, Ys = spGetViewGeometry()
+Xs, Ys = Xs*0.5, Ys*0.5
+function widget:ViewResize(viewSizeX, viewSizeY)
+ Xs, Ys = spGetViewGeometry()
+ Xs, Ys = Xs*0.5, Ys*0.5
+end
+
+function widget:DrawWorld()
+ local camX, camY, camZ = spGetCameraPosition()
+ local at, p = spTraceScreenRay(Xs,Ys,true,false,false)
+ local zoomY
+
+ if at == "ground" then
+ local dx, dy, dz = camX-p[1], camY-p[2], camZ-p[3]
+ zoomY = sqrt(dx*dx + dy*dy + dz*dz)
+ else
+ zoomY = camY - max(spGetGroundHeight(camX, camZ), 0)
+ end
+
+ if zoomY < 6 then zoomY = 6 end
+ glLineStipple(2, 8031)
+ glLineWidth(1.0)
+ for _, uid in pairs(spGetSelectedUnits()) do
+ local pos = unitFallbackPosition[uid]
+ if pos then
+ local x, y, z = spGetUnitPosition(uid)
+
+ if WG.disengaged[uid] then
+ glColor(1, 0.6, 0.6, 0.8)
+ else
+ glColor(0.75, 0.75, 0.75, 0.5)
+ end
+
+ glBeginEnd(GL_LINE_STRIP, tVerts, { {x, y, z}, { pos.x, pos.y, pos.z } })
+ local dotSize = sqrt(zoomY*0.12)
+ drawCircle(
+ { pos.x, pos.y, pos.z },
+ dotSize * (WG.disengaged[uid] and 2 or 1),
+ WG.disengaged[uid] and true or false)
+ end
+ end
+end
+
+function widget:Initialize()
+ WG['disengaged'] = {}
+
+ WG.CMD_DISENGAGE = CMD_DISENGAGE
+ WG.CMD_SET_FALLBACK_POSITION = CMD_SET_FALLBACK_POSITION
+end
+
+function widget:CommandsChanged()
+ local selectedUnits = Spring.GetSelectedUnits()
+ if #selectedUnits > 0 then
+ local customCommands = widgetHandler.customCommands
+ for i = 1, #selectedUnits do
+ local udid = Spring.GetUnitDefID(selectedUnits[i])
+ if UnitDefs[udid].canMove then
+ customCommands[#customCommands + 1] = {
+ id = CMD_SET_FALLBACK_POSITION,
+ type = CMDTYPE.ICON_FRONT,
+ tooltip = 'Set a fallback position for this unit.',
+ name = 'SetFallback',
+ cursor = 'areamex',
+ action = 'setfallback',
+ }
+
+ customCommands[#customCommands + 1] = {
+ id = CMD_DISENGAGE,
+ type = CMDTYPE.ICON,
+ tooltip = 'Disengage and fallback to a previously defined fallback position',
+ name = 'Disengage',
+ cursor = 'areamex',
+ action = 'disengage',
+ }
+ return
+ end
+ end
+ end
+end
+
diff --git a/disengage_damaged_units.lua b/disengage_damaged_units.lua
new file mode 100644
index 0000000..eecd364
--- /dev/null
+++ b/disengage_damaged_units.lua
@@ -0,0 +1,53 @@
+function widget:GetInfo()
+ return {
+ name = "DisengageDamagedUnits",
+ desc = "Disengages damaged units from the battle",
+ author = "UnicodeSnowman",
+ date = "May 2023",
+ license = "GPL v2",
+ layer = 0,
+ enabled = true
+ }
+end
+
+-- Some defines to make the language server happier.
+widgetHandler = assert(widgetHandler)
+widget = assert(widget)
+Spring = assert(Spring)
+WG = assert(WG)
+
+-- Units below this damage threshold will be disengaged. TODO make this
+-- configurable.
+local damageThreshold = 0.3
+
+-- Spring commands for more speed.
+local spGetSelectedUnits = Spring.GetSelectedUnits
+local spGetUnitHealth = Spring.GetUnitHealth
+local spGiveOrderToUnit = Spring.GiveOrderToUnit
+local spSelectUnitArray = Spring.SelectUnitArray
+
+local function disengageDamagedUnits()
+ local newSelection = {}
+
+ for _, uid in pairs(spGetSelectedUnits()) do
+ local health, maxHealth, _, _, _ = spGetUnitHealth(uid)
+ local percent = health / maxHealth
+
+ if percent < damageThreshold then
+ Spring.Echo("Disengaging " .. uid)
+ spGiveOrderToUnit(uid, WG.CMD_DISENGAGE, {}, 0)
+ else
+ table.insert(newSelection, uid)
+ end
+ end
+
+ spSelectUnitArray(newSelection)
+end
+
+function widget:Initialize()
+ -- The units with health below the health threshold will be disegaged from
+ -- combat and be sent back to the fallback position.
+ widgetHandler:AddAction(
+ "disengage_damaged_units", disengageDamagedUnits, nil, "p")
+end
+