Initial commit

This commit is contained in:
Ambroise Garel
2025-07-22 10:22:50 +02:00
parent 16f53c8a47
commit edb28205cd
422 changed files with 17001 additions and 2 deletions
+195
View File
@@ -0,0 +1,195 @@
-- ====================================================================================
-- TUM.AIRFORCE - HANDLES THE FRIENDLY AND ENEMY COMBAT AIR PATROL
-- ====================================================================================
-- ====================================================================================
TUM.airForce = {}
do
local desiredUnitCount = { 4, 4 } -- Desired max number of aircraft in the air at any single time
local fighterGroups = { {}, {} }
local playerCenter = nil
local function getSkillLevel(side)
-- Friendly AI is always excellent
if side == TUM.settings.getPlayerCoalition() then return "Excellent" end
local airForceLevel = TUM.settings.getValue(TUM.settings.id.ENEMY_AIR_FORCE) - 1
-- if airForceLevel <= 1 then return "Average"
-- elseif airForceLevel == 2 then return DCSEx.table.getRandom({"Average", "Good"})
-- elseif airForceLevel == 3 then return DCSEx.table.getRandom({"Good", "High"})
-- else return DCSEx.table.getRandom({"High", "Excellent"})
-- end
if airForceLevel <= 2 then return "Average"
elseif airForceLevel == 3 then return DCSEx.table.getRandom({"Average", "Good"})
else return DCSEx.table.getRandom({"Good", "High", "Excellent"})
end
end
local function randomizeDesiredAircraftCount(side)
local airForceLevel = 0
if side == TUM.settings.getPlayerCoalition() then
if TUM.settings.getValue(TUM.settings.id.AI_CAP) == 1 then
airForceLevel = 2
else
airForceLevel = 0
end
else
airForceLevel = TUM.settings.getValue(TUM.settings.id.ENEMY_AIR_FORCE) - 1
end
if airForceLevel == 0 then
desiredUnitCount[side] = 0
else
desiredUnitCount[side] = math.random(airForceLevel, math.ceil(airForceLevel * 1.5)) + 1
end
end
local function getAirborneUnitCount(side)
local count = 0
for _,id in ipairs(fighterGroups[side]) do
local g = DCSEx.world.getGroupByID(id)
if g then
count = count + g:getSize()
end
end
return count
end
local function launchNewAircraftGroup(side, airbases)
local groupSize = DCSEx.table.getRandom({ 1, 2, 2, 2, 2, 3, 3, 4 })
groupSize = math.min(groupSize, desiredUnitCount[side] - getAirborneUnitCount(side))
if groupSize <= 0 then return false end
local faction = TUM.settings.getEnemyFaction()
if side == TUM.settings.getPlayerCoalition() then faction = TUM.settings.getPlayerFaction() end
local units = Library.factions.getUnits(faction, DCSEx.enums.unitFamily.PLANE_FIGHTER, groupSize, true)
if not units or #units == 0 then return false end -- No aircraft found
local launchAirbase = airbases[DCSEx.math.clamp(math.random(1, math.ceil(math.sqrt(#airbases))), 1, #airbases)]
local originPt = DCSEx.math.vec3ToVec2(launchAirbase:getPoint())
local groupInfo = DCSEx.unitGroupMaker.create(
side, Group.Category.AIRPLANE,
originPt, units,
{
moveTo = DCSEx.math.randomPointInCircle(TUM.objectives.getCenter(), TUM.objectives.getRadius(), 0),
silenced = true,
skill = getSkillLevel(side),
takeOff = true,
taskCAP = true,
unlimitedFuel = true
})
if not groupInfo then return false end
table.insert(fighterGroups[side], groupInfo.groupID)
if side == TUM.settings.getPlayerCoalition() then
local newUnit = nil
local newGroup = DCSEx.world.getGroupByID(groupInfo.groupID)
if newGroup then newUnit = newGroup:getUnit(1) end
local callsign = "FRIENDLY CAP"
local typeName = "Fighter aircraft"
if newUnit then
callsign = newUnit:getCallsign()
typeName = Library.objectNames.get(newUnit)
end
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "pilotNewFriendlyAircraft", { typeName, launchAirbase:getName() }, callsign)
else
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "commandNewEnemyAircraft", { tostring(groupSize), launchAirbase:getName() }, "Command")
end
return true
end
local function updateAirForce(side)
if desiredUnitCount[side] <= 0 then return false end -- No airforce
if not TUM.DEBUG_MODE and #DCSEx.world.getPlayersInAir() == 0 then return false end -- No players currently in the air, don't spawn new AI aircraft (except in debug mode)
local airbases = coalition.getAirbases(side)
if not airbases or #airbases == 0 then return false end -- No airbases found for this coalition, nowhere to takeoff from
local center = nil
if side == TUM.settings.getPlayerCoalition() then
center = playerCenter
else
center = TUM.objectives.getCenter()
end
airbases = DCSEx.dcs.getNearestObjects(center, airbases)
local airborneUnitCount = getAirborneUnitCount(side)
if airborneUnitCount < desiredUnitCount[side] then
if math.random(1, 2 + math.ceil(airborneUnitCount / 2)) == 1 then
if math.random(1, 4) then
randomizeDesiredAircraftCount(side)
end
return launchNewAircraftGroup(side, airbases)
end
end
return false
end
----------------------------------------------------------
-- Called on every mission update tick (every 10-20 seconds)
-- @param side The side for which air force must be updated
-- @return True if something was done this tick, false otherwise
----------------------------------------------------------
function TUM.airForce.onClockTick(side)
if TUM.mission.getStatus() == TUM.mission.status.NONE then return false end -- Not currenly in a mission
if TUM.objectives.getCount() <= 0 then return false end -- No objectives, nothing to defend for CAP
return updateAirForce(side)
end
function TUM.airForce.create()
TUM.airForce.removeAll()
TUM.log("Creating friendly and enemy air forces...")
for side=1,2 do
randomizeDesiredAircraftCount(side)
end
end
function TUM.airForce.removeAll()
if #fighterGroups[1] > 0 or #fighterGroups[2] > 0 then
TUM.log("Removing all friendly and enemy air force...")
end
for side=1,2 do
for _,id in ipairs(fighterGroups[side]) do
DCSEx.world.destroyGroupByID(id)
end
end
fighterGroups = { {}, {} }
end
function TUM.airForce.onStartUp()
playerCenter = { x = env.mission.map.centerX, y = env.mission.map.centerY }
local playerSlots = DCSEx.envMission.getPlayerGroups()
if #playerSlots > 0 then
playerCenter = { x = 0, y = 0 }
for _,p in ipairs(playerSlots) do
playerCenter.x = playerCenter.x + p.x
playerCenter.y = playerCenter.y + p.y
end
playerCenter.x = playerCenter.x / #playerSlots
playerCenter.y = playerCenter.y / #playerSlots
end
return true
end
end
@@ -0,0 +1,447 @@
-- ====================================================================================
-- TUM.AMBIENTRADIO - HANDLES AMBIENT CHATTER/RADIO MESSAGES REACTING TO A MISSION EVENT
-- ====================================================================================
-- (local) doAmbientChatter(stringID, callsign, minimumDelaySinceLastMessage, replacements, centerPoint, maxRadiusInNM)
-- (local) onEventDead(event)
-- (local) onEventEjection(event)
-- (local) onEventHit(event)
-- (local) onEventKill(event)
-- (local) onEventLand(event)
-- (local) onEventLandingAfterEjection(event)
-- (local) onEventPlayerEnterUnit(event)
-- (local) onEventShootingStart(event)
-- (local) onEventShotFriendly(event)
-- (local) onEventShotHostile(event)
-- (local) onEventShot(event)
-- (local) onEventTakeOff(event)
-- TUM.ambientRadio.onEvent(event)
-- ====================================================================================
TUM.ambientRadio = {}
do
local lastAmbientChatter = 0
-------------------------------------
-- Plays an ambient radio message
-------------------------------------
-- @param messageID ID of the radio message in scrambe.db.radioMessages
-- @param replacements String placeholders ($1, $2...) replacements in the message
-- @param callsign (optional) Callsign of the caller unit
-- @param minimumDelaySinceLastMessage (optional) If the last message happened less than this number of seconds from the current time, don't play the message
-- @param replacements (optional) Table of strings to use as replacement for $1, $2, $3...
-- @param centerPoint (optional) Center point used for max radius measurement
-- @param maxRadiusInNM (optional) Maximum radius (in nm) beyond which units will not recieve the message
-------------------------------------
local function doAmbientChatter(messageID, replacements, callsign, minimumDelaySinceLastMessage, centerPoint, maxRadiusInNM)
-- Check parameters
callsign = callsign or "FLIGHT"
minimumDelaySinceLastMessage = minimumDelaySinceLastMessage or 1
if maxRadiusInNM then maxRadiusInNM = DCSEx.converter.nmToMeters(maxRadiusInNM) end
-- Don't play this message if another message was played too recently
local currentTime = timer.getAbsTime()
if currentTime < lastAmbientChatter + minimumDelaySinceLastMessage then return end
lastAmbientChatter = currentTime
local players = coalition.getPlayers(TUM.settings.getPlayerCoalition())
if not players or #players == 0 then return end
for _,p in pairs(players) do
local tooFar = false
-- If the message is restricted to a given zone, make sure the player isn't too far
if centerPoint and maxRadiusInNM then
if DCSEx.math.getDistance2D(centerPoint, p:getPoint()) > maxRadiusInNM then
tooFar = true
end
end
if not tooFar then
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), messageID, replacements, callsign)
end
end
end
----------------------------------------------
-- Called when a DEAD event happens
--
-- @param event Event data
----------------------------------------------
local function onEventDead(event)
if not event.initiator then return end -- No initiator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Not a friendly
local unitDesc = event.initiator:getDesc()
if unitDesc.category == Unit.Category.AIRPLANE or unitDesc.category == Unit.Category.HELICOPTER then
doAmbientChatter("commandFriendlyDown", { event.initiator:getCallsign() }, "COMMAND", 1)
end
end
----------------------------------------------
-- Called when an EJECTION event happens
--
-- @param event Event data
----------------------------------------------
local function onEventEjection(event)
if not event.initiator then return end -- No initator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Initiator isn't a friendly
if event.initiator:getPlayerName() then return end -- No "ejecting!" message for players, so it won't cut the "mission failed" music which is played at the same time
doAmbientChatter("pilotEjecting", nil, event.initiator:getCallsign(), 1)
end
----------------------------------------------
-- Called when a HIT event happens
--
-- @param event Event data
----------------------------------------------
local function onEventHit(event)
if not event.target then return end -- No target
if Object.getCategory(event.target) ~= Object.Category.UNIT then return end -- Target isn't an unit
if event.target:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Target isn't a friendly
-- Blue on blue event
if event.initiator then
if Object.getCategory(event.initiator) == Object.Category.UNIT then -- Attacker is an unit
if event.initiator:getCoalition() == TUM.settings.getPlayerCoalition() then -- Attacker is a friendly
doAmbientChatter("commandBlueOnBlue", nil, "COMMAND", 1)
return
end
end
end
-- Friendly aircraft hit
if event.target:getDesc().category == Unit.Category.AIRPLANE or event.target:getDesc().category == Unit.Category.HELICOPTER then
if not event.initiator:getPlayerName() then -- Players don't radio out when they're hit
doAmbientChatter("pilotImHit", nil, event.target:getCallsign(), 3)
end
end
end
----------------------------------------------
-- Called when a KILL event happens
--
-- @param event Event data
----------------------------------------------
local function onEventKill(event)
if not event.target or not event.initiator then return end -- No event target or initiator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Killer isn't an unit
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Killer isn't a friendly
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) and event.initiator:getPlayerName() then return end -- No player chatter in MP
local targetDesc = event.target:getDesc()
local killerName = nil
if event.initiator:getCallsign() then
killerName = event.initiator:getCallsign()
else
killerName = event.initiator:getName()
end
local killMessage = "pilotKillGround"
if Object.getCategory(event.target) == Object.Category.SCENERY then
if TUM.objectives.getSceneryObjectObjective(event.target) then
doAmbientChatter("pilotKillStrike", nil, killerName, 1)
else
return -- Not a scenery target, congratulations you just bombed a random civilian target lol
end
elseif Object.getCategory(event.target) == Object.Category.STATIC then
killMessage = "pilotKillStrike"
doAmbientChatter("pilotKillStrike", nil, killerName, 1)
elseif Object.getCategory(event.target) == Object.Category.UNIT then
local killUnitType = "unit"
if targetDesc.category == Unit.Category.AIRPLANE then
killMessage = "pilotKillAir"
if event.target:hasAttribute("AWACS") then
killUnitType = "AWACS"
elseif event.target:hasAttribute("Tankers") then
killUnitType = "tanker"
elseif event.target:hasAttribute("Transports") then
killUnitType = "transport"
elseif event.target:hasAttribute("Bombers") then
killUnitType = "bomber"
elseif event.target:hasAttribute("Multirole fighters") or event.target:hasAttribute("Fighters") then
killUnitType = "fighter"
elseif event.target:hasAttribute("Interceptors") then
killUnitType = "interceptor"
elseif event.target:hasAttribute("UAVs") then
killUnitType = "UAV"
else
killUnitType = "aircraft"
end
elseif targetDesc.category == Unit.Category.HELICOPTER then
killMessage = "pilotKillAir"
if event.target:hasAttribute("Attack helicopters") then
killUnitType = "attack "
elseif event.target:hasAttribute("Transport helicopters") then
killUnitType = "transport "
else
killUnitType = ""
end
killUnitType = killUnitType.." "..DCSEx.table.getRandom({"helicopter", "helo", "chopper"})
elseif targetDesc.category == Unit.Category.GROUND_UNIT then
if event.target:hasAttribute("Infantry") then return end -- No kill message for infantry (yet?)
killMessage = "pilotKillGround"
if event.target:hasAttribute("MANPADS") then
killUnitType = "MANPADS"
elseif event.target:hasAttribute("Infantry") then
killUnitType = "infantry"
elseif event.target:hasAttribute("SR SAM") then
killUnitType = "short-range SAM"
elseif event.target:hasAttribute("SAM SR") then
killUnitType = "SAM search radar"
elseif event.target:hasAttribute("SAM TR") then
killUnitType = "SAM tracking radar"
elseif event.target:hasAttribute("SAM LL") then
killUnitType = "SAM launcher"
elseif event.target:hasAttribute("AAA") then
killUnitType = "AAA"
elseif event.target:hasAttribute("Air Defence") then
killUnitType = "air defense"
elseif event.target:hasAttribute("Artillery") then
killUnitType = "artillery"
elseif event.target:hasAttribute("Armored vehicles") then
killUnitType = "armor"
elseif event.target:hasAttribute("Trucks") then
killUnitType = "truck"
else
killUnitType = "vehicle"
end
elseif targetDesc.category == Unit.Category.SHIP then
killMessage = "pilotKillShip"
if event.target:hasAttribute("Aircraft Carriers") then
killUnitType = "carrier"
elseif event.target:hasAttribute("Heavy armed ships") then
killUnitType = "warship"
elseif event.target:hasAttribute("Light armed ships") then
killUnitType = "armed ship"
elseif event.target:hasAttribute("Unarmed ships") then
killUnitType = "cargo ship"
else
killUnitType = "ship"
end
elseif targetDesc.category == Unit.Category.STRUCTURE then
killMessage = "pilotKillStrike"
end
killUnitType = Library.objectNames.get(event.target)
doAmbientChatter(killMessage, killUnitType, killerName, 1)
end
end
----------------------------------------------
-- Called when a LAND event happens
--
-- @param event Event data
----------------------------------------------
local function onEventLand(event)
if not event.initiator then return end -- No event initiator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Not a friendly
local baseName = "AIRBASE"
if event.place then
baseName = event.place:getName():upper()
end
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) or not event.initiator:getPlayerName() then
doAmbientChatter("atcSafeLanding", {event.initiator:getCallsign()}, baseName.." ATC", 1)
end
end
----------------------------------------------
-- Called when a LANDING_AFTER_EJECTION event happens
--
-- @param event Event data
----------------------------------------------
local function onEventLandingAfterEjection(event)
if not event.initiator then return end -- No event initiator
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Not a friendly
doAmbientChatter("commandFriendlyPilotOnGround", nil, "COMMAND", 1)
end
----------------------------------------------
-- Called when a PLAYER_ENTER_UNIT event happens
--
-- @param event Event data
----------------------------------------------
local function onEventPlayerEnterUnit(event)
if not event.initiator then return end -- No event initiator
-- TODO
end
----------------------------------------------
-- Called when a SHOOTING_START event happens
--
-- @param event Event data
----------------------------------------------
local function onEventShootingStart(event)
if not event.initiator then return end -- No event initiator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit
-- Plane or helicopter
if event.initiator:getDesc().category == Unit.Category.AIRPLANE or event.initiator:getDesc().category == Unit.Category.HELICOPTER then
if event.initiator:getCoalition() == TUM.settings.getPlayerCoalition() then
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) and event.initiator:getPlayerName() then return end -- No player chatter in MP
doAmbientChatter("pilotLaunchGuns", nil, event.initiator:getCallsign(), 2)
return
end
end
-- AAA
if event.initiator:hasAttribute("AAA") and event.initiator:getCoalition() == TUM.settings.getEnemyCoalition() then
doAmbientChatter("pilotWarningAAA", nil, "Flight", 2)
return
end
end
--------------------------------------------------------------
-- Called when a SHOT event happens, with a friendly initiator
--
-- @param event Event data
--------------------------------------------------------------
local function onEventShotFriendly(event)
if not event.initiator then return end -- No event initiator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit
local unitCategory = event.initiator:getDesc().category
local weaponDesc = event.weapon:getDesc()
if unitCategory == Unit.Category.AIRPLANE or unitCategory == Unit.Category.HELICOPTER then
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) and event.initiator:getPlayerName() then return end -- No player chatter in MP
if weaponDesc.category == Weapon.Category.BOMB then
doAmbientChatter("pilotLaunchPickle", nil, event.initiator:getCallsign(), 1)
elseif weaponDesc.category == Weapon.Category.ROCKET then
doAmbientChatter(event.initiator:getCoalition(),"pilotLaunchRocket", nil, event.initiator:getCallsign(), 1)
elseif weaponDesc.category == Weapon.Category.MISSILE then
if weaponDesc.missileCategory == Weapon.MissileCategory.AAM then
if weaponDesc.guidance == Weapon.GuidanceType.IR then
doAmbientChatter("pilotLaunchFox2", nil, event.initiator:getCallsign(), 1)
elseif weaponDesc.guidance == Weapon.GuidanceType.RADAR_ACTIVE then
doAmbientChatter("pilotLaunchFox3", nil, event.initiator:getCallsign(), 1)
elseif weaponDesc.guidance == Weapon.GuidanceType.RADAR_SEMI_ACTIVE then
doAmbientChatter("pilotLaunchFox1", nil, event.initiator:getCallsign(), 1)
else
doAmbientChatter("pilotLaunchMissile", nil, event.initiator:getCallsign(), 1)
end
elseif weaponDesc.missileCategory == Weapon.MissileCategory.ANTI_SHIP or weaponDesc.typeName == "weapons.missiles.AGM_84D" then
doAmbientChatter("pilotLaunchBruiser", nil, event.initiator:getCallsign(), 1)
elseif weaponDesc.guidance == Weapon.GuidanceType.RADAR_PASSIVE then
doAmbientChatter("pilotLaunchMagnum", nil, event.initiator:getCallsign(), 1)
else
doAmbientChatter("pilotLaunchRifle", nil, event.initiator:getCallsign(), 1)
end
end
-- elseif unitCategory == Unit.Category.GROUND_UNIT then
-- if event.initiator:hasAttribute("MANPADS") then
-- -- Do nothing, no message for MANPADS
-- elseif event.initiator:hasAttribute("IR Guided SAM") then
-- -- doAmbientChatter("Friendly SAM engaging", nil, "Air defense HQ", 2)
-- elseif event.initiator:hasAttribute("SAM") then
-- -- doAmbientChatter("Friendly SAM engaging", nil, "Air defense HQ", 2)
-- end
-- elseif unitCategory == Unit.Category.SHIP then
-- TODO
end
end
---------------------------------------------------------------------
-- Called when a SHOT event happens, with a hostile or neutral initiator
--
-- @param event Event data
---------------------------------------------------------------------
local function onEventShotHostile(event)
if not event.initiator then return end -- No event initiator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit
if event.initiator:getDesc().category == Unit.Category.AIRPLANE or event.initiator:getDesc().category == Unit.Category.HELICOPTER then
-- if weaponDesc.category == Weapon.Category.MISSILE then
-- if weaponDesc.missileCategory == Weapon.MissileCategory.AAM then
-- doAmbientChatter("Missile!", nil, nil, 2, event.initiator:getPoint(), 8)
-- end
-- end
elseif event.initiator:getDesc().category == Unit.Category.GROUND_UNIT or event.initiator:getDesc().category == Unit.Category.SHIP then
if event.initiator:hasAttribute("MANPADS") then
doAmbientChatter("pilotWarningMANPADS", nil, nil, 2, event.initiator:getPoint(), 8)
elseif event.initiator:hasAttribute("IR Guided SAM") then
doAmbientChatter("pilotWarningSAMLaunch", nil, nil, 2, event.initiator:getPoint(), 12)
elseif event.initiator:hasAttribute("SAM SR") then
doAmbientChatter("pilotWarningSAMLaunch", nil, nil, 2, event.initiator:getPoint(), 12)
elseif event.initiator:hasAttribute("SAM") or event.initiator:hasAttribute("SAM LL") or event.initiator:hasAttribute("SAM CC") or event.initiator:hasAttribute("SAM LR") then
doAmbientChatter("pilotWarningSAMLaunch", nil, nil, 2, event.initiator:getPoint(), 24)
end
end
end
-----------------------------------
-- Called when a SHOT event happens
--
-- @param event Event data
-----------------------------------
local function onEventShot(event)
if not event.initiator then return end -- No event initiator
if not event.weapon then return end -- No weapon shot, abort
if event.initiator:getCoalition() == TUM.settings.getPlayerCoalition() then
onEventShotFriendly(event)
else
onEventShotHostile(event)
end
end
----------------------------------------------
-- Called when a TAKEOFF event happens
--
-- @param event Event data
----------------------------------------------
local function onEventTakeOff(event)
if not event.initiator then return end -- No event initiator
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Not a friendly
local airbaseName = "airbase"
if event.place then airbaseName = event.place:getName() end
local callsign = event.initiator:getCallsign() or "aircraft"
doAmbientChatter("Fly safe, "..callsign.."!", nil, airbaseName) -- TODO: proper message
end
-------------------------------------
-- Called when an event is raised
-- @param event The DCS World event
-------------------------------------
function TUM.ambientRadio.onEvent(event)
if event.id == world.event.S_EVENT_DEAD then
onEventDead(event)
elseif event.id == world.event.S_EVENT_EJECTION then
onEventEjection(event)
elseif event.id == world.event.S_EVENT_HIT then
onEventHit(event)
elseif event.id == world.event.S_EVENT_KILL then
onEventKill(event)
elseif event.id == world.event.S_EVENT_LAND then
onEventLand(event)
elseif event.id == world.event.S_EVENT_LANDING_AFTER_EJECTION then
onEventLandingAfterEjection(event)
-- elseif event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT then
-- onEventPlayerEnterUnit(event)
elseif event.id == world.event.S_EVENT_SHOOTING_START then
onEventShootingStart(event)
elseif event.id == world.event.S_EVENT_SHOT then
onEventShot(event)
-- elseif event.id == world.event.S_EVENT_TAKEOFF then
-- onEventTakeOff(event)
end
end
end
@@ -0,0 +1,75 @@
-- ====================================================================================
-- TUM.AMBIENTWORLD - HANDLES LITTLE DETAILS DESIGNED TO MAKE THE GAME WORLD MORE ALIVE
-- ====================================================================================
-- ====================================================================================
TUM.ambientWorld = {}
do
local groupIDs = {}
---------------
-- CONSTANTS --
---------------
local ESCAPING_CREW_ONE_TIME_OUT_OF = 6 -- one time of out this number, crew will flee destroyed vehicles
local function doSpawnEscapingCrew(point3)
local options = {
disableWeapons = true,
hidden = true,
invisible = true,
moveBy = 250,
spreadDistance = math.random(2, 3),
}
local unitTypes = Library.factions.getUnits(TUM.settings.getEnemyFaction(), DCSEx.enums.unitFamily.GROUND_INFANTRY, math.random(1, 3))
if not unitTypes or #unitTypes == 0 then return end
local groupInfo = DCSEx.unitGroupMaker.create(TUM.settings.getEnemyCoalition(), Group.Category.GROUND, DCSEx.math.vec3ToVec2(point3), unitTypes, options)
if groupInfo then
table.insert(groupIDs, groupInfo.groupID)
end
end
-- Called when a unit is destroyed
local function onEventDead(event)
if not event.initiator then return end -- Nothing was hit
-- TODO: spawn from target scenery buildings and static structures
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Target wasn't an unit
if event.initiator:getDesc().category ~= Unit.Category.GROUND_UNIT then return end -- Wasn't a ground unit
if not event.initiator:hasAttribute("Vehicles") then return end -- Wasn't a vehicle
if event.initiator:getCoalition() ~= TUM.settings.getEnemyCoalition() then return end -- Only spawn escaping crew from enemy vehicles
if math.random(1, ESCAPING_CREW_ONE_TIME_OUT_OF) ~= 1 then return end -- Do not spawn every time
TUM.log("Spawning crew escaping from destroyed unit "..event.initiator:getName())
timer.scheduleFunction(
doSpawnEscapingCrew,
event.initiator:getPoint(),
timer.getTime() + math.random(4, 7)
)
end
function TUM.ambientWorld.removeAll()
for _,id in ipairs(groupIDs) do
DCSEx.world.destroyGroupByID(id)
end
groupIDs = {}
end
-------------------------------------
-- Called when an event is raised
-- @param event The DCS World event
-------------------------------------
function TUM.ambientWorld.onEvent(event)
if not event then return end -- No event
if event.id == world.event.S_EVENT_DEAD then
onEventDead(event)
end
end
end
@@ -0,0 +1,79 @@
-- ====================================================================================
-- TUM.DEBUGMENU - HANDLES THE F10 DEBUG MENU
-- ====================================================================================
-- (local) doMarkersBoom()
-- TUM.debugMenu.onStartUp()
-- ====================================================================================
TUM.debugMenu = {}
do
local function doMarkersAirBoom()
local panels = world.getMarkPanels()
local boomCount = 0
for _,p in pairs(panels) do
local nearestPoint = nil
local nearestDistance = 99999999
if p.text:lower() == "airboom" then
for _,c in pairs({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}) do
for _,u in DCSEx.world.getAllUnits(nil, c) do
local distance = DCSEx.math.getDistance3D(p.pos, u:getPoint())
if distance < nearestDistance then
nearestDistance = distance
nearestPoint = u:getPoint()
end
end
end
if nearestPoint then
trigger.action.explosion(nearestPoint, 1024)
boomCount = boomCount + 1
end
end
end
TUM.log("Detonated "..tostring(boomCount).. " \"airboom\" marker(s).")
end
local function doMarkersBoom()
local panels = world.getMarkPanels()
local boomCount = 0
for _,p in pairs(panels) do
if p.text:lower() == "boom" then
trigger.action.explosion(p.pos, 8192)
boomCount = boomCount + 1
end
end
TUM.log("Detonated "..tostring(boomCount).. " \"boom\" marker(s).")
end
local function doAwardPointsAndObjectives()
TUM.playerScore.award(100, "debug cheat")
TUM.playerScore.awardCompletedObjective()
end
local function doSimulatePlayerLanding()
-- TUM.playerCareer.awardScore(TUM.playerScore.getScore(), TUM.playerScore.getCompletedObjectives())
local event = {
id = world.event.S_EVENT_LAND,
initiator = coalition.getPlayers(TUM.settings.getPlayerCoalition())[1]
}
TUM.onEvent(event)
end
function TUM.debugMenu.createMenu()
if not TUM.DEBUG_MODE then return end
local rootMenu = missionCommands.addSubMenu("[DEBUG]")
missionCommands.addCommand("Detonate \"boom\" map markers", rootMenu, doMarkersBoom, nil)
missionCommands.addCommand("Detonate aircraft near \"airboom\" map markers", rootMenu, doMarkersAirBoom, nil)
missionCommands.addCommand("Award 100 points and 1 objective", rootMenu, doAwardPointsAndObjectives, nil)
missionCommands.addCommand("Simulate player landing", rootMenu, doSimulatePlayerLanding, nil)
missionCommands.addCommand("Reset player stats", rootMenu, TUM.playerCareer.reset, nil)
end
end
@@ -0,0 +1,242 @@
-- ====================================================================================
-- TUM.ENEMYAIRDEFENSE - HANDLES THE OPFOR AIR DEFENSE
-- ====================================================================================
-- (local) addSAMCoverage(point2)
-- (local) isPointCoveredBySAM(point2)
-- (local) createEnemyStrategicAirDefense()
-- (local) createEnemyScatteredAirDefense()
-- TUM.enemyAirDefense.onStartUp()
-- ====================================================================================
TUM.enemyAirDefense = {}
do
local AIR_DEFENSE_RANGE = { -- in meters
[DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS] = 750,
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_STATIC] = 1500,
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = 1500,
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR] = 6000,
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = 8000,
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM] = 35000,
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = 70000,
}
local airDefenseGroups = {} -- Stores info about all air defense groups (groupID, point2, range and unitFamily)
local function getPointDefenseFamily(forceSHORAD)
local airDefenseLevel = TUM.settings.getValue(TUM.settings.id.ENEMY_AIR_DEFENSE) - 1
if airDefenseLevel > 1 and forceSHORAD then
return DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT
end
if airDefenseLevel <= 1 then return DCSEx.table.getRandom({DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE, DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR})
elseif airDefenseLevel == 2 then return DCSEx.table.getRandom({DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE, DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT})
elseif airDefenseLevel == 3 then return DCSEx.table.getRandom({DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT})
else return DCSEx.table.getRandom({DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT})
end
end
local function getSkillLevel()
local airDefenseLevel = TUM.settings.getValue(TUM.settings.id.ENEMY_AIR_DEFENSE) - 1
if airDefenseLevel <= 1 then return "Average"
elseif airDefenseLevel == 2 then return DCSEx.table.getRandom({"Average", "Good", "Good", "High"})
elseif airDefenseLevel == 3 then return DCSEx.table.getRandom({"High", "Excellent"})
else return "Excellent"
end
end
local function getEnemyPointUnitsProtectingPoint(point2)
local count = 0
for _,adg in ipairs(airDefenseGroups) do
if DCSEx.math.getDistance2D(adg.point2, point2) < adg.range then
count = count + 1
end
end
return count
end
local function addAirDefenseGroup(side, faction, unitFamily, point)
if not point then return end
-- Make sure no air defense unit is spawned in range of an allied player
for _,p in ipairs(coalition.getPlayers(TUM.settings.getPlayerCoalition())) do
if DCSEx.math.getDistance2D(point, DCSEx.math.vec3ToVec2(p:getPoint())) < AIR_DEFENSE_RANGE[unitFamily] then
return false
end
end
local units = Library.factions.getUnits(faction, unitFamily, 1)
if not units or #units == 0 then return false end -- No valid units found
local groupInfo = DCSEx.unitGroupMaker.create(side, Group.Category.GROUND, point, units, { skill = getSkillLevel() })
if not groupInfo then return false end -- Failed to create group
local adGroup = {
groupID = groupInfo.groupID,
point2 = point,
range = AIR_DEFENSE_RANGE[unitFamily],
unitFamily = unitFamily
}
table.insert(airDefenseGroups, adGroup)
return true
end
local function createPointAirDefense(airDefenseLevel, side, faction)
-- Add point air defense near all objectives
for i=1,TUM.objectives.getCount() do
local objPoint2 = TUM.objectives.getObjective(i).point2 -- Objective location
local desiredCount = math.random(math.floor(airDefenseLevel / 3), math.ceil(airDefenseLevel / 1.5)) -- Number of desired point air defense groups for this objective
-- local desiredCount = math.random(math.floor(airDefenseLevel / 2), math.ceil(airDefenseLevel / 1.25)) -- Number of desired point air defense groups for this objective
local adCount = desiredCount - getEnemyPointUnitsProtectingPoint(objPoint2) -- Number of point air defense groups already defending this objective
local realCount = 0
if adCount > 0 then -- Not enough groups? Create a few to reach the desired number
for j=1,adCount do
local forceSHORAD = false
-- if j == 1 then forceSHORAD = (math.random(1, 2) == 1) end
if j == 1 then forceSHORAD = (math.random(1, 5) <= 2) end
local unitFamily = getPointDefenseFamily(forceSHORAD)
local point = DCSEx.math.randomPointInCircle(objPoint2, AIR_DEFENSE_RANGE[unitFamily], AIR_DEFENSE_RANGE[unitFamily] / 2, land.SurfaceType.LAND)
if addAirDefenseGroup(side, faction, unitFamily, point) then
realCount = realCount + 1
else
TUM.log("Failed to add point air defense group near objective "..TUM.objectives.getObjective(i).name..".", TUM.logLevel.WARNING)
end
end
end
TUM.log(string.format("Spawned %d air defense unit(s) near objective %s.", realCount, TUM.objectives.getObjective(i).name))
end
end
local function createLocalAirDefense(airDefenseLevel, side, faction, objectivesCenter, objectivesRadius)
-- local count = math.ceil(math.random(2, 3) * math.max(1, math.sqrt(objectivesRadius) / 200) * math.sqrt(airDefenseLevel))
local count = math.ceil(math.random(2, 3) * math.max(1, math.sqrt(objectivesRadius) / 300) * math.sqrt(airDefenseLevel))
if count <= 0 then return end
local realCount = 0
for i=1,count do
local forceSHORAD = false
-- if i <= math.max(1, count / 4) then forceSHORAD = true end
if i <= math.max(1, count / 6) then forceSHORAD = true end
local unitFamily = getPointDefenseFamily(forceSHORAD)
local point = DCSEx.math.randomPointInCircle(objectivesCenter, objectivesRadius, 0, land.SurfaceType.LAND)
if addAirDefenseGroup(side, faction, unitFamily, point) then
realCount = realCount + 1
else
TUM.log("Failed to add local air defense group.", TUM.logLevel.WARNING)
end
end
TUM.log(string.format("Spawned %d air defense unit(s) around the objectives.", realCount))
end
local function createMANPADs(airDefenseLevel, side, faction, objectivesCenter, objectivesRadius)
local count = math.ceil(math.random(2, 3) * math.max(1, math.sqrt(objectivesRadius) / 120) * math.sqrt(airDefenseLevel))
if count <= 0 then return end
local realCount = 0
for _=1,count do
local point = DCSEx.math.randomPointInCircle(objectivesCenter, objectivesRadius, 0, land.SurfaceType.LAND)
if addAirDefenseGroup(side, faction, DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS, point) then
realCount = realCount + 1
else
TUM.log("Failed to add local MANPADS group.", TUM.logLevel.WARNING)
end
end
TUM.log(string.format("Spawned %d MANPADS around the objectives.", realCount))
end
local function addStrategicSAMSite(side, faction, unitFamily, objectivesCenter, objectivesRadius)
local point = DCSEx.math.randomPointInCircle(objectivesCenter, objectivesRadius)
if point then
local zoneCenter = TUM.territories.getRandomPointInTerritory(side, land.SurfaceType.LAND)
if not zoneCenter then zoneCenter = TUM.territories.getRandomPointInTerritory(side) end
if not zoneCenter then zoneCenter = DCSEx.table.getRandom(TUM.territories.getTerritoryZones(side)) end
local vector = { x = zoneCenter.x - point.x, y = zoneCenter.y - point.y }
vector = DCSEx.math.normalizeVec2(vector)
local distance = math.floor(DCSEx.math.getDistance2D(zoneCenter, point) * 2)
local step = math.min(distance / 10, 2500)
for __=0,distance,step do
point.x = point.x + vector.x * step
point.y = point.y + vector.y * step
if TUM.territories.getPointOwner(point) == side and land.getSurfaceType(point) == land.SurfaceType.LAND then
local distanceToObjectives = DCSEx.math.getDistance2D(objectivesCenter, point)
if distanceToObjectives > AIR_DEFENSE_RANGE[unitFamily] * 2 then return false end -- Went too far, no need to spawn a SAM site here, it will never engage the players and only eat up CPU
if distanceToObjectives > AIR_DEFENSE_RANGE[unitFamily] / 2 then
return addAirDefenseGroup(side, faction, unitFamily, point)
end
end
end
end
return false
end
local function createStrategicAirDefense(airDefenseLevel, side, faction, objectivesCenter, objectivesRadius)
if airDefenseLevel <= 1 then return end
local count = math.random(1, math.ceil(airDefenseLevel / 1.5))
local realCount = 0
local rerollsLeft = 5
for i=1,count do
local unitFamily = DCSEx.table.getRandom({DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM, DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG})
if addStrategicSAMSite(side, faction, unitFamily, objectivesCenter, objectivesRadius) then
realCount = realCount + 1
elseif rerollsLeft > 0 then -- Small chance to retry if couldn't spawn the SAM site last time
if math.random(1, 3) == 1 then
i = i - 1
rerollsLeft = rerollsLeft - 1
end
end
end
TUM.log(string.format("Spawned %d strategic SAM(s) on enemy territory.", realCount))
end
function TUM.enemyAirDefense.create()
TUM.enemyAirDefense.removeAll() -- Destroy all pre-existing air defense
TUM.log("Creating enemy air defense...")
if TUM.objectives.getCount() == 0 then return end -- No objectives, no air defense
local airDefenseLevel = TUM.settings.getValue(TUM.settings.id.ENEMY_AIR_DEFENSE) - 1
if airDefenseLevel <= 0 then return end -- No surface-to-air defense at all
if TUM.settings.getValue(TUM.settings.id.TASKING) == DCSEx.enums.taskFamily.ANTISHIP then return end -- No ground air defense during antiship strikes
local side = TUM.settings.getEnemyCoalition()
local faction = TUM.settings.getEnemyFaction()
local objectivesCenter = TUM.objectives.getCenter()
local objectivesRadius = TUM.objectives.getRadius()
createPointAirDefense(airDefenseLevel, side, faction) -- Must be created before the other layers, else it may think objective sites are already protected and fail to generate point defense
createLocalAirDefense(airDefenseLevel, side, faction, objectivesCenter, objectivesRadius)
createMANPADs(airDefenseLevel, side, faction, objectivesCenter, objectivesRadius)
createStrategicAirDefense(airDefenseLevel, side, faction, objectivesCenter, objectivesRadius)
end
function TUM.enemyAirDefense.removeAll()
if #airDefenseGroups > 0 then TUM.log("Removing all enemy air defense...") end
for _,g in ipairs(airDefenseGroups) do
DCSEx.world.destroyGroupByID(g.groupID)
end
airDefenseGroups = {}
end
end
@@ -0,0 +1,121 @@
-- ====================================================================================
-- TUM.INTERMISSION - HANDLES THE MENU DISPLAYED BETWEEN MISSIONS
-- ====================================================================================
-- TUM.intermission.createMenu()
-- TUM.intermission.onInitialize()
-- ====================================================================================
TUM.intermission = {}
do
local missionZonesMarkers = {}
local function doCommandStartMission()
local players = DCSEx.world.getAllPlayers()
if #players == 0 then
trigger.action.outText("No player slots occupied. At least one client slot must be occupied by a player to start the mission.", 5)
trigger.action.outSound("UI-Error.ogg")
return
end
if not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then
for _,p in ipairs(players) do
if p:inAir() then
trigger.action.outText("Cannot start a single player mission while the player is in the air. Please land before starting the mission.", 5)
trigger.action.outSound("UI-Error.ogg")
return
end
end
end
trigger.action.outText("Generating mission and loading assets, this can take some time...", 5)
timer.scheduleFunction(TUM.mission.beginMission, false, timer.getTime() + 1)
-- TUM.mission.beginMission()
end
local function setSetting(args)
if not args.id or not args.value then return end
TUM.settings.setValue(args.id, args.value, false)
TUM.intermission.createMenu()
end
local function createSubMenu(id, parentMenu)
local rootMenu = nil
rootMenu = missionCommands.addSubMenu(TUM.settings.getSettingsName(id)..": "..TUM.settings.getValue(id, true), parentMenu)
for i,v in ipairs(TUM.settings.getPossibleValues(id)) do
local commandText = v
if id == TUM.settings.id.TARGET_LOCATION then
local playerCenter = DCSEx.world.getUnitsCenter(DCSEx.world.getAllPlayers())
local distance = math.floor(DCSEx.converter.metersToNM(DCSEx.math.getDistance2D(playerCenter, DCSEx.zones.getByName(v))))
commandText = commandText.."(≈"..tostring(distance).." nm)"
end
missionCommands.addCommand(commandText, rootMenu, setSetting, { id = id, value = i, redrawMenu = true })
end
end
function TUM.intermission.createMissionZonesMarkers()
TUM.intermission.removeMissionZonesMarkers()
local missionZones = TUM.territories.getMissionZones()
for _,z in ipairs(missionZones) do
local zoneOwner = TUM.territories.getPointOwner(z)
local color = DCSEx.dcs.getCoalitionColor(zoneOwner)
local ids = DCSEx.zones.drawOnMap(z, { color[1], color[2], color[3], 1 }, { color[1], color[2], color[3], .5 }, DCSEx.enums.lineType.SOLID, true, true)
if ids then
table.insert(missionZonesMarkers, ids[1])
table.insert(missionZonesMarkers, ids[2])
end
end
end
function TUM.intermission.removeMissionZonesMarkers()
for _,id in ipairs(missionZonesMarkers) do
trigger.action.removeMark(id)
end
missionZonesMarkers = {}
end
-------------------------------------
-- Creates the mission briefing menu
-------------------------------------
function TUM.intermission.createMenu()
missionCommands.removeItem() -- Clear the menu
local briefingText = "Welcome to The Universal Mission for DCS World, a highly customizable mission available for single-player and PvE.\n\nOpen the communication menu and select the ''F10. Other'' option to access mission settings."
DCSEx.envMission.setBriefing(coalition.side.RED, briefingText)
DCSEx.envMission.setBriefing(coalition.side.BLUE, briefingText)
TUM.intermission.createMissionZonesMarkers() -- Show the available mission zones on the F10 map
missionCommands.addCommand(" Display mission settings", nil, TUM.settings.printSettingsSummary, false)
local settingsMenu = missionCommands.addSubMenu("✎ Change mission settings")
createSubMenu(TUM.settings.id.COALITION_BLUE, settingsMenu)
createSubMenu(TUM.settings.id.COALITION_RED, settingsMenu)
createSubMenu(TUM.settings.id.TASKING, settingsMenu)
createSubMenu(TUM.settings.id.TARGET_LOCATION, settingsMenu)
createSubMenu(TUM.settings.id.TARGET_COUNT, settingsMenu)
createSubMenu(TUM.settings.id.ENEMY_AIR_DEFENSE, settingsMenu)
createSubMenu(TUM.settings.id.ENEMY_AIR_FORCE, settingsMenu)
createSubMenu(TUM.settings.id.AI_CAP, settingsMenu)
TUM.playerCareer.createMenu()
missionCommands.addCommand("➤ Begin mission", nil, doCommandStartMission, nil)
TUM.debugMenu.createMenu() -- Append debug menu to other menus (if debug mode enabled)
end
-------------------------------------
-- Called on mission start up
-- @return True if started up properly, false if an error happened
-------------------------------------
function TUM.intermission.onStartUp()
TUM.intermission.createMenu() -- Create the briefing menu
return true
end
end
+208
View File
@@ -0,0 +1,208 @@
-- ====================================================================================
-- TUM.MISSION - HANDLES THE MAIN MISSION
-- ====================================================================================
-- ====================================================================================
TUM.mission = {}
TUM.mission.status = {
NONE = 0,
IN_PROGRESS = 1,
COMPLETED = 2,
FAILED = 3,
}
TUM.mission.endCause = { -- Why did the mission end?
ABORTED = 1,
COMPLETED = 2,
FAILED = 3
}
do
local OBJECTIVES_REMINDER_INTERVAL = 5
local missionStatus = TUM.mission.status.NONE
local objectivesReminderIntervalLeft = OBJECTIVES_REMINDER_INTERVAL
function TUM.mission.getStatus()
return missionStatus
end
local function closeMission(removeAllUnits)
if removeAllUnits then
TUM.airForce.removeAll()
TUM.ambientWorld.removeAll()
TUM.enemyAirDefense.removeAll()
TUM.objectives.removeAll()
end
missionStatus = TUM.mission.status.NONE
end
function TUM.mission.checkMissionStatus(silent)
silent = silent or false
if missionStatus ~= TUM.mission.status.IN_PROGRESS then return end
if TUM.objectives.areAllCompleted() then
missionStatus = TUM.mission.status.COMPLETED
DCSEx.dcs.outPicture("Pic-MissionComplete.png", 5, true, 0, 1, 1, 25, 1)
trigger.action.outSound("UI-MissionEnd.ogg")
if not silent then
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "commandMissionComplete", nil, "Command", true)
end
end
end
function TUM.mission.beginMission(silent)
silent = silent or false
closeMission(true)
TUM.intermission.removeMissionZonesMarkers()
for _=1,TUM.settings.getValue(TUM.settings.id.TARGET_COUNT) do
TUM.objectives.add()
end
if TUM.objectives.getCount() == 0 then
TUM.log("Couldn't create any objective, mission creation failed.", TUM.logLevel.WARNING)
closeMission(true)
return
end
TUM.supportAWACS.create() -- Create the AWACS aircraft if it wasn't airborne already
TUM.enemyAirDefense.create() -- Must be called once objectives have been created
TUM.airForce.create() -- Must be called once objectives have been created
TUM.missionMenu.create() -- Must be called once objectives have been created
local briefingOrder = DCSEx.table.shuffle({1, 2, 3, 4, 5}) -- Just to make sure the same description is used twice
local briefingText = ""
for i=1,TUM.objectives.getCount() do
local obj = TUM.objectives.getObjective(i)
briefingText = briefingText.."Objective "..obj.name..":\n"
local descriptions = Library.tasks[obj.taskID].description.briefing
briefingText = briefingText..descriptions[DCSEx.math.clamp(briefingOrder[i], 1, #descriptions)]
if i < TUM.objectives.getCount() then
briefingText = briefingText.."\n\n"
end
end
DCSEx.envMission.setBriefing(TUM.settings.getPlayerCoalition(), briefingText)
DCSEx.envMission.setBriefing(TUM.settings.getEnemyCoalition(), "")
missionStatus = TUM.mission.status.IN_PROGRESS
if not silent then
DCSEx.dcs.outPicture("Pic-MissionStart.png", 5, true, 0, 1, 1, 25, 1)
trigger.action.outSound("UI-MissionStart.ogg")
end
trigger.action.outText("MISSION OBJECTIVES:\n"..TUM.mission.getSummaryString(), 10)
objectivesReminderIntervalLeft = OBJECTIVES_REMINDER_INTERVAL
end
function TUM.mission.getSummaryString(onlyShowIncomplete, doublePercentage)
onlyShowIncomplete = onlyShowIncomplete or false
if missionStatus == TUM.mission.status.NONE then return "" end
local missionSummary = ""
for i=1,TUM.objectives.getCount() do
local o = TUM.objectives.getObjective(i)
if o then
if not o.completed or not onlyShowIncomplete then
missionSummary = missionSummary.."- Objective "..o.name..": "..Library.tasks[o.taskID].description.short
if not o.completed then
missionSummary = missionSummary.." ("..TUM.objectives.getObjectiveProgress(i, doublePercentage)..")"
else
missionSummary = missionSummary.." [DONE!]"
end
if i < TUM.objectives.getCount() then
missionSummary = missionSummary.."\n"
end
end
end
end
return missionSummary
end
function TUM.mission.endMission(endCause)
endCause = endCause or TUM.mission.endCause.ABORTED
if endCause == TUM.mission.endCause.ABORTED then
DCSEx.dcs.outPicture("Pic-MissionAborted.png", 5, true, 0, 1, 1, 25, 1)
TUM.playerScore.reset(true, "mission aborted")
elseif endCause == TUM.mission.endCause.COMPLETED then
DCSEx.dcs.outPicture("Pic-MissionComplete.png", 5, true, 0, 1, 1, 25, 1)
elseif endCause == TUM.mission.endCause.FAILED then
DCSEx.dcs.outPicture("Pic-MissionFailed.png", 5, true, 0, 1, 1, 25, 1)
end
trigger.action.outSound("UI-MissionEnd.ogg")
closeMission(true)
end
----------------------------------------------------------
-- Called on every mission update tick (every 15 seconds)
-- @return True if a radio message or other output was triggered, false otherwise
----------------------------------------------------------
function TUM.mission.onClockTick()
if TUM.mission.getStatus() == TUM.mission.status.NONE then return false end -- Not currenly in a mission
if TUM.objectives.getCount() <= 0 then return false end -- No objectives
objectivesReminderIntervalLeft = objectivesReminderIntervalLeft - 1
if objectivesReminderIntervalLeft > 0 then return false end
objectivesReminderIntervalLeft = OBJECTIVES_REMINDER_INTERVAL
TUM.mission.playMissionSummaryRadioMessage(true, false)
return true
end
-------------------------------------
-- Called when an event is raised
-- @param event The DCS World event
-------------------------------------
function TUM.mission.onEvent(event)
if missionStatus == TUM.mission.status.NONE then return end
if not event.initiator then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end
if not event.initiator:getPlayerName() then return end
-- All objectives complete and all players on the ground? Mission is complete
if event.id == world.event.S_EVENT_RUNWAY_TOUCH or event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT then
if TUM.objectives.areAllCompleted() and #DCSEx.world.getPlayersInAir(TUM.settings.getPlayerCoalition()) == 0 then
TUM.mission.endMission(TUM.mission.endCause.COMPLETED)
end
end
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end
-- When player dies in single-player, fail the mission
if event.id == world.event.S_EVENT_CRASH or event.id == world.event.S_EVENT_EJECTION or event.id == world.event.S_EVENT_PILOT_DEAD then
TUM.mission.endMission(TUM.mission.endCause.FAILED)
end
end
function TUM.mission.playMissionSummaryRadioMessage(onlyShowIncomplete, delayed)
onlyShowIncomplete = onlyShowIncomplete or false
delayed = delayed or false
local incompleteObjectives = TUM.objectives.getCount() - TUM.objectives.getCompletedCount()
local messageID = "commandMissionComplete"
if incompleteObjectives > 1 then
messageID = "commandObjectivesManyLeft"
elseif incompleteObjectives == 1 then
messageID = "commandObjectivesOneLeft"
end
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), messageID, { TUM.mission.getSummaryString(onlyShowIncomplete, true) }, "COMMAND", delayed)
end
end
@@ -0,0 +1,66 @@
-- ====================================================================================
-- TUM.MISSION - HANDLES THE F10 MENU DISPLAYED DURING A MISSION
-- ====================================================================================
-- ====================================================================================
TUM.missionMenu = {}
do
local function doCommandAbortMission()
TUM.mission.endMission(TUM.mission.endCause.ABORTED)
TUM.intermission.createMenu()
end
local function doCommandMissionStatus()
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "playerCommandMissionStatus", nil, "Flight", false)
TUM.mission.playMissionSummaryRadioMessage(false, true)
end
local function doCommandObjectiveLocation(index)
local obj = TUM.objectives.getObjective(index)
if not obj then return end
local messageSuffix = ""
if obj.preciseCoordinates then messageSuffix = "Precise" end
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "playerCommandRequireObjectives", { obj.name }, "Flight", false)
local players = coalition.getPlayers(TUM.settings.getPlayerCoalition())
for _,p in ipairs(players) do
local coordinates = DCSEx.world.getCoordinatesAsString(obj.waypoint3, false)
local braa = DCSEx.dcs.getBRAA(obj.waypoint3, p:getPoint(), false)
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "commandObjectiveCoordinates"..messageSuffix, { obj.name, coordinates, braa }, "Command", true)
end
end
function TUM.missionMenu.create()
missionCommands.removeItem() -- Clear the menu
missionCommands.addCommand("☱ Mission status", nil, doCommandMissionStatus, nil)
local objectivesMenuRoot = missionCommands.addSubMenu("Objectives")
for i=1,TUM.objectives.getCount() do
local obj = TUM.objectives.getObjective(i)
if obj then
local objRoot = missionCommands.addSubMenu("Objective "..obj.name.." ("..Library.tasks[obj.taskID].description.short..")", objectivesMenuRoot)
missionCommands.addCommand("Request objective coordinates", objRoot, doCommandObjectiveLocation, i)
TUM.supportJTAC.setupJTACOnObjective(i, objRoot)
end
end
TUM.supportAWACS.createMenu()
if not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then -- If not multiplayer, add "show mission score" command
missionCommands.addCommand("★ Display mission score", nil, TUM.playerScore.showScore, nil)
end
local abortRoot = missionCommands.addSubMenu("⬣ Abort mission")
if not TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) and DCSEx.io.canReadAndWrite() then
missionCommands.addCommand("✓ Confirm (all xp will be lost!)", abortRoot, doCommandAbortMission, nil)
else
missionCommands.addCommand("✓ Confirm", abortRoot, doCommandAbortMission, nil)
end
missionCommands.addCommand("✕ Cancel", abortRoot, DCSEx.dcs.doNothing, nil)
TUM.debugMenu.createMenu() -- Append debug menu to other menus (if debug mode enabled)
end
end
+206
View File
@@ -0,0 +1,206 @@
-- ====================================================================================
-- TUM.OBJECTIVES - HANDLES THE MISSION OBJECTIVES
-- ====================================================================================
-- ====================================================================================
TUM.objectives = {}
do
local objectives = {}
local function updateObjectiveText(index)
if index < 1 or index > #objectives then return end
local taskDB = Library.tasks[objectives[index].taskID]
local suffix = ""
if DCSEx.table.contains(taskDB.flags, DCSEx.enums.taskFlag.MOVING) then
suffix = "\n(last position known to intel, target is moving)"
end
local text = "Objective "..objectives[index].name..":\n"..taskDB.description.short.." ("..TUM.objectives.getObjectiveProgress(index)..")"..suffix
trigger.action.setMarkupText(objectives[index].markerTextID, text)
end
function TUM.objectives.add()
local objective = TUM.objectivesMaker.create()
if not objective then
TUM.log("Failed to spawn a group for objective #"..tostring(#objectives + 1)..".", TUM.logLevel.WARNING)
return false
end
table.insert(objectives, objective)
updateObjectiveText(#objectives)
return true
end
function TUM.objectives.getCount()
return #objectives
end
function TUM.objectives.getCompletedCount()
if #objectives == 0 then return 0 end
local count = 0
for i=1,#objectives do
if objectives[i].completed then count = count + 1 end
end
return count
end
function TUM.objectives.getCenter()
local point2 = { x = 0, y = 0 }
if #objectives == 0 then return point2 end
for _,o in ipairs(objectives) do
point2.x = point2.x + o.point2.x
point2.y = point2.y + o.point2.y
end
point2.x = point2.x / #objectives
point2.y = point2.y / #objectives
return point2
end
function TUM.objectives.getRadius()
if #objectives < 2 then return 10000 end -- Default to a 10km radius if no objectives or a single objective
local center = TUM.objectives.getCenter()
local radius = 0
for _,o in ipairs(objectives) do
local dist = DCSEx.math.getDistance2D(center, o.point2)
if dist > radius then radius = dist end
end
return radius
end
function TUM.objectives.getObjective(index)
if index < 1 or index > #objectives then return nil end
return DCSEx.table.deepCopy(objectives[index])
end
function TUM.objectives.getObjectiveProgress(index, doublePercentage)
doublePercentage = doublePercentage or false
if index < 1 or index > #objectives then return "" end
if TUM.DEBUG_MODE then
return tostring(#objectives[index].completedUnitsID).."/"..tostring(math.max(1, #objectives[index].unitsID))
else
local percentage = 0
if #objectives[index].unitsID > 0 then
percentage = math.floor((#objectives[index].completedUnitsID / math.max(1, #objectives[index].unitsID)) * 100.0)
end
if doublePercentage then
return tostring(percentage).."%%"
else
return tostring(percentage).."%"
end
end
end
function TUM.objectives.removeAll()
TUM.log("Removing all objectives...")
for _,o in ipairs(objectives) do
if o.groupID then
local g = DCSEx.world.getGroupByID(o.groupID)
if g then g:destroy() end
elseif o.unitsID then -- Some objects (such as static object) do not belong to a group, must be removed one by one
for _,id in ipairs(o.unitsID) do
local u = DCSEx.world.getUnitByID(id)
if u then
u:destroy()
else
local s = DCSEx.world.getStaticObjectByID(id)
if s then s:destroy() end
end
end
end
trigger.action.removeMark(o.markerID)
trigger.action.removeMark(o.markerTextID)
end
objectives = {}
end
function TUM.objectives.areAllCompleted()
if #objectives == 0 then return false end
return TUM.objectives.getCompletedCount() == TUM.objectives.getCount()
end
function TUM.objectives.getSceneryObjectObjective(sceneryObject)
if #objectives == 0 then return nil end
if not sceneryObject then return nil end
if Object.getCategory(sceneryObject) ~= Object.Category.SCENERY then return nil end
for i=1,#objectives do
if DCSEx.math.isSamePoint(sceneryObject:getPoint(), objectives[i].point3) then
return i
end
end
return nil
end
local function markObjectiveAsComplete(index)
if index < 1 or index > #objectives then return end -- Out of bounds
if objectives[index].completed then return end -- Objective already completed
objectives[index].completed = true
TUM.playerScore.awardCompletedObjective()
if TUM.objectives.areAllCompleted() then
TUM.mission.checkMissionStatus()
else
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "commandObjectiveComplete", { objectives[index].name }, "Command", true)
end
DCSEx.dcs.outPicture("Pic-ObjectiveComplete.png", 5, true, 0, 1, 1, 25, 1)
trigger.action.outSound("UI-MissionEnd.ogg")
end
local function onObjectiveEvent(index, event)
if index < 1 or index > #objectives then return end -- Out of bounds
if objectives[index].completed then return end -- Objective already completed
if event.id ~= world.event.S_EVENT_DEAD then return end
if not event.initiator then return end
if objectives[index].isSceneryTarget then
if Object.getCategory(event.initiator) == Object.Category.SCENERY then
if DCSEx.math.isSamePoint(event.initiator:getPoint(), objectives[index].point3) then
-- markObjectiveAsComplete(index)
timer.scheduleFunction(markObjectiveAsComplete, index, timer.getTime() + 3)
end
end
else
if Object.getCategory(event.initiator) == Object.Category.UNIT or Object.getCategory(event.initiator) == Object.Category.STATIC then
local unitID = DCSEx.dcs.getObjectIDAsNumber(event.initiator)
if DCSEx.table.contains(objectives[index].completedUnitsID, unitID) then return end
if not DCSEx.table.contains(objectives[index].unitsID, unitID) then return end
table.insert(objectives[index].completedUnitsID, unitID)
if #objectives[index].completedUnitsID == #objectives[index].unitsID then
timer.scheduleFunction(markObjectiveAsComplete, index, timer.getTime() + 3)
end
end
end
updateObjectiveText(index)
end
function TUM.objectives.onEvent(event)
for i,_ in ipairs(objectives) do
onObjectiveEvent(i, event)
end
end
end
@@ -0,0 +1,179 @@
-- ====================================================================================
-- TUM.OBJECTIVESMAKER - CREATE MISSION OBJECTIVES
-- ====================================================================================
-- ====================================================================================
TUM.objectivesMaker = {}
do
local function pickRandomTask()
local taskFamily = TUM.settings.getValue(TUM.settings.id.TASKING)
local validTaskIDs = {}
for k,t in pairs(Library.tasks) do
if t.taskFamily == taskFamily then
table.insert(validTaskIDs, k)
end
end
if #validTaskIDs == 0 then return nil end
return DCSEx.table.getRandom(validTaskIDs)
end
local function pickWaterPoint(nearThisPoint)
local waterZones = TUM.territories.getWaterZones()
if not waterZones or #waterZones == 0 then return nil end -- No "water" zones on this map
local possiblePoints = {}
for _=1,24 do
local point = DCSEx.zones.getRandomPointInside(DCSEx.table.getRandom(waterZones), land.SurfaceType.WATER)
if point then
table.insert(possiblePoints, point)
end
end
if #possiblePoints == 0 then return nil end
possiblePoints = DCSEx.dcs.getNearestPoints(nearThisPoint, possiblePoints, 1)
return possiblePoints[1]
end
function TUM.objectivesMaker.create()
local zone = DCSEx.zones.getByName(TUM.settings.getValue(TUM.settings.id.TARGET_LOCATION, true))
local taskID = pickRandomTask()
if not taskID then
TUM.log("Failed to find a valid task.", TUM.logLevel.WARNING)
return nil
end
local objectiveDB = Library.tasks[taskID]
local spawnPoint = nil
local isSceneryTarget = false
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.SCENERY_TARGET) then
local validSceneries = DCSEx.world.getSceneriesInZone(zone, DCSEx.zones.getRadius(zone), 100)
if not validSceneries or #validSceneries == 0 then
TUM.log("Failed to find a valid scenery object to use as target.", TUM.logLevel.WARNING)
return nil
end
local pickedScenery = DCSEx.table.getRandom(validSceneries)
spawnPoint = pickedScenery:getPoint()
isSceneryTarget = true
elseif objectiveDB.surfaceType == land.SurfaceType.WATER then
spawnPoint = pickWaterPoint(zone)
if not spawnPoint then
spawnPoint = DCSEx.world.getSpawnPoint(zone, objectiveDB.surfaceType, objectiveDB.safeRadius)
end
else
spawnPoint = DCSEx.world.getSpawnPoint(zone, objectiveDB.surfaceType, objectiveDB.safeRadius)
end
if not spawnPoint then
TUM.log("Failed to find a spawn point for objective.", TUM.logLevel.WARNING)
return nil
end
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.ON_ROADS) then
spawnPoint = DCSEx.world.getClosestPointOnRoadsVec2(spawnPoint)
end
local objective = {
completed = false,
completedUnitsID = {},
isSceneryTarget = isSceneryTarget,
markerID = DCSEx.world.getNextMarkerID(),
markerTextID = DCSEx.world.getNextMarkerID(),
name = Library.objectiveNames.get():upper(),
point2 = DCSEx.table.deepCopy(spawnPoint),
point3 = DCSEx.math.vec2ToVec3(spawnPoint, "land"),
preciseCoordinates = objectiveDB.waypointInaccuracy <= 0,
taskID = taskID,
unitsID = {}
}
if objectiveDB.waypointInaccuracy <= 0 then -- Exact coordinates are available
objective.waypoint2 = DCSEx.table.deepCopy(objective.point2)
objective.waypoint3 = DCSEx.table.deepCopy(objective.point3)
else -- No exact coordinates available, create the waypoint near the target
objective.waypoint2 = DCSEx.math.randomPointInCircle(objective.point2, objectiveDB.waypointInaccuracy)
objective.waypoint3 = DCSEx.math.vec2ToVec3(objective.waypoint2, "land")
end
if not DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.SCENERY_TARGET) then
-- Check group options
local groupOptions = {}
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.MOVING) then
local destPoint = DCSEx.math.randomPointInCircle(objective.point2, 5000, 2500, land.SurfaceType.LAND)
if destPoint then
groupOptions.isMoving = true
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.ON_ROADS) then
groupOptions.onRoad = true
destPoint = DCSEx.world.getClosestPointOnRoadsVec2(destPoint)
end
groupOptions.moveTo = destPoint
end
end
local units = Library.factions.getUnits(TUM.settings.getEnemyFaction(), objectiveDB.targetFamilies, math.random(objectiveDB.targetCount[1], objectiveDB.targetCount[2]))
local groupInfo = nil
if objectiveDB.targetFamilies[1] == DCSEx.enums.unitFamily.STATIC_STRUCTURE then
if units and #units >= 1 then
groupInfo = {}
groupInfo.unitsID = { DCSEx.unitGroupMaker.createStatic(TUM.settings.getEnemyCoalition(), objective.point2, units[1], "") }
end
else
groupInfo = DCSEx.unitGroupMaker.create(TUM.settings.getEnemyCoalition(), DCSEx.dcs.getUnitTypeFromFamily(objectiveDB.targetFamilies[1]), objective.point2, units, groupOptions)
end
if not groupInfo then
TUM.log("Failed to spawn a group for objective.", TUM.logLevel.WARNING)
return nil
end
objective.groupID = groupInfo.groupID
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.DESTROY_TRACK_RADARS_ONLY) then
objective.unitsID = {}
for i=1,#groupInfo.unitTypeNames do
if Unit.getDescByName(groupInfo.unitTypeNames[i]).attributes["SAM TR"] then
table.insert(objective.unitsID, groupInfo.unitsID[i])
end
end
if #objective.unitsID == 0 then
objective.unitsID = DCSEx.table.deepCopy(groupInfo.unitsID)
end
else
objective.unitsID = DCSEx.table.deepCopy(groupInfo.unitsID)
end
end
---------------------------------------------------------------------
-- Create dot marker (accurate WPs) or circle marker (inaccurate WPs)
---------------------------------------------------------------------
if objectiveDB.waypointInaccuracy <= 0 then
trigger.action.markToAll(objective.markerID, "Objective "..objective.name.."\n\n"..DCSEx.world.getCoordinatesAsString(objective.point3, false), objective.point3, true)
else
local circleRadius = math.max(objectiveDB.waypointInaccuracy, 1000)
trigger.action.circleToAll(
-1, objective.markerID,
objective.waypoint3, circleRadius,
{ 1, 1, 1, 1 }, { 1, 0, 0, 0.25 } , 2, true)
end
---------------------
-- Create text marker
---------------------
local textPoint3 = DCSEx.table.deepCopy(objective.waypoint3)
textPoint3.x = textPoint3.x + 224
textPoint3.z = textPoint3.z + 224
-- Text marker is created with an empty string, its content will be updated by TUM.MissionObjectives when it's added
trigger.action.textToAll(-1, objective.markerTextID, textPoint3, { 1, 1, 1, 1 }, { 0, 0, 0, .5 }, 12, true, "")
return objective
end
end
@@ -0,0 +1,308 @@
-- ====================================================================================
-- TUM.PLAYERCAREER - HANDLES THE PERSISTENT PILOT CAREER IN SINGLE-PLAYER MISSIONS
-- ====================================================================================
-- (local const) MAX_RIBBONS
-- (local const) MEDAL_BOX_DISPLAY_TIME
-- (local const) OBJECTIVES_PER_RIBBON
-- (local const) MEDALS
-- (local const) RANKS
-- (local table) careerStats
-- (local) fixIncompleteStats()
-- (local) getHighestMedal()
-- (local) getRibbonCount()
-- TUM.playerCareer.awardScore(score, objectives)
-- TUM.playerCareer.createMenu()
-- TUM.playerCareer.displayMedalBox(printSummary)
-- TUM.playerCareer.getCareerSummary()
-- TUM.playerCareer.load()
-- TUM.playerCareer.onStartUp()
-- TUM.playerCareer.reset()
-- TUM.playerCareer.save()
-- ====================================================================================
TUM.playerCareer = {}
do
local MAX_RIBBONS = 40 -- Maximum number of ribbons
local MEDAL_BOX_DISPLAY_TIME = 15 -- in seconds
local OBJECTIVES_PER_RIBBON = 4 -- How many completed objectives to gain a new ribbon?
local MEDALS = {
{ "Air medal", 200 },
{ "Bronze star", 300 },
{ "Airman's medal", 400 },
{ "Distinguished Flying Cross", 500 },
{ "Silver Star for Valor", 600 },
{ "Air Force Cross", 700 },
{ "Congressional Medal of Honor", 800 },
}
local RANKS = {
{ "2d Lt.", "Second lieutenant", 0 },
{ "1st Lt.", "First lieutenant", 500 },
{ "Capt.", "Captain", 2000 },
{ "Maj.", "Major", 8000 },
{ "Lt Col.", "Lieutenant colonel", 16000 },
{ "Col.", "Colonel", 32000 },
}
local careerStats = {}
-------------------------------------
-- Adds missing fields, if any, to the careerStats table
-------------------------------------
local function fixIncompleteStats()
if not careerStats then careerStats = { } end
if not careerStats.bestSortie then careerStats.bestSortie = 0 end
if not careerStats.completedObjectives then careerStats.completedObjectives = 0 end
if not careerStats.completedSorties then careerStats.completedSorties = 0 end
if not careerStats.medals then careerStats.medals = 0 end
if not careerStats.medalWounded then careerStats.medalWounded = false end
if not careerStats.rank then careerStats.rank = 1 end
if not careerStats.score then careerStats.score = 0 end
careerStats.version = TUM.VERSION_NUMBER
end
-------------------------------------
-- Returns the highest medal a player has obtained for scoring a high number of points during a single sortie
-- @return A number (index in the MEDALS table)
-------------------------------------
local function getHighestMedal()
local medal = 0
for i=1,#MEDALS do
if careerStats.bestSortie >= MEDALS[i][2] then
medal = i
end
end
return medal
end
-------------------------------------
-- Returns the current number a ribbons a player was awarded for completing objectives
-- @return A number
-------------------------------------
local function getRibbonCount()
return DCSEx.math.clamp(math.floor(careerStats.completedObjectives / OBJECTIVES_PER_RIBBON), 0, MAX_RIBBONS)
end
-------------------------------------
-- Awards career points. Only works in single-player missions
-- @param score Number of career points to award
-- @param objectives Number of completed objectives to award
-- @return True if a ribbon, medal or promotion was awarded, false otherwise
-------------------------------------
function TUM.playerCareer.awardScore(score, objectives)
if not DCSEx.io.canReadAndWrite() then return false end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return false end -- No career in multiplayer
score = math.max(0, math.floor(score or 0))
fixIncompleteStats()
local oldRibbonCount = getRibbonCount()
careerStats.bestSortie = math.max(careerStats.bestSortie, score)
careerStats.score = careerStats.score + score
careerStats.completedObjectives = careerStats.completedObjectives + objectives
TUM.log("Awarded "..tostring(score).." xp and "..tostring(objectives).." completed objectives to player.")
local newRibbonCount = getRibbonCount()
local somethingWasAwarded = false
-- Check for promotions
if careerStats.rank < #RANKS and careerStats.score >= RANKS[careerStats.rank + 1][3] then
careerStats.rank = careerStats.rank + 1
somethingWasAwarded = true
trigger.action.outText("✪ You have been promoted to the rank of "..RANKS[careerStats.rank][2]..".", MEDAL_BOX_DISPLAY_TIME)
end
-- Check for medals
for i=1,#MEDALS do
if i > careerStats.medals and score >= MEDALS[i][2] then
trigger.action.outText("✪ You have been awarded the "..MEDALS[i][1]..".", MEDAL_BOX_DISPLAY_TIME)
careerStats.medals = i
somethingWasAwarded = true
break
end
end
-- Check for ribbons
if newRibbonCount > oldRibbonCount then
trigger.action.outText("✪ You have been awarded a battle ribbon.", MEDAL_BOX_DISPLAY_TIME)
somethingWasAwarded = true
end
TUM.playerCareer.save()
if somethingWasAwarded then
TUM.playerCareer.displayMedalBox(false)
end
return somethingWasAwarded
end
-------------------------------------
-- Appends the career menu to the F10 menu. Only works in single-player missions
-------------------------------------
function TUM.playerCareer.createMenu()
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No career in multiplayer
missionCommands.addCommand("✪ View pilot career stats", nil, TUM.playerCareer.displayMedalBox, true)
end
-------------------------------------
-- Displays the player's medal box and carrer summary. Only works in single-player missions
-------------------------------------
function TUM.playerCareer.displayMedalBox(printSummary)
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No career in multiplayer
fixIncompleteStats()
if printSummary then
trigger.action.outText(TUM.playerCareer.getCareerSummary(), MEDAL_BOX_DISPLAY_TIME, true)
end
DCSEx.dcs.outPicture("Pic-MedalBox.png", MEDAL_BOX_DISPLAY_TIME, true, 0, 2, 2, 50, 1)
DCSEx.dcs.outPicture("Pic-Rank"..tostring(careerStats.rank)..".png", MEDAL_BOX_DISPLAY_TIME, false, 0, 2, 2, 50, 1)
local ribbonCount = getRibbonCount()
for i=1,ribbonCount do
DCSEx.dcs.outPicture("Pic-Ribbon"..tostring(i)..".png", MEDAL_BOX_DISPLAY_TIME, false, 0, 2, 2, 50, 1)
end
for i=1,careerStats.medals do
DCSEx.dcs.outPicture("Pic-Medal"..tostring(i)..".png", MEDAL_BOX_DISPLAY_TIME, false, 0, 2, 2, 50, 1)
end
trigger.action.outSound("UI-Career.ogg")
end
-------------------------------------
-- Returns the player career summary as a string. Only works in single-player missions
-- @return A string
-------------------------------------
function TUM.playerCareer.getCareerSummary()
if not DCSEx.io.canReadAndWrite() then return "" end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return "" end -- No career in multiplayer
fixIncompleteStats()
local playerName = "Player"
local players = DCSEx.world.getAllPlayers()
if #players > 0 then
playerName = players[1]:getPlayerName()
end
local summary = ""
summary = summary.."CAREER STATS FOR "..RANKS[careerStats.rank][1]:upper().." "..playerName:upper()..":\n"
summary = summary.."=======================\n"
summary = summary.."- Rank: "..RANKS[careerStats.rank][2].."\n"
summary = summary.."- Best sortie XP: "..DCSEx.string.toStringThousandsSeparator(careerStats.bestSortie).."\n"
summary = summary.."- Total career XP: "..DCSEx.string.toStringThousandsSeparator(careerStats.score).."\n"
summary = summary.."- Completed objectives: "..tostring(careerStats.completedObjectives).."\n"
if careerStats.medals == 0 then
summary = summary.."- Medals: None"
else
summary = summary.."- Medals:"
for i=1,careerStats.medals do
summary = summary.."\n - "..MEDALS[i][1]
end
end
local ribbonCount = getRibbonCount()
if ribbonCount < MAX_RIBBONS or careerStats.rank < #RANKS or careerStats.medals < #MEDALS then
summary = summary.."\n"
if careerStats.rank < #RANKS then
summary = summary.."\n- Next promotion: "..DCSEx.string.toStringThousandsSeparator(RANKS[careerStats.rank + 1][3]).." xp"
end
if ribbonCount < MAX_RIBBONS then
summary = summary.."\n- Next ribbon: "..tostring((ribbonCount + 1) * OBJECTIVES_PER_RIBBON).." objectives"
end
if careerStats.medals < #MEDALS then
summary = summary.."\n- Next medal: "..DCSEx.string.toStringThousandsSeparator(MEDALS[careerStats.medals + 1][2]).." xp in a single flight"
end
end
return summary
end
-------------------------------------
-- Loads the player career from the disk. Only works in single-player missions
-- @return True if everything worked (or disabled because of MP), false if an error happened
-------------------------------------
function TUM.playerCareer.load()
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return true end -- No career in multiplayer
if not DCSEx.io.canReadAndWrite() then return false end
local jsonString = DCSEx.io.load("TheUniversalMission.sav")
if jsonString then
-- TODO: what if Json is malformed?
careerStats = net.json2lua(jsonString)
if not careerStats then
careerStats = {}
fixIncompleteStats()
TUM.log("Failed to load player career data, career data reset.")
else
TUM.log("Player career data loaded successfully.")
end
else
fixIncompleteStats()
return false
end
fixIncompleteStats()
end
-------------------------------------
-- Called on mission start up
-- @return True if started up properly, false if an error happened
-------------------------------------
function TUM.playerCareer.onStartUp()
fixIncompleteStats()
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return true end -- No career in multiplayer
if DCSEx.io.canReadAndWrite() then
TUM.log("Lua IO module available, can read and write.")
TUM.playerCareer.load()
else
local msg = "IO module is disabled, CANNOT read and write persistant data. Player progress will NOT be saved.\n"
msg = msg.."To enable the IO module, comment or remove the \"sanitizeModule('io')\" line in \n"
msg = msg.."[DCSWorld installation directory]\\Scripts\\MissionScripting.lua and restart the game."
TUM.log(msg, TUM.logLevel.WARNING)
end
return true
end
-------------------------------------
-- Resets the player career stats and save them. Only works in single-player missions
-------------------------------------
function TUM.playerCareer.reset()
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No career in multiplayer
careerStats = nil
fixIncompleteStats()
TUM.playerCareer.save()
end
-------------------------------------
-- Save the player career to the disk. Only works in single-player missions
-- @return True if everything worked (or disabled), false if an error happened
-------------------------------------
function TUM.playerCareer.save()
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return true end -- No career in multiplayer
if not DCSEx.io.canReadAndWrite() then return true end -- IO disabled, career and scoring disabled
fixIncompleteStats()
if DCSEx.io.save("TheUniversalMission.sav", net.lua2json(careerStats)) then
return true
else
return false
end
end
end
@@ -0,0 +1,391 @@
-- ====================================================================================
-- TUM.PLAYERSCORE - HANDLES ALL SCORING DURING SINGLE-PLAYER MISSIONS
-- ====================================================================================
-- (const) SKILL_MULTIPLIER_BONUS
-- (const) SCORE_REMINDER_INTERVAL
-- (local) getKillValue(killedObject)
-- (local) onKillEvent(event)
-- (local) onLandEvent(event)
-- (local) onResetEvent(event)
-- (local) printReminder()
-- TUM.playerScore.award(amount, message, silent)
-- TUM.playerScore.getCompletedObjectives()
-- TUM.playerScore.getScore()
-- TUM.playerScore.getScoreMultiplier(settingID, settingValue)
-- TUM.playerScore.getTotalScoreMultiplier()
-- TUM.playerScore.onClockTick(clockTick)
-- TUM.playerScore.onEvent(event)
-- TUM.playerScore.reset(showMessage, reason)
-- TUM.playerScore.showScore()
-- ====================================================================================
TUM.playerScore = {}
do
local SKILL_MULTIPLIER_BONUS = {
0,
0.1, -- 0.05
0.2, -- 0.125
0.4, -- 0.25
0.6 -- 0.5
}
local SCORE_REMINDER_INTERVAL = 5 -- in minutes
local completedObjectives = 0
local score = 0
local scoreReminderIntervalLeft = SCORE_REMINDER_INTERVAL
local function getKillValue(killedObject)
if not killedObject then return 0 end
if Object.getCategory(killedObject) == Object.Category.BASE then return 60 end
if Object.getCategory(killedObject) == Object.Category.STATIC then return 60 end
if Object.getCategory(killedObject) == Object.Category.SCENERY then
for i=1,TUM.objectives.getCount() do
local obj = TUM.objectives.getObjective(i)
if obj then
if obj.isSceneryTarget then
if DCSEx.math.isSamePoint(killedObject:getPoint(), obj.point3) then
return 60
end
end
end
end
return 0
end
if Object.getCategory(killedObject) ~= Object.Category.UNIT then return 0 end
local objectDesc = killedObject:getDesc()
if not objectDesc or not objectDesc.attributes then return 10 end -- No description, assume a default value of 10 points
local groundMultiplier = 1
if not killedObject:inAir() then groundMultiplier = 0.5 end -- Aircraft killed on the ground are worth less points
-- Misc
if objectDesc.attributes["Missiles"] then return 10 end
if objectDesc.attributes["UAVs"] then return math.floor(15 * groundMultiplier) end
-- Fixed wing
if objectDesc.attributes["Fighters"] then return math.floor(40 * groundMultiplier) end
if objectDesc.attributes["Interceptors"] then return math.floor(40 * groundMultiplier) end
if objectDesc.attributes["Interceptors"] then return math.floor(40 * groundMultiplier) end
if objectDesc.attributes["Planes"] then return math.floor(25 * groundMultiplier) end
-- Rotary wing
if objectDesc.attributes["Attack helicopters"] then return math.floor(30 * groundMultiplier) end
if objectDesc.attributes["Helicopters"] then return math.floor(25 * groundMultiplier) end
-- Default air
if objectDesc.attributes["Air"] then return math.floor(20 * groundMultiplier) end
-- Ships
if objectDesc.attributes["Aircraft Carriers"] then return 300 end
if objectDesc.attributes["Cruisers"] then return 250 end
if objectDesc.attributes["Destroyers"] then return 150 end
if objectDesc.attributes["Frigates"] then return 150 end
if objectDesc.attributes["Corvettes"] then return 100 end
if objectDesc.attributes["Heavy armed ships"] then return 75 end
if objectDesc.attributes["Ships"] then return 25 end
-- Air defense
if objectDesc.attributes["MANPADS AUX"] then return 5 end
if objectDesc.attributes["MANPADS"] then return 10 end
if objectDesc.attributes["SR SAM"] then return 20 end
if objectDesc.attributes["IR Guided SAM"] then return 15 end
if objectDesc.attributes["SAM TR"] then return 25 end
if objectDesc.attributes["SAM SR"] then return 15 end
if objectDesc.attributes["Armed Air Defence"] then return 10 end
if objectDesc.attributes["SAM elements"] then return 5 end
if objectDesc.attributes["SAM related"] then return 5 end
-- Ground vehicles
if objectDesc.attributes["Modern Tanks"] then return 20 end
if objectDesc.attributes["Tanks"] then return 15 end
if objectDesc.attributes["Modern Tanks"] then return 25 end
if objectDesc.attributes["HeavyArmoredUnits"] then return 20 end
if objectDesc.attributes["LightArmoredUnits"] then return 15 end
if objectDesc.attributes["NonArmoredUnits"] then return 10 end
if objectDesc.attributes["Unarmed vehicles"] then return 10 end
-- Infantry
if objectDesc.attributes["Infantry"] then return 3 end
return 10 -- Don't know what this thing is, assume a default value of 10 points
end
-------------------------------------
-- Called by TUM.playerScore.onEvent when a KILL event is triggered
-- @param event The DCS World event
-------------------------------------
local function onKillEvent(event)
if not event.target then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end
if not event.initiator:getPlayerName() then return end
local killValue = getKillValue(event.target)
if killValue <= 0 then return end
-- Higher reward for higher threat levels
local scoreMultiplier = TUM.playerScore.getTotalScoreMultiplier()
killValue = math.max(1, math.floor(killValue * scoreMultiplier))
-- Penalty for destroying friendly or civilian units
local prefix = ""
if Object.getCategory(event.target) == Object.Category.UNIT then
if event.initiator:getCoalition() == event.target:getCoalition() then
prefix = "friendly "
killValue = killValue * -5.0
elseif event.target:getCoalition() == coalition.side.NEUTRAL then
prefix = "neutral "
killValue = killValue * -2.0
end
end
TUM.playerScore.award(killValue, "destroyed "..prefix..Library.objectNames.get(event.target))
end
-------------------------------------
-- Called by TUM.playerScore.onEvent when a LAND event is triggered
-- @param event The DCS World event
-------------------------------------
local function onLandEvent(event)
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end
if not event.initiator:getPlayerName() then return end
local muteRadioMessage = false
if score > 0 or completedObjectives > 0 then
muteRadioMessage = TUM.playerCareer.awardScore(score, completedObjectives)
end
-- Single-player landing radio message is handled here instead of in AmbientRadio to avoid
-- "conflicts" with the "awardScore" message if both the medal case and the radio message are
-- triggered at the same time (delaying the radio message wouldn't be a solution as this would
-- interrupt the medal case music very quickly)
if not muteRadioMessage then
local baseName = "AIRBASE"
if event.place then
baseName = event.place:getName():upper()
end
TUM.radio.playForAll("atcSafeLandingPlayer", {event.initiator:getCallsign(), baseName}, baseName.." ATC")
end
TUM.playerScore.reset(false)
end
-------------------------------------
-- Called by TUM.playerScore.onEvent when any event causing a score reset (crash, ejection, slot change...) is triggered
-- @param event The DCS World event
-------------------------------------
local function onResetEvent(event)
if not event.initiator:getPlayerName() then return end
local reason = nil
if event.id == world.event.S_EVENT_CRASH then
reason = "you crashed"
elseif event.id == world.event.S_EVENT_PILOT_DEAD then
reason = "you were killed"
elseif event.id == world.event.S_EVENT_EJECTION then
reason = "you ejected"
elseif event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT then
reason = "you've taken control of a new aircraft"
end
TUM.playerScore.reset(true, reason)
end
-------------------------------------
-- Print a reminder that the player has to land for their current score and completed objectives to be added to their flight profile
-- @return True if a reminded was printed, false if it was not needed (no score to award, etc)
-------------------------------------
local function printReminder()
if score <= 0 and completedObjectives <= 0 then return false end -- Nothing to remind the player of
local msg = ""
if score > 0 and completedObjectives > 0 then
msg = string.format("You've been awarded %s point(s) and have completed %d objective(s).", DCSEx.string.toStringThousandsSeparator(score), completedObjectives)
elseif score > 0 then
msg = string.format("You've been awarded %s point(s).", DCSEx.string.toStringThousandsSeparator(score))
else
msg = string.format("You have completed %d objective(s).", completedObjectives)
end
trigger.action.outText("REMINDER: "..msg.." They will be awarded to your flight career once you've landed.", 5)
trigger.action.outSound("UI-Ok.ogg")
return true
end
-------------------------------------
-- Awards points to the player. Only works in single-player missions
-- @param amount Number of points to award
-- @param message Message to display (why are these points awarded?). If missing, no message will be displayed.
-------------------------------------
function TUM.playerScore.award(amount, message)
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No scoring in multiplayer
score = score + amount
if message then
TUM.log("Awarded "..DCSEx.string.toStringThousandsSeparator(amount).." points ("..message..").")
end
end
-------------------------------------
-- Awards a new completed objective to the player. Only works in single-player missions
-------------------------------------
function TUM.playerScore.awardCompletedObjective()
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No scoring in multiplayer
completedObjectives = completedObjectives + 1
end
-------------------------------------
-- Returns the current number of completed objectives. Only works in single-player missions
-- @return A number
-------------------------------------
function TUM.playerScore.getCompletedObjectives()
if not DCSEx.io.canReadAndWrite() then return 0 end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return 0 end -- No scoring in multiplayer
return completedObjectives
end
-------------------------------------
-- Returns the current player score. Only works in single-player missions
-- @return A number
-------------------------------------
function TUM.playerScore.getScore()
if not DCSEx.io.canReadAndWrite() then return 0 end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return 0 end -- No scoring in multiplayer
return score
end
-------------------------------------
-- Returns the value to add to the global score multiplier according to a given setting
-- @param settingID ID of the setting (from the TUM.settings.id enum)
-- @param settingValue The setting value
-- @return A number (0 if no multiplier)
-------------------------------------
function TUM.playerScore.getScoreMultiplier(settingID, settingValue)
if not DCSEx.io.canReadAndWrite() then return 0 end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return 0 end -- No scoring in multiplayer
if settingID == TUM.settings.id.ENEMY_AIR_DEFENSE or settingID == TUM.settings.id.ENEMY_AIR_FORCE then
return SKILL_MULTIPLIER_BONUS[settingValue]
end
return 0
end
-------------------------------------
-- Returns the global score multiplier according the current settings
-- @return A number (1.0 if no multiplier)
-------------------------------------
function TUM.playerScore.getTotalScoreMultiplier()
if not DCSEx.io.canReadAndWrite() then return 1.0 end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return 1.0 end -- No scoring in multiplayer
local scoreMultiplier = 1.0
if TUM.settings.getValue(TUM.settings.id.TASKING) ~= DCSEx.enums.taskFamily.ANTISHIP then -- No ground air defense during antiship strikes
scoreMultiplier = scoreMultiplier +TUM.playerScore.getScoreMultiplier(TUM.settings.id.ENEMY_AIR_DEFENSE, TUM.settings.getValue(TUM.settings.id.ENEMY_AIR_DEFENSE))
end
scoreMultiplier = scoreMultiplier +TUM.playerScore.getScoreMultiplier(TUM.settings.id.ENEMY_AIR_FORCE, TUM.settings.getValue(TUM.settings.id.ENEMY_AIR_FORCE))
return scoreMultiplier
end
----------------------------------------------------------
-- Called on every mission update tick (every 10-20 seconds)
-- @return True if something was done this tick, false otherwise
----------------------------------------------------------
function TUM.playerScore.onClockTick()
if not DCSEx.io.canReadAndWrite() then return false end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return false end -- No scoring in multiplayer
if score == 0 and completedObjectives == 0 then return false end -- Nothing to remind the player of
scoreReminderIntervalLeft = scoreReminderIntervalLeft - 1
if scoreReminderIntervalLeft == 0 then
scoreReminderIntervalLeft = SCORE_REMINDER_INTERVAL
return printReminder()
end
return false
end
-------------------------------------
-- Called when an event is raised
-- @param event The DCS World event
-------------------------------------
function TUM.playerScore.onEvent(event)
if not event.initiator then return end
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No scoring in multiplayer
if event.id == world.event.S_EVENT_KILL then
onKillEvent(event)
return
end
if event.id == world.event.S_EVENT_LAND then
onLandEvent(event)
return
end
if event.id == world.event.S_EVENT_CRASH or event.id == world.event.S_EVENT_PILOT_DEAD or event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT or event.id == world.event.S_EVENT_EJECTION then
onResetEvent(event)
return
end
end
-------------------------------------
-- Resets the player score to 0
-- @param showMessage Should a message be displayed (if any points are lost)?
-- @param reason The reason for the point loss, displayed in the message
-------------------------------------
function TUM.playerScore.reset(showMessage, reason)
if completedObjectives == 0 and score == 0 then return end
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No scoring in multiplayer
if showMessage then
local msg = ""
if reason then
msg = "Unstowed progress lost because "..reason.."."
else
msg = "Unstowed progress lost."
end
msg = msg.." You lost "..DCSEx.string.toStringThousandsSeparator(score).." xp and "..tostring(completedObjectives).." completed objective(s)."
trigger.action.outText(msg, 5)
trigger.action.outSound("UI-Error.ogg")
else
TUM.log("Mission score reset.")
end
completedObjectives = 0
score = 0
end
-------------------------------------
-- Shows the current mission score
-------------------------------------
function TUM.playerScore.showScore()
if not DCSEx.io.canReadAndWrite() then return end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return end -- No scoring in multiplayer
local scoreMsg = "CURRENT PROGRESS (will be awarded to your career profile on landing):\n"
scoreMsg = scoreMsg.."XP: "..DCSEx.string.toStringThousandsSeparator(score).."\n"
scoreMsg = scoreMsg.."Completed objectives: "..tostring(completedObjectives)
trigger.action.outText(scoreMsg, 5)
trigger.action.outSound("UI-Ok.ogg")
end
end
+160
View File
@@ -0,0 +1,160 @@
-- ====================================================================================
-- TUM.RADIO - HANDLES FUNCTIONS TO PLAY RADIO MESSAGES
-- ====================================================================================
-- (const, local) ANSWER_DELAY
-- (local) function doRadioMessage(args, time)
-- TUM.radio.playForAll(messageID, replacements, callsign, delayed, functionToRun, functionParameters)
-- TUM.radio.playForCoalition(coalitionID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
-- TUM.radio.playForGroup(groupID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
-- TUM.radio.playForUnit(unitID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
-- ====================================================================================
TUM.radio = {}
do
-- Min/max time to get a answer to a radio message, in seconds
local ANSWER_DELAY = { 3.0, 5.0 }
-------------------------------------
-- Executes a radio message
-- @param args Message parameters
-- @param time Game time at the moment the message is played
-------------------------------------
local function doRadioMessage(args, time)
if not args or not args.messageID then return nil end
local callsign = args.callsign
if not callsign then
local unit = DCSEx.world.getUnitByID(args.unitID)
if not unit then
callsign = "Flight"
else
callsign = unit:getCallsign()
end
end
local message = ""
local oggFile = args.messageID
if type(Library.radioMessages[args.messageID]) == "table" then
local index = DCSEx.table.getRandomIndex(Library.radioMessages[args.messageID])
oggFile = oggFile..tostring(index)
message = Library.radioMessages[args.messageID][index]
else
message = Library.radioMessages[args.messageID]
end
if args.replacements then
for i,r in ipairs(args.replacements) do
message = message:gsub("$"..tostring(i), tostring(r))
end
end
local duration = DCSEx.string.getReadingTime(message)
-- Print message
trigger.action.outTextForUnit(args.unitID, callsign:upper()..": "..message, duration, false)
-- Play sound
trigger.action.outSoundForUnit(args.unitID, "Radio-"..oggFile..".ogg")
if args.functionToRun then -- a function was provided, run it
args.functionToRun(args.functionParameters)
end
return nil -- disable scheduling, if any
end
-------------------------------------
-- Plays a message to all players in a coalition
-- @param messageID ID of the radio message in scrambe.db.radioMessages
-- @param replacements String placeholders ($1, $2...) replacements in the message
-- @param callsign Name of the person speaking or nil to use unitID's callsign
-- @param delayed Should the message be delayed (used for message answers)
-- @param functionToRun Function to run when the message is played
-- @param functionParameters Parameters for the function to run when the message is played
-------------------------------------
function TUM.radio.playForAll(messageID, replacements, callsign, delayed, functionToRun, functionParameters)
local players = DCSEx.world.getAllPlayers()
for _, unit in pairs(players) do
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(unit), messageID, replacements, callsign, delayed, functionToRun, functionParameters)
end
end
-------------------------------------
-- Plays a message to all players in a given coalition
-- @param coalitionID ID of the coalition (coalition.side.XXX)
-- @param messageID ID of the radio message in scrambe.db.radioMessages
-- @param replacements String placeholders ($1, $2...) replacements in the message
-- @param callsign Name of the person speaking or nil to use unitID's callsign
-- @param delayed Should the message be delayed (used for message answers)
-- @param functionToRun Function to run when the message is played
-- @param functionParameters Parameters for the function to run when the message is played
-------------------------------------
function TUM.radio.playForCoalition(coalitionID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
local players = coalition.getPlayers(coalitionID)
for _,u in pairs(players) do
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(u), messageID, replacements, callsign, delayed, functionToRun, functionParameters)
end
end
-------------------------------------
-- Plays a message to players from a certain group
-- @param groupID ID of the group sending/receiving the message
-- @param messageID ID of the radio message in scrambe.db.radioMessages
-- @param replacements String placeholders ($1, $2...) replacements in the message
-- @param callsign Name of the person speaking or nil to use unitID's callsign
-- @param delayed Should the message be delayed (used for message answers)
-- @param functionToRun Function to run when the message is played
-- @param functionParameters Parameters for the function to run when the message is played
-------------------------------------
function TUM.radio.playForGroup(groupID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
local group = DCSEx.world.getGroupByID(groupID)
if not group then return end -- group does not exist
for _,u in pairs(group:getUnits()) do
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(u), messageID, replacements, callsign, delayed, functionToRun, functionParameters)
end
end
-------------------------------------
-- Plays a message for a given unit only
-- @param unitID ID of the unit receiving the message
-- @param messageID ID of the radio message in scrambe.db.radioMessages
-- @param replacements String placeholders ($1, $2...) replacements in the message
-- @param callsign Name of the person speaking or nil to use unitID's callsign
-- @param replacements Table of two tables used for string replacements. E.g. { {"UNIT_NAME", "TIME"}, {"Enfield11", "12:30"}}
-- @param delayed Should the message be delayed (used for message answers)
-- @param functionToRun Function to run when the message is played
-- @param functionParameters Parameters for the function to run when the message is played
-------------------------------------
function TUM.radio.playForUnit(unitID, messageID, replacements, callsign, delayed, functionToRun, functionParameters)
if not messageID then return end
if not Library.radioMessages[messageID] then return end
delayed = delayed or false
if replacements and type(replacements) ~= "table" then
replacements = { replacements }
end
local radioArgs = {
callsign = callsign,
functionToRun = functionToRun,
functionParameters = functionParameters,
messageID = messageID,
replacements = replacements,
unitID = unitID
}
if delayed then -- message is delayed, schedule it
timer.scheduleFunction(
doRadioMessage,
radioArgs,
timer.getTime() + DCSEx.math.randomFloat(ANSWER_DELAY[1], ANSWER_DELAY[2])
)
else -- no delay, play the message at once
doRadioMessage(radioArgs, nil)
end
end
end
+232
View File
@@ -0,0 +1,232 @@
-- ====================================================================================
-- TUM.SETTINGS - HANDLES THE MISSION SETTINGS
-- ====================================================================================
-- (enum) TUM.settings.id
-- TUM.settings.getName(id)
-- TUM.settings.getValue(id, returnAsString)
-- TUM.settings.setAllToDefaults()
-- TUM.settings.setValue(id, value, updateMenu)
-- ====================================================================================
TUM.settings = {}
TUM.settings.id = {
AI_CAP = 1,
COALITION_BLUE = 2,
COALITION_RED = 3,
ENEMY_AIR_DEFENSE = 4,
ENEMY_AIR_FORCE = 5,
MULTIPLAYER = 6,
PLAYER_COALITION = 7,
TARGET_COUNT = 8,
TARGET_LOCATION = 9,
TASKING = 10,
TIME_PERIOD = 11,
}
do
local settings = {}
local SETTING_NAMES = {
[TUM.settings.id.AI_CAP] = "Friendly AI CAP",
[TUM.settings.id.COALITION_BLUE] = "Blue coalition",
[TUM.settings.id.COALITION_RED] = "Red coalition",
[TUM.settings.id.ENEMY_AIR_DEFENSE] = "Enemy air defense",
[TUM.settings.id.ENEMY_AIR_FORCE] = "Enemy air force",
[TUM.settings.id.MULTIPLAYER] = "Multiplayer",
[TUM.settings.id.PLAYER_COALITION] = "Player coalition",
[TUM.settings.id.TARGET_COUNT] = "Target count",
[TUM.settings.id.TARGET_LOCATION] = "Target location",
[TUM.settings.id.TASKING] = "Mission type",
[TUM.settings.id.TIME_PERIOD] = "Time period",
}
local SETTING_VALUES = {
[TUM.settings.id.AI_CAP] = { "Enabled", "Disabled" },
[TUM.settings.id.COALITION_BLUE] = { },
[TUM.settings.id.COALITION_RED] = { },
[TUM.settings.id.ENEMY_AIR_DEFENSE] = { "None", "Green", "Regular", "Veteran", "Elite" },
[TUM.settings.id.ENEMY_AIR_FORCE] = { "None", "Green", "Regular", "Veteran", "Elite" },
[TUM.settings.id.PLAYER_COALITION] = { "Red", "Blue" }, -- Must match values in the coalition.side enum
[TUM.settings.id.TARGET_COUNT] = { "1", "2", "3", "4" },
[TUM.settings.id.TARGET_LOCATION] = { },
[TUM.settings.id.TASKING] = { "Antiship strike", "Ground attack", "Interception", "SEAD", "Strike" }, -- Must match values in the DCSEx.enums.taskFamily enum
[TUM.settings.id.TIME_PERIOD] = { "World War 2", "Korea War", "Vietnam War", "Late Cold War", "Modern" }, -- Must match values in the DCSEx.enums.timePeriod enum
}
local function getFaction(side)
if side == coalition.side.BLUE then
return TUM.settings.getValue(TUM.settings.id.COALITION_BLUE, true)
else
return TUM.settings.getValue(TUM.settings.id.COALITION_RED, true)
end
end
local function setAllToDefaults(coreSettings)
settings = {
[TUM.settings.id.AI_CAP] = 1, -- Enabled
[TUM.settings.id.COALITION_BLUE] = 1,
[TUM.settings.id.COALITION_RED] = 2,
[TUM.settings.id.ENEMY_AIR_DEFENSE] = 3,
[TUM.settings.id.ENEMY_AIR_FORCE] = 2,
[TUM.settings.id.MULTIPLAYER] = coreSettings.multiplayer,
[TUM.settings.id.PLAYER_COALITION] = coalition.side.BLUE,
[TUM.settings.id.TARGET_COUNT] = 2,
[TUM.settings.id.TARGET_LOCATION] = 1,
[TUM.settings.id.TASKING] = DCSEx.enums.taskFamily.GROUND_ATTACK,
[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.MODERN
}
-- TODO: set default time period according to mission year
-- if env.mission.date.Year <= 1945 then
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.WORLD_WAR_2
-- elseif env.mission.date.Year < 1960 then
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.KOREA_WAR
-- elseif env.mission.date.Year < 1975 then
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.VIETNAM_WAR
-- elseif env.mission.date.Year < 1990 then
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.COLD_WAR
-- else
-- settings[TUM.settings.id.TIME_PERIOD] = DCSEx.enums.timePeriod.MODERN
-- end
for i,id in pairs(SETTING_VALUES[TUM.settings.id.COALITION_BLUE]) do
if id == Library.factions.defaults[coalition.side.BLUE] then settings[TUM.settings.id.COALITION_BLUE] = i end
if id == Library.factions.defaults[coalition.side.RED] then settings[TUM.settings.id.COALITION_RED] = i end
end
if #DCSEx.envMission.getPlayerGroups(coalition.side.RED) > 0 then
settings[TUM.settings.id.PLAYER_COALITION] = coalition.side.RED
end
end
function TUM.settings.getSettingsName(id)
return SETTING_NAMES[id]
end
function TUM.settings.getPossibleValues(id)
return SETTING_VALUES[id];
end
function TUM.settings.getValue(id, returnAsString)
returnAsString = returnAsString or false
if returnAsString then
return SETTING_VALUES[id][settings[id]]
end
return settings[id]
end
function TUM.settings.getPlayerCoalition()
return settings[TUM.settings.id.PLAYER_COALITION]
end
function TUM.settings.getEnemyCoalition()
return DCSEx.dcs.getOppositeCoalition(settings[TUM.settings.id.PLAYER_COALITION])
end
function TUM.settings.getPlayerFaction()
return getFaction(TUM.settings.getPlayerCoalition())
end
function TUM.settings.getEnemyFaction()
return getFaction(TUM.settings.getEnemyCoalition())
end
function TUM.settings.getSettingsSummary()
local showScoreMultiplier = true
if not DCSEx.io.canReadAndWrite() or TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then
showScoreMultiplier = false
end
local settingsOrder = {
TUM.settings.id.PLAYER_COALITION,
TUM.settings.id.MULTIPLAYER,
-1,
TUM.settings.id.TIME_PERIOD,
TUM.settings.id.COALITION_BLUE,
TUM.settings.id.COALITION_RED,
-1,
TUM.settings.id.TASKING,
TUM.settings.id.TARGET_LOCATION,
TUM.settings.id.TARGET_COUNT,
-1,
TUM.settings.id.ENEMY_AIR_DEFENSE,
TUM.settings.id.ENEMY_AIR_FORCE,
-1,
TUM.settings.id.AI_CAP,
}
local summary = ""
for _,v in pairs(settingsOrder) do
if v < 0 then
summary = summary.."\n"
else
summary = summary.."\n"..SETTING_NAMES[v]:upper()..": "
if type(settings[v]) == "boolean" then
if settings[v] then
summary = summary.."Enabled"
else
summary = summary.."Disabled"
end
else
summary = summary..SETTING_VALUES[v][settings[v]]
end
if showScoreMultiplier then
local settingMultiplier = TUM.playerScore.getScoreMultiplier(v, settings[v])
if settingMultiplier > 0.0 then
summary = summary.." (+"..tostring(math.ceil(settingMultiplier * 100)).."% xp)"
end
end
end
end
if showScoreMultiplier then
summary = summary.."\n\nTotal XP modifier: "..tostring(math.ceil(TUM.playerScore.getTotalScoreMultiplier() * 100)).."%"
end
return summary
end
function TUM.settings.printSettingsSummary(clearView)
trigger.action.outText("MISSION SETTINGS\n"..TUM.settings.getSettingsSummary(), 15, clearView or false)
trigger.action.outSound("UI-Ok.ogg")
end
function TUM.settings.setValue(id, value, silent)
silent = silent or false
settings[id] = value
if not silent then
TUM.settings.printSettingsSummary(true)
trigger.action.outSound("UI-Ok.ogg")
end
end
function TUM.settings.onStartUp(coreSettings)
-- Load mission zones
SETTING_VALUES[TUM.settings.id.TARGET_LOCATION] = {}
local missionZones = TUM.territories.getMissionZones()
for _,m in ipairs(missionZones) do
table.insert(SETTING_VALUES[TUM.settings.id.TARGET_LOCATION], m.name)
end
-- Load available coalitions
SETTING_VALUES[TUM.settings.id.COALITION_BLUE] = { }
SETTING_VALUES[TUM.settings.id.COALITION_RED] = { }
for k,faction in pairs(Library.factions.tables) do
if not faction.theaters or #faction.theaters == 0 or DCSEx.table.contains(faction.theaters, env.mission.theatre) then
table.insert(SETTING_VALUES[TUM.settings.id.COALITION_BLUE], k)
table.insert(SETTING_VALUES[TUM.settings.id.COALITION_RED], k)
end
end
setAllToDefaults(coreSettings)
TUM.settings.printSettingsSummary()
return true
end
end
@@ -0,0 +1,122 @@
-- ====================================================================================
-- TUM.SUPPORTAWACS - HANDLES THE FRIENDLY AWACS
-- ====================================================================================
-- ====================================================================================
TUM.supportAWACS = {}
do
local awacsGroupID = nil
local awacsCallsign = "AWACS"
local function doAwacsPicture(bogeyDope, delayAnswer)
delayAnswer = delayAnswer or false
if not awacsGroupID then return end
local awacsGroup = DCSEx.world.getGroupByID(awacsGroupID)
if not awacsGroup then return end
local awacsUnit = awacsGroup:getUnits()[1]
if not awacsGroup then return end
local awacsController = awacsUnit:getController()
if not awacsController then return end
-- local aircraftGroups = awacsController:getDetectedTargets(Controller.Detection.RADAR)
local detectedUnits = awacsController:getDetectedTargets()
local detectedAircraft = {}
for _,u in pairs(detectedUnits) do
if u.object and u.distance and Object.getCategory(u.object) == Object.Category.UNIT and u.object:inAir() then
if u.object:getCoalition() ~= TUM.settings.getPlayerCoalition() then
table.insert(detectedAircraft, u.object)
end
end
end
-- No aircraft on picture
if #detectedAircraft == 0 then
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "awacsPictureClear", { awacsCallsign }, "Overlord", delayAnswer)
return
end
for _,p in pairs(coalition.getPlayers(TUM.settings.getPlayerCoalition())) do
local pVec3 = p:getPoint()
local maxCount = 5
if bogeyDope then maxCount = 1 end -- TODO: Only report the nearest FIGHTER (not transport, awacs, etc)
local sortedThreats = DCSEx.dcs.getNearestObjects(pVec3, detectedAircraft, maxCount)
local pictureMsg = ""
for ___,u in pairs(sortedThreats) do
local typeName = Library.objectNames.get(u)
pictureMsg = pictureMsg.."\n- "..typeName..", "..DCSEx.dcs.getBRAA(u:getPoint(), pVec3, true)
end
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "awacsPicture", { awacsCallsign, pictureMsg }, "Overlord", delayAnswer)
end
end
local function doCommandPicture()
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "playerAwacsPicture", { awacsCallsign }, "Flight", false)
doAwacsPicture(false, true)
end
local function doCommandBogeyDope()
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "playerAwacsBogeyDope", { awacsCallsign }, "Flight", false)
doAwacsPicture(true, true)
end
----------------------------------------------------------
-- Called on every mission update tick (every 15 seconds)
-- @return True if something was done this tick, false otherwise
----------------------------------------------------------
function TUM.supportAWACS.onClockTick()
if not awacsGroupID then return false end -- No awacs aircraft
if TUM.mission.getStatus() == TUM.mission.status.NONE then return false end -- Not in a mission
doAwacsPicture(false, false)
return true
end
function TUM.supportAWACS.createMenu()
if not awacsGroupID then return end -- No AWACS
local rootPath = missionCommands.addSubMenu("✈ Awacs")
missionCommands.addCommand("Bogey dope", rootPath, doCommandBogeyDope, nil)
missionCommands.addCommand("Picture", rootPath, doCommandPicture, nil)
end
function TUM.supportAWACS.create()
if awacsGroupID then return end -- Already spawned
local awacsUnits = Library.factions.getUnits(TUM.settings.getPlayerFaction(), DCSEx.enums.unitFamily.PLANE_AWACS, 1)
awacsCallsign = "AWACS"
if awacsUnits and #awacsUnits > 0 then
local groupInfo = DCSEx.unitGroupMaker.create(
TUM.settings.getPlayerCoalition(),
Group.Category.AIRPLANE,
TUM.territories.getTerritoryCenter(TUM.settings.getPlayerCoalition()),
{ DCSEx.table.getRandom(awacsUnits) },
{
immortal = true,
invisible = true,
silenced = true,
taskAwacs = true,
unlimitedFuel = true
})
if groupInfo then
awacsGroupID = groupInfo.groupID
if groupInfo.callsign then
awacsCallsign = groupInfo.callsign.name:sub(1, #groupInfo.callsign.name - 1)
end
TUM.log("Spawned AWACS aircraft")
else
TUM.log("Failed to create AWACS aircraft", TUM.logLevel.WARNING)
end
else
TUM.log("No AWACS aircraft available")
end
end
end
@@ -0,0 +1,93 @@
-- ====================================================================================
-- TUM.SUPPORTJTAC - HANDLES FRIENDLY JTAC SMOKE MARKERS AND LASING
-- ====================================================================================
-- ====================================================================================
TUM.supportJTAC = {}
do
local JTAC_CALLSIGNS = {
"Anvil",
"Axeman",
"Badger",
"Darknight",
"Deathstar",
"Eyeball",
"Ferret",
"Finger",
"Firefly",
"Hammer",
"Jaguar",
"Mantis",
"Moonbeam",
"Pinpoint",
"Playboy",
"Pointer",
"Shaba",
"Warrior",
"Whiplash",
}
local SMOKE_DURATION = 300 -- in seconds
local SMOKE_MARKER_PENALTY = -25
local jtacName = {}
local lastSmoke = {}
local function spawnSmoke(args)
trigger.action.smoke(args.point3, args.smokeColor)
end
local function doCommandSmoke(index)
local obj = TUM.objectives.getObjective(index)
if not obj then return end
-- Pick a smoke color
local smokeColor = DCSEx.table.getRandom({ trigger.smokeColor.Red, trigger.smokeColor.Orange }) -- TODO: green or blue smoke when marking friendlies
local smokeColorName = "red"
if smokeColor == trigger.smokeColor.Orange then smokeColorName = "orange" end
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "playerJTACSmoke", { jtacName[index], obj.name }, "Flight", false)
if not lastSmoke[index] then lastSmoke[index] = -3600 end
if lastSmoke[index] + SMOKE_DURATION > timer.getAbsTime() then
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacSmokeAlreadyOut", { jtacName[index], obj.name }, jtacName[index], true)
return
end
if obj.completed then
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacSmokeNoTarget", { jtacName[index] }, jtacName[index], true)
return
end
if obj.isSceneryTarget then
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacSmokeOK", { jtacName[index], smokeColorName }, jtacName[index], true, spawnSmoke, { point3 = obj.point3, smokeColor = smokeColor })
else
for _,id in ipairs(obj.unitsID) do
if not DCSEx.table.contains(obj.completedUnitsID, id) then
local unit = DCSEx.world.getUnitByID(id)
if unit and unit:isActive() and unit:getLife() > 0 then
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacSmokeOK", { jtacName[index], smokeColorName }, jtacName[index], true, spawnSmoke, { point3 = unit:getPoint(), smokeColor = smokeColor })
lastSmoke[index] = timer.getAbsTime()
TUM.playerScore.award(SMOKE_MARKER_PENALTY, "called for smoke marker")
return
end
end
end
end
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacSmokeNoTarget", { jtacName[index] }, jtacName[index], true)
end
function TUM.supportJTAC.setupJTACOnObjective(index, menuRoot)
local obj = TUM.objectives.getObjective(index)
if not obj then return end
jtacName[index] = DCSEx.table.getRandom(JTAC_CALLSIGNS)
lastSmoke[index] = -3600
local objectiveDB = Library.tasks[obj.taskID]
if not DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.ALLOW_JTAC) then return end -- No JTAC for this objective
missionCommands.addCommand("Require smoke marker on target ("..tostring(SMOKE_MARKER_PENALTY).."xp)", menuRoot, doCommandSmoke, index)
end
end
@@ -0,0 +1,176 @@
-- ====================================================================================
-- TUM.TERRITORIES - HANDLES THE MISSION SPECIAL ZONES (COALITION TERRITORIES, WATER AND BATTLE ZONES)
-- ====================================================================================
-- (local) assignTerritoryToZone(coalitionID, zone)
-- TUM.territories.getCenter(side)
-- TUM.territories.getPointOwner(point)
-- TUM.territories.getRandomPoint(side, surfaceType)
-- TUM.territories.getSurfaceArea(side)
-- TUM.territories.onInitialize()
-- ====================================================================================
TUM.territories = {}
do
local coalitionZones = { {}, {} }
local missionZones = {}
local waterZones = {}
local function addZoneToCoalition(zone, side)
table.insert(coalitionZones[side], zone)
local airbases = world.getAirbases()
for _,ab in pairs(airbases) do
local airbasePoint2 = DCSEx.math.vec3ToVec2(ab:getPoint())
if DCSEx.zones.isPointInside(zone, airbasePoint2) then
if ab:getDesc().category ~= Airbase.Category.SHIP then -- Ignore ships
ab:setCoalition(side)
end
end
end
end
function TUM.territories.getMissionZones()
return DCSEx.table.deepCopy(missionZones)
end
function TUM.territories.getWaterZones()
return DCSEx.table.deepCopy(waterZones)
end
-------------------------------------
-- Returns the coalition to which belong a given point on the map.
-- Return coalition.side.NEUTRAL if the point isn't in any coalition territory.
-- @param point A vec2 or vec3
-- @return A value from the coalition.side enum
-------------------------------------
function TUM.territories.getPointOwner(point)
for side=1,2 do
for _,z in ipairs(coalitionZones[side]) do
if DCSEx.zones.isPointInside(z, point) then
return side
end
end
end
return coalition.side.NEUTRAL
end
function TUM.territories.getTerritoryZones(side)
return DCSEx.table.deepCopy(coalitionZones[side])
end
function TUM.territories.getTerritoryCenter(side)
local center = { x = 0, y = 0 }
if #coalitionZones[side] == 0 then return center end
for _,z in ipairs(coalitionZones[side]) do
center.x = center.x + z.x
center.y = center.y + z.y
end
-- TODO: bigger zones should skew the center in their favor
center.x = center.x / #coalitionZones[side]
center.y = center.y / #coalitionZones[side]
return center
end
-- function TUM.territories.getCenter(side)
-- return DCSEx.zones.getCenter(zones[side])
-- end
-- function TUM.territories.getSurfaceArea(side)
-- return DCSEx.zones.getSurfaceArea(zones[side])
-- end
-- function TUM.territories.getZone(side)
-- return DCSEx.table.deepCopy(zones[side])
-- end
function TUM.territories.getRandomPointInTerritory(side, surfaceType)
if #coalitionZones[side] == 0 then return nil end
local zone = DCSEx.table.getRandom(coalitionZones[side]) -- TODO: bigger zones should be selected more often
return DCSEx.zones.getRandomPointInside(zone, surfaceType)
end
-------------------------------------
-- Called on mission start up
-- @return True if started up properly, false if an error happened
-------------------------------------
function TUM.territories.onStartUp()
coalitionZones = { {}, {} }
missionZones = {}
waterZones = {}
-- Disable autocapture for all bases and give them to the neutral coalition
for _,ab in pairs(world.getAirbases()) do
ab:autoCapture(false)
if ab:getDesc().category ~= Airbase.Category.SHIP then -- Ignore ships
ab:setCoalition(coalition.side.NEUTRAL)
end
end
local zones = DCSEx.zones.getAll()
for _,z in ipairs(zones) do
if DCSEx.string.startsWith(z.name:lower(), "blufor") then
addZoneToCoalition(z, coalition.side.BLUE)
elseif DCSEx.string.startsWith(z.name:lower(), "redfor") then
addZoneToCoalition(z, coalition.side.RED)
elseif DCSEx.string.startsWith(z.name:lower(), "water") then
table.insert(waterZones, z)
else
table.insert(missionZones, z)
end
end
for side=1,2 do
if #coalitionZones[side] == 0 or #coalition.getAirbases(side) == 0 then
local name = DCSEx.dcs.getCoalitionAsString(side)
local zoneName = "BLUFOR"
if side == 1 then zoneName = "REDFOR" end
TUM.log("Coalition "..name.." has no territory zones and/or controls no airfields. Please add zone with a name starting with "..zoneName.." in the mission editor and make sure at least one contains an airbase.", TUM.logLevel.ERROR)
return false
end
end
if #missionZones == 0 then
TUM.log("No mission zones found. Create at least one mission zone in the mission editor.", TUM.logLevel.ERROR)
return false
end
if #missionZones > 10 then
TUM.log("Too many mission zones, extra zones removed.", TUM.logLevel.WARNING)
while #missionZones > 10 do
table.remove(missionZones, 11)
end
end
-- zones = {}
-- zones[coalition.side.BLUE] = DCSEx.zones.getByName("BLUFOR")
-- zones[coalition.side.RED] = DCSEx.zones.getByName("REDFOR")
-- if not zones[coalition.side.BLUE] then
-- TUM.log("BLUFOR zone not found.", TUM.logLevel.ERROR)
-- return false
-- elseif not zones[coalition.side.RED] then
-- TUM.log("REDFOR zone not found.", TUM.logLevel.ERROR)
-- return false
-- end
-- -- TODO: square kilometers if "metric system" enabled?
-- for side=1,2 do
-- TUM.log(
-- "Coalition "..DCSEx.dcs.getCoalitionAsString(side):upper().."'s territory is "..
-- DCSEx.string.toStringThousandsSeparator(math.floor(TUM.territories.getSurfaceArea(side) / 2589988.110336)).." squared miles.")
-- end
return true
end
end