Added marker and voice alerts for allied/owned units and buildings

This commit is contained in:
Luke Hubmayer-Werner 2020-06-07 22:17:59 +09:30
parent 250d614338
commit bca2153f53
1 changed files with 250 additions and 23 deletions

View File

@ -2,15 +2,134 @@ function widget:GetInfo()
return { return {
name = "Attack Warning", name = "Attack Warning",
desc = "Warns if stuff gets attacked", desc = "Warns if stuff gets attacked",
author = "knorke", author = "knorke, Birdulon",
date = "Oct 2011", date = "Oct 2011, June 2020",
license = "GNU GPL, v2 or later", license = "GNU GPL, v2 or later",
layer = 0, layer = 0,
enabled = true, enabled = true,
} }
end 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 spGetSpectatingState = Spring.GetSpectatingState
local spEcho = Spring.Echo
local spSetLastMessagePosition = Spring.SetLastMessagePosition
local warningDelay = 30 * 5 --in frames local warningDelay = 30 * 5 --in frames
local lastWarning = 0 --in frames local lastWarning = 0 --in frames
@ -21,23 +140,129 @@ local function languageChanged ()
under_attack_translation = WG.Translate("interface", "unit_under_attack") under_attack_translation = WG.Translate("interface", "unit_under_attack")
end 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) function widget:UnitDamaged(unitID, unitDefID, unitTeam, damage, paralyzer, weaponID, attackerID, attackerDefID, attackerTeam)
if damage <= 0 then return end if damage <= 0 then return end
local currentFrame = Spring.GetGameFrame () --spEcho("game_message: Processing damage event! " .. tostring(unitID) .. ", " .. tostring(unitDefID) .. ", " .. tostring(unitTeam) .. ", " .. tostring(attackerTeam) .. ", " .. tostring(attackerID));
if (lastWarning+warningDelay > currentFrame) then if Spring.IsUnitInView(unitID) then return end
return if not spAreTeamsAllied(localTeamID, unitTeam) then return end
end if not unitTeam then return end -- I don't even?
if (localTeamID==unitTeam and not Spring.IsUnitInView (unitID)) then if not unitDefID then return end -- Haven't encountered this being nil but I'm not risking it
lastWarning = currentFrame if attackerTeam and spAreTeamsAllied(unitTeam, attackerTeam) then return end -- Get a lot of nil for attacker stats, probably from things out of vision/radar.
Spring.Echo ("game_message: " .. Spring.Utilities.GetHumanName(UnitDefs[unitDefID]) .. " " .. under_attack_translation) local x,y,z = spGetUnitPosition(unitID)
--Spring.PlaySoundFile (blabla attack.wav, ... "userinterface") if not (x and y and z) then return end
local x,y,z = Spring.GetUnitPosition (unitID) TextAlert(unitDefID, x, y, z)
if (x and y and z) then MarkerAlert(unitTeam, unitDefID, x, y, z)
Spring.SetLastMessagePosition (x,y,z)
end
end
end end
function widget:Initialize() function widget:Initialize()
if spGetSpectatingState() then if spGetSpectatingState() then
widgetHandler:RemoveWidget() widgetHandler:RemoveWidget()
@ -46,10 +271,12 @@ function widget:Initialize()
WG.InitializeTranslation(languageChanged, GetInfo().name) WG.InitializeTranslation(languageChanged, GetInfo().name)
end end
function widget:Shutdown() function widget:Shutdown()
WG.ShutdownTranslation(GetInfo().name) WG.ShutdownTranslation(GetInfo().name)
end end
--changing teams, rejoin, becoming spec etc --changing teams, rejoin, becoming spec etc
function widget:PlayerChanged(playerID) function widget:PlayerChanged(playerID)
if spGetSpectatingState() then if spGetSpectatingState() then