2025-08-05 17:04:24 +02:00

312 lines
13 KiB
Lua

-- ====================================================================================
-- 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", 150 },
{ "Bronze star", 225 },
{ "Airman's medal", 300 },
{ "Distinguished Flying Cross", 375 },
{ "Silver Star for Valor", 450 },
{ "Air Force Cross", 525 },
{ "Congressional Medal of Honor", 600 },
}
local RANKS = {
{ "2d Lt.", "Second lieutenant", 0 },
{ "1st Lt.", "First lieutenant", 400 },
{ "Capt.", "Captain", 1600 },
{ "Maj.", "Major", 6400 },
{ "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
trigger.action.outText(DCSEx.string.toStringThousandsSeparator(score).." xp and "..tostring(objectives).." completed objective(s) were registered in your flight log.", 5)
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
TUM.playerScore.reset(false)
return somethingWasAwarded
end
-------------------------------------
-- Appends the career menu to the F10 menu. Only works in single-player missions
-------------------------------------
function TUM.playerCareer.createMenu()
local rootMenu = TUM.getOrCreateRootMenu()
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", rootMenu, 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.logger.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