288 lines
9.1 KiB
Lua
288 lines
9.1 KiB
Lua
function widget:GetInfo()
|
|
return {
|
|
name = "Attack Warning",
|
|
desc = "Warns if stuff gets attacked",
|
|
author = "knorke, Birdulon",
|
|
date = "Oct 2011, June 2020",
|
|
license = "GNU GPL, v2 or later",
|
|
layer = 0,
|
|
enabled = true,
|
|
}
|
|
end
|
|
|
|
options_path = "Settings/Alerts"
|
|
options_order = {
|
|
'voiceSelfVolume', 'voiceAllyVolume', 'voiceSelfCooldown', 'voiceAllyCooldown',
|
|
'markersEnable', 'markersSelfDuration', 'markersAllyDuration', 'markersSelfRadius', 'markersAllyRadius',
|
|
}
|
|
options = {
|
|
markersEnable = {
|
|
name = "Enable attack alert markers",
|
|
type = "bool",
|
|
value = true,
|
|
desc = "Place temporary map markers to increase visibility of off-screen attacks",
|
|
noHotkey = true,
|
|
},
|
|
markersSelfDuration = {
|
|
name = "Attack marker duration (s) - self",
|
|
type = "number",
|
|
value = 5.0,
|
|
min = 0.5,
|
|
max = 15.0,
|
|
step = 0.5,
|
|
desc = "Attack markers for your own forces will be removed after this many seconds",
|
|
noHotkey = true,
|
|
},
|
|
markersSelfRadius = {
|
|
name = "Attack marker spacing (elmo) - self",
|
|
type = "number",
|
|
value = 750,
|
|
min = 250,
|
|
max = 2000,
|
|
step = 250,
|
|
desc = "If your forces are attacked within this distance of an existing attack marker, suppress the alert",
|
|
noHotkey = true,
|
|
},
|
|
markersAllyDuration = {
|
|
name = "Attack marker duration (s) - ally",
|
|
type = "number",
|
|
value = 2.5,
|
|
min = 0.5,
|
|
max = 15.0,
|
|
step = 0.5,
|
|
desc = "Attack markers for allied forces will be removed after this many seconds. You might want them to expire quicker than for your own forces.",
|
|
noHotkey = true,
|
|
},
|
|
markersAllyRadius = {
|
|
name = "Attack marker spacing (elmo) - ally",
|
|
type = "number",
|
|
value = 1500,
|
|
min = 250,
|
|
max = 3000,
|
|
step = 250,
|
|
desc = "If allied forces are attacked within this distance of an existing attack marker, suppress the alert. You might want them spaced out further than attacks on your forces.",
|
|
noHotkey = true,
|
|
},
|
|
voiceSelfVolume = {
|
|
name = "Attack alert volume - self",
|
|
type = "number",
|
|
value = 1.0,
|
|
min = 0.0,
|
|
max = 1.0,
|
|
step = 0.05,
|
|
desc = "Volume for attack alerts for your forces",
|
|
noHotkey = true,
|
|
},
|
|
voiceSelfCooldown = {
|
|
name = "Attack alert cooldown (s) - self",
|
|
type = "number",
|
|
value = 6,
|
|
min = 1,
|
|
max = 30,
|
|
step = 1,
|
|
desc = "Attack alerts for your forces will not be sounded if the last alert sound was within this period",
|
|
noHotkey = true,
|
|
},
|
|
voiceAllyVolume = {
|
|
name = "Attack alert volume - ally",
|
|
type = "number",
|
|
value = 1.0,
|
|
min = 0.0,
|
|
max = 1.0,
|
|
step = 0.05,
|
|
desc = "Volume for attack alerts for your forces",
|
|
noHotkey = true,
|
|
},
|
|
voiceAllyCooldown = {
|
|
name = "Attack alert cooldown (s) - ally",
|
|
type = "number",
|
|
value = 6,
|
|
min = 1,
|
|
max = 30,
|
|
step = 1,
|
|
desc = "Attack alerts for your forces will not be sounded if the last alert sound was within this period",
|
|
noHotkey = true,
|
|
},
|
|
}
|
|
|
|
local voiceFilenameSelfBaseAttacked = "sounds/alerts/SelfBaseAttacked.ogg"
|
|
local voiceFilenameSelfUnitAttacked = "sounds/alerts/SelfUnitAttacked.ogg"
|
|
local voiceFilenameAllyBaseAttacked = "sounds/alerts/AllyBaseAttacked.ogg"
|
|
local voiceFilenameAllyUnitAttacked = "sounds/alerts/AllyUnitAttacked.ogg"
|
|
local voiceFilenames = { -- [bSelf][isBuilding]
|
|
[false]={
|
|
[false]=voiceFilenameAllyUnitAttacked,
|
|
[true]=voiceFilenameAllyBaseAttacked,
|
|
},
|
|
[true]={
|
|
[false]=voiceFilenameSelfUnitAttacked,
|
|
[true]=voiceFilenameSelfBaseAttacked,
|
|
},
|
|
}
|
|
|
|
local osClock = os.clock
|
|
local spPlaySoundFile = Spring.PlaySoundFile -- (filename, volume, posx, posy, posz, speedx, speedy, speedz, channel)
|
|
local spMarkerAddPoint = Spring.MarkerAddPoint -- (x, y, z, text, localonly)
|
|
local spMarkerErasePosition = Spring.MarkerErasePosition -- (x, y, z) Sadly you can't keep handles of markers
|
|
local spGetUnitPosition = Spring.GetUnitPosition
|
|
local spAreTeamsAllied = Spring.AreTeamsAllied
|
|
local spGetGameFrame = Spring.GetGameFrame
|
|
local spGetSpectatingState = Spring.GetSpectatingState
|
|
local spEcho = Spring.Echo
|
|
local spSetLastMessagePosition = Spring.SetLastMessagePosition
|
|
|
|
local warningDelay = 30 * 5 --in frames
|
|
local lastWarning = 0 --in frames
|
|
local localTeamID = Spring.GetLocalTeamID()
|
|
|
|
local under_attack_translation
|
|
local function languageChanged()
|
|
under_attack_translation = WG.Translate("interface", "unit_under_attack")
|
|
end
|
|
|
|
local function dist2(x1, z1, x2, z2)
|
|
-- Distance squared between two points
|
|
-- Coords are X and Z because Spring uses Y as the vertical axis, and we only care about the planar position.
|
|
local dx, dz = x2-x1, z2-z1
|
|
return dx*dx + dz*dz
|
|
end
|
|
|
|
function TextAlert(unitDefID, x, y, z)
|
|
local currentFrame = spGetGameFrame()
|
|
if (lastWarning+warningDelay > currentFrame) then return end
|
|
lastWarning = currentFrame
|
|
spEcho("game_message: " .. Spring.Utilities.GetHumanName(UnitDefs[unitDefID]) .. " " .. under_attack_translation)
|
|
spSetLastMessagePosition(x, y, z)
|
|
end
|
|
|
|
local attackMarkers = {}
|
|
local tLastVoice = 0
|
|
local markerOffset = 2 -- Offset the marker Y coord by this much to avoid potential conflict with user-placed markers
|
|
|
|
local data = { -- [bSelf]
|
|
[false]={
|
|
voiceCooldown=options.voiceAllyCooldown.value,
|
|
voiceVolume=options.voiceAllyVolume.value,
|
|
markerD2=options.markersAllyRadius.value*options.markersAllyRadius.value,
|
|
markerPrefix={[false]="Allied Unit", [true]="Allied Base"},
|
|
markerDuration=options.markersAllyDuration.value,
|
|
},
|
|
[true]={
|
|
voiceCooldown=options.voiceSelfCooldown.value,
|
|
voiceVolume=options.voiceSelfVolume.value,
|
|
markerD2=options.markersSelfRadius.value*options.markersSelfRadius.value,
|
|
markerPrefix={[false]="Unit", [true]="Base"},
|
|
markerDuration=options.markersSelfDuration.value,
|
|
},
|
|
}
|
|
function UpdateOptions()
|
|
data = { -- [bSelf]
|
|
[false]={
|
|
voiceCooldown=options.voiceAllyCooldown.value,
|
|
voiceVolume=options.voiceAllyVolume.value,
|
|
markerD2=options.markersAllyRadius.value*options.markersAllyRadius.value,
|
|
markerPrefix={[false]="Allied Unit", [true]="Allied Base"},
|
|
markerDuration=options.markersAllyDuration.value,
|
|
},
|
|
[true]={
|
|
voiceCooldown=options.voiceSelfCooldown.value,
|
|
voiceVolume=options.voiceSelfVolume.value,
|
|
markerD2=options.markersSelfRadius.value*options.markersSelfRadius.value,
|
|
markerPrefix={[false]="Unit", [true]="Base"},
|
|
markerDuration=options.markersSelfDuration.value,
|
|
},
|
|
}
|
|
end
|
|
|
|
|
|
function MarkerAlert(unitTeam, unitDefID, x, y, z)
|
|
-- Assumptions for calling this function:
|
|
-- - Unit is allied to the player
|
|
-- - Unit is not on-screen for the player
|
|
y = y + markerOffset
|
|
local t = osClock()
|
|
local isBuilding = UnitDefs[unitDefID].isBuilding
|
|
local bSelf = (unitTeam == localTeamID)
|
|
|
|
-- Do voice alert if appropriate
|
|
if t > (tLastVoice + data[bSelf].voiceCooldown) then
|
|
spPlaySoundFile(voiceFilenames[bSelf][isBuilding], data[bSelf].voiceVolume, nil, nil, nil, nil, nil, nil, "userinterface")
|
|
spSetLastMessagePosition(x, y, z)
|
|
tLastVoice = t
|
|
end
|
|
|
|
-- Check if the new marker would be too close to any existing markers
|
|
for i=1,#attackMarkers do
|
|
local marker = attackMarkers[i]
|
|
local d2 = dist2(x, z, marker.x, marker.z)
|
|
if d2 < data[bSelf].markerD2 then return end
|
|
end
|
|
-- Place a marker
|
|
attackMarkers[#attackMarkers+1] = {x=x, y=y, z=z, tDeath=t+data[bSelf].markerDuration}
|
|
spMarkerAddPoint(x, y, z, data[bSelf].markerPrefix[isBuilding] .. " attacked", true)
|
|
spSetLastMessagePosition(x, y, z)
|
|
end
|
|
|
|
|
|
function CleanMarkers()
|
|
local t = osClock()
|
|
for i=#attackMarkers,1,-1 do
|
|
local m = attackMarkers[i]
|
|
if m.tDeath < t then
|
|
spMarkerErasePosition(m.x, m.y, m.z)
|
|
table.remove(attackMarkers, i)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local UPDATE_PERIOD = 0.25 -- Check to remove markers every 250ms
|
|
local timeframetimer = 0.0
|
|
function widget:Update(dt)
|
|
timeframetimer = timeframetimer + dt
|
|
if (timeframetimer > UPDATE_PERIOD) then
|
|
UpdateOptions() -- Couldn't find a suitable call-in for options being changed
|
|
CleanMarkers()
|
|
timeframetimer = 0
|
|
end
|
|
end
|
|
|
|
|
|
function widget:UnitDamaged(unitID, unitDefID, unitTeam, damage, paralyzer, weaponID, attackerID, attackerDefID, attackerTeam)
|
|
if damage <= 0 then return end
|
|
--spEcho("game_message: Processing damage event! " .. tostring(unitID) .. ", " .. tostring(unitDefID) .. ", " .. tostring(unitTeam) .. ", " .. tostring(attackerTeam) .. ", " .. tostring(attackerID));
|
|
if Spring.IsUnitInView(unitID) then return end
|
|
if not spAreTeamsAllied(localTeamID, unitTeam) then return end
|
|
if not unitTeam then return end -- I don't even?
|
|
if not unitDefID then return end -- Haven't encountered this being nil but I'm not risking it
|
|
if attackerTeam and spAreTeamsAllied(unitTeam, attackerTeam) then return end -- Get a lot of nil for attacker stats, probably from things out of vision/radar.
|
|
local x,y,z = spGetUnitPosition(unitID)
|
|
if not (x and y and z) then return end
|
|
TextAlert(unitDefID, x, y, z)
|
|
MarkerAlert(unitTeam, unitDefID, x, y, z)
|
|
end
|
|
|
|
|
|
function widget:Initialize()
|
|
if spGetSpectatingState() then
|
|
widgetHandler:RemoveWidget()
|
|
end
|
|
|
|
WG.InitializeTranslation(languageChanged, GetInfo().name)
|
|
end
|
|
|
|
|
|
function widget:Shutdown()
|
|
WG.ShutdownTranslation(GetInfo().name)
|
|
end
|
|
|
|
|
|
--changing teams, rejoin, becoming spec etc
|
|
function widget:PlayerChanged(playerID)
|
|
if spGetSpectatingState() then
|
|
--Spring.Echo("<Attack Warning>: Spectator mode. Widget removed.")
|
|
widgetHandler:RemoveWidget()
|
|
end
|
|
localTeamID = Spring.GetLocalTeamID()
|
|
end
|