diff --git a/Moose Development/Moose/Functional/SimpleScore.lua b/Moose Development/Moose/Functional/SimpleScore.lua new file mode 100644 index 000000000..be20612e9 --- /dev/null +++ b/Moose Development/Moose/Functional/SimpleScore.lua @@ -0,0 +1,649 @@ +--- **Functional** - (R2.5) - Simple and Persistent Player Scoring. +-- +-- +-- Score! +-- +-- +-- ## Main Features: +-- +-- * Good stuff. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.SimpleScore +-- @image Functional_SimpleScore.png + + +--- SCORE class. +-- @type SCORE +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table menuadded Table of groups the menu was added for. +-- @field #boolean menudisabled If true, F10 menu for players is disabled. +-- @field #table playerscores Player score table. +-- @extends Core.Fsm#FSM + +--- Goooooaaaallllll! +-- +-- === +-- +-- ![Banner Image](..\Presentations\SCORE\SCORE_Main.png) +-- +-- # The Scoring Concept +-- +-- Score +-- +-- # Basic Script +-- +-- -- Create a new score object. +-- score=SCORE:New() +-- +-- -- Start score. +-- SCORE:Start() +-- +-- # Training Zones +-- +-- Players are only protected if they are inside one of the training zones. +-- +-- -- Create a new missile trainer object. +-- SCORE=SCORE:New() +-- +-- -- Add training zones. +-- SCORE:AddSafeZone(ZONE:New("Training Zone Alpha") +-- SCORE:AddSafeZone(ZONE:New("Training Zone Bravo") +-- +-- -- Start missile trainer. +-- SCORE:Start() +-- +-- +-- Todo! +-- +-- +-- @field #SCORE +SCORE = { + ClassName = "SCORE", + Debug = false, + lid = nil, + menuadded = {}, + menudisabled = nil, + playerscores = nil, +} + + +--- Player data table holding all important parameters of each player. +-- @type SCORE.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field Wrapper.Group#GROUP group Aircraft group of player. +-- @field #string groupname Name of the the player aircraft group. +-- @field #string name Player name. +-- @field #number coalition Coalition number of player. +-- @field #table hit Table of hit units. + +--- Hit unit data. +-- @type SCORE.HitData +-- @field #string unitname Name of the hit unit. +-- @field #string category Category of hit unit. +-- @field #number score Score +-- @field #boolean friendly If true, unit is friendly. +-- @field #boolean destroyed If true, unit was destroyed. + + + +--- Create a new SCORE class object. +-- @param #SCORE self +-- @return #SCORE self. +function SCORE:New() + + self.lid="SCORE | " + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #SCORE + + -- Defaults: + self:SetDefaultMissileDestruction(true) + self:SetDefaultLaunchAlerts(true) + self:SetDefaultLaunchMarks(true) + self:SetExplosionPower() + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start SCORE script. + self:AddTransition("*", "Status", "*") -- Status update. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the SCORE. Initializes parameters and starts event handlers. + -- @function [parent=#SCORE] Start + -- @param #SCORE self + + --- Triggers the FSM event "Start" after a delay. Starts the SCORE. Initializes parameters and starts event handlers. + -- @function [parent=#SCORE] __Start + -- @param #SCORE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the SCORE and all its event handlers. + -- @param #SCORE self + + --- Triggers the FSM event "Stop" after a delay. Stops the SCORE and all its event handlers. + -- @function [parent=#SCORE] __Stop + -- @param #SCORE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#SCORE] Status + -- @param #SCORE self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#SCORE] __Status + -- @param #SCORE self + -- @param #number delay Delay in seconds. + + return self +end + + +--- On after Start event. Starts the missile trainer and adds event handlers. +-- @param #SCORE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SCORE:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting SCORE v%s", SCORE.version) + env.info(text) + + -- Init player scores table. + self.playerscores={} + + -- Event handlers + self:HandleEvent(EVENTS.Dead, self._EventDeadOrCrash) + self:HandleEvent(EVENTS.Crash, self._EventDeadOrCrash) + self:HandleEvent(EVENTS.Hit, self._EventHit) + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.PlayerLeaveUnit) + + -- Handle events: + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Shot) + + if self.Debug then + self:TraceClass(self.ClassName) + self:TraceLevel(2) + end + + self:__Status(-10) +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- SCORE event handler for event birth. +-- @param #SCORE self +-- @param Core.Event#EVENTDATA EventData +function SCORE:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + + -- Player unit and name. + local _unitName=EventData.IniUnitName + local playerunit, playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug info. + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(playername)) + + -- Check if player entered. + if playerunit and playername then + + local _uid=playerunit:GetID() + local _group=playerunit:GetGroup() + local _callsign=playerunit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Add F10 radio menu for player. + if not self.menudisabled then + SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + end + + -- Player data. + local playerData={} --#SCORE.PlayerData + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.unitname = _unitName + playerData.group = _group + playerData.groupname = _group:GetName() + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(_unitName, nil, true) + playerData.coalition = _group:GetCoalition() + + playerData.hit = playerData.hit or {} + + -- Init player data. + self.players[playername]=playerData + + end +end + + +--- Handles the OnHit event for the scoring. +-- @param #SCORE self +-- @param Core.Event#EVENTDATA Event +function SCORE:_EventOnHit(Event) + self:F( { Event } ) + + local InitUnit = nil + local InitUNIT = nil + local InitUnitName = "" + local InitGroup = nil + local InitGroupName = "" + local InitPlayerName = nil + + local InitCoalition = nil + local InitCategory = nil + local InitType = nil + local InitUnitCoalition = nil + local InitUnitCategory = nil + local InitUnitType = nil + + local TargetUnit = nil + local TargetUNIT = nil + local TargetUnitName = "" + local TargetGroup = nil + local TargetGroupName = "" + local TargetPlayerName = nil + + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + InitUnit = Event.IniDCSUnit + InitUNIT = Event.IniUnit + InitUnitName = Event.IniDCSUnitName + InitGroup = Event.IniDCSGroup + InitGroupName = Event.IniDCSGroupName + InitPlayerName = Event.IniPlayerName + + InitCoalition = Event.IniCoalition + InitCategory = Event.IniCategory + InitType = Event.IniTypeName + + InitUnitCoalition = _SCORINGCoalition[InitCoalition] + InitUnitCategory = _SCORINGCategory[InitCategory] + InitUnitType = InitType + + self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType , InitUnitCoalition, InitUnitCategory, InitUnitType } ) + end + + + if Event.TgtDCSUnit then + + TargetUnit = Event.TgtDCSUnit + TargetUNIT = Event.TgtUnit + TargetUnitName = Event.TgtDCSUnitName + TargetGroup = Event.TgtDCSGroup + TargetGroupName = Event.TgtDCSGroupName + TargetPlayerName = Event.TgtPlayerName + + TargetCoalition = Event.TgtCoalition + TargetCategory = Event.TgtCategory + TargetType = Event.TgtTypeName + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + end + + -- Current time. + local Tnow=timer.getTime() + + if InitPlayerName~=nil then -- It is a player that is hitting something + + self:T("Hitting Something") + + -- What is he hitting? + if TargetCategory then + + -- A target got hit, score it. + -- Player contains the score data from self.Players[InitPlayerName] + local player=self.Players[InitPlayerName] --#SCORE.PlayerData + + -- Hit table. + player.hit[TargetUnitName]=player.hit[TargetUnitName] or {} + + local hit=player.hit[TargetUnitName] --#SCORE.HitData + + hit.score = hit.score or 0 + hit.penalty = hit.penalty or 0 + hit.timeStamp = hit.timeStamp or 0 + hit.unit = hit.unit or TargetUNIT + hit.threatLevel, hit.threatType = hit.unit:GetThreatLevel() + + -- Only grant hit scores if there was more than one second between the last hit. + if Tnow-hit.timeStamp>1 then + + -- Update timestamp + hit.timeStamp = Tnow + + if TargetPlayerName~=nil then + -- It is a player hitting another player ... + hit.otherplayer=true + end + + local Score = 0 + + if InitCoalition then -- A coalition object was hit. + if InitCoalition == TargetCoalition then + + ------------------ + -- Hit Friendly -- + ------------------ + + hit.friendly=true + + player.penalty = player.penalty-10 + hit.penalty=hit.penalty-10 + + -- Check if target was another player. + if TargetPlayerName~=nil then -- It is a player hitting another player ... + + else + + end + + -- Write penalty to CSV file. + --self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + + else + + ---------------- + -- Hit Enemey -- + ---------------- + + hit.friendly=true + + player.score=player.score+1 + hit.score=hit.score+1 + + --hit.scoreHit = hit.ScoreHit + 1 + + -- Check if it was a player. + if TargetPlayerName~=nil then -- It is a player hitting another player ... + + else + + end + + -- Write score to CSV file. Score 1 PT. + + end + + else -- A scenery object was hit. + + + end + + end + end + end + +end + +--- Track DEAD or CRASH events for the scoring. +-- @param #SCORING self +-- @param Core.Event#EVENTDATA Event +function SCORING:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + local TargetUnit = nil + local TargetGroup = nil + local TargetUnitName = "" + local TargetGroupName = "" + local TargetPlayerName = "" + local TargetCoalition = nil + local TargetCategory = nil + local TargetType = nil + local TargetUnitCoalition = nil + local TargetUnitCategory = nil + local TargetUnitType = nil + + if Event.IniDCSUnit then + + TargetUnit = Event.IniUnit + TargetUnitName = Event.IniDCSUnitName + TargetGroup = Event.IniDCSGroup + TargetGroupName = Event.IniDCSGroupName + TargetPlayerName = Event.IniPlayerName + + TargetCoalition = Event.IniCoalition + TargetCategory = Event.IniCategory + TargetType = Event.IniTypeName + + TargetUnitCoalition = _SCORINGCoalition[TargetCoalition] + TargetUnitCategory = _SCORINGCategory[TargetCategory] + TargetUnitType = TargetType + + self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) + end + + -- Player contains the score and reference data for the player. + for PlayerName, _player in pairs(self.Players) do + local player=_player --#SCORE.PlayerData + + if player and player.unit and player.unit:IsAlive() then + + self:T( "Something got destroyed" ) + + -- Some variables + local InitUnitName = Player.UnitName + local InitUnitType = Player.UnitType + local InitCoalition = Player.UnitCoalition + local InitCategory = Player.UnitCategory + local InitUnitCoalition = _SCORINGCoalition[InitCoalition] + local InitUnitCategory = _SCORINGCategory[InitCategory] + + self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } ) + + local Destroyed = false + + local hit=player.hit[TargetUnitName] --#SCORE.HitData + + -- What is the player destroying? + if hit and hit.timeStamp~=0 then -- Was there a hit for this unit for this player before registered? + + local TargetThreatLevel = hit.threatLevel + local TargetThreatType = hit.threatType + + Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {} + Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {} + + local destroy={} --#SCORE.DestroyData + + -- PlayerDestroy contains the destroy score data per category and target type of the player. + local TargetDestroy = Player.Destroy[TargetCategory][TargetType] + TargetDestroy.Score = TargetDestroy.Score or 0 + TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0 + TargetDestroy.Penalty = TargetDestroy.Penalty or 0 + TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0 + + if TargetCoalition then + if InitCoalition == TargetCoalition then + + --- + -- Hit Friendly + --- + + local ThreatLevelTarget = TargetThreatLevel + local ThreatTypeTarget = TargetThreatType + local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 + local ThreatPenalty = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyPenalty / 10 ) + self:F( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + + Player.Penalty = Player.Penalty + ThreatPenalty + TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty + TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1 + + if Player.HitPlayers[TargetPlayerName] then + -- A player destroyed another friendly player. + else + -- A player destroyed another friendly target. + end + + -- Write CSV penalty. + Destroyed = true + --self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + + else + + --- + -- Hit Enemy + --- + + local ThreatLevelTarget = TargetThreatLevel + local ThreatTypeTarget = TargetThreatType + local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1 + local ThreatScore = math.ceil( ( ThreatLevelTarget / ThreatLevelPlayer ) * self.ScaleDestroyScore / 10 ) + + self:F( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } ) + + Player.Score = Player.Score + ThreatScore + TargetDestroy.Score = TargetDestroy.Score + ThreatScore + TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1 + + if Player.HitPlayers[TargetPlayerName] then + -- A player destroyed another enemy player. + else + -- A player destroyed another enemy target. + end + + -- Write CSV. + Destroyed = true + --self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + + local UnitName = TargetUnit:GetName() + + -- Scoring for special objects. + local Score = self.ScoringObjects[UnitName] + + if Score then + + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information + ) + :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() ) + + -- Write CSV file. + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + Destroyed = true + end + + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs(self.ScoringZones) do + self:F( { ScoringZone = ScoreZoneData } ) + + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + + local Score = ScoreZoneData.Score + + -- Check if target is in zone. + if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + + MESSAGE + :NewType( self.DisplayMessagePrefix .. "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." .. + "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. + "Total: " .. Player.Score - Player.Penalty, + MESSAGE.Type.Information ) + :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() ) + :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() ) + + -- Write CSV. + self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType ) + Destroyed = true + end + end + + end + + else + + --- + -- Scenery Destroyed + --- + + -- Check if there are Zones where the destruction happened. + for ZoneName, ScoreZoneData in pairs(self.ScoringZones) do + + local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE + local Score = ScoreZoneData.Score + + if ScoreZone:IsVec2InZone(TargetUnit:GetVec2()) then + + Player.Score = Player.Score + Score + TargetDestroy.Score = TargetDestroy.Score + Score + + -- Player destroyed scenery in scoring zone. + + --Write CSV. + Destroyed = true + --self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType ) + end + end + + end + + -- Delete now the hit cache if the target was destroyed. + -- Otherwise points will be granted every time a target gets killed by the players that hit that target. + -- This is only relevant for player to player destroys. + if Destroyed then + Player.Hit[TargetCategory][TargetUnitName].TimeStamp = 0 + end + + end + end + end +end + +