diff options
Diffstat (limited to 'disengage.lua')
-rw-r--r-- | disengage.lua | 365 |
1 files changed, 365 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 + |