PlayerScore 2.0
Owned Zones refactor (started)
This commit is contained in:
Christian Franz 2023-04-27 07:33:57 +02:00
parent aeafc854ac
commit 97d3c10540
14 changed files with 3361 additions and 1145 deletions

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,66 +1,11 @@
cfxOwnedZones = {}
cfxOwnedZones.version = "1.3.0"
cfxOwnedZones.version = "2.0.0"
cfxOwnedZones.verbose = false
cfxOwnedZones.announcer = true
cfxOwnedZones.name = "cfxOwnedZones"
--[[-- VERSION HISTORY
1.0.3 - added getNearestFriendlyZone
- added getNearestOwnedZone
- added hasOwnedZones
- added getNearestOwnedZoneToPoint
1.0.4 - changed addOwnedZone code to use cfxZone.getCoalitionFromZoneProperty
- changed to use dcsCommon.coalition2county
- changed to using correct coalition for spawing attackers and defenders
1.0.5 - repairing defenders switches to country instead coalition when calling createGroundUnitsInZoneForCoalition -- fixed
1.0.6 - removed call to conqTemplate
- verified that pause will also apply to init
- unbeatable zones
- untargetable zones
- hidden attribute
1.0.7 - optional cfxGroundTroops module, error message when attackers
- support of 'none' type string to indicate no attackers/defenders
- updated property access
- module check
- cfxOwnedZones.usesDefenders(aZone)
- verifyZone
1.0.8 - repairDefenders trims types to allow blanks in
type separator
1.1.0 - config zone
- bang! support r, b, n capture
- defaulting attackDelta to 10 instead of radius
- verbose code for spawning
- verbose code for state transition
- attackers have (A) in name, defenders (D)
- exit createDefenders if no troops
- exit createAttackers if no troops
- usesAttackers/usesDefenders checks for neutral ownership
- verbose state change
- nearestZone supports moving zones
- remove exiting defenders from zone after cap to avoid
shocked state
- announcer
1.1.1 - conq+1 flag
1.1.2 - corrected type bug in zoneConquered
1.2.0 - support for persistence
- conq+1 --> conquered!
- no cfxGroundTroop bug (no delay)
1.2.1 - fix in load to correctly re-establish all attackers for subsequent save
1.2.2 - redCap! and blueCap!
1.2.3 - fix for persistence bug when not using conquered flag
1.2.4 - pause? and activate? inputs
1.3.0 - new update method
- new fastEval option in config
- new numCap option in config
- new numKeep option in config
- new easyContest option in config
- new logic to keep and lose zones. controlled with numKeep and numCap.
- winSound
- loseSound
- redLost! zone output
- blueLost! zone output
- ownedBy direct zone output
- neutral! zone output
2.0.0 - factored from cfxOwnedZones 1.x, separating out production
--]]--
cfxOwnedZones.requiredLibs = {
@ -68,60 +13,27 @@ cfxOwnedZones.requiredLibs = {
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course
"cfxCommander", -- to make troops do stuff
-- "cfxGroundTroops", -- optional, used for attackers only
}
cfxOwnedZones.zones = {}
cfxOwnedZones.ups = 1
cfxOwnedZones.initialized = false
cfxOwnedZones.defendingTime = 100 -- 100 seconds until new defenders are produced
cfxOwnedZones.attackingTime = 300 -- 300 seconds until new attackers are produced
cfxOwnedZones.shockTime = 200 -- 200 -- 'shocked' period of inactivity
cfxOwnedZones.repairTime = 200 -- 200 -- time until we raplace one lost unit, also repairs all other units to 100%
--[[--
owned zones is a module that managers conquerable zones and keeps a record
of who owns the zone based on rules
-- persistence: all attackers we ever sent out.
-- is regularly verified and cut to size
cfxOwnedZones.spawnedAttackers = {}
*** EXTENTDS ZONES ***, so compatible with cfxZones, pilotSafe (limited airframes), may conflict with FARPZones
-- owned zones is a module that managers 'conquerable' zones and keeps a
-- record of who owns the zone
-- based on some simple rules that are regularly checked
--
-- *** EXTENTDS ZONES ***, so compatible with cfxZones, pilotSafe (limited airframes), may conflict with FARPZones
--
owned zones are identified by the 'owner' property. It can be initially set to nothing (default), NEUTRAL, RED or BLUE
-- owned zones are identified by the 'owner' property. It can be initially set to nothing (default), NEUTRAL, RED or BLUE
-- when a zone changes hands, a callback can be installed to be told of that fact
-- callback has the format (zone, newOwner, formerOwner) with zone being the Zone, and new owner and former owners
when a zone changes hands, a callback can be installed to be told of that fact
callback has the format (zone, newOwner, formerOwner) with zone being the Zone, and new owner and former owners
--]]--
cfxOwnedZones.conqueredCallbacks = {}
--
-- zone attributes when owned
-- owner: coalition that owns the zone
-- status: FSM for spawning
-- defendersRED/BLUE - coma separated type string for the group to spawn on defense cycle completion
-- attackersRED/BLUE - as above for attack cycle.
-- timeStamp - time when zone switched into current state
-- spawnRadius - overrides zone's radius when placing defenders. can be use to place defenders inside or outside zone itself
-- formation - defender's formation
-- attackFormation - attackers formation
-- attackRadius - radius of circle in which attackers are spawned. informs formation
-- attackDelta - polar coord: r from zone center where attackers are spawned
-- attackPhi - polar degrees where attackers are to be spawned
-- paused - will not spawn. default is false
-- unbeatable - can't be conquered by other side. default is false
-- untargetable - will not be targeted by either side. make unbeatable
-- owned zones untargetable, or they'll become a troop magnet for
-- zoneAttackers
-- hidden - if set (default no), it no markings on the map
--
-- to create an owned zone that can't be conquered and does nothing
-- add the following properties to a zone
-- owner = <x>, paused = true, unbeatable = true
--
-- callback handling
--
@ -173,7 +85,7 @@ function cfxOwnedZones.drawZoneInMap(aZone)
local lineColor = {1.0, 0, 0, 1.0} -- red
local fillColor = {1.0, 0, 0, 0.2} -- red
local owner = cfxOwnedZones.getOwnerForZone(aZone)
local owner = aZone.owner -- cfxOwnedZones.getOwnerForZone(aZone)
if owner == 2 then
lineColor = {0.0, 0, 1.0, 1.0}
fillColor = {0.0, 0, 1.0, 0.2}
@ -198,337 +110,46 @@ function cfxOwnedZones.getOwnedZoneByName(zName)
end
function cfxOwnedZones.addOwnedZone(aZone)
local owner = cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) -- is already readm read it again
aZone.owner = owner -- add this attribute to zone
-- now init all other owned zone properties
aZone.state = "init"
aZone.timeStamp = timer.getTime()
--aZone.defendersRED = "Soldier M4,Soldier M4,Soldier M4,Soldier M4,Soldier M4" -- vehicles allocated to defend when red
aZone.defendersRED = cfxZones.getStringFromZoneProperty(aZone, "defendersRED", "none")
aZone.defendersBLUE = cfxZones.getStringFromZoneProperty(aZone, "defendersBLUE", "none")
aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "attackersRED", "none")
aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "attackersBLUE", "none")
local formation = cfxZones.getZoneProperty(aZone, "formation")
if not formation then formation = "circle_out" end
aZone.formation = formation
formation = cfxZones.getZoneProperty(aZone, "attackFormation")
if not formation then formation = "circle_out" end
aZone.attackFormation = formation
local spawnRadius = cfxZones.getNumberFromZoneProperty(aZone, "spawnRadius", aZone.radius-5) -- "-5" so they remaininside radius
aZone.spawnRadius = spawnRadius
local attackRadius = cfxZones.getNumberFromZoneProperty(aZone, "attackRadius", aZone.radius)
aZone.attackRadius = attackRadius
local attackDelta = cfxZones.getNumberFromZoneProperty(aZone, "attackDelta", 10) -- aZone.radius)
aZone.attackDelta = attackDelta
local attackPhi = cfxZones.getNumberFromZoneProperty(aZone, "attackPhi", 0)
aZone.attackPhi = attackPhi
local paused = cfxZones.getBoolFromZoneProperty(aZone, "paused", false)
aZone.paused = paused
local owner = aZone.owner --cfxZones.getCoalitionFromZoneProperty(aZone, "owner", 0) -- is already readm read it again
if cfxZones.hasProperty(aZone, "conquered!") then
aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conquered!", "*<cfxnone>")
elseif cfxZones.hasProperty(aZone, "conq+1") then
aZone.conqueredFlag = cfxZones.getStringFromZoneProperty(aZone, "conq+1", "*<cfxnone>")
end
if cfxZones.hasProperty(aZone, "redCap!") then
aZone.redCap = cfxZones.getStringFromZoneProperty(aZone, "redCap!", "none")
end
if cfxZones.hasProperty(aZone, "redLost!") then
aZone.redLost = cfxZones.getStringFromZoneProperty(aZone, "redLost!", "none")
end
if cfxZones.hasProperty(aZone, "blueCap!") then
aZone.blueCap = cfxZones.getStringFromZoneProperty(aZone, "blueCap!", "none")
end
if cfxZones.hasProperty(aZone, "blueLost!") then
aZone.blueLost = cfxZones.getStringFromZoneProperty(aZone, "blueLost!", "none")
end
if cfxZones.hasProperty(aZone, "neutral!") then
aZone.neutralCap = cfxZones.getStringFromZoneProperty(aZone, "neutral!", "none")
end
if cfxZones.hasProperty(aZone, "ownedBy") then
aZone.ownedBy = cfxZones.getStringFromZoneProperty(aZone, "ownedBy", "none")
end
-- pause? and activate?
if cfxZones.hasProperty(aZone, "pause?") then
aZone.pauseFlag = cfxZones.getStringFromZoneProperty(aZone, "pause?", "none")
aZone.lastPauseValue = trigger.misc.getUserFlag(aZone.pauseFlag)
end
if cfxZones.hasProperty(aZone, "activate?") then
aZone.activateFlag = cfxZones.getStringFromZoneProperty(aZone, "activate?", "none")
aZone.lastActivateValue = trigger.misc.getUserFlag(aZone.activateFlag)
end
aZone.ownedTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change")
if cfxZones.hasProperty(aZone, "ownedTriggerMethod") then
aZone.ownedTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "ownedTriggerMethod", "change")
end
aZone.unbeatable = cfxZones.getBoolFromZoneProperty(aZone, "unbeatable", false)
aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false)
aZone.hidden = cfxZones.getBoolFromZoneProperty(aZone, "hidden", false)
cfxOwnedZones.zones[aZone] = aZone
cfxOwnedZones.drawZoneInMap(aZone)
cfxOwnedZones.verifyZone(aZone)
end
function cfxOwnedZones.verifyZone(aZone)
-- do some sanity checks
if not cfxGroundTroops and (aZone.attackersRED ~= "none" or aZone.attackersBLUE ~= "none") then
trigger.action.outText("+++owdZ: " .. aZone.name .. " attackers need cfxGroundTroops to function", 30)
end
end
function cfxOwnedZones.getOwnerForZone(aZone)
local theZone = cfxOwnedZones.zones[aZone]
if not theZone then return 0 end -- unknown zone, return neutral as default
return theZone.owner
end
function cfxOwnedZones.getEnemyZonesFor(aCoalition)
local enemyZones = {}
local ourEnemy = dcsCommon.getEnemyCoalitionFor(aCoalition)
for zKey, aZone in pairs(cfxOwnedZones.zones) do
if aZone.owner == ourEnemy then -- only check enemy owned zones
-- note: will include untargetable zones
table.insert(enemyZones, aZone)
end
end
return enemyZones
end
function cfxOwnedZones.getNearestOwnedZoneToPoint(aPoint)
local shortestDist = math.huge
local closestZone = nil
for zKey, aZone in pairs(cfxOwnedZones.zones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and
currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestOwnedZone(theZone)
local shortestDist = math.huge
local closestZone = nil
local aPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(cfxOwnedZones.zones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestEnemyOwnedZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies
local zPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(cfxOwnedZones.zones) do
if targetNeutral then
-- return all zones that do not belong to us
if aZone.owner ~= theZone.owner then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(aPoint, zPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- return zones that are taken by the Enenmy
if aZone.owner == ourEnemy then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.getNearestFriendlyZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies nor friends, all zones would be legal.
local zPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(cfxOwnedZones.zones) do
if targetNeutral then
-- target all zones that do not belong to the enemy
if aZone.owner ~= ourEnemy then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- only target zones that are taken by us
if aZone.owner == theZone.owner then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function cfxOwnedZones.enemiesRemaining(aZone)
if cfxOwnedZones.getNearestEnemyOwnedZone(aZone) then return true end
return false
end
function cfxOwnedZones.spawnAttackTroops(theTypes, aZone, aCoalition, aFormation)
local unitTypes = {} -- build type names
-- split theTypes into an array of types
unitTypes = dcsCommon.splitString(theTypes, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
-- simply exit, no troops specified
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: no attackers for " .. aZone.name .. ". exiting", 30)
end
return
end
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: spawning attackers for " .. aZone.name, 30)
end
--local theCountry = dcsCommon.coalition2county(aCoalition)
local spawnPoint = {x = aZone.point.x, y = aZone.point.y, z = aZone.point.z} -- copy struct
local rads = aZone.attackPhi * 0.01745
spawnPoint.x = spawnPoint.x + math.cos(aZone.attackPhi) * aZone.attackDelta
spawnPoint.y = spawnPoint.y + math.sin(aZone.attackPhi) * aZone.attackDelta
local spawnZone = cfxZones.createSimpleZone("attkSpawnZone", spawnPoint, aZone.attackRadius)
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
aCoalition, -- theCountry,
aZone.name .. " (A) " .. dcsCommon.numberUUID(), -- must be unique
spawnZone,
unitTypes,
aFormation, -- outward facing
0)
return theGroup, theData
end
function cfxOwnedZones.spawnDefensiveTroops(theTypes, aZone, aCoalition, aFormation)
local unitTypes = {} -- build type names
-- split theTypes into an array of types
unitTypes = dcsCommon.splitString(theTypes, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
-- simply exit, no troops specified
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: no defenders for " .. aZone.name .. ". exiting", 30)
end
return
end
--local theCountry = dcsCommon.coalition2county(aCoalition)
local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius)
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
aCoalition, --theCountry,
aZone.name .. " (D) " .. dcsCommon.numberUUID(), -- must be unique
spawnZone, unitTypes,
aFormation, -- outward facing
0)
return theGroup, theData
end
--
-- U P D A T E
--
function cfxOwnedZones.sendOutAttackers(aZone)
-- only spawn if there are zones to attack
if not cfxOwnedZones.enemiesRemaining(aZone) then
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ - no enemies, resting ".. aZone.name, 30)
end
return
end
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ - attack cycle for ".. aZone.name, 30)
end
-- load the attacker typestring
-- step one: get the attackers
local attackers = aZone.attackersRED;
if (aZone.owner == 2) then attackers = aZone.attackersBLUE end
if attackers == "none" then return end
local theGroup, theData = cfxOwnedZones.spawnAttackTroops(attackers, aZone, aZone.owner, aZone.attackFormation)
local troopData = {}
troopData.groupData = theData
troopData.orders = "attackOwnedZone" -- lazy coding!
troopData.side = aZone.owner
cfxOwnedZones.spawnedAttackers[theData.name] = troopData
-- submit them to ground troops handler as zoneseekers
-- and our groundTroops module will handle the rest
if cfxGroundTroops then
local troops = cfxGroundTroops.createGroundTroops(theGroup)
troops.orders = "attackOwnedZone"
troops.side = aZone.owner
cfxGroundTroops.addGroundTroopsToPool(troops) -- hand off to ground troops
else
if cfxOwnedZones.verbose then
trigger.action.outText("+++ Owned Zones: no ground troops module on send out attackers", 30)
end
end
end
-- bang support
function cfxOwnedZones.bangNeutral(value)
if not cfxOwnedZones.neutralTriggerFlag then return end
local newVal = trigger.misc.getUserFlag(cfxOwnedZones.neutralTriggerFlag) + value
@ -616,286 +237,7 @@ function cfxOwnedZones.zoneConquered(aZone, theSide, formerOwner) -- 0 = neutral
-- update map
cfxOwnedZones.drawZoneInMap(aZone) -- update status in map. will erase previous version
-- remove all defenders to avoid shock state
aZone.defenders = nil
-- change to captured
aZone.state = "captured"
aZone.timeStamp = timer.getTime()
end
function cfxOwnedZones.repairDefenders(aZone)
--trigger.action.outText("+++ enter repair for ".. aZone.name, 30)
-- find a unit that is missing from my typestring and replace it
-- one by one until we are back to full strength
-- step one: get the defenders and create a type array
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
local unitTypes = {} -- build type names
-- if none, we are done
if defenders == "none" then return end
-- split theTypes into an array of types
allTypes = dcsCommon.trimArray(
dcsCommon.splitString(defenders, ",")
)
local livingTypes = {} -- init to emtpy, so we can add to it if none are alive
if (aZone.defenders) then
-- some remain. add one of the killed
livingTypes = dcsCommon.getGroupTypes(aZone.defenders)
-- we now iterate over the living types, and remove their
-- counterparts from the allTypes. We then take the first that
-- is left
if #livingTypes > 0 then
for key, aType in pairs (livingTypes) do
if not dcsCommon.findAndRemoveFromTable(allTypes, aType) then
trigger.action.outText("+++OwdZ WARNING: found unmatched type <" .. aType .. "> while trying to repair defenders for ".. aZone.name, 30)
else
-- all good
end
end
end
end
-- when we get here, allTypes is reduced to those that have been killed
if #allTypes < 1 then
trigger.action.outText("+++owdZ: WARNING: all types exist when repairing defenders for ".. aZone.name, 30)
else
table.insert(livingTypes, allTypes[1]) -- we simply use the first that we find
end
-- remove the old defenders
if aZone.defenders then
aZone.defenders:destroy()
end
-- now livingTypes holds the full array of units we need to spawn
local theCountry = dcsCommon.getACountryForCoalition(aZone.owner)
local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius)
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
aZone.owner, -- was wrongly: theCountry
aZone.name .. dcsCommon.numberUUID(), -- must be unique
spawnZone,
livingTypes,
aZone.formation, -- outward facing
0)
aZone.defenders = theGroup
aZone.lastDefenders = theGroup:getSize()
end
function cfxOwnedZones.inShock(aZone)
-- a unit was destroyed, everyone else is in shock, no rerpairs
-- group can re-shock when another unit is destroyed
end
function cfxOwnedZones.spawnDefenders(aZone)
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
-- before we spawn new defenders, remove the old ones
if aZone.defenders then
if aZone.defenders:isExist() then
aZone.defenders:destroy()
end
aZone.defenders = nil
end
-- if 'none', simply exit
if defenders == "none" then return end
local theGroup, theData = cfxOwnedZones.spawnDefensiveTroops(defenders, aZone, aZone.owner, aZone.formation)
-- the troops reamin, so no orders to move, no handing off to ground troop manager
aZone.defenders = theGroup
aZone.defenderData = theData -- used for persistence
if theGroup then
--aZone.defenderMax = theGroup:getInitialSize() -- so we can determine if some units were destroyed
aZone.lastDefenders = theGroup:getInitialSize() --- aZone.defenderMax -- if this is larger than current number, someone bit the dust
--trigger.action.outText("+++ spawned defenders for ".. aZone.name, 30)
else
trigger.action.outText("+++owdZ: WARNING: spawned no defenders for ".. aZone.name, 30)
aZone.defenderData = nil
end
end
--
-- per-zone update, run down the FSM to determine what to do.
-- FSM uses timeStamp since when state was set. Possible states are
-- - init -- has just been inited for the first time. will usually immediately produce defenders,
-- and then transition to defending
-- - catured -- has just been captured. transition to defending
-- - defending -- wait until timer has reached goal, then produce defending units and transition to attacking.
-- - attacking -- wait until timer has reached goal, and then produce attacking units and send them to closest enemy zone.
-- state is interrupted as soon as a defensive unit is lost. state then goes to defending with timer starting
-- - idle - do nothing, zone's actions are turned off
-- - shocked -- a unit was destroyed. group is in shock for a time until it starts repairing. If another unit is
-- destroyed during the shocked period, the timer resets to zero and repairs are delayed
-- - repairing -- as long as we aren't at full strength, units get replaced one by one until at full strength
-- each time the timer counts down, another missing unit is replaced, and all other unit's health
-- is reset to 100%
--
-- a Zone with the paused attribute set to true will cause it to not do anything
--
-- check if defenders are specified
function cfxOwnedZones.usesDefenders(aZone)
if aZone.owner == 0 then return false end
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
return defenders ~= "none"
end
function cfxOwnedZones.usesAttackers(aZone)
if aZone.owner == 0 then return false end
local attackers = aZone.attackersRED;
if (aZone.owner == 2) then defenders = aZone.attackersBLUE end
return attackers ~= "none"
end
function cfxOwnedZones.updateZone(aZone)
-- a zone can be paused, causing it to not progress anything
-- even if zone status is still init, will NOT produce anything
-- if paused is on.
if aZone.paused then return end
nextState = aZone.state;
-- first, check if my defenders have been attacked and one of them has been killed
-- if so, we immediately switch to 'shocked'
if cfxOwnedZones.usesDefenders(aZone) and
aZone.defenders then
-- we have defenders
if aZone.defenders:isExist() then
-- isee if group was damaged
if not aZone.lastDefenders then
-- fresh group, probably from persistence, needs init
aZone.lastDefenders = -1
end
if aZone.defenders:getSize() < aZone.lastDefenders then
-- yes, at least one unit destroyed
aZone.timeStamp = timer.getTime()
aZone.lastDefenders = aZone.defenders:getSize()
if aZone.lastDefenders == 0 then
aZone.defenders = nil
end
aZone.state = "shocked"
return
else
aZone.lastDefenders = aZone.defenders:getSize()
end
else
-- group was destroyed. erase link, and go into shock for the last time
aZone.state = "shocked"
aZone.timeStamp = timer.getTime()
aZone.lastDefenders = 0
aZone.defenders = nil
return
end
end
if aZone.state == "init" then
-- during init we instantly create the defenders since
-- we assume the zone existed already
if aZone.owner > 0 then
cfxOwnedZones.spawnDefenders(aZone)
-- now drop into attacking mode to produce attackers
nextState = "attacking"
else
nextState = "idle"
end
aZone.timeStamp = timer.getTime()
elseif aZone.state == "idle" then
-- nothing to do, zone is effectively switched off.
-- used for neutal zones or when forced to turn off
-- in some special cases
elseif aZone.state == "captured" then
-- start the clock on defenders
nextState = "defending"
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
elseif aZone.state == "defending" then
if timer.getTime() > aZone.timeStamp + cfxOwnedZones.defendingTime then
cfxOwnedZones.spawnDefenders(aZone)
-- now drop into attacking mode to produce attackers
nextState = "attacking"
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
elseif aZone.state == "repairing" then
-- we are currently rebuilding defenders unit by unit
if timer.getTime() > aZone.timeStamp + cfxOwnedZones.repairTime then
aZone.timeStamp = timer.getTime()
-- wait's up, repair one defender, then check if full strength
cfxOwnedZones.repairDefenders(aZone)
-- see if we are full strenght and if so go to attack, else set timer to reair the next unit
if aZone.defenders and aZone.defenders:isExist() and aZone.defenders:getSize() >= aZone.defenders:getInitialSize() then
-- we are at max size, time to produce some attackers
-- progress to next state
nextState = "attacking"
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
end
elseif aZone.state == "shocked" then
-- we are currently rebuilding defenders unit by unit
if timer.getTime() > aZone.timeStamp + cfxOwnedZones.shockTime then
nextState = "repairing"
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
elseif aZone.state == "attacking" then
if timer.getTime() > aZone.timeStamp + cfxOwnedZones.attackingTime then
cfxOwnedZones.sendOutAttackers(aZone)
-- reset timer
aZone.timeStamp = timer.getTime()
if cfxOwnedZones.verbose then
trigger.action.outText("+++owdZ: State " .. aZone.state .. " reset for " .. aZone.name, 30)
end
end
else
-- unknown zone state
end
aZone.state = nextState
end
function cfxOwnedZones.GC()
-- GC run. remove all my dead remembered troops
local before = #cfxOwnedZones.spawnedAttackers
local filteredAttackers = {}
for gName, gData in pairs (cfxOwnedZones.spawnedAttackers) do
-- all we need to do is get the group of that name
-- and if it still returns units we are fine
local gameGroup = Group.getByName(gName)
if gameGroup and gameGroup:isExist() and gameGroup:getSize() > 0 then
filteredAttackers[gName] = gData
end
end
cfxOwnedZones.spawnedAttackers = filteredAttackers
if cfxOwnedZones.verbose then
trigger.action.outText("owned zones GC ran: before <" .. before .. ">, after <" .. #cfxOwnedZones.spawnedAttackers .. ">", 30)
end
end
function cfxOwnedZones.update()
@ -951,9 +293,15 @@ function cfxOwnedZones.update()
-- trigger.action.outText(theZone.name .. " blue: " .. theZone.numBlue .. " red " .. theZone.numRed, 30)
local lastOwner = theZone.owner
local newOwner = 0 -- neutral is default
if theZone.unbeatable then -- Parker Lewis can't lose. Neither this zone.
newOwner = lastOwner
end
-- determine new owner
-- step one: no troops here. Become neutral?
if theZone.numRed < 1 and theZone.numBlue < 1 then
if theZone.unbeatable then
-- we do nothing
elseif theZone.numRed < 1 and theZone.numBlue < 1 then
-- no troops here. Become neutral?
if cfxOwnedZones.numKeep < 1 then
newOwner = lastOwner -- keep it, else turns neutral
else
@ -1018,71 +366,13 @@ function cfxOwnedZones.update()
end
theZone.owner = newOwner
-- production & flags
-- see if pause/unpause was issued
-- note that capping a zone will not change pause status
if theZone.pauseFlag and cfxZones.testZoneFlag(theZone, theZone.pauseFlag, theZone.ownedTriggerMethod, "lastPauseValue") then
theZone.paused = true
end
if theZone.activateFlag and cfxZones.testZoneFlag(theZone, theZone.activateFlag, theZone.ownedTriggerMethod, "lastActivateValue") then
theZone.paused = false
end
-- update ownership flag if exists
if theZone.ownedBy then
cfxZones.setFlagValue(theZone.ownedBy, theZone.owner, theZone)
end
-- now, perhaps with their new owner call updateZone()
-- to calcualte production for this zone
cfxOwnedZones.updateZone(theZone)
end -- iterating all zones
end
function cfxOwnedZones.updateOLD()
cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.updateOLD, {}, timer.getTime() + 1/cfxOwnedZones.ups)
-- iterate all zones, and determine their current ownership status
for key, aZone in pairs(cfxOwnedZones.zones) do
-- a hand change can only happen if there are only ground troops from the OTHER side in
-- the zone
local categ = Group.Category.GROUND
local theBlues = cfxZones.groupsOfCoalitionPartiallyInZone(2, aZone, categ)
local theReds = cfxZones.groupsOfCoalitionPartiallyInZone(1, aZone, categ)
local currentOwner = aZone.owner
if #theBlues > 0 and #theReds == 0 and aZone.unbeatable ~= true then
-- this now belongs to blue
if currentOwner ~= 2 then
cfxOwnedZones.zoneConquered(aZone, 2, currentOwner)
end
elseif #theBlues == 0 and #theReds > 0 and aZone.unbeatable ~= true then
-- this now belongs to red
if currentOwner ~= 1 then
cfxOwnedZones.zoneConquered(aZone, 1, currentOwner)
end
end
-- see if pause/unpause was issued
-- note that capping a zone will not change pause status
if aZone.pauseFlag and cfxZones.testZoneFlag(aZone, aZone.pauseFlag, aZone.ownedTriggerMethod, "lastPauseValue") then
aZone.paused = true
end
if aZone.activateFlag and cfxZones.testZoneFlag(aZone, aZone.activateFlag, aZone.ownedTriggerMethod, "lastActivateValue") then
aZone.paused = false
end
-- now, perhaps with their new owner call updateZone()
cfxOwnedZones.updateZone(aZone)
end
end
function cfxOwnedZones.houseKeeping()
timer.scheduleFunction(cfxOwnedZones.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes
cfxOwnedZones.GC()
end
function cfxOwnedZones.sideOwnsAll(theSide)
@ -1116,37 +406,17 @@ function cfxOwnedZones.saveData()
-- iterate all my zones and create data
for idx, theZone in pairs(cfxOwnedZones.zones) do
local zoneData = {}
if theZone.defenderData then
zoneData.defenderData = dcsCommon.clone(theZone.defenderData)
dcsCommon.synchGroupData(zoneData.defenderData)
end
if theZone.conqueredFlag then
zoneData.conquered = cfxZones.getFlagValue(theZone.conqueredFlag, theZone)
end
zoneData.owner = theZone.owner
zoneData.state = theZone.state -- will prevent immediate spawn
-- since new zones are spawned with 'init'
allZoneData[theZone.name] = zoneData
end
-- now iterate all attack groups that we have spawned and that
-- (maybe) are still alive
cfxOwnedZones.GC() -- start with a GC run to remove all dead
local livingAttackers = {}
for gName, gData in pairs (cfxOwnedZones.spawnedAttackers) do
-- all we need to do is get the group of that name
-- and if it still returns units we are fine
-- spawnedAttackers is a [groupName] table with {.groupData, .orders, .side}
local gameGroup = Group.getByName(gName)
if gameGroup and gameGroup:isExist() then
if gameGroup:getSize() > 0 then
local sData = dcsCommon.clone(gData)
dcsCommon.synchGroupData(sData.groupData)
livingAttackers[gName] = sData
end
end
end
-- now write the info for the flags that we output for #red, etc
local flagInfo = {}
flagInfo.neutral = cfxZones.getFlagValue(cfxOwnedZones.neutralTriggerFlag, cfxOwnedZones)
@ -1154,7 +424,6 @@ function cfxOwnedZones.saveData()
flagInfo.blue = cfxZones.getFlagValue(cfxOwnedZones.blueTriggerFlag, cfxOwnedZones)
-- assemble the data
theData.zoneData = allZoneData
theData.attackers = livingAttackers
theData.flagInfo = flagInfo
-- return it
@ -1180,19 +449,7 @@ function cfxOwnedZones.loadData()
-- access zone
local theZone = cfxOwnedZones.getOwnedZoneByName(zName)
if theZone then
if zData.defenderData then
if theZone.defenders and theZone.defenders:isExist() then
-- should not happen, but so be it
theZone.defenders:destroy()
end
local gData = zData.defenderData
local cty = gData.cty
local cat = gData.cat
theZone.defenders = coalition.addGroup(cty, cat, gData)
theZone.defenderData = zData.defenderData
end
theZone.owner = zData.owner
theZone.state = zData.state
if zData.conquered then
cfxZones.setFlagValue(theZone.conqueredFlag, zData.conquered, theZone)
end
@ -1203,27 +460,6 @@ function cfxOwnedZones.loadData()
end
end
-- now process all attackers
local allAttackers = theData.attackers
for gName, gdTroop in pairs(allAttackers) do
-- table is {.groupData, .orders, .side}
local gData = gdTroop.groupData
local orders = gdTroop.orders
local side = gdTroop.side
local cty = gData.cty
local cat = gData.cat
-- add to my own attacker queue so we can save later
local dClone = dcsCommon.clone(gdTroop)
cfxOwnedZones.spawnedAttackers[gName] = dClone
local theGroup = coalition.addGroup(cty, cat, gData)
if cfxGroundTroops then
local troops = cfxGroundTroops.createGroundTroops(theGroup)
troops.orders = orders
troops.side = side
cfxGroundTroops.addGroundTroopsToPool(troops) -- hand off to ground troops
end
end
-- now process module global flags
local flagInfo = theData.flagInfo
if flagInfo then
@ -1242,19 +478,10 @@ function cfxOwnedZones.readConfigZone(theZone)
cfxOwnedZones.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
cfxOwnedZones.announcer = cfxZones.getBoolFromZoneProperty(theZone, "announcer", true)
-- if cfxZones.hasProperty(theZone, "r!") then
cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "*<cfxnone>")
-- end
-- if cfxZones.hasProperty(theZone, "b!") then
cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "*<cfxnone>")
-- end
-- if cfxZones.hasProperty(theZone, "n!") then
cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "*<cfxnone>")
-- end
cfxOwnedZones.defendingTime = cfxZones.getNumberFromZoneProperty(theZone, "defendingTime", 100)
cfxOwnedZones.attackingTime = cfxZones.getNumberFromZoneProperty(theZone, "attackingTime", 300)
cfxOwnedZones.shockTime = cfxZones.getNumberFromZoneProperty(theZone, "shockTime", 200)
cfxOwnedZones.repairTime = cfxZones.getNumberFromZoneProperty(theZone, "repairTime", 200)
cfxOwnedZones.redTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "r!", "*<cfxnone>")
cfxOwnedZones.blueTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "b!", "*<cfxnone>")
cfxOwnedZones.neutralTriggerFlag = cfxZones.getStringFromZoneProperty(theZone, "n!", "*<cfxnone>")
-- numKeep, numCap, fastEval, easyContest
cfxOwnedZones.numCap = cfxZones.getNumberFromZoneProperty(theZone, "numCap", 1) -- minimal number of units required to cap zone
cfxOwnedZones.numKeep = cfxZones.getNumberFromZoneProperty(theZone, "numKeep", 0) -- number required to keep zone
@ -1299,11 +526,7 @@ function cfxOwnedZones.init()
initialized = true
cfxOwnedZones.updateSchedule = timer.scheduleFunction(cfxOwnedZones.update, {}, timer.getTime() + 1/cfxOwnedZones.ups)
-- start housekeeping
cfxOwnedZones.houseKeeping()
trigger.action.outText("cx/x owned zones v".. cfxOwnedZones.version .. " started", 30)
return true
end

File diff suppressed because it is too large Load Diff

View File

@ -1,294 +1,87 @@
cfxPlayerScoreUI = {}
cfxPlayerScoreUI.version = "1.0.3"
cfxPlayerScoreUI.version = "2.0.0"
cfxPlayerScoreUI.verbose = false
--[[-- VERSION HISTORY
- 1.0.2 - initial version
- 1.0.3 - module check
- 2.0.0 - removed cfxPlayer dependency, handles own commands
--]]--
-- WARNING: REQUIRES cfxPlayerScore to work.
-- WARNING: ASSUMES SINGLE_PLAYER GROUPS!
cfxPlayerScoreUI.requiredLibs = {
"cfxPlayerScore", -- this is doing score keeping
"cfxPlayer", -- player events, comms
}
-- find & command cfxGroundTroops-based jtacs
-- UI installed via OTHER for all groups with players
-- module based on xxxGrpUI and jtacUI
cfxPlayerScoreUI.groupConfig = {} -- all inited group private config data
cfxPlayerScoreUI.simpleCommands = true -- if true, f10 other invokes directly
--
-- C O N F I G H A N D L I N G
-- =============================
--
-- Each group has their own config block that can be used to
-- store group-private data and configuration items.
--
function cfxPlayerScoreUI.resetConfig(conf)
end
function cfxPlayerScoreUI.createDefaultConfig(theGroup)
local conf = {}
conf.theGroup = theGroup
conf.name = theGroup:getName()
conf.id = theGroup:getID()
conf.coalition = theGroup:getCoalition()
cfxPlayerScoreUI.resetConfig(conf)
conf.mainMenu = nil; -- this is where we store the main menu if we branch
conf.myCommands = nil; -- this is where we store the commands if we branch
return conf
end
-- getConfigFor group will allocate if doesn't exist in DB
-- and add to it
function cfxPlayerScoreUI.getConfigForGroup(theGroup)
if not theGroup then
trigger.action.outText("+++WARNING: cfxPlayerScoreUI nil group in getConfigForGroup!", 30)
return nil
end
local theName = theGroup:getName()
local c = cfxPlayerScoreUI.getConfigByGroupName(theName) -- we use central accessor
if not c then
c = cfxPlayerScoreUI.createDefaultConfig(theGroup)
cfxPlayerScoreUI.groupConfig[theName] = c -- should use central accessor...
end
return c
end
function cfxPlayerScoreUI.getConfigByGroupName(theName) -- DOES NOT allocate when not exist
if not theName then return nil end
return cfxPlayerScoreUI.groupConfig[theName]
end
function cfxPlayerScoreUI.getConfigForUnit(theUnit)
-- simple one-off step by accessing the group
if not theUnit then
trigger.action.outText("+++WARNING: cfxPlayerScoreUI nil unit in getConfigForUnit!", 30)
return nil
end
local theGroup = theUnit:getGroup()
return getConfigForGroup(theGroup)
end
--
--
-- M E N U H A N D L I N G
-- =========================
--
--
function cfxPlayerScoreUI.clearCommsSubmenus(conf)
if conf.myCommands then
for i=1, #conf.myCommands do
missionCommands.removeItemForGroup(conf.id, conf.myCommands[i])
end
end
conf.myCommands = {}
end
function cfxPlayerScoreUI.removeCommsFromConfig(conf)
cfxPlayerScoreUI.clearCommsSubmenus(conf)
if conf.myMainMenu then
missionCommands.removeItemForGroup(conf.id, conf.myMainMenu)
conf.myMainMenu = nil
end
end
-- this only works in single-unit groups. may want to check if group
-- has disappeared
function cfxPlayerScoreUI.removeCommsForUnit(theUnit)
if not theUnit then return end
if not theUnit:isExist() then return end
-- perhaps add code: check if group is empty
local conf = cfxPlayerScoreUI.getConfigForUnit(theUnit)
cfxPlayerScoreUI.removeCommsFromConfig(conf)
end
function cfxPlayerScoreUI.removeCommsForGroup(theGroup)
if not theGroup then return end
if not theGroup:isExist() then return end
local conf = cfxPlayerScoreUI.getConfigForGroup(theGroup)
cfxPlayerScoreUI.removeCommsFromConfig(conf)
end
--
-- set main root in F10 Other. All sub menus click into this
--
--function cfxPlayerScoreUI.isEligibleForMenu(theGroup)
-- return true
--end
function cfxPlayerScoreUI.setCommsMenuForUnit(theUnit)
if not theUnit then
trigger.action.outText("+++WARNING: cfxPlayerScoreUI nil UNIT in setCommsMenuForUnit!", 30)
return
end
if not theUnit:isExist() then return end
local theGroup = theUnit:getGroup()
cfxPlayerScoreUI.setCommsMenu(theGroup)
end
function cfxPlayerScoreUI.setCommsMenu(theGroup)
-- depending on own load state, we set the command structure
-- it begins at 10-other, and has 'grpUI' as main menu with submenus
-- as required
if not theGroup then return end
if not theGroup:isExist() then return end
-- we test here if this group qualifies for
-- the menu. if not, exit
--if not cfxPlayerScoreUI.isEligibleForMenu(theGroup) then return end
local conf = cfxPlayerScoreUI.getConfigForGroup(theGroup)
conf.id = theGroup:getID(); -- we do this ALWAYS so it is current even after a crash
if cfxPlayerScoreUI.simpleCommands then
-- we install directly in F-10 other
if not conf.myMainMenu then
local commandTxt = "Score / Kills"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
nil,
cfxPlayerScoreUI.redirectCommandX,
{conf, "score"}
)
conf.myMainMenu = theCommand
end
return
end
-- ok, first, if we don't have an F-10 menu, create one
if not (conf.myMainMenu) then
conf.myMainMenu = missionCommands.addSubMenuForGroup(conf.id, 'Score / Kills')
end
-- clear out existing commands
cfxPlayerScoreUI.clearCommsSubmenus(conf)
-- now we have a menu without submenus.
-- add our own submenus
cfxPlayerScoreUI.addSubMenus(conf)
end
function cfxPlayerScoreUI.addSubMenus(conf)
-- add menu items to choose from after
-- user clickedf on MAIN MENU. In this implementation
-- they all result invoked methods
local commandTxt = "Show Score / Kills"
local theCommand = missionCommands.addCommandForGroup(
conf.id,
commandTxt,
conf.myMainMenu,
cfxPlayerScoreUI.redirectCommandX,
{conf, "score"}
)
table.insert(conf.myCommands, theCommand)
end
--
-- each menu item has a redirect and timed invoke to divorce from the
-- no-debug zone in the menu invocation. Delay is .1 seconds
--
cfxPlayerScoreUI.rootCommands = {} -- by unit's group name, for player aircraft
-- redirect: avoid the debug environ of missionCommand
function cfxPlayerScoreUI.redirectCommandX(args)
timer.scheduleFunction(cfxPlayerScoreUI.doCommandX, args, timer.getTime() + 0.1)
end
function cfxPlayerScoreUI.doCommandX(args)
local conf = args[1] -- < conf in here
local what = args[2] -- < second argument in here
local theGroup = conf.theGroup
-- now fetch the first player that drives a unit in this group
-- a simpler method would be to access conf.primeUnit
local groupName = args[1]
local playerName = args[2]
local what = args[3] -- "score" or other commands
local theGroup = Group.getByName(groupName)
local gid = theGroup:getID()
local playerName, playerUnit = cfxPlayer.getFirstGroupPlayerName(theGroup)
if playerName == nil or playerUnit == nil then
trigger.action.outText("scoreUI: nil player name or unit for group " .. theGroup:getName(), 30)
if not cfxPlayerScore.scoreTextForPlayerNamed then
trigger.action.outText("***pSGui: CANNOT FIND PlayerScore MODULE", 30)
return
end
local desc = cfxPlayerScore.scoreTextForPlayerNamed(playerName)
trigger.action.outTextForGroup(conf.id, desc, 30)
trigger.action.outSoundForGroup(conf.id, "Quest Snare 3.wav")
trigger.action.outTextForGroup(gid, desc, 30)
trigger.action.outSoundForGroup(gid, "Quest Snare 3.wav")
end
--
-- G R O U P M A N A G E M E N T
-- event handling: we are only interested in birth events
-- for player aircraft
--
-- Group Management is required to make sure all groups
-- receive a comms menu and that they receive a clean-up
-- when required
--
-- Callbacks are provided by cfxPlayer module to which we
-- subscribe during init
--
function cfxPlayerScoreUI.playerChangeEvent(evType, description, player, data)
if evType == "newGroup" then
cfxPlayerScoreUI.setCommsMenu(data.group)
return
end
function cfxPlayerScoreUI:onEvent(event)
if event.id ~= 15 then return end -- only birth
if not event.initiator then return end -- no initiator, no joy
local theUnit = event.initiator
if not theUnit.getPlayerName then return end -- no player name, bye!
local playerName = theUnit:getPlayerName()
if not playerName then return end
if evType == "removeGroup" then
-- we must remove the comms menu for this group else we try to add another one to this group later
local conf = cfxPlayerScoreUI.getConfigByGroupName(data.name)
if conf then
cfxPlayerScoreUI.removeCommsFromConfig(conf) -- remove menus
cfxPlayerScoreUI.resetConfig(conf) -- re-init this group for when it re-appears
else
trigger.action.outText("+++ scoreUI: can't retrieve group <" .. data.name .. "> config: not found!", 30)
-- so now we know it's a player plane. get group name
local theGroup = theUnit:getGroup()
local groupName = theGroup:getName()
local gid = theGroup:getID()
-- see if this group already has a score command
if cfxPlayerScoreUI.rootCommands[groupName] then
-- need re-init to store new pilot name
if cfxPlayerScoreUI.verbose then
trigger.action.outText("++pSGui: group <" .. groupName .. "> already has score menu, removing.", 30)
end
return
missionCommands.removeItemForGroup(gid, cfxPlayerScoreUI.rootCommands[groupName])
cfxPlayerScoreUI.rootCommands[groupName] = nil
end
-- we need to install a group menu item for scores.
-- will persist through death
local commandTxt = "Show Score / Kills"
local theCommand = missionCommands.addCommandForGroup(
gid,
commandTxt,
nil, -- root level
cfxPlayerScoreUI.redirectCommandX,
{groupName, playerName, "score"}
)
cfxPlayerScoreUI.rootCommands[groupName] = theCommand
if cfxPlayerScoreUI.verbose then
trigger.action.outText("++pSGui: installed player score menu for group <" .. groupName .. ">", 30)
end
end
--
-- Start
--
function cfxPlayerScoreUI.start()
if not dcsCommon.libCheck("cfx PlayerScoreUI",
cfxPlayerScoreUI.requiredLibs)
then
return false
end
-- iterate existing groups so we have a start situation
-- now iterate through all player groups and install the Assault Troop Menu
allPlayerGroups = cfxPlayerGroups -- cfxPlayerGroups is a global, don't fuck with it!
-- contains per group player record. Does not resolve on unit level!
for gname, pgroup in pairs(allPlayerGroups) do
local theUnit = pgroup.primeUnit -- get any unit of that group
cfxPlayerScoreUI.setCommsMenuForUnit(theUnit) -- set up
end
-- now install the new group notifier to install Assault Troops menu
function cfxPlayerScoreUI.start()
-- install the event handler for new player planes
world.addEventHandler(cfxPlayerScoreUI)
cfxPlayer.addMonitor(cfxPlayerScoreUI.playerChangeEvent)
trigger.action.outText("cf/x cfxPlayerScoreUI v" .. cfxPlayerScoreUI.version .. " started", 30)
return true
end
@ -296,7 +89,6 @@ end
--
-- GO GO GO
--
if not cfxPlayerScoreUI.start() then
cfxPlayerScoreUI = nil
trigger.action.outText("cf/x PlayerScore UI aborted: missing libraries", 30)

View File

@ -1,5 +1,5 @@
cfxZones = {}
cfxZones.version = "3.0.6"
cfxZones.version = "3.0.8"
-- cf/x zone management module
-- reads dcs zones and makes them accessible and mutable
@ -125,6 +125,8 @@ cfxZones.version = "3.0.6"
- 3.0.6 - new createSimplePolyZone()
- new createSimpleQuadZone()
- 3.0.7 - getPoint() can also get land y when passing true as second param
- 3.0.8 - new cfxZones.pointInOneOfZones(thePoint, zoneArray, useOrig)
--]]--
cfxZones.verbose = false
cfxZones.caseSensitiveProperties = false -- set to true to make property names case sensitive
@ -1141,6 +1143,15 @@ function cfxZones.markZoneWithSmokePolarRandom(theZone, radius, smokeColor)
cfxZones.markZoneWithSmokePolar(theZone, radius, degrees, smokeColor)
end
function cfxZones.pointInOneOfZones(thePoint, zoneArray, useOrig)
if not zoneArray then zoneArray = cfxZones.zones end
for idx, theZone in pairs(zoneArray) do
local isIn, percent, dist = cfxZones.pointInZone(thePoint, theZone, useOrig)
if isIn then return isIn, percent, dist, theZone end
end
return false, 0, 0, nil
end
-- unitInZone returns true if theUnit is inside the zone
-- the second value returned is the percentage of distance

View File

@ -1,5 +1,5 @@
countDown = {}
countDown.version = "1.3.1"
countDown.version = "1.3.2"
countDown.verbose = false
countDown.ups = 1
countDown.requiredLibs = {
@ -27,7 +27,8 @@ countDown.requiredLibs = {
- new reset? input
- improved verbosity
- zone-local verbosity
1.3.2 - enableCounter? to balande disableCounter?
--]]--
countDown.counters = {}
@ -141,12 +142,18 @@ function countDown.createCountDownWithZone(theZone)
theZone.counterOut = cfxZones.getStringFromZoneProperty(theZone, "counterOut!", "<none>")
end
-- disableFlag
-- disableFlag/enableFlag
theZone.counterDisabled = false
if cfxZones.hasProperty(theZone, "disableCounter?") then
theZone.disableCounterFlag = cfxZones.getStringFromZoneProperty(theZone, "disableCounter?", "<none>")
theZone.disableCounterFlagVal = cfxZones.getFlagValue(theZone.disableCounterFlag, theZone)
end
if cfxZones.hasProperty(theZone, "enableCounter?") then
theZone.enableCounterFlag = cfxZones.getStringFromZoneProperty(theZone, "enableCounter?", "<none>")
theZone.enableCounterFlagVal = cfxZones.getFlagValue(theZone.enableCounterFlag, theZone)
end
end
--
@ -260,6 +267,12 @@ function countDown.update()
aZone.counterDisabled = true
end
if cfxZones.testZoneFlag(aZone, aZone.enableCounterFlag, aZone.ctdwnTriggerMethod, "enableCounterFlagVal") then
if countDown.verbose then
trigger.action.outText("+++cntD: ENabling counter " .. aZone.name, 30)
end
aZone.counterDisabled = false
end
end
end

View File

@ -145,7 +145,9 @@ dcsCommon.version = "2.8.5"
- new rotatePointAroundPointRad()
- getClosestAirbaseTo() now supports passing list of air bases
2.8.5 - better guard in getGroupUnit()
2.8.6 - phonetic helpers
new spellString()
--]]--
@ -3221,7 +3223,7 @@ function dcsCommon.LSR(a, num)
end
--
-- string windcards
-- string wildcards
--
function dcsCommon.processStringWildcards(inMsg)
-- Replace STATIC bits of message like CR and zone name
@ -3236,6 +3238,82 @@ function dcsCommon.processStringWildcards(inMsg)
return outMsg
end
--
-- phonetic alphabet
--
dcsCommon.alphabet = {
a = "alpha",
b = "bravo",
c = "charlie",
d = "delta",
e = "echo",
f = "foxtrot",
g = "golf",
h = "hotel",
i = "india",
j = "juliet",
k = "kilo",
l = "lima",
m = "mike",
n = "november",
o = "oscar",
p = "papa",
q = "quebec",
r = "romeo",
s = "sierra",
t = "tango",
u = "uniform",
v = "victor",
w = "whiskey",
x = "x-ray",
y = "yankee",
z = "zulu",
["0"] = "zero",
["1"] = "wun",
["2"] = "too",
["3"] = "tree",
["4"] = "fower",
["5"] = "fife" ,
["6"] = "six",
["7"] = "seven",
["8"] = "att",
["9"] = "niner",
[" "] = "break",
}
function dcsCommon.letter(inChar)
local theChar = ""
if type(inChar == "string") then
if #inChar < 1 then return "#ERROR0#" end
inChar = string.lower(inChar)
theChar = string.sub(inChar, 1, 1)
elseif type(inChar == "number") then
if inChar > 255 then return "#ERROR>#" end
if inChar < 0 then return "#ERROR<#" end
theChar = char(inChar)
else
return "#ERRORT#"
end
-- trigger.action.outText("doing <" .. theChar .. ">", 30)
local a = dcsCommon.alphabet[theChar]
if a == nil then a = "#ERROR?#" end
return a
end
function dcsCommon.spellString(inString)
local res = ""
local first = true
for i = 1, #inString do
local c = inString:sub(i,i)
if first then
res = dcsCommon.letter(c)
first = false
else
res = res .. " " .. dcsCommon.letter(c)
end
end
return res
end
--
-- SEMAPHORES

886
modules/factoryZone.lua Normal file
View File

@ -0,0 +1,886 @@
factoryZone = {}
factoryZone.version = "1.0.0"
factoryZone.verbose = false
factoryZone.name = "factoryZone"
--[[-- VERSION HISTORY
1.0.0 - refactored production part from cfxOwnedZones 1.xpcall
--]]--
factoryZone.requiredLibs = {
"dcsCommon", -- common is of course needed for everything
-- pretty stupid to check for this since we
-- need common to invoke the check, but anyway
"cfxZones", -- Zones, of course
"cfxCommander", -- to make troops do stuff
}
factoryZone.zones = {} -- my factory zones
factoryZone.ups = 1
factoryZone.initialized = false
factoryZone.defendingTime = 100 -- seconds until new defenders are produced
factoryZone.attackingTime = 300 -- seconds until new attackers are produced
factoryZone.shockTime = 200 -- 'shocked' period of inactivity
factoryZone.repairTime = 200 -- time until we raplace one lost unit, also repairs all other units to 100%
-- persistence: all attackers we ever sent out.
-- is regularly verified and cut to size
factoryZone.spawnedAttackers = {}
-- factoryZone is a module that managers production of units
-- inside zones and can switch production based on who owns the
-- zone. Zone ownership can by dynamic (by using OwnedZones or
-- using scripts to change the 'owner' flag
--
-- *** EXTENTDS ZONES ***
--
-- zone attributes when owned
-- owner: coalition that owns the zone. Managed externally
-- status: FSM for spawning
-- defendersRED/BLUE - coma separated type string for the group to spawn on defense cycle completion
-- attackersRED/BLUE - as above for attack cycle.
-- timeStamp - time when zone switched into current state
-- spawnRadius - overrides zone's radius when placing defenders. can be use to place defenders inside or outside zone itself
-- formation - defender's formation
-- attackFormation - attackers formation
-- attackRadius - radius of circle in which attackers are spawned. informs formation
-- attackDelta - polar coord: r from zone center where attackers are spawned
-- attackPhi - polar degrees where attackers are to be spawned
-- paused - will not spawn. default is false
-- unbeatable - can't be conquered by other side. default is false
-- untargetable - will not be targeted by either side. make unbeatable
-- owned zones untargetable, or they'll become a troop magnet for
-- zoneAttackers
--
-- M I S C
--
function factoryZone.getFactoryZoneByName(zName)
for zKey, theZone in pairs (factoryZone.zones) do
if theZone.name == zName then return theZone end
end
return nil
end
function factoryZone.addFactoryZone(aZone)
aZone.worksFor = cfxZones.getCoalitionFromZoneProperty(aZone, "factory", 0) -- currently unused, have RED/BLUE separate types
aZone.state = "init"
aZone.timeStamp = timer.getTime()
aZone.defendersRED = cfxZones.getStringFromZoneProperty(aZone, "defendersRED", "none")
aZone.defendersBLUE = cfxZones.getStringFromZoneProperty(aZone, "defendersBLUE", "none")
if cfxZones.hasProperty(aZone, "attackersRED") then
-- legacy support
aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "attackersRED", "none")
else
aZone.attackersRED = cfxZones.getStringFromZoneProperty(aZone, "productionRED", "none")
end
if cfxZones.hasProperty(aZone, "attackersBLUE") then
-- legacy support
aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "attackersBLUE", "none")
else
aZone.attackersBLUE = cfxZones.getStringFromZoneProperty(aZone, "productionBLUE", "none")
end
aZone.formation = cfxZones.getStringFromZoneProperty(aZone, "formation", "circle_out")
aZone.attackFormation = cfxZones.getStringFromZoneProperty(aZone, "attackFormation", "circle_out") -- cfxZones.getZoneProperty(aZone, "attackFormation")
aZone.spawnRadius = cfxZones.getNumberFromZoneProperty(aZone, "spawnRadius", aZone.radius-5) -- "-5" so they remaininside radius
aZone.attackRadius = cfxZones.getNumberFromZoneProperty(aZone, "attackRadius", aZone.radius)
aZone.attackDelta = cfxZones.getNumberFromZoneProperty(aZone, "attackDelta", 10) -- aZone.radius)
aZone.attackPhi = cfxZones.getNumberFromZoneProperty(aZone, "attackPhi", 0)
aZone.paused = cfxZones.getBoolFromZoneProperty(aZone, "paused", false)
aZone.factoryOwner = aZone.owner -- copy so we can compare next round
-- pause? and activate?
if cfxZones.hasProperty(aZone, "pause?") then
aZone.pauseFlag = cfxZones.getStringFromZoneProperty(aZone, "pause?", "none")
aZone.lastPauseValue = trigger.misc.getUserFlag(aZone.pauseFlag)
end
if cfxZones.hasProperty(aZone, "activate?") then
aZone.activateFlag = cfxZones.getStringFromZoneProperty(aZone, "activate?", "none")
aZone.lastActivateValue = trigger.misc.getUserFlag(aZone.activateFlag)
end
aZone.factoryTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "triggerMethod", "change")
if cfxZones.hasProperty(aZone, "factoryTriggerMethod") then
aZone.factoryTriggerMethod = cfxZones.getStringFromZoneProperty(aZone, "factoryTriggerMethod", "change")
end
aZone.untargetable = cfxZones.getBoolFromZoneProperty(aZone, "untargetable", false)
factoryZone.zones[aZone.name] = aZone
factoryZone.verifyZone(aZone)
end
function factoryZone.verifyZone(aZone)
-- do some sanity checks
if not cfxGroundTroops and (aZone.attackersRED ~= "none" or aZone.attackersBLUE ~= "none") then
trigger.action.outText("+++factZ: " .. aZone.name .. " attackers need cfxGroundTroops to function", 30)
end
end
function factoryZone.getEnemyZonesFor(aCoalition)
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local enemyZones = {}
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
local ourEnemy = dcsCommon.getEnemyCoalitionFor(aCoalition)
for zKey, aZone in pairs(allZones) do
if aZone.owner == ourEnemy then -- only check enemy owned zones
-- note: will include untargetable zones
table.insert(enemyZones, aZone)
end
end
return enemyZones
end
function factoryZone.getNearestOwnedZoneToPoint(aPoint)
local shortestDist = math.huge
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local closestZone = nil
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
for zKey, aZone in pairs(allZones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and
currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function factoryZone.getNearestOwnedZone(theZone)
local shortestDist = math.huge
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local closestZone = nil
local aPoint = cfxZones.getPoint(theZone)
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
for zKey, aZone in pairs(allZones) do
local zPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
return closestZone, shortestDist
end
function factoryZone.getNearestEnemyOwnedZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies
local zPoint = cfxZones.getPoint(theZone)
for zKey, aZone in pairs(allZones) do
if targetNeutral then
-- return all zones that do not belong to us
if aZone.owner ~= theZone.owner then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(aPoint, zPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- return zones that are taken by the Enenmy
if aZone.owner == ourEnemy then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function factoryZone.getNearestFriendlyZone(theZone, targetNeutral)
if not targetNeutral then targetNeutral = false else targetNeutral = true end
local shortestDist = math.huge
local closestZone = nil
local ourEnemy = dcsCommon.getEnemyCoalitionFor(theZone.owner)
if not ourEnemy then return nil end -- we called for a neutral zone. they have no enemies nor friends, all zones would be legal.
local zPoint = cfxZones.getPoint(theZone)
-- when cfxOwnedZones is present, or it will return only those
-- else it scans all zones from cfxZones
local allZones = cfxZones.zones
if cfxOwnedZones then
allZones = cfxOwnedZones.zones
end
for zKey, aZone in pairs(allZones) do
if targetNeutral then
-- target all zones that do not belong to the enemy
if aZone.owner ~= ourEnemy then
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
else
-- only target zones that are taken by us
if aZone.owner == theZone.owner then -- only check own zones
local aPoint = cfxZones.getPoint(aZone)
currDist = dcsCommon.dist(zPoint, aPoint)
if aZone.untargetable ~= true and currDist < shortestDist then
shortestDist = currDist
closestZone = aZone
end
end
end
end
return closestZone, shortestDist
end
function factoryZone.enemiesRemaining(aZone)
if factoryZone.getNearestEnemyOwnedZone(aZone) then return true end
return false
end
function factoryZone.spawnAttackTroops(theTypes, aZone, aCoalition, aFormation)
local unitTypes = {} -- build type names
-- split theTypes into an array of types
unitTypes = dcsCommon.splitString(theTypes, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
-- simply exit, no troops specified
if factoryZone.verbose then
trigger.action.outText("+++factZ: no attackers for " .. aZone.name .. ". exiting", 30)
end
return
end
if factoryZone.verbose then
trigger.action.outText("+++factZ: spawning attackers for " .. aZone.name, 30)
end
local spawnPoint = {x = aZone.point.x, y = aZone.point.y, z = aZone.point.z} -- copy struct
local rads = aZone.attackPhi * 0.01745
spawnPoint.x = spawnPoint.x + math.cos(aZone.attackPhi) * aZone.attackDelta
spawnPoint.y = spawnPoint.y + math.sin(aZone.attackPhi) * aZone.attackDelta
local spawnZone = cfxZones.createSimpleZone("attkSpawnZone", spawnPoint, aZone.attackRadius)
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
aCoalition, -- theCountry,
aZone.name .. " (A) " .. dcsCommon.numberUUID(), -- must be unique
spawnZone,
unitTypes,
aFormation, -- outward facing
0)
return theGroup, theData
end
function factoryZone.spawnDefensiveTroops(theTypes, aZone, aCoalition, aFormation)
local unitTypes = {} -- build type names
-- split theTypes into an array of types
unitTypes = dcsCommon.splitString(theTypes, ",")
if #unitTypes < 1 then
table.insert(unitTypes, "Soldier M4") -- make it one m4 trooper as fallback
-- simply exit, no troops specified
if factoryZone.verbose then
trigger.action.outText("+++factZ: no defenders for " .. aZone.name .. ". exiting", 30)
end
return
end
--local theCountry = dcsCommon.coalition2county(aCoalition)
local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius)
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
aCoalition, --theCountry,
aZone.name .. " (D) " .. dcsCommon.numberUUID(), -- must be unique
spawnZone, unitTypes,
aFormation, -- outward facing
0)
return theGroup, theData
end
--
-- U P D A T E
--
function factoryZone.sendOutAttackers(aZone)
-- sanity check: never done for non-neutral zones
if aZone.owner == 0 then
if aZone.verbose or factoryZone.verbose then
trigger.action.outText("+++factZ: SendAttackers invoked for NEUTRAL zone <" .. aZone.name .. ">", 30)
end
return
end
-- only spawn if there are zones to attack
if not factoryZone.enemiesRemaining(aZone) then
if factoryZone.verbose then
trigger.action.outText("+++factZ - no enemies, resting ".. aZone.name, 30)
end
return
end
if factoryZone.verbose then
trigger.action.outText("+++factZ - attack cycle for ".. aZone.name, 30)
end
-- step one: get the attackers
local attackers = aZone.attackersRED;
if (aZone.owner == 2) then attackers = aZone.attackersBLUE end
if attackers == "none" then return end
local theGroup, theData = factoryZone.spawnAttackTroops(attackers, aZone, aZone.owner, aZone.attackFormation)
local troopData = {}
troopData.groupData = theData
troopData.orders = "attackOwnedZone" -- lazy coding!
troopData.side = aZone.owner
factoryZone.spawnedAttackers[theData.name] = troopData
-- submit them to ground troops handler as zoneseekers
-- and our groundTroops module will handle the rest
if cfxGroundTroops then
local troops = cfxGroundTroops.createGroundTroops(theGroup)
troops.orders = "attackOwnedZone"
troops.side = aZone.owner
cfxGroundTroops.addGroundTroopsToPool(troops) -- hand off to ground troops
else
if factoryZone.verbose then
trigger.action.outText("+++ Owned Zones: no ground troops module on send out attackers", 30)
end
end
end
function factoryZone.repairDefenders(aZone)
-- sanity check: never done for non-neutral zones
if aZone.owner == 0 then
if aZone.verbose or factoryZone.verbose then
trigger.action.outText("+++factZ: repairDefenders invoked for NEUTRAL zone <" .. aZone.name .. ">", 30)
end
return
end
-- find a unit that is missing from my typestring and replace it
-- one by one until we are back to full strength
-- step one: get the defenders and create a type array
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
local unitTypes = {} -- build type names
-- if none, we are done
if defenders == "none" then return end
-- split theTypes into an array of types
allTypes = dcsCommon.trimArray(
dcsCommon.splitString(defenders, ",")
)
local livingTypes = {} -- init to emtpy, so we can add to it if none are alive
if (aZone.defenders) then
-- some remain. add one of the killed
livingTypes = dcsCommon.getGroupTypes(aZone.defenders)
-- we now iterate over the living types, and remove their
-- counterparts from the allTypes. We then take the first that
-- is left
if #livingTypes > 0 then
for key, aType in pairs (livingTypes) do
if not dcsCommon.findAndRemoveFromTable(allTypes, aType) then
trigger.action.outText("+++factZ WARNING: found unmatched type <" .. aType .. "> while trying to repair defenders for ".. aZone.name, 30)
else
-- all good
end
end
end
end
-- when we get here, allTypes is reduced to those that have been killed
if #allTypes < 1 then
trigger.action.outText("+++factZ: WARNING: all types exist when repairing defenders for ".. aZone.name, 30)
else
table.insert(livingTypes, allTypes[1]) -- we simply use the first that we find
end
-- remove the old defenders
if aZone.defenders then
aZone.defenders:destroy()
end
-- now livingTypes holds the full array of units we need to spawn
local theCountry = dcsCommon.getACountryForCoalition(aZone.owner)
local spawnZone = cfxZones.createSimpleZone("spawnZone", aZone.point, aZone.spawnRadius)
local theGroup, theData = cfxZones.createGroundUnitsInZoneForCoalition (
aZone.owner, -- was wrongly: theCountry
aZone.name .. dcsCommon.numberUUID(), -- must be unique
spawnZone,
livingTypes,
aZone.formation, -- outward facing
0)
aZone.defenders = theGroup
aZone.lastDefenders = theGroup:getSize()
end
function factoryZone.inShock(aZone)
-- a unit was destroyed, everyone else is in shock, no rerpairs
-- group can re-shock when another unit is destroyed
end
function factoryZone.spawnDefenders(aZone)
-- sanity check: never done for non-neutral zones
if aZone.owner == 0 then
if aZone.verbose or factoryZone.verbose then
trigger.action.outText("+++factZ: spawnDefenders invoked for NEUTRAL zone <" .. aZone.name .. ">", 30)
end
return
end
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
-- before we spawn new defenders, remove the old ones
if aZone.defenders then
if aZone.defenders:isExist() then
aZone.defenders:destroy()
end
aZone.defenders = nil
end
-- if 'none', simply exit
if defenders == "none" then return end
local theGroup, theData = factoryZone.spawnDefensiveTroops(defenders, aZone, aZone.owner, aZone.formation)
-- the troops reamin, so no orders to move, no handing off to ground troop manager
aZone.defenders = theGroup
aZone.defenderData = theData -- used for persistence
if theGroup then
aZone.lastDefenders = theGroup:getInitialSize()
else
trigger.action.outText("+++factZ: WARNING: spawned no defenders for ".. aZone.name, 30)
aZone.defenderData = nil
end
end
--
-- per-zone update, run down the FSM to determine what to do.
-- FSM uses timeStamp since when state was set. Possible states are
-- - init -- has just been inited for the first time. will usually immediately produce defenders,
-- and then transition to defending
-- - catured -- has just been captured. transition to defending
-- - defending -- wait until timer has reached goal, then produce defending units and transition to attacking.
-- - attacking -- wait until timer has reached goal, and then produce attacking units and send them to closest enemy zone.
-- state is interrupted as soon as a defensive unit is lost. state then goes to defending with timer starting
-- - idle - do nothing, zone's actions are turned off
-- - shocked -- a unit was destroyed. group is in shock for a time until it starts repairing. If another unit is
-- destroyed during the shocked period, the timer resets to zero and repairs are delayed
-- - repairing -- as long as we aren't at full strength, units get replaced one by one until at full strength
-- each time the timer counts down, another missing unit is replaced, and all other unit's health
-- is reset to 100%
--
-- a Zone with the paused attribute set to true will cause it to not do anything
--
-- check if defenders are specified
function factoryZone.usesDefenders(aZone)
if aZone.owner == 0 then return false end
local defenders = aZone.defendersRED;
if (aZone.owner == 2) then defenders = aZone.defendersBLUE end
return defenders ~= "none"
end
function factoryZone.usesAttackers(aZone)
if aZone.owner == 0 then return false end
local attackers = aZone.attackersRED;
if (aZone.owner == 2) then defenders = aZone.attackersBLUE end
return attackers ~= "none"
end
function factoryZone.updateZoneProduction(aZone)
-- a zone can be paused, causing it to not progress anything
-- even if zone status is still init, will NOT produce anything
-- if paused is on.
if aZone.paused then return end
nextState = aZone.state;
-- first, check if my defenders have been attacked and one of them has been killed
-- if so, we immediately switch to 'shocked'
if factoryZone.usesDefenders(aZone) and
aZone.defenders then
-- we have defenders
if aZone.defenders:isExist() then
-- isee if group was damaged
if not aZone.lastDefenders then
-- fresh group, probably from persistence, needs init
aZone.lastDefenders = -1
end
if aZone.defenders:getSize() < aZone.lastDefenders then
-- yes, at least one unit destroyed
aZone.timeStamp = timer.getTime()
aZone.lastDefenders = aZone.defenders:getSize()
if aZone.lastDefenders == 0 then
aZone.defenders = nil
end
aZone.state = "shocked"
return
else
aZone.lastDefenders = aZone.defenders:getSize()
end
else
-- group was destroyed. erase link, and go into shock for the last time
aZone.state = "shocked"
aZone.timeStamp = timer.getTime()
aZone.lastDefenders = 0
aZone.defenders = nil
return
end
end
if aZone.state == "init" then
-- during init we instantly create the defenders since
-- we assume the zone existed already
if aZone.owner > 0 then
factoryZone.spawnDefenders(aZone)
-- now drop into attacking mode to produce attackers
nextState = "attacking"
else
nextState = "idle"
end
aZone.timeStamp = timer.getTime()
elseif aZone.state == "idle" then
-- nothing to do, zone is effectively switched off.
-- used for neutal zones or when forced to turn off
-- in some special cases
elseif aZone.state == "captured" then
-- start the clock on defenders
nextState = "defending"
aZone.timeStamp = timer.getTime()
if factoryZone.verbose then
trigger.action.outText("+++factZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
elseif aZone.state == "defending" then
if timer.getTime() > aZone.timeStamp + factoryZone.defendingTime then
factoryZone.spawnDefenders(aZone)
-- now drop into attacking mode to produce attackers
nextState = "attacking"
aZone.timeStamp = timer.getTime()
if factoryZone.verbose then
trigger.action.outText("+++factZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
elseif aZone.state == "repairing" then
-- we are currently rebuilding defenders unit by unit
if timer.getTime() > aZone.timeStamp + factoryZone.repairTime then
aZone.timeStamp = timer.getTime()
-- wait's up, repair one defender, then check if full strength
factoryZone.repairDefenders(aZone)
-- see if we are full strenght and if so go to attack, else set timer to reair the next unit
if aZone.defenders and aZone.defenders:isExist() and aZone.defenders:getSize() >= aZone.defenders:getInitialSize() then
-- we are at max size, time to produce some attackers
-- progress to next state
nextState = "attacking"
aZone.timeStamp = timer.getTime()
if factoryZone.verbose then
trigger.action.outText("+++factZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
end
elseif aZone.state == "shocked" then
-- we are currently rebuilding defenders unit by unit
if timer.getTime() > aZone.timeStamp + factoryZone.shockTime then
nextState = "repairing"
aZone.timeStamp = timer.getTime()
if factoryZone.verbose then
trigger.action.outText("+++factZ: State " .. aZone.state .. " to " .. nextState .. " for " .. aZone.name, 30)
end
end
elseif aZone.state == "attacking" then
if timer.getTime() > aZone.timeStamp + factoryZone.attackingTime then
factoryZone.sendOutAttackers(aZone)
-- reset timer
aZone.timeStamp = timer.getTime()
if factoryZone.verbose then
trigger.action.outText("+++factZ: State " .. aZone.state .. " reset for " .. aZone.name, 30)
end
end
else
-- unknown zone state
end
aZone.state = nextState
end
function factoryZone.GC()
-- GC run. remove all my dead remembered troops
local before = #factoryZone.spawnedAttackers
local filteredAttackers = {}
for gName, gData in pairs (factoryZone.spawnedAttackers) do
-- all we need to do is get the group of that name
-- and if it still returns units we are fine
local gameGroup = Group.getByName(gName)
if gameGroup and gameGroup:isExist() and gameGroup:getSize() > 0 then
filteredAttackers[gName] = gData
end
end
factoryZone.spawnedAttackers = filteredAttackers
if factoryZone.verbose then
trigger.action.outText("owned zones GC ran: before <" .. before .. ">, after <" .. #factoryZone.spawnedAttackers .. ">", 30)
end
end
function factoryZone.update()
factoryZone.updateSchedule = timer.scheduleFunction(factoryZone.update, {}, timer.getTime() + 1/factoryZone.ups)
-- iterate all zones to see if ownership has
-- changed
for idz, theZone in pairs(factoryZone.zones) do
local lastOwner = theZone.factoryOwner
local newOwner = theZone.owner
if (newOwner ~= lastOwner) then
theZone.state = "captured"
theZone.timeStamp = timer.getTime()
theZone.factoryOwner = theZone.owner
if theZone.verbose or factoryZone.verbose then
trigger.action.outText("+++factZ: detected factory <" .. theZone.name .. "> changed ownership from <" .. lastOwner .. "> to <" .. theZone.owner .. ">", 30)
end
end
-- see if pause/unpause was issued
if theZone.pauseFlag and cfxZones.testZoneFlag(theZone, theZone.pauseFlag, theZone.factoryTriggerMethod, "lastPauseValue") then
theZone.paused = true
end
if theZone.activateFlag and cfxZones.testZoneFlag(theZone, theZone.activateFlag, theZone.factoryTriggerMethod, "lastActivateValue") then
theZone.paused = false
end
-- do production for this zone
factoryZone.updateZoneProduction(theZone)
end -- iterating all zones
end
function factoryZone.houseKeeping()
timer.scheduleFunction(factoryZone.houseKeeping, {}, timer.getTime() + 5 * 60) -- every 5 minutes
factoryZone.GC()
end
--
-- load / save data
--
function factoryZone.saveData()
-- this is called from persistence when it's time to
-- save data. returns a table with all my data
local theData = {}
local allZoneData = {}
-- iterate all my zones and create data
for idx, theZone in pairs(factoryZone.zones) do
local zoneData = {}
if theZone.defenderData then
zoneData.defenderData = dcsCommon.clone(theZone.defenderData)
dcsCommon.synchGroupData(zoneData.defenderData)
end
zoneData.owner = theZone.owner
zoneData.state = theZone.state -- will prevent immediate spawn
-- since new zones are spawned with 'init'
allZoneData[theZone.name] = zoneData
end
-- now iterate all attack groups that we have spawned and that
-- (maybe) are still alive
factoryZone.GC() -- start with a GC run to remove all dead
local livingAttackers = {}
for gName, gData in pairs (factoryZone.spawnedAttackers) do
-- all we need to do is get the group of that name
-- and if it still returns units we are fine
-- spawnedAttackers is a [groupName] table with {.groupData, .orders, .side}
local gameGroup = Group.getByName(gName)
if gameGroup and gameGroup:isExist() then
if gameGroup:getSize() > 0 then
local sData = dcsCommon.clone(gData)
dcsCommon.synchGroupData(sData.groupData)
livingAttackers[gName] = sData
end
end
end
-- now write the info for the flags that we output for #red, etc
local flagInfo = {} -- no longer used
-- assemble the data
theData.zoneData = allZoneData
theData.attackers = livingAttackers
theData.flagInfo = flagInfo
-- return it
return theData
end
function factoryZone.loadData()
-- remember to draw in map with new owner
if not persistence then return end
local theData = persistence.getSavedDataForModule("factoryZone")
if not theData then
if factoryZone.verbose then
trigger.action.outText("factZ: no save date received, skipping.", 30)
end
return
end
-- theData contains the following tables:
-- zoneData: per-zone data
-- flagInfo: module-global flags
-- attackers: all spawned attackers that we feed to groundTroops
local allZoneData = theData.zoneData
for zName, zData in pairs(allZoneData) do
-- access zone
local theZone = factoryZone.getOwnedZoneByName(zName)
if theZone then
if zData.defenderData then
if theZone.defenders and theZone.defenders:isExist() then
-- should not happen, but so be it
theZone.defenders:destroy()
end
local gData = zData.defenderData
local cty = gData.cty
local cat = gData.cat
theZone.defenders = coalition.addGroup(cty, cat, gData)
theZone.defenderData = zData.defenderData
end
theZone.owner = zData.owner
theZone.factoryOwner = theZone.owner
theZone.state = zData.state
else
trigger.action.outText("factZ: load - data mismatch: cannot find zone <" .. zName .. ">, skipping zone.", 30)
end
end
-- now process all attackers
local allAttackers = theData.attackers
for gName, gdTroop in pairs(allAttackers) do
-- table is {.groupData, .orders, .side}
local gData = gdTroop.groupData
local orders = gdTroop.orders
local side = gdTroop.side
local cty = gData.cty
local cat = gData.cat
-- add to my own attacker queue so we can save later
local dClone = dcsCommon.clone(gdTroop)
factoryZone.spawnedAttackers[gName] = dClone
local theGroup = coalition.addGroup(cty, cat, gData)
if cfxGroundTroops then
local troops = cfxGroundTroops.createGroundTroops(theGroup)
troops.orders = orders
troops.side = side
cfxGroundTroops.addGroundTroopsToPool(troops) -- hand off to ground troops
end
end
-- now process module global flags
local flagInfo = theData.flagInfo
if flagInfo then
end
end
--
function factoryZone.readConfigZone(theZone)
if not theZone then theZone = cfxZones.createSimpleZone("factoryZoneConfig") end
factoryZone.name = "factoryZone" -- just in case, so we can access with cfxZones
factoryZone.verbose = cfxZones.getBoolFromZoneProperty(theZone, "verbose", false)
factoryZone.defendingTime = cfxZones.getNumberFromZoneProperty(theZone, "defendingTime", 100)
factoryZone.attackingTime = cfxZones.getNumberFromZoneProperty(theZone, "attackingTime", 300)
factoryZone.shockTime = cfxZones.getNumberFromZoneProperty(theZone, "shockTime", 200)
factoryZone.repairTime = cfxZones.getNumberFromZoneProperty(theZone, "repairTime", 200)
end
function factoryZone.init()
-- check libs
if not dcsCommon.libCheck("cfx Owned Zones",
factoryZone.requiredLibs) then
return false
end
-- read my config zone
local theZone = cfxZones.getZoneByName("factoryZoneConfig")
factoryZone.readConfigZone(theZone)
-- collect all owned zones by their 'factory' property
-- start the process
local pZones = cfxZones.zonesWithProperty("factory")
-- now add all zones to my zones table, and convert the owner property into
-- a proper attribute
for k, aZone in pairs(pZones) do
factoryZone.addFactoryZone(aZone)
end
if persistence then
-- sign up for persistence
callbacks = {}
callbacks.persistData = factoryZone.saveData
persistence.registerModule("factoryZone", callbacks)
-- now load my data
factoryZone.loadData()
end
initialized = true
factoryZone.updateSchedule = timer.scheduleFunction(factoryZone.update, {}, timer.getTime() + 1/factoryZone.ups)
-- start housekeeping
factoryZone.houseKeeping()
trigger.action.outText("cx/x factory zones v".. factoryZone.version .. " started", 30)
return true
end
if not factoryZone.init() then
trigger.action.outText("cf/x Factory Zones aborted: missing libraries", 30)
factoryZone = nil
end
-- add property to factory attribute to restrict production to that side,
-- eg factory blue will only work for blue, else will work for any side
-- currently not needed since we have defendersRED/BLUE and productionRED/BLUE

View File

@ -1,5 +1,5 @@
persistence = {}
persistence.version = "1.0.6"
persistence.version = "1.0.7"
persistence.ups = 1 -- once every 1 seconds
persistence.verbose = false
persistence.active = false
@ -26,7 +26,8 @@ persistence.requiredLibs = {
new 'saveNotification" can be off
1.0.4 - new optional 'root' property
1.0.5 - desanitize check on readConfig to early-abort
1.0.6 - removed potential verbosity bug
1.0.6 - removed potential verbosity bug
1.0.7 - correct abort for sanitized DCS, when non-verbose
PROVIDES LOAD/SAVE ABILITY TO MODULES
@ -513,24 +514,24 @@ function persistence.start()
if not dcsCommon.libCheck("persistence", persistence.requiredLibs) then
return false
end
-- read config
persistence.saveFileName = dcsCommon.getMissionName() .. " Data.txt"
persistence.readConfigZone()
-- let's see it lfs and io are online
persistence.active = false
if not _G["lfs"] then
if (not _G["lfs"]) or (not lfs) then
if persistence.verbose then
trigger.action.outText("+++persistence requires 'lfs'", 30)
return false
end
return false
end
if not _G["io"] then
if persistence.verbose then
trigger.action.outText("+++persistence requires 'io'", 30)
return false
end
return false
end
-- local mainDir = lfs.writedir() .. persistence.serverDir

Binary file not shown.