407 lines
18 KiB
Lua

-- ====================================================================================
-- 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 ENEMY_DEFENSE_MULTIPLIER_BONUS = { -0.1, 0.1, 0.3, 0.5, 0.7 }
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
-- Aircraft killed on the ground are worth less points, except AWACS, bombers and transports
if not objectDesc.attributes["AWACS"] and not objectDesc.attributes["Transports"] and not objectDesc.attributes["Strategic bombers"] then
groundMultiplier = 0.5
end
end
-- 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(25 * groundMultiplier) end
if objectDesc.attributes["Helicopters"] then return math.floor(15 * 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
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.." All progress will be recorded to your flight log upon landing.", 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, or nil to use the current value
-- @return A number in the -1.0 to +1.0 range, or nil if this setting has no effect on XP gains
-------------------------------------
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
settingValue = TUM.settings.getValue(settingID)
if settingID == TUM.settings.id.ENEMY_AIR_DEFENSE or settingID == TUM.settings.id.ENEMY_AIR_FORCE then
return ENEMY_DEFENSE_MULTIPLIER_BONUS[settingValue]
elseif settingID == TUM.settings.id.WINGMEN then
return 0 - 0.10 * (settingValue - 1)
elseif settingID == TUM.settings.id.TARGET_LOCATION then
local zoneName = TUM.settings.getValue(TUM.settings.id.TARGET_LOCATION, true)
local tgtZone = DCSEx.zones.getByName(zoneName)
if not tgtZone then return 0 end
if TUM.territories.getPointOwner(tgtZone) == TUM.settings.getEnemyCoalition() then return 0.3 end
return 0
elseif settingID == TUM.settings.id.AI_CAP then
if settingValue == 2 and TUM.settings.getValue(TUM.settings.id.ENEMY_AIR_FORCE) > 1 then return 0.15 end
return 0
end
return nil
end
-------------------------------------
-- Returns the global score multiplier according the current settings
-- @return A number (where 1.0 means default value, 0.5 is -50%, etc)
-------------------------------------
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
-- Base XP multiplier is 1.0 (100%)
local scoreMultiplier = 1.0
-- Add XP multipliers for game settings
for _,v in pairs(TUM.settings.id) do
scoreMultiplier = scoreMultiplier + (TUM.playerScore.getScoreMultiplier(v) or 0.0)
end
-- Add XP multipliers for weather
scoreMultiplier = scoreMultiplier + TUM.weather.getWeatherXPModifier()
scoreMultiplier = scoreMultiplier + TUM.weather.getWindXPModifier()
return math.max(0.0, 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 and event.place 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