mirror of
https://github.com/weyne85/DML.git
synced 2025-10-29 16:57:49 +00:00
Version 2.3.1
- Code hardening - Experimental: inferno and airtank modules
This commit is contained in:
parent
f7a8705aa5
commit
695303940f
Binary file not shown.
Binary file not shown.
@ -560,6 +560,10 @@ end
|
||||
-- Start
|
||||
--
|
||||
function FARPZones.releaseFARPS()
|
||||
if FARPZones.verbose then
|
||||
trigger.action.outText("FARPz: releasing FARP ownership to mission in progress", 30)
|
||||
end
|
||||
|
||||
for idx, aFarp in pairs(FARPZones.lockup) do
|
||||
aFarp:autoCapture(true)
|
||||
end
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
LZ = {}
|
||||
LZ.version = "1.1.0"
|
||||
LZ.version = "1.2.1"
|
||||
LZ.verbose = false
|
||||
LZ.ups = 1
|
||||
LZ.requiredLibs = {
|
||||
@ -15,7 +15,8 @@ LZ.LZs = {}
|
||||
Version History
|
||||
1.0.0 - initial version
|
||||
1.1.0 - persistence
|
||||
|
||||
1.2.0 - dcs 2024-07-11 and dcs 2024-07-22 updates (new events)
|
||||
1.2.1 - theZone --> aZone typo at input management
|
||||
--]]--
|
||||
|
||||
function LZ.addLZ(theZone)
|
||||
@ -216,7 +217,9 @@ function LZ:onEvent(event)
|
||||
|
||||
-- only interested in S_EVENT_TAKEOFF and events
|
||||
if event.id ~= world.event.S_EVENT_TAKEOFF and
|
||||
event.id ~= world.event.S_EVENT_LAND then
|
||||
event.id ~= world.event.S_EVENT_LAND and
|
||||
event.id ~= world.event.S_EVENT_RUNWAY_TAKEOFF and
|
||||
event.id ~= world.event.S_EVENT_RUNWAY_TOUCH then
|
||||
return
|
||||
end
|
||||
|
||||
@ -231,14 +234,20 @@ function LZ:onEvent(event)
|
||||
-- see if this unit interests us at all
|
||||
if LZ.unitIsInterestingForZone(theUnit, aZone) then
|
||||
-- interesting unit in zone triggered the event
|
||||
if aZone.lzDeparted and event.id == world.event.S_EVENT_TAKEOFF then
|
||||
if aZone.lzDeparted and
|
||||
(event.id == world.event.S_EVENT_TAKEOFF or
|
||||
event.id == world.event.S_EVENT_RUNWAY_TAKEOFF)
|
||||
then
|
||||
if LZ.verbose or aZone.verbose then
|
||||
trigger.action.outText("+++LZ: detected departure from <" .. aZone.name .. ">", 30)
|
||||
end
|
||||
cfxZones.pollFlag(aZone.lzDeparted, aZone.lzMethod, aZone)
|
||||
end
|
||||
|
||||
if aZone.lzLanded and event.id == world.event.S_EVENT_LAND then
|
||||
if aZone.lzLanded and
|
||||
(event.id == world.event.S_EVENT_LAND or
|
||||
event.id == world.event.S_EVENT_RUNWAY_TOUCH)
|
||||
then
|
||||
if LZ.verbose or aZone.verbose then
|
||||
trigger.action.outText("+++LZ: detected landing in <" .. aZone.name .. ">", 30)
|
||||
end
|
||||
@ -264,14 +273,14 @@ function LZ.update()
|
||||
for idx, aZone in pairs(LZ.LZs) do
|
||||
-- see if we are being paused or unpaused
|
||||
if cfxZones.testZoneFlag(aZone, aZone.lzPause, aZone.LZTriggerMethod, "lzLastPause") then
|
||||
if LZ.verbose or theZone.verbose then
|
||||
if LZ.verbose or aZone.verbose then
|
||||
trigger.action.outText("+++LZ: triggered pause? for <".. aZone.name ..">", 30)
|
||||
end
|
||||
aZone.isPaused = true
|
||||
end
|
||||
|
||||
if cfxZones.testZoneFlag(aZone, aZone.lzContinue, aZone.LZTriggerMethod, "lzLastContinue") then
|
||||
if LZ.verbose or theZone.verbose then
|
||||
if LZ.verbose or aZone.verbose then
|
||||
trigger.action.outText("+++LZ: triggered continue? for <".. aZone.name ..">", 30)
|
||||
end
|
||||
aZone.isPaused = false
|
||||
|
||||
631
modules/airtank.lua
Normal file
631
modules/airtank.lua
Normal file
@ -0,0 +1,631 @@
|
||||
airtank = {}
|
||||
airtank.version = "0.9.9"
|
||||
-- Module to extinguish fires controlled by the 'inferno' module.
|
||||
-- For 'airtank' fire extinguisher aircraft modules.
|
||||
airtank.requiredLibs = {
|
||||
"dcsCommon",
|
||||
"cfxZones",
|
||||
}
|
||||
airtank.tanks = {} -- player data by GROUP name, will break with multi-unit groups
|
||||
-- tank attributes
|
||||
-- pumpArmed -- for hovering fills.
|
||||
-- armed -- for triggering drops below trigger alt-
|
||||
-- dropping -- if drop has triggered
|
||||
-- carrying -- how mach retardant / fluid am I carrying?
|
||||
-- capacity -- how much can I carry
|
||||
|
||||
-- uName
|
||||
-- pName
|
||||
-- theUnit
|
||||
-- gID
|
||||
-- lastDeparture -- timestamp to avoid double notifications
|
||||
-- lastLanding -- timestamp to avoid double dip
|
||||
|
||||
airtank.zones = {} -- come here to refill your tanks
|
||||
|
||||
airtank.roots = {} -- roots for player by group name
|
||||
airtank.mainMenu = nil -- handles attachTo:
|
||||
|
||||
airtank.types = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D", "CH-47Fbl1"} -- which helicopters/planes can firefight and get menu. can be amended with config and zone
|
||||
airtank.capacities = {
|
||||
["UH-1H"] = 1200,
|
||||
["Mi-8MT"] = 4000,
|
||||
["Mi-24P"] = 2400,
|
||||
["CH-47Fbl1"] = 9000,
|
||||
["OH58D"] = 500,
|
||||
} -- how much each helo can carry. default is 500kg, can be amended with zone
|
||||
airtank.pumpSpeed = 100 -- liters/kg per second, for all helos same
|
||||
airtank.dropSpeed = 1000 -- liters per second, for all helos same
|
||||
airtank.releaseAlt = 100 -- m = 90 ft
|
||||
airtank.pumpAlt = 10 -- m = 30 feet
|
||||
--
|
||||
-- airtank zones - land inside to refill. can have limited capa
|
||||
--
|
||||
function airtank.addZone(theZone)
|
||||
airtank.zones[theZone.name] = theZone
|
||||
end
|
||||
|
||||
function airtank.readZone(theZone)
|
||||
theZone.capacity = theZone:getNumberFromZoneProperty("airtank", 99999999) -- should be enough
|
||||
theZone.amount = theZone:getNumberFromZoneProperty("amount", theZone.capacity)
|
||||
end
|
||||
|
||||
function airtank.refillWithZone(theZone, data)
|
||||
local theUnit = data.theUnit
|
||||
local wanted = data.capacity - data.carrying
|
||||
if theZone.amount > wanted then
|
||||
theZone.amount = theZone.amount - wanted
|
||||
data.carrying = data.capacity
|
||||
trigger.action.outTextForGroup(data.gID, "Roger, " .. data.uName .. ", topped up tanks, you now carry " .. data.carrying .. "kg of flame retardant.", 30)
|
||||
trigger.action.outSoundForGroup(data.gID, airtank.actionSound)
|
||||
trigger.action.setUnitInternalCargo(data.uName, data.carrying + 10)
|
||||
return true
|
||||
end
|
||||
trigger.action.outTextForGroup(data.gID, "Negative, " .. data.uName .. ", out of flame retardant.", 30)
|
||||
return false
|
||||
end
|
||||
|
||||
--
|
||||
-- event handling
|
||||
--
|
||||
function airtank:onEvent(theEvent)
|
||||
-- catch birth events of helos
|
||||
if not theEvent then return end
|
||||
local theUnit = theEvent.initiator
|
||||
if not theUnit then return end
|
||||
if not theUnit.getPlayerName then return end
|
||||
local pName = theUnit:getPlayerName()
|
||||
if not pName then return end
|
||||
-- we have a player unit
|
||||
if not dcsCommon.unitIsOfLegalType(theUnit, airtank.types) then
|
||||
if airtank.verbose then
|
||||
trigger.action.outText("aTnk: unit <" .. theUnit:getName() .. ">, type <" .. theUnit:getTypeName() .. "> not an airtank.", 30)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local uName = theUnit:getName()
|
||||
local uType = theUnit:getTypeName()
|
||||
local theGroup = theUnit:getGroup()
|
||||
local gName = theGroup:getName()
|
||||
|
||||
if theEvent.id == 15 then -- birth
|
||||
-- make sure this aircraft is legit
|
||||
airtank.installMenusForUnit(theUnit)
|
||||
local theData = airtank.newData(theUnit)
|
||||
theData.pName = pName
|
||||
airtank.tanks[gName] = theData
|
||||
if airtank.verbose then
|
||||
trigger.action.outText("+++aTnk: new player airtank <" .. uName .. "> type <" .. uType .. "> for <" .. pName .. ">", 30)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if theEvent.id == 4 or -- land
|
||||
theEvent.id == 55 -- runway touch
|
||||
then
|
||||
-- see if landed inside a refill zone and
|
||||
-- automatically top off if pumpArmed
|
||||
local data = airtank.tanks[gName]
|
||||
if data and data.lastLanding then return end -- no double dip
|
||||
if data and data.pumpArmed then
|
||||
data.lastLanding = timer.getTime()
|
||||
data.lastDeparture = nil
|
||||
local p = theUnit:getPoint()
|
||||
for idx, theZone in pairs (airtank.zones) do
|
||||
if theZone:pointInZone(p) then
|
||||
if airtank.refillWithZone(theZone, data) then
|
||||
data.armed = false
|
||||
data.pumpArmed = false
|
||||
return
|
||||
end -- if refill received
|
||||
end -- if in zone
|
||||
end -- for zones
|
||||
elseif data then
|
||||
data.lastLanding = timer.getTime()
|
||||
data.lastDeparture = nil
|
||||
local p = theUnit:getPoint()
|
||||
for idx, theZone in pairs (airtank.zones) do
|
||||
if theZone:pointInZone(p) then
|
||||
trigger.action.outTextForGroup(data.gID, "Welcome to " .. theZone.name .. ", " .. pName .. ", firefighting services are available.", 30)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if theEvent.id == 3 or -- takeoff
|
||||
theEvent.id == 54 -- runway takeoff
|
||||
then
|
||||
local data = airtank.tanks[gName]
|
||||
local now = timer.getTime()
|
||||
if data then
|
||||
data.lastLanding = nil
|
||||
-- suppress double take-off notifications for 20 seconds
|
||||
if data.lastDeparture then -- and data.lastDeparture + 60 < now then
|
||||
return
|
||||
end
|
||||
data.lastDeparture = now
|
||||
if data.carrying < data.capacity * 0.5 then
|
||||
trigger.action.outTextForGroup(data.gID, "Good luck, " .. pName .. ", remember to top off your tanks before going in.", 30)
|
||||
else
|
||||
trigger.action.outTextForGroup(data.gID, "Good luck and godspeed, " .. pName .. "!", 30)
|
||||
end
|
||||
trigger.action.outSoundForGroup(data.gID, airtank.actionSound)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
function airtank.newData(theUnit)
|
||||
local theType = theUnit:getTypeName()
|
||||
local data = {}
|
||||
local capa = airtank.capacities[theType]
|
||||
if not capa then capa = 500 end -- default capa.
|
||||
data.capacity = capa
|
||||
data.carrying = 0
|
||||
data.pumpArmed = false
|
||||
data.armed = false
|
||||
data.dropping = false
|
||||
data.uName = theUnit:getName()
|
||||
data.pName = theUnit:getPlayerName()
|
||||
data.theUnit = theUnit
|
||||
local theGroup = theUnit:getGroup()
|
||||
data.gID = theGroup:getID()
|
||||
trigger.action.setUnitInternalCargo(data.uName, data.carrying + 10)
|
||||
return data
|
||||
end
|
||||
--
|
||||
-- comms
|
||||
--
|
||||
function airtank.installMenusForUnit(theUnit) -- assumes all unfit types are weeded out
|
||||
-- if already exists, remove old
|
||||
if not theUnit then return end
|
||||
if not Unit.isExist(theUnit) then return end
|
||||
local theGroup = theUnit:getGroup()
|
||||
local uName = theUnit:getName()
|
||||
local uType = theUnit:getTypeName()
|
||||
local pName = theUnit:getPlayerName()
|
||||
local gName = theGroup:getName()
|
||||
local gID = theGroup:getID()
|
||||
local pRoot = airtank.roots[gName]
|
||||
if pRoot then
|
||||
missionCommands.removeItemForGroup(gID, pRoot)
|
||||
end
|
||||
-- now add the airtank menu
|
||||
pRoot = missionCommands.addSubMenuForGroup(gID, airtank.menuName, airtank.mainMenu)
|
||||
airtank.roots[gName] = pRoot -- save for later
|
||||
local args = {gName, uName, gID, uType, pName}
|
||||
-- menus:
|
||||
-- status: loaded, capa, armed etc
|
||||
-- ready pump -- turn off drop system and start sucking, if landing in zone will fully charge, else suck in water when hovering over water when low enough (below 10m)
|
||||
-- arm release -- get ready to drop. auto-release when alt is below 30m
|
||||
local m1 = missionCommands.addCommandForGroup(gID , "Tank Status" , pRoot, airtank.redirectStatus, args)
|
||||
local mx2 = missionCommands.addCommandForGroup(gID , "MANUAL RELEASE" , pRoot, airtank.redirectManDrop, args)
|
||||
local m2 = missionCommands.addCommandForGroup(gID , "*Arm*AUTODROP*trigger" , pRoot, airtank.redirectArmDrop, args)
|
||||
local m3 = missionCommands.addCommandForGroup(gID , "Activate/Ready intake" , pRoot, airtank.redirectArmPump, args)
|
||||
local m4 = missionCommands.addCommandForGroup(gID , "Secure ship" , pRoot, airtank.redirectSecure, args)
|
||||
end
|
||||
|
||||
function airtank.redirectStatus(args)
|
||||
timer.scheduleFunction(airtank.doStatus, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function airtank.doStatus(args)
|
||||
local gName = args[1]
|
||||
local uName = args[2]
|
||||
local gID = args[3]
|
||||
local uType = args[4]
|
||||
local pName = args[5]
|
||||
local ralm = airtank.releaseAlt
|
||||
local ralf = math.floor(ralm * 3.28084)
|
||||
local data = airtank.tanks[gName]
|
||||
local remains = data.capacity - data.carrying
|
||||
local msg = "\nAirtank <" .. uName .. "> (" .. uType .. "), commanded by " .. pName .. "\n capacity: " .. data.capacity .. "kg, carrying " .. data.carrying .. "kg (free " .. remains .. "kg)"
|
||||
-- add info to nearest refuel zone?
|
||||
if data.armed then msg = msg .. "\n\n *** RELEASE TRIGGER ARMED (" .. ralm .. "m/" .. ralf .. "ft AGL)***" end
|
||||
ralm = airtank.pumpAlt
|
||||
ralf = math.floor(ralm * 3.28084)
|
||||
if data.pumpArmed then msg = msg .. "\n\n --- intake pumps ready (below " .. ralm .. "m/" .. ralf .. "ft AGL)" end
|
||||
msg = msg .. "\n"
|
||||
trigger.action.outTextForGroup(gID, msg, 30)
|
||||
end
|
||||
|
||||
|
||||
function airtank.redirectManDrop(args)
|
||||
timer.scheduleFunction(airtank.doManDrop, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function airtank.doManDrop(args)
|
||||
local gName = args[1]
|
||||
local uName = args[2]
|
||||
local theUnit = Unit.getByName(uName)
|
||||
local gID = args[3]
|
||||
local uType = args[4]
|
||||
local pName = args[5]
|
||||
local data = airtank.tanks[gName]
|
||||
local remains = data.capacity - data.carrying
|
||||
local alt = dcsCommon.getUnitAGL(theUnit)
|
||||
local ralm = math.floor(alt)
|
||||
local ralf = math.floor(ralm * 3.28084)
|
||||
local msg = ""
|
||||
local agl = dcsCommon.getUnitAGL(theUnit)
|
||||
if not theUnit:inAir() then
|
||||
trigger.action.outTextForGroup(data.gID, "Please get into the air before releasing flame retardant.", 30)
|
||||
trigger.action.outSoundForGroup(data.gID, airtank.actionSound)
|
||||
return
|
||||
end
|
||||
if data.carrying < 1 then
|
||||
msg = "\nRetard tanks empty. Your " .. uType .. " can carry up to " .. data.capacity .. "kg.\n"
|
||||
data.armed = false
|
||||
data.dropping = false
|
||||
data.pumpArmed = false
|
||||
elseif data.carrying < 100 then
|
||||
msg = "\nTanks empty (" .. data.carrying .. "kg left), safeties are engaged.\n"
|
||||
data.armed = false
|
||||
data.dropping = false
|
||||
data.pumpArmed = false
|
||||
else
|
||||
msg = "\n *** Opened drop valve at " .. ralm .. "m/" .. ralf .. "ft RALT.\n"
|
||||
data.armed = false
|
||||
data.pumpArmed = false
|
||||
data.dropResults = {}
|
||||
data.dropping = true
|
||||
trigger.action.outTextForGroup(gID, msg, 30)
|
||||
trigger.action.outSoundForGroup(gID, airtank.actionSound)
|
||||
airtank.dropFor(data)
|
||||
return
|
||||
end
|
||||
trigger.action.outTextForGroup(gID, msg, 30)
|
||||
trigger.action.outSoundForGroup(gID, airtank.actionSound)
|
||||
end
|
||||
|
||||
|
||||
function airtank.redirectArmDrop(args)
|
||||
timer.scheduleFunction(airtank.doArmDrop, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function airtank.doArmDrop(args)
|
||||
local gName = args[1]
|
||||
local uName = args[2]
|
||||
local theUnit = Unit.getByName(uName)
|
||||
local gID = args[3]
|
||||
local uType = args[4]
|
||||
local pName = args[5]
|
||||
local data = airtank.tanks[gName]
|
||||
local remains = data.capacity - data.carrying
|
||||
local ralm = airtank.releaseAlt
|
||||
local ralf = math.floor(ralm * 3.28084)
|
||||
local msg = ""
|
||||
local agl = dcsCommon.getUnitAGL(theUnit)
|
||||
if data.carrying < 1 then
|
||||
msg = "\nRetard tanks empty. Your " .. uType .. " can carry up to " .. data.capacity .. "kg.\n"
|
||||
data.armed = false
|
||||
data.dropping = false
|
||||
data.pumpArmed = false
|
||||
elseif agl < airtank.releaseAlt then
|
||||
msg = "Get above " .. ralm .. "m/" .. ralf .. "ft ALG (radar) to arm trigger."
|
||||
elseif data.carrying < 100 then
|
||||
msg = "\nTank empty (" .. data.carrying .. "kg left), safeties are engaged.\n"
|
||||
data.armed = false
|
||||
data.dropping = false
|
||||
data.pumpArmed = false
|
||||
else
|
||||
msg = "\n *** Release valve primed to trigger below " .. ralm .. "m/" .. ralf .. "ft RALT.\n\nRelease starts automatically at or below trigger altitude.\n"
|
||||
data.armed = true
|
||||
data.dropping = false
|
||||
data.pumpArmed = false
|
||||
end
|
||||
trigger.action.outTextForGroup(gID, msg, 30)
|
||||
trigger.action.outSoundForGroup(gID, airtank.actionSound)
|
||||
end
|
||||
|
||||
function airtank.redirectArmPump(args)
|
||||
timer.scheduleFunction(airtank.doArmPump, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function airtank.doArmPump(args)
|
||||
local gName = args[1]
|
||||
local uName = args[2]
|
||||
local theUnit = Unit.getByName(uName)
|
||||
local gID = args[3]
|
||||
local uType = args[4]
|
||||
local pName = args[5]
|
||||
local data = airtank.tanks[gName]
|
||||
local remains = data.capacity - data.carrying
|
||||
local ralm = airtank.pumpAlt
|
||||
local ralf = math.floor(ralm * 3.28084)
|
||||
local msg = ""
|
||||
-- if we are on the ground, check if we are inside a
|
||||
-- zone that can refill us
|
||||
if not theUnit:inAir() then
|
||||
local p = theUnit:getPoint()
|
||||
for idx, theZone in pairs (airtank.zones) do
|
||||
if theZone:pointInZone(p) then
|
||||
if airtank.refillWithZone(theZone, data) then
|
||||
data.armed = false
|
||||
data.pumpArmed = false
|
||||
data.dropping = false
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
msg = "\n *** Intake valves ready, descend to " .. ralm .. "m/" .. ralf .. "ft RALT over water, or land at a firefighting supply base.\n"
|
||||
data.armed = false
|
||||
data.dropping = false
|
||||
data.pumpArmed = true
|
||||
|
||||
trigger.action.outTextForGroup(gID, msg, 30)
|
||||
trigger.action.outSoundForGroup(gID, airtank.actionSound)
|
||||
end
|
||||
|
||||
function airtank.redirectSecure(args)
|
||||
timer.scheduleFunction(airtank.doSecure, args, timer.getTime() + 0.1)
|
||||
end
|
||||
|
||||
function airtank.doSecure(args)
|
||||
local gName = args[1]
|
||||
local gID = args[3]
|
||||
local data = airtank.tanks[gName]
|
||||
local msg = ""
|
||||
msg = "\n All valves secure and stored for cruise operation\n"
|
||||
data.armed = false
|
||||
data.dropping = false
|
||||
data.pumpArmed = false
|
||||
trigger.action.outTextForGroup(gID, msg, 30)
|
||||
trigger.action.outSoundForGroup(gID, airtank.actionSound)
|
||||
end
|
||||
|
||||
--
|
||||
-- update
|
||||
--
|
||||
function airtank.dropFor(theData) -- drop onto ground/fire
|
||||
local theUnit = theData.theUnit
|
||||
local qty = airtank.dropSpeed
|
||||
if qty > theData.carrying then qty = math.floor(theData.carrying) end
|
||||
-- calculate position where it will hit
|
||||
local alt = dcsCommon.getUnitAGL(theUnit)
|
||||
if alt < 0 then alt = 0 end
|
||||
local vel = theUnit:getVelocity() -- vec 3, we only need x and z to project the point where the water will impact (we ignore vel.z)
|
||||
-- calculation: agl=height, no downward vel, will accelerate at G= 10 m/ss
|
||||
-- i.e. t = sqrt(2*agl/10)
|
||||
local agl = dcsCommon.getUnitAGL(theUnit)
|
||||
local t = math.sqrt(0.2*agl)
|
||||
local p = theUnit:getPoint()
|
||||
local impact = {x = p.x + t * vel.x, y = 0, z = p.z + t * vel.z}
|
||||
|
||||
-- tell inferno about it, get some feedback
|
||||
if inferno.waterDropped then
|
||||
local diag = inferno.waterDropped(impact, qty, theData)
|
||||
if diag then table.insert(theData.dropResults, diag) end
|
||||
else
|
||||
trigger.action.outText("WARNING: airtank can't find 'inferno' module.", 30)
|
||||
return
|
||||
end
|
||||
|
||||
-- update what we have left in tank
|
||||
theData.carrying = theData.carrying - qty
|
||||
if theData.carrying < 0 then theData.carrying = 0 end
|
||||
local ralm = math.floor(agl)
|
||||
local ralf = math.floor(ralm * 3.28084)
|
||||
|
||||
local msg = "Dropping " .. qty .. "kg at RALT " .. ralm .. "m/" .. ralf .. "ft, " .. theData.carrying .. " kg remaining"
|
||||
local snd = airtank.releaseSound
|
||||
|
||||
-- close vent if empty
|
||||
if theData.carrying < 100 then
|
||||
-- close hatch
|
||||
theData.dropping = false
|
||||
theData.armed = false
|
||||
msg = msg .. ", CLOSING VENTS\n\n"
|
||||
snd = airtank.actionSound
|
||||
-- add all drop diagnoses
|
||||
if #theData.dropResults < 1 then
|
||||
msg = msg .. "No discernible results.\n"
|
||||
else
|
||||
msg = msg .. "Good delivey:\n"
|
||||
for idx, res in pairs(theData.dropResults) do
|
||||
msg = msg .. " - " .. res .. "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
-- set internal cargo
|
||||
trigger.action.setUnitInternalCargo(theData.uName, theData.carrying + 10)
|
||||
-- say how much we dropped and (if so) what we are over
|
||||
trigger.action.outTextForGroup(theData.gID, msg, 30, true)
|
||||
trigger.action.outSoundForGroup(theData.gID, snd)
|
||||
end
|
||||
|
||||
function airtank.updateDataFor(theData)
|
||||
local theUnit = theData.theUnit
|
||||
-- see if we are dropping
|
||||
if theData.dropping then
|
||||
-- valve is open
|
||||
airtank.dropFor(theData) -- drop contents of tank, sets weight
|
||||
elseif theData.armed then
|
||||
-- see if we are below 10*trigger
|
||||
local alt = dcsCommon.getUnitAGL(theUnit)
|
||||
if alt < 10 * airtank.releaseAlt then
|
||||
-- see if we trigger
|
||||
if alt <= airtank.releaseAlt then
|
||||
-- !! trigger flow
|
||||
theData.dropResults = {}
|
||||
theData.dropping = true
|
||||
-- trigger.action.outText("allocated dropResults", 30)
|
||||
theData.armed = false
|
||||
airtank.dropFor(theData) -- sets weight
|
||||
trigger.action.outSoundForGroup(theData.gID, airtank.releaseSound)
|
||||
else
|
||||
-- flash current alt and say when we will trigger
|
||||
local calm = math.floor(alt)
|
||||
local calf = math.floor(calm * 3.28084)
|
||||
local ralm = airtank.releaseAlt
|
||||
local ralf = math.floor(ralm * 3.28084)
|
||||
trigger.action.outTextForGroup(theData.gID, "Current RALT " .. calm .. "m/" .. calf .. "ft, will release at " .. ralm .. "m/" .. ralf .. "ft", 30, true) -- erase all
|
||||
trigger.action.outSoundForGroup(theData.gID, airtank.blipSound)
|
||||
end
|
||||
end
|
||||
-- see if the intake valve is open
|
||||
elseif theData.pumpArmed then
|
||||
p = theUnit:getPoint()
|
||||
local sType = land.getSurfaceType({x=p.x, y=p.z})
|
||||
if sType ~= 2 and sType ~= 3 then -- not over water
|
||||
return
|
||||
end
|
||||
local alt = dcsCommon.getUnitAGL(theUnit)
|
||||
local calm = math.floor(alt)
|
||||
local calf = math.floor(calm * 3.28084)
|
||||
if alt < 5 * airtank.pumpAlt then
|
||||
local msg = "RALT " .. calm .. "m/" .. calf .. "ft, "
|
||||
if alt <= airtank.pumpAlt then -- in pump range
|
||||
theData.carrying = theData.carrying + airtank.pumpSpeed
|
||||
if theData.carrying > theData.capacity then theData.carrying = theData.capacity end
|
||||
trigger.action.setUnitInternalCargo(theData.uName, theData.carrying + 10)
|
||||
end
|
||||
msg = msg .. theData.carrying .. "/" .. theData.capacity .. "kg"
|
||||
if theData.carrying >= theData.capacity - 50 then
|
||||
theData.pumpArmed = false
|
||||
msg = msg .. " PUMP DISENGAGED"
|
||||
end
|
||||
trigger.action.outTextForGroup(theData.gID, msg, 30, true)
|
||||
trigger.action.outSoundForGroup(theData.gID, airtank.pumpSound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function airtank.update() -- update all firefighters
|
||||
timer.scheduleFunction(airtank.update, {}, timer.getTime() + airtank.ups) -- next time
|
||||
local filtered = {} -- filter existing only
|
||||
for gName, data in pairs(airtank.tanks) do
|
||||
local theUnit = data.theUnit
|
||||
if Unit.isExist(theUnit) then
|
||||
if theUnit:inAir() then
|
||||
airtank.updateDataFor(data)
|
||||
end
|
||||
filtered[gName] = data
|
||||
end
|
||||
end
|
||||
airtank.tanks = filtered
|
||||
end
|
||||
|
||||
--
|
||||
-- start and config
|
||||
--
|
||||
function airtank.readConfigZone()
|
||||
airtank.name = "airtankConfig" -- make compatible with dml zones
|
||||
local theZone = cfxZones.getZoneByName("airtankConfig")
|
||||
if not theZone then
|
||||
theZone = cfxZones.createSimpleZone("airtankConfig")
|
||||
end
|
||||
|
||||
airtank.verbose = theZone.verbose
|
||||
airtank.ups = theZone:getNumberFromZoneProperty("ups", 1)
|
||||
airtank.menuName = theZone:getStringFromZoneProperty("menuName", "Firefighting")
|
||||
|
||||
airtank.actionSound = theZone:getStringFromZoneProperty("actionSound", "none")
|
||||
airtank.blipSound = theZone:getStringFromZoneProperty("blipSound", airtank.actionSound)
|
||||
airtank.releaseSound = theZone:getStringFromZoneProperty("releaseSound", airtank.actionSound)
|
||||
airtank.pumpSound = theZone:getStringFromZoneProperty("pumpSound", airtank.actionSound)
|
||||
|
||||
if theZone:hasProperty("attachTo:") then
|
||||
local attachTo = theZone:getStringFromZoneProperty("attachTo:", "<none>")
|
||||
if radioMenu then -- requires optional radio menu to have loaded
|
||||
local mainMenu = radioMenu.mainMenus[attachTo]
|
||||
if mainMenu then
|
||||
airtank.mainMenu = mainMenu
|
||||
else
|
||||
trigger.action.outText("+++airtank: cannot find super menu <" .. attachTo .. ">", 30)
|
||||
end
|
||||
else
|
||||
trigger.action.outText("+++airtank: REQUIRES radioMenu to run before inferno. 'AttachTo:' ignored.", 30)
|
||||
end
|
||||
end
|
||||
|
||||
-- add own troop carriers
|
||||
if theZone:hasProperty("airtanks") then
|
||||
local tc = theZone:getStringFromZoneProperty("airtanks", "UH-1D")
|
||||
tc = dcsCommon.splitString(tc, ",")
|
||||
airtank.types = dcsCommon.trimArray(tc)
|
||||
if airtank.verbose then
|
||||
trigger.action.outText("+++aTnk: redefined air tanks to types:", 30)
|
||||
for idx, aType in pairs(airtank.types) do
|
||||
trigger.action.outText(aType, 30)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- add capacities and types from airTankSpecs zone
|
||||
local capaZone = cfxZones.getZoneByName("airTankSpecs")
|
||||
if capaZone then
|
||||
if airtank.verbose then
|
||||
trigger.action.outText("aTnk: found and processing 'airTankSpecs' zone data.", 30)
|
||||
end
|
||||
-- read all into my types registry, replacing whatever is there
|
||||
local rawCapa = cfxZones.getAllZoneProperties(capaZone)
|
||||
local newCapas = airtank.processCapas(rawCapa)
|
||||
-- now types to existing types if not already there
|
||||
for aType, aCapa in pairs(newCapas) do
|
||||
airtank.capacities[aType] = aCapa
|
||||
dcsCommon.addToTableIfNew(airtank.types, aType)
|
||||
if civAir.verbose then
|
||||
trigger.action.outText("+++aTnk: processed aircraft <" .. aType .. "> for capacity <" .. aCapa .. ">", 30)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function airtank.processCapas(rawIn)
|
||||
local newCapas = {}
|
||||
-- now iterate the input table, and generate new types and
|
||||
-- liveries from it
|
||||
for theType, capa in pairs (rawIn) do
|
||||
if airtank.verbose then
|
||||
trigger.action.outText("+++aTnk: processing type <" .. theType .. ">:<" .. capa .. ">", 30)
|
||||
end
|
||||
local pcapa = tonumber(capa)
|
||||
if not pcapa then capa = 0 end
|
||||
newCapas[theType] = pcapa
|
||||
end
|
||||
|
||||
return newCapas
|
||||
end
|
||||
|
||||
|
||||
function airtank.start()
|
||||
if not dcsCommon.libCheck then
|
||||
trigger.action.outText("cfx airtank requires dcsCommon", 30)
|
||||
return false
|
||||
end
|
||||
if not dcsCommon.libCheck("cfx airtank", airtank.requiredLibs) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- read config
|
||||
airtank.readConfigZone()
|
||||
|
||||
-- process airtank Zones
|
||||
local attrZones = cfxZones.getZonesWithAttributeNamed("airtank")
|
||||
for k, aZone in pairs(attrZones) do
|
||||
airtank.readZone(aZone) -- process attributes
|
||||
airtank.addZone(aZone) -- add to list
|
||||
end
|
||||
|
||||
-- connect event handler
|
||||
world.addEventHandler(airtank)
|
||||
|
||||
-- start update
|
||||
timer.scheduleFunction(airtank.update, {}, timer.getTime() + 1/airtank.ups)
|
||||
|
||||
-- say Hi!
|
||||
trigger.action.outText("cf/x airtank v" .. airtank.version .. " started.", 30)
|
||||
return true
|
||||
end
|
||||
|
||||
if not airtank.start() then
|
||||
trigger.action.outText("airtank failed to start up")
|
||||
airtank = nil
|
||||
end
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
civHelo = {}
|
||||
civHelo.version = "1.0.0"
|
||||
civHelo.version = "1.0.1"
|
||||
civHelo.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
"cfxZones",
|
||||
@ -7,7 +7,7 @@ civHelo.requiredLibs = {
|
||||
--[[--
|
||||
Version History
|
||||
1.0.0 - Initial version
|
||||
|
||||
1.0.1 - livery hardening
|
||||
--]]--
|
||||
|
||||
civHelo.flights = {} -- currently active flights
|
||||
@ -282,10 +282,24 @@ function civHelo.getType(theZone)
|
||||
end
|
||||
|
||||
function civHelo.getLiveryForType(theType, theData)
|
||||
-- trigger.action.outText("picking liviery for helo type <" .. theType .. ">", 30)
|
||||
if civHelo.liveries[theType] then
|
||||
local available = civHelo.liveries[theType]
|
||||
local chosen = dcsCommon.pickRandom(available)
|
||||
if available then
|
||||
-- trigger.action.outText("We have a livery selection available for helo", 30)
|
||||
else
|
||||
-- trigger.action.outText("No liveries for type.", 30)
|
||||
end
|
||||
local chosen = dcsCommon.pickRandom(available)
|
||||
if chosen then
|
||||
-- trigger.action.outText("a fine livery choice: <" .. chosen .. "> for type <" .. theType .. ">", 30)
|
||||
else
|
||||
-- trigger.action.outText("No choice, no livery.", 30)
|
||||
end
|
||||
|
||||
theData.livery_id = chosen
|
||||
else
|
||||
-- trigger.action.outText("No livery for type <" .. theType .. ">", 30)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -1050,17 +1050,13 @@ destinationReached! -- adds script to last waypoint to hit this signal, also ini
|
||||
dead! signal and cb. only applies to ground troops? can they disembark troops when hit?
|
||||
attacked signal each time a unit is destroyed
|
||||
importantType - type that must survive=
|
||||
coalition / masterOwner
|
||||
isActive# 0/1
|
||||
coalition / masterOwner tie-in
|
||||
doWipe? to wipe all my convoys?
|
||||
tacTypes = desinate units types that must survive. Upon start, ensure that at least one tac type is pressenr
|
||||
when arriving, verify that it still is, or fail earlier when all tactypes are destroyed.
|
||||
convoy status UI
|
||||
|
||||
csar integration. when losing a vehicle, pSCAR match (say, 50%), whole convoy pushes a HOLD order to defend until time runs out or csar mission picks up evacuee (pop task).
|
||||
do:
|
||||
when escort engages, send notice
|
||||
when escort damaged, send notice
|
||||
mark source and dest of convoy on map for same side
|
||||
?mark source and dest of convoy on map for same side
|
||||
make routes interchangeable between convoys?
|
||||
make inf units disembark when convoy attacked
|
||||
--]]--
|
||||
@ -1,5 +1,5 @@
|
||||
dcsCommon = {}
|
||||
dcsCommon.version = "3.1.2"
|
||||
dcsCommon.version = "3.1.3"
|
||||
--[[-- VERSION HISTORY
|
||||
3.0.0 - removed bad bug in stringStartsWith, only relevant if caseSensitive is false
|
||||
- point2text new intsOnly option
|
||||
@ -29,19 +29,37 @@ dcsCommon.version = "3.1.2"
|
||||
3.1.0 - updates to events, DCS update 7-11 2024 hardening
|
||||
3.1.1 - added Chinook to troop carriers
|
||||
3.1.2 - isTroopCarrier() hardening against DCS sillieness
|
||||
3.1.3 - new dcsCommon.unitIsOfLegalType() analogue to isTroopCarrier
|
||||
- new DCS Patch section
|
||||
--]]--
|
||||
|
||||
-- dcsCommon is a library of common lua functions
|
||||
-- for easy access and simple mission programming
|
||||
-- (c) 2021 - 2024 by Christian Franz and cf/x AG
|
||||
|
||||
--
|
||||
-- DCS API PATCHES FOR DCS-INTERNAL BUGS
|
||||
--
|
||||
|
||||
Group.getByNameBase = Group.getByName -- thanks, Dzsekeb(?)
|
||||
|
||||
function Group.getByName(name) -- patch getByName to protect against empty groups
|
||||
local g = Group.getByNameBase(name)
|
||||
if not g then return nil end
|
||||
if g:getSize() == 0 then return nil end
|
||||
return g
|
||||
end
|
||||
|
||||
--
|
||||
-- DML FOUNDATION
|
||||
--
|
||||
dcsCommon.verbose = false -- set to true to see debug messages. Lots of them
|
||||
dcsCommon.uuidStr = "uuid-"
|
||||
dcsCommon.simpleUUID = 76543 -- a number to start. as good as any
|
||||
|
||||
-- globals
|
||||
dcsCommon.cbID = 0 -- callback id for simple callback scheduling
|
||||
dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D", "CH-47Fbl1"} -- Ka-50, Apache and Gazelle can't carry troops
|
||||
dcsCommon.troopCarriers = {"Mi-8MT", "UH-1H", "Mi-24P", "OH58D", "CH-47Fbl1"} -- Ka-50, Apache and Gazelle can't carry troops, the Kiowa can!
|
||||
dcsCommon.coalitionSides = {0, 1, 2}
|
||||
dcsCommon.maxCountry = 86 -- number of countries defined in total
|
||||
|
||||
@ -2839,6 +2857,10 @@ function dcsCommon.isTroopCarrierType(theType, carriers)
|
||||
return false
|
||||
end
|
||||
|
||||
function dcsCommon.unitIsOfLegalType(theUnit, types)
|
||||
return dcsCommon.isTroopCarrier(theUnit, types)
|
||||
end
|
||||
|
||||
function dcsCommon.isTroopCarrier(theUnit, carriers)
|
||||
-- return true if conf can carry troups
|
||||
if not theUnit then return false end
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
cfxHeloTroops = {}
|
||||
cfxHeloTroops.version = "3.1.0"
|
||||
cfxHeloTroops.version = "3.1.2"
|
||||
cfxHeloTroops.verbose = false
|
||||
cfxHeloTroops.autoDrop = true
|
||||
cfxHeloTroops.autoPickup = false
|
||||
@ -18,7 +18,8 @@ cfxHeloTroops.requestRange = 500 -- meters
|
||||
3.0.4 - also handles picking up troops with orders "captureandhold"
|
||||
3.0.5 - worked around a new issues accessing a unit's name
|
||||
3.1.0 - compatible with DCS 2.9.6 dynamic spawning
|
||||
|
||||
3.1.1 - deployTroopsFromHelicopter() captureandhold
|
||||
3.1.2 - doLoadGroup - test if group is still alive edge case handling
|
||||
--]]--
|
||||
|
||||
|
||||
@ -613,6 +614,7 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
|
||||
local moveFormation = conf.troopsOnBoard.moveFormation
|
||||
|
||||
if not orders then orders = "guard" end
|
||||
orders = string.lower(orders)
|
||||
|
||||
-- order processing: if the orders were pre-pended with "wait-"
|
||||
-- we now remove that, so after dropping they do what their
|
||||
@ -646,7 +648,11 @@ function cfxHeloTroops.deployTroopsFromHelicopter(conf)
|
||||
-- and make them run to the wrong zone
|
||||
dest = cfxGroundTroops.getClosestEnemyZone(troop)
|
||||
troopData.destination = dest
|
||||
trigger.action.outText("Inserting troops to capture zone <" .. dest.name .. ">", 30)
|
||||
if dest then
|
||||
trigger.action.outText("Inserting troops to capture zone <" .. dest.name .. ">", 30)
|
||||
else
|
||||
trigger.action.outText("+++heloT: WARNING: cap&hold: can't find a zone to cap.", 30)
|
||||
end
|
||||
end
|
||||
|
||||
troop.destination = dest -- transfer target zone for attackzone oders
|
||||
@ -678,6 +684,8 @@ end
|
||||
function cfxHeloTroops.doLoadGroup(args)
|
||||
local conf = args[1]
|
||||
local group = args[2]
|
||||
if not group then return end
|
||||
if not Group.isExist(group) then return end -- edge case: group died in the past 0.1 seconds
|
||||
conf.troopsOnBoard = {}
|
||||
-- all we need to do is disassemble the group into type
|
||||
conf.troopsOnBoard.types = dcsCommon.getGroupTypeString(group)
|
||||
|
||||
734
modules/inferno.lua
Normal file
734
modules/inferno.lua
Normal file
@ -0,0 +1,734 @@
|
||||
inferno = {}
|
||||
inferno.version = "0.9.9"
|
||||
inferno.requiredLibs = {
|
||||
"dcsCommon",
|
||||
"cfxZones",
|
||||
}
|
||||
--
|
||||
-- Inferno models fires inside inferno zones. Fires can spread and
|
||||
-- be extinguished by aircraft from the airtank module
|
||||
-- (c) 2024 by Christian "cfrag" Franz
|
||||
--
|
||||
inferno.zones = {}
|
||||
inferno.maxStages = 10 -- tied to fuel consumption, 1 burns small, maxStages at max size. A burning fire untended increases in stage by one each tick until it reaches maxStages
|
||||
inferno.threshold = 4.9 -- when fire spreads to another field
|
||||
inferno.fireExtinguishedCB = {} -- CB for other modules / scripts
|
||||
|
||||
--
|
||||
-- CB
|
||||
--
|
||||
function inferno.installExtinguishedCB(theCB)
|
||||
table.insert(inferno.fireExtinguishedCB, theCB)
|
||||
end
|
||||
|
||||
function inferno.invokeFireExtinguishedCB(theZone)
|
||||
for idx, cb in pairs(inferno.fireExtinguishedCB) do
|
||||
cb(theZone, theZone.heroes)
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Reading zones
|
||||
--
|
||||
function inferno.addZone(theZone)
|
||||
inferno.zones[theZone.name] = theZone
|
||||
end
|
||||
|
||||
function inferno.buildGrid(theZone)
|
||||
-- default for circular zone
|
||||
local radius = theZone.radius
|
||||
local side = radius * 2
|
||||
local p = theZone:getPoint()
|
||||
local minx = p.x - radius
|
||||
local minz = p.z - radius
|
||||
local xside = side
|
||||
local zside = side
|
||||
local xradius = radius
|
||||
local zradius = radius
|
||||
if theZone.isPoly then
|
||||
-- build the params for a (rectangular) zone from a
|
||||
-- quad zone, makes area an AABB (axis-aligned bounding box)
|
||||
minx = theZone.bounds.ll.x
|
||||
minz = theZone.bounds.ll.z
|
||||
xside = theZone.bounds.ur.x - theZone.bounds.ll.x
|
||||
zside = theZone.bounds.ur.z - theZone.bounds.ll.z
|
||||
end
|
||||
local cellx = theZone.cellSize -- square cells assumed
|
||||
local cellz = theZone.cellSize
|
||||
local numX = math.floor(xside / cellx)
|
||||
if numX < 1 then numX = 1 end
|
||||
if numX > 100 then
|
||||
trigger.action.outText("***inferno: limited x from <" .. numX .. "> to 100", 30)
|
||||
numX = 100
|
||||
end
|
||||
|
||||
local numZ = math.floor(zside / cellz)
|
||||
if numX > 100 then
|
||||
trigger.action.outText("***inferno: limited z from <" .. numZ .. "> to 100", 30)
|
||||
numZ = 100
|
||||
end
|
||||
if numZ < 1 then numZ = 1 end
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("infernal zone <" .. theZone.name .. ">: cellSize <" .. theZone.cellSize .. "> --> x <" .. numX .. ">, z <" .. numZ .. ">", 30)
|
||||
end
|
||||
local grid = {}
|
||||
local goodCells = {}
|
||||
-- Remember that in DCS
|
||||
-- X is North/South, with positive X being North/South
|
||||
-- and Z is East/West with positve Z being EAST
|
||||
local fCount = 0
|
||||
theZone.burning = false
|
||||
for x=1, numX do -- "up/down"
|
||||
grid[x] = {}
|
||||
for z=1, numZ do -- "left/right"
|
||||
local ele = {}
|
||||
-- calculate center for each cell
|
||||
local xc = minx + (x-1) * cellx + cellx/2
|
||||
local zc = minz + (z-1) * cellz + cellz/2
|
||||
local xf = xc
|
||||
local zf = zc
|
||||
|
||||
local lp = {x=xc, y=zc}
|
||||
local yc = land.getHeight(lp)
|
||||
ele.center = {x=xc, y=yc, z=zc}
|
||||
if theZone.markCell then
|
||||
dcsCommon.createStaticObjectForCoalitionAtLocation(0, ele.center, dcsCommon.uuid(theZone.name), "Black_Tyre_RF", 0, false)
|
||||
|
||||
dcsCommon.createStaticObjectForCoalitionAtLocation(0, {x=ele.center.x - cellx/2, y=0, z=ele.center.z - cellz/2}, dcsCommon.uuid(theZone.name), "Windsock", 0, false)
|
||||
dcsCommon.createStaticObjectForCoalitionAtLocation(0, {x=ele.center.x + cellx/2, y=0, z=ele.center.z - cellz/2}, dcsCommon.uuid(theZone.name), "Windsock", 0, false)
|
||||
dcsCommon.createStaticObjectForCoalitionAtLocation(0, {x=ele.center.x - cellx/2, y=0, z=ele.center.z + cellz/2}, dcsCommon.uuid(theZone.name), "Windsock", 0, false)
|
||||
dcsCommon.createStaticObjectForCoalitionAtLocation(0, {x=ele.center.x + cellx/2, y=0, z=ele.center.z + cellz/2}, dcsCommon.uuid(theZone.name), "Windsock", 0, false)
|
||||
end
|
||||
|
||||
ele.fxpos = {x=xf, y=yc, z=zf}
|
||||
ele.myType = land.getSurfaceType(lp) -- LAND=1, SHALLOW_WATER=2, WATER=3, ROAD=4, RUNWAY=5
|
||||
-- we don not burn if a cell has shallow or deep water, or roads or runways
|
||||
ele.inside = theZone:pointInZone(ele.center)
|
||||
if theZone.freeBorder then
|
||||
if x == 1 or x == numX then ele.inside = false end
|
||||
if z == 1 or z == numZ then ele.inside = false end
|
||||
end
|
||||
ele.myStage = 0 -- not burning
|
||||
if ele.inside and ele.myType == 1 then -- land only
|
||||
-- create a position for the fire -- visual only
|
||||
if theZone.stagger then
|
||||
repeat
|
||||
xf = xc + (0.5 - math.random()) * cellx
|
||||
zf = zc + (0.5 - math.random()) * cellz
|
||||
lp = {x=xf, y=zf}
|
||||
until land.getSurfaceType(lp) == 1
|
||||
ele.fxpos = {x=xf, y=yc, z=zf}
|
||||
end
|
||||
|
||||
-- place a fire on the cell
|
||||
if theZone.fullBlaze then
|
||||
ele.fxname = dcsCommon.uuid(theZone.name)
|
||||
trigger.action.effectSmokeBig(ele.fxpos, 4, 0.5 , ele.fxname)
|
||||
ele.myStage = inferno.maxStages
|
||||
ele.fxsize = 4
|
||||
theZone.burning = true
|
||||
end
|
||||
local sparkable = {x=x, z=z}
|
||||
table.insert(goodCells, sparkable)
|
||||
fCount = fCount + 1
|
||||
else
|
||||
ele.inside = false -- optim: not in poly or burnable
|
||||
end
|
||||
ele.fuel = theZone.fuel -- use rnd?
|
||||
ele.eternal = theZone.eternal -- unlimited fuel
|
||||
grid[x][z] = ele
|
||||
end -- for z
|
||||
end -- for x
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("inferno: zone <" .. theZone.name .. "> has <" .. fCount .. "> hot spots", 30)
|
||||
end
|
||||
if fCount < 1 then
|
||||
trigger.action.outText("WARNING: <" .. theZone.name .. "> has no good burn cells!", 30)
|
||||
end
|
||||
theZone.numX = numX
|
||||
theZone.numZ = numZ
|
||||
theZone.minx = minx
|
||||
theZone.minz = minz
|
||||
theZone.xside = xside
|
||||
theZone.grid = grid
|
||||
theZone.goodCells = goodCells
|
||||
|
||||
-- find bestCell from goodCells closest to center
|
||||
local bestdist = math.huge
|
||||
local bestCell = nil
|
||||
for idx, aCell in pairs(goodCells) do
|
||||
local x = aCell.x
|
||||
local z = aCell.z
|
||||
local ele = grid[x][z]
|
||||
local cp = ele.center
|
||||
local d = dcsCommon.dist(cp, p)
|
||||
if d < bestdist then
|
||||
bestCell = aCell
|
||||
bestdist = d
|
||||
end
|
||||
end
|
||||
theZone.bestCell = bestCell
|
||||
end
|
||||
|
||||
function inferno.readZone(theZone)
|
||||
theZone.cellSize = theZone:getNumberFromZoneProperty("cellSize", inferno.cellSize)
|
||||
-- FUEL: amount of fuel to burn PER CELL. when at zero, fire goes out
|
||||
-- expansion: make it a random range
|
||||
theZone.fuel = theZone:getNumberFromZoneProperty("fuel", 100)
|
||||
theZone.rndLoc = theZone:getBoolFromZoneProperty("rndLoc", false)
|
||||
theZone.freeBorder = theZone:getBoolFromZoneProperty("freeBorder", true) -- ring zone with non-burning zones
|
||||
theZone.eternal = theZone:getBoolFromZoneProperty("eternal", true)
|
||||
theZone.stagger = theZone:getBoolFromZoneProperty("stagger", true) -- randomize inside cell
|
||||
theZone.fullBlaze = theZone:getBoolFromZoneProperty("fullBlaze", false )
|
||||
theZone.canSpread = theZone:getBoolFromZoneProperty("canSpread", true)
|
||||
theZone.maxSpread = theZone:getNumberFromZoneProperty("maxSpread", 999999)
|
||||
theZone.impactSmoke = theZone:getBoolFromZoneProperty("impactSmoke", inferno.impactSmoke)
|
||||
theZone.markCell = theZone:getBoolFromZoneProperty("markCell", false)
|
||||
inferno.buildGrid(theZone)
|
||||
theZone.heroes = {} -- remembers all who dropped into zone
|
||||
theZone.onStart = theZone:getBoolFromZoneProperty("onStart", false)
|
||||
if theZone:hasProperty("ignite?") then
|
||||
theZone.ignite = theZone:getStringFromZoneProperty("ignite?", "none")
|
||||
theZone.lastIgnite = trigger.misc.getUserFlag(theZone.ignite)
|
||||
end
|
||||
if theZone:hasProperty("douse?") then
|
||||
theZone.douse = theZone:getStringFromZoneProperty("douse?", "none")
|
||||
theZone.lastDouse = trigger.misc.getUserFlag(theZone.douse)
|
||||
end
|
||||
if theZone:hasProperty("extinguished!") then
|
||||
theZone.extinguished = theZone:getStringFromZoneProperty("extinguished", "none")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
--
|
||||
-- API for water droppers
|
||||
--
|
||||
function inferno.surroundDelta(theZone, p, x, z)
|
||||
if x < 1 then return math.huge end
|
||||
if z < 1 then return math.huge end
|
||||
if x > theZone.numX then return math.huge end
|
||||
if z > theZone.numZ then return math.huge end
|
||||
local ele = theZone.grid[x][z]
|
||||
if not ele then return math.huge end
|
||||
if not ele.inside then return math.huge end
|
||||
if not ele.sparked then return math.huge end
|
||||
if ele.myStage < 1 then return math.huge end
|
||||
return dcsCommon.dist(p, ele.fxpos)
|
||||
end
|
||||
|
||||
function inferno.waterInZone(theZone, p, amount)
|
||||
-- water dropped (as point source) into a inferno zone.
|
||||
-- find the cell that it was dropped in
|
||||
local x = p.x - theZone.minx
|
||||
local z = p.z - theZone.minz
|
||||
local xc = math.floor(x / theZone.cellSize) + 1 -- square cells!
|
||||
local zc = math.floor(z / theZone.cellSize) + 1
|
||||
local ele = theZone.grid[xc][zc]
|
||||
if not ele then
|
||||
trigger.action.outText("Inferno: no ele for <" .. theZone.name .. ">: x<" .. x .. ">z<" .. z .. ">", 30)
|
||||
return "NIL ele for x" .. xc .. ",z" .. zc .. " in " .. theZone.name
|
||||
end
|
||||
|
||||
-- empty ele pre-proccing:
|
||||
-- if not burning, see if we find a better burning cell nearby
|
||||
if (not ele.sparked) or ele.extinguished then -- not burning, note that we do NOT test inside here!
|
||||
local hitDelta = math.sqrt(2) * theZone.cellSize -- dcsCommon.dist(p, ele.center) + 0.5 * theZone.cellSize -- give others a chance
|
||||
local bestDelta = hitDelta
|
||||
local ofx = 0
|
||||
local ofz = 0
|
||||
for dx = -1, 1 do
|
||||
for dz = -1, 1 do
|
||||
if dx == 0 and dz == 0 then -- skip this one
|
||||
else
|
||||
local newDelta = inferno.surroundDelta(theZone, p, xc + dx, zc + dz)
|
||||
if newDelta < bestDelta then
|
||||
bestDelta = newDelta
|
||||
ofx = dx
|
||||
ofz = dz
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
xc = xc + ofx
|
||||
zc = zc + ofz
|
||||
ele = theZone.grid[xc][zc]
|
||||
-- else
|
||||
-- trigger.action.outText("inferno dropper: NO ALIGNMENT, beds are burning.", 30)
|
||||
end
|
||||
if theZone.impactSmoke then
|
||||
if inferno.verbose then
|
||||
trigger.action.smoke(ele.center, 1) -- red is ele center
|
||||
end
|
||||
trigger.action.smoke(p, 4) -- blue is actual impact
|
||||
end
|
||||
|
||||
-- inside?
|
||||
if ele.inside then
|
||||
-- force this cell's eternal to OFF, now it consumes fuel
|
||||
ele.eternal = false -- from now on, this cell burns own fuel
|
||||
|
||||
if not ele.sparked then
|
||||
-- not burning. remove all fuel, make it
|
||||
-- extinguished so it won't catch fire in the future
|
||||
ele.extinguished = true -- will now negatively contribute
|
||||
ele.fuel = 0
|
||||
return "Good peripheral delivery, will prevent spread."
|
||||
end
|
||||
|
||||
-- calculate dispersal of water. the higher, the more dispersed
|
||||
-- and less fuel on the ground is 'removed'
|
||||
local dispAmount = amount -- currently no dispersal, full amount hits ground
|
||||
ele.fuel = ele.fuel - dispAmount
|
||||
|
||||
-- we can restage fx to smaller fire and reset stage
|
||||
-- so fire consumes less fuel ?
|
||||
-- NYI, later.
|
||||
return "Direct delivery into fire cell!"
|
||||
end
|
||||
|
||||
-- not inside or a water tile
|
||||
return nil
|
||||
end
|
||||
|
||||
function inferno.waterDropped(p, amount, data) -- if returns non-nil, has hit a cell
|
||||
-- p is (x, 0, z) of where the water hits the ground
|
||||
for name, theZone in pairs(inferno.zones) do
|
||||
if theZone:pointInZone(p) then
|
||||
if inferno.verbose then
|
||||
trigger.action.outText("inferno: INSIDE <" .. theZone.name .. ">", 30)
|
||||
end
|
||||
-- if available, remember and increase the number of drops in
|
||||
-- zone for player
|
||||
if data and data.pName then
|
||||
if not theZone.heroes then theZone.heroes = {} end
|
||||
if theZone.heroes[data.pName] then
|
||||
theZone.heroes[data.pName] = theZone.heroes[data.pName] + 1
|
||||
else
|
||||
theZone.heroes[data.pName] = 1
|
||||
end
|
||||
-- trigger.action.outText("registering <" .. data.pName .. "> for drop in <" .. theZone.name .. ">", 30)
|
||||
|
||||
-- else
|
||||
-- trigger.action.outText("Not registered, no data", 30)
|
||||
end
|
||||
return inferno.waterInZone(theZone, p, amount)
|
||||
end
|
||||
end
|
||||
if inferno.impactSmoke then
|
||||
-- mark the position with a blue smoke
|
||||
trigger.action.smoke(p, 4)
|
||||
end
|
||||
if inferno.verbose then
|
||||
trigger.action.outText("water drop outside any inferno zone", 30)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
--
|
||||
-- IGNITE & DOUSE
|
||||
--
|
||||
function inferno.sparkCell(theZone, x, z)
|
||||
local ele = theZone.grid[x][z]
|
||||
if not ele.inside then
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("ele x<" .. x .. ">z<" .. z .. "> is outside, no spark!", 30)
|
||||
end
|
||||
return
|
||||
false end
|
||||
ele.fxname = dcsCommon.uuid(theZone.name)
|
||||
trigger.action.effectSmokeBig(ele.fxpos, 1, 0.5 , ele.fxname)
|
||||
ele.myStage = 1
|
||||
ele.fxsize = 1
|
||||
ele.sparked = true
|
||||
return true
|
||||
end
|
||||
|
||||
function inferno.ignite(theZone)
|
||||
if theZone.burning then
|
||||
-- later expansion: add more fires
|
||||
-- will give error when fullblaze is set
|
||||
trigger.action.outText("Zone <" .. theZone.name .. "> already burning", 30)
|
||||
return
|
||||
end
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("igniting <" .. theZone.name .. ">", 30)
|
||||
end
|
||||
|
||||
local midNum = math.floor((#theZone.goodCells + 1)/ 2)
|
||||
if midNum < 1 then midNum = 1 end
|
||||
local midCell = theZone.bestCell
|
||||
if not midCell then midCell = theZone.goodCells[midNum] end
|
||||
if theZone.rndLoc then midCell = dcsCommon.pickRandom(theZone.goodCells) end
|
||||
local x = midCell.x
|
||||
local z = midCell.z
|
||||
|
||||
if inferno.sparkCell(theZone, x, z) then
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("Sparking cell x<" .. x .. ">z<" .. z .. "> for <" .. theZone.name .. ">", 30)
|
||||
end
|
||||
else
|
||||
trigger.action.outText("Inferno: fire in <" .. theZone.name .. "> @ center x<" .. x .. ">z<" .. z .. "> didin't catch", 30)
|
||||
end
|
||||
theZone.hasSpread = 0 -- how many times we have spread
|
||||
theZone.burning = true
|
||||
end
|
||||
|
||||
function inferno.startFire(theZone)
|
||||
if theZone.burning then
|
||||
return
|
||||
end
|
||||
inferno.ignite(theZone)
|
||||
end
|
||||
|
||||
function inferno.douseFire(theZone)
|
||||
-- if not theZone.burning then
|
||||
-- return
|
||||
-- end
|
||||
-- walk the grid, and kill all flames, set all eles
|
||||
-- to end state
|
||||
for x=1, theZone.numX do
|
||||
for z=1, theZone.numZ do
|
||||
local ele = theZone.grid[x][z]
|
||||
if ele.inside then
|
||||
if ele.fxname then
|
||||
trigger.action.effectSmokeStop(ele.fxname)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
inferno.buildGrid(theZone) -- prep next fire in here
|
||||
theZone.heroes = {}
|
||||
theZone.burning = false
|
||||
end
|
||||
|
||||
--
|
||||
-- Fire Tick: progress/grow fire, burn fuel, expand conflagration etc.
|
||||
--
|
||||
function inferno.fireUpdate() -- update all burning fires
|
||||
--[[--
|
||||
every tick, we progress the fire status of all cells
|
||||
fire stages (per cell):
|
||||
< 1 : not burning, perhaps dying
|
||||
0 : not burning, not smoking
|
||||
-1..-5 : smoking. the closer to 0, less smoke
|
||||
1..10 : burning, number = flame size, contib. heat and fuel consumption
|
||||
--]]--
|
||||
timer.scheduleFunction(inferno.fireUpdate, {}, timer.getTime() + inferno.fireTick) -- next time
|
||||
for zName, theZone in pairs(inferno.zones) do
|
||||
if theZone.fullBlaze then
|
||||
-- do nothing, just testing layout
|
||||
elseif theZone.burning then
|
||||
inferno.burnOneTick(theZone) -- expand fuel, see if it spreads
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function inferno.burnOneTick(theZone)
|
||||
-- iterate all cells and see if the fire spreads
|
||||
local isBurning = false
|
||||
local grid = theZone.grid
|
||||
local newStage = {} -- new states
|
||||
local numX = theZone.numX
|
||||
local numZ = theZone.numZ
|
||||
-- pass 1:
|
||||
-- calculate new stages
|
||||
for x = 1, numX do -- up
|
||||
newStage[x] = {}
|
||||
for z = 1, numZ do
|
||||
local ele = grid[x][z]
|
||||
if ele.inside then
|
||||
local stage = ele.myStage
|
||||
-- we will only continue burning if we have fuel
|
||||
if ele.extinguished then
|
||||
-- we are drenched and can't re-ignite
|
||||
newStage[x][z] = 0
|
||||
elseif ele.fuel > 0 then -- we have fuel
|
||||
if stage > 0 then -- it's already burning
|
||||
if not ele.eternal then
|
||||
ele.fuel = ele.fuel - stage -- consume fuel if burning and not eternal
|
||||
if theZone.verbose then
|
||||
trigger.action.outText(stage .. " fuel consumed. remain: "..ele.fuel, 30)
|
||||
end
|
||||
end
|
||||
stage = stage + 1
|
||||
if stage > inferno.maxStages then
|
||||
stage = inferno.maxStages
|
||||
end
|
||||
newStage[x][z] = stage -- fire is growing
|
||||
elseif stage < 0 then
|
||||
-- fire is dying, can't be here if fuel > 0
|
||||
newStage[x][z] = stage
|
||||
else -- not burning. see if the surrounding sides are contributing
|
||||
if theZone.canSpread and (theZone.hasSpread < theZone.maxSpread) then
|
||||
local accu = 0
|
||||
-- now do all surrounding 8 fields
|
||||
-- NOTE: use wind direction to modify below if we use wind - NYI
|
||||
accu = accu + inferno.contribute(x-1, z-1, theZone)
|
||||
accu = accu + inferno.contribute(x, z-1, theZone)
|
||||
accu = accu + inferno.contribute(x+1, z-1, theZone)
|
||||
accu = accu + inferno.contribute(x-1, z, theZone)
|
||||
accu = accu + inferno.contribute(x+1, z, theZone)
|
||||
accu = accu + inferno.contribute(x-1, z+1, theZone)
|
||||
accu = accu + inferno.contribute(x, z+1, theZone)
|
||||
accu = accu + inferno.contribute(x+1, z+1, theZone)
|
||||
accu = accu / 2 -- half intensity
|
||||
-- 10% chance to spread when above threshold
|
||||
if accu > inferno.threshold and math.random() < 0.1 then
|
||||
stage = 1 -- start small fire
|
||||
theZone.hasSpread = theZone.hasSpread + 1
|
||||
end
|
||||
end
|
||||
newStage[x][z] = stage
|
||||
end
|
||||
else -- fuel is spent let flames die down if they exist
|
||||
if stage == 0 then -- wasn't burning before
|
||||
newStage[x][z] = 0
|
||||
else
|
||||
if stage > 0 then
|
||||
newStage[x][z] = stage - 1
|
||||
if newStage[x][z] == 0 then
|
||||
newStage[x][z] = - 5
|
||||
end
|
||||
else
|
||||
newStage[x][z] = stage + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
newStage[x][z] = 0 -- outside, will always be 0
|
||||
end
|
||||
end -- for z
|
||||
end -- for x
|
||||
-- pass 2:
|
||||
-- see what changed and handle accordingly
|
||||
for x = 1, numX do -- up
|
||||
for z = 1, numZ do
|
||||
local ele = grid[x][z]
|
||||
if ele.inside then
|
||||
local stage = ele.myStage
|
||||
|
||||
local ns = newStage[x][z]
|
||||
if not ns then ns = 0 end
|
||||
if theZone.verbose and ele.sparked then
|
||||
trigger.action.outText("x<" .. x .. ">z<" .. z .. "> - next stage is " .. ns, 30)
|
||||
end
|
||||
if ns ~= stage then
|
||||
-- fire has changed: spread or dying down
|
||||
if stage == 0 then -- fire has spread!
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("Fire in <" .. theZone.name .. "> has spread to x<" .. x .. ">z<" .. z .. ">", 30)
|
||||
end
|
||||
ele.sparked = true
|
||||
elseif ns == 0 then -- fire has died down fully
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("Fire in <" .. theZone.name .. "> at x<" .. x .. ">z<" .. z .. "> has been extinguished", 30)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle fire fx
|
||||
-- determine fx number
|
||||
local fx = 0
|
||||
if stage > 0 then
|
||||
fx = math.floor(stage / 2) -- 1..10--> 1..4
|
||||
if fx < 1 then fx = 1 end
|
||||
if fx > 4 then fx = 4 end
|
||||
isBurning = true
|
||||
elseif stage < 0 then
|
||||
fx = 4-stage -- -5 .. -1 --> 6..10
|
||||
if fx < 5 then fx = 5 end
|
||||
if fx > 8 then fx = 8 end
|
||||
isBurning = true -- keep as 'burning'
|
||||
end
|
||||
if fx ~= ele.fxsize then
|
||||
if ele.fxname then
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("removing old fx <" .. ele.fxsize .. "> [" .. ele.fxname .. "] for <" .. fx .. "> in <" .. theZone.name .. "> x<" .. x .. ">z<" .. z .. ">", 30)
|
||||
end
|
||||
-- remove old fx
|
||||
trigger.action.effectSmokeStop(ele.fxname)
|
||||
end
|
||||
-- start new
|
||||
if fx > 0 then
|
||||
ele.fxname = dcsCommon.uuid(theZone.name)
|
||||
trigger.action.effectSmokeBig(ele.fxpos, fx, 0.5 , ele.fxname)
|
||||
else
|
||||
if theZone.verbose then
|
||||
trigger.action.outText("expiring <" .. theZone.name .. "> x<" .. x .. ">z<" .. z .. ">", 30)
|
||||
end
|
||||
end
|
||||
ele.fxsize = fx
|
||||
end
|
||||
-- save new stage
|
||||
ele.myStage = ns
|
||||
else
|
||||
if not ele.sparked then
|
||||
-- not yet ignited, ignore
|
||||
elseif ele.extinguished then
|
||||
-- ignore, we are already off
|
||||
elseif stage ~= 0 then
|
||||
-- still burning bright
|
||||
isBurning = true
|
||||
else
|
||||
-- remove last fx
|
||||
trigger.action.effectSmokeStop(ele.fxname)
|
||||
-- clear this zone or add debris now?
|
||||
ele.extinguished = true -- now can't re-ignite
|
||||
end
|
||||
end -- if ns <> stage
|
||||
end -- if inside
|
||||
end -- for z
|
||||
end -- for x
|
||||
if not isBurning then
|
||||
trigger.action.outText("inferno in <" .. theZone.name .. "> has been fully extinguished", 30)
|
||||
theZone.burning = false
|
||||
if theZone.extinguished then
|
||||
theZone:pollFlag(theZone.extinguished, "inc")
|
||||
end
|
||||
inferno.invokeFireExtinguishedCB(theZone)
|
||||
-- also fully douse this one, so we can restart it later
|
||||
inferno.douseFire(theZone)
|
||||
end
|
||||
end
|
||||
|
||||
function inferno.contribute(x, z, theZone)
|
||||
-- a cell starts burning if there is fuel
|
||||
-- and the total contribution of all surrounding cells is
|
||||
-- 10 or more, meaning that a 10 fire will ignite all surrounding
|
||||
-- fields
|
||||
-- bounds check
|
||||
if x < 1 then return 0 end
|
||||
if z < 1 then return 0 end
|
||||
local numX = theZone.numX
|
||||
if x > numX then return 0 end
|
||||
local numZ = theZone.numZ
|
||||
if z > numZ then return 0 end
|
||||
local ele = theZone.grid[x][z]
|
||||
if not ele.inside then return 0 end
|
||||
if not ele.sparked then return 0 end -- not burning
|
||||
if ele.extinguished then return -2 end -- water spill dampens
|
||||
-- return stage that we are in if > 0
|
||||
if ele.myStage >= 0 then
|
||||
return ele.myStage -- mystage is positive int, "heat" 1..10
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
--
|
||||
-- UPDATE
|
||||
--
|
||||
function inferno.update() -- for flag polling etc
|
||||
timer.scheduleFunction(inferno.update, {}, timer.getTime() + 1/inferno.ups)
|
||||
for idx, theZone in pairs (inferno.zones) do
|
||||
if theZone.ignite and
|
||||
theZone:testZoneFlag(theZone.ignite, "change", "lastIgnite") then
|
||||
inferno.startFire(theZone)
|
||||
end
|
||||
|
||||
if theZone.douse and theZone:testZoneFlag(theZone.douse, "change", "lastDouse") then
|
||||
inferno.douseFire(theZone)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- CONFIG & START
|
||||
--
|
||||
function inferno.readConfigZone()
|
||||
inferno.name = "infernoConfig" -- make compatible with dml zones
|
||||
local theZone = cfxZones.getZoneByName("infernoConfig")
|
||||
if not theZone then
|
||||
theZone = cfxZones.createSimpleZone("infernoConfig")
|
||||
end
|
||||
|
||||
inferno.verbose = theZone.verbose
|
||||
inferno.ups = theZone:getNumberFromZoneProperty("ups", 1)
|
||||
inferno.fireTick = theZone:getNumberFromZoneProperty("fireTick", 10)
|
||||
inferno.cellSize = theZone:getNumberFromZoneProperty("cellSize", 100)
|
||||
inferno.menuName = theZone:getStringFromZoneProperty("menuName", "Firefighting")
|
||||
inferno.impactSmoke = theZone:getBoolFromZoneProperty("impactSmoke", false)
|
||||
|
||||
if theZone:hasProperty("attachTo:") then
|
||||
local attachTo = theZone:getStringFromZoneProperty("attachTo:", "<none>")
|
||||
if radioMenu then -- requires optional radio menu to have loaded
|
||||
local mainMenu = radioMenu.mainMenus[attachTo]
|
||||
if mainMenu then
|
||||
inferno.mainMenu = mainMenu
|
||||
else
|
||||
trigger.action.outText("+++inferno: cannot find super menu <" .. attachTo .. ">", 30)
|
||||
end
|
||||
else
|
||||
trigger.action.outText("+++inferno: REQUIRES radioMenu to run before inferno. 'AttachTo:' ignored.", 30)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
function inferno.start()
|
||||
if not dcsCommon.libCheck then
|
||||
trigger.action.outText("cfx inferno requires dcsCommon", 30)
|
||||
return false
|
||||
end
|
||||
if not dcsCommon.libCheck("cfx inferno", inferno.requiredLibs) then
|
||||
return false
|
||||
end
|
||||
|
||||
-- read config
|
||||
inferno.readConfigZone()
|
||||
|
||||
-- process inferno Zones
|
||||
local attrZones = cfxZones.getZonesWithAttributeNamed("inferno")
|
||||
for k, aZone in pairs(attrZones) do
|
||||
inferno.readZone(aZone) -- process attributes
|
||||
inferno.addZone(aZone) -- add to list
|
||||
end
|
||||
|
||||
-- start update (DML)
|
||||
timer.scheduleFunction(inferno.update, {}, timer.getTime() + 1/inferno.ups)
|
||||
|
||||
-- start fire tick update
|
||||
timer.scheduleFunction(inferno.fireUpdate, {}, timer.getTime() + inferno.fireTick)
|
||||
|
||||
-- start all zones that have onstart
|
||||
for gName, theZone in pairs(inferno.zones) do
|
||||
if theZone.onStart then
|
||||
inferno.ignite(theZone)
|
||||
end
|
||||
end
|
||||
-- say Hi!
|
||||
trigger.action.outText("cf/x inferno v" .. inferno.version .. " started.", 30)
|
||||
return true
|
||||
end
|
||||
|
||||
if not inferno.start() then
|
||||
trigger.action.outText("inferno failed to start up")
|
||||
inferno = nil
|
||||
end
|
||||
|
||||
--[[--
|
||||
"ele" structure in grid
|
||||
- fuel amount of fuel to burn. when < 0 the fire starves over the next cycles. By dumping water helicopters/planes reduce amount of available fuel
|
||||
- extinguished if true, can't re-ignite
|
||||
- myStage -- FSM for flames: >0 == burning, consumes fuel, <0 is starved of fuel and get smaller each tick
|
||||
- fxname to reference flame fx
|
||||
- eternal if true does not consume fuel. goes false when the first drop of water enters the cell from players
|
||||
|
||||
- center point of center
|
||||
- fxpos - point in cell that has the fx
|
||||
- mytype land type. only 1 burns
|
||||
- inside is it inside the zone and can burn? true if so. used to make cells unburnable
|
||||
- sparked if false this isn't burning
|
||||
|
||||
|
||||
to do:
|
||||
OK - callback for extinguis
|
||||
OK - remember who contributes dropped inside successful and then receives ack when zone fully doused
|
||||
|
||||
Possible enhancements
|
||||
- random range fuel
|
||||
- wind for contribute (can precalc into table)
|
||||
- boom in ignite
|
||||
- clear after burn out
|
||||
- leave debris after burn out, mabe place in ignite
|
||||
--]]--
|
||||
@ -1,5 +1,5 @@
|
||||
limitedAirframes = {}
|
||||
limitedAirframes.version = "1.6.0"
|
||||
limitedAirframes.version = "1.7.0"
|
||||
|
||||
limitedAirframes.warningSound = "Quest Snare 3.wav"
|
||||
limitedAirframes.loseSound = "Death PIANO.wav"
|
||||
@ -11,33 +11,6 @@ limitedAirframes.requiredLibs = {
|
||||
}
|
||||
|
||||
--[[-- VERSION HISTORY
|
||||
- 1.0.0 - initial version
|
||||
- 1.0.1 - out text to coalition for switch
|
||||
- less verbose
|
||||
- win/lose sound by coalition
|
||||
- 1.0.2 - corrected some to to-->for Groups typos
|
||||
- 1.0.3 - renamed to 'pilot' instead of airframe:
|
||||
pilotSafe attribute
|
||||
- fixed MP bug, switched to EventMonII code
|
||||
- 1.0.4 - added CSAR integration: create CSAR and callback
|
||||
- safe ditch only at less than 7 kmh and 10m agl
|
||||
- 1.0.5 - replaced 5 (crash) check for helos only
|
||||
- 1.0.6 - changed alt and speed tests to inAir
|
||||
reduced verbosity
|
||||
made reporting of new units for coalition side only
|
||||
- 1.0.7 - if unlimited pilots it says so when you return one
|
||||
to base
|
||||
- 1.0.8 - now can query remaining pilots
|
||||
- 1.0.9 - better formatted remaining pilots
|
||||
- 1.1.0 - module manager
|
||||
- separated out settings
|
||||
- hand change in pilotsafe zones that can be landed in
|
||||
- 1.2.0 - limitedAirframesConfig zone
|
||||
- 1.3.0 - added network dead override logic via unitFlownByPlayer
|
||||
- 1.4.0 - DML integration, verbosity, clean-up, QoL improvements
|
||||
redSafe, blueSafe with attribute, backward compatible
|
||||
currRed
|
||||
- 1.4.1 - removed dependency to cfxPlayer
|
||||
- 1.5.0 - persistence support
|
||||
- 1.5.1 - new "announcer" attribute
|
||||
- 1.5.2 - integration with autoCSAR: prevent limitedAF from creating csar
|
||||
@ -49,28 +22,17 @@ limitedAirframes.requiredLibs = {
|
||||
- new hasUI attribute
|
||||
- minor clean-up
|
||||
- set numRed and numBlue on startup
|
||||
1.7.0 - dcs jul-11, jul-22 bug prevention
|
||||
- some cleanup
|
||||
|
||||
--]]--
|
||||
|
||||
-- limitedAirframes manages the number of available player airframes
|
||||
-- per scenario and side. Each time a player crashes the plane
|
||||
-- outside of safe zones, the number is decreased for that side
|
||||
-- when the number reaches -1 or smaller, other side wins
|
||||
-- !!!Only affects player planes!!
|
||||
|
||||
-- safe zones must have a property "pilotSafe"
|
||||
-- - pilotSafe - this is a zone to safely change airframes in
|
||||
-- - can also carry 'red' or 'blue' to enable
|
||||
-- if zone can change ownership, player's coalition
|
||||
-- is checked against current zone ownership
|
||||
-- zone owner.
|
||||
|
||||
|
||||
limitedAirframes.safeZones = {} -- safezones are zones where a crash or change plane does not
|
||||
|
||||
limitedAirframes.myEvents = {5, 9, 30, 6, 20, 21, 15 } -- 5 = crash, 9 - dead, 30 - unit lost, 6 - eject, 20 - enter unit, 21 - leave unit, 15 - birth
|
||||
|
||||
-- guarantee a min of 2 seconds between events
|
||||
-- guarantee a minimum of 2 seconds between events
|
||||
-- for this we save last event per player
|
||||
limitedAirframes.lastEvents = {}
|
||||
-- each time a plane crashes or is abandoned check
|
||||
@ -196,10 +158,16 @@ end
|
||||
-- and also adds the player if unknown
|
||||
function limitedAirframes.addPlayerUnit(theUnit)
|
||||
local theSide = theUnit:getCoalition()
|
||||
local uName = theUnit:getName()
|
||||
if not uName then uName = "**XXXX**" end
|
||||
local pName = theUnit:getPlayerName()
|
||||
if not pName then pName = "**????**" end
|
||||
local uName = "**XXXX**"
|
||||
if theUnit.getName then
|
||||
uName = theUnit:getName()
|
||||
end
|
||||
-- if not uName then uName = "**XXXX**" end
|
||||
local pName = "**????**"
|
||||
if theUnit.getPlayerName then
|
||||
pName = theUnit:getPlayerName()
|
||||
end
|
||||
-- if not pName then pName = "**????**" end
|
||||
limitedAirframes.updatePlayer(pName, "alive")
|
||||
|
||||
local desc = "unit <" .. uName .. "> controlled by <" .. pName .. ">"
|
||||
@ -273,6 +241,7 @@ end
|
||||
--
|
||||
-- E V E N T H A N D L I N G
|
||||
--
|
||||
--[[--
|
||||
function limitedAirframes.XXXisKnownPlayerUnit(theUnit)
|
||||
if not theUnit then return false end
|
||||
local aName = theUnit:getName()
|
||||
@ -281,6 +250,7 @@ function limitedAirframes.XXXisKnownPlayerUnit(theUnit)
|
||||
end
|
||||
return false
|
||||
end
|
||||
--]]--
|
||||
|
||||
function limitedAirframes.isInteresting(eventID)
|
||||
-- return true if we are interested in this event, false else
|
||||
@ -294,6 +264,7 @@ function limitedAirframes.preProcessor(event)
|
||||
-- make sure it has an initiator
|
||||
if not event.initiator then return false end -- no initiator
|
||||
local theUnit = event.initiator
|
||||
if not theUnit.getName then return false end -- DCS Jul-22 bug
|
||||
local uName = theUnit:getName()
|
||||
|
||||
|
||||
@ -353,6 +324,7 @@ function limitedAirframes.somethingHappened(event)
|
||||
end
|
||||
|
||||
local theUnit = event.initiator
|
||||
if not theUnit.getName then return end
|
||||
local unitName = theUnit:getName()
|
||||
local ID = event.id
|
||||
local myType = theUnit:getTypeName()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
cfxObjectSpawnZones = {}
|
||||
cfxObjectSpawnZones.version = "2.1.0"
|
||||
cfxObjectSpawnZones.version = "2.1.1"
|
||||
cfxObjectSpawnZones.requiredLibs = {
|
||||
"dcsCommon", -- common is of course needed for everything
|
||||
-- pretty stupid to check for this since we
|
||||
@ -15,7 +15,7 @@ cfxObjectSpawnZones.verbose = false
|
||||
version history
|
||||
2.0.0 - dmlZones
|
||||
2.1.0 - autoTurn attribute
|
||||
|
||||
2.1.1 - now spawns the correct country
|
||||
--]]--
|
||||
|
||||
-- respawn currently happens after theSpawns is deleted and cooldown seconds have passed
|
||||
@ -246,7 +246,8 @@ function cfxObjectSpawnZones.spawnObjectNTimes(aSpawner, theType, n, container)
|
||||
end
|
||||
|
||||
-- spawn in dcs
|
||||
local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- create in dcs
|
||||
--local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- create in dcs
|
||||
local theObject = coalition.addStaticObject(aSpawner.country, theStaticData) -- create in dcs
|
||||
table.insert(container, theObject) -- add to collection
|
||||
if aSpawner.isCargo and aSpawner.managed then
|
||||
if cfxCargoManager then
|
||||
@ -303,7 +304,8 @@ function cfxObjectSpawnZones.spawnObjectNTimes(aSpawner, theType, n, container)
|
||||
end
|
||||
|
||||
-- spawn in dcs
|
||||
local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- this will generate an event!
|
||||
-- local theObject = coalition.addStaticObject(aSpawner.rawOwner, theStaticData) -- this will generate an event!
|
||||
local theObject = coalition.addStaticObject(aSpawner.country, theStaticData) -- this will generate an event!
|
||||
table.insert(container, theObject)
|
||||
-- see if it is managed cargo
|
||||
if aSpawner.isCargo and aSpawner.managed then
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
cfxOwnedZones = {}
|
||||
cfxOwnedZones.version = "1.3.0"
|
||||
cfxOwnedZones.version = "1.3.0gamma"
|
||||
cfxOwnedZones.verbose = false
|
||||
cfxOwnedZones.announcer = true
|
||||
cfxOwnedZones.name = "cfxOwnedZones"
|
||||
@ -61,6 +61,7 @@ cfxOwnedZones.name = "cfxOwnedZones"
|
||||
- blueLost! zone output
|
||||
- ownedBy direct zone output
|
||||
- neutral! zone output
|
||||
1.3.0g - DML 2.x compatibility
|
||||
|
||||
--]]--
|
||||
cfxOwnedZones.requiredLibs = {
|
||||
@ -1193,7 +1194,7 @@ function cfxOwnedZones.loadData()
|
||||
end
|
||||
theZone.owner = zData.owner
|
||||
theZone.state = zData.state
|
||||
if zData.conquered then
|
||||
if zData.conquered and theZone.conqueredFlag then
|
||||
cfxZones.setFlagValue(theZone.conqueredFlag, zData.conquered, theZone)
|
||||
end
|
||||
-- update mark in map
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
cfxOwnedZones = {}
|
||||
cfxOwnedZones.version = "2.4.0"
|
||||
cfxOwnedZones.version = "2.4.1"
|
||||
cfxOwnedZones.verbose = false
|
||||
cfxOwnedZones.announcer = true
|
||||
cfxOwnedZones.name = "cfxOwnedZones"
|
||||
@ -44,6 +44,7 @@ cfxOwnedZones.name = "cfxOwnedZones"
|
||||
- code clean-up
|
||||
2.3.1 - restored getNearestOwnedZoneToPoint
|
||||
2.4.0 - dmlZones masterOwner update
|
||||
2.4.1 - conquered flag now correctly guarded in loadData()
|
||||
|
||||
--]]--
|
||||
cfxOwnedZones.requiredLibs = {
|
||||
@ -761,7 +762,7 @@ function cfxOwnedZones.loadData()
|
||||
local theZone = cfxOwnedZones.getOwnedZoneByName(zName)
|
||||
if theZone then
|
||||
theZone.owner = zData.owner
|
||||
if zData.conquered then
|
||||
if zData.conquered and theZone.conqueredFlag then
|
||||
theZone:setFlagValue(theZone.conqueredFlag, zData.conquered)
|
||||
end
|
||||
-- update mark in map
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
radioMenu = {}
|
||||
radioMenu.version = "4.0.0"
|
||||
radioMenu.version = "4.0.1"
|
||||
radioMenu.verbose = false
|
||||
radioMenu.ups = 1
|
||||
radioMenu.requiredLibs = {
|
||||
"dcsCommon", -- always
|
||||
"cfxZones", -- Zones, of course
|
||||
"cfxMX",
|
||||
}
|
||||
-- note: cfxMX is optional unless using types or groups attributes
|
||||
radioMenu.menus = {}
|
||||
@ -18,6 +19,7 @@ radioMenu.lateGroups = {} -- dict by ID
|
||||
detect cyclic references
|
||||
4.0.0 - added infrastructure for dynamic players (DCS 2.9.6)
|
||||
- added dynamic player support
|
||||
4.0.1 - MX no longer optional, so ask for it
|
||||
--]]--
|
||||
|
||||
function radioMenu.addRadioMenu(theZone)
|
||||
|
||||
@ -930,14 +930,7 @@ function cfxReconMode.readConfigZone()
|
||||
-- note: must match exactly!!!!
|
||||
local theZone = cfxZones.getZoneByName("reconModeConfig")
|
||||
if not theZone then
|
||||
if cfxReconMode.verbose then
|
||||
trigger.action.outText("+++rcn: no config zone!", 30)
|
||||
end
|
||||
theZone = cfxZones.createSimpleZone("reconModeConfig")
|
||||
else
|
||||
if cfxReconMode.verbose then
|
||||
trigger.action.outText("+++rcn: found config zone!", 30)
|
||||
end
|
||||
end
|
||||
|
||||
cfxReconMode.verbose = theZone.verbose --cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
|
||||
|
||||
@ -12,8 +12,17 @@ slotty.version = "1.1.0"
|
||||
Version history
|
||||
1.0.0 - Initial version
|
||||
1.1.0 - "noSlotty" global disable flag, anti-mirror SSB flag
|
||||
|
||||
1.2.0 - better (delayed) nilling for cfxSSB ocupied clients to
|
||||
- avoid a race condition
|
||||
|
||||
--]]--
|
||||
function slotty.delayedSSBNil(args)
|
||||
uName = args
|
||||
if cfxSSBClient.verbose then
|
||||
trigger.action.outText("slotty-->SSBClient: nilling <" .. args .. ">, now unoccupied", 30)
|
||||
end
|
||||
cfxSSBClient.occupiedUnits[uName] = nil
|
||||
end
|
||||
|
||||
function slotty:onEvent(event)
|
||||
if not event.initiator then return end
|
||||
@ -42,7 +51,10 @@ function slotty:onEvent(event)
|
||||
|
||||
-- interface with SSBClient for compatibility
|
||||
if cfxSSBClient and cfxSSBClient.occupiedUnits then
|
||||
cfxSSBClient.occupiedUnits[uName] = nil
|
||||
-- to resolve a race condition, we schedule nilling the
|
||||
-- ssbClientSlot in 1/2 second
|
||||
-- cfxSSBClient.occupiedUnits[uName] = nil
|
||||
timer.scheduleFunction(slotty.delayedSSBNil, uName, timer.getTime() + 0.5)
|
||||
end
|
||||
|
||||
if isSP then
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
stopGap = {}
|
||||
stopGap.version = "1.2.0"
|
||||
stopGap.version = "1.3.0"
|
||||
stopGap.verbose = false
|
||||
stopGap.ssbEnabled = true
|
||||
stopGap.ignoreMe = "-sg"
|
||||
stopGap.spIgnore = "-sp" -- only single-player ignored
|
||||
stopGap.noParking = false -- turn on to ignore all 'from parking'
|
||||
stopGap.isMP = false
|
||||
stopGap.running = true
|
||||
stopGap.refreshInterval = -1 -- seconds to refresh all statics. -1 = never, 3600 = once every hour
|
||||
@ -15,7 +16,7 @@ stopGap.requiredLibs = {
|
||||
"cfxMX",
|
||||
}
|
||||
--[[--
|
||||
Written and (c) 2023 by Christian Franz
|
||||
Written and (c) 2023-2024 by Christian Franz
|
||||
|
||||
Replace all player units with static aircraft until the first time
|
||||
that a player slots into that plane. Static is then replaced with live player unit.
|
||||
@ -55,6 +56,8 @@ stopGap.requiredLibs = {
|
||||
1.2.0 - DCS dynamic player spawn compatibility
|
||||
stopGaps only works with MX data, so we are good, no changes
|
||||
required
|
||||
1.3.0 - carriers in shallow waters also no longer handled as viable
|
||||
- noParking option
|
||||
|
||||
--]]--
|
||||
|
||||
@ -102,11 +105,26 @@ function stopGap.isGroundStart(theGroup)
|
||||
if action == "Turning Point" then return false end
|
||||
if action == "Landing" then return false end
|
||||
if action == "From Runway" then return false end
|
||||
if stopGap.noParking then
|
||||
local loAct = string.lower(action)
|
||||
if loAct == "from parking area" or
|
||||
loAct == "from parking area hot" then
|
||||
if stopGap.verbose then
|
||||
trigger.action.outText("StopG: Player Group <" .. theGroup.name .. "> NOPARKING: [" .. action .. "] must be skipped.", 30)
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
-- looks like aircraft is on the ground
|
||||
-- but is it in water (carrier)?
|
||||
local u1 = theGroup.units[1]
|
||||
local sType = land.getSurfaceType(u1) -- has fields x and y
|
||||
if sType == 3 then return false end
|
||||
if sType == 3 or sType == 2 then
|
||||
if stopGap.verbose then
|
||||
trigger.action.outText("StopG: Player Group <" .. theGroup.name .. "> SEA BASED: [" .. action .. "], land type " .. sType .. " should be skipped.", 30)
|
||||
end
|
||||
return false
|
||||
end -- water & shallow water a no-go
|
||||
if stopGap.verbose then
|
||||
trigger.action.outText("StopG: Player Group <" .. theGroup.name .. "> GROUND BASED: " .. action .. ", land type " .. sType, 30)
|
||||
end
|
||||
@ -422,6 +440,7 @@ function stopGap.readConfigZone(theZone)
|
||||
stopGap.verbose = theZone.verbose
|
||||
stopGap.ssbEnabled = theZone:getBoolFromZoneProperty("ssb", true)
|
||||
stopGap.enabled = theZone:getBoolFromZoneProperty("onStart", true)
|
||||
stopGap.noParking = theZone:getBoolFromZoneProperty("noParking", false)
|
||||
if theZone:hasProperty("on?") then
|
||||
stopGap.turnOnFlag = theZone:getStringFromZoneProperty("on?", "*<none>")
|
||||
stopGap.lastTurnOnFlag = trigger.misc.getUserFlag(stopGap.turnOnFlag)
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user