mirror of
https://github.com/akaAgar/the-universal-mission-for-dcs-world.git
synced 2026-08-27 15:07:50 +00:00
Initial commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user