2020-06-04 16:42:38 +09:30
function widget : GetInfo ( )
return {
name = " Attack Warning " ,
desc = " Warns if stuff gets attacked " ,
2020-06-07 22:17:59 +09:30
author = " knorke, Birdulon " ,
date = " Oct 2011, June 2020 " ,
2020-06-04 16:42:38 +09:30
license = " GNU GPL, v2 or later " ,
layer = 0 ,
enabled = true ,
}
end
2020-06-07 22:17:59 +09:30
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 ,
} ,
}
2020-06-04 16:42:38 +09:30
2020-06-07 22:17:59 +09:30
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 ( )
2020-06-04 16:42:38 +09:30
local under_attack_translation
2020-06-07 22:17:59 +09:30
local function languageChanged ( )
under_attack_translation = WG.Translate ( " interface " , " unit_under_attack " )
2020-06-04 16:42:38 +09:30
end
2020-06-07 22:17:59 +09:30
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
2020-06-04 16:42:38 +09:30
end
2020-06-07 22:17:59 +09:30
-- 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 )
2020-06-04 16:42:38 +09:30
end
end
end
2020-06-07 22:17:59 +09:30
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
2020-06-04 16:42:38 +09:30
function widget : Initialize ( )
if spGetSpectatingState ( ) then
widgetHandler : RemoveWidget ( )
end
2020-06-07 22:17:59 +09:30
WG.InitializeTranslation ( languageChanged , GetInfo ( ) . name )
2020-06-04 16:42:38 +09:30
end
2020-06-07 22:17:59 +09:30
2020-06-04 16:42:38 +09:30
function widget : Shutdown ( )
WG.ShutdownTranslation ( GetInfo ( ) . name )
end
2020-06-07 22:17:59 +09:30
2020-06-04 16:42:38 +09:30
--changing teams, rejoin, becoming spec etc
2020-06-07 22:17:59 +09:30
function widget : PlayerChanged ( playerID )
2020-06-04 16:42:38 +09:30
if spGetSpectatingState ( ) then
--Spring.Echo("<Attack Warning>: Spectator mode. Widget removed.")
widgetHandler : RemoveWidget ( )
end
2020-06-07 22:17:59 +09:30
localTeamID = Spring.GetLocalTeamID ( )
2020-06-04 16:42:38 +09:30
end