Patches/Build 317/Diff

From Clogopedia, the Natural Selection 2 wiki
Jump to: navigation, search

<source lang="diff"> diff --git a/.gitignore b/.gitignore index 67a720fa9..00286fa1f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ directories.txt

*.tga
*.deuser
*.deproj

-*.tile_cache_grid

*.animation_graph
*.dll
*.exe

diff --git a/bootcamp/alien_1/lua/TutNS2Gamerules.lua b/bootcamp/alien_1/lua/TutNS2Gamerules.lua index d6783ba81..78690b6a6 100644 --- a/bootcamp/alien_1/lua/TutNS2Gamerules.lua +++ b/bootcamp/alien_1/lua/TutNS2Gamerules.lua @@ -102,8 +102,6 @@ if Server then

                self:UpdatePlayerSkill()
                self:UpdateNumPlayersForScoreboard()

- self:UpdatePerfTags(timePassed) - self:UpdateCustomNetworkSettings()

            end

diff --git a/bootcamp/command_1/lua/TutNS2Gamerules.lua b/bootcamp/command_1/lua/TutNS2Gamerules.lua index 0627d2e1e..21bcbf475 100644 --- a/bootcamp/command_1/lua/TutNS2Gamerules.lua +++ b/bootcamp/command_1/lua/TutNS2Gamerules.lua @@ -58,8 +58,6 @@ if Server then

                self:UpdatePlayerSkill()
                self:UpdateNumPlayersForScoreboard()

- self:UpdatePerfTags(timePassed) - self:UpdateCustomNetworkSettings()

            end

diff --git a/bootcamp/command_2/lua/TutConsoleCommands_Server.lua b/bootcamp/command_2/lua/TutConsoleCommands_Server.lua index e972d5491..a1bf9521e 100644 --- a/bootcamp/command_2/lua/TutConsoleCommands_Server.lua +++ b/bootcamp/command_2/lua/TutConsoleCommands_Server.lua @@ -392,7 +392,7 @@ local function OnBotTraining(client)

    GetGamerules():SetBotTraining(true)

- Server.AddTag("ignore_playnow") + Server.DisableQuickPlay()

end

-- Generic console commands

diff --git a/bootcamp/command_2/lua/TutNS2Gamerules.lua b/bootcamp/command_2/lua/TutNS2Gamerules.lua index c6c9a0995..d4b4e004e 100644 --- a/bootcamp/command_2/lua/TutNS2Gamerules.lua +++ b/bootcamp/command_2/lua/TutNS2Gamerules.lua @@ -58,8 +58,6 @@ if Server then

                self:UpdatePlayerSkill()
                self:UpdateNumPlayersForScoreboard()

- self:UpdatePerfTags(timePassed) - self:UpdateCustomNetworkSettings()

            end

diff --git a/bootcamp/marine_1/lua/TutNS2Gamerules.lua b/bootcamp/marine_1/lua/TutNS2Gamerules.lua index 185ee58ae..ae1d0794e 100644 --- a/bootcamp/marine_1/lua/TutNS2Gamerules.lua +++ b/bootcamp/marine_1/lua/TutNS2Gamerules.lua @@ -98,8 +98,6 @@ if Server then

                self:UpdatePlayerSkill()
                self:UpdateNumPlayersForScoreboard()

- self:UpdatePerfTags(timePassed) - self:UpdateCustomNetworkSettings()

            end

diff --git a/challenges/hive_challenge/lua/FCGamerules.lua b/challenges/hive_challenge/lua/FCGamerules.lua index 4147198bb..5cea8304b 100644 --- a/challenges/hive_challenge/lua/FCGamerules.lua +++ b/challenges/hive_challenge/lua/FCGamerules.lua @@ -238,8 +238,6 @@ function NS2Gamerules:OnUpdate(timePassed)

                self:UpdatePlayerSkill()
                self:UpdateNumPlayersForScoreboard()

- self:UpdatePerfTags(timePassed) - self:UpdateCustomNetworkSettings()

            end

diff --git a/challenges/skulk_challenge/fastload/maps/ns2_skulk_challenge_1.level-client.tile_cache_grid b/challenges/skulk_challenge/fastload/maps/ns2_skulk_challenge_1.level-client.tile_cache_grid new file mode 100644 index 000000000..c5946316e Binary files /dev/null and b/challenges/skulk_challenge/fastload/maps/ns2_skulk_challenge_1.level-client.tile_cache_grid differ diff --git a/challenges/skulk_challenge/fastload/maps/ns2_skulk_challenge_1.level-server.tile_cache_grid b/challenges/skulk_challenge/fastload/maps/ns2_skulk_challenge_1.level-server.tile_cache_grid new file mode 100644 index 000000000..c5946316e Binary files /dev/null and b/challenges/skulk_challenge/fastload/maps/ns2_skulk_challenge_1.level-server.tile_cache_grid differ diff --git a/challenges/skulk_challenge/lua/SCAlien_Client.lua b/challenges/skulk_challenge/lua/SCAlien_Client.lua new file mode 100644 index 000000000..57cd18571 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCAlien_Client.lua @@ -0,0 +1,12 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCAlien_Client.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Prevent player from opening evolve menu. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +function Alien:Buy() +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCBiteLeap.lua b/challenges/skulk_challenge/lua/SCBiteLeap.lua new file mode 100644 index 000000000..a92cbd197 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCBiteLeap.lua @@ -0,0 +1,51 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCBiteLeap.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- - Don't hit skulk holograms. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local kRange = 1.42 +local kEnzymedRange = 1.55 + +-- Replace filter function for the bite capsule trace to ignore skulk holograms +function BiteLeap:OnTag(tagName) + + PROFILE("BiteLeap:OnTag") + + if tagName == "hit" then + + local player = self:GetParent() + + if player then + + local range = (player.GetIsEnzymed and player:GetIsEnzymed()) and kEnzymedRange or kRange + + local didHit, target, endPoint = AttackMeleeCapsule(self, player, kBiteDamage, range, nil, false, + function(test) + return (test == player or test:isa("SkulkHologram")) + end) + + if Client and didHit then + self:TriggerFirstPersonHitEffects(player, target) + end + + if target and HasMixin(target, "Live") and not target:GetIsAlive() then + self:TriggerEffects("bite_kill") + elseif Server and target and target.TriggerEffects and GetReceivesStructuralDamage(target) and (not HasMixin(target, "Live") or target:GetCanTakeDamage()) then + target:TriggerEffects("bite_structure", {effecthostcoords = Coords.GetTranslation(endPoint), isalien = GetIsAlienUnit(target)}) + end + + player:DeductAbilityEnergy(self:GetEnergyCost()) + self:TriggerEffects("bite_attack") + + self:DoAbilityFocusCooldown(player, kAttackDuration) + + end + + end + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCClient.lua b/challenges/skulk_challenge/lua/SCClient.lua new file mode 100644 index 000000000..1f33e0fef --- /dev/null +++ b/challenges/skulk_challenge/lua/SCClient.lua @@ -0,0 +1,52 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCClient.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Client-only stuff for Skulk Racing mod. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/SCGUIRaceTimer.lua") +Script.Load("lua/SCGuideSpline.lua") +Script.Load("lua/SCGUITips.lua") +Script.Load("lua/SCTips.lua") + +gSCTargetPts = {} + +gDisableHelpScreen = true -- help screen not very helpful in this context. + +function SC_OnMapLoadEntity(className, groupName, values) + + if className == "cinematic" and values.startsOnMessage ~= nil and values.startsOnMessage ~= "" then + + gSCCinematics = gSCCinematics or {} + gSCCinematics[values.startsOnMessage] = gSCCinematics[values.startsOnMessage] or {} + + local newCin = {} + newCin.fileName = values.cinematicName + newCin.coords = values.angles:GetCoords(values.origin) + + if values.repeatStyle == 1 then + newCin.repeatStyle = Cinematic.Repeat_Loop + else + newCin.repeatStyle = Cinematic.Repeat_Endless + end + + table.insert(gSCCinematics[values.startsOnMessage], newCin) + + elseif className == "target_point" then + + gSCTargetPts[values.targetName] = values.origin + + elseif className == "cheering_marine" then + + local coords = values.angles:GetCoords(values.origin) + SC_CreateCheeringMarine(coords) + + end + +end + +Event.Hook("MapLoadEntity", SC_OnMapLoadEntity) diff --git a/challenges/skulk_challenge/lua/SCDeathMessage_Client.lua b/challenges/skulk_challenge/lua/SCDeathMessage_Client.lua new file mode 100644 index 000000000..64a6ca611 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCDeathMessage_Client.lua @@ -0,0 +1,76 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCDeathMessage_Client.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Disable death messages for skulk challenge mode. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local kSubImageWidth = 128 +local kSubImageHeight = 64 + +local queuedDeathMessages = { } + +local resLostMarine = 0 +local resLostAlien = 0 +local rtsLostMarine = 0 +local rtsLostAlien = 0 +local resRecovered = 0 + +function DeathMsgUI_GetMessages() + return {} +end + +function DeathMsgUI_MenuImage() + return "death_messages" +end + +function DeathMsgUI_GetTechOffsetX(doerId) + return 0 +end + +function DeathMsgUI_GetTechOffsetY(iconIndex) + + if not iconIndex then + iconIndex = 1 + end + + return (iconIndex - 1)*kSubImageHeight + +end + +function DeathMsgUI_GetTechWidth(doerId) + return kSubImageWidth +end + +function DeathMsgUI_GetTechHeight(doerId) + return kSubImageHeight +end + +function GetKillerNameAndWeaponIcon() + return nil +end + +function DeathMsgUI_GetRtsLost(teamNumber) + + return 0 + +end + +function DeathMsgUI_ResetStats() +end + +function DeathMsgUI_AddResLost(teamNumber, res) +end + +function DeathMsgUI_AddRtsLost(teamNumber, rts) +end + +function DeathMsgUI_AddResRecovered(amount) +end + +local function OnDeathMessage(message) +end +Client.HookNetworkMessage("DeathMessage", OnDeathMessage) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCFileHooks.lua b/challenges/skulk_challenge/lua/SCFileHooks.lua new file mode 100644 index 000000000..0cc19b46d --- /dev/null +++ b/challenges/skulk_challenge/lua/SCFileHooks.lua @@ -0,0 +1,34 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCFileHooks.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- File hooks for Skulk Racing mod. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +ModLoader.SetupFileHook("lua/Server.lua", "lua/SCServer_overrides.lua", "post") +ModLoader.SetupFileHook("lua/Gamerules.lua", "lua/SCGamerules.lua", "post") +ModLoader.SetupFileHook("lua/ReadyRoomPlayer.lua", "lua/SCReadyRoomPlayer.lua", "post") +ModLoader.SetupFileHook("lua/Skulk.lua", "lua/SCSkulk.lua", "post") +ModLoader.SetupFileHook("lua/Globals.lua", "lua/SCGlobals_Override.lua", "post") +ModLoader.SetupFileHook("lua/NS2Utility.lua", "lua/SCNS2Utility.lua", "post") +ModLoader.SetupFileHook("lua/menu/GUIMainMenu.lua", "lua/SCGUIMainMenu.lua", "post") +ModLoader.SetupFileHook("lua/Voting.lua", "lua/SCVoting.lua", "post") +ModLoader.SetupFileHook("lua/GUIAlienTeamMessage.lua", "lua/SCGUIAlienTeamMessage.lua", "post") +ModLoader.SetupFileHook("lua/Player.lua", "lua/SCPlayer.lua", "post") +ModLoader.SetupFileHook("lua/GUIFirstPersonSpectate.lua", "lua/SCGUIFirstPersonSpectate.lua", "replace") +ModLoader.SetupFileHook("lua/GUISpectator.lua", "lua/SCGUISpectator.lua", "replace") +ModLoader.SetupFileHook("lua/GUIScoreboard.lua", "lua/SCGUIScoreboard.lua", "post") +ModLoader.SetupFileHook("lua/Player_Server.lua", "lua/SCPlayer_Server.lua", "post") +ModLoader.SetupFileHook("lua/NS2ConsoleCommands_Server.lua", "lua/SCNS2ConsoleCommands_Server.lua", "replace") +ModLoader.SetupFileHook("lua/Alien_Client.lua", "lua/SCAlien_Client.lua", "post") +ModLoader.SetupFileHook("lua/DeathMessage_Client.lua", "lua/SCDeathMessage_Client.lua", "replace") +ModLoader.SetupFileHook("lua/GUIDeathScreen.lua", "lua/SCGUIDeathScreen.lua", "replace") +ModLoader.SetupFileHook("lua/GUIAlienHUD.lua", "lua/SCGUIAlienHUD.lua", "post") +ModLoader.SetupFileHook("lua/GUIMinimapFrame.lua", "lua/SCGUIMinimapFrame.lua", "post") +ModLoader.SetupFileHook("lua/Player_Client.lua", "lua/SCPlayer_Client.lua", "post") +ModLoader.SetupFileHook("lua/GUITechMap.lua", "lua/SCGUITechMap.lua", "post") +ModLoader.SetupFileHook("lua/GUIUnitStatus.lua", "lua/SCGUIUnitStatus.lua", "post") +ModLoader.SetupFileHook("lua/Weapons/Alien/BiteLeap.lua", "lua/SCBiteLeap.lua", "post") \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCFinishLine.lua b/challenges/skulk_challenge/lua/SCFinishLine.lua new file mode 100644 index 000000000..bb4d11ccc --- /dev/null +++ b/challenges/skulk_challenge/lua/SCFinishLine.lua @@ -0,0 +1,47 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCFinishLine.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Area that, when entered, ends the race. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +class 'FinishLine' (Trigger) + +FinishLine.kMapName = "finish_line" + +function FinishLine:OnCreate() + + Trigger.OnCreate(self) + +end + +function FinishLine:OnInitialize() + + Trigger.OnInitialized(self) + self:SetTriggerCollisionEnabled(true) + self:SetUpdates(false) + +end + +if Server then + + function FinishLine:OnTriggerEntered(enterEnt, triggerEnt) + + -- Make bot skulks run to the end. + if enterEnt:isa("Skulk") then + enterEnt.endSequenceRunning = true + end + + -- Only trigger on real skulks (eg normal or replay). Don't want to end the race if the ghost finishes first! + if enterEnt:isa("Skulk") and not enterEnt:isa("SkulkHologram") then + GetSkulkChallenge():OnFinish() + end + + end + +end + +Shared.LinkClassToMap("FinishLine", FinishLine.kMapName, {}) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUIAlienHUD.lua b/challenges/skulk_challenge/lua/SCGUIAlienHUD.lua new file mode 100644 index 000000000..aeab03ec8 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIAlienHUD.lua @@ -0,0 +1,27 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIAlienHUD.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Hide the alien hud. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local old_GUIAlienHUD_SetIsVisible = GUIAlienHUD.SetIsVisible + +function GUIAlienHUD:SetIsVisible(isVisible) +end + +local old_GUIAlienHUD_Initialize = GUIAlienHUD.Initialize +function GUIAlienHUD:Initialize() + + old_GUIAlienHUD_Initialize(self) + + old_GUIAlienHUD_SetIsVisible(self, false) + self.adrenalineEnergy:SetIsVisible(false) + +end + +function GUIAlienHUD:Update(deltaTime) +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUIAlienTeamMessage.lua b/challenges/skulk_challenge/lua/SCGUIAlienTeamMessage.lua new file mode 100644 index 000000000..d2649544f --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIAlienTeamMessage.lua @@ -0,0 +1,26 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIAlienTeamMessage.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Permanently disable team messages. ("Commander needed", etc popups) +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +function GUIAlienTeamMessage:Initialize() + GUIAnimatedScript.Initialize(self) +end + +function GUIAlienTeamMessage:OnHelpScreenVisChange(state) +end + +function GUIAlienTeamMessage:UpdateVisibility() +end + +function GUIAlienTeamMessage:Uninitialize() + GUIAnimatedScript.Uninitialize(self) +end + +function GUIAlienTeamMessage:SetTeamMessage(message) +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUIDeathScreen.lua b/challenges/skulk_challenge/lua/SCGUIDeathScreen.lua new file mode 100644 index 000000000..06e202c02 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIDeathScreen.lua @@ -0,0 +1,90 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIDeathScreen.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Disable the death screen for the skulk challenge mode. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/GUIAnimatedScript.lua") +Script.Load("lua/DeathMessage_Client.lua") + +class 'GUIDeathScreen' (GUIAnimatedScript) + +function GUIDeathScreen:Initialize() + + GUIAnimatedScript.Initialize(self) + + self.background = self:CreateAnimatedGraphicItem() + self.background:SetColor(Color(0,0,0,0)) + self.background:SetIsScaling(false) + self.background:SetSize(Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0)) + self.background:SetLayer(kGUILayerDeathScreen) + + self.updateInterval = kUpdateIntervalAnimation + +end + +function GUIDeathScreen:Reset() + + GUIAnimatedScript.Reset(self) + + self.background:SetSize(Vector(Client.GetScreenWidth(), Client.GetScreenHeight(),0)) + +end + +function GUIDeathScreen:OnResolutionChanged() + + self:Uninitialize() + self:Initialize() + +end + +function GUIDeathScreen:Update(deltaTime) + + PROFILE("GUIDeathScreen:Update") + + GUIAnimatedScript.Update(self, deltaTime) + + local isDead = PlayerUI_GetIsDead() and not PlayerUI_GetIsSpecating() + + if isDead ~= self.lastIsDead then + + -- Check for the killer name as it will be nil if it hasn't been received yet. + local killerName = nil + local weaponIconIndex = nil + if isDead then + + local player = Client.GetLocalPlayer() + if player and not self.cinematic then + self.cinematic = Client.CreateCinematic(RenderScene.Zone_ViewModel) + self.cinematic:SetCinematic(player:GetFirstPersonDeathEffect()) + end + + self.background:FadeIn(2, "FADE_DEATH_SCREEN") + + else + + fadingOut = false + + if self.cinematic then + + if IsValid(self.cinematic) then + self.cinematic:SetIsVisible(false) + Client.DestroyCinematic(self.cinematic) + end + self.cinematic = nil + + end + + self.background:FadeOut(0.5, "FADE_DEATH_SCREEN") + + end + + self.lastIsDead = isDead + + end + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUIFader.lua b/challenges/skulk_challenge/lua/SCGUIFader.lua new file mode 100644 index 000000000..316e62182 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIFader.lua @@ -0,0 +1,161 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- challenges\skulk_challenge\lua\SCGUIFader.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Super simple fade-in/out GUIScript. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +class 'SCGUIFader' (GUIScript) + +SCGUIFader.kLayer = 255 + +function SCGUIFader:DoFade(duration, goal, callback) + + self.status = "fading" + + self.duration = duration + self.goal = goal + self.callback = callback + + self:Update(0) + +end + +function SCGUIFader:DoFadeIn(duration, callback) + + self:DoFade(duration, 0.0, callback) + self.updateInterval = 0.0 + +end + +function SCGUIFader:DoFadeOut(duration, callback) + + self:DoFade(duration, 1.0, callback) + self.updateInterval = 0.0 + +end + +function SCGUIFader:FadeInImmediately() + + self.status = "waiting" + self.fadeOpacity = 0.0 + + self:UpdateOpacity() + + self.updateInterval = 1.0 + +end + +function SCGUIFader:ToBlackImmediately() + + self.status = "waiting" + self.fadeOpacity = 1.0 + + self:UpdateOpacity() + + self.updateInterval = 1.0 + +end + +function SCGUIFader:Initialize() + + self.black = GUI.CreateItem() + + self:OnResolutionChanged() + + self.status = "waiting" + self.fadeOpacity = 0.0 -- whether we want it to be faded out or not (1 = faded out, 0 = invisible) + self.staticOpacity = 1.0 -- Opacity we want it to be when fully "faded out". + + self:UpdateOpacity() + self:SetLayer(self.kLayer) -- default layer value + + self.updateInterval = 1.0 -- low update rate by default. + self.deltaIsFrameTime = true -- tell GUIManager that we want the frame time passed into the Update() call, + -- not the time since this script was last updated. This fixes issues caused + -- by relying on deltaTime in the updates immediately following changes to + -- updateInterval, which would result in sometimes instantaneous fade-outs. :( + +end + +function SCGUIFader:Uninitialize() + + GUI.DestroyItem(self.black) + self.black = nil + self.callback = nil + self.status = "waiting" + +end + +function SCGUIFader:OnResolutionChanged() + + self.black:SetSize(Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0)) + self.black:SetPosition(Vector(0,0,0)) + +end + +-- used to set how dark the screen can get, not for performing the actual fade itself. +function SCGUIFader:SetOpacity(opacity) + + self.staticOpacity = opacity + self:UpdateOpacity() + +end + +function SCGUIFader:SetLayer(layer) + + self.black:SetLayer(layer) + +end + +function SCGUIFader:UpdateOpacity() + + self.black:SetColor(Color(0,0,0, self.staticOpacity * self.fadeOpacity)) + +end + +function SCGUIFader:FinishFade() + + self.status = "waiting" + self.fadeOpacity = self.goal + self:UpdateOpacity() + self.updateInterval = 1.0 + + if self.callback then + local tempCallback = self.callback + self.callback = nil + tempCallback() + end + +end + +function SCGUIFader:Update(deltaTime) + + if self.status == "fading" then + + if self.fadeOpacity == self.goal then + self:FinishFade() + return + end + + local change = deltaTime / self.duration + + if math.abs(self.goal - self.fadeOpacity) <= change then + self:FinishFade() + return + end + + if self.goal < self.duration then + change = -change + end + + self.fadeOpacity = self.fadeOpacity + change + self:UpdateOpacity() + + end + +end + diff --git a/challenges/skulk_challenge/lua/SCGUIFirstPersonSpectate.lua b/challenges/skulk_challenge/lua/SCGUIFirstPersonSpectate.lua new file mode 100644 index 000000000..4db1a2e8b --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIFirstPersonSpectate.lua @@ -0,0 +1,17 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIFirstPersonSpectate.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Get rid of this crap... +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +class 'GUIFirstPersonSpectate' (GUIScript) + +function GUIFirstPersonSpectate:Initialize() +end + +function GUIFirstPersonSpectate:Uninitialize() +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUILeaderboard.lua b/challenges/skulk_challenge/lua/SCGUILeaderboard.lua new file mode 100644 index 000000000..ecb7f1517 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUILeaderboard.lua @@ -0,0 +1,449 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- challenges\skulk_challenge\lua\SCGUILeaderboard.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Concrete implementation of the challenge leaderboard classes for skulk challenge. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/challenge/GUIChallengeLeaderboardAlien.lua") +Script.Load("lua/challenge/GUIReplayDownloaderAlien.lua") +Script.Load("lua/challenge/ReplayManager.lua") + +class 'SCGUILeaderboard' (GUIChallengeLeaderboardAlien) + +SCGUILeaderboard.kReplayTexture = PrecacheAsset("ui/challenge/replay_icon.dds") +SCGUILeaderboard.kGhostTexture = PrecacheAsset("ui/challenge/ghost_icon.dds") + +SCGUILeaderboard.kTimeXOffset = 306 +SCGUILeaderboard.kTimeYOffset = 144 + +SCGUILeaderboard.kReplayLine2XOffset = 449 -- center position of line2, relative to board +SCGUILeaderboard.kReplayLine2YOffset = 83 +SCGUILeaderboard.kReplayLine1YOffset = 40 -- center position of line1, relative to line2. + +SCGUILeaderboard.kReplayIconXOffset = 419 +SCGUILeaderboard.kReplayIconYOffset = 127 + +SCGUILeaderboard.kArrowXOffset = 500 +SCGUILeaderboard.kHighlightWidth = 600 +SCGUILeaderboard.kPlayerButtonWidth = 160 + +SCGUILeaderboard.kDownloaderLayerOffset = 10 + +SCGUILeaderboard.kFadeDuration = 1.0 + +SCGUILeaderboard.kLeaderboardPosition = Vector(1180, 224, 0) + +function SCGUILeaderboard:UpdateRowVisibility(rowIndex) + + GUIChallengeLeaderboardAlien.UpdateRowVisibility(self, rowIndex) + + local row = self.rows[rowIndex] + + row.timeItem:SetIsVisible(self.visible) + row.timeShadowItem:SetIsVisible(self.visible) + row.replayButton:SetIsVisible(row.visible) + row.ghostButton:SetIsVisible(row.visible) + +end + +function SCGUILeaderboard:UpdateVisibility() + + GUIChallengeLeaderboardAlien.UpdateVisibility(self) + + self.timeHeaderItem:SetIsVisible(self.visible) + self.timeHeaderShadowItem:SetIsVisible(self.visible) + + self.replayHeader1Item:SetIsVisible(self.visible) + self.replayHeader1ShadowItem:SetIsVisible(self.visible) + self.replayHeader2Item:SetIsVisible(self.visible) + self.replayHeader2ShadowItem:SetIsVisible(self.visible) + +end + +function SCGUILeaderboard:UpdateRowLayers(index, row) + + GUIChallengeLeaderboardAlien.UpdateRowLayers(self, index, row) + + row.timeItem:SetLayer(self.layer + self.kContentLayerOffset) + row.timeShadowItem:SetLayer(self.layer + self.kContentShadowLayerOffset) + + row.replayButton:UpdateLayers() + row.ghostButton:UpdateLayers() + +end + +function SCGUILeaderboard:UpdateLayers() + + GUIChallengeLeaderboardAlien.UpdateLayers(self) + + local contentLayer = self.layer + self.kContentLayerOffset + local shadowLayer = self.layer + self.kContentShadowLayerOffset + + self.timeHeaderItem:SetLayer(contentLayer) + self.timeHeaderShadowItem:SetLayer(shadowLayer) + + self.replayHeader1Item:SetLayer(contentLayer) + self.replayHeader1ShadowItem:SetLayer(shadowLayer) + self.replayHeader2Item:SetLayer(contentLayer) + self.replayHeader2ShadowItem:SetLayer(shadowLayer) + + if self.downloader then + self.downloader:SetLayer(self.layer + self.kDownloaderLayerOffset) + end + +end + +function SCGUILeaderboard:UpdateRowTransform(rowIndex) + + GUIChallengeLeaderboardAlien.UpdateRowTransform(self, rowIndex) + + local rowOffset = Vector(0, self.kRowSpacing * (rowIndex-1), 0) + local shadowOffset = self.kShadowOffset * self.scale + + local row = self.rows[rowIndex] + + local timePosition = ((Vector(self.kTimeXOffset, self.kTimeYOffset, 0) + rowOffset) * self.scale) + self.position + row.timeItem:SetPosition(timePosition) + row.timeShadowItem:SetPosition(timePosition + shadowOffset) + row.timeItem:SetScale(self.fontScale) + row.timeShadowItem:SetScale(self.fontScale) + + -- Replay and Ghost buttons + local icon1Origin = Vector(self.kReplayIconXOffset, self.kReplayIconYOffset, 0) + rowOffset + local icon2Origin = Vector(icon1Origin.x + self.kIconSize + self.kCommonMargin, icon1Origin.y, 0) + local icon1Origin = icon1Origin * self.scale + self.position + local icon2Origin = icon2Origin * self.scale + self.position + local iconSize = self.scale * self.kIconSize + row.replayButton.realSize = Vector(iconSize) + row.ghostButton.realSize = Vector(iconSize) + row.replayButton.realPos = Vector(icon1Origin) + row.ghostButton.realPos = Vector(icon2Origin) + row.replayButton:SetPosition(icon1Origin, shadowOffset) + row.ghostButton:SetPosition(icon2Origin, shadowOffset) + row.replayButton:SetSize(iconSize) + row.ghostButton:SetSize(iconSize) + +end + +function SCGUILeaderboard:UpdateRowStencilFunc(rowIndex, sFunc) + + GUIChallengeLeaderboardAlien.UpdateRowStencilFunc(self, rowIndex, sFunc) + + local row = self.rows[rowIndex] + + row.timeItem:SetStencilFunc(sFunc) + row.timeShadowItem:SetStencilFunc(sFunc) + + row.replayButton:SetStencilFunc(sFunc) + row.ghostButton:SetStencilFunc(sFunc) + +end + +function SCGUILeaderboard:UpdateTransform() + + GUIChallengeLeaderboardAlien.UpdateTransform(self) + + local shadowOffset = self.kShadowOffset * self.scale + + -- Time + local timePosition = (Vector(self.kTimeXOffset, self.kHeaderYOffset, 0) * self.scale) + self.position + self.timeHeaderItem:SetPosition(timePosition) + self.timeHeaderShadowItem:SetPosition(timePosition + shadowOffset) + self.timeHeaderItem:SetScale(self.fontScale) + self.timeHeaderShadowItem:SetScale(self.fontScale) + + -- Replay & Ghost + local replayPosition2 = Vector(self.kReplayLine2XOffset, self.kReplayLine2YOffset, 0) + local replayPosition1 = Vector(replayPosition2.x, replayPosition2.y - self.kReplayLine1YOffset, 0) + + replayPosition2 = replayPosition2 * self.scale + self.position + replayPosition1 = replayPosition1 * self.scale + self.position + self.replayHeader1Item:SetPosition(replayPosition1) + self.replayHeader1ShadowItem:SetPosition(replayPosition1 + shadowOffset) + self.replayHeader1Item:SetScale(self.fontScale) + self.replayHeader1ShadowItem:SetScale(self.fontScale) + + self.replayHeader2Item:SetPosition(replayPosition2) + self.replayHeader2ShadowItem:SetPosition(replayPosition2 + shadowOffset) + self.replayHeader2Item:SetScale(self.fontScale) + self.replayHeader2ShadowItem:SetScale(self.fontScale) + +end + +function SCGUILeaderboard:PlayReplay(replayData, score) + + GetSkulkChallenge():LoadReplay(replayData, score) + +end + +function SCGUILeaderboard:PlayGhost(replayData) + + GetSkulkChallenge():LoadGhost(replayData) + +end + +function SCGUILeaderboard:OnReplayDownloadSuccessful() + + local replayData = {} + + if not Client.GetReplayFromDownloadedUGC(self.downloader.ugcHandle, self.downloader.downloadedFileSize, replayData, Challenge.ReplayType_SkulkChallenge) then + Log("ERROR! Problem converting downloaded UGC to Skulk Challenge replay!") + return + end + + GetReplayManager():AddReplay(replayData, self.downloader.steamId) + + self:PlayReplay(replayData, self.replayScore) + +end + +function SCGUILeaderboard:OnGhostDownloadSuccessful() + + local replayData = {} + + if not Client.GetReplayFromDownloadedUGC(self.downloader.ugcHandle, self.downloader.downloadedFileSize, replayData, Challenge.ReplayType_SkulkChallenge) then + Log("ERROR! Problem converting downloaded UGC to Skulk Challenge replay!") + return + end + + GetReplayManager():AddReplay(replayData, self.downloader.steamId) + + self:PlayGhost(replayData) + + +end + +function SCGUILeaderboard:OnDownloaderTerminated() + + -- Downloader is being destroyed, release its hold of inactivity on this window and its siblings. + for i=1, US_GetSize(self.siblingScripts) do + local script = US_GetElement(self.siblingScripts, i) + script:SetWindowActive(self.downloader, true) + end + self:SetWindowActive(self.downloader, true) + + -- Downloader destroys itself. + self.downloader = nil + +end + +function SCGUILeaderboard:SetScale(scale) + + GUIChallengeLeaderboard.SetScale(self, scale) + + if self.downloader then + self.downloader:SetScale(self.scale) + end + +end + +function SCGUILeaderboard:CreateDownloader() + + assert(self.downloader == nil) + + self.downloader = GetGUIManager():CreateGUIScript("GUIReplayDownloaderAlien") + self.downloader:SetLayer(self.layer + self.kDownloaderLayerOffset) + self.downloader:SetScale(self.scale) + self.downloader:SetupCallback("terminated", function() self:OnDownloaderTerminated() end) + + -- set the current window and all of its siblings inactive, attributed to the downloader. + for i=1, US_GetSize(self.siblingScripts) do + local script = US_GetElement(self.siblingScripts, i) + script:SetWindowActive(self.downloader, false) + end + self:SetWindowActive(self.downloader, false) + +end + +function SCGUILeaderboard:OnReplayButtonClicked(rowIndex) + + local entry = self:GetEntryDisplayedAtIndex(rowIndex) + assert(entry.ugcHandle ~= self.kInvalidHandle) + + -- See if this replay is cached. + local replayData = GetReplayManager():GetReplay(entry.steamId) + + self.replayScore = entry.score + + if not replayData then + -- need to download replay + self:CreateDownloader(entry.ugcHandle) + self.downloader.steamId = entry.steamId + self.downloader:SetOkButtonText("VIEW_REPLAY") + self.downloader:SetupCallback("ok", function() self:OnReplayDownloadSuccessful() end) + self.downloader:BeginDownloadingUGC(entry.ugcHandle) + return + end + + -- Replay is already cached, no need to download it again. + self:PlayReplay(replayData, self.replayScore) + +end + +function SCGUILeaderboard:OnGhostButtonClicked(rowIndex) + + local entry = self:GetEntryDisplayedAtIndex(rowIndex) + assert(entry.ugcHandle ~= self.kInvalidHandle) + + -- See if this replay is cached. + local replayData = GetReplayManager():GetReplay(entry.steamId) + + -- See if this replay is cached. + if not replayData then + -- need to download replay + self:CreateDownloader(entry.ugcHandle) + self.downloader.steamId = entry.steamId + self.downloader:SetOkButtonText("RACE_GHOST") + self.downloader:SetupCallback("ok", function() self:OnGhostDownloadSuccessful() end) + self.downloader:BeginDownloadingUGC(entry.ugcHandle) + return + end + + -- Replay is already cached, no need to download it again. + self:PlayGhost(replayData) + +end + +function SCGUILeaderboard:InitializeRow(rowIndex) + + GUIChallengeLeaderboardAlien.InitializeRow(self, rowIndex) + + local row = self.rows[rowIndex] + + row.timeItem, row.timeShadowItem = self:CreateTextItem(true) + row.timeItem:SetTextAlignmentX(GUIItem.Align_Center) + row.timeShadowItem:SetTextAlignmentX(GUIItem.Align_Center) + + row.replayButton = self:CreateSimpleButton(self.kReplayTexture, + function(button) + button.board:OnReplayButtonClicked(button.rowIndex) + end, "LEADERBOARD_TOOLTIP_REPLAY", "LEADERBOARD_TOOLTIP_REPLAY_MISSING") + row.replayButton.rowIndex = rowIndex + + row.ghostButton = self:CreateSimpleButton(self.kGhostTexture, + function(button) + button.board:OnGhostButtonClicked(button.rowIndex) + end, "LEADERBOARD_TOOLTIP_GHOST", "LEADERBOARD_TOOLTIP_REPLAY_MISSING") + row.ghostButton.rowIndex = rowIndex + +end + +function SCGUILeaderboard:InitGUI() + + GUIChallengeLeaderboardAlien.InitGUI(self) + + -- Initialze "time" column heading text. + self.timeHeaderItem, self.timeHeaderShadowItem = self:CreateTextItem(true) + self.timeHeaderItem:SetText(Locale.ResolveString("TIME")) + self.timeHeaderShadowItem:SetText(Locale.ResolveString("TIME")) + self.timeHeaderItem:SetTextAlignmentX(GUIItem.Align_Center) + self.timeHeaderShadowItem:SetTextAlignmentX(GUIItem.Align_Center) + + -- Initialize line1 of "replay & ghost" heading text. + self.replayHeader1Item, self.replayHeader1ShadowItem = self:CreateTextItem(true) + self.replayHeader1Item:SetText(Locale.ResolveString("CHALLENGE_REPLAY_LINE_1")) + self.replayHeader1ShadowItem:SetText(Locale.ResolveString("CHALLENGE_REPLAY_LINE_1")) + self.replayHeader2Item, self.replayHeader2ShadowItem = self:CreateTextItem(true) + self.replayHeader2Item:SetText(Locale.ResolveString("CHALLENGE_REPLAY_LINE_2")) + self.replayHeader2ShadowItem:SetText(Locale.ResolveString("CHALLENGE_REPLAY_LINE_2")) + self.replayHeader1Item:SetTextAlignmentX(GUIItem.Align_Center) + self.replayHeader1ShadowItem:SetTextAlignmentX(GUIItem.Align_Center) + self.replayHeader2Item:SetTextAlignmentX(GUIItem.Align_Center) + self.replayHeader2ShadowItem:SetTextAlignmentX(GUIItem.Align_Center) + +end + +function SCGUILeaderboard:Uninitialize() + + GUIChallengeLeaderboard.Uninitialize(self) + + if self.downloader then + GetGUIManager():DestroyGUIScript(self.downloader) + self.downloader = nil + end + +end + +function SCGUILeaderboard:HideRow(rowIndex) + + GUIChallengeLeaderboardAlien.HideRow(self, rowIndex) + + local row = self.rows[rowIndex] + + row.timeItem:SetIsVisible(false) + row.timeShadowItem:SetIsVisible(false) + + row.replayButton:SetIsVisible(false) + row.ghostButton:SetIsVisible(false) + +end + +function SCGUILeaderboard:SetRowData(rowIndex, data) + + GUIChallengeLeaderboardAlien.SetRowData(self, rowIndex, data) + + local row = self.rows[rowIndex] + + local text = ConvertMillisecondsToString(data.score) + row.timeItem:SetText(text) + row.timeShadowItem:SetText(text) + + local ugcValid = data.ugcHandle ~= self.kInvalidHandle + row.replayButton:SetIsEnabled(ugcValid) + row.ghostButton:SetIsEnabled(ugcValid) + + return true + +end + +-- the width of the row, before scaling. +function SCGUILeaderboard:GetRowWidth() + + return self.kReplayIconXOffset + ((self.kIconSize + self.kCommonMargin) * 2.0) + +end + +function SCGUILeaderboard:UpdateRowOpacity(rowIndex, opacity) + + GUIChallengeLeaderboardAlien.UpdateRowOpacity(self, rowIndex, opacity) + + local row = self.rows[rowIndex] + + local modifiedColor = Color(self.kColor) + modifiedColor.a = modifiedColor.a * opacity + + local modifiedShadowColor = Color(self.kShadowColor) + modifiedShadowColor.a = modifiedShadowColor.a * opacity + + row.timeItem:SetColor(modifiedColor) + row.timeShadowItem:SetColor(modifiedShadowColor) + + row.replayButton:SetOpacity(opacity) + row.ghostButton:SetOpacity(opacity) + +end + +function SCGUILeaderboard:UpdateOpacity(opacity) + + GUIChallengeLeaderboardAlien.UpdateOpacity(self, opacity) + + local modifiedColor = Color(self.kColor) + modifiedColor.a = modifiedColor.a * opacity + + local modifiedShadowColor = Color(self.kShadowColor) + modifiedShadowColor.a = modifiedShadowColor.a * opacity + + self.timeHeaderItem:SetColor(modifiedColor) + self.timeHeaderShadowItem:SetColor(modifiedShadowColor) + + self.replayHeader1Item:SetColor(modifiedColor) + self.replayHeader1ShadowItem:SetColor(modifiedShadowColor) + self.replayHeader2Item:SetColor(modifiedColor) + self.replayHeader2ShadowItem:SetColor(modifiedShadowColor) + +end + diff --git a/challenges/skulk_challenge/lua/SCGUILightArray.lua b/challenges/skulk_challenge/lua/SCGUILightArray.lua new file mode 100644 index 000000000..d4ef7fbb5 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUILightArray.lua @@ -0,0 +1,216 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUILightArray.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Group of several SCGUIStartLight scripts to perform the starting animation. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/SCGUIStartLight.lua") + +class 'SCGUILightArray' (GUIScript) + +local kDefaultLayer = 40 + +SCGUILightArray.kLightSpacing = 192.0 -- how far between light centers? +SCGUILightArray.kLightTopOffset = 192.0 -- how far from the top of the screen are the lights' centers. + +SCGUILightArray.kTemporalSpacing = 0.1 + +SCGUILightArray.kSoundPrerollDuration = 0.25 -- the "ding" is about 0.25 seconds into the sound. +SCGUILightArray.kCountdownSound = PrecacheAsset("sound/NS2.fev/skulk_challenge/count_down") +SCGUILightArray.kGoSound = PrecacheAsset("sound/NS2.fev/skulk_challenge/go") + +Client.PrecacheLocalSound(SCGUILightArray.kCountdownSound) +Client.PrecacheLocalSound(SCGUILightArray.kGoSound) + +SCGUILightArray.kInitialDelay = SCGUILightArray.kSoundPrerollDuration + +SCGUILightArray.kNumLights = 5 -- -2 = 3 second countdown + +function SCGUILightArray:CreateLight(colorName, lit) + + local newLight = GetGUIManager():CreateGUIScript("SCGUIStartLight") + newLight:SetColor(colorName) + if lit then + newLight:SetIsLit(true) + end + return newLight + +end + +function SCGUILightArray:UpdateTransform() + + local startOffset = (#self.lights - 1) * 0.5 * -self.kLightSpacing + for i=1, #self.lights do + local index = i-1 + local pos = Vector(startOffset + self.kLightSpacing * index, self.kLightTopOffset, 0) * self.scale + self.position + self.lights[i]:SetPosition(pos) + self.lights[i]:SetScale(self.scale) + end + +end + +function SCGUILightArray:Initialize() + + assert(self.kNumLights >= 2) + self.lights = {} + self.lights[1] = self:CreateLight("red", true) + + for i=2, self.kNumLights - 1 do + self.lights[i] = self:CreateLight("yellow", false) + end + + self.lights[#self.lights+1] = self:CreateLight("green", false) + + self.position = Vector(0,0,0) + self.scale = Vector(1,1,1) + self.updateInterval = 0 + + self.delayedActions = {} -- series of timed callbacks. + + self:SetLayer(kDefaultLayer) + + self:PositionOnScreen() + +end + +function SCGUILightArray:Uninitialize() + + for i=1, #self.lights do + GetGUIManager():DestroyGUIScript(self.lights[i]) + end + +end + +function SCGUILightArray:OnResolutionChanged() + + self:PositionOnScreen() + +end + +function SCGUILightArray:UpdateLayers() + + for i=1, #self.lights do + self.lights[i]:SetLayer(self.layer) + end + +end + +function SCGUILightArray:SetLayer(layer) + + self.layer = layer + self:UpdateLayers() + +end + +function SCGUILightArray:SetPosition(pos) + + self.position = pos + self:UpdateTransform() + +end + +function SCGUILightArray:SetScale(scale) + + self.scale = scale + self:UpdateTransform() + +end + +function SCGUILightArray:PositionOnScreen() + + local pos, scale = Fancy_Transform(Vector(960, 0, 0), Vector(1,1,1)) + self:SetPosition(pos) + self:SetScale(scale) + +end + +function SCGUILightArray:FlyIn() + + for i=1, #self.lights do + local index = i-1 + self.lights[i]:FlyIn(-self.kTemporalSpacing * index) + end + +end + +function SCGUILightArray:FlyOut() + + for i=1, #self.lights do + local index = i-1 + self.lights[i]:FlyOut(-self.kTemporalSpacing * index) + end + +end + +function SCGUILightArray:AddDelayedAction(delay, func) + + local action = {} + action.fireTime = Shared.GetSystemTimeReal() + delay + action.func = func + table.insert(self.delayedActions, action) + +end + +-- clears ALL pending actions immediately, makes the lights animate out, and once finished destroys itself. +function SCGUILightArray:Dispose() + + self.delayedActions = {} + self:FlyOut() + self:AddDelayedAction(2.0, + function(self, action) + GetGUIManager():DestroyGUIScript(self) + end) + +end + +-- Begins the countdown, and performs a callback function when it finishes. +function SCGUILightArray:DoCountdown(callback) + + for i=2, #self.lights do -- skip the red light, it's just there for show. :) + + local index = i - 2 + + -- sound effect + self:AddDelayedAction(self.kInitialDelay - self.kSoundPrerollDuration + index, + function(self, action) + StartSoundEffect((i < #self.lights) and self.kCountdownSound or self.kGoSound) + end) + + -- light + self:AddDelayedAction(self.kInitialDelay + index, + function(self, action) + self.lights[i]:Light(action.fireTime - Shared.GetSystemTimeReal()) + if i == #self.lights then + callback() + end + end) + + end + +end + +function SCGUILightArray:Update(deltaTime) + + local now = Shared.GetSystemTimeReal() + + for i = #self.delayedActions, 1, -1 do + + local action = self.delayedActions[i] + if action then + if now >= action.fireTime then + action.func(self, action) + table.remove(self.delayedActions, i) + end + end + + end + +end + + + + diff --git a/challenges/skulk_challenge/lua/SCGUIMainMenu.lua b/challenges/skulk_challenge/lua/SCGUIMainMenu.lua new file mode 100644 index 000000000..69419072c --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIMainMenu.lua @@ -0,0 +1,158 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIMainMenu.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Change the menu options to be skulk-challenge-specific. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local LinkItems = +{ + [1] = { "MENU_RESUME_GAME", function(self) + + self.scriptHandle:SetIsVisible(not self.scriptHandle:GetIsVisible()) + + end + }, + [2] = { "MENU_GO_TO_READY_ROOM", function(self) + + self.scriptHandle:SetIsVisible(not self.scriptHandle:GetIsVisible()) + Shared.ConsoleCommand("rr") + + end, "readyroom" + }, + [3] = { "MENU_VOTE", function(self) + + OpenVoteMenu() + self.scriptHandle:SetIsVisible(false) + + end, "vote" + }, + [4] = { "MENU_SERVER_BROWSER", function(self) + + self.scriptHandle:AttemptToOpenServerBrowser() + + end, "browser" + }, + [5] = { "MENU_ORGANIZED_PLAY", function(self) + + self.scriptHandle:ActivateGatherWindow() + + end + }, + [6] = { "MENU_OPTIONS", function(self) + + if not self.scriptHandle.optionWindow then + self.scriptHandle:CreateOptionWindow() + end + self.scriptHandle:TriggerOpenAnimation(self.scriptHandle.optionWindow) + self.scriptHandle:HideMenu() + + end, "options" + }, + [7] = { "MENU_CUSTOMIZE_PLAYER", function(self) + + self.scriptHandle:ActivateCustomizeWindow() + self.scriptHandle.screenFade = GetGUIManager():CreateGUIScript("GUIScreenFade") + self.scriptHandle.screenFade:Reset() + + end, "customize" + }, + [8] = { "MENU_DISCONNECT", function(self) + + self.scriptHandle:HideMenu() + + Shared.ConsoleCommand("disconnect") + + self.scriptHandle:ShowMenu() + + end, "disconnect" + }, + [9] = { "MENU_TRAINING", function(self) + + self.scriptHandle:OpenTraining() + + end, "training" + }, + [10] = { "MENU_MODS", function(self) + + if not self.scriptHandle.modsWindow then + self.scriptHandle:CreateModsWindow() + end + self.scriptHandle.modsWindow.sorted = false + self.scriptHandle:TriggerOpenAnimation(self.scriptHandle.modsWindow) + self.scriptHandle:HideMenu() + + end, "mods" + }, + [11] = { "MENU_CREDITS", function(self) + + self.scriptHandle:HideMenu() + self.creditsScript = GetGUIManager():CreateGUIScript("menu/GUICredits") + MainMenu_OnPlayButtonClicked() + self.creditsScript:SetPlayAnimation("show") + self.creditsScript.closeEvent:AddHandler( self, function() self.scriptHandle:ShowMenu() end) + + end, "credits" + }, + [12] = { "MENU_EXIT", function() + + Client.Exit() + + if Sabot.GetIsInGather() then + Sabot.QuitGather() + end + + end, "exit" + }, + [13] = { "MENU_PLAY", function(self) + + MainMenu_OnPlayButtonClicked() --Play click sound + + self.scriptHandle:HideMenu() + self.scriptHandle.playScreen:Show() + + end, + }, + [14] = { "MENU_END_RUN", function(self) + + self.scriptHandle:SetIsVisible(not self.scriptHandle:GetIsVisible()) + GetSkulkChallenge():EndRun() + + end, + }, + [15] = { "MENU_RESTART_RUN", function(self) + + self.scriptHandle:SetIsVisible(not self.scriptHandle:GetIsVisible()) + GetSkulkChallenge():RestartRun() + + end, + }, +} +--Id of Links table is used to order links +local LinkOrder = +{ + { 13,9,6,7,10,11,12 }, + { 1,15,14,6,7,8 } +} + +function GUIMainMenu:CreateMainLinks() + + local index = MainMenu_IsInGame() and 2 or 1 + local linkOrder = LinkOrder[index] + for i=1, #linkOrder do + local linkId = linkOrder[i] + local text = LinkItems[linkId][1] + local callbackTable = LinkItems[linkId][2] + local event = LinkItems[linkId][3] + if event then + event = ( MainMenu_IsInGame() and "igmenu_" or "menu_" ) .. event + callbackTable = RecordEventWrap( callbackTable, event ) + end + local link = self:CreateMainLink(text, i, callbackTable) + table.insert(self.Links, link) + end + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUIMinimapFrame.lua b/challenges/skulk_challenge/lua/SCGUIMinimapFrame.lua new file mode 100644 index 000000000..93dc2eff3 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIMinimapFrame.lua @@ -0,0 +1,13 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIMinimapFrame.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Disable the minimap, it's not needed. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +function GUIMinimapFrame:SendKeyEvent(key, down) + return false +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUIRaceTimer.lua b/challenges/skulk_challenge/lua/SCGUIRaceTimer.lua new file mode 100644 index 000000000..92fedb7cc --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIRaceTimer.lua @@ -0,0 +1,360 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUITimer.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Timer for the skulk time trial. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +class 'GUITimeDisplay' (GUIScript) + +-- the following pixel-count measurements are based on a 1920x1080 screen size. Measurements will +-- scale linearly with the screen's height. +GUITimeDisplay.expectedScreenHeight = 1080 -- size measurements are intended for +GUITimeDisplay.digitActualHeight = 46 +GUITimeDisplay.digitSize = Vector(64, GUITimeDisplay.digitActualHeight + 2, 0) +GUITimeDisplay.colonSize = Vector(24, 48, 0) +GUITimeDisplay.verticalSpacing = 24 -- spacing between GUITimeDisplay displays +GUITimeDisplay.cornerSpacing = Vector(128, 128, 0) +GUITimeDisplay.dropShadowOffset = Vector(5, 5, 0) +GUITimeDisplay.font = Fonts.kMicrogrammaDMedExt_Large +GUITimeDisplay.shadowColor = Color(0, 0, 0, 0.5) +GUITimeDisplay.staticScaleFactor = GUITimeDisplay.digitActualHeight / 25 -- font must not cover full size +GUITimeDisplay.kFadeTime = 1.0 +local kDefaultColor = Color(1,1,1,1) + +GUITimeDisplay.kIconHeight = 64 +GUITimeDisplay.kIconSpacing = 16 + +local function CreateNewTextCharacterGraphic() + local newCharacter = GUI.CreateItem() + newCharacter:SetOptionFlag(GUIItem.ManageRender) + newCharacter:SetTextAlignmentX(GUIItem.Align_Center) + newCharacter:SetTextAlignmentY(GUIItem.Align_Center) + newCharacter:SetLayer(kGUILayerPlayerHUDForeground2) + newCharacter:SetFontName(GUITimeDisplay.font) + newCharacter:SetAnchor(GUIItem.Left, GUIItem.Top) + newCharacter:SetIsVisible(true) + + return newCharacter +end + +local function SetDigits(self, time) + + local minutes, tensOfSeconds, seconds, tSecs, hSecs = ConvertSecondsToFormattedTime(time) + + self.digits[1]:SetText(tostring(minutes)) + self.digitsShadow[1]:SetText(tostring(minutes)) + self.digits[2]:SetText(tostring(tensOfSeconds)) + self.digitsShadow[2]:SetText(tostring(tensOfSeconds)) + self.digits[3]:SetText(tostring(seconds)) + self.digitsShadow[3]:SetText(tostring(seconds)) + self.digits[4]:SetText(tostring(tSecs)) + self.digitsShadow[4]:SetText(tostring(tSecs)) + self.digits[5]:SetText(tostring(hSecs)) + self.digitsShadow[5]:SetText(tostring(hSecs)) + +end + +local function UpdateScaling(display) + + -- scale factor for screen-pixel calculations + local h = Client.GetScreenHeight() + display.scaleFactor = h / display.expectedScreenHeight + local scaleFactor = nil + + -- figure out text scale factor to make text fit within its defined space. + local unscaledHeight = display.digits[1]:GetTextHeight("0") + local desiredHeight = display.digitActualHeight + scaleFactor = ( display.digitActualHeight / unscaledHeight ) * display.scaleFactor * display.staticScaleFactor + local scaleVector = Vector(scaleFactor, scaleFactor, 1) + + for i=1, #display.digits do + display.digits[i]:SetScale(scaleVector) + display.digitsShadow[i]:SetScale(scaleVector) + end + + -- update the icon, if available. + if display.iconItem then + local iconSize = Vector() + iconSize.y = display.kIconHeight * display.scaleFactor + iconSize.x = (display.iconItem:GetTextureWidth() / display.iconItem:GetTextureHeight()) * iconSize.y + display.iconItem:SetSize(iconSize) + end + +end + +local function UpdatePositions(display) + -- first, calculate everything as if this was 1920x1080, then scale everything as a final step. + local centerY = display.positionIndex * ( display.verticalSpacing + display.digitSize.y ) + ( display.digitSize.y * 0.5 ) + display.cornerSpacing.y + local offsetX = display.cornerSpacing.x + ( display.digitSize.x * 0.5 ) + for i=1, #display.digits do + local scaledPosition = Vector(offsetX, centerY, 0) * display.scaleFactor + display.digits[i]:SetPosition(scaledPosition) + display.digitsShadow[i]:SetPosition(scaledPosition + (display.dropShadowOffset * display.scaleFactor) ) + + -- prepare for next digit's position + offsetX = offsetX + display.digitSize.x + if i % 2 == 1 then + offsetX = offsetX + display.colonSize.x + end + end + + -- update the icon, if available. + if display.iconItem then + local iconPos = Vector() + iconPos.x = display.cornerSpacing.x - display.kIconSpacing + iconPos.y = centerY + iconPos = iconPos * display.scaleFactor + iconPos.x = iconPos.x - display.iconItem:GetSize().x + iconPos.y = iconPos.y - display.iconItem:GetSize().y * 0.5 + display.iconItem:SetPosition(iconPos) + end + +end + +local function SharedUpdate(self) + if self.isStatic ~= false then -- equate nil to true + SetDigits(self, self.startingDisplayedTime) + UpdatePositions(self) + return + end + + SetDigits(self, self.startingDisplayedTime + (Shared.GetSystemTimeReal() - self.realStartTime)) + UpdatePositions(self) +end + +local function DestroyIcon(self) + + if self.iconItem then + GUI.DestroyItem(self.iconItem) + self.iconItem = nil + end + +end + +local function CreateIcon(self, fileName) + + DestroyIcon(self) + + self.iconItem = GUI.CreateItem() + self.iconItem:SetTexture(fileName) + self.iconItem:SetIsVisible(true) + + UpdateScaling(self) + UpdatePositions(self) + self:UpdateColor() + +end + +function GUITimeDisplay:SetIcon(fileName) + + if not fileName then + DestroyIcon(self) + end + + CreateIcon(self, fileName) + +end + +function GUITimeDisplay:Initialize() + + self.startingDisplayedTime = 0 + self.realStartTime = 0 + + self.positionIndex = 0 + self.digits = {} + self.digitsShadow = {} + for i=1, 5 do + local newDigit = CreateNewTextCharacterGraphic() + local newDigitShadow = CreateNewTextCharacterGraphic() + GUIItem.SetText(newDigit, "0") + GUIItem.SetText(newDigitShadow, "0") + newDigitShadow:SetLayer(kGUILayerPlayerHUDForeground1) + newDigit:SetIsVisible(true) + newDigitShadow:SetIsVisible(true) + newDigitShadow:SetColor(GUITimeDisplay.shadowColor) + table.insert(self.digits, newDigit) + table.insert(self.digitsShadow, newDigitShadow) + end + + -- 1 fps for now... no need to update any faster until we know this time display will be updated in + -- real time. (Some GUITimeDisplay objects are only used to display a static time, eg best time or medal times) + self.updateInterval = 1 + self.deltaIsFrameTime = true -- changing updateInterval can mean the deltaTime value passed into Update() isn't valid. + self.isStatic = true + + self:SetOpacityInstantly(1.0) + self:SetColor(kDefaultColor) + + UpdateScaling(self) + UpdatePositions(self) + +end + +function GUITimeDisplay:Uninitialize() + + assert(#self.digits == #self.digitsShadow) + for i=1, #self.digits do + GUI.DestroyItem(self.digits[i]) + GUI.DestroyItem(self.digitsShadow[i]) + end + +end + +function GUITimeDisplay:SetColor(color) + + self.color = color + self:UpdateColor() + +end + +function GUITimeDisplay:SetOpacityInstantly(opacity) + + self.opacity = opacity + self.opacityTarget = opacity + self:UpdateColor() + +end + +function GUITimeDisplay:SetOpacityTarget(opacity) + + self.opacityTarget = opacity + self.updateInterval = 0 + +end + +function GUITimeDisplay:SetIndexedPosition(index) + + -- indexes start at 0. + self.positionIndex = index + UpdatePositions(self) + +end + +function GUITimeDisplay:OnResolutionChanged() + + UpdateScaling(self) + UpdatePositions(self) + +end + +function GUITimeDisplay:Start() + self.startingDisplayedTime = self.startingDisplayedTime or 0 + self.realStartTime = Shared.GetSystemTimeReal() + self.isStatic = false + self.updateInterval = 0 +end + +-- does NOT stop the timer if it's running. +function GUITimeDisplay:SetTime(time) + + self.startingDisplayedTime = time + self.realStartTime = Shared.GetSystemTimeReal() + SharedUpdate(self) + +end + +function GUITimeDisplay:Stop() + + if self.isStatic ~= false then + return + end + + self.startingDisplayedTime = self.startingDisplayedTime + Shared.GetSystemTimeReal() - self.realStartTime + + self.isStatic = true + self.updateInterval = 1 + SharedUpdate(self) + +end + +-- time as a score is given in integer milliseconds for skulk challenge. To avoid rounding errors, we +-- truncate the time to the nearest millisecond. +function GUITimeDisplay:Quantize() + + assert(self.isStatic == true) + + self.startingDisplayedTime = math.floor(self.startingDisplayedTime * 1000.0) * 0.001 + +end + +-- Stops timer and sets all digits to 0 +function GUITimeDisplay:Reset() + + self:Stop() + self.startingDisplayedTime = 0 + SharedUpdate(self) + +end + +-- Returns time, in seconds. +function GUITimeDisplay:GetTime() + + if self.isStatic then + return self.startingDisplayedTime + end + + return self.startingDisplayedTime + ( Shared.GetSystemTimeReal() - (self.realStartTime or Shared.GetSystemTimeReal()) ) + +end + +function GUITimeDisplay:UpdateColor() + + assert(#self.digits == #self.digitsShadow) + + local digitsColor = Color(self.color) + digitsColor.a = digitsColor.a * self.opacity + + local digitsShadowColor = Color(self.shadowColor) + digitsShadowColor.a = digitsShadowColor.a * self.opacity + + for i=1, #self.digits do + + self.digits[i]:SetColor(digitsColor) + self.digitsShadow[i]:SetColor(digitsShadowColor) + + end + + if self.iconItem then + self.iconItem:SetColor(Color(1,1,1,self.opacity)) + end + +end + +function GUITimeDisplay:UpdateOpacity(self, deltaTime) + + if self.opacity == self.opacityTarget then + return + end + + local opacityChange = deltaTime / self.kFadeTime + if math.abs(self.opacity - self.opacityTarget) <= opacityChange then + self.opacity = self.opacityTarget + self:UpdateColor() + + if self.isStatic then + self.updateInterval = 1 + end + + return + end + + if self.opacity > self.opacityTarget then + opacityChange = -opacityChange + end + + self.opacity = self.opacity + opacityChange + + self:UpdateColor() + +end + +function GUITimeDisplay:Update(deltaTime) + + SharedUpdate(self) + self:UpdateOpacity(self, deltaTime) + +end diff --git a/challenges/skulk_challenge/lua/SCGUIScoreboard.lua b/challenges/skulk_challenge/lua/SCGUIScoreboard.lua new file mode 100644 index 000000000..e1361c377 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIScoreboard.lua @@ -0,0 +1,15 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIScoreboard.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Prevent scoreboard from appearing in Skulk Challenge. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +function GUIScoreboard:SendKeyEvent(key, down) + + return false + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUISpectator.lua b/challenges/skulk_challenge/lua/SCGUISpectator.lua new file mode 100644 index 000000000..98ee4ed7d --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUISpectator.lua @@ -0,0 +1,11 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUISpectator.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Clear out spectator gui stuff... +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +class 'GUISpectator' (GUIScript) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUISpeedometer.lua b/challenges/skulk_challenge/lua/SCGUISpeedometer.lua new file mode 100644 index 000000000..c66ff05db --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUISpeedometer.lua @@ -0,0 +1,231 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUISpeedometer.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local kDefaultLayer = 40 + +class 'SCGUISpeedometer' (GUIScript) + +-- offsets to apply to graphics for center position (before scaling) +SCGUISpeedometer.kNeedleOffset = Vector(-82, -394, 0) +SCGUISpeedometer.kFrameOffset = Vector(-503, -505, 0) +SCGUISpeedometer.kFrameLitOffset = Vector(-556, -576, 0) +SCGUISpeedometer.kCenterOffset = Vector(-114, -96, 0) +SCGUISpeedometer.kCenterLitOffset = Vector(-217, -204, 0) +SCGUISpeedometer.kNeedleRotationOffset = Vector(7, -211, 0) + +SCGUISpeedometer.kNeedleTexture = PrecacheAsset("ui/challenge/skulk_challenge/speedometer_needle.dds") +SCGUISpeedometer.kFrameTexture = PrecacheAsset("ui/challenge/skulk_challenge/speedometer_frame.dds") +SCGUISpeedometer.kFrameLitTexture = PrecacheAsset("ui/challenge/skulk_challenge/speedometer_frame_lit.dds") +SCGUISpeedometer.kCenterTexture = PrecacheAsset("ui/challenge/skulk_challenge/speedometer_center.dds") +SCGUISpeedometer.kCenterLitTexture = PrecacheAsset("ui/challenge/skulk_challenge/speedometer_center_lit.dds") + +-- pivot-point of needle is positioned 90% of the way to the bottom of the screen, centered horizontally. +SCGUISpeedometer.kVerticalPositionFraction = 0.9 +SCGUISpeedometer.kBaseScaleFactor = 0.4 + +SCGUISpeedometer.kNeedleSmoothFactor = 0.125 -- lower = faster convergence + +SCGUISpeedometer.kSpeedThresholdFast = 10.0 +SCGUISpeedometer.kSpeedThresholdSuperFast = 11.0 + +SCGUISpeedometer.kSpeedMaxDuration = 1.5 +SCGUISpeedometer.kWiggleAmpMax = 15.0 +SCGUISpeedometer.kWiggleAngleAmpMax = 0.2 + +-- 135 degrees either side. +SCGUISpeedometer.kRotationMin = -math.pi * 0.75 +SCGUISpeedometer.kRotationMax = math.pi * 0.75 + +local function Wiggle(val, amp, t) + + return math.sin(21 * t) * math.sin(19 * t) * math.sin(17 * t) * math.sin(11 * t) * amp + val + +end + +local function WiggleVector2d(val, amp, t) + + return Vector(Wiggle(val.x, amp, t), Wiggle(val.y, amp, t+5), 0) + +end + +function SCGUISpeedometer:Initialize() + + self.needleItem = GUI.CreateItem() + self.frameItem = GUI.CreateItem() + self.frameLitItem = GUI.CreateItem() + self.centerItem = GUI.CreateItem() + self.centerLitItem = GUI.CreateItem() + + self.needleItem:SetTexture(self.kNeedleTexture) + self.frameItem:SetTexture(self.kFrameTexture) + self.frameLitItem:SetTexture(self.kFrameLitTexture) + self.centerItem:SetTexture(self.kCenterTexture) + self.centerLitItem:SetTexture(self.kCenterLitTexture) + + self.kNeedleSize = Vector(self.needleItem:GetTextureWidth(), self.needleItem:GetTextureHeight(), 0) + self.kFrameSize = Vector(self.frameItem:GetTextureWidth(), self.frameItem:GetTextureHeight(), 0) + self.kFrameLitSize = Vector(self.frameLitItem:GetTextureWidth(), self.frameLitItem:GetTextureHeight(), 0) + self.kCenterSize = Vector(self.centerItem:GetTextureWidth(), self.centerItem:GetTextureHeight(), 0) + self.kCenterLitSize = Vector(self.centerLitItem:GetTextureWidth(), self.centerLitItem:GetTextureHeight(), 0) + + self:SetLayer(kDefaultLayer) + self:SetLightIntensity(0.0) + + self.smoothedSpeed = 0.0 + self.shakeTime = 0.0 + self.needleShakeTime = 0.0 + self.fastSpeed = 0.0 + self.updateInterval = 0 + + self:PositionOnScreen() + +end + +function SCGUISpeedometer:Uninitialize() + + GUI.DestroyItem(self.needleItem) + GUI.DestroyItem(self.frameItem) + GUI.DestroyItem(self.frameLitItem) + GUI.DestroyItem(self.centerItem) + GUI.DestroyItem(self.centerLitItem) + +end + +function SCGUISpeedometer:SetLayer(layer) + + self.frameItem:SetLayer(layer) + self.frameLitItem:SetLayer(layer + 1) + self.needleItem:SetLayer(layer + 2) + self.centerItem:SetLayer(layer + 3) + self.centerLitItem:SetLayer(layer + 4) + +end + +function SCGUISpeedometer:SetLightIntensity(i) + + self.frameLitItem:SetColor(Color(1,1,1,i)) + self.centerLitItem:SetColor(Color(1,1,1,i)) + +end + +function SCGUISpeedometer:OnResolutionChanged() + + self:PositionOnScreen() + +end + +function SCGUISpeedometer:UpdateSpeedReading(deltaTime) + + -- Smooth out the needle's rotation. + self.speedActual = GetSkulkChallenge():GetCurrentSpeed() + local interpVal = 1.0 - math.pow(self.kNeedleSmoothFactor, deltaTime) + self.smoothedSpeed = self.smoothedSpeed * (1.0 - interpVal) + self.speedActual * interpVal + + -- Accumulate the amount of time the player is at or above a "fast" speed. Count up until a set number of + -- seconds have accumulated. Certain effects on the speedometer will occur at "fast" and "super fast" speeds. + -- If the player drops below the required speed, they have a little bit of leeway to get back up to the speed + -- before the effects disappear. + -- Fast speed + if self.speedActual >= self.kSpeedThresholdFast then + self.fastSpeed = math.min(self.fastSpeed + deltaTime, self.kSpeedMaxDuration) + else + self.fastSpeed = math.max(self.fastSpeed - deltaTime, 0.0) + end + +end + +function SCGUISpeedometer:UpdateNeedleRotation(deltaTime) + + self:UpdateSpeedReading(deltaTime) + + -- Wiggle needle if going really fast. + self.needleShakeTime = self.needleShakeTime + (self.smoothedSpeed / self.kSpeedThresholdSuperFast) * deltaTime + local needleAngle = (1.0 - Clamp((self.smoothedSpeed / self.kSpeedThresholdSuperFast), 0, 1)) * (self.kRotationMax - self.kRotationMin) + self.kRotationMin + if self.fastSpeed > 0.0 then + needleAngle = Wiggle(needleAngle, (self.fastSpeed / self.kSpeedMaxDuration) * self.kWiggleAngleAmpMax, self.needleShakeTime) + end + + self.needleItem:SetRotation(Vector(0,0,needleAngle)) + +end + +function SCGUISpeedometer:UpdateAnimation(deltaTime, forceUpdateTransform) + + -- Rotate speedometer needle based on current speed + self:UpdateNeedleRotation(deltaTime) + + -- Adjust lighting of speedometer based on current speed. + local lightLevel = Clamp(self.speedActual / self.kSpeedThresholdSuperFast, 0, 1) + lightLevel = lightLevel * lightLevel + + -- Shake speedometer and flicker lights when traveling really fast + local transformChanged = false + self.shakeTime = self.shakeTime + (self.smoothedSpeed / self.kSpeedThresholdSuperFast) * deltaTime + if self.fastSpeed > 0.0 then + self.animatedPosition = WiggleVector2d(self.position, self.kWiggleAmpMax * self.scale, self.shakeTime) + local flickerIntensity = self.fastSpeed / self.kSpeedMaxDuration + lightLevel = Wiggle(lightLevel, flickerIntensity , self.shakeTime * 2.0) + transformChanged = true + end + + self:SetLightIntensity(Clamp(lightLevel, 0, 1)) + + if transformChanged or forceUpdateTransform then + self:UpdateTransform(deltaTime) + end + +end + +function SCGUISpeedometer:UpdateTransform(deltaTime) + + local pos = self.animatedPosition or self.position + local scale = self.scale + + self.frameItem:SetSize(self.kFrameSize * scale) + self.frameItem:SetPosition(self.kFrameOffset * scale + pos) + + self.frameLitItem:SetSize(self.kFrameLitSize * scale) + self.frameLitItem:SetPosition(self.kFrameLitOffset * scale + pos) + + self.needleItem:SetSize(self.kNeedleSize * scale) + self.needleItem:SetPosition(self.kNeedleOffset * scale + pos) + self.needleItem:SetRotationOffset(self.kNeedleRotationOffset * scale) + + self.centerItem:SetSize(self.kCenterSize * scale) + self.centerItem:SetPosition(self.kCenterOffset * scale + pos) + + self.centerLitItem:SetSize(self.kCenterLitSize * scale) + self.centerLitItem:SetPosition(self.kCenterLitOffset * scale + pos) + +end + +function SCGUISpeedometer:Update(deltaTime) + + self:UpdateAnimation(deltaTime) + +end + +function SCGUISpeedometer:SetIsVisible(state) + + self.frameItem:SetIsVisible(state) + self.frameLitItem:SetIsVisible(state) + self.needleItem:SetIsVisible(state) + self.centerItem:SetIsVisible(state) + self.centerLitItem:SetIsVisible(state) + +end + +function SCGUISpeedometer:PositionOnScreen() + + self.position = Vector(Client.GetScreenWidth() * 0.5, Client.GetScreenHeight() * self.kVerticalPositionFraction, 0) + self.scale = (Client.GetScreenHeight() / 1080.0) * self.kBaseScaleFactor + + self:UpdateAnimation(0, true) + +end + diff --git a/challenges/skulk_challenge/lua/SCGUISplatScreen.lua b/challenges/skulk_challenge/lua/SCGUISplatScreen.lua new file mode 100644 index 000000000..667f6ef98 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUISplatScreen.lua @@ -0,0 +1,472 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUISplatScreen.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- The graphics and animation for the "GO", "FINISH", and "FAILURE!" screens in skulk challenge. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/menu/FancyUtilities.lua") + +local kDefaultLayer = 40 + +class 'SCGUISplatScreen' (GUIScript) + +SCGUISplatScreen.textures = +{ + go = + { + path = PrecacheAsset("ui/challenge/skulk_challenge/splat_go.dds"), + size = Vector(644, 467, 0), -- desired size on a 1920x1080 screen. Actual texture size is bigger. + offset = Vector(-600, -234, 0), + }, + + finish = + { + path = PrecacheAsset("ui/challenge/skulk_challenge/splat_finish.dds"), + size = Vector(590, 525, 0), -- desired size on a 1920x1080 screen. Actual texture size is bigger. + offset = Vector(-510, -294, 0), -- offset required to transform from center-right position on graphic to upper-left corner of graphic. + }, + + failure = + { + path = PrecacheAsset("ui/challenge/skulk_challenge/splat_failure.dds"), + size = Vector(590, 523, 0), + offset = Vector(-500, -295, 0), + }, +} + +SCGUISplatScreen.kCenterXSpacing = 50 -- spacing at 1920x1080 scale between graphic/text and center of screen. + +-- bounce around a little +local function Bounce(amp, freq, decay, v0, t) + + return v0 * amp * math.sin(freq*t*2*math.pi)/math.exp(decay*t) + +end + +local function Zoom(t) + + return 2.71828182846 ^ (10*(t-1)) + +end + +local function Animation_ScaleUp(self, anim) + + local key = 0.0833333 -- (~5 frames at 60fps) + local scale + if anim.time <= key then + + -- from frame 0 to 5 (60fps), do linear scale from 2 to 1 on x axis, and 0 to 1 on y axis. + local t = math.min(math.max(0, anim.time) / key, 1) + scale = t + + else + + -- bounce into place. + local t = anim.time - key + scale = 1 + + Bounce( 0.05, -- amplitude + 4.0, -- frequency + 8.0, -- decay + 12.0, -- starting velocity + t -- time + ) + + end + + self.animScale = self.animScale * scale + +end + +local function Animation_GoScaleUp(self, anim) + + local key = 0.0833333 -- (~5 frames at 60fps) + local scale + if anim.time <= key then + + -- from frame 0 to 5 (60fps), do linear scale from 2 to 1 on x axis, and 0 to 1 on y axis. + local t = math.min(math.max(0, anim.time) / key, 1) + scale = Vector(2,0,0) * (1-t) + Vector(1,1,0) * t + + else + + -- bounce into place. + local t = anim.time - key + scale = Vector(1,1,0) + + Bounce( 0.05, -- amplitude + 4.0, -- frequency + 8.0, -- decay + Vector(-12, 12, 0), -- starting velocity + t -- time + ) + + end + + self.animScale = self.animScale * scale + +end + +local function Animation_ZoomForward(self, anim) + + local scaleTarget = 15 + local duration = 0.5 + + local scale = (Zoom(anim.time / duration) * (scaleTarget - 1)) + 1 + + self.animScale = self.animScale * scale + +end + +function SCGUISplatScreen:UpdateTransform() + + local margin = Vector(self.kCenterXSpacing * self.animScale.x, 0, 0) + self.text:SetPosition(self.animPos + margin) + self.text:SetScale(self.animScale) + + if not self.graphicData then + return + end + self.graphic:SetPosition((self.animPos - margin) + (self.graphicData.offset * self.animScale)) + self.graphic:SetSize(self.graphicData.size * self.animScale) + +end + +function SCGUISplatScreen:SetLayer(layer) + + self.graphic:SetLayer(layer) + self.text:SetLayer(layer) + +end + +function SCGUISplatScreen:SetText(text) + + self.text:SetText(text) + +end + +function SCGUISplatScreen:SetColor(color) + + self.text:SetColor(color) + +end + +function SCGUISplatScreen:SetTextScaleFactor(scalar) + + self.textScaleFactor = scalar + self:UpdateTransform() + +end + +function SCGUISplatScreen:SetGraphic(graphicName) + + local graphicData = self.textures[graphicName] + if not graphicData then + error("No splat graphic data found named '%s'!", graphicName) + return + end + + self.graphic:SetTexture(graphicData.path) + self.graphicData = graphicData + + self:UpdateTransform() + +end + +function SCGUISplatScreen:Initialize() + + self.graphic = GUI.CreateItem() + self.text = GetGUIManager():CreateGUIScript("SCGUISplatScreenText") + + self:SetLayer(kDefaultLayer) + self.position = Vector(0,0,0) + self.scale = Vector(1,1,1) + + self.updateInterval = 0 + + self.anims = {} + self.animNames = {} + self.animPos = Vector(self.position) + self.animScale = Vector(self.scale) + + self:CenterOnScreen() + +end + +function SCGUISplatScreen:Uninitialize() + + GUI.DestroyItem(self.graphic) + GetGUIManager():DestroyGUIScript(self.text) + +end + +function SCGUISplatScreen:CenterOnScreen() + + -- Mockup is for 1920x1080. We want to position elements here relative to center of screen. + self.position, self.scale = Fancy_Transform(Vector(960,540,0), Vector(1,1,1)) + self:UpdateTransform() + +end + +function SCGUISplatScreen:OnResolutionChanged() + + self:CenterOnScreen() + +end + +function SCGUISplatScreen:UpdateAnimations(deltaTime) + + if #self.animNames == 0 then + return -- no animations to update. + end + + self.animPos = Vector(self.position) + self.animScale = Vector(self.scale) + + for i=1, #self.animNames do + local animation = self.anims[self.animNames[i]] + animation.time = animation.time + deltaTime + animation.func(self, animation) + end + + self:UpdateTransform() + +end + +function SCGUISplatScreen:Discard(destructionDelay) + + self.destroyTime = Shared.GetTime() + destructionDelay + +end + +function SCGUISplatScreen:Update(deltaTime) + + self:UpdateAnimations(deltaTime) + + if self.destroyTime and Shared.GetTime() >= self.destroyTime then + GetGUIManager():DestroyGUIScript(self) + end + +end + +function SCGUISplatScreen:AddAnimationLayer(name, func, startingTimeOffset) + + local newAnim = {} + newAnim.name = name + newAnim.time = startingTimeOffset or 0.0 + newAnim.func = func + + if self.anims[name] == nil then + table.insert(self.animNames, name) + end + self.anims[name] = newAnim + +end + +function SCGUISplatScreen:AnimateIn(name) + + name = name or "regular" + if name == "go" then + + self:AddAnimationLayer("animateIn", Animation_GoScaleUp, 0.0) + + elseif name == "regular" then + + self:AddAnimationLayer("animateIn", Animation_ScaleUp, 0.0) + + end + +end + +function SCGUISplatScreen:AnimateOut() + + self:AddAnimationLayer("animateOut", Animation_ZoomForward, 0.0) + self:Discard(1.0) + +end + +-- multi-line text display. +class 'SCGUISplatScreenText' (GUIScript) + +SCGUISplatScreenText.kFontName = Fonts.kMicrogrammaDBolExt_Huge + +SCGUISplatScreenText.kShadowOffset = Vector(7, 7, 0) +SCGUISplatScreenText.kShadowColor = Color(0,0,0,0.75) + +SCGUISplatScreenText.kTextMaxWidth = 600 -- maximum width of a line of text. +SCGUISplatScreenText.kTextMaxHeight = 138 -- maximum height of a character at 1920x1080 scale, before animation. +SCGUISplatScreenText.kFontNormalHeight = 51 -- height of the 'O' character in Microgramma Bold huge. +SCGUISplatScreenText.kLineGap = 27 -- space between the bottom of one character and the top of another. +SCGUISplatScreenText.kIdealTextBlockHeight = 220 -- desired height of the entire block of text. + +SCGUISplatScreenText.kTextShader = PrecacheAsset("shaders/GUICrispyText.surface_shader") + +function SCGUISplatScreenText:UpdateTransform() + + if #self.textLines == 0 then + return + end + + local scale = self.textFitScaleFactor * self.scale + local shadowOffset = self.kShadowOffset * self.scale + + local lineOffset = Vector(0, (self.kLineGap * self.scale.y) + (self.kFontNormalHeight * scale.y), 0) + local startOffset = (#self.textLines - 1) * -0.5 * lineOffset + + -- Font was not designed to be as big as it's going to be. Configure the "crispy" shader to try to recover + -- some of this sharpness. + local minScale = math.min(scale.x, scale.y) + local crispy + local bias = 0.75 -- allow it to be slightly blurred by uprez, to avoid some aliasing/over-sharpening. + if minScale > (1.0 + bias) then + crispy = 2.0 ^ (minScale-bias) + else + crispy = 1.0 + end + + for i=1, #self.textLines do + local pos = self.position + startOffset + (lineOffset * (i-1)) + self.textLines[i].text:SetPosition(pos) + self.textLines[i].shadow:SetPosition(pos + shadowOffset) + self.textLines[i].text:SetScale(scale) + self.textLines[i].shadow:SetScale(scale) + self.textLines[i].text:SetFloatParameter("crispy", crispy) + self.textLines[i].shadow:SetFloatParameter("crispy", crispy) + end + +end + +function SCGUISplatScreenText:SetPosition(pos) + + self.position = pos + self:UpdateTransform() + +end + +function SCGUISplatScreenText:SetScale(scale) + + self.scale = scale + self:UpdateTransform() + +end + +function SCGUISplatScreenText:SetPositionAndScale(pos, scale) + + self.position = pos + self.scale = scale + self:UpdateTransform() + +end + +function SCGUISplatScreenText:SetLayer(layer) + + for i=1, #self.textLines do + self.textLines[i].text:SetLayer(layer + 1) + self.textLines[i].shadow:SetLayer(layer) + end + + self.layer = layer + +end + +function SCGUISplatScreenText:Initialize() + + self.position = Vector(0,0,0) + self.scale = Vector(1,1,0) + self.color = Color(1,1,1,1) + self.textFitScaleFactor = 1.0 + self.layer = kDefaultLayer + + self.updateInterval = 1 + + self.textLines = {} + +end + +function SCGUISplatScreenText:Uninitialize() + + self:ClearText() + +end + +function SCGUISplatScreenText:ClearText() + + for i=1, #self.textLines do + GUI.DestroyItem(self.textLines[i].text) + GUI.DestroyItem(self.textLines[i].shadow) + end + self.textLines = {} + +end + +function SCGUISplatScreenText:SetColor(color) + + self.color = color + for i=1, #self.textLines do + self.textLines[i].text:SetColor(color) + end + +end + +function SCGUISplatScreenText:SetText(text) + + self:ClearText() + + local lines = Fancy_SplitStringIntoTable(text, "\n") + if #lines == 0 then + return + end + + -- Create text and shadow items for each line to be displayed. + for i=1, #lines do + local newLine = {} + newLine.text = GUI.CreateItem() + newLine.text:SetOptionFlag(GUIItem.ManageRender) + newLine.text:SetFontName(self.kFontName) + newLine.text:SetColor(self.color) + newLine.text:SetText(lines[i]) + newLine.text:SetTextAlignmentX(GUIItem.Align_Min) + newLine.text:SetTextAlignmentY(GUIItem.Align_Center) + newLine.text:SetLayer(self.layer + 1) + newLine.text:SetShader(self.kTextShader) + + newLine.shadow = GUI.CreateItem() + newLine.shadow:SetOptionFlag(GUIItem.ManageRender) + newLine.shadow:SetFontName(self.kFontName) + newLine.shadow:SetColor(self.kShadowColor) + newLine.shadow:SetText(lines[i]) + newLine.shadow:SetTextAlignmentX(GUIItem.Align_Min) + newLine.shadow:SetTextAlignmentY(GUIItem.Align_Center) + newLine.shadow:SetLayer(self.layer) + newLine.shadow:SetShader(self.kTextShader) + + self.textLines[i] = newLine + end + + -- Resize the text as big as possible, to a maximum scale. + -- It is assumed that the parent script will handle scaling for different resolutions, so for our calculations here, + -- we'll just assume the mockup resolution (1920x1080) + + -- Whole block should be a certain height... + local lineHeight = (self.kIdealTextBlockHeight - ((#lines - 1) * self.kLineGap)) / #lines + + -- But ensure characters don't get too big... + lineHeight = math.min(lineHeight, self.kTextMaxHeight) + + -- Calculate longest line. + local maxLineWidth = 0 + for i=1, #self.textLines do + maxLineWidth = math.max(maxLineWidth, self.textLines[i].text:GetTextWidth(self.textLines[i].text:GetText())) + end + + -- adjust maximum line width to take into account scaling due to line height + maxLineWidth = maxLineWidth * (lineHeight / self.kFontNormalHeight) + + -- Ensure longest line isn't too long. Scale everything else down to make this one fit. + self.textFitScaleFactor = (lineHeight / self.kFontNormalHeight) * (math.min(maxLineWidth, self.kTextMaxWidth) / maxLineWidth) + + self:UpdateTransform() + +end diff --git a/challenges/skulk_challenge/lua/SCGUIStartLight.lua b/challenges/skulk_challenge/lua/SCGUIStartLight.lua new file mode 100644 index 000000000..af33e9b0c --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIStartLight.lua @@ -0,0 +1,255 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIStartLight.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Single light for the starting light array. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +class 'SCGUIStartLight' (GUIScript) + +SCGUIStartLight.kTexture = PrecacheAsset("ui/challenge/skulk_challenge/start_light.dds") +SCGUIStartLight.kShader = "shaders/GUIWavyHueShift.surface_shader" +SCGUIStartLight.kDefaultSize = Vector(256, 256, 0) + +SCGUIStartLight.kUnlitColor = Color(0.4, 0.4, 0.4, 1.0) +SCGUIStartLight.kUnlitScale = Vector(0.667, 0.667, 0) + +SCGUIStartLight.kOffScreenOffset = Vector(0, -600, 0) + +local kDefaultLayer = 40 + +SCGUIStartLight.kColorsByName = +{ + ["green"] = 117.0 / 360.0; + ["yellow"] = 36.0 / 360.0; + ["red"] = 0.0; +} + +local function Lerp(a, b, t) + + return a * (1.0 - t) + b * t + +end + +local function jiggleIn(t) + local v = 1.2 + local q = 4 + local d = 3.0 + local s = 340 + local e = 2.71828182846 + if t <= 0 then + return 0 + end + local c = 1 - 1 / (s * t + 1) + return c + ((v * math.sin(q * t * math.pi)) / (e^(d*t))) +end + +-- eases in, with f(0)=1, f(1)=0 +local function InterpIn(t) + if t <= 0 then + return 1 + end + return (1-t) / (2^(5*t)) +end + +-- accelerates out, with f(0)=0, f(1)=1 +local function InterpOut(t) + if t <= 0 then + return 0 + end + return t / (2^((t * -5) + 5)) +end + +function SCGUIStartLight:UpdateLayers() + + self.item:SetLayer(self.layer) + +end + +function SCGUIStartLight:UpdateTransform() + + self.size = self.kDefaultSize * self.animScale + + self.item:SetPosition(self.animPos - (self.size * 0.5)) + self.item:SetSize(self.size) + +end + +function SCGUIStartLight:Initialize() + + self.item = GUI.CreateItem() + self.item:SetShader(self.kShader) + self.item:SetTexture(self.kTexture) + + self.position = Vector(0,0,0) + self.scale = Vector(1,1,1) + self.layer = kDefaultLayer + + self.anims = {} + self.animNames = {} + self.animPos = Vector(self.position) + self.animScale = Vector(self.scale) + + self.item:SetFloatParameter("timeOffset", math.random() * 5.0) + local angle = math.random() * 2.0 * math.pi + self.item:SetRotation(Vector(0,0,angle)) + + -- add an animation that shifts the light off-screen. + self:AddAnimation("flyIn", + function(self, anim) + self.animPos = self.animPos + self.kOffScreenOffset + return true -- animation persists until it is removed by another animation. + end) + + -- add an animation that makes the light smaller while it is unlit. + self:AddAnimation("light", + function(self, anim) + self.animScale = self.animScale * self.kUnlitScale + self.item:SetColor(self.kUnlitColor) + return true + end) + + self:Update(0) + + self.updateInterval = 0 + +end + +function SCGUIStartLight:AddAnimation(name, func, timeOffset) + + local newAnim = {} + newAnim.name = name + newAnim.time = timeOffset or 0.0 + newAnim.func = func + + self.anims[name] = newAnim + table.insert(self.animNames, name) + +end + +function SCGUIStartLight:Uninitialize() + + GUI.DestroyItem(self.item) + +end + +function SCGUIStartLight:SetHueOffset(offset) + + self.item:SetFloatParameter("hueOffset", offset) + +end + +function SCGUIStartLight:SetColor(colorName) + + self:SetHueOffset(self.kColorsByName[colorName] or 0.0) + +end + +-- Immediately changes the state of the light, without animation. +function SCGUIStartLight:SetIsLit(state) + + if state == true then + self:AddAnimation("light", + function(self, anim) + self.item:SetColor(Color(1,1,1,1)) + return true + end) + else + self:AddAnimation("light", + function(self, anim) + self.animScale = self.animScale * self.kUnlitScale + self.item:SetColor(self.kUnlitColor) + return true + end) + end + +end + +--- animates the light changing from unlit -> lit +function SCGUIStartLight:Light(timeOffset) + + self:AddAnimation("light", + function(self, anim) + local t = jiggleIn(anim.time) + self.animScale = self.animScale * Lerp(self.kUnlitScale, Vector(1,1,1), t) + self.item:SetColor(Lerp(self.kUnlitColor, Color(1,1,1,1), t)) + return true -- animation persists until it is removed by another animation. + end, timeOffset) + +end + +function SCGUIStartLight:FlyIn(timeOffset) + + self:AddAnimation("flyIn", + function(self, anim) + local t = InterpIn(anim.time) + self.animPos = self.animPos + self.kOffScreenOffset * t + return true + end, timeOffset) + +end + +function SCGUIStartLight:FlyOut(timeOffset) + + self:AddAnimation("flyOut", + function(self, anim) + local t = InterpOut(anim.time) + self.animPos = self.animPos + self.kOffScreenOffset * t + return true + end, timeOffset) + +end + +function SCGUIStartLight:SetPosition(pos) + + self.position = pos + self:UpdateTransform() + +end + +function SCGUIStartLight:SetScale(scale) + + self.scale = scale + self:UpdateTransform() + +end + +function SCGUIStartLight:SetLayer(layer) + + self.layer = layer + self:UpdateLayers() + +end + +function SCGUIStartLight:UpdateAnimations(deltaTime) + + self.animPos = Vector(self.position) + self.animScale = Vector(self.scale) + + local i = 1 + while i <= #self.animNames do + local anim = self.anims[self.animNames[i]] + anim.time = anim.time + deltaTime + if anim.func(self, anim) then + i = i + 1 + else + table.remove(self.animNames, i) + self.anims[anim.name] = nil + end + end + + self:UpdateTransform() + +end + +function SCGUIStartLight:Update(deltaTime) + + self:UpdateAnimations(deltaTime) + +end + + + diff --git a/challenges/skulk_challenge/lua/SCGUITechMap.lua b/challenges/skulk_challenge/lua/SCGUITechMap.lua new file mode 100644 index 000000000..7aa2ea86c --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUITechMap.lua @@ -0,0 +1,12 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUITechMap.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Disable the tech map, it's not needed. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +function GUITechMap:SendKeyEvent(key, down) +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGUITips.lua b/challenges/skulk_challenge/lua/SCGUITips.lua new file mode 100644 index 000000000..8102074bd --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUITips.lua @@ -0,0 +1,312 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ======= +-- +-- lua\SCGUITips.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Displays tip text in the middle of the screen, because otherwise they will never see it. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/menu/FancyUtilities.lua") + +local kDefaultLayer = 40 + +local tipsScript + +function GetTipsGUIScript() + + if not tipsScript then + tipsScript = GetGUIManager():CreateGUIScript("SCGUITips") + end + + return tipsScript + +end + +class 'SCGUITips' (GUIScript) + +SCGUITips.kShadowColor = Color(0,0,0,0.5) +SCGUITips.kShadowOffset = Vector(3,3,0) + +SCGUITips.kFadeTime = 1.0 + +SCGUITips.kFontName = Fonts.kAgencyFB_Large +SCGUITips.kFontDesiredSize = 37 +SCGUITips.kFontActualSize = 28 +SCGUITips.kLineOffset = 48 -- space between top of one line and top of adjacent line. + +SCGUITips.kTextColor = HexToColor("db9d22") + +-- returns the amount of extra time the tip is being used to be faded-in and out. +function SCGUITips:GetExtraTime() + + return self.kFadeTime * 2.0 + +end + +function SCGUITips:UpdateTransform() + + if #self.lineItems == 0 then + return + end + + local startYOffset = -(#self.lineItems - 1) * 0.5 * self.kLineOffset + local scale = self.scale * (self.kFontDesiredSize / self.kFontActualSize) + local scaleVec = Vector(scale, scale, 0) + + for i=1, #self.lineItems do + local index = i-1 + self.lineItems[i]:SetPosition(Vector(0, startYOffset + index * self.kLineOffset, 0) * self.scale + self.position) + self.lineItems[i]:SetScale(scaleVec) + end + +end + +function SCGUITips:UpdateColor() + + if #self.lineItems == 0 then + return + end + + local color = Color(self.kTextColor) + color.a = color.a * self.opacity + + for i=1, #self.lineItems do + self.lineItems[i]:SetColor(color) + end + +end + +function SCGUITips:Initialize() + + self.updateInterval = 0.0333333 -- ~30fps + + self.opacity = 0.0 + self.opacityTarget = 0.0 + self.allowedVisible = true + + self.lineItems = {} + self.queuedText = nil + + self:SetLayer(kDefaultLayer) + self:CenterOnScreen() + +end + +function SCGUITips:CreateTextItem(text) + + local newItem = GUI.CreateItem() + newItem:SetOptionFlag(GUIItem.ManageRender) + newItem:SetFontName(self.kFontName) + newItem:SetText(text) + newItem:SetTextAlignmentX(GUIItem.Align_Center) + newItem:SetTextAlignmentY(GUIItem.Align_Center) + + return newItem + +end + +function SCGUITips:CreateLine(text) + + local newLine = {} + newLine.text = self:CreateTextItem(text) + newLine.shadow = self:CreateTextItem(text) + + newLine.SetColor = function(line, color) + line.text:SetColor(color) + local shadowColor = Color(self.kShadowColor) + shadowColor.a = shadowColor.a * color.a + line.shadow:SetColor(shadowColor) + end + + newLine.SetPosition = function(line, pos) + line.text:SetPosition(pos) + line.shadow:SetPosition(pos + self.kShadowOffset * self.scale) + end + + newLine.SetScale = function(line, scale) + line.text:SetScale(scale) + line.shadow:SetScale(scale) + end + + newLine.SetLayer = function(line, layer) + line.text:SetLayer(layer+1) + line.shadow:SetLayer(layer) + end + + newLine:SetLayer(self.layer) + + return newLine + +end + +function SCGUITips:DestroyLine(line) + + GUI.DestroyItem(line.text) + GUI.DestroyItem(line.shadow) + +end + +function SCGUITips:ClearText() + + for i=1, #self.lineItems do + self:DestroyLine(self.lineItems[i]) + end + + self.lineItems = {} + +end + +function SCGUITips:SetText(text) + + self:ClearText() + if not text or #text == 0 then + return + end + + local splitText = Fancy_SplitStringIntoTable(text, "\n") + for i=1, #splitText do + self.lineItems[#self.lineItems+1] = self:CreateLine(splitText[i]) + end + + self:UpdateColor() + self:UpdateTransform() + +end + +function SCGUITips:SetLayer(layer) + + self.layer = layer + for i=1, #self.lineItems do + self.lineItems[i]:SetLayer(layer) + end + +end + +function SCGUITips:Uninitialize() + + self:ClearText() + self.queuedText = nil + +end + +function SCGUITips:SetOpacityTarget(opacity) + + self.opacityTarget = opacity + +end + +function SCGUITips:CenterOnScreen() + + self.position = Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0) * 0.5 + self.scale = Client.GetScreenHeight() / 1080.0 + self:UpdateTransform() + +end + +function SCGUITips:OnResolutionChanged() + + self:CenterOnScreen() + +end + +function SCGUITips:UpdateOpacity(deltaTime) + + local realTarget = self.opacityTarget + if not self.allowedVisible then + -- allowed visible acts as an override, forcing it to be invisible, even if it was displaying a tip. + realTarget = 0.0 + end + + if self.opacity == realTarget then + return + end + + local diff = math.abs(self.opacity - realTarget) + if diff <= self.kFadeTime * deltaTime then + self.opacity = realTarget + self:UpdateColor() + return + end + + if self.opacity < realTarget then + self.opacity = self.opacity + self.kFadeTime * deltaTime + else + self.opacity = self.opacity - self.kFadeTime * deltaTime + end + self:UpdateColor() + +end + +function SCGUITips:UpdateCurrentMessage(deltaTime) + + if self.currentDuration == nil then + -- no tip is currently being displayed. + return + end + + if self.currentDuration <= 0.0 then + self:SetOpacityTarget(0.0) -- fade out + + if self.opacity <= 0.0 then + self.currentDuration = nil + end + else + self.currentDuration = self.currentDuration - deltaTime + end + +end + +function SCGUITips:UpdateQueuedMessage(deltaTime) + + if self.queuedText == nil then + -- nothing is queued up + return + end + + if self.currentDuration ~= nil then + -- message is still being displayed + return + end + + -- display queued message. + assert(self.opacity == 0.0) + self:SetText(self.queuedText) + self.currentDuration = self.queuedTipDuration + self:SetOpacityTarget(1.0) + + GetTipsManager():OnTipUsed() + + self.queuedText = nil + +end + +function SCGUITips:Update(deltaTime) + + self:UpdateOpacity(deltaTime) + self:UpdateCurrentMessage(deltaTime) + self:UpdateQueuedMessage(deltaTime) + +end + +-- To allow text to be hidden when it doesn't make sense to show it (eg when the race isn't running.) +function SCGUITips:SetIsAllowedVisible(state) + + self.allowedVisible = state + + -- clear old queued tip that is possibly no longer relevant. + self.queuedText = nil + +end + +-- Queues up the given text to be displayed as a tip for the given length of time. The script will automatically +-- fade out any text already being displayed when it has been displayed for long enough, and then fade up with this +-- new text. +function SCGUITips:DisplayTip(text, duration) + + self.queuedText = text + self.queuedTipDuration = duration + self.kFadeTime -- don't start counting until it's completely faded-in. + +end + diff --git a/challenges/skulk_challenge/lua/SCGUIUnitStatus.lua b/challenges/skulk_challenge/lua/SCGUIUnitStatus.lua new file mode 100644 index 000000000..94a211842 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGUIUnitStatus.lua @@ -0,0 +1,34 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGUIUnitStatus.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Hide nameplates. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +function GUIUnitStatus:Initialize() +end + +function GUIUnitStatus:SetIsVisible(state) +end + +function GUIUnitStatus:GetIsVisible() + return false +end + +function GUIUnitStatus:Uninitialize() +end + +function GUIUnitStatus:OnResolutionChanged(oldX, oldY, newX, newY) +end + +function GUIUnitStatus:EnableMarineStyle() +end + +function GUIUnitStatus:EnableAlienStyle() +end + +function GUIUnitStatus:Update(deltaTime) +end diff --git a/challenges/skulk_challenge/lua/SCGamerules.lua b/challenges/skulk_challenge/lua/SCGamerules.lua new file mode 100644 index 000000000..6d4126801 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGamerules.lua @@ -0,0 +1,47 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGamerules.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Overrides the behavior of many aspects of NS2 gamerules to make it work as a singleplayer +-- Skulk time-trial mode. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/Gamerules.lua") +Script.Load("lua/NS2Gamerules.lua") + +class 'SCGamerules' (NS2Gamerules) + +SCGamerules.kMapName = "skulk_challenge_gamerules" + +if Server then + + function SCGamerules:OnCreate() + NS2Gamerules.OnCreate(self) + end + + function NS2Gamerules:GetWarmUpPlayerLimit() + return 0 + end + + function SCGamerules:OnClientConnect(client) + + Gamerules.OnClientConnect(self, client) + + -- Keep track of the flesh-and-blood client's player. The first call to GetSkulkChallenge() also + -- creates it. + GetSkulkChallenge():OnClientConnected(client) + + end + + function SCGamerules:ResetGame() + end + + function SCGamerules:UpdateWarmUp() + end + +end + +Shared.LinkClassToMap("SCGamerules", SCGamerules.kMapName, {}) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGlobals.lua b/challenges/skulk_challenge/lua/SCGlobals.lua new file mode 100644 index 000000000..857b42b6c --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGlobals.lua @@ -0,0 +1,52 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGlobals.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +kSkulkChallengeFadeDuration = 1.0 +kSkulkChallengeDimDuration = 1.0 +kSkulkChallengePreCountdownDuration = 3.0 +kSkulkChallengeCountdownDuration = 3.0 +kSkulkChallengeTipStartTime = 6.0 -- time after race starts when tips can start appearing. + +kSkulkChallengePostStartEffectsLingerDuration = 5.0 +kSkulkChallengeEffectDissipationDuration = 3.0 + +kSkulkChallengeMoveRate = 26.0 +-- 4 minutes and some change. +kSkulkChallengeMaxRecordTime = (4 * 60) + kSkulkChallengePreCountdownDuration + kSkulkChallengeCountdownDuration + +kSkulkChallengeEndSequenceCameraLerpFactor = 0.9 -- 0..1, lower = faster +kSkulkChallengeEndSequenceDuration = 5.0 +kSkulkChallengeEndSequenceDoorCloseDelay = 2.0 +kSkulkChallengeDoorSoundOffset = Vector(0,2,0) -- play the door open/close sound 2 meters above the door's origin. +kSkulkChallengeDoorOpenSound = PrecacheAsset("sound/NS2.fev/skulk_challenge/door_open") +kSkulkChallengeDoorCloseSound = PrecacheAsset("sound/NS2.fev/skulk_challenge/door_close") + +-- on top of fader, so it can be seen before we fade in. +kSkulkChallengeInitialNagLayer = 90 +kSkulkChallengeFaderLayer = 80 +kSkulkChallengeNagLayer = 60 +kSkulkChallengeSplatLayer = 60 +kSkulkChallengeResultsLayer = 50 +kSkulkChallengeLeaderboardLayer = 50 +kSkulkChallengeDimmerLayer = 40 +kSkulkChallengeSpeedometerLayer = 30 + +kSkulkChallengeSplatTimeShort = 2.0 +kSkulkChallengeSplatTimeLong = 3.5 +kSkulkChallengeSplatTimeGap = 0.5 + +kSkulkChallengeFinishTextColor = HexToColor("2DFFB2") +kSkulkChallengeGoTextColor = HexToColor("ffd800") +kSkulkChallengeFailureTextColor = HexToColor("de3c29") + +kSkulkChallengeFinishMusic = "sound/NS2.fev/music/challenge_finish" +kSkulkChallengeDeathMusic = "sound/NS2.fev/music/challenge_death" +if Client then + Client.PrecacheLocalSound(kSkulkChallengeFinishMusic) + Client.PrecacheLocalSound(kSkulkChallengeDeathMusic) +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGlobals_Override.lua b/challenges/skulk_challenge/lua/SCGlobals_Override.lua new file mode 100644 index 000000000..effa5dea6 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGlobals_Override.lua @@ -0,0 +1,22 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGlobals_Override.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Overrides for contents of lua/Globals.lua. +-- +-- Added a "SkulkHologram" minimap blip type. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +kMinimapBlipType = enum( { 'Undefined', 'TechPoint', 'ResourcePoint', 'Scan', 'EtherealGate', 'HighlightWorld', + 'Sentry', 'CommandStation', + 'Extractor', 'InfantryPortal', 'Armory', 'AdvancedArmory', 'PhaseGate', 'Observatory', + 'RoboticsFactory', 'ArmsLab', 'PrototypeLab', + 'Hive', 'Harvester', 'Hydra', 'Egg', 'Embryo', 'Crag', 'Whip', 'Shade', 'Shift', 'Shell', 'Veil', 'Spur', 'TunnelEntrance', 'BoneWall', + 'Marine', 'JetpackMarine', 'Exo', 'Skulk', 'SkulkHologram', 'Lerk', 'Onos', 'Fade', 'Gorge', + 'Door', 'PowerPoint', 'DestroyedPowerPoint', 'UnsocketedPowerPoint', + 'BlueprintPowerPoint', 'ARC', 'Drifter', 'MAC', 'Infestation', 'InfestationDying', 'MoveOrder', 'AttackOrder', 'BuildOrder', 'SensorBlip', 'SentryBattery' } ) + +kPlayerStatus = enum( { "Hidden", "Dead", "Evolving", "Embryo", "Commander", "Exo", "GrenadeLauncher", "Rifle", "HeavyMachineGun", "Shotgun", "Flamethrower", "Void", "Spectator", "Skulk", "SkulkHologram", "Gorge", "Fade", "Lerk", "Onos", "SkulkEgg", "GorgeEgg", "FadeEgg", "LerkEgg", "OnosEgg" } ) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCGuideSpline.lua b/challenges/skulk_challenge/lua/SCGuideSpline.lua new file mode 100644 index 000000000..57d41a444 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCGuideSpline.lua @@ -0,0 +1,790 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGuideSpline.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Contains all the functionality to create the spline visualization of the path. +-- Uses the Centripetal Catmull-Rom spline technique. +-- +-- Wiki: https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline +-- Graph: https://www.desmos.com/calculator/9kazaxavsf +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +local guideMeshes = nil +function GetGuideMeshes() + return guideMeshes +end + +local kGuideLineMaterial = PrecacheAsset("materials/skulk_challenge/guide_line.material") + +local kMaxSegmentLength = 5.0 -- anything longer than 5 meters will be subdivided. + +-- we divide the dynamic render mesh into sections so we don't render the whole thing at once. +local kMaxMeshLength = 40 -- maximum length of the spline each mesh chunk should represent + +local kMaxDrawDist = 70.0 -- any segments further away should be hidden. + +local kCurveMaxAnglePerVert = 5.0 -- degrees before we divide it into smaller segments. +local kMinDotProduct = math.cos(kCurveMaxAnglePerVert * (math.pi / 180.0)) + +local kSphereTraceRadius = 0.25 -- radius of sphere used to trace downwards to snap points to floor. +local kObstacleTraceRadius = 0.125 -- radius of sphere used to detect obstacles between two points. +local kObstacleDisplaceFactor = 0.125 +local kObstacleCheckDistance = 1.0 +local kObstacleCheckIterations = 8 + +local kLineWidth = 0.5 +local kTextureSizeRatio = 0.25 + +-- Returns a catmull-rom spline that can then be interpolated using CRS_Interp(). +-- Spline is 4 points, result is useful for smoothly interpolating between the middle two points. +-- For more points, multiple splines are pieced together. +function CRS_CalculateSpline( points ) + + if not points then + error("CRS_CalculateSpline takes table of 4 vectors, got nil") + return nil + end + + if #points ~= 4 then + error("CRS_CalculateSpline takes table of 4 vectors, got %s entries.", #points) + end + + -- check that every entry is a vector, and is finite + local fail = false + for i=1, #points do + if not points[i].isa or not points[i]:isa("Vector") then + error("Entry %s of table passed to CRS_CalculateSpline was not a vector!", i) + fail = true + end + end + if fail then + return nil + end + + local tVals = {0,0,0,0} + for i=2, 4 do + tVals[i] = math.pow((points[i] - points[i-1]):GetLengthSquared(), 0.25) + tVals[i-1] + end + + return { points = points, tVals = tVals } + +end + +-- Returns a point that is an interpolation between the second and third points of the given spline. +-- At t=0, P2 is returned, and at t=1, P3 is returned. +-- Note that the "tVals" stored in the spline do NOT reflect this 0-1 mapping. The 0-1 mapping is +-- a convenience. +function CRS_Interp( spline, t ) + + local t0 = spline.tVals[1] + local t1 = spline.tVals[2] + local t2 = spline.tVals[3] + local t3 = spline.tVals[4] + + local P0 = spline.points[1] + local P1 = spline.points[2] + local P2 = spline.points[3] + local P3 = spline.points[4] + + -- adjust t so it maps between P2 and P3 + local debugT = t + t = (1-t)*t1 + t*t2 + + local A1 = ((t1 - t) / (t1 - t0)) * P0 + ((t - t0) / (t1 - t0)) * P1 + local A2 = ((t2 - t) / (t2 - t1)) * P1 + ((t - t1) / (t2 - t1)) * P2 + local A3 = ((t3 - t) / (t3 - t2)) * P2 + ((t - t2) / (t3 - t2)) * P3 + + local B1 = ((t2 - t) / (t2 - t0)) * A1 + ((t - t0) / (t2 - t0)) * A2 + local B2 = ((t3 - t) / (t3 - t1)) * A2 + ((t - t1) / (t3 - t1)) * A3 + + return ((t2 - t) / (t2 - t1)) * B1 + ((t - t1) / (t2 - t1)) * B2 + +end + +-- Since the "track" might not always sit on the pathing mesh exactly, we have the capability of +-- inserting named points as-is, or pathing between them, as defined in SCMapData.lua. For example, +-- in ns2_skulk_race.level, the finish line lies in the train station of docking, where no pathing +-- mesh exists, however the bulk of the track can have points automatically generated by the pathing +-- system, so we use that as much as possible. +-- CreatePointList() creates a table of points from the start of the track to the end. These points +-- will be used to generate a smoothed path in a later step. +local function CreatePointList() + local pathPointNameData = SC_GetPathPointNameData() + if #pathPointNameData < 2 then + return nil + end + + local pointTable = {Target(pathPointNameData[1].name)} + for i=2, #pathPointNameData do + local a = pathPointNameData[i-1] + local b = pathPointNameData[i] + + if b.noMesh then + -- straight shot from a to b, ignore pathing mesh + table.insert(pointTable, Target(b.name)) + else + -- add all pathing-calculated points from a to b to the table. + local done = false + local maxIterations = 4 + local startPoint = Target(a.name) + local endPoint = Target(b.name) + while not done do + if maxIterations <= 0 then + break + end + maxIterations = maxIterations - 1 + + local points = PointArray() + Pathing.GetPathPoints(startPoint, endPoint, points) + for i=2, #points do -- skip first point, which is the same as the source point. + table.insert(pointTable, points[i]) + end + + if (points[#points] - endPoint):GetLengthXZ() <= 0.5 then + -- at the end point, done with this segment + break + else + -- sometimes the pathing system only returns a path of a certain length, + -- so multiple pathing calls are required to get the full point list. + startPoint = points[#points] + end + end + end + end + + return pointTable +end + +-- Walks along the path, creating a new path by placing a new point every kMaxSegmentLength meters +-- along the path. +local function OptimizePath(pointList) + + local newPointList = { pointList[1] } + local lengthRemaining = kMaxSegmentLength + local prevPt = pointList[1] + for i=2, #pointList do + local pt = pointList[i] + local diff = pt - prevPt + local segLength = diff:GetLength() + local vec = nil + while segLength > lengthRemaining do + vec = vec or diff / segLength + prevPt = vec * lengthRemaining + prevPt + table.insert(newPointList, prevPt) + segLength = segLength - lengthRemaining + lengthRemaining = kMaxSegmentLength + end + lengthRemaining = lengthRemaining - segLength + prevPt = pt + end + + if lengthRemaining > 0 then + table.insert(newPointList, pointList[#pointList]) + end + + return newPointList + +end + +-- points returned by pathing are floating in midair. We need to position them more closely to the +-- ground. Do a capsule trace to find the nearest surface point on the ground, without allowing us +-- to fall through tiny cracks. +local function SnapPointsToLevel(pointList) + + local newPointList = {} + for i=1, #pointList do + local startPoint = pointList[i] + Vector(0, 1, 0) + local endPoint = startPoint + Vector(0, -10, 0) + local trace = Shared.TraceCapsule(startPoint, endPoint, kSphereTraceRadius, 0.0, CollisionRep.Move, PhysicsMask.AIMovement, EntityFilterAll()) + + if trace.fraction == 1 then + table.insert(newPointList, pointList[i]) + else + local resultPoint = pointList[i] + resultPoint.y = trace.endPoint.y - kSphereTraceRadius * 0.75 + table.insert(newPointList, resultPoint) + end + end + + return newPointList + +end + +-- adds two points to this list -- one at the front of the list, and one in back. Each point is +-- given a position that is an extrapolation between the former first and second points. +-- We do this because catmull-rom splines only interpolate between the second and third points, +-- leaving the first and fourth unused. To ensure the path starts where we want, we simply add +-- two additional throwaway points to the list. However, the spline becomes undefined if we simply +-- copy the first and last points, hence the offset. +local function DilateList(pointList) + local frontPt = pointList[1] - pointList[2] + pointList[1] + local backPt = pointList[#pointList] - pointList[#pointList-1] + pointList[#pointList] + table.insert(pointList, 1, frontPt) + table.insert(pointList, backPt) + return pointList +end + +-- trace both ways between the two points, returning the mean intersection point (ie trace one way is going to +-- intersect an obstacle closer to p0 than the opposite direction trace. We return the middle of these two points, +-- or false, nil if no intersections are found. +local function CalculateObstacleFraction(startPoint, endPoint) + + local forwardTrace = Shared.TraceCapsule(startPoint, endPoint, kObstacleTraceRadius, 0.0, CollisionRep.Move, PhysicsMask.AIMovement, EntityFilterAll()) + + if forwardTrace.fraction == 1 then + -- no obstacles detected, can early exit + return false, nil + end + + local backwardTrace = Shared.TraceCapsule(endPoint, startPoint, kObstacleTraceRadius, 0.0, CollisionRep.Move, PhysicsMask.AIMovement, EntityFilterAll()) + + local avgTraceFraction = (forwardTrace.fraction + 1.0 - backwardTrace.fraction) * 0.5 + assert(avgTraceFraction <= 1.0) + assert(avgTraceFraction >= 0.0) + + return true, avgTraceFraction + +end + +local function CalculateSegmentDisplacement(pts, index) + + p0 = pts[index - 1] + p1 = pts[index] + + -- check left and right to see if we can find clearance there. + vect = (p1 - p0):GetUnit() + --local pVect = Vector(vect.z, 0, -vect.x) * kObstacleCheckDistance + local pVect = Vector(0,1,0):CrossProduct(vect):GetUnit() + + --left + if not CalculateObstacleFraction(p0 + pVect, p1 + pVect) then + return pVect + end + + --right + if not CalculateObstacleFraction(p0 - pVect, p1 - pVect) then + return -pVect + end + + -- does shifting upwards clear the obstacle? + if not CalculateObstacleFraction(p0 + Vector(0, kObstacleCheckDistance, 0), p1 + Vector(0, kObstacleCheckDistance, 0)) then + -- no obstacle above + return Vector(0, kObstacleCheckDistance, 0) + end + + -- fail + return Vector(0,0,0) + +end + +local function GetTableOfZeroVectors(count) + local newTable = {} + for i=1, count do + table.insert(newTable, Vector(0,0,0)) + end + return newTable +end + +-- Shifts points to prevent their line segments from intersecting obstacles. +local function AvoidObstacles(pointList) + + local offsets = GetTableOfZeroVectors(#pointList) + + for i=2, #pointList do -- for each segment... + local result, frac = CalculateObstacleFraction(pointList[i-1], pointList[i]) + if result then + -- obstacle was hit + local displacement = CalculateSegmentDisplacement(pointList, i) * kObstacleDisplaceFactor + offsets[i-1] = offsets[i-1] + displacement * frac + offsets[i] = offsets[i] + displacement * (1.0 - frac) + end + end + + local newPts = {} + for i=1, #pointList do + table.insert(newPts, pointList[i] + offsets[i]) + end + + return newPts + +end + +-- index = index of second position in line segment. +local function SplitSegment(segPts, spline, index) + local startFrac = segPts[index - 1].frac + local endFrac = segPts[index].frac + local newFrac = (startFrac + endFrac) * 0.5 + local newPt = { co = CRS_Interp( spline, newFrac ), frac = newFrac, } + table.insert(segPts, index, newPt) + return segPts + +end + +local function ProcessSegment(pointList, pointIndex) + + local p0 = pointList[pointIndex - 2] + local p1 = pointList[pointIndex - 1] + local p2 = pointList[pointIndex] + local p3 = pointList[pointIndex + 1] + local spline = CRS_CalculateSpline({p0, p1, p2, p3}) + + local segPts = {} + table.insert(segPts, { co = p1, frac = 0.0, } ) + table.insert(segPts, { co = p2, frac = 1.0, } ) + + -- divide the segment into 4 segments. + segPts = SplitSegment(segPts, spline, 2) -- splits it in half + segPts = SplitSegment(segPts, spline, 2) -- splits it in half + segPts = SplitSegment(segPts, spline, 4) -- splits it in half + + -- ensure segments satisfy all criteria for our spline. + local accepted = false + local debugCount = 20 + while not accepted and debugCount > 0 do + debugCount = debugCount - 1 + accepted = true + local segsToDivide = {} + + -- ensure the maximum angle between two segs isn't too high + for i=2, #segPts-1 do + local v0 = segPts[i-1].co + local v1 = segPts[i].co + local v2 = segPts[i+1].co + local s0 = (v1 - v0):GetUnit() + local s1 = (v2 - v1):GetUnit() + if s0:DotProduct(s1) < kMinDotProduct then + accepted = false + segsToDivide[i] = true + segsToDivide[i+1] = true + end + end + + -- divide any segments that need it + local initialSegCount = #segPts + for i=initialSegCount, 2, -1 do + if segsToDivide[i] then + segPts = SplitSegment(segPts, spline, i) + end + end + + end + + -- only return the coordinate data -- spline interp fractions can be discarded now. + local segPts2 = {} + for i=1, #segPts do + table.insert(segPts2, segPts[i].co) + end + + return segPts2 + +end + +-- Returns a list of smoothed vertex positions for the spline based on the passed in point list. +-- Each element of the resulting "segPts" table is a table of positions for that particular segment. +local function ProcessSegments(pointList) + + local segPts = {} -- each "segment" will contain a table of points. + for i=3, #pointList-1 do + local newSeg = ProcessSegment(pointList, i) + table.insert(segPts, newSeg) + end + + return segPts + +end + +-- combine the list of segments passed into it into one long list of points. +local function CombineSegments(segPts) + + local path = {} + table.insert(path, segPts[1][1]) + for i=1, #segPts do + for j=2, #segPts[i] do + table.insert(path, segPts[i][j]) + end + end + + return path + +end + +local function CalculatePathVectors(path) + + local vects = { Vector(0,0,0), } -- first is a copy of the second. Replace this when done. + local prevPt = path[1] + local pt = nil + for i=2, #path do + pt = path[i] + local diff = pt - prevPt + local dist = diff:GetLength() + assert(dist > 0) + table.insert(vects, diff / dist) + prevPt = pt + end + + vects[1] = vects[2] * 1.0 -- multiply by one so it forces a copy. + + return vects + +end + +-- Calculates a tangent line that is perpendicular to both the up vector (0,1,0) and the path's +-- vector at this point. +local function CalculatePathTangents(vects) + + local tanVects = {} + local prevTanVec = nil -- fallback just in case. + for i=1, #vects do + local up = Vector(0,1,0) + local tanVec = nil + if math.abs(vects[i]:DotProduct(up)) == 1 then + if prevTanVec then + tanVec = prevTanVec * 1.0 -- force a copy + else + up = Vector(1,0,0) + end + end + + if not tanVec then + tanVec = vects[i]:CrossProduct(up):GetUnit() + end + + prevTanVec = tanVec + table.insert(tanVects, tanVec) + + end + + return tanVects + +end + +-- Combines each tangent vector with that of the next tangent vector, to facilitate proper miter +-- joints. Note that the resulting vectors are NOT unit vectors. They are scaled the appropriate +-- amount to ensure an even thickness for each section of the path (thickness when two left and +-- right parallel edges are measured along their perpendicular axis). Front and back tangents +-- are left unchanged. +local function SmoothPathTangents(tanVects) + + local smoothTanVects = {} + table.insert(smoothTanVects, tanVects[1]) + for i=2, #tanVects-1 do + local tanA = tanVects[i] + local tanB = tanVects[i+1] + local dot = Clamp(tanA:DotProduct(tanB), 0, 1) + local inverseMagnitude = math.cos(math.acos(dot) * 0.5) + local newTan = (tanA + tanB):GetUnit() / inverseMagnitude + table.insert(smoothTanVects, newTan) + end + table.insert(smoothTanVects, tanVects[#tanVects]) + + return smoothTanVects + +end + +-- Creates two lists of equal length for vertices on the left and right side of the spline. +-- The positioning of the verts are chosen such that the tangent of the resulting triangles is +-- always perpendicular to the y axis. In other words, it always tries to face "upwards", like +-- a rollercoaster with no banking on the turns. +-- to illustrate... +-- _ +-- | |_| +-- | ---> |_| +-- | |_| +local function ThickenPath(path) + + local offset = kLineWidth * 0.5 + + local vects = CalculatePathVectors(path) + local tanVects = CalculatePathTangents(vects) + tanVects = SmoothPathTangents(tanVects) + + assert(#path == #vects) + assert(#vects == #tanVects) + + local left = {} + local right = {} + for i=1, #path do + table.insert(left, path[i] - (tanVects[i] * offset)) + table.insert(right, path[i] + (tanVects[i] * offset)) + end + + return left, right + +end + +-- effectively split a path when it gets too long for more efficient rendering. Returns a table +-- where each element is a table that contains a start index, and an end index. +-- We also take advantage of this time to store the total distance for each vert along the path, +-- to aid in texture coordinate calculations later. +local function CalculateRangeList(path) + + local rangeList = {} + local distances = {} + + local length = 0.0 + local totalLength = 0.0 + local lastPt = path[1] + local startIndex = 1 + + table.insert(distances, 0) + + for i=2, #path do + local pt = path[i] + local dist = (pt - lastPt):GetLength() + length = length + dist + + if length >= kMaxMeshLength or i == #path then + table.insert(rangeList, { startIndex = startIndex, endIndex = i }) + length = 0.0 + startIndex = i + end + + lastPt = pt + + totalLength = totalLength + dist + table.insert(distances, totalLength) + end + + return rangeList, distances + +end + +-- Combines the list of left and right vertices to form one long list -- left first, then right, and +-- returns it as a list of single-dimensional values (so x0 y0 z0 x1 y1 z1.... etc) +local function CombineLeftAndRight(range, left, right) + + assert(#left == #right) + + local newVerts = {} + for i=range.startIndex, range.endIndex do + table.insert(newVerts, left[i].x) + table.insert(newVerts, left[i].y) + table.insert(newVerts, left[i].z) + end + + for i=range.startIndex, range.endIndex do + table.insert(newVerts, right[i].x) + table.insert(newVerts, right[i].y) + table.insert(newVerts, right[i].z) + end + + return newVerts + +end + +-- Calculates the texture coordinates for the mesh. The texture will be oriented vertically, with +-- the left vertices having an x value of 0, and the right vertices having an x value of 1.0. The +-- y value for both left and right will be based on the path distance calculated earlier. +-- Then these values are simply scaled to appear correct. +local function CalculateTextureCoordinates(range, distances) + + local texCoords = {} + for i=range.startIndex, range.endIndex do + table.insert(texCoords, 0.0) + table.insert(texCoords, -distances[i] * kTextureSizeRatio / kLineWidth) + end + + for i=range.startIndex, range.endIndex do + table.insert(texCoords, 1.0) + table.insert(texCoords, -distances[i] * kTextureSizeRatio / kLineWidth) + end + + return texCoords + +end + +-- Calculates the indices for the triangles of this mesh. We divide each segment's quad into +-- two triangles using the shortest diagonal. +local function CalculateMeshIndices(range, left, right) + + assert(#left == #right) + + local indices = {} + local rangeSize = range.endIndex - range.startIndex + 1 + for i=range.startIndex + 1, range.endIndex do + local backLeft = left[i-1] + local backRight = right[i-1] + local frontLeft = left[i] + local frontRight = right[i] + local diag1 = (frontRight - backLeft):GetLengthSquared() + local diag2 = (frontLeft - backRight):GetLengthSquared() + + -- remember, indices start at 0, not 1!!! + -- also, remember CCW winding order. + if diag1 < diag2 then + -- triangle 1 + table.insert(indices, i - range.startIndex - 1) -- back left vertex + table.insert(indices, i - range.startIndex - 1 + rangeSize) -- back right vertex + table.insert(indices, i - range.startIndex - 1 + rangeSize + 1) -- front right vertex + -- triangle 2 + table.insert(indices, i - range.startIndex - 1) -- back left vertex + table.insert(indices, i - range.startIndex - 1 + rangeSize + 1) -- front right vertex + table.insert(indices, i - range.startIndex - 1 + 1) -- front left vertex + else + -- triangle 1 + table.insert(indices, i - range.startIndex - 1) -- back left vertex + table.insert(indices, i - range.startIndex - 1 + rangeSize) -- back right vertex + table.insert(indices, i - range.startIndex - 1 + 1) -- front left vertex + -- triangle 2 + table.insert(indices, i - range.startIndex - 1 + rangeSize) -- back right vertex + table.insert(indices, i - range.startIndex - 1 + rangeSize + 1) -- front right vertex + table.insert(indices, i - range.startIndex - 1 + 1) -- front left vertex + end + end + + return indices + +end + +-- Generate normals that are pointing along the edge that connects the two sides of the spline. We'll test against +-- this in a shader to make the line fade out smoothly when at a grazing angle, to make it look better. A bit of a +-- hack, yes, but we don't need proper normals for something that's purely emissive anyways... +local function GenerateNormals(vertList, indices) + + assert(#vertList %3 == 0) -- 3 components per vector + assert(#indices %3 == 0) -- 3 vertices per triangle + + -- group into triangles + local tris = {} + local indicesIndex = 1 + while indicesIndex < #indices do + + local tri = {} + tri.indices = {} + tri.indices[1] = indices[indicesIndex] + tri.indices[2] = indices[indicesIndex + 1] + tri.indices[3] = indices[indicesIndex + 2] + + local p1 = Vector(vertList[tri.indices[1] * 3 + 1], vertList[tri.indices[1] * 3 + 2], vertList[tri.indices[1] * 3 + 3]) + local p2 = Vector(vertList[tri.indices[2] * 3 + 1], vertList[tri.indices[2] * 3 + 2], vertList[tri.indices[2] * 3 + 3]) + local p3 = Vector(vertList[tri.indices[3] * 3 + 1], vertList[tri.indices[3] * 3 + 2], vertList[tri.indices[3] * 3 + 3]) + + local e1 = p2 - p1 + local e2 = p3 - p1 + + tri.norm = e1:CrossProduct(e2):GetUnit() + + table.insert(tris, tri) + + indicesIndex = indicesIndex + 3 + + end + + -- init list of vectors -- one for each vertex + local nVecs = {} + for i=1, #vertList / 3 do + nVecs[i] = Vector(0,0,0) + end + + -- add triangles' normals to all vertices they touch. + for i=1, #tris do + + local tri = tris[i] + + for j=1, 3 do + local idx = tri.indices[j] + 1 + nVecs[idx] = nVecs[idx] + tri.norm + end + + end + + -- create component list of normals + local nComps = {} + for i=1, #nVecs do + + assert(nVecs[i]:GetLengthSquared() > 0.0) + + local vec = nVecs[i]:GetUnit() + table.insert(nComps, vec.x) + table.insert(nComps, vec.y) + table.insert(nComps, vec.z) + end + + assert(#nComps == #vertList) + + return nComps + +end + +local function SetupMesh(range, distances, path, left, right) + + local mesh = Client.CreateRenderDynamicMesh(RenderScene.Zone_Default) + local vertList = CombineLeftAndRight(range, left, right) + mesh:SetVertices(vertList, #vertList) + + local texCoords = CalculateTextureCoordinates(range, distances) + mesh:SetTexCoords(texCoords, #texCoords) + + local indices = CalculateMeshIndices(range, left, right) + mesh:SetIndices(indices, #indices) + + local normals = GenerateNormals(vertList, indices) + mesh:SetNormals(normals, #normals) + + mesh:SetMaterial(kGuideLineMaterial) + + return mesh + +end + +-- Creates a dynamic mesh object for each set of points within the range provided. +local function SetupMeshes(rangeList, distances, path, left, right) + + local meshes = {} + for i=1, #rangeList do + local range = rangeList[i] + local mesh = SetupMesh(range, distances, path, left, right) + table.insert(meshes, mesh) + end + + return meshes + +end + +function CreateGuideMeshes() + + -- Combine all the named points from the map editor into a single list, storing intermediate + -- pathing-calculated points when not disabled. + local pointList = CreatePointList() + if not pointList then + return nil + end + + -- Reduce the number of points in the path. + pointList = OptimizePath(pointList) + + -- Ensure points of path don't clip through terrain + pointList = SnapPointsToLevel(pointList) + + -- Add garbage points to front and back so we can interpolate the whole spline. + pointList = DilateList(pointList) + + -- Try to figure out where the smoothed line is going to intersect with objects, and adjust the points + -- to make the line move around the obstacle. + for i=1, kObstacleCheckIterations do + pointList = AvoidObstacles(pointList) + end + + -- Create smoothed list of points using the CRS interpolation. + local segPts = ProcessSegments(pointList) + + -- Combine the table of tables into one long list of points. + local path = CombineSegments(segPts) + + -- "Thicken" the line into two lists, left and right edges. These will be the actual + -- vertex lists. + local left, right = ThickenPath(path) + + -- We split the mesh into smaller pieces so we don't have to render it all at once. + -- We split according to length of the original spline. + local rangeList, distances = CalculateRangeList(path) + + -- Create a new dynamic mesh object for each range, or "section" of the path. + guideMeshes = SetupMeshes(rangeList, distances, path, left, right) + +end + + diff --git a/challenges/skulk_challenge/lua/SCMapData.lua b/challenges/skulk_challenge/lua/SCMapData.lua new file mode 100644 index 000000000..1aabae8ab --- /dev/null +++ b/challenges/skulk_challenge/lua/SCMapData.lua @@ -0,0 +1,251 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCMapData.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Stores map-specific data, for example the medal reward times. Only one map at the time of writing, +-- but we could make more... :) +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +gMedalColors = +{ + shadow = Color(131/255, 92/255, 202/255, 1), + gold = Color(219/255, 160/255, 16/255, 1), + silver = Color(159/255, 172/255, 194/255, 1), + bronze = Color(167/255, 136/255, 103/255, 1), +} + +gMedals = -- must be sorted best to worst. +{ + "shadow", + "gold", + "silver", + "bronze", +} +-- create name->index mapping +for i=1, #gMedals do + gMedals[gMedals[i]] = i +end + +gSCMapData = +{ + ns2_skulk_challenge_1 = + { + medalTimings = -- must match up to gMedals + { + 83, -- shadow + 88, -- gold + 98, -- silver + 113, -- bronze + }, + + medalIcons = -- must match up to gMedals + { + PrecacheAsset("ui/challenge/medal_alien_shadow.dds"), + PrecacheAsset("ui/challenge/medal_alien_gold.dds"), + PrecacheAsset("ui/challenge/medal_alien_silver.dds"), + PrecacheAsset("ui/challenge/medal_alien_bronze.dds"), + }, + + pathSetup = -- list of target_point names to path/move between for course. + { + { name = "starting_position", }, + { name = "glass_jump_a", }, + { name = "glass_jump_b", noMesh = true }, -- ignore pathing mesh between glass_jump_a and b. + { name = "end_point", noMesh = true }, + { name = "finish_sequence_end", noMesh = true }, + }, + + leaderboardName = "skulk_challenge_1", + steamStatName = "skulk_challenge_1", + resultsScreenTitle = "SKULK_CHALLENGE_1", + } +} + +function SC_GetResultsScreenTitle() + local mapName = Shared.GetMapName() + local mapData = gSCMapData[mapName] + if not mapData then + return nil + end + + return mapData.resultsScreenTitle +end + +function SC_GetLeaderboardName() + local mapName = Shared.GetMapName() + local mapData = gSCMapData[mapName] + if not mapData then + return nil + end + + return mapData.leaderboardName +end + +function SC_GetSteamStatName() + local mapName = Shared.GetMapName() + local mapData = gSCMapData[mapName] + if not mapData then + return nil + end + + return mapData.steamStatName +end + +function SC_GetPathPointNameData() + local mapName = Shared.GetMapName() + local mapData = gSCMapData[mapName] + if not mapData then + return nil + end + + return mapData.pathSetup +end + +local function GetMapData() + local mapName = Shared.GetMapName() + return gSCMapData[mapName] +end + +-- Returns the medal name (if any, or nil if none) for the highest medal awarded for the given time. +function SC_GetMedalForTime(seconds) + + assert(seconds) + + local mapData = GetMapData() + if mapData == nil then + return nil + end + + local medalTimings = mapData.medalTimings + + for i=1, #medalTimings do + if medalTimings[i] > seconds then + return gMedals[i] + end + end + + return nil + +end + +function SC_AwardBadgeForTime(seconds) + + local medal = SC_GetMedalForTime(seconds) + if not medal then + return + end + + local statName = SC_GetSteamStatName() + if not statName then + return + end + + local medalRank = gMedals[medal] + + -- a stat value of 4 gives the shadow medal, which is medal index 1 :( + local medalStatValue = 5 - medalRank + + if not Client.SetUserStat_Int(statName, medalStatValue) then + Log("ERROR: Unable to set user stat named '%s' to value %s.", statName, medalStatValue) + end + +end + +function SC_GetNextMedal(currentTime) + + local mapData = GetMapData() + if mapData == nil then + return nil + end + + local medalTimings = mapData.medalTimings + + -- default to slower time than last medal. + currentTime = currentTime or medalTimings[#medalTimings] + 1 + + local medalIndex + for i=1, #medalTimings do + if currentTime > medalTimings[i] then + medalIndex = i + end + end + + if medalIndex == nil then + return nil -- they've got the best time already + end + + return gMedals[medalIndex] + +end + +-- Returns the medal's time, in seconds, of the given medal, taken as either the string name of the medal, or the +-- index of the medal. Returns nil if medal is not found, or input is invalid. +function SC_GetMedalTime(nameOrIndex) + + if nameOrIndex == nil then + return nil + end + + local index + if type(nameOrIndex) == "number" then + index = nameOrIndex + else + index = gMedals[nameOrIndex] + end + + if index == nil then + return nil + end + + local mapData = GetMapData() + if mapData == nil then + return nil + end + + return mapData.medalTimings[index] + +end + +function SC_GetMedalIcon(nameOrIndex) + + if nameOrIndex == nil then + return nil + end + + local index + if type(nameOrIndex) == "number" then + index = nameOrIndex + else + index = gMedals[nameOrIndex] + end + + local mapData = GetMapData() + if mapData == nil then + return nil + end + + return mapData.medalIcons[index] + +end + +function SC_GetMedalTextColor(nameOrIndex) + + if nameOrIndex == nil then + return nil + end + + local name + if type(nameOrIndex) == "number" then + name = gMedals[nameOrIndex] + else + name = nameOrIndex + end + + return gMedalColors[name] + +end + + diff --git a/challenges/skulk_challenge/lua/SCMusicManager.lua b/challenges/skulk_challenge/lua/SCMusicManager.lua new file mode 100644 index 000000000..0a8ecfb7c --- /dev/null +++ b/challenges/skulk_challenge/lua/SCMusicManager.lua @@ -0,0 +1,263 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCMusicManager.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Handles playing music and shuffling the songs it's already played. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +local volumeModifier = 0.3 + +Shared.RegisterNetworkMessage("RequestSoundLength", +{ + soundName = "string (64)", +}) +Shared.RegisterNetworkMessage("ReceiveSoundLength", +{ + soundName = "string (64)", + soundLength = "float", +}) + +local tracks = +{ + { soundName = "sound/NS2.fev/music/Dark Nebula", preRoll = 8.649, volume = 0.75 }, + { soundName = "sound/NS2.fev/music/Exosuit", preRoll = 5.644, volume = 0.5 }, + { soundName = "sound/NS2.fev/music/Frontiersmen", preRoll = 1.160, volume = 0.5 }, + { soundName = "sound/NS2.fev/music/Gorilla", preRoll = 1.663, volume = 0.375 }, + { soundName = "sound/NS2.fev/music/Infesting", preRoll = 1.145, volume = 0.5 }, + { soundName = "sound/NS2.fev/music/Rush", preRoll = 0.622, volume = 0.5 }, + { soundName = "sound/NS2.fev/music/Rust and Steam", preRoll = 1.947, volume = 0.5 }, +} + +if Client then + + local musicManager + function GetMusicManager() + + if not musicManager then + musicManager = SCMusicManager() + musicManager:Initialize() + end + + return musicManager + + end + + class 'SCMusicManager' + + function SCMusicManager:GetVolumeModifier() + return volumeModifier + end + + function SCMusicManager:Initialize() + + self.tracks = {} + self.maxPreRoll = 0.0 + self.musicPlaying = false + self.scheduledStart = nil + self.nextTrackTime = nil + self.soundLengthRequests = {} + + end + + -- Randomizes the order of all the tracks. It is assumed this is only called when music is not yet playing. + function SCMusicManager:InitialShuffleTracks() + + -- for some reason this supposedly makes it "more random..." + math.random() + math.random() + math.random() + math.random() + math.random() + + self.initialShuffle = true + + local newTrackList = {} + while #self.tracks > 0 do + local index = math.random(1, #self.tracks) + newTrackList[#newTrackList + 1] = self.tracks[index] + table.remove(self.tracks, index) + end + + self.tracks = newTrackList + + end + + -- Remove the track from the front of the table, and re-insert it somewhere randomly in the table. We ensure it's + -- at least halfway towards the back, so we don't get this one repeated too soon. + function SCMusicManager:ReshuffleFirstTrack() + + assert(#self.tracks > 0) + + local track = self.tracks[1] + table.remove(self.tracks, 1) + + local index = math.random(math.floor(#self.tracks * 0.5) + 1, #self.tracks + 1) + table.insert(self.tracks, index, track) + + end + + -- Some music tracks have a pre roll longer than the normal amount of time set aside for the beginning of the race. + -- In these cases, we need to add more time. + function SCMusicManager:GetPreRollOverflow(bufferTime) + + assert(#self.tracks > 0) + + if not self.initialShuffle then + self:InitialShuffleTracks() + end + + if self.musicPlaying then + return 0.0 + end + + return math.max(0.0, self.tracks[1].preRoll - bufferTime) + + end + + -- Adds a music track to the list of tracks this music manager class recognizes. PrerollDuration is the + -- amount of time by which to start the song early so a specific part of the song plays when we want it to. + -- Eg. Dark Nebula and Exosuit have some build up in the beginning, and it's pretty awesome to sync up that + -- first beat with the "GO!" of the race. + function SCMusicManager:AddMusic(path, preRollDuration, volume) + + self.initialShuffle = false + + preRollDuration = preRollDuration or 0.0 + + local newTrack = {} + newTrack.preRoll = preRollDuration + newTrack.path = path + newTrack.volume = volume + + self.tracks[#self.tracks+1] = newTrack + + self.maxPreRoll = math.max(self.maxPreRoll, preRollDuration) + + -- Request length of sound from server. + self.soundLengthRequests[#self.soundLengthRequests+1] = path + if self.loadComplete then + self:RequestPendingSoundLengths() + end + + end + + -- Asks the server what the lengths of the sounds are. + function SCMusicManager:RequestPendingSoundLengths() + + for i=1, #self.soundLengthRequests do + Client.SendNetworkMessage("RequestSoundLength", {soundName = self.soundLengthRequests[i]}, true) + end + self.soundLengthRequests = {} + + end + + -- Set the time when you want the music to play. We subtract from this the pre roll time for our randomly + -- selected track. + function SCMusicManager:ScheduleMusicStart(time) + + if self.musicPlaying then + return + end + + if not self.initialShuffle then + self:InitialShuffleTracks() + end + + -- shift the starting time so the first track's pre-roll matches up. + self.scheduledStart = time - self.tracks[1].preRoll + + end + + -- Stops music immediately. This will cause the sounds to fade out gracefully, unless a new music is immediately + -- played, thus forcing it to steal priority (only one can play at a time, setup in FMOD). + function SCMusicManager:StopMusic() + + self.musicPlaying = false + self.nextTrackTime = nil + self.scheduledStart = nil + self.currentTrackStartTime = nil + Shared.StopSound(nil, self.tracks[1].path) + + self:ReshuffleFirstTrack() + + end + + function SCMusicManager:Update(deltaTime) + + local now = Shared.GetSystemTimeReal() + + -- start the first track. + if self.scheduledStart ~= nil and not self.musicPlaying then + if now >= self.scheduledStart then + Shared.PlaySound(nil, self.tracks[1].path, volumeModifier * self.tracks[1].volume) + self.currentTrackStartTime = now + self.musicPlaying = true + end + end + + -- know when we're going to be playing the next track. + if self.musicPlaying and not self.nextTrackTime and self.tracks[1].length then + self.nextTrackTime = self.currentTrackStartTime + self.tracks[1].length + end + + -- play next track. + if self.musicPlaying and self.nextTrackTime and now >= self.nextTrackTime then + self:ReshuffleFirstTrack() + Shared.PlaySound(nil, self.tracks[1].path, volumeModifier * self.tracks[1].volume) + self.currentTrackStartTime = now + self.nextTrackTime = nil + end + + end + + local function OnReceiveSoundLength(msg) + local self = GetMusicManager() + for i=1, #self.tracks do + if self.tracks[i].path == msg.soundName then + self.tracks[i].length = msg.soundLength + end + end + end + Client.HookNetworkMessage("ReceiveSoundLength", OnReceiveSoundLength) + + local function OnUpdateClient(deltaTime) + GetMusicManager():Update(deltaTime) + end + Event.Hook("UpdateClient", OnUpdateClient) + + local function OnLoadComplete() + + GetMusicManager().loadComplete = true + GetMusicManager():RequestPendingSoundLengths() + + end + Event.Hook("LoadComplete", OnLoadComplete) + + for i=1, #tracks do + Client.PrecacheLocalSound(tracks[i].soundName) + GetMusicManager():AddMusic(tracks[i].soundName, tracks[i].preRoll, tracks[i].volume) + end + +end + +if Server then + + local function OnRequestSoundLength(client, msg) + Server.SendNetworkMessage("ReceiveSoundLength", + { + soundName = msg.soundName, + soundLength = GetSoundEffectLength(msg.soundName) + }, true) + end + Server.HookNetworkMessage("RequestSoundLength", OnRequestSoundLength) + + for i=1, #tracks do + PrecacheAsset(tracks[i].soundName) + end + +end + + diff --git a/challenges/skulk_challenge/lua/SCNS2ConsoleCommands_Server.lua b/challenges/skulk_challenge/lua/SCNS2ConsoleCommands_Server.lua new file mode 100644 index 000000000..286df454f --- /dev/null +++ b/challenges/skulk_challenge/lua/SCNS2ConsoleCommands_Server.lua @@ -0,0 +1,1729 @@ +-- ======= Copyright (c) 2003-2011, Unknown Worlds Entertainment, Inc. All rights reserved. ======= +-- +-- lua\ConsoleCommands_Server.lua +-- +-- Created by: Charlie Cleveland (charlie@unknownworlds.com) and +-- Max McGuire (max@unknownworlds.com) +-- +-- NS2 Gamerules specific console commands. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/ScenarioHandler_Commands.lua") + +local gLastPosition = nil + +local function JoinTeam(player, teamIndex) + + if player ~= nil and player:GetTeamNumber() == kTeamReadyRoom then + + -- Auto team balance checks. + local allowed, reason = GetGamerules():GetCanJoinTeamNumber(player, teamIndex) + + if allowed or Shared.GetCheatsEnabled() then + return GetGamerules():JoinTeam(player, teamIndex) + else + Server.SendNetworkMessage(player, "JoinError", BuildJoinErrorMessage(reason), true) + return false + end + + end + + return false + +end + +local function JoinTeamOne(player) + return JoinTeam(player, kTeam1Index) +end + +local function JoinTeamTwo(player) + return JoinTeam(player, kTeam2Index) +end + +local function JoinTeamRandom(player) + return JoinRandomTeam(player) +end + +local function ReadyRoom(player) + + if player and not player:isa("ReadyRoomPlayer") then + + player:SetCameraDistance(0) + return GetGamerules():JoinTeam(player, kTeamReadyRoom) + + end + +end + +local function Spectate(player) + return GetGamerules():JoinTeam(player, kSpectatorIndex) +end + +local function OnCommandJoinTeamOne(client) + + local player = client:GetControllingPlayer() + JoinTeamOne(player) + +end + +local function OnCommandJoinTeamTwo(client) + + local player = client:GetControllingPlayer() + JoinTeamTwo(player) + +end + +local function OnCommandJoinTeamRandom(client) + + local player = client:GetControllingPlayer() + JoinTeamRandom(player) + +end + +local function OnCommandReadyRoom(client) + + local player = client:GetControllingPlayer() + ReadyRoom(player) + +end + +local function OnCommandSpectate(client) + + local player = client:GetControllingPlayer() + Spectate(player) + +end + +local function OnCommandFilm(client) + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() or Shared.GetDevMode() or (player:GetTeamNumber() == kTeamReadyRoom) then + + Shared.Message("Film mode enabled. Hold crouch for dolly, movement modifier for speed or attack to orbit then press movement keys.") + + local success, newPlayer = Spectate(player) + + -- Transform class into FilmSpectator + newPlayer:Replace(FilmSpectator.kMapName, newPlayer:GetTeamNumber(), false) + + end + +end + +-- +-- Forces the game to end for testing purposes +-- +local function OnCommandEndGame(client) + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() and GetGamerules():GetGameStarted() then + GetGamerules():EndGame(player:GetTeam()) + end + +end + +local function OnCommandTeamResources(client, value) + + value = value and tonumber(value) or kMaxTeamResources + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() then + player:GetTeam():SetTeamResources(value) + end + +end + +local function OnCommandResources(client, value) + + value = value and tonumber(value) or kMaxPersonalResources + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() then + player:SetResources(value) + end + +end + +local function OnCommandAutobuild(client) + + if Shared.GetCheatsEnabled() then + GetGamerules():SetAutobuild(not GetGamerules():GetAutobuild()) + Print("Autobuild now %s", ToString(GetGamerules():GetAutobuild())) + + -- Now build any existing structures that aren't built + for index, constructable in ipairs(GetEntitiesWithMixin("Construct")) do + + if not constructable:GetIsBuilt() then + constructable:SetConstructionComplete() + end + + end + + end + +end + +local function OnCommandEnergy(client) + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() then + + -- Give energy to all structures on our team. + for index, ent in ipairs(GetEntitiesWithMixinForTeam("Energy", player:GetTeamNumber())) do + ent:SetEnergy(ent:GetMaxEnergy()) + end + + end + +end + +local function OnCommandMature(client) + + if Shared.GetCheatsEnabled() then + + -- Give energy to all structures on our team. + for index, ent in ipairs(GetEntitiesWithMixin("Maturity")) do + ent:SetMature() + end + + end + +end + + +local function OnCommandTakeDamage(client, amount, optionalEntId) + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() then + + local damage = tonumber(amount) + if damage == nil then + damage = 20 + math.random() * 10 + end + + local damageEntity = nil + optionalEntId = optionalEntId and tonumber(optionalEntId) + if optionalEntId then + damageEntity = Shared.GetEntity(optionalEntId) + else + + damageEntity = player + if player:isa("Commander") then + + -- Find command structure we're in and do damage to that instead. + local commandStructures = Shared.GetEntitiesWithClassname("CommandStructure") + for index, commandStructure in ientitylist(commandStructures) do + + local comm = commandStructure:GetCommander() + if comm and comm:GetId() == player:GetId() then + + damageEntity = commandStructure + break + + end + + end + + end + + end + + if not damageEntity:GetCanTakeDamage() then + damage = 0 + end + + Print("Doing %.2f damage to %s", damage, damageEntity:GetClassName()) + damageEntity:DeductHealth(damage, player, player) + + end + +end + +local function OnCommandHeal(client, amount) + + if Shared.GetCheatsEnabled() then + + amount = amount and tonumber(amount) or 10 + local player = client:GetControllingPlayer() + player:AddHealth(amount) + + end + +end + +local function OnCommandGiveAmmo(client) + + if client ~= nil and Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + local weapon = player:GetActiveWeapon() + + if weapon ~= nil and weapon:isa("ClipWeapon") then + weapon:GiveAmmo(1) + end + + end + +end + + +local function OnCommandNanoShield(client) + + if client ~= nil and Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + + if HasMixin(player, "NanoShieldAble") then + player:ActivateNanoShield() + end + + end + +end + +local function OnCommandParasite(client, duration) + + if client ~= nil and Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + + if HasMixin(player, "ParasiteAble") then + + if player:GetIsParasited() and duration == nil then + player:RemoveParasite() + else + player:SetParasited( nil, tonumber(duration) ) + end + + end + + end + +end + + +local function OnCommandEnts(client, className) + + -- Allow it to be run on dedicated server + if client == nil or Shared.GetCheatsEnabled() or Shared.GetTestEnabled() then + + local entityCount = Shared.GetEntitiesWithClassname("Entity"):GetSize() + + local weaponCount = Shared.GetEntitiesWithClassname("Weapon"):GetSize() + local playerCount = Shared.GetEntitiesWithClassname("Player"):GetSize() + local structureCount = #GetEntitiesWithMixin("Construct") + local team1 = GetGamerules():GetTeam1() + local team2 = GetGamerules():GetTeam2() + local playersOnPlayingTeams = team1:GetNumPlayers() + team2:GetNumPlayers() + local commandStationsOnTeams = team1:GetNumCommandStructures() + team2:GetNumCommandStructures() + local blipCount = Shared.GetEntitiesWithClassname("Blip"):GetSize() + local infestCount = Shared.GetEntitiesWithClassname("Infestation"):GetSize() + + if className then + local numClassEnts = Shared.GetEntitiesWithClassname(className):GetSize() + Shared.Message(Pluralize(numClassEnts, className)) + else + + local formatString = "%d entities (%s, %d playing, %s, %s, %s, %s, %d command structures on teams)." + Shared.Message( string.format(formatString, + entityCount, + Pluralize(playerCount, "player"), playersOnPlayingTeams, + Pluralize(weaponCount, "weapon"), + Pluralize(structureCount, "structure"), + Pluralize(blipCount, "blip"), + Pluralize(infestCount, "infest"), + commandStationsOnTeams)) + end + end + +end + +local function OnCommandServerEntities(client, entityType) + + if client == nil or Shared.GetCheatsEnabled() or Shared.GetTestEnabled() then + DumpEntityCounts(entityType) + end + +end + +local function OnCommandEntityInfo(client, entityId) + + if client == nil or Shared.GetCheatsEnabled() or Shared.GetTestEnabled() then + + local ent = Shared.GetEntity(tonumber(entityId)) + if not ent then + + Shared.Message("No entity matching Id: " .. entityId) + return + + end + + local entInfo = GetEntityInfo(ent) + Shared.Message(entInfo) + + end + +end + +local function OnCommandServerEntInfo(client, entityId) + + if client == nil or Shared.GetCheatsEnabled() then + end + +end + +local function OnCommandDamage(client,multiplier) + + if(Shared.GetCheatsEnabled()) then + local m = multiplier and tonumber(multiplier) or 1 + GetGamerules():SetDamageMultiplier(m) + Shared.Message("Damage multipler set to " .. m) + end + +end + +local function OnCommandHighDamage(client) + + if Shared.GetCheatsEnabled() and GetGamerules():GetDamageMultiplier() < 10 then + + GetGamerules():SetDamageMultiplier(10) + Print("highdamage on (10x damage)") + + -- Toggle off + elseif not Shared.GetCheatsEnabled() or GetGamerules():GetDamageMultiplier() > 1 then + + GetGamerules():SetDamageMultiplier(1) + Print("highdamage off") + + end + +end + +local function OnCommandGive(client, itemName) + + local player = client:GetControllingPlayer() + if(Shared.GetCheatsEnabled() and itemName ~= nil and itemName ~= "alien") then + local newItem = player:GiveItem(itemName, nil, true) + if newItem and newItem.UpdateWeaponSkins then + newItem:UpdateWeaponSkins( client ) + end + --player:SetActiveWeapon(itemName) + end + +end + +local function OnCommandSpawn(client, itemName, teamnum, useLastPos) + + local player = client:GetControllingPlayer() + if(Shared.GetCheatsEnabled() and itemName ~= nil and itemName ~= "alien") then + + -- trace along players zAxis and spawn the item there + local startPoint = player:GetEyePos() + local endPoint = startPoint + player:GetViewCoords().zAxis * 100 + local usePos = nil + + if not teamnum then + teamnum = player:GetTeamNumber() + else + teamnum = tonumber(teamnum) + end + + if useLastPos and gLastPosition then + usePos = gLastPosition + else + + local trace = Shared.TraceRay(startPoint, endPoint, CollisionRep.Default, PhysicsMask.Bullets, EntityFilterAll()) + usePos = trace.endPoint + + end + + local newItem = CreateEntity(itemName, usePos, teamnum) + + Print("spawned \""..itemName.."\" at Vector("..usePos.x..", "..usePos.y..", "..usePos.z..")") + + end + +end + +local function OnCommandTrace(client) + + local player = client:GetControllingPlayer() + + -- trace along players zAxis and spawn the item there + local startPoint = player:GetEyePos() + local endPoint = startPoint + player:GetViewCoords().zAxis * 100 + local trace = Shared.TraceRay(startPoint, endPoint, CollisionRep.Default, PhysicsMask.Bullets, EntityFilterAll()) + local hitPos = trace.endPoint + + Print("Surface: " .. ToString(trace.surface)) + Print("Vector("..hitPos.x..", "..hitPos.y..", "..hitPos.z.."),") + +end + +local function OnCommandShoot(client, projectileName, velocity) + + local player = client:GetControllingPlayer() + if Shared.GetCheatsEnabled() and projectileName ~= nil then + + velocity = velocity or 15 + + local viewAngles = player:GetViewAngles() + local viewCoords = viewAngles:GetCoords() + local startPoint = player:GetEyePos() + viewCoords.zAxis * 1 + + local startPointTrace = Shared.TraceRay(player:GetEyePos(), startPoint, CollisionRep.Damage, PhysicsMask.Bullets, EntityFilterOne(player)) + startPoint = startPointTrace.endPoint + + local startVelocity = viewCoords.zAxis * velocity + + local projectile = CreateEntity(projectileName, startPoint, player:GetTeamNumber()) + projectile:Setup(player, startVelocity, true, nil, player) + + end + +end + +local function OnCommandGiveUpgrade(client, techIdString) + + if Shared.GetCheatsEnabled() then + + local techId = techIdStringToTechId(techIdString) + + if techId ~= nil then + + local player = client:GetControllingPlayer() + + if not player:GetTechTree():GiveUpgrade(techId) then + + if not player:GiveUpgrade(techId) then + Print("Error: GiveUpgrade(%s) not researched and not an upgraded", EnumToString(kTechId, techId)) + end + + end + + else + Shared.Message("Error: " .. techIdString .. " does not match any Tech Id") + end + + end + +end + +local function OnCommandLogout(client) + + local player = client:GetControllingPlayer() + if player:GetIsCommander() and GetCommanderLogoutAllowed() then + + player:Logout() + + end + +end + +local function OnCommandGotoIdleWorker(client) + + local player = client:GetControllingPlayer() + if player:GetIsCommander() then + player:GotoIdleWorker() + end + +end + +local function OnCommandGotoPlayerAlert(client) + + local player = client:GetControllingPlayer() + if player:GetIsCommander() then + player:GotoPlayerAlert() + end + +end + +local function OnCommandSelectAllPlayers(client) + + local player = client:GetControllingPlayer() + if player.SelectAllPlayers then + player:SelectAllPlayers() + end + +end + +local function OnCommandSetFOV(client, fovValue) + + local player = client:GetControllingPlayer() + if Shared.GetDevMode() then + player:SetFov(tonumber(fovValue)) + end + +end + +local function OnCommandChangeClass(className, teamNumber, extraValues) + + return function(client) + + local player = client:GetControllingPlayer() + + -- Don't allow to use these commands if you're in the RR + if player:GetTeamNumber() == kTeam1Index or player:GetTeamNumber() == kTeam2Index then + + -- Switch teams if necessary + if player:GetTeamNumber() ~= teamNumber then + if Shared.GetCheatsEnabled() and not player:GetIsCommander() then + + -- Remember position and team for calling player for debugging + local playerOrigin = player:GetOrigin() + local playerViewAngles = player:GetViewAngles() + + local newTeamNumber = kTeam1Index + if player:GetTeamNumber() == kTeam1Index then + newTeamNumber = kTeam2Index + end + + local success, newPlayer = GetGamerules():JoinTeam(player, kTeamReadyRoom) + success, newPlayer = GetGamerules():JoinTeam(newPlayer, newTeamNumber) + + newPlayer:SetOrigin(playerOrigin) + newPlayer:SetViewAngles(playerViewAngles) + + player = client:GetControllingPlayer() + + end + end + + -- Respawn shenanigans + if Shared.GetCheatsEnabled() then + local newPlayer = player:Replace(className, player:GetTeamNumber(), nil, nil, extraValues) + + -- Always disable 3rd person + newPlayer:SetDesiredCameraDistance(0) + + -- Turns out if you give weapons to exos the game implodes! Who would've thought! + if teamNumber == kTeam1Index and (className == "marine" or className == "jetpackmarine") and newPlayer.lastWeaponList then + -- Restore weapons in reverse order so the main weapons gets selected on respawn + for i = #newPlayer.lastWeaponList, 1, -1 do + if newPlayer.lastWeaponList[i] ~= "axe" then + newPlayer:GiveItem(newPlayer.lastWeaponList[i]) + end + end + end + + if teamNumber == kTeam2Index and newPlayer.lastUpgradeList then + -- I have no idea if this will break, but I don't care! + -- Thug life! + -- Ghetto code incoming, you've been warned + newPlayer.upgrade1 = newPlayer.lastUpgradeList[1] or 1 + newPlayer.upgrade2 = newPlayer.lastUpgradeList[2] or 1 + newPlayer.upgrade3 = newPlayer.lastUpgradeList[3] or 1 + end + + end + + end + + end + +end + +local function OnCommandRespawn(client) + + local player = client:GetControllingPlayer() + + if player.lastClass and player.lastDeathPos and (player:GetTeamNumber() == kTeam1Index or player:GetTeamNumber() == kTeam2Index) and Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + local teamNumber = kTeam2Index + local extraValues = nil + + if player.lastClass == "exo" or player.lastClass == "marine" or player.lastClass == "jetpackmarine" then + teamNumber = kTeam1Index + + if player.lastClass == "exo" then + extraValues = player.lastExoLayout + end + end + + local func = OnCommandChangeClass(player.lastClass, teamNumber, extraValues) + + func(client) + + player = client:GetControllingPlayer() + player:SetOrigin(player.lastDeathPos) + end + +end + +local function OnCommandRespawnClear(client) + + local player = client:GetControllingPlayer() + + player.lastDeathPos = nil + player.lastWeaponList = nil + player.lastClass = nil + player.lastExoLayout = nil + +end + +-- Switch player from one team to the other, while staying in the same place +local function OnCommandSwitch(client) + + local func = nil + local player = client:GetControllingPlayer() + local teamNumber = player:GetTeamNumber() + + -- For some reason the player team is swapped here, maybe the old function has been run first? + if teamNumber == kTeam1Index then + func = OnCommandChangeClass("skulk", kTeam2Index) + elseif teamNumber == kTeam2Index then + func = OnCommandChangeClass("marine", kTeam1Index) + end + + if func ~= nil then + func(client) + end + + player:SetDesiredCameraDistance(0) + +end + +local function OnCommandSandbox(client) + + local player = client:GetControllingPlayer() + if(Shared.GetCheatsEnabled()) then + + MarineTeam.gSandboxMode = not MarineTeam.gSandboxMode + Print("Setting sandbox mode %s", ConditionalValue(MarineTeam.gSandboxMode, "on", "off")) + + end + +end + +local function OnCommandCommand(client) + + local player = client:GetControllingPlayer() + if Shared.GetCheatsEnabled() then + + -- Find hive/command station on our team and use it + local ents = GetEntitiesForTeam("CommandStructure", player:GetTeamNumber()) + if #ents > 0 then + + player:SetOrigin(ents[1]:GetOrigin() + Vector(0, 1, 0)) + player:UseTarget(ents[1], 0) + ents[1]:UpdateCommanderLogin(true) + + end + + end + +end + +local function OnCommandCatPack(client) + + local player = client:GetControllingPlayer() + if(Shared.GetCheatsEnabled() and player:isa("Marine")) then + player:ApplyCatPack() + end +end + +local function OnCommandAllTech(client) + + local player = client:GetControllingPlayer() + if(Shared.GetCheatsEnabled()) then + + local newAllTechState = not GetGamerules():GetAllTech() + GetGamerules():SetAllTech(newAllTechState) + Print("Setting alltech cheat %s", ConditionalValue(newAllTechState, "on", "off")) + + end + +end + +local function OnCommandFastEvolve(client) + + local player = client:GetControllingPlayer() + if(Shared.GetCheatsEnabled()) then + + Embryo.gFastEvolveCheat = not Embryo.gFastEvolveCheat + Print("Setting fastevolve cheat %s", ConditionalValue(Embryo.gFastEvolveCheat, "on", "off")) + + end + +end + +local function OnCommandAllFree(client) + + local player = client:GetControllingPlayer() + if(Shared.GetCheatsEnabled()) then + + Player.kAllFreeCheat = not Player.kAllFreeCheat + Print("Setting allfree cheat %s", ConditionalValue(Player.kAllFreeCheat, "on", "off")) + + end + +end + +local function OnCommandLocation(client) + + local player = client:GetControllingPlayer() + local locationName = player:GetLocationName() + if locationName ~= "" then + Print("You are in \"%s\".", locationName) + else + Print("You are nowhere.") + end + +end + +local function OnCommandCloseMenu(client) + local player = client:GetControllingPlayer() + player:CloseMenu() +end + +-- Weld all doors shut immediately +local function OnCommandWeldDoors(client) + + if Shared.GetCheatsEnabled() then + + for index, door in ientitylist(Shared.GetEntitiesWithClassname("Door")) do + + if door:GetIsAlive() then + door:SetState(Door.kState.Welded) + end + + end + + end + +end + +local function OnCommandGore(client) + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() and player and player:isa("Marine") then + + player.interruptAim = true + player.interruptStartTime = Shared.GetTime() + + end + +end + +local function OnCommandPoison(client) + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() and player and player:isa("Marine") then + + player:SetPoisoned() + + end + +end + +local function OnCommandStun(client) + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() and player and HasMixin(player, "Stun") then + player:SetStun(kDisruptMarineTime) + end + +end + +local function OnCommandVortex(client) + + local player = client:GetControllingPlayer() + + if Shared.GetCheatsEnabled() and player and player.SetVortexDuration then + player:SetVortexDuration(4) + end + +end + +local function OnCommandSpit(client) + + if Shared.GetCheatsEnabled() then + local player = client:GetControllingPlayer() + if player and player:isa("Marine") then + player:OnSpitHit() + end + end + +end + +local function OnCommandPush(client) + + if Shared.GetCheatsEnabled() then + local player = client:GetControllingPlayer() + if player then + player:AddPushImpulse(Vector(50,10,0)) + end + end + +end + +local function OnCommandEnzyme(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player.TriggerEnzyme then + player:TriggerEnzyme(10) + end + + end + +end + +local function OnCommandUmbra(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and HasMixin(player, "Umbra") then + player:SetHasUmbra(true, 5) + end + + end + +end + +local function OnCommandOrderSelf(client) + + if Shared.GetCheatsEnabled() then + GetGamerules():SetOrderSelf(not GetGamerules():GetOrderSelf()) + Print("Order self is now %s.", ToString(GetGamerules():GetOrderSelf())) + end + +end + +local function techIdStringToTechId(techIdString) + + local techId = tonumber(techIdString) + + if type(techId) ~= "number" then + techId = StringToEnum(kTechId, techIdString) + end + + return techId + +end + +-- Create structure, weapon, etc. near player. +local function OnCommandCreate(client, techIdString, number, teamNum) + + if Shared.GetCheatsEnabled() then + + local techId = techIdStringToTechId(techIdString) + local attachClass = LookupTechData(techId, kStructureAttachClass) + + number = number or 1 + + if techId ~= nil then + + for i = 1, number do + + local success = false + -- Persistence is the path to victory. + for index = 1, 2000 do + + local player = client:GetControllingPlayer() + local teamNumber = tonumber(teamNum) or player:GetTeamNumber() + if techId == kTechId.Scan then + teamNumber = GetEnemyTeamNumber(teamNumber) + end + local position = nil + + if attachClass then + + local attachEntity = GetNearestFreeAttachEntity(techId, player:GetOrigin(), 1000) + if attachEntity then + position = attachEntity:GetOrigin() + end + + else + + --[[local modelName = LookupTechData(techId, kTechDataModel) + local modelIndex = Shared.GetModelIndex(modelName) + local model = Shared.GetModel(modelIndex) + local minExtents, maxExtents = model:GetExtents() + Print(modelName .. " bounding box min: " .. ToString(minExtents) .. " max: " .. ToString(maxExtents)) + local extents = maxExtents + DebugBox(player:GetOrigin(), player:GetOrigin(), maxExtents - minExtents, 1000, 1, 0, 0, 1) + DebugBox(player:GetOrigin(), player:GetOrigin(), minExtents, 1000, 0, 1, 0, 1) + DebugBox(player:GetOrigin(), player:GetOrigin(), maxExtents, 1000, 0, 0, 1, 1)--]] + --position = GetRandomSpawnForCapsule(extents.y, extents.x, player:GetOrigin() + Vector(0, 0.5, 0), 2, 10) + --position = position - Vector(0, extents.y, 0) + + position = CalculateRandomSpawn(nil, player:GetOrigin() + Vector(0, 0.5, 0), techId, true, 2, 10, 3) + + end + + if position then + + success = true + CreateEntityForTeam(techId, position, teamNumber, player) + break + + end + + end + + if not success then + Print("Create %s: Couldn't find space for entity", EnumToString(kTechId, techId)) + end + + end + + else + Print("Usage: create (techId name)") + end + + end + +end + +local function OnCommandRandomDebug(s) + + if Shared.GetCheatsEnabled() then + + local newState = not gRandomDebugEnabled + Print("OnCommandRandomDebug() now %s", ToString(newState)) + gRandomDebugEnabled = newState + + end + +end + +local function OnCommandDistressBeacon(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + local ent = GetNearest(player:GetOrigin(), "Observatory", player:GetTeamNumber()) + if ent and ent.TriggerDistressBeacon then + + ent:TriggerDistressBeacon() + + end + + end + +end + +local function OnCommandSetGameEffect(client, gameEffectString, trueFalseString) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + local gameEffectBitMask = kGameEffect[gameEffectString] + if gameEffectBitMask ~= nil then + + Print("OnCommandSetGameEffect(%s) => %s", gameEffectString, ToString(gameEffectBitMask)) + + local state = true + if trueFalseString and ((trueFalseString == "false") or (trueFalseString == "0")) then + state = false + end + + player:SetGameEffectMask(gameEffectBitMask, state) + + else + Print("Couldn't find bitmask in %s for %s", ToString(kGameEffect), gameEffectString) + end + + end + +end + +local function OnCommandChangeGCSettingServer(client, settingName, newValue) + + if Shared.GetCheatsEnabled() then + + if settingName == "setpause" or settingName == "setstepmul" then + Shared.Message("Changing server GC setting " .. settingName .. " to " .. tostring(newValue)) + collectgarbage(settingName, newValue) + else + Shared.Message(settingName .. " is not a valid setting") + end + + end + +end + +local function OnCommandEject(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player.Eject then + + player:Eject() + + end + + end + +end + + +local function GetClosestCyst(player) + local origin = player:GetOrigin() + -- get closest cyst inside 5m + local targets = GetEntitiesWithinRange("Cyst", origin, 5) + local target, range + for _,t in ipairs(targets) do + local r = (t:GetOrigin() - origin):GetLength() + if target == nil or range > r then + target, range = t, r + end + end + return target +end + +local function OnCommandCyst(client, cmd) + + if client ~= nil and (Shared.GetCheatsEnabled() or Shared.GetDevMode()) then + local cyst = GetClosestCyst(client:GetControllingPlayer()) + if cyst == nil then + Print("Have to be within 5m of a Cyst for the command to work") + else + if cmd == "track" then + if cyst == nil then + Log("%s has no track", cyst) + else + Log("track %s", cyst) + cyst:Debug() + end + elseif cmd == "reconnect" then + TrackYZ.kTrace,TrackYZ.kTraceTrack,TrackYZ.logTable["log"] = true,true,true + Log("Try reconnect %s", cyst) + cyst:TryToFindABetterParent() + TrackYZ.kTrace,TrackYZ.kTraceTrack,TrackYZ.logTable["log"] = false,false,false + else + Print("Usage: cyst track - show track to parent") + end + end + end +end + +-- +-- Show debug info for the closest entity that has a self.targetSelector +-- +local function OnCommandTarget(client, cmd) + + if client ~= nil and (Shared.GetCheatsEnabled() or Shared.GetDevMode()) then + local player = client:GetControllingPlayer() + local origin = player:GetOrigin() + local structs = GetEntitiesWithinRange("ScriptActor", origin, 5) + local sel, selRange = nil,nil + for _, struct in ipairs(structs) do + if struct.targetSelector or HasMixin(struct, "AiAttacks") then + local r = (origin - struct:GetOrigin()):GetLength() + if not sel or r < selRange then + sel,selRange = struct,r + end + end + end + Log("debug %s", sel) + if sel then + if HasMixin(sel, "AiAttacks") then + sel:AiAttacksDebug(cmd) + else + sel.targetSelector:Debug(cmd) + end + end + end +end + +local function OnCommandHasTech(client, cmd) + + if client ~= nil and Shared.GetCheatsEnabled() then + + if type(cmd) == "string" then + + local techId = StringToEnum(kTechId, cmd) + if techId == nil then + Print("Couldn't find tech id \"%s\" (should be something like ShotgunTech)", ToString(cmd)) + return + end + + local player = client:GetControllingPlayer() + if player then + + local techTree = player:GetTechTree() + if techTree then + local hasText = ConditionalValue(techTree:GetHasTech(techId), "has", "doesn't have") + Print("Your team %s \"%s\" tech.", hasText, cmd) + end + + end + + else + Print("Pass case-sensitive upgrade name.") + end + + end + +end + +local function OnCommandEggSpawnTimes(client, cmd) + + if Shared.GetCheatsEnabled() then + + Print("Printing out egg spawn times:") + + for playerIndex = 1, 16 do + + local s = string.format("%d players: ", playerIndex) + + for eggIndex = 1, kAlienEggsPerHive do + s = s .. string.format("%d eggs = %.2f ", eggIndex, CalcEggSpawnTime(playerIndex, eggIndex)) + end + + Print(s) + + end + + end + +end + +local function OnCommandTestOrder(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + + if player and HasMixin(player, "Orders") then + + local eyePos = player:GetEyePos() + local endPos = eyePos + player:GetViewAngles():GetCoords().zAxis * 50 + local trace = Shared.TraceRay(eyePos, endPos, CollisionRep.Default, PhysicsMask.Bullets, EntityFilterAll()) + local target = trace.endPoint + + player:GiveOrder(kTechId.Move, 0, target) + + end + + end + +end + +local function FindNearestAIUnit(player) + -- find where player is looking + local eyePos = player:GetEyePos() + local endPos = eyePos + player:GetViewAngles():GetCoords().zAxis * 50 + local trace = Shared.TraceRay(eyePos, endPos, CollisionRep.Default, PhysicsMask.Bullets, EntityFilterAll()) + local target = trace.endPoint + + local ents = Shared.GetEntitiesWithClassname("ScriptActor") + + local selected = nil + local selectedRange = 0 + + for i,entity in ientitylist(ents) do + + if entity:isa("Whip") or entity:isa("Drifter") or entity:isa("MAC") or entity:isa("ARC") then + + local r = (entity:GetOrigin() - target):GetLength() + if not selected or r < selectedRange then + + selected = entity + selectedRange = r + + end + + end + + end + + return trace, selected, selectedRange + +end + +-- call for the nearest AI unit to come to your location. Useful when testing pathing/animation +local function OnCommandFollowAndWeld(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + + local trace, selected, selectedRange = FindNearestAIUnit(player) + + if selected then + + local target = trace.entity or player + + Log("%s ordered to follow and weld %s", selected, target) + selected:GiveOrder(kTechId.FollowAndWeld, player:GetId()) + + else + Shared.Message("No AI entitity available") + end + + end + +end + +-- call for the nearest AI unit to come to your location. Useful when testing pathing/animation +local function OnCommandGoThere(client) + + if Shared.GetCheatsEnabled() then + local player = client:GetControllingPlayer() + + selected,selectedRange = FindNearestAIUnit(player) + + if selected then + + Shared.Message(string.format("Giving order to %s-%s", selected:GetClassName(), selected:GetId())) + selected:GiveOrder(kTechId.Move, player:GetId(), target) + -- Override the target Id to be invalidId so the AI unit doesn't follow the player. + selected:GetCurrentOrder():Initialize(kTechId.Move, Entity.invalidId, target, 0) + + else + Shared.Message("No AI entitity available") + end + + end + +end + +local function OnCommandRupture(client, classname) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player:isa("Marine") then + player:SetRuptured() + end + + end + +end + +local function OnCommandCommanderPing(client, classname) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player:GetTeam() then + + -- trace along crosshair + local startPoint = player:GetEyePos() + local endPoint = startPoint + player:GetViewCoords().zAxis * 100 + + local trace = Shared.TraceRay(startPoint, endPoint, CollisionRep.Default, PhysicsMask.Bullets, EntityFilterAll()) + + player:GetTeam():SetCommanderPing(trace.endPoint) + + end + + end + +end + +local function OnCommandThreat(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player then + + local startPoint = player:GetEyePos() + local endPoint = startPoint + player:GetViewCoords().zAxis * 100 + local trace = Shared.TraceRay(startPoint, endPoint, CollisionRep.Default, PhysicsMask.Bullets, EntityFilterAll()) + CreatePheromone(kTechId.ThreatMarker, trace.endPoint, 2) + + end + + end + +end + +local function OnCommandFire(client) + + if Shared.GetCheatsEnabled() then + local player = client:GetControllingPlayer() + if player then + player:SetOnFire(nil, nil) + end + end + +end + +local function OnCommandDeployARCs() + + if Shared.GetCheatsEnabled() then + + for index, arc in ientitylist(Shared.GetEntitiesWithClassname("ARC")) do + arc.deployMode = ARC.kDeployMode.Deploying + end + + end + +end + +local function OnCommandUndeployARCs() + + if Shared.GetCheatsEnabled() then + + for index, arc in ientitylist(Shared.GetEntitiesWithClassname("ARC")) do + arc.deployMode = ARC.kDeployMode.Undeploying + end + + end + +end + +local function OnCommandDebugCommander(client, vm) + + if Shared.GetCheatsEnabled() then + BuildUtility_SetDebug(vm) + end + +end + +local function OnCommandRespawnTeam(client, teamNum) + + if Shared.GetCheatsEnabled() then + + teamNum = tonumber(teamNum) + if teamNum == 1 then + GetGamerules():GetTeam1():ReplaceRespawnAllPlayers() + elseif teamNum == 2 then + GetGamerules():GetTeam2():ReplaceRespawnAllPlayers() + end + + end + +end + +local function OnCommandGreenEdition(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player:isa("Marine") then + player:SetVariant("green", "male") + end + + end + +end + +local function OnCommandBlackEdition(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player:isa("Marine") then + player:SetVariant("special", "male") + end + + end + +end + +local function OnCommandMakeSpecialEdition(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player:isa("Marine") then + player:SetVariant("deluxe", "male") + end + + end + +end + +local function OnCommandGreenEditionFemale(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player:isa("Marine") then + player:SetVariant("green", "female") + end + + end + +end + +local function OnCommandBlackEditionFemale(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player:isa("Marine") then + player:SetVariant("special", "female") + end + + end + +end + +local function OnCommandMakeSpecialEditionFemale(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and player:isa("Marine") then + player:SetVariant("deluxe", "female") + end + + end + +end + +local function OnCommandMake(client, sex, variant) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player and HasMixin(player, "PlayerVariant") then + player:SetVariant(variant, sex) + end + + end + +end + +local function OnCommandHell(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player then + + for _, flammable in ipairs(GetEntitiesWithMixin("Fire")) do + flammable:SetOnFire(player, player) + end + + end + + end + +end + +local function OnCommandStoreLastPosition(client) + + if Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player then + gLastPosition = player:GetOrigin() + Print("stored position %s", ToString(gLastPosition)) + end + + end + +end + +local function OnCommandEvolveLastUpgrades(client) + + local player = client:GetControllingPlayer() + if player and player:isa("Alien") and player:GetIsAlive() and not player:isa("Embryo") then + + local upgrades = player.lastUpgradeList + if upgrades and #upgrades > 0 then + player:ProcessBuyAction(upgrades) + end + + end + +end + +local function OnCommandThrowObject(client, season) + + -- need to protect on the serverside too so people don't just trigger mid-game + local player = client:GetControllingPlayer() + if player:GetIsCommander() then + return + end + + if player:GetTeamNumber() ~= kTeamReadyRoom and GetGamerules():GetGameState() >= kGameState.PreGame then + return + end + + FireSeasonalProjectile(player) + +end + +-- +-- hack; turn dev mode on, do a trace, turn dev mode off and dump the trace you got back +-- +local function OnCommandDevTrace(client, box) + + Log("box %s", box) + + if Server and Shared.GetCheatsEnabled() then + + local player = client:GetControllingPlayer() + if player then + + -- trace to a surface and draw the decal + local startPoint = player:GetEyePos() + local endPoint = startPoint + player:GetViewCoords().zAxis * 100 + local trace = nil + Shared.ConsoleCommand("dev 1") + if box then + trace = Shared.TraceBox(Vector(0.1,0.1,0.1), startPoint, endPoint, CollisionRep.Default, PhysicsMask.Bullets, EntityFilterOne(player)) + else + trace = Shared.TraceRay(startPoint, endPoint, CollisionRep.Default, PhysicsMask.Bullets, EntityFilterOne(player)) + end + Shared.ConsoleCommand("dev 0") + + if trace.fraction ~= 1 then + + DebugLine(startPoint, trace.endPoint, 5, 1, 0, 0, 1) + Log("%s: frac %s, ent %s, surf %s", player, trace.fraction, trace.entity, trace.surface) + + end + + end + + end + +end + +Event.Hook("Console_devtrace", OnCommandDevTrace) + +-- GC commands +Event.Hook("Console_changegcsettingserver", OnCommandChangeGCSettingServer) + +-- NS2 game mode console commands +--Event.Hook("Console_jointeamone", OnCommandJoinTeamOne) +--Event.Hook("Console_jointeamtwo", OnCommandJoinTeamTwo) +--Event.Hook("Console_jointeamthree", OnCommandJoinTeamRandom) +--Event.Hook("Console_readyroom", OnCommandReadyRoom) +--Event.Hook("Console_spectate", OnCommandSpectate) +--Event.Hook("Console_film", OnCommandFilm) + +-- Shortcuts because we type them so much +--Event.Hook("Console_j1", OnCommandJoinTeamOne) +--Event.Hook("Console_j2", OnCommandJoinTeamTwo) +--Event.Hook("Console_j3", OnCommandJoinTeamRandom) +--Event.Hook("Console_rr", OnCommandReadyRoom) + +Event.Hook("Console_endgame", OnCommandEndGame) +Event.Hook("Console_logout", OnCommandLogout) +Event.Hook("Console_gotoidleworker", OnCommandGotoIdleWorker) +Event.Hook("Console_gotoplayeralert", OnCommandGotoPlayerAlert) +Event.Hook("Console_selectallplayers", OnCommandSelectAllPlayers) + +-- Cheats +Event.Hook("Console_tres", OnCommandTeamResources) +Event.Hook("Console_pres", OnCommandResources) +Event.Hook("Console_allfree", OnCommandAllFree) +Event.Hook("Console_autobuild", OnCommandAutobuild) +Event.Hook("Console_energy", OnCommandEnergy) +Event.Hook("Console_mature", OnCommandMature) +Event.Hook("Console_takedamage", OnCommandTakeDamage) +Event.Hook("Console_heal", OnCommandHeal) +Event.Hook("Console_giveammo", OnCommandGiveAmmo) +Event.Hook("Console_nanoshield", OnCommandNanoShield) +Event.Hook("Console_parasite", OnCommandParasite) +Event.Hook("Console_respawn_team", OnCommandRespawnTeam) + +Event.Hook("Console_ents", OnCommandEnts) +Event.Hook("Console_sents", OnCommandServerEntities) +Event.Hook("Console_entinfo", OnCommandEntityInfo) + +Event.Hook("Console_switch", OnCommandSwitch) +Event.Hook("Console_damage", OnCommandDamage) +Event.Hook("Console_highdamage", OnCommandHighDamage) +Event.Hook("Console_give", OnCommandGive) +Event.Hook("Console_spawn", OnCommandSpawn) +Event.Hook("Console_storeposition", OnCommandStoreLastPosition) +Event.Hook("Console_shoot", OnCommandShoot) +Event.Hook("Console_giveupgrade", OnCommandGiveUpgrade) +Event.Hook("Console_setfov", OnCommandSetFOV) + +-- For testing lifeforms +Event.Hook("Console_skulk", OnCommandChangeClass("skulk", kTeam2Index)) +Event.Hook("Console_gorge", OnCommandChangeClass("gorge", kTeam2Index)) +Event.Hook("Console_lerk", OnCommandChangeClass("lerk", kTeam2Index)) +Event.Hook("Console_fade", OnCommandChangeClass("fade", kTeam2Index)) +Event.Hook("Console_onos", OnCommandChangeClass("onos", kTeam2Index)) +Event.Hook("Console_marine", OnCommandChangeClass("marine", kTeam1Index)) +Event.Hook("Console_jetpack", OnCommandChangeClass("jetpackmarine", kTeam1Index)) +Event.Hook("Console_exo", OnCommandChangeClass("exo", kTeam1Index, { layout = "ClawMinigun" })) +Event.Hook("Console_dualminigun", OnCommandChangeClass("exo", kTeam1Index, { layout = "MinigunMinigun" })) +Event.Hook("Console_clawrailgun", OnCommandChangeClass("exo", kTeam1Index, { layout = "ClawRailgun" })) +Event.Hook("Console_dualrailgun", OnCommandChangeClass("exo", kTeam1Index, { layout = "RailgunRailgun" })) + +Event.Hook("Console_respawn", OnCommandRespawn) +Event.Hook("Console_respawn_clear", OnCommandRespawnClear) + +Event.Hook("Console_sandbox", OnCommandSandbox) + +Event.Hook("Console_command", OnCommandCommand) +Event.Hook("Console_catpack", OnCommandCatPack) +Event.Hook("Console_alltech", OnCommandAllTech) +Event.Hook("Console_fastevolve", OnCommandFastEvolve) +Event.Hook("Console_location", OnCommandLocation) +Event.Hook("Console_gore", OnCommandGore) +Event.Hook("Console_poison", OnCommandPoison) +Event.Hook("Console_stun", OnCommandStun) +Event.Hook("Console_vortex", OnCommandVortex) +Event.Hook("Console_spit", OnCommandSpit) +Event.Hook("Console_push", OnCommandPush) +Event.Hook("Console_enzyme", OnCommandEnzyme) +Event.Hook("Console_umbra", OnCommandUmbra) +Event.Hook("Console_deployarcs", OnCommandDeployARCs) +Event.Hook("Console_undeployarcs", OnCommandUndeployARCs) + +Event.Hook("Console_closemenu", OnCommandCloseMenu) +Event.Hook("Console_welddoors", OnCommandWeldDoors) +Event.Hook("Console_orderself", OnCommandOrderSelf) + +Event.Hook("Console_create",OnCommandCreate) +Event.Hook("Console_random_debug", OnCommandRandomDebug) +Event.Hook("Console_beacon", OnCommandDistressBeacon) +Event.Hook("Console_setgameeffect", OnCommandSetGameEffect) + +Event.Hook("Console_eject", OnCommandEject) +Event.Hook("Console_cyst", OnCommandCyst) +Event.Hook("Console_target", OnCommandTarget) +Event.Hook("Console_hastech", OnCommandHasTech) +Event.Hook("Console_eggspawntimes", OnCommandEggSpawnTimes) +Event.Hook("Console_gothere", OnCommandGoThere) +Event.Hook("Console_follow", OnCommandFollowAndWeld) +Event.Hook("Console_testorder", OnCommandTestOrder) + +Event.Hook("Console_rupture", OnCommandRupture) +Event.Hook("Console_commanderping", OnCommandCommanderPing) +Event.Hook("Console_threat", OnCommandThreat) +Event.Hook("Console_fire", OnCommandFire) +Event.Hook("Console_makegreen", OnCommandGreenEdition) +Event.Hook("Console_makeblack", OnCommandBlackEdition) +Event.Hook("Console_makespecial", OnCommandMakeSpecialEdition) +Event.Hook("Console_makegreenfemale", OnCommandGreenEditionFemale) +Event.Hook("Console_makeblackfemale", OnCommandBlackEditionFemale) +Event.Hook("Console_makespecialfemale", OnCommandMakeSpecialEditionFemale) +Event.Hook("Console_make", OnCommandMake) + +--Halloween2015 +Event.Hook("Console_throwobject", OnCommandThrowObject) + +Event.Hook("Console_evolvelastupgrades", OnCommandEvolveLastUpgrades) + +Event.Hook("Console_debugcommander", OnCommandDebugCommander) +Event.Hook("Console_hell", OnCommandHell) +Event.Hook("Console_trace", OnCommandTrace) + +Event.Hook("Console_dlc", function(client) + if Shared.GetCheatsEnabled() then + GetHasDLC = function(pid, client) + return true + end + end +end) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCNS2Utility.lua b/challenges/skulk_challenge/lua/SCNS2Utility.lua new file mode 100644 index 000000000..5cfad2f78 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCNS2Utility.lua @@ -0,0 +1,22 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCNS2Utility.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Overrides for contents of lua/NS2Utility.lua. +-- +-- Added a "skulk hologram" grid for the sprite sheet (same as skulk's icon). +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local old_BuildClassToGrid = BuildClassToGrid +function BuildClassToGrid() + + local ClassToGrid = old_BuildClassToGrid() + + ClassToGrid["SkulkHologram"] = ClassToGrid["Skulk"] + + return ClassToGrid + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCPlayer.lua b/challenges/skulk_challenge/lua/SCPlayer.lua new file mode 100644 index 000000000..c8adffc96 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCPlayer.lua @@ -0,0 +1,24 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCPlayer.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Modified to prevent player input when prohibited (eg when they've ended the round prematurely, or they've +-- finished watching a replay.) +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local old_Player_OverrideInput = Player.OverrideInput +function Player:OverrideInput(input) + + input = old_Player_OverrideInput(self, input) + + if self.movementDisabled then + input.commands = 0x00000000 + input.move = Vector(0,0,0) + end + + return input + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCPlayer_Client.lua b/challenges/skulk_challenge/lua/SCPlayer_Client.lua new file mode 100644 index 000000000..52f22270f --- /dev/null +++ b/challenges/skulk_challenge/lua/SCPlayer_Client.lua @@ -0,0 +1,12 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCPlayer_Client.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Disable the minimap, it's not needed. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +function Player:OnShowMap(show) +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCPlayer_Server.lua b/challenges/skulk_challenge/lua/SCPlayer_Server.lua new file mode 100644 index 000000000..4a9bc6d64 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCPlayer_Server.lua @@ -0,0 +1,35 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCPlayer_Server.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Override behavior of Player_Server.lua. +-- Change behavior of dying to just not do anything afterwards. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +-- Copied from Player_Server.lua +local function DestroyViewModel(self) + + assert(self.viewModelId ~= Entity.invalidId) + + DestroyEntity(self:GetViewModelEntity()) + self.viewModelId = Entity.invalidId + +end + +function Player:OnUpdatePlayer(deltaTime) + + -- strip out everything that was there, not needed. + +end + +function Player:OnKill(killer, doer, point, direction) + + -- no need to display any messages or anything. + + GetSkulkChallenge():OnPlayerKilled(self) + DestroyViewModel(self) + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCPreciseDelayedCallback.lua b/challenges/skulk_challenge/lua/SCPreciseDelayedCallback.lua new file mode 100644 index 000000000..b4c23d043 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCPreciseDelayedCallback.lua @@ -0,0 +1,72 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCPreciseDelayedCallback.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Using AddTimedCallback in entities can lead to slightly differing times from run to run, machine to machine. +-- This class provides a more accurate alternative, that relies on Shared.GetSystemTimeReal() instead of +-- Shared.GetTime() +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local callbacks = {} + +if Client or Server then + + -- Create a delayed callback that will trigger the given function as soon as possible after the given delay. + function AddPreciseDelayedCallback(callingEnt, delay, callbackFunction) + + if not callbackFunction or not delay then + return + end + + local newCallback = {} + newCallback.entId = callingEnt:GetId() + newCallback.triggerTime = Shared.GetSystemTimeReal() + delay + newCallback.func = callbackFunction + + callbacks[#callbacks+1] = newCallback + + end + + local function UpdateCallbacks() + + if #callbacks == 0 then + return + end + + local now = Shared.GetSystemTimeReal() + + for i=#callbacks, 1, -1 do + local callback = callbacks[i] + + if now >= callback.triggerTime then + + local entity = nil + if callback.entId then + entity = Shared.GetEntity(callback.entId) + end + + if entity then + -- pass entity as self + callback.func(entity) + else + -- no params + callback.func() + end + + table.remove(callbacks, i) + + end + end + + end + + if Client then + Event.Hook("UpdateClient", UpdateCallbacks) + elseif Server then + Event.Hook("UpdateServer", UpdateCallbacks) + end + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCReadyRoomPlayer.lua b/challenges/skulk_challenge/lua/SCReadyRoomPlayer.lua new file mode 100644 index 000000000..28662d8b0 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCReadyRoomPlayer.lua @@ -0,0 +1,38 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ======= +-- +-- lua\SCReadyRoomPlayer.lua +-- +-- Created by: Trevor "BeigeAlert" Harris (trevor@naturalselection2.com) +-- +-- Overrides behavior of ready room player class. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ======================= + + +local oldReadyRoomPlayer_OnInitialized = ReadyRoomPlayer.OnInitialized +function ReadyRoomPlayer:OnInitialized() + + oldReadyRoomPlayer_OnInitialized(self) + + self:SetIsVisible(false) + +end + +function ReadyRoomPlayer:GetVelocity() + return Vector(0,0,0) +end + +function ReadyRoomPlayer:GetVelocityFromPolar() + return Vector(0,0,0) +end + +function ReadyRoomPlayer:GetGravityEnabled() + return true +end + +function ReadyRoomPlayer:SetGravityEnabled(enabled) +end + +function ReadyRoomPlayer:GetIsVisible() + return false +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCServer.lua b/challenges/skulk_challenge/lua/SCServer.lua new file mode 100644 index 000000000..49e5a70c2 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCServer.lua @@ -0,0 +1,26 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCServer.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Server-only stuff for the Skulk Racing mod. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/SCGamerules.lua") +Script.Load("lua/SCSkulkMoveRecorder.lua") + +gSCTargetPts = {} + +function SC_OnMapLoadEntity(className, groupName, values) + + if className == "target_point" then + + gSCTargetPts[values.targetName] = values.origin + + end + +end + +Event.Hook("MapLoadEntity", SC_OnMapLoadEntity) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCShared.lua b/challenges/skulk_challenge/lua/SCShared.lua new file mode 100644 index 000000000..491db7339 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCShared.lua @@ -0,0 +1,20 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCGamerules.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Stuff that all VMs have in common for Skulk Racing mod. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/SCGlobals.lua") +Script.Load("lua/SCUtils.lua") +Script.Load("lua/SCMapData.lua") +Script.Load("lua/SCFinishLine.lua") +Script.Load("lua/SCSkulkChallenge.lua") +Script.Load("lua/SCSkulkHologram.lua") +Script.Load("lua/SCSpectator.lua") +Script.Load("lua/SCMusicManager.lua") +Script.Load("lua/SCPreciseDelayedCallback.lua") +Script.Load("lua/debugstats.lua") \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCSkulk.lua b/challenges/skulk_challenge/lua/SCSkulk.lua new file mode 100644 index 000000000..467b28c2b --- /dev/null +++ b/challenges/skulk_challenge/lua/SCSkulk.lua @@ -0,0 +1,80 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCSkulk.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- - Report to SkulkChallenge whenever a jump is performed. +-- - Take control of player's movement when they hit the finish line. +-- - Display the skulk's world model instead of first person model during end sequence. +-- - Make skulks not collide with anything except the world. +-- - Set skulks to always propagate to clients (always relevant). +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/SCUtils.lua") + +if Client or Server then + + local old_Skulk_OnJump = Skulk.OnJump + function Skulk:OnJump( modifiedVelocity ) + + old_Skulk_OnJump(self, modifiedVelocity) + + if not self:isa("SkulkHologram") then + GetSkulkChallenge():OnJump() + end + + self.lastJumpTime = Shared.GetTime() + + end + +end + +if Client then + + local old_Skulk_OverrideInput = Skulk.OverrideInput + function Skulk:OverrideInput(input) + + input = old_Skulk_OverrideInput(self, input) + + if self.endSequenceRunning then + + SC_ModifyMoveForEndSequence(input, Target("finish_sequence_end"), self) + + end + + return input + + end + + local old_Skulk_GetDrawWorld = Skulk.GetDrawWorld + function Skulk:GetDrawWorld(isLocal) + + if self.endSequenceRunning then + return true + end + + return old_Skulk_GetDrawWorld(self, isLocal) + + end + +end + +function Skulk:GetMovePhysicsMask() + return PhysicsMask.AllButPCs +end + +local old_Skulk_OnCreate = Skulk.OnCreate +function Skulk:OnCreate() + + old_Skulk_OnCreate(self) + + if Server then + -- Because there will really only ever be 1, maybe 2 skulks in existence, and I just do not want to + -- deal with relevancy on top of everything else... + self:SetPropagate(Entity.Propagate_Always) + end + +end + diff --git a/challenges/skulk_challenge/lua/SCSkulkChallenge.lua b/challenges/skulk_challenge/lua/SCSkulkChallenge.lua new file mode 100644 index 000000000..00ff277ae --- /dev/null +++ b/challenges/skulk_challenge/lua/SCSkulkChallenge.lua @@ -0,0 +1,82 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCSkulkChallenge.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Entity that drives all the behavior for the skulk challenge mode. A bit of an odd-ball in how +-- it works: Since the lion's share of the functionality rests on the Client (leaderboard, replay +-- functionality) and to make presentation a bit easier, all behavior related to the gameplay flow +-- is implemented on the client, with "SC_ServerCommand" network messages going to server for when +-- server functions are needed. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/EntityChangeMixin.lua") + +class 'SkulkChallenge' (Entity) + +SkulkChallenge.kMapName = "skulk_challenge" + +SkulkChallenge.kCommands = enum({ + "Reset_Normal", -- Resets the round for a normal run + "Reset_Ghost", -- Resets the round for a vs. ghost run. + "Reset_Playback", -- Resets the round for a playback run + "BeginPreCountdown", -- Begins recording and/or playback for the run. + "BeginRace", -- Tells the server to do everything it needs to to start the race (eg open the doors) + "TransferReplay", -- Send the replay data to the client. + "EndRace", -- Race has ended prematurely. + "GetJumpCount", -- Get the number of jumps performed from a replay playback. + "HaltPlayback", -- Stops the playback bot. Used when run is ended/reset prematurely. + }) + +-- name -> enum value mapping. +SkulkChallenge.kCommandNames = {} +for i=1, #SkulkChallenge.kCommands do + SkulkChallenge.kCommandNames[SkulkChallenge.kCommands[i]] = i +end + +local netVars = +{ + humanPlayerId = "entityid", + reset = "boolean", +} + +function SkulkChallenge:OnCreate() + + if Server then + InitMixin(self, EntityChangeMixin) + end + + self.humanPlayerId = Entity.invalidId + self.reset = false + +end + +function SkulkChallenge:GetIsMapEntity() + return true +end + +-- Returns human player (not what they're spectating). +function SkulkChallenge:GetHumanPlayer() + + return Shared.GetEntity(self.humanPlayerId) + +end + +Shared.LinkClassToMap("SkulkChallenge", SkulkChallenge.kMapName, netVars) + +Shared.RegisterNetworkMessage("SC_ServerCommand", { command = "enum SkulkChallenge.kCommands" }) +Shared.RegisterNetworkMessage("SC_RaceFinished") +Shared.RegisterNetworkMessage("SC_CameraRelevancy", { offset = "vector" }) +Shared.RegisterNetworkMessage("SC_ReplayReceived") +Shared.RegisterNetworkMessage("SC_RaceEnded") +Shared.RegisterNetworkMessage("SC_JumpCount", { jumpCount = "float" }) -- float for unlimited jumps... + +if Client then + Script.Load("lua/SCSkulkChallenge_Client.lua") +end + +if Server then + Script.Load("lua/SCSkulkChallenge_Server.lua") +end diff --git a/challenges/skulk_challenge/lua/SCSkulkChallenge_Client.lua b/challenges/skulk_challenge/lua/SCSkulkChallenge_Client.lua new file mode 100644 index 000000000..29c5cba27 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCSkulkChallenge_Client.lua @@ -0,0 +1,1413 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCSkulkChallenge_Client.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/challenge/SteamLeaderboardManager.lua") +Script.Load("lua/SCGUILeaderboard.lua") +Script.Load("lua/challenge/GUIChallengeResultsAlien.lua") +Script.Load("lua/SCGUIFader.lua") +Script.Load("lua/challenge/GUIChallengePromptAlien.lua") +Script.Load("lua/SCGUILightArray.lua") +Script.Load("lua/SCGUISplatScreen.lua") +Script.Load("lua/SCGUISpeedometer.lua") + +gSkulkChallengeEntity = nil +function GetSkulkChallenge() + + if not gSkulkChallengeEntity then + gSkulkChallengeEntity = EntityListToTable(Shared.GetEntitiesWithClassname("SkulkChallenge"))[1] + end + + return gSkulkChallengeEntity + +end + +function SkulkChallenge:OnInitialCloudResolved() + + self:OnInitialCloudFinished() + +end + +function SkulkChallenge:OnInitialCloudDeclined(steamToo) + + self.ignoreCloud = true + if steamToo then + self.ignoreSteam = true + end + self:OnInitialCloudFinished() + +end + +local function HideAndDestroyCloudScript(self) + + self.cloudNag:Hide( + function(script) + GetGUIManager():DestroyGUIScript(script) + end) + self.cloudNag = nil + +end + +function SkulkChallenge:InitialCloudStatusCheck() + + local cloudStatus = Client.GetIsSteamCloudEnabled() + + if cloudStatus == NetworkServices.CloudStatus_Enabled then + self:OnInitialCloudResolved() + return true + end + + if cloudStatus == NetworkServices.CloudStatus_DisabledByUserAccount then + if not self.cloudNag then + self.cloudNag = GetGUIManager():CreateGUIScript("GUIChallengePromptAlien") + self.cloudNag:SetLayer(kSkulkChallengeInitialNagLayer) + self.cloudNag:SetPromptText("") + else + self.cloudNag:SetPromptText("STEAM_CLOUD_ACCT_DISABLED_PROMPT") + end + + self.cloudNag:ClearButtons() + self.cloudNag:SetDescriptionText("STEAM_CLOUD_ACCT_DISABLED_DESC") + self.cloudNag:AddButton("STEAM_CLOUD_ACCT_DISABLED_TRY_NOW", + function() + if (self:InitialCloudStatusCheck()) then + HideAndDestroyCloudScript(self) + end + end) + self.cloudNag:AddButton("STEAM_CLOUD_ACCT_DISABLED_NO_THANKS", + function() + HideAndDestroyCloudScript(self) + self:OnInitialCloudDeclined() + end) + self.cloudNag:AddButton("QUIT", + function() + self:OnQuitClicked() + end) + self.cloudNag:SetIcon("halt") + self.cloudNag:Show() + + elseif cloudStatus == NetworkServices.CloudStatus_SteamDown then + if not self.cloudNag then + self.cloudNag = GetGUIManager():CreateGUIScript("GUIChallengePromptAlien") + self.cloudNag:SetLayer(kSkulkChallengeInitialNagLayer) + end + + self.cloudNag:ClearButtons() + self.cloudNag:SetPromptText("STEAM_CLOUD_STEAM_DOWN_PROMPT") + self.cloudNag:SetDescriptionText("STEAM_CLOUD_STEAM_DOWN_DESC") + self.cloudNag:AddButton("RETRY", + function() + if (self:InitialCloudStatusCheck()) then + HideAndDestroyCloudScript(self) + end + end) + self.cloudNag:AddButton("IGNORE", + function() + HideAndDestroyCloudScript(self) + self:OnInitialCloudDeclined(true) + end) + self.cloudNag:AddButton("QUIT", + function() + self:OnQuitClicked() + end) + self.cloudNag:SetIcon("halt") + self.cloudNag:Show() + + elseif cloudStatus == NetworkServices.CloudStatus_SteamRemoteStorageDown or cloudStatus == NetworkServices.CloudStatus_NetworkServicesUnavailable then + if not self.cloudNag then + self.cloudNag = GetGUIManager():CreateGUIScript("GUIChallengePromptAlien") + self.cloudNag:SetLayer(kSkulkChallengeInitialNagLayer) + end + + self.cloudNag:ClearButtons() + self.cloudNag:SetPromptText("STEAM_CLOUD_GAME_DOWN_PROMPT") + self.cloudNag:SetDescriptionText("STEAM_CLOUD_GAME_DOWN_DESC") + self.cloudNag:AddButton("IGNORE", + function() + HideAndDestroyCloudScript(self) + self:OnInitialCloudDeclined(true) + end) + self.cloudNag:AddButton("QUIT", + function() + self:OnQuitClicked() + end) + self.cloudNag:SetIcon("halt") + self.cloudNag:Show() + + elseif cloudStatus == NetworkServices.CloudStatus_DisabledByApp then + -- They don't have a score to upload anyways, so we just won't worry about it here. We'll prompt them about + -- this once they've finished their first run. For now, treat it like it's enabled. + self:OnInitialCloudResolved() + return true + + end + + return false + +end + +local cloudErrorNames = nil + +-- Cloud status check for when we're about to attempt to upload a replay. +function SkulkChallenge:CloudStatusCheck() + + -- Have to declare them here, unfortunately. These "CloudStatus_" constants aren't defined until after the client is + -- loaded. :( + if not cloudErrorNames then + cloudErrorNames = + { + [NetworkServices.CloudStatus_DisabledByUserAccount] = "CloudStatus_DisabledByUserAccount", + [NetworkServices.CloudStatus_SteamDown] = "CloudStatus_SteamDown", + [NetworkServices.CloudStatus_SteamRemoteStorageDown] = "CloudStatus_SteamRemoteStorageDown", + [NetworkServices.CloudStatus_NetworkServicesUnavailable] = "CloudStatus_NetworkServicesUnavailable", + [NetworkServices.CloudStatus_DisabledByApp] = "CloudStatus_DisabledByApp", + } + end + + local cloudStatus = Client.GetIsSteamCloudEnabled() + + if cloudStatus == NetworkServices.CloudStatus_Enabled then + self:CloudStatusResolved(true) + return true + end + + -- Check if they've already given consent to enable it just for replays. + if cloudStatus == NetworkServices.CloudStatus_DisabledByApp and self.onlyReplaysAllowed == true then + self:CloudStatusResolved(true) + return true + end + + if cloudStatus == NetworkServices.CloudStatus_DisabledByApp then + if not self.cloudNag then + self.cloudNag = GetGUIManager():CreateGUIScript("GUIChallengePromptAlien") + self.cloudNag:SetLayer(kSkulkChallengeNagLayer) + end + + self.cloudNag:ClearButtons() + self.cloudNag:SetPromptText("STEAM_CLOUD_APP_DISABLED_PROMPT") + self.cloudNag:SetDescriptionText("STEAM_CLOUD_APP_DISABLED_DESC") + self.cloudNag:AddButton("STEAM_CLOUD_YES", + function() + Client.SetIsSteamCloudEnabled(true) + HideAndDestroyCloudScript(self) + self:CloudStatusResolved(true) + end) + + self.cloudNag:AddButton("STEAM_CLOUD_APP_DISABLED_JUST_FOR_REPLAYS", + function() + self.onlyReplaysAllowed = true + HideAndDestroyCloudScript(self) + self:CloudStatusResolved(true) + end) + + self.cloudNag:AddButton("STEAM_CLOUD_NO", + function() + HideAndDestroyCloudScript(self) + self.ignoreCloud = true + self:CloudStatusResolved(false) + end) + + self.cloudNag:SetIcon("choice") + + self.cloudNag:Show() + return false + + end + + -- If we made it to here... we don't know what the issue is. Perhaps they lost their steam/internet connection + -- during the run. + if not self.cloudNag then + self.cloudNag = GetGUIManager():CreateGUIScript("GUIChallengePromptAlien") + self.cloudNag:SetLayer(kSkulkChallengeNagLayer) + end + + self.cloudNag:ClearButtons() + self.cloudNag:SetPromptTextLiteral(cloudErrorNames[cloudStatus] or "unknown error") + self.cloudNag:SetDescriptionText("STEAM_CLOUD_ODD_ERROR") + + self.cloudNag:AddButton("RETRY", + function() + if self:CloudStatusCheck() then + HideAndDestroyCloudScript(self) + end + end) + + self.cloudNag:AddButton("IGNORE", + function() + HideAndDestroyCloudScript(self) + self:CloudStatusResolved(false) + end) + + self.cloudNag:SetIcon("halt") + + self.cloudNag:Show() + return false + +end + +function SkulkChallenge:OnInitialCloudFinished() + + self.initialCloudFinished = true + +end + +function SkulkChallenge:OnInitialized() + + self.callbackFunctions = {} + self.waits = {} + + self.fader = GetGUIManager():CreateGUIScript("SCGUIFader") + self.fader:ToBlackImmediately() + self.fader:SetLayer(kSkulkChallengeFaderLayer) + + self.dimmer = GetGUIManager():CreateGUIScript("SCGUIFader") + self.dimmer:SetOpacity(0.5) + self.dimmer:SetLayer(kSkulkChallengeDimmerLayer) + + self.speedometer = GetGUIManager():CreateGUIScript("SCGUISpeedometer") + self.speedometer:SetLayer(kSkulkChallengeSpeedometerLayer) + + CreateGuideMeshes() + + self.runType = "normal" + self.running = false + self.speed = 0.0 + self.tipsEnabled = false -- tips will only be enabled if the player hasn't earned a medal yet. + + -- until player completes one run, we have nothing to replay! (their leaderboard entry is a separate thing.) + self.hasLastRunGhost = false + + self.waitingOnInitialAsyncTasks = true + + -- Check if player has steam cloud enabled, and if not, warn them ahead of time that they won't + -- be able to upload replays without steam cloud on. + self:InitialCloudStatusCheck() + + -- Request player's score data from Steam + self.waitingForPlayerInfo = Shared.GetTime() + GetSteamLeaderboardManager():RequestPlayerScore(SC_GetLeaderboardName(), + function(success, entryTable) + if success and #entryTable == 1 then + self.localPlayerCurrentBest = entryTable[1].score / 1000.0 + + -- retroactively give badges to those who tested skulk challenge before badges were implemented. + SC_AwardBadgeForTime(self.localPlayerCurrentBest) + end + self.waitingOnPlayerInfo = false + end) + + self:SetUpdates(true) + + -- precache text crisping shader for splat screen. + local precacheText = GUI.CreateItem() + precacheText:SetShader(SCGUISplatScreenText.kTextShader) + GUI.DestroyItem(precacheText) + +end + +function SkulkChallenge:GetBestTime() + + return self.localPlayerCurrentBest + +end + +function SkulkChallenge:FadeOutTimers() + + if self.timer then + self.timer:SetOpacityTarget(0.0) + end + + if self.goalTimer then + self.goalTimer:SetOpacityTarget(0.0) + end + + if self.bestTimer then + self.bestTimer:SetOpacityTarget(0.0) + end + +end + +-- has the round been going on long enough to show tips? +function SkulkChallenge:GetCanShowTips() + + if not self.tipsEnabled then + return false + end + + if not self:GetIsRunning() then + return false + end + + if not self:GetIsHumanParticipating() then + return false + end + + if not self:GetHumanPlayer() then + return false + end + + if not self.timer or self.timer:GetTime() < kSkulkChallengeTipStartTime then + return false + end + + return true + +end + +-- Create/reset the timer gui scripts. +function SkulkChallenge:SetupTimers() + + -- Ensure the current round timer exists + if not self.timer then + self.timer = GetGUIManager():CreateGUIScript("GUITimeDisplay") + end + + self.timer:SetIndexedPosition(0) + self.timer:Reset() + self.timer:SetOpacityInstantly(1.0) + + local timerIndex = 1 + -- Create a static time display for the next medal the player should shoot for, if any. + local nextMedal = SC_GetNextMedal(self:GetBestTime()) + if nextMedal == "bronze" then + self.tipsEnabled = true + else + self.tipsEnabled = false + end + + local nextMedalTime = SC_GetMedalTime(nextMedal) + if nextMedalTime then + -- create timer if needed + if not self.goalTimer then + self.goalTimer = GetGUIManager():CreateGUIScript("GUITimeDisplay") + end + self.goalTimer:Reset() + self.goalTimer:SetTime(nextMedalTime) + self.goalTimer:SetIcon(SC_GetMedalIcon(nextMedal)) + self.goalTimer:SetColor(SC_GetMedalTextColor(nextMedal)) + self.goalTimer:SetIndexedPosition(timerIndex) + self.goalTimer:SetOpacityInstantly(1.0) + timerIndex = timerIndex + 1 + else + -- destroy goal timer since it is no longer needed. + if self.goalTimer then + GetGUIManager():DestroyGUIScript(self.goalTimer) + self.goalTimer = nil + end + end + + -- Create a time display for the player's best run, if it exists. + local bestTime = self:GetBestTime() + if bestTime then + if not self.bestTimer then + self.bestTimer = GetGUIManager():CreateGUIScript("GUITimeDisplay") + end + self.bestTimer:Reset() + self.bestTimer:SetTime(bestTime) + self.bestTimer:SetIndexedPosition(timerIndex) + self.bestTimer:SetOpacityInstantly(1.0) + self.bestTimer:SetIcon("*avatar") + end + +end + +function SkulkChallenge:SendResetCommandToServer() + + local command + if self.runType == "normal" then + command = "Reset_Normal" + elseif self.runType == "ghost" then + command = "Reset_Ghost" + elseif self.runType == "playback" then + command = "Reset_Playback" + end + + assert(command) + + self:SendCommandToServer(command, + function(self) + + self:AddTimedCallback( + function() + + if self.reset then + self:BeginRun() + return false + end + return true + + end, 0.25) + + end) + +end + +-- Called when the player's screen has faded completely to black. This is called "between" rounds, to begin the next +-- round, or called at the beginning to start the cycle. +function SkulkChallenge:ResetRun() + + self:SetupTimers() + + self.endSequenceRunning = false + + -- Stop any music that is playing... + Shared.StopSound(nil, kSkulkChallengeFinishMusic) + Shared.StopSound(nil, kSkulkChallengeDeathMusic) + GetMusicManager():StopMusic() + + -- calls self:BeginRun() after sending the reset command to the server and waiting for the skulks to be reset. + self:SendResetCommandToServer() + +end + +-- Creates a "splat", a full screen graphic with a message (eg "GO!", and "FINISH!"), animates it onto the screen, and then +-- automatically destroys it after animating it off. +local function DoSplat(self, iconName, textLiteral, inAnimName, holdDuration, color) + + local splat = GetGUIManager():CreateGUIScript("SCGUISplatScreen") + splat:SetGraphic(iconName) + splat:SetText(textLiteral) + splat:AnimateIn(inAnimName) + splat:SetColor(color) + splat:SetLayer(kSkulkChallengeSplatLayer) + + self:AddTimedCallback( + function(self, delta) + splat:AnimateOut() + end, holdDuration) + +end + +function SkulkChallenge:BeginRun_AfterMusic() + + if self.runCancelled then + return + end + + -- Pre-countdown wait. + -- Either start recording, or begin playback, or both, depending on the type of run. + self:SendCommandToServer("BeginPreCountdown") + + -- countdown gui fly onto screen from above. + self.countdownScript:FlyIn() + + AddPreciseDelayedCallback(self, kSkulkChallengePreCountdownDuration - SCGUILightArray.kInitialDelay, + function(self) + + if self.runCancelled then + return + end + + -- Will automatically turn off effects after a certain amount of time, then will destroy them after + -- an even further amount of time. Designed to not be tripped up by several overlapping calls. + SC_TriggerEffects("countdown") + + -- Start countdown + self.countdownScript:DoCountdown( + function() + + if self.runCancelled then + return + end + + -- make countdown script fly away after 2 seconds. + self.countdownScript:AddDelayedAction(2.0, + function(cdScript, action) + cdScript:Dispose() + + -- script will automatically destroy itself, so remove our reference now. + self.countdownScript = nil + end) + + -- Race has started + self:SendCommandToServer("BeginRace", nil) + self.timer:Start() + self.running = true + + self.topSpeed = 0.0 + self.speedAccumulation = 0.0 + self.timeAccumulation = 0.0 + self.jumpCount = 0 + + -- "GO!" splat + DoSplat(self, + "go", + Locale.ResolveString("SKULK_CHALLENGE_GO"), + "go", + kSkulkChallengeSplatTimeShort, + kSkulkChallengeGoTextColor) + + end) + + return false + + end) + + return false + +end + +function SkulkChallenge:BeginRun() + + -- Make the player's view face the doors + local coords = Coords.GetLookAt(Target("starting_position"), Target("start_doors"), Vector(0,1,0)) + local angles = Angles() + angles:BuildFromCoords(coords) + local player = Client:GetLocalPlayer() + player:SetOffsetAngles(angles) + + self.runCancelled = false + + -- ensure none of the countdown effects are still lingering -- possible if round is reset while countdown is happening. + SC_DestroyEffectsImmediately("countdown") + + self.speedometer:SetIsVisible(true) + + -- display countdown gui + assert(self.countdownScript == nil) + self.countdownScript = GetGUIManager():CreateGUIScript("SCGUILightArray") + + self.dimmer:FadeInImmediately() -- ensure dimmer isn't still... erm... dim + self.fader:DoFadeIn(kSkulkChallengeFadeDuration, + function() + if self.runCancelled then + return + end + + -- ensure the music has enough time to hit it's mark at the race start. + local pregameDuration = kSkulkChallengePreCountdownDuration + kSkulkChallengeCountdownDuration + local preRollOverflow = GetMusicManager():GetPreRollOverflow(pregameDuration) + + GetMusicManager():ScheduleMusicStart(Shared.GetSystemTimeReal() + pregameDuration + preRollOverflow) + + if preRollOverflow > 0.0 then + AddPreciseDelayedCallback(self, preRollOverflow, SkulkChallenge.BeginRun_AfterMusic) + else + self:BeginRun_AfterMusic() + end + + end) + +end + +function SkulkChallenge:OnQuitClicked() + + if self.resultsScript then + self.resultsScript:SetWindowActive("fadingOut", false) + end + + if self.leaderboardScript then + self.leaderboardScript:SetWindowActive("fadingOut", false) + end + + if self.cloudNag then + self.cloudNag:SetWindowActive("fadingOut", false) + end + + GetMusicManager():StopMusic() + + self.fader:DoFadeOut(kSkulkChallengeFadeDuration, + function() + Shared.ConsoleCommand("disconnect") + end) + +end + +function SkulkChallenge:OnResultsPlayClicked() + + self.resultsScript:SetWindowActive("fadingOut", false) + if self.leaderboardScript then + self.leaderboardScript:SetWindowActive("fadingOut", false) + end + + -- Stop any music that is playing... + Shared.StopSound(nil, kSkulkChallengeFinishMusic) + Shared.StopSound(nil, kSkulkChallengeDeathMusic) + GetMusicManager():StopMusic() + + self.fader:DoFadeOut(kSkulkChallengeFadeDuration, + function() + self:DestroyLeaderboard() + self:DestroyResults() + + self.runType = "normal" + self:ResetRun() + end) + +end + +-- Player will play their ghost from the previous round. +function SkulkChallenge:OnResultsPlayGhostClicked() + + -- Begin a new round against a ghost of the replay that is loaded on the Server. + + self.resultsScript:SetWindowActive("fadingOut", false) + if self.leaderboardScript then + self.leaderboardScript:SetWindowActive("fadingOut", false) + end + + -- Stop any music that is playing... + Shared.StopSound(nil, kSkulkChallengeFinishMusic) + Shared.StopSound(nil, kSkulkChallengeDeathMusic) + GetMusicManager():StopMusic() + + self.fader:DoFadeOut(kSkulkChallengeFadeDuration, + function() + self:DestroyLeaderboard() + self:DestroyResults() + + self.runType = "ghost" + SC_SendReplay(GetReplayManager():GetReplay(nil)) + end) + +end + +function SkulkChallenge:DimScreen(state, callback) + + if state then + -- dim + self.dimmer:DoFadeOut(0.5, callback) + else + -- un-dim + self.dimmer:DoFadeIn(0.5, callback) + end + +end + +function SkulkChallenge:ShowResults(resultsTable) + + if self.resultsScript then + return -- results are already being displayed + end + + self.resultsScript = GetGUIManager():CreateGUIScript("GUIChallengeResultsAlien") + self.resultsScript:SetParentEntity(self) -- so we can receive callbacks + self.resultsScript:AddButton("RETRY_SOLO", function() self:OnResultsPlayClicked() end, "retry") + self.resultsScript:AddButton("RACE_LAST_GHOST", function() self:OnResultsPlayGhostClicked() end, "ghost") + self.resultsScript:SetButtonEnabled(self.resultsScript:GetButtonByName("ghost"), self.hasLastRunGhost) + self.resultsScript:SetLayer(kSkulkChallengeResultsLayer) + + -- begin all the animations, and show leaderboard once results screen is done animating. + self.resultsScript:ShowWithResults(resultsTable, + function() + local self = GetSkulkChallenge() + self:ShowLeaderboard() + end) + + self:FadeOutTimers() -- hide timer displays so they aren't weirdly behind results screen. + +end + +function SkulkChallenge:ShowLeaderboard() + + if self.leaderboardScript then + return -- already being displayed. + end + + self.leaderboardScript = GetGUIManager():CreateGUIScript("SCGUILeaderboard") + self.leaderboardScript:SetParentEntity(self) + self.leaderboardScript:SetSteamLeaderboardName(SC_GetLeaderboardName()) + self.leaderboardScript:DoFadeInAnimation( + function(board) + -- as soon as it's done with this fade-in animation, put ourselves in a state where we'll be waiting + -- for downloaded leaderboard entries (download was started via SetSteamLeaderboardName). + board.animation = "waitingForDownload" + end) + self.leaderboardScript:SetLayer(kSkulkChallengeLeaderboardLayer) + + if self.resultsScript then + self.leaderboardScript:AddSiblingScript(self.resultsScript) + end + +end + +-- the "Do" implies it's not an immediate thing, and that it has a callback. +function SkulkChallenge:DoHideLeaderboard(callbackFunction) + + if not self.leaderboardScript then + return -- not being displayed + end + +end + +-- Destroys leaderboard without waiting for any fancy effects to finish. +function SkulkChallenge:DestroyLeaderboard() + + if not self.leaderboardScript then + return + end + + GetGUIManager():DestroyGUIScript(self.leaderboardScript) + self.leaderboardScript = nil + +end + +function SkulkChallenge:DestroyResults() + + if not self.resultsScript then + return + end + + GetGUIManager():DestroyGUIScript(self.resultsScript) + self.resultsScript = nil + +end + +function SkulkChallenge:LoadReplay(replayData, score) + + assert(replayData) + + -- ensure user cannot click anything else in this time. + if self.leaderboardScript then + self.leaderboardScript:SetWindowActive("fadingOut", false) + end + + if self.resultsScript then + self.resultsScript:SetWindowActive("fadingOut", false) + end + + -- Stop any music that is playing... + Shared.StopSound(nil, kSkulkChallengeFinishMusic) + Shared.StopSound(nil, kSkulkChallengeDeathMusic) + GetMusicManager():StopMusic() + + self.fader:DoFadeOut(kSkulkChallengeFadeDuration, + function() + self:DestroyLeaderboard() + self:DestroyResults() + + -- Replay data has to be on the Server, not the Client. + SC_SendReplay(replayData) + end) + + self.runType = "playback" + + -- store this so we can fudge it to match. Replays won't get the EXACT same score... they'll be close, but not exact, + -- and that's due to a lack of determinism in the way NS2 was written. :( Nothing we can do about that at this point. + self.replayScore = score + +end + +function SkulkChallenge:LoadGhost(replayData) + + assert(replayData) + + -- ensure user cannot click anything else in this time. + if self.leaderboardScript then + self.leaderboardScript:SetWindowActive("fadingOut", false) + end + + if self.resultsScript then + self.resultsScript:SetWindowActive("fadingOut", false) + end + + -- Stop any music that is playing... + Shared.StopSound(nil, kSkulkChallengeFinishMusic) + Shared.StopSound(nil, kSkulkChallengeDeathMusic) + GetMusicManager():StopMusic() + + self.fader:DoFadeOut(kSkulkChallengeFadeDuration, + function() + self:DestroyLeaderboard() + self:DestroyResults() + + -- Replay data has to be on the Server, not the Client. + SC_SendReplay(replayData) + end) + + self.runType = "ghost" + +end + +function SkulkChallenge:UploadReplay() + + if self.onlyReplaysAllowed then + Client.SetIsSteamCloudEnabled(true) + end + + GetSteamLeaderboardManager():UploadReplay(SC_GetReplayFileName(), GetReplayManager():GetReplay(nil), Challenge.ReplayType_SkulkChallenge, + function(success, ugcHandle, fileName) + + if success then + + GetSteamLeaderboardManager():AttachUGCToLeaderboard(ugcHandle, SC_GetLeaderboardName(), + function(success, errorCode) + + if self.onlyReplaysAllowed then + Client.SetIsSteamCloudEnabled(false) + end + + self:DoneWithReplayUpload() + + end) + + else + + Log("ERROR: Unable to upload replay to leaderboard.") + + if self.onlyReplaysAllowed then + Client.SetIsSteamCloudEnabled(false) + end + + self:DoneWithReplayUpload() + -- TODO retry option? + + end + + end) + +end + +function SkulkChallenge:OnReplayReceived(replayData) + + -- store the user's last run locally. + GetReplayManager():AddReplay(replayData, nil) + self.receivedReplay = true + +end + +function SkulkChallenge:SendCommandToServer(commandName, callbackFunction) + + assert(self.callbackFunctions[commandName] == nil) + assert(self.kCommandNames[commandName] ~= nil) + + self.callbackFunctions[commandName] = callbackFunction + Client.SendNetworkMessage("SC_ServerCommand", { command = self.kCommandNames[commandName] }, true) + +end + +local function OnSCServerCommand(msg) + + local self = GetSkulkChallenge() + + local commandName = EnumToString(self.kCommands, msg.command) + if self.callbackFunctions[commandName] then + -- use a temporary variable to store the function, because we want to set it to nil, but also don't want to + -- overwrite any callbacks that are created by the callback function we're calling. + local tempCallbackFunc = self.callbackFunctions[commandName] + self.callbackFunctions[commandName] = nil + tempCallbackFunc(self) + end + +end +Client.HookNetworkMessage("SC_ServerCommand", OnSCServerCommand) + +-- Attempt to upload the replay of the last run, if we can upload. Otherwise, just skip it. +function SkulkChallenge:CloudStatusResolved(canUpload) + + if not canUpload then + self:DoneWithReplayUpload() + return + end + + self:UploadReplay() + +end + +-- Generates the table of data that is fed to the results GUI script. It uses as much data as it can find, +-- ignoring data that is missing. +function SkulkChallenge:GenerateResultsTable() + + local medalName = nil + local currentTime = self.timer:GetTime() + local bestTime = self:GetBestTime() + + if not bestTime or currentTime < bestTime then + bestTime = currentTime + end + + local nextMedalTime = SC_GetMedalTime(SC_GetNextMedal(bestTime)) + + local topSpeed = self.topSpeed + local averageSpeed = self.averageSpeed + local jumpCount = self.jumpCount + + local results = {} + results.rows = {} + + -- Only display the user's time and award them a medal if they actually finished the run. + if not self.endedPrematurely then + + medalName = SC_GetMedalForTime(currentTime) + table.insert(results.rows, { type = "Time", name = "TIME", value = currentTime } ) + + end + + if self.runType == "normal" or self.runType == "ghost" then + + -- only display personal stats if the player was actually the one racing + table.insert(results.rows, { type = "Time", name = "BEST_TIME", value = bestTime } ) + table.insert(results.rows, { type = "Time", name = "NEXT_MEDAL_TIME", value = nextMedalTime } ) + table.insert(results.rows, { type = "Divider" } ) + + end + + table.insert(results.rows, { type = "Speed", name = "TOP_SPEED", value = topSpeed } ) + table.insert(results.rows, { type = "Speed", name = "AVERAGE_SPEED", value = averageSpeed } ) + table.insert(results.rows, { type = "Integer", name = "JUMP_COUNT", value = jumpCount } ) + + results.medalAwarded = medalName + results.title = SC_GetResultsScreenTitle() + + return results + +end + +-- Control arrives here when a replay is done uploading, or it is skipped, or the player just got done viewing a +-- replay. +function SkulkChallenge:DoneWithReplayUpload() + + self:OnSCEndSequenceFinished() + +end + +function SkulkChallenge:OnJump() + + if self.running then + self.jumpCount = self.jumpCount + 1 + end + +end + +-- sends the actual relevancy message, called by the cooldown function. +function SkulkChallenge:SendRelevancyMessage() + + local offset = Vector(0,0,0) + local player = Client.GetLocalPlayer() + + if self.endSequenceRunning and self.relevancyLocation and player then + offset = self.relevancyLocation - player:GetOrigin() + end + + Client.SendNetworkMessage("SC_CameraRelevancy", {offset = offset}, true) + +end + +-- Handles the cooldown for updating relevancy location. +function SkulkChallenge:UpdateRelevancy(deltaTime) + + if not self.relevancyCooldown or self.relevancyCooldown <= 0.0 then + self.relevancyCooldown = 1.0 + self:SendRelevancyMessage() + else + self.relevancyCooldown = (self.relevancyCooldown or 0.0) - deltaTime + end + +end + +function SkulkChallenge:GetCurrentSpeed() + + if self.endSequenceRunning then + return 0.0 + end + + return self.speed or 0.0 + +end + +function SkulkChallenge:OnUpdate(deltaTime) + + -- Update game start (wait for async tasks to complete or timeout before beginning) + if self.waitingOnInitialAsyncTasks then + if self.initialCloudFinished then + if self.waitingOnPlayerInfo == false or (type(self.waitingOnPlayerInfo) == "number" and (Shared.GetTime() - self.waitingOnPlayerInfo) > 5.0) then + -- wait only 5 seconds to hear back from steam about player info, otherwise skip it. + self.waitingOnInitialAsyncTasks = false + self:ResetRun() + end + end + end + + -- Update stats while running. + self.speed = Client.GetLocalPlayer():GetVelocity():GetLengthXZ() + if self.running then + + self.speedAccumulation = self.speedAccumulation + self.speed * deltaTime + self.timeAccumulation = self.timeAccumulation + deltaTime + self.topSpeed = math.max(self.topSpeed, self.speed) + + end + + self:UpdateRelevancy(deltaTime) + +end + +-- Called from Client.lua +-- Used to move the camera around independent of the player model during the finish line sequence. +function OnUpdateRenderOverride() + + local self = GetSkulkChallenge() + if not self or not self.endSequenceRunning or not gSCTargetPts["finish_camera_pos"] then + return false + end + + local now = Shared.GetTime() + local deltaTime + if self.prevSequenceTime then + deltaTime = now - self.prevSequenceTime + else + deltaTime = 0.0 + end + + self.prevSequenceTime = now + + local player = Client.GetLocalPlayer() + if not player then + return false + end + + if not self.cameraPos then + -- start the sequence from the player's POV. + self.cameraPos = player:GetCameraViewCoords().origin + return false + end + + local interpVal = 1.0 - math.pow( kSkulkChallengeEndSequenceCameraLerpFactor, deltaTime) + self.cameraPos = self.cameraPos * (1.0 - interpVal) + Target("finish_camera_pos") * interpVal + + local coords = Coords.GetLookAt(self.cameraPos, player:GetEngagementPoint(), Vector(0,1,0)) + + self.relevancyLocation = coords.origin -- store for later. + + gRenderCamera:SetCoords(coords) + Client.SetRenderCamera(gRenderCamera) + + HiveVision_SetEnabled(false) + EquipmentOutline_SetEnabled(false) + + return true + +end + +local endSequenceWaitingCallCount = 0 +function SkulkChallenge:OnSCEndSequenceFinished() + + endSequenceWaitingCallCount = endSequenceWaitingCallCount - 1 + + if endSequenceWaitingCallCount > 0 then + -- still waiting on one or more asynchronous tasks to wrap up before we do this. + return + end + + -- Create the table that will be fed to the results GUI script. + local results = self:GenerateResultsTable() + self:DimScreen(true, + function() + self:ShowResults(results) + end) + +end + +-- The player successfully completed the race, and has a new time and replay available. +local function OnSCRaceFinished() + + local self = GetSkulkChallenge() + + self.timer:Stop() + self.timer:Quantize() -- truncate to milliseconds. + + self.running = false + + self.averageSpeed = self.speedAccumulation / self.timeAccumulation + self.endedPrematurely = false + + -- Play winning music + GetMusicManager():StopMusic() + Shared.PlaySound(nil, kSkulkChallengeFinishMusic, GetMusicManager():GetVolumeModifier()) + + -- Short ending sequence showing skulk running through tunnel. + self.endSequenceRunning = true + Client.GetLocalPlayer().endSequenceRunning = true + self.prevSequenceTime = nil + self.cameraPos = nil + + self.speedometer:SetIsVisible(false) + + -- During the end sequence, we are also attempting to upload the user's score and replay to steam. + -- Therefore, we have two racing conditions: the uploads (they could both finish before sequence has ended), + -- and the sequence itself (it could finish first if something goes wrong with the upload.)j + -- Wait until both of these have finished before displaying the results screen. + endSequenceWaitingCallCount = endSequenceWaitingCallCount + 2 + self:AddTimedCallback(SkulkChallenge.OnSCEndSequenceFinished, kSkulkChallengeEndSequenceDuration) + + -- only submit scores if the player was actually playing! + if self.runType == "normal" or self.runType == "ghost" then + + -- Transfer the replay, to keep a local copy, and possibly to upload to steam. + self.receivedReplay = false + self:SendCommandToServer("TransferReplay") + self.hasLastRunGhost = true + + local previousBest = self:GetBestTime() + + -- Award player badge if they've got a good enough time. + if (not previousBest or previousBest > self.timer:GetTime()) then + SC_AwardBadgeForTime(self.timer:GetTime()) + end + + -- Play "FINISH" splat, and if they got a new personal best, display a splat for that too. + if previousBest and previousBest > self.timer:GetTime() then + + -- Finish splat + DoSplat(self, + "finish", + Locale.ResolveString("SKULK_CHALLENGE_FINISH"), + "regular", + kSkulkChallengeSplatTimeShort, + kSkulkChallengeFinishTextColor) + + -- New personal record splat. + self:AddTimedCallback( + function(self) + DoSplat(self, + "go", -- fired-up looking skulk graphic... nothing to do with "go", really. + Locale.ResolveString("SKULK_CHALLENGE_NEW_PERSONAL_BEST"), + "regular", + kSkulkChallengeSplatTimeShort, + kSkulkChallengeGoTextColor) + end, kSkulkChallengeSplatTimeShort + kSkulkChallengeSplatTimeGap) + + -- Clear the replay cache. We will need to re-download the player's replay. + GetReplayManager():ClearCachedReplaysFromLeaderboard() + + else + + -- Finish splat + DoSplat(self, + "finish", + Locale.ResolveString("SKULK_CHALLENGE_FINISH"), + "regular", + kSkulkChallengeSplatTimeLong, + kSkulkChallengeFinishTextColor) + end + + -- Keep track of the player's current best score locally, for quick lookup. + if self.localPlayerCurrentBest then + self.localPlayerCurrentBest = math.min(self.localPlayerCurrentBest, self.timer:GetTime()) + else + self.localPlayerCurrentBest = self.timer:GetTime() + end + + -- hold until we've received the replay from the server + self:AddTimedCallback( + function() + + if self.receivedReplay then + + -- Skip uploading, etc if we're set to ignoreSteam + if self.ignoreSteam then + -- Go straight to results screen. + self:OnSCEndSequenceFinished() + + else + -- Attempt to upload this score to Steam. + GetSteamLeaderboardManager():UploadScore(SC_GetLeaderboardName(), math.floor(self.timer:GetTime() * 1000.0), nil, + function(success, uploadResult) + if success then + if (not previousBest or previousBest > self.timer:GetTime()) and not self.ignoreCloud then + -- attempt to upload replay of score. + self:CloudStatusCheck() -- passes control to CloudStatusResolved() + else + -- score wasn't better, or we're not uploading replays. + self:DoneWithReplayUpload() + end + else + Log("ERROR: Unable to upload score to Steam!") + self:DoneWithReplayUpload() + end + end) + end + + return false + + else + return true + end + + end, 0.25) + + + elseif self.runType == "playback" then + + self:SendCommandToServer("GetJumpCount") + + -- Set it to the time we know it was when the person recorded the replay, otherwise it may be off by a few + -- hundredths or even tenths of a second and that causes... forum posts....... + self.timer:SetTime(self.replayScore * 0.001) + + -- Go straight to the results/leaderboard screen. + self:OnSCEndSequenceFinished() + self:GetHumanPlayer().movementDisabled = true + + -- Play "FINISH" splat + DoSplat(self, + "finish", + Locale.ResolveString("SKULK_CHALLENGE_FINISH"), + "regular", + kSkulkChallengeSplatTimeLong, + kSkulkChallengeFinishTextColor) + + end + +end +Client.HookNetworkMessage("SC_RaceFinished", OnSCRaceFinished) + +-- Called when the server has received a replay from the client. +local function OnReplayReceivedOnServer() + + local self = GetSkulkChallenge() + + -- Begin the next round... + self:ResetRun() + +end +Client.HookNetworkMessage("SC_ReplayReceived", OnReplayReceivedOnServer) + +function SkulkChallenge:GetIsRunning() + + return (self.running == true) + +end + +function SkulkChallenge:InterruptRun() + + self:GetHumanPlayer().movementDisabled = true + + self:SendCommandToServer("HaltPlayback") + + self.timer:Stop() + self.timer:Quantize() + self.running = false + self.runCancelled = true -- in case they stopped before the round even started (eg during countdown). + + self.speedometer:SetIsVisible(false) + + if self.timeAccumulation and self.timeAccumulation > 0.0 then + self.averageSpeed = self.speedAccumulation / self.timeAccumulation + else + self.averageSpeed = nil + end + self.endedPrematurely = true + +end + +-- Called to prematurely end the run (eg from the main menu, or if player gets themselves killed), and return to the +-- results/leaderboard screen. +function SkulkChallenge:EndRun(delay) + + if self.runCancelled then + return -- we're already canceling the stinkin run! + end + + if self.endSequenceRunning then + return -- we're at the end of a run. + end + + -- gracefully animate-off the countdown interface. + if self.countdownScript then + self.countdownScript:Dispose() + self.countdownScript = nil + end + + self:InterruptRun() + self:SendCommandToServer("EndRace") + self:SendCommandToServer("GetJumpCount") + + endSequenceWaitingCallCount = 1 + + -- sometimes we want to delay for splat screens to have time to disappear. + if delay then + self:AddTimedCallback( + function(self) + self:OnSCEndSequenceFinished() + end, delay) + else + self:OnSCEndSequenceFinished() + end +end + +-- player died. +local function OnRaceEndedFromServer() + + local self = GetSkulkChallenge() + + if self.running then + + DoSplat(self, + "failure", -- fired-up looking skulk graphic... nothing to do with "go", really. + Locale.ResolveString("SKULK_CHALLENGE_DEATH"), + "regular", + kSkulkChallengeSplatTimeLong, + kSkulkChallengeFailureTextColor) + + -- Play death music + Shared.PlaySound(nil, kSkulkChallengeDeathMusic, GetMusicManager():GetVolumeModifier()) + GetMusicManager():StopMusic() + + end + + self:EndRun(kSkulkChallengeSplatTimeShort) +end +Client.HookNetworkMessage("SC_RaceEnded", OnRaceEndedFromServer) + +-- Called to immediately restart the current run, with the same settings (eg ghost, replay, normal). +function SkulkChallenge:RestartRun() + + if self.runCancelled then + return -- we're already canceling the stinkin run! + end + + if self.endSequenceRunning then + return -- we're at the end of a run. + end + + -- Stop any music that is playing... + Shared.StopSound(nil, kSkulkChallengeFinishMusic) + Shared.StopSound(nil, kSkulkChallengeDeathMusic) + GetMusicManager():StopMusic() + + self:InterruptRun() + + self.fader:DoFadeOut(kSkulkChallengeFadeDuration, + function() + if self.countdownScript then + GetGUIManager():DestroyGUIScript(self.countdownScript) + self.countdownScript = nil + end + self:SetupTimers() + self:SendResetCommandToServer() + end) + +end + +function SkulkChallenge:GetIsHumanParticipating() + + if self.runType == "normal" or self.runType == "ghost" then + return true + end + + return false + +end + +local function OnJumpCountReceived(msg) + + local self = GetSkulkChallenge() + self.jumpCount = math.floor(msg.jumpCount) + +end +Client.HookNetworkMessage("SC_JumpCount", OnJumpCountReceived) + +local function OnSCTips() + + local self = GetSkulkChallenge() + + self.tipsEnabled = not self.tipsEnabled + Log("Skulk Challenge tips are now %s", self.tipsEnabled and "enabled" or "disabled") + +end +Event.Hook("Console_sctips", OnSCTips) diff --git a/challenges/skulk_challenge/lua/SCSkulkChallenge_Server.lua b/challenges/skulk_challenge/lua/SCSkulkChallenge_Server.lua new file mode 100644 index 000000000..e8b4aab8f --- /dev/null +++ b/challenges/skulk_challenge/lua/SCSkulkChallenge_Server.lua @@ -0,0 +1,260 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCSkulkChallenge_Server.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/SCSkulkMoveRecorder.lua") + +gSkulkChallengeEntity = nil +function GetSkulkChallenge() + + if not gSkulkChallengeEntity then + gSkulkChallengeEntity = Server.CreateEntity(SkulkChallenge.kMapName) + end + + return gSkulkChallengeEntity + +end + +function SkulkChallenge:OnJump() + + self.jumpCount = self.jumpCount + 1 + +end + +function SkulkChallenge:OnInitialized() + + self.relevancyOffset = Vector(0,0,0) + self.jumpCount = 0 -- jumps are counted on the server during replays :( + + self:SetUpdates(true) + + -- to prevent doors from popping in during resets. + SC_SetupDoorRelevancy() + + -- disable cheats. + Server.SetVariableTableCommandsAllowed(false) + +end + +function SkulkChallenge:OnClientConnected(serverClient) + + if not self.humanClient then + -- only set it the first time, as all other connects will be bots. + self.humanClient = serverClient + self.humanPlayerId = serverClient:GetPlayer():GetId() + end +end + +function SkulkChallenge:OnEntityChange(oldId, newId) + + if oldId ~= self.humanPlayerId then + return + end + + self.humanPlayerId = newId + +end + +function SkulkChallenge:OnUpdate(deltaTime) + + local player = self:GetHumanPlayer() + if not player then + return + end + + player:ConfigureRelevancy(self.relevancyOffset, 0) + +end + +function SkulkChallenge:OnReplayReceived(replayData) + + GetMovementPlayer():LoadRecording(replayData) + Server.SendNetworkMessage("SC_ReplayReceived", {}, true) + +end + +-- Sets the initial position and angle of either the player or the playback bot (for ghost or replays) +function SkulkChallenge:SetParticipantInitialState(player) + + local coords = Coords.GetLookAt(Target("starting_position"), Target("start_doors"), Vector(0,1,0)) + local angles = Angles() + angles:BuildFromCoords(coords) + player:SetOffsetAngles(angles) + player:SetOrigin(coords.origin) + + -- There aren't any other player entities that we want to collide with. Simple solution to prevent player and + -- hologram from colliding is to just set the physics group to default. + player:SetPhysicsGroupFilterMask(PhysicsMask.DefaultOnly) + + -- signal that we've reset. We use a netvar rather than a network message for this because they propagate differently, + -- and we only want to reset when the client is at the starting location. Messages results in client arriving slightly + -- before, and thus fading up before they were in position. + self.reset = true + +end + +-- Reset the skulk challenge to its initial state. This is called when the user's screen is completely black and +-- we're just about to start a new run. +function SkulkChallenge:ResetRun() + + SC_SetAllDoorsOpenState("instantclose") -- ensure doors are fully closed when run starts. + + if self.runType == "playback" or self.runType == "ghost" then + + -- ensure we have a bot ready for playing the replay. + local className = self.runType == "ghost" and "SkulkHologram" or "Skulk" + self:SetParticipantInitialState(GetMovementPlayer():SetupBotForPlayback(className)) + + else + + -- bot is not being used, remove it. + GetMovementPlayer():RemoveBot() + + end + + if self.runType == "playback" then + + -- make player a spectator, to get a first person view of the skulk. + local player = self:GetHumanPlayer() + player = player:Replace(SkulkChallengeSpectator.kMapName, kSpectatorIndex) + player:SetSpectatorMode(kSpectatorMode.FirstPerson) + + elseif self.runType == "normal" or self.runType == "ghost" then + + local newPlayer = self:GetHumanPlayer():Replace(Skulk.kMapName, kAlienTeamType, false, Target("starting_position")) + self:SetParticipantInitialState(newPlayer) + + end + +end + +function SkulkChallenge:OnPlayerKilled(player) + + if self:GetHumanPlayer() ~= player then + -- we only care about the player dying, not the ghost or anything else. + return + end + + GetMovementRecorder():StopRecording() + self.raceActive = false + + Server.SendNetworkMessage("SC_RaceEnded", {}, true) + +end + +function SkulkChallenge:SC_CommandFunction_Reset_Normal() + + self.runType = "normal" + self:ResetRun() + +end + +function SkulkChallenge:SC_CommandFunction_Reset_Ghost() + + self.runType = "ghost" + self:ResetRun() + +end + +function SkulkChallenge:SC_CommandFunction_Reset_Playback() + + self.runType = "playback" + self:ResetRun() + +end + +function SkulkChallenge:SC_CommandFunction_BeginPreCountdown() + + -- Begin playback if requested. + if self.runType == "playback" or self.runType == "ghost" then + GetMovementPlayer():PlayRecording() + end + + -- Begin recording the current run for the player. + if self.runType ~= "playback" then + GetMovementRecorder():StartRecordingForPlayer(self:GetHumanPlayer()) + end + + self.reset = false -- don't need to know this anymore. + +end + +function SkulkChallenge:SC_CommandFunction_BeginRace() + + SC_SetAllDoorsOpenState("open") + self.raceActive = true + self.jumpCount = 0 + +end + +function SkulkChallenge:SC_CommandFunction_EndRace() + + self.raceActive = false + GetMovementRecorder():StopRecording() + +end + +function SkulkChallenge:SC_CommandFunction_TransferReplay() + + GetMovementRecorder():SaveRecording() + +end + +function SkulkChallenge:SC_CommandFunction_GetJumpCount() + + Server.SendNetworkMessage("SC_JumpCount", { jumpCount = self.jumpCount }, true) + +end + +function SkulkChallenge:SC_CommandFunction_HaltPlayback() + + GetMovementPlayer():StopPlayback() + +end + +local function OnSCServerCommand(client, msg) + + local self = GetSkulkChallenge() + + local commandName = EnumToString(self.kCommands, msg.command) + local functionName = "SC_CommandFunction_"..commandName + self[functionName](self) + + Server.SendNetworkMessage(client, "SC_ServerCommand", msg, true) + +end +Server.HookNetworkMessage("SC_ServerCommand", OnSCServerCommand) + +function SkulkChallenge:OnFinish() + + if not self.raceActive then + return + end + + self:AddTimedCallback( + function() + SC_SetAllDoorsOpenState("close") + end, kSkulkChallengeEndSequenceDoorCloseDelay) + + GetMovementRecorder():StopRecording() + self.raceActive = false + Server.SendNetworkMessage("SC_RaceFinished", {}, true) + +end + +local function OnCameraRelevancyUpdate(client, message) + + local offset = message.offset + local self = GetSkulkChallenge() + + self.relevancyOffset = message.offset + +end +Server.HookNetworkMessage("SC_CameraRelevancy", OnCameraRelevancyUpdate) + + + diff --git a/challenges/skulk_challenge/lua/SCSkulkHologram.lua b/challenges/skulk_challenge/lua/SCSkulkHologram.lua new file mode 100644 index 000000000..16d669570 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCSkulkHologram.lua @@ -0,0 +1,142 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCSkulkHologram.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Skulk with a fancy holographic shader. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/Skulk.lua") + +class 'SkulkHologram' (Skulk) + +SkulkHologram.kModelName = PrecacheAsset("models/challenge/skulk_challenge/skulk_hologram.model") +SkulkHologram.kMapName = "skulk_hologram" +local kSkulkAnimationGraph = PrecacheAsset("models/alien/skulk/skulk.animation_graph") + +local netVars = {} + +Shared.LinkClassToMap("SkulkHologram", SkulkHologram.kMapName, netVars, true) + +function SkulkHologram:GetIgnoreVariantModels() + return true +end + +function SkulkHologram:OnInitialized() + + Skulk.OnInitialized(self) + + self:SetModel(self.kModelName, kSkulkAnimationGraph) + + if Client then + self:InitTrailEffect() + end + +end + +if Client then + function SkulkHologram:OnDestroy() + + Skulk.OnDestroy(self) + + self.destroyed = true + self:DestroyTrailEffect() + + end +end + +-- Trail effects +if Client then + + local trailCount = 10 + local trailDuration = 0.5 + local trailTemporalSpacing = trailDuration / trailCount + + function SkulkHologram:InitTrailEffect() + + self.trailData = {} + for i=1, trailCount do + local newTrailData = {} + newTrailData.boneCoords = nil + newTrailData.coords = Coords() + + newTrailData.renderModel = Client.CreateRenderModel(RenderScene.Zone_Default) + newTrailData.renderModel:SetIsVisible(false) + newTrailData.renderModel:SetModelByName(self.kModelName) + newTrailData.renderModel:InstanceMaterials() + + newTrailData.spawnTime = nil + + self.trailData[i] = newTrailData + end + + self.nextTrailUpdate = Shared.GetTime() + self.trailDataIndex = 0 + + self:AddTimedCallback(SkulkHologram.UpdateTrailEffect, 1/60) + + end + + function SkulkHologram:UpdateTrailEffect() + + if self.destroyed then + return false + end + + -- check if it's been long enough since the last "clone" of the tail was added. + local now = Shared.GetTime() + if now >= self.nextTrailUpdate then + + self.nextTrailUpdate = self.nextTrailUpdate + trailTemporalSpacing + self.trailDataIndex = (self.trailDataIndex % trailCount) + 1 + + local trailData = self.trailData[self.trailDataIndex] + + trailData.renderModel:SetIsVisible(true) + trailData.renderModel:SetBoneCoords(self.boneCoords) + trailData.renderModel:SetCoords(Coords(self._modelCoords)) + + trailData.spawnTime = now + + end + + for i=1, #self.trailData do + + local trail = self.trailData[i] + if trail.spawnTime then + local age = now - trail.spawnTime + local opacity = 1.0 - Clamp(age / trailDuration, 0, 1) + + -- shape the opacity a little nicer + opacity = opacity * opacity * 0.5 + + trail.renderModel:SetMaterialParameter("opacity", opacity) + end + + end + + return true + + end + + function SkulkHologram:DestroyTrailEffect() + + for i=1, #self.trailData do + Client.DestroyRenderModel(self.trailData[i].renderModel) + end + + end + +end + +if Server then + + function SkulkHologram:UpdateSilenceLevel() + -- skulk holograms are always full silence. + self.silenceLevel = 3 + + end + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCSkulkMoveRecorder.lua b/challenges/skulk_challenge/lua/SCSkulkMoveRecorder.lua new file mode 100644 index 000000000..59a583291 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCSkulkMoveRecorder.lua @@ -0,0 +1,273 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCSkulkMoveRecorder.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Records moves sent to server, and can play those moves back. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +if not Server then + error("The move recorder should only be loaded on the Server.") +end + +Script.Load("lua/SCGlobals.lua") +Script.Load("lua/SCUtils.lua") + +local function CreateRecorder() + local newRecorder = MoveRecorder() + + -- preallocate enough space for the full time of move data. + local maxMoves = kSkulkChallengeMaxRecordTime * kSkulkChallengeMoveRate + + -- preallocate all of the data + newRecorder.moves = {} + for i=1, maxMoves do + newRecorder.moves[i] = {} + newRecorder.moves[i].commands = 0 + newRecorder.moves[i].pitch = 0.0 + newRecorder.moves[i].yaw = 0.0 + newRecorder.moves[i].origin = Vector(0, 0, 0) + newRecorder.moves[i].velocity = Vector(0, 0, 0) + end + + -- data that will be stored in the file + newRecorder.skinName = "default" + newRecorder.numMoves = 0 + newRecorder.mapName = Shared.GetMapName() + + -- data that is only used for recording/playback. + newRecorder.moveIndex = 1 -- next move to be written + newRecorder.recording = false + newRecorder.maxMoves = maxMoves + + return newRecorder +end + +-- a recorder that will always be available to record the player's current run. +local recorder +function GetMovementRecorder() + if not recorder then + recorder = CreateRecorder() + end + + return recorder +end + +-- a recorder object that will be used to play back a recording, or drive a ghost. +-- its data is received by either a movement recorder (from player's previous run), or +-- from a downloaded UGC. +local playback +function GetMovementPlayer() + if not playback then + playback = CreateRecorder() + end + + return playback +end + +class 'PlaybackBot' (Bot) + +function PlaybackBot:_LazilyInitBrain() +end + +function PlaybackBot:GenerateMove() + + local move = Move() + + if self.recording and self.recording.playing then + + local readMove = self.recording:GetNextMove() + if readMove then + move.commands = readMove.commands + move.yaw = readMove.yaw + move.pitch = readMove.pitch + + self:GetPlayer():SetOrigin(readMove.origin) + self:GetPlayer():SetVelocity(readMove.velocity) + + local angles = Angles() + angles.pitch = readMove.pitch + angles.yaw = readMove.yaw + self:GetPlayer():SetViewAngles(angles) + end + + end + + -- Keep running for the end sequence. + if self:GetPlayer().endSequenceRunning then + + SC_ModifyMoveForEndSequence(move, Target("finish_sequence_end"), self:GetPlayer()) + + end + + return move + +end + +class 'MoveRecorder' + +function MoveRecorder:GetNextMove() + + if self.playbackFinished or not self.playbackStartTime then + return nil + end + + local now = Shared.GetSystemTimeReal() + local relativeTime = now - self.playbackStartTime + + if relativeTime > self.recordDuration then + self.playbackFinished = true + self.playing = false + return nil + end + + -- get the move index closest to the time this was during the recording + local endIndex = self.numMoves + local moveIndex = math.floor((endIndex * (relativeTime / self.recordDuration)) + 0.5) + + return self.moves[moveIndex+1] + +end + +function MoveRecorder:StopPlayback() + + self.playing = false + +end + +-- Start recording the moves generated by this player. +function MoveRecorder:StartRecordingForPlayer(player) + if not player then + error("StartRecordingForPlayer(player) expected a player, got... something else...") + end + player.isRecording = true + self.recordingPlayer = player + + self.recordStartTime = Shared.GetSystemTimeReal() + + self.moveIndex = 1 + +end + +function MoveRecorder:StopRecording() + if not self.recordingPlayer then + return + end + + self.recordEndTime = Shared.GetSystemTimeReal() + self.recordDuration = self.recordEndTime - self.recordStartTime + self.recordingPlayer.isRecording = nil + self.recordingPlayer = nil + self.numMoves = self.moveIndex - 1 + +end + +function MoveRecorder:RemoveBot() + + if self.playbackBot then + self.playbackBot:Disconnect() + self.playbackBot = nil + end + +end + +function MoveRecorder:SetupBotForPlayback(className) + + -- create bot to play back move inside + if not self.playbackBot then + self.playbackBot = PlaybackBot() + self.playbackBot:Initialize( kTeam2Index, true ) + end + + local newPlayer = self.playbackBot:GetPlayer():Replace( _G[className].kMapName, kTeam2Index) + newPlayer:SetOrigin(self.moves[1].origin) + newPlayer:SetVelocity(self.moves[1].velocity) + local angles = Angles() + angles.pitch = self.moves[1].pitch + angles.yaw = self.moves[1].yaw + newPlayer:SetViewAngles(angles) + self.playbackBot.recording = self + + self.playbackFinished = false + self.playing = false + + return newPlayer + +end + +function MoveRecorder:PlayRecording() + + self.playbackStartTime = Shared.GetSystemTimeReal() + self.playing = true + +end + +function MoveRecorder:LoadRecording(recData) + + self.skinName = recData.skinName + self.mapName = recData.mapName + self.recordDuration = recData.duration + self.numMoves = recData.numMoves + self.moves = recData.moves + +end + +function MoveRecorder:SaveRecording() + + local savedRecording = {} + savedRecording.skinName = self.skinName + savedRecording.mapName = self.mapName + savedRecording.duration = self.recordDuration + savedRecording.numMoves = self.numMoves + + savedRecording.moves = {} + for i=1, self.numMoves do + local moveData = {} + moveData.commands = self.moves[i].commands + moveData.pitch = self.moves[i].pitch + moveData.yaw = self.moves[i].yaw + moveData.origin = self.moves[i].origin + moveData.velocity = self.moves[i].velocity + savedRecording.moves[i] = moveData + end + + SC_SendReplay(savedRecording) + +end + +function MoveRecorder:LogMove(input, player) + if self.moveIndex > self.maxMoves then + Log("Buffer full. Recording stopped.") + self.recordingPlayer.isRecording = false + return + end + + -- Don't store the whole move -- only store what we need. + local newMove = {} + newMove.commands = input.commands + + newMove.origin = player:GetOrigin() + + newMove.yaw = input.yaw + newMove.pitch = input.pitch + + newMove.velocity = player:GetVelocity() + + self.moves[self.moveIndex] = newMove + + self.moveIndex = self.moveIndex + 1 +end + +-- hook into the player move function so we can divert a copy of the move to the recorder +local old_Player_OnProcessMove = Player.OnProcessMove +function Player:OnProcessMove(input) + + old_Player_OnProcessMove(self, input) + + if self.isRecording then + GetMovementRecorder():LogMove(input, self) + end + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCSpectator.lua b/challenges/skulk_challenge/lua/SCSpectator.lua new file mode 100644 index 000000000..801607138 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCSpectator.lua @@ -0,0 +1,35 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCSpectator.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- A spectator watching a replay for skulk challenge. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +Script.Load("lua/Spectator.lua") + +class 'SkulkChallengeSpectator' (Spectator) + +SkulkChallengeSpectator.kMapName = "scspectator" + +function SkulkChallengeSpectator:OnInitialized() + + Spectator.OnInitialized(self) + + if Server then + -- needs to be accessible. + self:SetPropagate(Entity.Propagate_Always) + end + +end + +function SkulkChallengeSpectator:SetSpectateMode(mode) + + -- don't allow other spectator modes. + Spectator.SetSpectateMode(self, kSpectatorMode.FirstPerson) + +end + +Shared.LinkClassToMap("SkulkChallengeSpectator", SkulkChallengeSpectator.kMapName, {}) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCTips.lua b/challenges/skulk_challenge/lua/SCTips.lua new file mode 100644 index 000000000..4d561ad0e --- /dev/null +++ b/challenges/skulk_challenge/lua/SCTips.lua @@ -0,0 +1,108 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ======= +-- +-- lua\SCTips.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Contains the tip content for the skulk challenge mode. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/SCTipsManager.lua") +Script.Load("lua/SCGUITips.lua") + +GetTipsManager():AddTip("walljump1", 15.0, 3, + function(tipData) + + if Shared.GetTime() - GetSkulkChallenge():GetHumanPlayer().timeLastWallJump < 5.0 then + return nil + end + + return Locale.ResolveString("SKULK_CHALLENGE_TIP_1") + end) + +GetTipsManager():AddTip("groundFriction", 20.0, 2, + function(tipData) + + tipData.airValue = tipData.airValue or 5.0 + + local human = GetSkulkChallenge():GetHumanPlayer() + if human:GetIsOnGround() and not human:GetIsWallWalking() then + tipData.airValue = math.max(tipData.airValue - 0.7, 0.0) + else + tipData.airValue = math.min(tipData.airValue + 1.0, 5.0) + end + + if tipData.airValue > 0.0 then + return + end + + return Locale.ResolveString("SKULK_CHALLENGE_TIP_2"), 5.0 + end) + +GetTipsManager():AddTip("walljump2", 20.0, 1, + function(tipData) + + tipData.wallJumpScore = tipData.wallJumpScore or 15 + tipData.wallJumpsChained = tipData.wallJumpsChained or 0 + + local human = GetSkulkChallenge():GetHumanPlayer() + if human.timeLastWallJump ~= tipData.timeLastWallJump then + tipData.timeLastWallJump = human.timeLastWallJump + tipData.wallJumpsChained = tipData.wallJumpsChained + 1 + end + + local justJumped = false + if human.lastJumpTime ~= tipData.lastJumpTime then + justJumped = true + tipData.lastJumpTime = human.lastJumpTime + end + + -- Whenever the player lands on the ground, see how many wall jumps they performed while they were in the air. + -- If they do at least 2, we give them a "point". If they don't do at least 2, we take away part of a point. + if (justJumped or human:GetIsOnGround()) and not human:GetCanWallJump() and not tipData.waitingForAirtime then + -- on the ground that is not a wall. + if tipData.wallJumpsChained >= 2 then + tipData.wallJumpScore = 15 + else + tipData.wallJumpScore = math.max(tipData.wallJumpScore - 1, 0.0) + end + tipData.wallJumpsChained = 0 + tipData.waitingForAirtime = true + end + + -- we're in the air, start looking for wall jumps or landings. + if not human:GetIsOnGround() then + tipData.waitingForAirtime = false + end + + -- last time we jumped, we chained some wall jumps. + if tipData.wallJumpScore > 0 then + return + end + + return Locale.ResolveString("SKULK_CHALLENGE_TIP_3"), 5.0 + end) + +GetTipsManager():AddTip("wallwalking", 20.0, 2, + function(tipData) + + tipData.wallWalkTime = tipData.wallWalkTime or 2.0 + + local human = GetSkulkChallenge():GetHumanPlayer() + + if human:GetIsWallWalking() then + tipData.wallWalkTime = math.min(tipData.wallWalkTime + 0.125, 1.0) + else + tipData.wallWalkTime = math.max(tipData.wallWalkTime - 0.0625, 0.0) + end + + -- last time we jumped, we chained some wall jumps. + if tipData.wallWalkTime < 1.0 then + return + end + + local bindingName = GetFriendlyBindingName(BindingsUI_GetInputValue("Crouch")) + return string.format(Locale.ResolveString("SKULK_CHALLENGE_TIP_4"), bindingName), 5.0 + end) + diff --git a/challenges/skulk_challenge/lua/SCTipsManager.lua b/challenges/skulk_challenge/lua/SCTipsManager.lua new file mode 100644 index 000000000..d1949cc2e --- /dev/null +++ b/challenges/skulk_challenge/lua/SCTipsManager.lua @@ -0,0 +1,119 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ======= +-- +-- lua\SCTipsManager.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Detects if the player is having issues with skulk challenge, and sends relevant tips to the GUI. +-- System-only. Content is added elsewhere. (For skulk challenge, see SCTips.lua) +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +local tipsManager +function GetTipsManager() + + if not tipsManager then + tipsManager = SCTipsManager() + tipsManager:Initialize() + end + + return tipsManager + +end + +class 'SCTipsManager' + +SCTipsManager.throttle = 0.125 -- only update every 0.125 seconds + +function SCTipsManager:Initialize() + + self.updateCooldown = 0.0 -- to throttle updating tips + self.tipCooldown = 0.0 -- to keep tips from spamming + self.tips = {} + self.queuedTipPriority = -1 + self.queuedTip = nil -- tip that was last passed to the gui. + +end + +-- Adds a function that will be called every eighth of a second (unless it is on cooldown). If the tip is to be displayed, +-- the function should return the (locale-resolved) string to be displayed and the duration to display it for, +-- otherwise it should return nil. A tip returning a string does not guarantee it will be displayed. There is a +-- certain amount of time between when a tip returns and when the display actually happens. Any other tips that return +-- a string in this timeframe will override the first, if it has a higher priority. When a tip is displayed, it is +-- also put on cooldown, meaning it will not even be evaluated for X seconds after fading away after being displayed. +function SCTipsManager:AddTip(name, cooldown, priority, tipFunc) + + local newTip = {} + newTip.tipFunc = tipFunc + newTip.tipDataTable = {} + newTip.priority = priority or 1 + newTip.name = name + newTip.cooldown = cooldown + self.tips[#self.tips + 1] = newTip + +end + +function SCTipsManager:RemoveTip(name) + + for i=#self.tips, 1, -1 do + if self.tips[i].name == name then + table.remove(self.tips, i) + end + end + +end + +function SCTipsManager:OnTipUsed() + + self.queuedTipPriority = -1 + if self.queuedTip ~= nil then + self.tipCooldown = self.queuedTip.cooldown + self.queuedTipDuration + GetTipsGUIScript():GetExtraTime() + self.queuedTip = nil + end + +end + +function SCTipsManager:UpdateTips(deltaTime) + + local sc = GetSkulkChallenge() + if not sc or not sc:GetCanShowTips() then + return + end + + self.tipCooldown = self.tipCooldown - deltaTime + + for i=1, #self.tips do + + local tip = self.tips[i] + + local result, duration = tip.tipFunc(tip.tipDataTable) + duration = duration or 3.0 + + if result ~= nil and tip.priority > self.queuedTipPriority and self.tipCooldown <= 0.0 then + self.queuedTipPriority = tip.priority + GetTipsGUIScript():DisplayTip(result, duration) + self.queuedTip = tip + self.queuedTipDuration = duration + end + + end + +end + +function SCTipsManager:Update(deltaTime) + + self.updateCooldown = self.updateCooldown - deltaTime + if self.updateCooldown <= 0.0 then + self.updateCooldown = self.updateCooldown + self.throttle + else + return -- still waiting on cooldown. + end + + self:UpdateTips(self.throttle) + +end + +local function OnUpdateClient(deltaTime) + GetTipsManager():Update(deltaTime) +end +Event.Hook("UpdateClient", OnUpdateClient) \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/SCUtils.lua b/challenges/skulk_challenge/lua/SCUtils.lua new file mode 100644 index 000000000..c9c53bfbb --- /dev/null +++ b/challenges/skulk_challenge/lua/SCUtils.lua @@ -0,0 +1,428 @@ +-- ======= Copyright (c) 2003-2016, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCUtils.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Utility functions for the Skulk Challenge gamemode. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +if Client then + Script.Load("lua/GraphDrivenModel.lua") +end + +if Server then + function SC_GetCameraPosition() + + -- We don't know exactly where client's camera is, but we can get pretty close based on the messages + -- that are periodically sent to the server to configure relevancy. + local player = GetSkulkChallenge():GetHumanPlayer() + if not player then + return Vector(0,0,0) + end + + local pos + if player.GetEyePos then + pos = player:GetEyePos() + else + pos = player:GetOrigin() + end + + local offset = GetSkulkChallenge().relevancyOffset + if offset then + pos = pos + offset + end + + return pos + + end +end + +function SC_SetupDoorRelevancy() + + local propDynamicObjects = EntityListToTable(Shared.GetEntitiesWithClassname("PropDynamic")) + for i=1, #propDynamicObjects do + if propDynamicObjects[i] and propDynamicObjects[i].modelName == "models/challenge/skulk_challenge/refinery_tram_doors_animated.model" then + propDynamicObjects[i]:SetPropagate(Entity.Propagate_Always) + end + end + +end + +function SC_SetAllDoorsOpenState(state) + + local propDynamicObjects = EntityListToTable(Shared.GetEntitiesWithClassname("PropDynamic")) + local doors = {} + for i=1, #propDynamicObjects do + if propDynamicObjects[i] and propDynamicObjects[i].modelName == "models/challenge/skulk_challenge/refinery_tram_doors_animated.model" then + propDynamicObjects[i]:SetAnimationInput("animation", state) + table.insert(doors, propDynamicObjects[i]) + end + end + + -- if we are opening or closing (not instant-closing) a door, play a sound near the closest door to where + -- the player's camera is. + local soundName + if state == "close" then + soundName = kSkulkChallengeDoorCloseSound + elseif state == "open" then + soundName = kSkulkChallengeDoorOpenSound + end + + if not soundName then + return + end + + local camPos = SC_GetCameraPosition() + local closest, closestDistSq + for i=1, #doors do + local distSq = (doors[i]:GetOrigin() - camPos):GetLengthSquared() + if not closestDistSq or distSq < closestDistSq then + closest = doors[i] + closestDistSq = distSq + end + end + + if not closest then + return + end + + local soundPos = closest:GetOrigin() + kSkulkChallengeDoorSoundOffset + StartSoundEffectAtOrigin(soundName, soundPos) + +end + +function Target(name) + return gSCTargetPts[name] +end + +-- returns 5 values: minutes, tens of seconds, seconds, tenths of seconds, hundredths of seconds +-- Easily plugged into a timer display. Goes up to 9 minutes, 59.99 seconds. +function ConvertSecondsToFormattedTime(time) + + if time >= (600.0) then + -- if time is ten minutes or more, just return the maximum displayable value + return 9, 5, 9, 9, 9 + end + + if time <= 0.0 then + -- negative numbers not supported, just return 0. + return 0, 0, 0, 0, 0 + end + + local t = time + + local minutes = math.floor(t / 60.0) + t = t - (minutes * 60.0) + assert(minutes <= 9) + assert(minutes >= 0) + + local tensOfSeconds = math.floor(t / 10.0) + t = t - (tensOfSeconds * 10.0) + assert(tensOfSeconds <= 9) + assert(tensOfSeconds >= 0) + + local seconds = math.floor(t) + t = t - seconds + assert(seconds <= 9) + assert(seconds >= 0) + + local tSecs = math.floor(t * 10.0) + t = t - (tSecs * 0.1) + assert(tSecs <= 9) + assert(tSecs >= 0) + + local hSecs = math.floor(t * 100.0) + assert(hSecs <= 9) + assert(hSecs >= 0) + + return minutes, tensOfSeconds, seconds, tSecs, hSecs + +end + +function ConvertSecondsToString(t) + + local minutes, tensOfSeconds, seconds, tSecs, hSecs = ConvertSecondsToFormattedTime(t) + + return string.format("%d:%d%d.%d%d", minutes, tensOfSeconds, seconds, tSecs, hSecs) + +end + +function ConvertMillisecondsToString(t) + + return ConvertSecondsToString(t * 0.001) + +end + +-- Transmit replay data between client and server (replays are recorded on Server, but only the Client can +-- upload them to Steam. + +local function OnReceivedDataFromServer() + + local replay = {} + Client.GetReplayFromServerData(replay, Challenge.ReplayType_SkulkChallenge) + + GetSkulkChallenge():OnReplayReceived(replay) + +end +Event.Hook("ReceivedDataFromServer", OnReceivedDataFromServer) + +local function OnReceivedDataFromClient() + + local replay = {} + local result = Server.GetReplayFromClientData(replay, Challenge.ReplayType_SkulkChallenge) + + GetSkulkChallenge():OnReplayReceived(replay) + +end +Event.Hook("ReceivedDataFromClient", OnReceivedDataFromClient) + +function SC_SendReplay(replay) + + if Client then + + Client.SendReplayToServer(replay, Challenge.ReplayType_SkulkChallenge) + + elseif Server then + + Server.SendReplayToClient(replay, Challenge.ReplayType_SkulkChallenge) + + end + +end + +function SC_ModifyMoveForEndSequence(move, endPt, player) + + local startPt = player:GetOrigin() + local diff = (endPt - startPt):GetUnit() + move.pitch = math.asin(-diff.y) + move.yaw = math.atan2(diff.x, diff.z) - player.baseYaw + move.move = Vector(0,0,1) + move.commands = 0 + move.hotkey = 0 + +end + +if Client then + -- Trigger round begin particle effects. + local triggeredCinematics = {} + local effectsStatus = {} + local effectsDesiredStatus = {} + local effectsNextUpdateTime = {} + + local function GetEffectsStatus(category) + + if not effectsStatus[category] then + return nil + end + + return effectsStatus[category] + + end + + local function UpdateEffects(category) + + local desiredStatus = effectsDesiredStatus[category] + local currentStatus = GetEffectsStatus(category) + if currentStatus == desiredStatus then + return -- where we want to be, no need to update + end + + effectsStatus[category] = effectsDesiredStatus[category] + + if desiredStatus == "on" then + + -- activate effects + triggeredCinematics[category] = triggeredCinematics[category] or {} + local sources = gSCCinematics + if not sources then + -- no effects found, period. + return + end + + sources = sources[category] + if not sources then + -- no effects found in this category + return + end + + if currentStatus == nil then + -- effects must be created + for i=1, #sources do + local cin = Client.CreateCinematic() + cin:SetCinematic(sources[i].fileName) + cin:SetCoords(sources[i].coords) + cin:SetRepeatStyle(sources[i].repeatStyle) + cin:SetIsVisible(true) + cin:SetIsActive(true) + table.insert(triggeredCinematics[category], cin) + end + elseif currentStatus == "off" then + -- effects already exist, just turn them on. + for i=1, #triggeredCinematics[category] do + triggeredCinematics[category][i]:SetIsActive(true) + end + end + + elseif desiredStatus == "off" then + + -- deactivate effects + local cins = triggeredCinematics[category] + if not cins then + -- no triggered cinematics in this category. + return + end + + for i=1, #cins do + cins[i]:SetIsActive(false) + end + + else + + -- destroy effects + local cins = triggeredCinematics[category] + if not cins then + -- no triggered cinematics in this category. + return + end + + for i=1, #cins do + Client.DestroyCinematic(cins[i]) + end + triggeredCinematics[category] = {} + + end + + end + + function SC_DestroyEffectsImmediately(category) + + effectsDesiredStatus[category] = nil + UpdateEffects(category) + + end + + function SC_TriggerEffects(category) + + effectsDesiredStatus[category] = "on" + UpdateEffects(category) + + effectsNextUpdateTime[category] = Shared.GetTime() + kSkulkChallengeCountdownDuration + kSkulkChallengePostStartEffectsLingerDuration + + -- schedule the deactivation + local sc = GetSkulkChallenge() + sc:AddTimedCallback( + function() + + -- if something else has happened in the meantime while waiting to deactivate, don't do anything. + if not effectsNextUpdateTime[category] or effectsNextUpdateTime[category] > Shared.GetTime() then + -- The update that was scheduled is no longer the most up-to-date task. + return + end + + -- deactivate effects. + effectsDesiredStatus[category] = "off" + UpdateEffects(category) + + effectsNextUpdateTime[category] = Shared.GetTime() + kSkulkChallengeEffectDissipationDuration + + -- schedule the destruction of the effects. + sc:AddTimedCallback( + function() + + -- if something else has happened in the meantime while waiting to deactivate, don't do anything. + if not effectsNextUpdateTime[category] or effectsNextUpdateTime[category] > Shared.GetTime() then + -- The update that was scheduled is no longer the most up-to-date task. + return + end + + effectsDesiredStatus[category] = nil + UpdateEffects(category) + + end, kSkulkChallengeEffectDissipationDuration) + + return false + + end, kSkulkChallengeCountdownDuration + kSkulkChallengePostStartEffectsLingerDuration + 0.01) + + end + + function SC_GetReplayFileName() + + return SC_GetLeaderboardName()..".scr" + + end +end + +if Client then + + local cheeringMarineModelNames = + { + PrecacheAsset("models/marine/male/male.model"), + PrecacheAsset("models/marine/male/male_assault.model"), + PrecacheAsset("models/marine/male/male_kodiak.model"), + PrecacheAsset("models/marine/male/male_special.model"), + PrecacheAsset("models/marine/male/male_special_v1.model"), + PrecacheAsset("models/marine/male/male_tundra.model"), + + PrecacheAsset("models/marine/female/female.model"), + PrecacheAsset("models/marine/female/female_assault.model"), + PrecacheAsset("models/marine/female/female_kodiak.model"), + PrecacheAsset("models/marine/female/female_special.model"), + PrecacheAsset("models/marine/female/female_special_v1.model"), + PrecacheAsset("models/marine/female/female_tundra.model"), + } + + local cheeringMarineGraphName = PrecacheAsset("models/marine/male/male.animation_graph") + + function SC_CreateCheeringMarine(coords) + + local gdm = CreateGraphDrivenModel(cheeringMarineModelNames[math.random(1, #cheeringMarineModelNames)], cheeringMarineGraphName) + gdm.originalCoords = coords + gdm.cheerCooldown = math.random(1.0) + local cheerName = "cheer"..(math.random(1,3)) + gdm:SetAnimationInput("cheer", cheerName) + gdm:SetAnimationInput("alive", true) + gdm:SetPreUpdateCallbackFunction( + function(self, delta) + + if not Client.GetLocalPlayer() then + return + end + + -- make the cheering marine look at the player. + local baseCoords = gdm.originalCoords + local baseAngles = Angles() + baseAngles:BuildFromCoords(baseCoords) + + local clientEyePos = Client.GetLocalPlayer():GetEyePos() + local coords = Coords.GetLookAt(baseCoords.origin + Vector(0, 1.7, 0), clientEyePos, Vector(0,1,0)) + local angles = Angles() + angles:BuildFromCoords(coords) + + local eyePosXZ = Vector(clientEyePos) + eyePosXZ.y = baseCoords.origin.y + local flattenedCoords = Coords.GetLookAt(baseCoords.origin, eyePosXZ, Vector(0,1,0)) + self:SetCoords(flattenedCoords) + + local yawOffset = Math.Wrap(baseAngles.yaw - angles.yaw, -math.pi, math.pi) + + self:SetPoseParam("body_pitch", -Math.Wrap(Math.Degrees(angles.pitch), -180, 180)) + self:SetPoseParam("body_yaw", Math.Degrees(yawOffset)) + + -- every so often, randomize the marines cheering animation. + self.cheerCooldown = self.cheerCooldown or 0.0 + self.cheerCooldown = self.cheerCooldown - delta + if self.cheerCooldown <= 0.0 then + self.cheerCooldown = 1.0 + local cheerName = "cheer"..(math.random(1,3)) + self:SetAnimationInput("cheer", cheerName) + end + + end) + + end + +end + diff --git a/challenges/skulk_challenge/lua/SCVoting.lua b/challenges/skulk_challenge/lua/SCVoting.lua new file mode 100644 index 000000000..933e78888 --- /dev/null +++ b/challenges/skulk_challenge/lua/SCVoting.lua @@ -0,0 +1,17 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\SCVoting.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Disable voting. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +if Server then + + function GetStartVoteAllowed() + return false + end + +end \ No newline at end of file diff --git a/challenges/skulk_challenge/lua/entry/SkulkChallenge.entry b/challenges/skulk_challenge/lua/entry/SkulkChallenge.entry new file mode 100644 index 000000000..1c8f92d43 --- /dev/null +++ b/challenges/skulk_challenge/lua/entry/SkulkChallenge.entry @@ -0,0 +1,7 @@ +modEntry = { + Client = "lua/SCClient.lua", + Server = "lua/SCServer.lua", + Shared = "lua/SCShared.lua", + FileHooks = "lua/SCFileHooks.lua", + Priority = 1 +} \ No newline at end of file diff --git a/core/lua/MapCycle.lua b/core/lua/MapCycle.lua index 579e05b21..0e16494bf 100644 --- a/core/lua/MapCycle.lua +++ b/core/lua/MapCycle.lua @@ -60,6 +60,54 @@ function MapCycle_GetMapIsInCycle(mapName)

end

+local gameModePrefixes = +{ + "infest", "infext", +} + +local prefixToModId = +{ + ["infest"] = "2e813610", + ["infect"] = "2e813610", +} + +-- Returns nil if the prefix doesn't match, or the real map name if it does match. +local function HasPrefix(mapName, prefix) + + prefix = prefix .. "_" + + local mapPrefix = string.sub(mapName, 1, string.len(prefix)) + if mapPrefix ~= prefix then + return nil + end + + local newMapName = "ns2_" .. string.sub( mapName, string.len(prefix) + 1) + return newMapName + +end + +local function ApplyPrefixes(mapName, modsTable) + + for i=1, #gameModePrefixes do + + local prefix = gameModePrefixes[i] + + local result = HasPrefix(mapName, prefix) + if result then + + local modId = prefixToModId[prefix] + assert(modId) + modsTable[#modsTable+1] = modId + return result -- repaired map name + + end + + end + + return mapName -- original map name passed in... nothing has taken effect + +end +

local function StartMap(map)

    local mods = { }

@@ -74,6 +122,9 @@ local function StartMap(map)

        table.copy(map.mods, mods, true)
    end
    

+ -- check for alternate gamemodes in the level file prefix (eg "infest" for infested marines) + mapName = ApplyPrefixes(mapName, mods) +

    -- If we fail to load the world, the event "mapchangefailed" will be triggered
    -- the default behaviour of that is to cycle away from the failed map
    -- This replaces the previous "make sure the map is present before we try to start it",

diff --git a/ns2/editor_setup.xml b/ns2/editor_setup.xml index 68aeb1a8f..c4ba340fd 100644 --- a/ns2/editor_setup.xml +++ b/ns2/editor_setup.xml @@ -322,6 +322,13 @@

    </class>
    

+ + <class name="cheering_marine"> + <model> + <file>models/marine/male/male.model</file> + </model> + </class> +

    <class name="base_start" placeable="false">
        <model>

@@ -482,7 +489,34 @@

        </model>

    </class>

+ + + + <class name="skulk_challenge_gamerules" label="Use skulk challenge gameplay"> + + <parent>gamerules</parent> + + <model> + <file>models/system/editor/gamerules.model</file> + </model> + + </class> + + + <class name="finish_line" placeable="true">

+ <model> + <file>models/system/editor/location.model</file> + <scale>scale</scale> + </model> + + <size> + <scale>scale</scale> + </size> + <parameter name="scale" type="vector" label="Scale" default="1.0, 1.0, 1.0" help=""/> + <parameter name="name" label="Name" type="string" help="Name of trigger which can be referenced in lua." /> + + </class>

    <class name="reverb">

@@ -1126,4 +1160,4 @@

</reflection_probe_settings>

-</editor> \ No newline at end of file +</editor> diff --git a/ns2/gamestrings/enUS.txt b/ns2/gamestrings/enUS.txt index 096608507..af5bd060a 100644 --- a/ns2/gamestrings/enUS.txt +++ b/ns2/gamestrings/enUS.txt @@ -73,11 +73,17 @@ AURA = "Aura"

AURA_TOOLTIP = "Indicates health and position of enemy players."
AUTHENTICATING = "AUTHENTICATING"
AUTOJOIN = "ATTEMPT TO JOIN"

+AUTOJOIN_SPEC = "SPECTATE" +AUTOJOIN_SPEC_AND_RS = "JOIN" +AUTOJOIN = "ATTEMPT TO JOIN"

AUTOJOIN_CANCEL = "CANCEL"

-AUTOJOIN_JOIN = "WAITING FOR SLOT..." +AUTOJOIN_JOIN = "WAITING FOR PLAYER SLOT..."

AUTOJOIN_JOIN_TOOLTIP = "YOU CAN ATTEMPT TO JOIN IF YOU HAVE A RESERVED SLOT"

+AUTOJOIN_JOIN_TOOLTIP_SPEC = "YOU CAN JOIN TO SPECTATE" +AUTOJOIN_JOIN_TOOLTIP_SPEC_AND_RS = "YOU CAN JOIN TO SPECTATE OR TO USE YOUR RESERVED SLOT"

AUTO_CONCEDE_WARNING = "Teams uneven. %{teamName}s will Auto-Concede in %{time}d seconds."
AUTO_TEAM_BALANCE_TOOLTIP = "Team Balance enabled. You will spawn when your teammates die.\nYou can switch teams to spawn faster. Press %s to switch teams."

+AVERAGE_SPEED = "Average Speed"

AXE_TOOLTIP = "A weapon of desperation. BIND_PrimaryAttack to attack - good against structures."
BABBLER = "Babbler"
BABBLERS = "Babblers"

@@ -95,6 +101,8 @@ BADGE_ENSL_2017 = "ENSL Season 10 Champion"

BADGE_ENSL_NC_2017_BLUE = "ENSL Newcomer Tournament 2017 Participant"
BADGE_ENSL_NC_2017_SILVER = "ENSL Newcomer Tournament 2017 Finalist"
BADGE_ENSL_NC_2017_GOLD = "ENSL Newcomer Tournament 2017 Winner"

+BADGE_ENSL_S11_GOLD = "ENSL Season 11 Division 1 Winner" +BADGE_ENSL_S11_SILVER = "ENSL Season 11 Division 2 Winner"

BADGE_ENSL_WC_GOLD = "World Championship 2014 Winner"
BADGE_ENSL_WC_SILVER = "World Championship 2014 Finalist"
BADGE_ENSL_WC_BRONZE = "World Championship 2014 Semifinalist"

@@ -112,6 +120,10 @@ BADGE_REINFORCED6 = "Reinforced Onos"

BADGE_REINFORCED7 = "Reinforced Insider"
BADGE_REINFORCED8 = "Reinforced Game Director"
BADGE_SELECTION_HELP = "Drag and drop to select, click to remove"

+BADGE_SKULK_CHALLENGE_1_BRONZE = "Skulk Challenge (Tram) Bronze" +BADGE_SKULK_CHALLENGE_1_SILVER = "Skulk Challenge (Tram) Silver" +BADGE_SKULK_CHALLENGE_1_GOLD = "Skulk Challenge (Tram) Gold" +BADGE_SKULK_CHALLENGE_1_SHADOW = "Skulk Challenge (Tram) Shadow"

BADGE_SQUAD5_BLUE = "Squad 5 Blue"
BADGE_SQUAD5_GOLD = "Squad 5 Gold"
BADGE_SQUAD5_SILVER = "Squad 5 Silver"

@@ -129,6 +141,7 @@ BEACONING_COMMANDER = "Distress Beacon triggered. Teleport imminent."

BEACON_TO = "Distress Beacon: Teleporting to %s"
BELLY_SLIDE = "Belly slide"
BELLY_SLIDE_TOOLTIP = "Allows the Gorge to slide down ramps."

+BEST_TIME = "Best Time"

BETA_MAINMENU = "Build "
BILEBOMB = "Bile Bomb"
BILEBOMB_TOOLTIP = "Allows Gorges to fire siege projectile that does area damage to structures, and Marine/Exosuit armor."

@@ -222,6 +235,12 @@ CAT_PACK_TECH_TOOLTIP = "Increases Marine movement and reload speed."

CAT_PACK_TOOLTIP = "Increases Marine movement and reloading speed."
CELERITY = "Celerity"
CELERITY_TOOLTIP = "Increases movement speed and acceleration."

+CHALLENGE_MEDAL_BRONZE = "Bronze Medal" +CHALLENGE_MEDAL_SILVER = "Silver Medal" +CHALLENGE_MEDAL_GOLD = "Gold Medal" +CHALLENGE_MEDAL_SHADOW = "Shadow Medal" +CHALLENGE_REPLAY_LINE_1 = "Replay" +CHALLENGE_REPLAY_LINE_2 = "& Ghost"

CHANGE_EGG_HELP = "Press %s to spawn or %s to change eggs"
CHANGELOG_DISCORD = "Chat with us"
CHANGELOG_FEEDBACK = "Feedback:"

@@ -411,7 +430,10 @@ DISTRESS_BEACON_TOOLTIP = "Teleports Marines to Command Station nearest to Obser

DOOR = "Door"
DOOR_TOOLTIP = "You will be able to weld doors shut in the future."
DOUBLE_BUFFERED = "DOUBLE BUFFERED"

+DOWNLOAD_COMPLETE = "Download Complete!" +DOWNLOAD_FAILED = "Download Failed!"

DOWNLOADING_MODS = "DOWNLOADING MODS"

+DOWNLOADING_REPLAY = "Downloading Replay..."

DRAW_DAMAGE = "DRAW DAMAGE"
ENEMY_HEALTH_BARS = "SHOW ENEMY HEALTH"
DRAW_GAME = "Draw Game!"

@@ -734,6 +756,7 @@ HUD_DETAIL = "HUD DETAIL"

HYDRA = "Hydra"
HYDRA_HINT = "Gorge spike plant"
HYDRA_TOOLTIP = "Spike plant that can be built anywhere and fires at enemies. Keep it alive with your heal spray."

+IGNORE = "Ignore"

INCORRECT_PASSWORD = "Incorrect password"
INDESTRUCTABLE = "indestructable"
INFANTRY_PORTAL = "Infantry Portal"

@@ -776,9 +799,21 @@ JETPACK_TECH = "Research Jetpacks"

JETPACK_TOOLTIP = "Allows Marines to fly and access vents."
JOIN = "JOIN"
JOIN_ERROR_ROOKIE = "You have to play the tutorial before being able to play!"

+JOIN_ERROR_NO_PLAYER_SLOT_LEFT = "There are currently no player slots available. You can continue to spectate."

JOIN_ERROR_TOO_MANY = "There are too many players on this team. You can wait or join another team."
JOIN_ERROR_VETERAN = "This is a rookie only server. You are too skilled to play here."

+JUMP_COUNT = "Jump Count" +JUST_THIS_TIME = "Just this time"

LANGUAGE = "LANGUAGE"

+LEADERBOARD = "Leaderboard" +LEADERBOARD_PLAYER = "Player" +LEADERBOARD_TOOLTIP_ARROW = "Show more scores" +LEADERBOARD_TOOLTIP_FRIENDS = "Only show friends' scores" +LEADERBOARD_TOOLTIP_GHOST = "Race against this player's ghost" +LEADERBOARD_TOOLTIP_GLOBAL = "Show all scores" +LEADERBOARD_TOOLTIP_PROFILE = "Open this player's profile" +LEADERBOARD_TOOLTIP_REPLAY = "View the first-person replay of this score" +LEADERBOARD_TOOLTIP_REPLAY_MISSING = "This user did not attach a replay."

LEAP = "Leap"
LEAP_TOOLTIP = "Allows Skulks to leap into combat"
LEFT_MOUSE_BUTTON = "Left mouse button"

@@ -894,6 +929,7 @@ MENU_CREATE_GAME = "CREATE GAME"

MENU_CREDITS = "CREDITS"
MENU_CUSTOMIZE_PLAYER = "CUSTOMIZE PLAYER"
MENU_DISCONNECT = "DISCONNECT"

+MENU_END_RUN = "END RUN"

MENU_EXIT = "EXIT"
MENU_FPS = "FPS: %.0f"
MENU_GO_TO_READY_ROOM = "GO TO READY ROOM"

@@ -908,6 +944,7 @@ MENU_PLAY = "PLAY"

MENU_PLAY_NOW = "PLAY NOW"
MENU_QUICK_JOIN = "QUICK JOIN"
MENU_RESTART_CHALLENGE = "RESTART CHALLENGE"

+MENU_RESTART_RUN = "RESTART RUN"

MENU_RESUME_GAME = "RESUME GAME"
MENU_RETURN = "RETURN"
MENU_SERVER_BROWSER = "SERVER BROWSER"

@@ -1101,6 +1138,7 @@ NEW_ACHIEVEMENT_3_4_NAME = "The Last Straw"

NEW_ACHIEVEMENT_3_4_DESC = "Gorges can only take so much..."
NEW_INFORMATION = "New Info!"
NEXT = "Next"

+NEXT_MEDAL_TIME = "Next Medal Time"

NEXT_SPAWN_IN = "Next spawn in %d"
NICKNAME = "NICKNAME"
NS2 = "NS2"

@@ -1109,6 +1147,7 @@ NO = "NO"

NO_COMM = "Commander Needed"
NO_COMMANDER = "No Commander"
NO_IPS = "Infantry Portal Needed"

+NO_MEDAL = "No Medal"

NUTRIENT_MIST = "Nutrient mist"
NUTRIENT_MIST_TOOLTIP = "Speeds structure maturation, speeds player evolution, minor healing to cysts, and prevents damage to structures off infestation for 15 seconds"
OBJECTIVE_PROGRESS = "%{location}s %{name}s %{health}d%%"

@@ -1116,6 +1155,7 @@ OBSERVATORY = "Observatory"

OBSERVATORY_HINT = "Detects Aliens"
OBSERVATORY_TOOLTIP = "Detects enemies nearby"
OFF = "OFF"

+OK = "Ok"

ON = "ON"
ONOS = "Onos"
ONOS_EGG = "Onos egg"

@@ -1179,6 +1219,7 @@ PLAYERS = "PLAYERS"

PLAYER_COUNT_WARNING = "The server you are joining has more than 24 players. Natural Selection 2 was designed to have an optimal experience within a maximum of 24 players. On servers with player counts higher than 24, we can not guarantee acceptable performance, or balanced gameplay."
PLAY_BOOTCAMP = "BOOTCAMP"
PLAY_HIVE_CHALLENGE = "HIVE CHALLENGE"

+PLAY_SKULK_CHALLENGE = "SKULK CHALLENGE"

PLAY_MENU_ARCADE = "ARCADE"
PLAY_MENU_ARCADE_DESCRIPTION = "Play some alternate gamemodes!"
PLAY_MENU_BACK_DESCRIPTION = "Go back to the main menu"

@@ -1222,10 +1263,14 @@ PROTOTYPE_LAB = "Prototype lab"

PROTOTYPE_LAB_HINT = "Jetpacks, Exos"
PROTOTYPE_LAB_TOOLTIP = "You can buy Jetpacks and Exosuits from here."
PULSE_GRENADE = "Pulse Grenade"

-QUICK_JOIN = "QUICK JOIN" +QUICK_JOIN = "QUICK JOIN" +QUIT = "Quit" +RACE_GHOST = "Race Ghost" +RACE_LAST_GHOST = "Race Your Last Ghost"

RAILGUN = "Railgun"
RAILGUN_CLAW_TOOLTIP = "BIND_PrimaryAttack to punch with Claw. BIND_SecondaryAttack to fire railgun (unlimited ammo but slow rate of fire). BIND_Jump to activate jump jets."
RALLY_POINT_TOOLTIP = "New helper units automatically move here"

+RANK = "Rank"

RAW_INPUT = "RAW INPUT"
READY_ROOM_PLAYER = "Ready room player"
RECYCLE = "Recycle"

@@ -1273,6 +1318,8 @@ RESOURCE_NOZZLE = "Resource nozzle"

RESOURCE_NOZZLE_TOOLTIP = "Teams build structures here to gather resources."
RESOURCE_TOWERS = "Resource Towers"
RESTART = "RESTART"

+RETRY = "Retry" +RETRY_SOLO = "Retry Solo"

RETURN = "Return"
RETURN_DESCRIPTION = "Returns to the previous menu."
RETURN_TO_BASE = "Exclusive Zone! Retreat immediately!"

@@ -1291,7 +1338,7 @@ ROOKIE_ONLY = "(rookie only)"

ROOT_WHIP = "Root Whip"
ROOT_WHIP_TOOLTIP = "Root whip into ground (on Infestation)"
RUPTURE = "Rupture"

-RUPTURE_TOOLTIP = "Splash infestation onto nearby enemy units temporarily clouding their vision and parasiting them." +RUPTURE_TOOLTIP = "Splash infestation onto nearby enemy units temporarily clouding their vision and parasiting them."

SANDBOX = "SANDBOX"
SB_ASSISTS = "A"
SB_CLICK_FOR_MOUSE = "Click for mouse"

@@ -1381,6 +1428,8 @@ SERVERBROWSER_SERVER_DETAILS_MODS = "Installed Mods:"

SERVERBROWSER_SERVER_DETAILS_PERF = "Performance:"
SERVERBROWSER_SERVER_DETAILS_PING = "Ping:"
SERVERBROWSER_SERVER_DETAILS_PLAYERS = "Players:"

+SERVERBROWSER_SERVER_DETAILS_SPECTATORS = "Spectators:" +SERVERBROWSER_SPECTATORS = "SPECTATORS"

SERVERBROWSER_SHOW_EMPTY = "EMPTY"
SERVERBROWSER_SHOW_FULL = "FULL"
SERVERBROWSER_SHOW_UNRANKED = "UNRANKED"

@@ -1428,6 +1477,15 @@ SHOW_HINTS = "SHOW HINTS"

SILENCE = "Silence"
SILENCE_TOOLTIP = "Make no sound when moving or attacking."
SKULK = "Skulk"

+SKULK_CHALLENGE_1 = "Skulk Challenge (Tram)" +SKULK_CHALLENGE_DEATH = "YOU DIED!" +SKULK_CHALLENGE_GO = "GO!" +SKULK_CHALLENGE_FINISH = "FINISH!" +SKULK_CHALLENGE_NEW_PERSONAL_BEST = "NEW\nPERSONAL\nBEST!" +SKULK_CHALLENGE_TIP_1 = "Jump along walls to dramatically increase your speed." +SKULK_CHALLENGE_TIP_2 = "Try to stay off the ground as much as possible to not lose speed." +SKULK_CHALLENGE_TIP_3 = "Gain a lot of speed by chaining together multiple wall jumps." +SKULK_CHALLENGE_TIP_4 = "You can hold %s to stop sticking to walls, which slows you down."

SKULK_EGG = "Skulk egg"
SKULK_TOOLTIP = "Can attack with bite and track enemies with parasite. Can ambush enemies by climbing on walls and ceilings. Can evolve leap and xenocide."
SOCKET_POWER_NODE = "Socket Power Node"

@@ -1443,9 +1501,9 @@ SPAWN_MARINE_TOOLTIP = "Dead Marines respawn here every 9 seconds"

SPIKES = "Spikes"
SPIKES_TOOLTIP = "Long-range spike attack"
SPIT = "Spit"

-SPIT_TOOLTIP = "Weak ranged attack. BIND_SecondaryAttack is heal spray, heals players and structures. It also speeds growth of structures." +SPIT_TOOLTIP = "Weak ranged attack. BIND_SecondaryAttack is heal spray, heals players and structures. It also speeds growth of structures."

SPORES = "Spores"

-SPORES_TOOLTIP = "Gives Lerks the ability to project a cloud of poisonous spores." +SPORES_TOOLTIP = "Gives Lerks the ability to project a cloud of poisonous spores."

SPRAY = "Spray"
SPRAY_TOOLTIP = "Heals players and structures. It also speeds growth of structures."
SPUR = "Spur"

@@ -1477,6 +1535,20 @@ STATUS_SHOTGUN = "Shotgun"

STATUS_SKULK = "Skulk"
STATUS_SPECTATOR = "Spectator"
STATUS_VOID = " "

+STEAM_CLOUD_ACCT_DISABLED_DESC = "If you would like to save and share replays, you will need to enable Steam cloud for your account via Steam -> Settings -> Cloud -> 'Enable Steam Cloud synchronization for applications which support it'." +STEAM_CLOUD_ACCT_DISABLED_PROMPT = "Cloud is still disabled. Try again?" +STEAM_CLOUD_ACCT_DISABLED_TRY_NOW = "Try now" +STEAM_CLOUD_ACCT_DISABLED_NO_THANKS = "No thanks" +STEAM_CLOUD_APP_DISABLED_DESC = "Steam Cloud is disabled for Natural Selection 2. If you would like to save your replay and attach it to your score, it must be enabled." +STEAM_CLOUD_APP_DISABLED_PROMPT = "Would you like to enable Steam Cloud for Natural Selection 2 now?" +STEAM_CLOUD_APP_DISABLED_JUST_FOR_REPLAYS = "Only for replays" +STEAM_CLOUD_GAME_DOWN_DESC = "Something went wrong when attempting to connect to Steam. You will be unable to upload your scores or replays." +STEAM_CLOUD_GAME_DOWN_PROMPT = "Please restart Natural Selection 2 to fix this." +STEAM_CLOUD_NO = "No" +STEAM_CLOUD_ODD_ERROR = "An error occurred while attempting to upload the replay." +STEAM_CLOUD_STEAM_DOWN_DESC = "Unable to connect to Steam. Without a connection to Steam, you will be unable to upload scores or replays to the leaderboard!" +STEAM_CLOUD_STEAM_DOWN_PROMPT = "Would you like to retry?" +STEAM_CLOUD_YES = "Yes"

STOMP = "Stomp"
STOMP_TOOLTIP = "Allows an Onos to smash the ground, disrupting enemy units"
STOP = "Stop"

@@ -1509,6 +1581,7 @@ THREE_HIVES = "Three Hives"

THREE_SHELLS = "Three Shells"
THREE_SPURS = "Three Spurs"
THREE_VEILS = "Three Veils"

+TIME = "Time"

TIP = "TIP"
TIPVIDEO_1_EXO_EXOSUIT_BOOSTERS = "While in an Exosuit, Hold Shift to boost forward, or hold space to hover."
TIPVIDEO_1_EXO_EXOSUIT_BOOSTERS_TITEL = "Exosuit: Boosters"

@@ -1672,6 +1745,7 @@ TIPVIDEO_3_PLAYER_USING_THE_MICROPHONE_TITEL = "Using the Microphone"

TIPVIDEO_3_PLAYER_X_MENU = "Hold X to bring up your communication menu."
TIPVIDEO_3_PLAYER_X_MENU_TITEL = "X Menu"
TIP_CLIPS = "TIP CLIPS"

+TOP_SPEED = "Top Speed"

TRAINING = "Training"
TRIPLE_BUFFERED = "TRIPLE BUFFERED"
TUNDRA_BUNDLE_MSG = "You have a Tundra Bundle in your Inventory. Would you like to open it now to receive the Tundra items?"

@@ -1684,6 +1758,7 @@ TUNNEL_ENTRANCE_OWNER = "%s's Gorge Tunnel"

TUNNEL_ENTRANCE_OWNER_ENDS_WITH_S = "%s' Gorge Tunnel"
TUT_COMING_SOON = "COMING SOON"
TUT_HIVE_CHALLENGE_TOOLTIP = "Improve your skill as Lerk or Fade by trying to hold a single hive against waves of Marines for as long as you can."

+TUT_SKULK_CHALLENGE_TOOLTIP = "Master the art of skulk wall-jumping as you race against the clock -- or against the ghosts of your competitors!"

TUT_START = "START"
TUTNAG_CLOSE = "Close"
TUTNAG_MSG = "TSF Regulation 1.01 recommends new recruits to take the standard galactic Training course before deployment."

@@ -2201,6 +2276,7 @@ VEIL = "Veil"

VEIL_HINT = "Confusion upgrade"
VEIL_TOOLTIP = "Grants new deception Alien trait"
VENT_ENTRANCE = "You can enter a ventilation shaft here. Ventilation shafts are used by Aliens to bypass Marines and quickly travel between two locations"

+VIEW_REPLAY = "View Replay"

VOICE_VOLUME = "VOICE VOLUME"
VORTEX = "Vortex"
VORTEX_TOOLTIP = "Creates a gate to which the Fade teleports back on its first attack."

@@ -2229,6 +2305,7 @@ VOTE_RANDOMIZE_RR_QUERY = "Randomize Ready Room?"

VOTE_RESET_GAME = "Reset Game"
VOTE_RESET_GAME_QUERY = "Reset game?"
VOTE_SPAM = "Cannot start vote. You started a failed vote of this type recently."

+VOTE_TOO_EARLY = "Cannot start vote. Game has not started yet."

VOTE_TOO_LATE = "Cannot start vote. Too far into a game."
VOTE_WAITING = "Cannot start another vote yet."
VOTE_YES = "Yes [%{key}s]"

diff --git a/ns2/lua/Alien_Client.lua b/ns2/lua/Alien_Client.lua index 2751d344f..3844450c8 100644 --- a/ns2/lua/Alien_Client.lua +++ b/ns2/lua/Alien_Client.lua @@ -605,7 +605,8 @@ end

function Alien:UpdateRegenerationEffect()
    
    local GUIRegenerationFeedback = ClientUI.GetScript("GUIRegenerationFeedback")

- if GUIRegenerationFeedback and GetHasRegenerationUpgrade(self) and GUIRegenerationFeedback:GetIsAnimating() then + if GUIRegenerationFeedback and GUIRegenerationFeedback:GetIsAnimating() and + GetHasRegenerationUpgrade(self) and GetShellLevel(self:GetTeamNumber()) > 0 then

        if self.lastHealth then
        

diff --git a/ns2/lua/Alien_Server.lua b/ns2/lua/Alien_Server.lua index cc0724b56..66e34eccb 100644 --- a/ns2/lua/Alien_Server.lua +++ b/ns2/lua/Alien_Server.lua @@ -82,11 +82,11 @@ function Alien:UpdateAutoHeal()

    if self:GetIsHealable() and ( not self.timeLastAlienAutoHeal or self.timeLastAlienAutoHeal + kAlienRegenerationTime <= Shared.GetTime() ) then

        local healRate = 1

- local hasRegenUpgrade = GetHasRegenerationUpgrade(self)

        local shellLevel = GetShellLevel(self:GetTeamNumber())

+ local hasRegenUpgrade = shellLevel > 0 and GetHasRegenerationUpgrade(self)

        local maxHealth = self:GetBaseHealth()
        

- if hasRegenUpgrade and shellLevel > 0 then + if hasRegenUpgrade then

            healRate = Clamp(kAlienRegenerationPercentage * maxHealth, kAlienMinRegeneration, kAlienMaxRegeneration) * (shellLevel/3)
        else
            healRate = Clamp(kAlienInnateRegenerationPercentage * maxHealth, kAlienMinInnateRegeneration, kAlienMaxInnateRegeneration) 

diff --git a/ns2/lua/Badges_Client.lua b/ns2/lua/Badges_Client.lua index 41cc81002..7a4fa9bff 100644 --- a/ns2/lua/Badges_Client.lua +++ b/ns2/lua/Badges_Client.lua @@ -11,11 +11,14 @@ function Badges_FetchBadges(_, response)

    badges = Badges_FetchBadgesFromDLC(badges)
    badges = Badges_FetchBadgesFromItems(badges)

+ badges = Badges_FetchBadgesFromStats(badges)

    for _, badge in ipairs(badges) do
        local badgeid = rawget(gBadges, badge)
        local data = Badges_GetBadgeData(badgeid)

- ownedBadges[badgeid] = data.columns + if data then + ownedBadges[badgeid] = data.columns + end

    end

    Badges_ApplyHive1Badges(response)

@@ -70,7 +73,7 @@ Client.HookNetworkMessage("BadgeRows",

        else
            ownedBadges[msg.badge] = msg.columns

- --Check for empty columns and autoselect avaible badge + --Check for empty columns and autoselect available badge

            local columns = Badges_GetBadgeColumns(msg.columns)
            for i = 1, #columns do
                local column = columns[i]

@@ -172,11 +175,14 @@ local function OnLoadComplete()

    local badges = {}
    badges = Badges_FetchBadgesFromItems(badges)

+ badges = Badges_FetchBadgesFromStats(badges)

    for _, badge in ipairs(badges) do
        local badgeid = rawget(gBadges, badge)
        local data = Badges_GetBadgeData(badgeid)

- ownedBadges[badgeid] = data.columns + if data then + ownedBadges[badgeid] = data.columns + end

    end

    for i = 1, 10 do

diff --git a/ns2/lua/Badges_Server.lua b/ns2/lua/Badges_Server.lua index aca4e15fd..46b690c9a 100644 --- a/ns2/lua/Badges_Server.lua +++ b/ns2/lua/Badges_Server.lua @@ -28,26 +28,28 @@ function Badges_FetchBadges(clientId, response)

    local userId = client:GetUserId()

    badges = Badges_FetchBadgesFromDLC(badges, client)

+ badges = Badges_FetchBadgesFromStats(badges, client)

    userId2OwnedBadges[userId] = userId2OwnedBadges[userId] or {}
    for _, badge in ipairs(badges) do
        local badgeid = rawget(gBadges, badge)
        local badgedata = Badges_GetBadgeData(badgeid)

- - userId2OwnedBadges[userId][badgeid] = badgedata.columns - - local queuedColumns = gBadgeClientRequestQueue[clientId] and gBadgeClientRequestQueue[clientId][badgeid] - if queuedColumns then - for i, queuedColumn in ipairs(queuedColumns) do - if Badges_SetBadge(clientId, badgeid, queuedColumn) then - table.remove(gBadgeClientRequestQueue[clientId][badgeid], i) + if badgedata then + userId2OwnedBadges[userId][badgeid] = badgedata.columns + + local queuedColumns = gBadgeClientRequestQueue[clientId] and gBadgeClientRequestQueue[clientId][badgeid] + if queuedColumns then + for i, queuedColumn in ipairs(queuedColumns) do + if Badges_SetBadge(clientId, badgeid, queuedColumn) then + table.remove(gBadgeClientRequestQueue[clientId][badgeid], i) + end

                end
            end

- end - Server.SendNetworkMessage(client, "BadgeRows", BuildBadgeRowsMessage(badgeid, badgedata.columns), true) + Server.SendNetworkMessage(client, "BadgeRows", BuildBadgeRowsMessage(badgeid, badgedata.columns), true)

- if (badge == "dev" or badge == "community_dev") then - gClientIdDevs[userId] = true + if (badge == "dev" or badge == "community_dev") then + gClientIdDevs[userId] = true + end

        end
    end
end

@@ -228,5 +230,13 @@ local function OnClientDisconnect(client)

    gBadgeClientRequestQueue[ client:GetId() ] = nil
end

+local function OnReceivedStatsForClient(clientSteamId) + + local steam32Id = Server.ConvertSteamId64To32(clientSteamId) + Badges_FetchBadges(steam32Id, {}) + +end +

Event.Hook("ClientConnect", OnClientConnect)

-Event.Hook("ClientDisconnect", OnClientDisconnect) \ No newline at end of file +Event.Hook("ClientDisconnect", OnClientDisconnect) +Event.Hook("ReceivedSteamStatsForClient", OnReceivedStatsForClient) \ No newline at end of file diff --git a/ns2/lua/Badges_Shared.lua b/ns2/lua/Badges_Shared.lua index 7e3c75daa..150da797b 100644 --- a/ns2/lua/Badges_Shared.lua +++ b/ns2/lua/Badges_Shared.lua @@ -43,7 +43,13 @@ gBadges = {

    "ensl_wc_bronze",
    "tournament_mm_blue",
    "tournament_mm_silver",

- "tournament_mm_gold" + "tournament_mm_gold", + "ensl_s11_gold", + "ensl_s11_silver", + "skulk_challenge_1_bronze", + "skulk_challenge_1_silver", + "skulk_challenge_1_gold", + "skulk_challenge_1_shadow",

}

--Stores information about textures and names of the Badges

@@ -91,6 +97,23 @@ do

        return data
    end
    

+ -- Creates a badge whose availability is tied to which player stats. + -- badgeName is name of badge and prefix for badge file name. + -- statName is the api name of the steam user stat associated with the badge. + -- hasBadgeFunction is evaluated with the value of the stat passed as the only paramter. If it returns true, this + -- means the badge is available. False of course means the badge is not available. + local function MakeStatsBadgeData(badgeName, statName, statType, hasBadgeFunction) + + local data = MakeBadgeData(badgeName) + + data.statName = statName + data.statType = statType + data.hasBadgeFunction = hasBadgeFunction + + return data + + end +

    --vanilla badges data
    badgeData["dev"] = MakeBadgeData("dev")
    badgeData["dev_retired"] = MakeBadgeData("dev_retired")

@@ -127,6 +150,26 @@ do

    badgeData["tournament_mm_blue"] = MakeItemBadgeData("tournament_mm_blue", 1008)
    badgeData["tournament_mm_silver"] = MakeItemBadgeData("tournament_mm_silver", 1009)
    badgeData["tournament_mm_gold"] = MakeItemBadgeData("tournament_mm_gold", 1010)

+ badgeData["ensl_s11_gold"] = MakeItemBadgeData("ensl_s11_gold", 1011) + badgeData["ensl_s11_silver"] = MakeItemBadgeData("ensl_s11_silver", 1012) + + -- stats badges + badgeData["skulk_challenge_1_bronze"] = MakeStatsBadgeData("skulk_challenge_1_bronze", "skulk_challenge_1", "INT", + function(value) + return value >= 1 + end) + badgeData["skulk_challenge_1_silver"] = MakeStatsBadgeData("skulk_challenge_1_silver", "skulk_challenge_1", "INT", + function(value) + return value >= 2 + end) + badgeData["skulk_challenge_1_gold"] = MakeStatsBadgeData("skulk_challenge_1_gold", "skulk_challenge_1", "INT", + function(value) + return value >= 3 + end) + badgeData["skulk_challenge_1_shadow"] = MakeStatsBadgeData("skulk_challenge_1_shadow", "skulk_challenge_1", "INT", + function(value) + return value >= 4 + end)

    --custom badges
    local badgeFiles = {}

@@ -170,6 +213,9 @@ do

    --List of all badges which are assigned to an item
    gItemBadges = {}

+ -- List of all badges which are awarded based on the user's steam stats. + gStatsBadges = {} +

    for badgeId, badgeName in ipairs(gBadges) do
        local badgedata = badgeData[badgeName]
        if badgedata then

@@ -182,16 +228,26 @@ do

                gItemBadges[#gItemBadges+1] = badgeId
            end

+ if badgedata.statName then + gStatsBadges[#gStatsBadges+1] = badgeId

        end

+

    end
end

+end

function Badges_GetBadgeData(badgeId)

- - return badgeData[gBadges[badgeId]] + local enumVal = rawget(gBadges, badgeId) + if not enumVal then return nil end + return badgeData[enumVal]

end

function Badges_SetName(badgeId, name)

+ + -- ensure badge exists in the enum. + local enumVal = rawget(gBadges, badgeId) + if not enumVal then return false end +

    if not badgeData[gBadges[badgeId]] or not name then return false end

    badgeData[gBadges[badgeId]].name = tostring(name)

@@ -214,7 +270,7 @@ end

function Badges_FetchBadgesFromDLC(badges, client)
    for _, badgeid in ipairs(gDLCBadges) do
        local data = Badges_GetBadgeData(badgeid)

- if GetHasDLC(data.productId, client) then + if data and GetHasDLC(data.productId, client) then

            badges[#badges + 1] = gBadges[badgeid]
        end
    end

@@ -226,7 +282,7 @@ end

function Badges_FetchBadgesFromItems(badges)
    for _, badgeid in ipairs(gItemBadges) do
        local data = Badges_GetBadgeData(badgeid)

- if GetOwnsItem(data.itemId) then + if data and GetOwnsItem(data.itemId) then

            badges[#badges + 1] = gBadges[badgeid]
        end
    end

@@ -234,6 +290,108 @@ function Badges_FetchBadgesFromItems(badges)

    return badges
end

+local function GetAreStatsAvailable(client) + + if Client then + return true -- should always be loaded on client, long before they even join a game. + elseif Server then + + if not client then + return false + end + + for _, badgeId in ipairs(gStatsBadges) do + + local data = Badges_GetBadgeData(badgeId) + if data then + local apiFunc + if data.statType == "INT" then + apiFunc = Server.GetHasUserStat_Int + elseif data.statType == "FLOAT" then + apiFunc = Server.GetHasUserStat_Float + end + + if apiFunc and apiFunc(client, data.statName) then + -- When stats are downloaded from Steam, they're all sent at once. Therefore if we have at least one + -- present, we know we have all we're ever going to get. If some are missing, we'll just discard them, + -- but it will generate an error message, as it should. + return true + end + end + + end + + end + + return false + +end + +-- Assign badges based on user stats +function Badges_FetchBadgesFromStats(badges, client) + + -- Don't attempt to retrieve stat data if it's missing -- we'll get errors for that! + if not GetAreStatsAvailable(client) then + + if Server then + -- Request stats from Steam. When we receive them, this function will be called again. + Server.RequestUserStats(client) + end + + return badges + end + + local sanityTest = Server + + for _, badgeid in ipairs(gStatsBadges) do + + local data = Badges_GetBadgeData(badgeid) + if data then + local statValue + + if Server then + + local apiFunc + if data.statType == "INT" then + apiFunc = Server.GetUserStat_Int + elseif data.statType == "FLOAT" then + apiFunc = Server.GetUserStat_Float + else + assert(false) + end + + statValue = apiFunc(client, data.statName) + + elseif Client then + + local apiFunc + if data.statType == "INT" then + apiFunc = Client.GetUserStat_Int + elseif data.statType == "FLOAT" then + apiFunc = Client.GetUserStat_Float + else + assert(false) + end + + statValue = apiFunc(data.statName) + + else + assert(false) + end + + local hasBadge = data.hasBadgeFunction(statValue) + if hasBadge then + badges[#badges + 1] = gBadges[badgeid] + end + + end + + end + + return badges + +end +

------------------------------------------
--  Create network message spec
------------------------------------------

diff --git a/ns2/lua/Client.lua b/ns2/lua/Client.lua index e66e6e0c6..4a2316f08 100644 --- a/ns2/lua/Client.lua +++ b/ns2/lua/Client.lua @@ -1117,6 +1117,7 @@ local function OnLoadComplete()

    Input_SyncInputOptions()
    HitSounds_SyncOptions()
    OptionsDialogUI_SyncSoundVolumes()

+ MainMenu_Preload()

    HiveVision_Initialize()
    EquipmentOutline_Initialize()

@@ -1310,4 +1311,4 @@ Script.Load("lua/PostLoadMod.lua")

InitializeRenderCamera()

-- setup the time buffer for the killcam - 8 seconds long

-Client.SetTimeBuffer(8) \ No newline at end of file +--Client.SetTimeBuffer(8) \ No newline at end of file diff --git a/ns2/lua/ConsoleCommands_Server.lua b/ns2/lua/ConsoleCommands_Server.lua index ef48ffde7..e92eaea1f 100644 --- a/ns2/lua/ConsoleCommands_Server.lua +++ b/ns2/lua/ConsoleCommands_Server.lua @@ -392,7 +392,7 @@ local function OnBotTraining(client)

    GetGamerules():SetBotTraining(true)

- Server.AddTag("ignore_playnow") + Server.DisableQuickPlay()

end

-- Generic console commands

diff --git a/ns2/lua/Entity.lua b/ns2/lua/Entity.lua index 9246f15d4..a00c77249 100644 --- a/ns2/lua/Entity.lua +++ b/ns2/lua/Entity.lua @@ -405,7 +405,7 @@ function RadiusDamage(entities, centerOrigin, radius, fullDamage, doer, ignoreLO

            damageDirection:Normalize()
            
            -- we can't hit world geometry, so don't pass any surface params and let DamageMixin decide

- doer:DoDamage(damage, target, target:GetOrigin(), damageDirection, "none") + doer:DoDamage(damage, target, centerOrigin, damageDirection, "none")

        end
        

diff --git a/ns2/lua/Exosuit.lua b/ns2/lua/Exosuit.lua index 2d3352b6d..700f16bcc 100644 --- a/ns2/lua/Exosuit.lua +++ b/ns2/lua/Exosuit.lua @@ -20,6 +20,7 @@ Script.Load("lua/GameEffectsMixin.lua")

Script.Load("lua/CorrodeMixin.lua")
Script.Load("lua/ExoVariantMixin.lua")
Script.Load("lua/EntityChangeMixin.lua")

+Script.Load("lua/StaticTargetMixin.lua")

class 'Exosuit' (ScriptActor)

@@ -98,6 +99,8 @@ function Exosuit:OnInitialized()

        self:SetIgnoreHealth(true)
        self:SetMaxArmor(kExosuitArmor)
        self:SetArmor(kExosuitArmor)

+ + InitMixin(self, StaticTargetMixin)

    elseif Client then
    

@@ -328,4 +331,4 @@ function Exosuit:GetIsPermanent()

    return true
end

-Shared.LinkClassToMap("Exosuit", Exosuit.kMapName, networkVars) \ No newline at end of file +Shared.LinkClassToMap("Exosuit", Exosuit.kMapName, networkVars) diff --git a/ns2/lua/Fade.lua b/ns2/lua/Fade.lua index 0a5128b0b..c89ee7f80 100644 --- a/ns2/lua/Fade.lua +++ b/ns2/lua/Fade.lua @@ -516,8 +516,10 @@ function Fade:OnProcessMove(input)

    end
        

- if not self:GetHasMetabolizeAnimationDelay() and self.previousweapon ~= nil then - self:SetActiveWeapon(self.previousweapon) + if not self:GetHasMetabolizeAnimationDelay() and self.previousweapon ~= nil and not self:GetIsBlinking() then + if self:GetActiveWeapon():GetMapName() == Metabolize.kMapName then + self:SetActiveWeapon(self.previousweapon) + end

        self.previousweapon = nil
    end
    

diff --git a/ns2/lua/GUIAlienTeamMessage.lua b/ns2/lua/GUIAlienTeamMessage.lua index 14f220bd8..17d00cce9 100644 --- a/ns2/lua/GUIAlienTeamMessage.lua +++ b/ns2/lua/GUIAlienTeamMessage.lua @@ -52,6 +52,12 @@ end

function GUIAlienTeamMessage:OnHelpScreenVisChange(state)
    
    self.visible = not state

+ self:UpdateVisibility() + +end + +function GUIAlienTeamMessage:UpdateVisibility() +

    self.background:SetIsVisible(self.visible and self.backgroundVisible)
    
end

@@ -94,4 +100,6 @@ function GUIAlienTeamMessage:SetTeamMessage(message)

    local bounceStart = function() self.messageText:SetScale(GUIScale(Vector(1.1, 1.1, 1)), 0.2, "bounce_in", AnimateSin, bounceEnd) end
    self.messageText:SetScale(GUIScale(Vector(1, 1, 1)), 0.25, "start", AnimateSin, bounceStart)
    

+ self:UpdateVisibility() +

end

\ No newline at end of file diff --git a/ns2/lua/GUIGorgeBuildMenu.lua b/ns2/lua/GUIGorgeBuildMenu.lua index e2455b798..c63a45b47 100644 --- a/ns2/lua/GUIGorgeBuildMenu.lua +++ b/ns2/lua/GUIGorgeBuildMenu.lua @@ -132,8 +132,8 @@ GUIGorgeBuildMenu.kPersonalResourceIcon.Height = 32

GUIGorgeBuildMenu.kResourceTexture = "ui/alien_commander_textures.dds"
GUIGorgeBuildMenu.kIconTextXOffset = 5

-local kBackgroundNoiseTexture = "ui/alien_commander_bg_smoke.dds" -local kSmokeyBackgroundSize +GUIGorgeBuildMenu.kBackgroundNoiseTexture = "ui/alien_commander_bg_smoke.dds" +GUIGorgeBuildMenu.kSmokeyBackgroundSize = Vector(220, 400, 0)

local kDefaultStructureCountPos = Vector(-48, -24, 0)
local kCenteredStructureCountPos = Vector(0, -24, 0)

@@ -169,7 +169,7 @@ function GUIGorgeBuildMenu:Initialize()

    GUIAnimatedScript.Initialize(self)
    

- kSmokeyBackgroundSize = GUIScale(Vector(220, 400, 0)) + self.kSmokeyBackgroundSize = GUIScale(Vector(220, 400, 0))

    self.scale = Client.GetScreenHeight() / GUIGorgeBuildMenu.kBaseYResolution
    self.background = self:CreateAnimatedGraphicItem()

@@ -320,11 +320,11 @@ function GUIGorgeBuildMenu:CreateButton(techId, scale, frame, keybind, position)

    local smokeyBackground = GetGUIManager():CreateGraphicItem()
    smokeyBackground:SetAnchor(GUIItem.Middle, GUIItem.Center)

- smokeyBackground:SetSize(kSmokeyBackgroundSize) - smokeyBackground:SetPosition(kSmokeyBackgroundSize * -.5) + smokeyBackground:SetSize(self.kSmokeyBackgroundSize) + smokeyBackground:SetPosition(self.kSmokeyBackgroundSize * -.5)

    smokeyBackground:SetShader("shaders/GUISmokeHUD.surface_shader")
    smokeyBackground:SetTexture("ui/alien_logout_smkmask.dds")

- smokeyBackground:SetAdditionalTexture("noise", kBackgroundNoiseTexture) + smokeyBackground:SetAdditionalTexture("noise", self.kBackgroundNoiseTexture)

    smokeyBackground:SetFloatParameter("correctionX", 0.6)
    smokeyBackground:SetFloatParameter("correctionY", 1)
    

diff --git a/ns2/lua/GUIInsight_OtherHealthbars.lua b/ns2/lua/GUIInsight_OtherHealthbars.lua index b33aeb6d6..30f050b25 100644 --- a/ns2/lua/GUIInsight_OtherHealthbars.lua +++ b/ns2/lua/GUIInsight_OtherHealthbars.lua @@ -10,15 +10,10 @@

class 'GUIInsight_OtherHealthbars' (GUIScript)

-local isVisible -local otherList -local reuseItems -

local kOtherHealthDrainRate = 0.1 --Percent per ???

local kOtherHealthBarTexture = "ui/healthbarsmall.dds"
local kOtherHealthBarTextureSize = Vector(64, 6, 0)

-local kOtherHealthBarSize

local kHealthDrainColor = Color(1, 0, 0, 1)
local kOtherTypes = {
    "CommandStructure",

@@ -54,42 +49,42 @@ function GUIInsight_OtherHealthbars:Initialize()

    self.updateInterval = 0
    

- isVisible = true + self.isVisible = true

- kOtherHealthBarSize = GUIScale(Vector(64, 6, 0)) + self.kOtherHealthBarSize = GUIScale(Vector(64, 6, 0))

- otherList = table.array(24) - reuseItems = table.array(32) + self.otherList = table.array(24) + self.reuseItems = table.array(32)

end

function GUIInsight_OtherHealthbars:Uninitialize()
   
    -- All healthbars

- for i, other in pairs(otherList) do + for _, other in pairs(self.otherList) do

        GUI.DestroyItem(other.Background)
    end

- otherList = nil + self.otherList = nil

    -- Reuse items

- for index, background in ipairs(reuseItems) do + for _, background in ipairs(self.reuseItems) do

        GUI.DestroyItem(background["Background"])
    end

- reuseItems = nil + self.reuseItems = nil

end

function GUIInsight_OtherHealthbars:OnResolutionChanged(oldX, oldY, newX, newY)

    self:Uninitialize()

- kOtherHealthBarSize = GUIScale(Vector(64, 6, 0)) + self.kOtherHealthBarSize = GUIScale(Vector(64, 6, 0))

    self:Initialize()

end

function GUIInsight_OtherHealthbars:SetisVisible(bool)

- isVisible = bool + self.isVisible = bool

end

@@ -103,10 +98,10 @@ function GUIInsight_OtherHealthbars:Update(deltaTime)

        others = Shared.GetEntitiesWithClassname(kOtherTypes[i])        
        -- Add new and Update all units
        

- for index, other in ientitylist(others) do + for _, other in ientitylist(others) do

            local otherIndex = other:GetId()

- local relevant = other:GetIsVisible() and isVisible and other:GetIsAlive() + local relevant = other:GetIsVisible() and self.isVisible and other:GetIsAlive()

            if (other:isa("PowerPoint") and not other:GetIsSocketed()) then
                relevant = false
            end

@@ -119,23 +114,23 @@ function GUIInsight_OtherHealthbars:Update(deltaTime)

                -- Get/Create Healthbar
                local otherGUI

- if not otherList[otherIndex] then -- Add new GUI for new units + if not self.otherList[otherIndex] then -- Add new GUI for new units

                    otherGUI = self:CreateOtherGUIItem()
                    otherGUI.StoredValues.HealthFraction = healthFraction

- table.insert(otherList, otherIndex, otherGUI) + table.insert(self.otherList, otherIndex, otherGUI)

                else
                

- otherGUI = otherList[otherIndex] + otherGUI = self.otherList[otherIndex]

                end
                

- otherList[otherIndex].Visited = true + self.otherList[otherIndex].Visited = true

                local barScale = maxHealth/2400 -- Based off ARC health

- local backgroundSize = math.max(kOtherHealthBarSize.x, barScale * kOtherHealthBarSize.x) - local kHealthbarOffset = Vector(-backgroundSize/2, -kOtherHealthBarSize.y - GUIScale(8), 0) + local backgroundSize = math.max(self.kOtherHealthBarSize.x, barScale * self.kOtherHealthBarSize.x) + local kHealthbarOffset = Vector(-backgroundSize/2, -self.kOtherHealthBarSize.y - GUIScale(8), 0)

                -- Calculate Health Bar Screen position
                local min, max = other:GetModelExtents()

@@ -151,14 +146,14 @@ function GUIInsight_OtherHealthbars:Update(deltaTime)

                -- background
                local background = otherGUI.Background
                background:SetPosition(nameTagInScreenspace)

- background:SetSize(Vector(backgroundSize,kOtherHealthBarSize.y, 0)) + background:SetSize(Vector(backgroundSize,self.kOtherHealthBarSize.y, 0))

                -- healthbar
                local healthBar = otherGUI.HealthBar
                local healthBarSize =  healthFraction * backgroundSize - GUIScale(2)
                local healthBarTextureSize = healthFraction * kOtherHealthBarTextureSize.x
                healthBar:SetTexturePixelCoordinates(unpack({0, 0, healthBarTextureSize, kOtherHealthBarTextureSize.y}))

- healthBar:SetSize(Vector(healthBarSize, kOtherHealthBarSize.y, 0)) + healthBar:SetSize(Vector(healthBarSize, self.kOtherHealthBarSize.y, 0))

                healthBar:SetColor(color)
                
                -- health change bar              

@@ -170,7 +165,7 @@ function GUIInsight_OtherHealthbars:Update(deltaTime)

                    local changeBarSize = (previousHealthFraction - healthFraction) * backgroundSize
                    local changeBarTextureSize = (previousHealthFraction - healthFraction) * kOtherHealthBarTextureSize.x
                    healthChangeBar:SetTexturePixelCoordinates(unpack({healthBarTextureSize, 0, healthBarTextureSize + changeBarTextureSize, kOtherHealthBarTextureSize.y}))

- healthChangeBar:SetSize(Vector(changeBarSize, kOtherHealthBarSize.y, 0)) + healthChangeBar:SetSize(Vector(changeBarSize, self.kOtherHealthBarSize.y, 0))

                    healthChangeBar:SetPosition(Vector(healthBarSize, 0, 0))
                    otherGUI.StoredValues.HealthFraction = math.max(healthFraction, previousHealthFraction - (deltaTime * kOtherHealthDrainRate))
                    

@@ -186,11 +181,11 @@ function GUIInsight_OtherHealthbars:Update(deltaTime)

    -- Slay any Others that were not visited during the update.
    -- dragonglass lol

- for id, other in pairs(otherList) do + for id, other in pairs(self.otherList) do

        if not other.Visited then
            other.Background:SetIsVisible(false)

- table.insert(reuseItems, other) - otherList[id] = nil + table.insert(self.reuseItems, other) + self.otherList[id] = nil

        end
        other.Visited = false
    end

@@ -200,9 +195,8 @@ end

function GUIInsight_OtherHealthbars:CreateOtherGUIItem()

    -- Reuse an existing healthbar item if there is one.

- if table.count(reuseItems) > 0 then - local returnbackground = reuseItems[1] - table.remove(reuseItems, 1) + if #self.reuseItems > 0 then + local returnbackground = table.remove(self.reuseItems, 1)

        return returnbackground
    end

diff --git a/ns2/lua/GUIInsight_PlayerHealthbars.lua b/ns2/lua/GUIInsight_PlayerHealthbars.lua index c3049f851..52b785f5a 100644 --- a/ns2/lua/GUIInsight_PlayerHealthbars.lua +++ b/ns2/lua/GUIInsight_PlayerHealthbars.lua @@ -36,7 +36,8 @@ local kParasiteColor = Color(1, 1, 0, 1)

local kPoisonColor = Color(0, 1, 0, 1)
local kHealthDrainColor = Color(1, 0, 0, 1)
local kEnergyColor = Color(1,1,0,1)

-local kAmmoColors = { + +GUIInsight_PlayerHealthbars.kAmmoColors = {

    ["rifle"] = Color(0,0,1,1), -- blue
    ["pistol"] = Color(0,1,1,1), -- teal
    ["axe"] = Color(1,1,1,1), -- white

@@ -48,7 +49,8 @@ local kAmmoColors = {

    ["grenadelauncher"] = Color(1,0,1,1), -- magenta
    ["hmg"] = Color(1,0,0,1), -- red
    ["minigun"] = Color(1,0,0,1), -- red

- ["railgun"] = Color(1,0.5,0,1)} -- orange + ["railgun"] = Color(1,0.5,0,1) -- orange +}

function GUIInsight_PlayerHealthbars:Initialize()

@@ -251,7 +253,7 @@ function GUIInsight_PlayerHealthbars:UpdatePlayers(deltaTime)

            else
                local activeWeapon = player:GetActiveWeapon()
                if activeWeapon then

- local ammoColor = kAmmoColors[activeWeapon.kMapName] or kEnergyColor + local ammoColor = self.kAmmoColors[activeWeapon.kMapName] or kEnergyColor

                    if activeWeapon:isa("ClipWeapon") then
                        energyFraction = activeWeapon:GetClip() / activeWeapon:GetClipSize()
                    elseif activeWeapon:isa("ExoWeaponHolder") then

@@ -270,7 +272,7 @@ function GUIInsight_PlayerHealthbars:UpdatePlayers(deltaTime)

                            end
                            energyFraction = 1 - energyFraction
                        end                            

- ammoColor = kAmmoColors[rightWeapon.kMapName] + ammoColor = self.kAmmoColors[rightWeapon.kMapName]

                    end
                    energyBar:SetColor(ammoColor)
                end

diff --git a/ns2/lua/GUIManager.lua b/ns2/lua/GUIManager.lua index a69594048..3b699306a 100644 --- a/ns2/lua/GUIManager.lua +++ b/ns2/lua/GUIManager.lua @@ -277,7 +277,9 @@ function GUIManager:Update(deltaTime)

                local isDefault = script.updateInterval == GUIManager.kUpdateInterval 
                if not isDefault or numDefaultsUpdated < allowedUpdatesForDefaultUpdateInterval then
                    numDefaultsUpdated = numDefaultsUpdated + (isDefault and 1 or 0)

- dt = script.lastUpdateTime > 0 and now - script.lastUpdateTime or deltaTime + if not script.deltaIsFrameTime then -- allow script to request frame time to be passed instead + dt = script.lastUpdateTime > 0 and now - script.lastUpdateTime or deltaTime + end

                    script.lastUpdateTime = now
                else
                    script = nil

diff --git a/ns2/lua/GUIMarineTeamMessage.lua b/ns2/lua/GUIMarineTeamMessage.lua index 8a4008d0e..003aba209 100644 --- a/ns2/lua/GUIMarineTeamMessage.lua +++ b/ns2/lua/GUIMarineTeamMessage.lua @@ -49,6 +49,12 @@ end

function GUIMarineTeamMessage:OnHelpScreenVisChange(state)
    
    self.visible = not state

+ self:UpdateVisibility() + +end + +function GUIMarineTeamMessage:UpdateVisibility() +

    self.background:SetIsVisible(self.visible and self.backgroundVisible)
    
end

@@ -90,4 +96,6 @@ function GUIMarineTeamMessage:SetTeamMessage(message)

    local bounceStart = function() self.messageText:SetScale(GUIScale(Vector(1.1, 1.1, 1)), 0.2, "bounce_in", AnimateSin, bounceEnd) end
    self.messageText:SetScale(GUIScale(Vector(1, 1, 1)), 0.25, "start", AnimateSin, bounceStart)
    

+ self:UpdateVisibility() +

end

\ No newline at end of file diff --git a/ns2/lua/GUIMinimapFrame.lua b/ns2/lua/GUIMinimapFrame.lua index 0685ca9d9..d4e392360 100644 --- a/ns2/lua/GUIMinimapFrame.lua +++ b/ns2/lua/GUIMinimapFrame.lua @@ -73,7 +73,7 @@ local function UpdateItemsGUIScale(self)

    -- Cycling through modes resizes and respotions everything
    -- Not really elegant, but gets the job done

- local comMode = self.comMode + local comMode = self.comMode or 0

    self:SetBackgroundMode(0, true)
    self:SetBackgroundMode(1, true)
    self:SetBackgroundMode(comMode, true)

diff --git a/ns2/lua/GUIPing.lua b/ns2/lua/GUIPing.lua index f0722a79d..806a3ebd3 100644 --- a/ns2/lua/GUIPing.lua +++ b/ns2/lua/GUIPing.lua @@ -26,7 +26,9 @@ function GUIPing:Initialize()

    self.pingItem = GUICreateCommanderPing()
    self.screenDiagonalLength = math.sqrt(Client.GetScreenHeight() / 2) ^ 2 + (Client.GetScreenWidth() / 2)
    

- self.visible = true + self:SetIsVisible(true) -- init to true, but won't be visible until used. + self.pingItem.Frame:SetIsVisible(false) -- frame is not visible + self.expiredPingTime = 0 -- init to 0 so it isn't shown at the beginning.

end

diff --git a/ns2/lua/GUIScoreboard.lua b/ns2/lua/GUIScoreboard.lua index 4d99ffa97..475364a7a 100644 --- a/ns2/lua/GUIScoreboard.lua +++ b/ns2/lua/GUIScoreboard.lua @@ -69,7 +69,6 @@ local kSkillBarPadding = 4

local lastScoreboardVisState = false

local kSteamProfileURL = "http://steamcommunity.com/profiles/"

-local kHiveProfileURL = "http://hive.naturalselection2.com/profile/"

local kMinTruncatedNameLength = 8

-- Color constants.

@@ -114,7 +113,7 @@ end

local function CreateTeamBackground(self, teamNumber)

- local color = nil + local color

    local teamItem = GUIManager:CreateGraphicItem()
    teamItem:SetStencilFunc(GUIItem.NotEqual)
    

@@ -1305,9 +1304,6 @@ function GUIScoreboard:SendKeyEvent(key, down)

            local function openSteamProf()
                Client.ShowWebpage(string.format("%s[U:1:%s]", kSteamProfileURL, steamId))
            end

- local function openHiveProf() - Client.ShowWebpage(string.format("%s%s", kHiveProfileURL, steamId)) - end

            local function muteText()
                ChatUI_SetSteamIdTextMuted(steamId, not isTextMuted)
            end

@@ -1345,7 +1341,6 @@ function GUIScoreboard:SendKeyEvent(key, down)

            self.hoverMenu:SetBackgroundColor(bgColor)
            self.hoverMenu:AddButton(playerName, nameBgColor, nameBgColor, textColor)
            self.hoverMenu:AddButton(Locale.ResolveString("SB_MENU_STEAM_PROFILE"), teamColorBg, teamColorHighlight, textColor, openSteamProf)

- self.hoverMenu:AddButton(Locale.ResolveString("SB_MENU_HIVE_PROFILE"), teamColorBg, teamColorHighlight, textColor, openHiveProf)

            if Client.GetSteamId() ~= steamId then
                self.hoverMenu:AddSeparator("muteOptions")

diff --git a/ns2/lua/GUIUnitStatus.lua b/ns2/lua/GUIUnitStatus.lua index f6fc78d09..4de1bccfd 100644 --- a/ns2/lua/GUIUnitStatus.lua +++ b/ns2/lua/GUIUnitStatus.lua @@ -430,7 +430,7 @@ function AddAbilityBar(blipItem)

end

-local function UpdateUnitStatusBlip( self, blipData, updateBlip, localPlayerIsCommander, baseResearchRot, showHints, playerTeamType ) +function GUIUnitStatus:UpdateUnitStatusBlip( blipData, updateBlip, localPlayerIsCommander, baseResearchRot, showHints, playerTeamType )

    PROFILE("GUIUnitStatus:UpdateUnitStatusBlip")
    

@@ -741,8 +741,8 @@ local function UpdateUnitStatusList(self, activeBlips, deltaTime)

    -- Update current blip state.
    for i = 1, #self.activeBlipList do

- - UpdateUnitStatusBlip( self, activeBlips[i], self.activeBlipList[i], localPlayerIsCommander, baseResearchRot, showHints, playerTeamType ) + + self:UpdateUnitStatusBlip( activeBlips[i], self.activeBlipList[i], localPlayerIsCommander, baseResearchRot, showHints, playerTeamType )

    end

@@ -778,8 +778,8 @@ local function UpdatePerFrameInfo(self)

                blipData = self.activeStatusInfo[i]
                    
                if blipData then

- - UpdateUnitStatusBlip( self, blipData, self.activeBlipList[i], localPlayerIsCommander, baseResearchRot, showHints, playerTeamType ) + + self:UpdateUnitStatusBlip( blipData, self.activeBlipList[i], localPlayerIsCommander, baseResearchRot, showHints, playerTeamType )

                end
              

diff --git a/ns2/lua/Gamerules.lua b/ns2/lua/Gamerules.lua index 2dbd33b95..dfa91d2cc 100644 --- a/ns2/lua/Gamerules.lua +++ b/ns2/lua/Gamerules.lua @@ -125,6 +125,30 @@ function Gamerules:GetCanPlayerHearPlayer(listenerPlayer, speakerPlayer, channel

    return true    
end

+function Gamerules:GetCanJoinPlayingTeam(player) + if player:GetIsSpectator() then + local numPlayer = Server.GetNumPlayersTotal() + local maxPlayers = Server.GetMaxPlayers() + local numRes = Server.GetReservedSlotLimit() + + --check for empty player slots excluding reserved slots + if numPlayer >= maxPlayers then + Server.SendNetworkMessage(player, "JoinError", BuildJoinErrorMessage(3), true) + return false + end + + --check for empty player slots including reserved slots + local userId = player:GetSteamId() + local hasReservedSlot = GetHasReservedSlotAccess(userId) + if numPlayer >= (maxPlayers - numRes) and not hasReservedSlot then + Server.SendNetworkMessage(player, "JoinError", BuildJoinErrorMessage(3), true) + return false + end + end + + return true +end +

function Gamerules:RespawnPlayer(player)

    -- Randomly choose unobstructed spawn points to respawn the player

diff --git a/ns2/lua/GraphDrivenModel.lua b/ns2/lua/GraphDrivenModel.lua new file mode 100644 index 000000000..f7f451406 --- /dev/null +++ b/ns2/lua/GraphDrivenModel.lua @@ -0,0 +1,141 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\GraphDrivenModel.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Client-only, purely visual animated model that is driven by an animation graph. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +local graphDrivenModels = {} -- for updating them. + +class 'GraphDrivenModel' + +function CreateGraphDrivenModel(modelName, graphName, optionalRenderSceneZone) + + assert(modelName) + assert(modelName ~= "") + + assert(graphName) + assert(graphName ~= "") + + optionalRenderSceneZone = optionalRenderSceneZone or RenderScene.Zone_Default + + local gdm = GraphDrivenModel() + + gdm.renderModel = Client.CreateRenderModel(optionalRenderSceneZone) + gdm.renderModel:SetModelByName(modelName) + + gdm.modelIndex = Shared.GetModelIndex(modelName) + + gdm.graphIndex = Shared.GetAnimationGraphIndex(graphName) + if gdm.graphIndex == 0 then + error("Animation graph " .. graphName .. " does not exist!") + return nil + end + + gdm.coords = Coords() + gdm.poseParams = PoseParams() + gdm.boneCoords = CoordsArray() + gdm.animationState = AnimationGraphState() + + table.insert(graphDrivenModels, gdm) + + return gdm + +end + +function DestroyGraphDrivenModel(gdm) + + if gdm.renderModel then + Client.DestroyRenderModel(gdm.renderModel) + gdm.renderModel = nil + end + +end + +function GraphDrivenModel:SetCoords(coords) + + self.coords = coords + self.renderModel:SetCoords(coords) + +end + +function GraphDrivenModel:GetCoords() + + return self.coords + +end + +function GraphDrivenModel:SetPoseParam(name, value) + + local model = Shared.GetModel(self.modelIndex) + if model == nil then + return + end + + local paramIndex = model:GetPoseParamIndex(name) + self.poseParams:Set(paramIndex, value) + +end + +function GraphDrivenModel:SetAnimationInput(name, value) + + local graph = Shared.GetAnimationGraph(self.graphIndex) + if not graph then + return + end + self.animationState:SetInputValue(graph, name, value) + +end + +function GraphDrivenModel:SetPreUpdateCallbackFunction(callback) + + self.callback = callback + +end + +function GraphDrivenModel:Update() + + -- Skip updating if the render model was culled last frame. Ideally we'd be able to tell if it was going to be rendered + -- THIS frame... but that's going to be too expensive. A single non-updated frame is a small price to pay... and the + -- occlusion culling algorithm is pretty conservative anyways, so it'll likely never be noticed anyways. + if self.renderModel:GetNumFramesInvisible() > 0 then + return + end + + local now = Shared.GetTime() + local prev = Shared.GetPreviousTime() + local delta = now - prev + + if self.callback then + self.callback(self, delta) + end + + local graph = Shared.GetAnimationGraph(self.graphIndex) + if not graph then + return + end + + self.animationState:PrepareForGraph(graph) + + local model = Shared.GetModel(self.modelIndex) + local passedTags = {} + + self.animationState:Update(graph, model, self.poseParams, prev, now, passedTags) + self.animationState:Transition(graph, model, passedTags) + + self.animationState:GetBoneCoords(model, self.poseParams, self.boneCoords) + self.renderModel:SetBoneCoords(self.boneCoords) + +end + +local function UpdateGraphDrivenModels() + + for i=1, #graphDrivenModels do + graphDrivenModels[i]:Update() + end + +end +Event.Hook("UpdateRender", UpdateGraphDrivenModels) diff --git a/ns2/lua/Hud/GUIInventory.lua b/ns2/lua/Hud/GUIInventory.lua index b23562065..1ab13aa59 100644 --- a/ns2/lua/Hud/GUIInventory.lua +++ b/ns2/lua/Hud/GUIInventory.lua @@ -18,7 +18,7 @@ GUIInventory.kInactiveColor = Color(0.6, 0.6, 0.6, 0.6)

GUIInventory.kItemSize = Vector(96, 48, 0)
GUIInventory.kItemPadding = 20

-local function UpdateItemsGUIScale(self) +local function UpdateItemsGUIScale()

    GUIInventory.kBackgroundYOffset = GUIScale(-120)
end

@@ -34,7 +34,7 @@ function CreateInventoryDisplay(scriptHandle, hudLayer, frame)

end

-local function CreateInventoryItem(self, index, alienStyle) +function GUIInventory:CreateInventoryItem(_, alienStyle)

    local item = self.script:CreateAnimatedGraphicItem()
    

@@ -62,15 +62,15 @@ local function CreateInventoryItem(self, index, alienStyle)

end

-local function LocalAdjustSlot(self, index, hudSlot, techId, isActive, resetAnimations, alienStyle) +function GUIInventory:LocalAdjustSlot(index, hudSlot, techId, isActive, resetAnimations, alienStyle)

- local inventoryItem = nil + local inventoryItem

    if self.inventoryIcons[index] then
        inventoryItem = self.inventoryIcons[index]
    else

- inventoryItem = CreateInventoryItem(self, index) - inventoryItem.Graphic:Pause(2, "ANIM_INVENTORY_ITEM_PAUSE", AnimateLinear, function(script, item) item:FadeOut(0.5, "ANIM_INVENTORY_ITEM") end ) + inventoryItem = self:CreateInventoryItem(index, alienStyle) + inventoryItem.Graphic:Pause(2, "ANIM_INVENTORY_ITEM_PAUSE", AnimateLinear, function(_, item) item:FadeOut(0.5, "ANIM_INVENTORY_ITEM") end )

    end
    
    inventoryItem.KeyText:SetText(BindingsUI_GetInputValue("Weapon" .. hudSlot))

@@ -79,7 +79,7 @@ local function LocalAdjustSlot(self, index, hudSlot, techId, isActive, resetAnim

    inventoryItem.Graphic:SetPosition(Vector( (GUIInventory.kItemPadding + GUIInventory.kItemSize.x) * (index-1) , 0, 0) )
    
    if resetAnimations then

- inventoryItem.Graphic:Pause(2, "ANIM_INVENTORY_ITEM_PAUSE", AnimateLinear, function(script, item) item:FadeOut(0.5, "ANIM_INVENTORY_ITEM") end ) + inventoryItem.Graphic:Pause(2, "ANIM_INVENTORY_ITEM_PAUSE", AnimateLinear, function(_, item) item:FadeOut(0.5, "ANIM_INVENTORY_ITEM") end )

    end
    
    if inventoryItem.Graphic:GetHasAnimation("ANIM_INVENTORY_ITEM_PAUSE") then

@@ -92,7 +92,7 @@ function GUIInventory:Initialize()

    self.scale = 1
    

- UpdateItemsGUIScale(self) + UpdateItemsGUIScale()

    self.lastPersonalResources = 0
    

@@ -111,7 +111,7 @@ end

function GUIInventory:Reset(scale)

    self.scale = scale

- UpdateItemsGUIScale(self) + UpdateItemsGUIScale()

    self.background:SetPosition(Vector(0, GUIInventory.kBackgroundYOffset, 0) * self.scale)

end

@@ -164,7 +164,7 @@ function GUIInventory:Update(deltaTime, parameters)

        local alienStyle = PlayerUI_GetTeamType() == kAlienTeamType
        
        for index, inventoryItem in ipairs(inventoryTechIds) do

- LocalAdjustSlot(self, index, inventoryItem.HUDSlot, inventoryItem.TechId, inventoryItem.TechId == activeWeaponTechId, resetAnimations, alienStyle) + self:LocalAdjustSlot(index, inventoryItem.HUDSlot, inventoryItem.TechId, inventoryItem.TechId == activeWeaponTechId, resetAnimations, alienStyle)

        end
    
    end

diff --git a/ns2/lua/Hud/HelpScreen/HelpScreen.lua b/ns2/lua/Hud/HelpScreen/HelpScreen.lua index afa7a9434..39166dee2 100644 --- a/ns2/lua/Hud/HelpScreen/HelpScreen.lua +++ b/ns2/lua/Hud/HelpScreen/HelpScreen.lua @@ -581,6 +581,10 @@ function HelpScreen:GetShouldDisplay()

        return false
    end
    

+ if ChatUI_EnteringChatMessage() then + return false + end +

    local player = Client.GetLocalPlayer()
    if not player or player:isa("Commander") then
        return false

diff --git a/ns2/lua/Hud/HelpScreen/HelpScreenBinding.lua b/ns2/lua/Hud/HelpScreen/HelpScreenBinding.lua index 8954f67eb..d3f36e29b 100644 --- a/ns2/lua/Hud/HelpScreen/HelpScreenBinding.lua +++ b/ns2/lua/Hud/HelpScreen/HelpScreenBinding.lua @@ -461,7 +461,7 @@ function HelpScreenBinding:GetScaledSize()

    end
    
    if self.text then

- return Vector(self.text:GetTextWidth() * self.text:GetScale().x, self.text:GetTextHeight() * self.text:GetScale().y, 0) + return Vector(self.text:GetTextWidth(self.text:GetText()) * self.text:GetScale().x, self.text:GetTextHeight(self.text:GetText()) * self.text:GetScale().y, 0)

    end
    
    return Vector(0,0,0)

diff --git a/ns2/lua/MAC.lua b/ns2/lua/MAC.lua index 29d286415..1e32bf227 100644 --- a/ns2/lua/MAC.lua +++ b/ns2/lua/MAC.lua @@ -74,12 +74,12 @@ MAC.kWelderNode = "fxnode_welder"

-- Balance
MAC.kConstructRate = 0.4
MAC.kWeldRate = 0.5

-MAC.kOrderScanRadius = 10 +MAC.kOrderScanRadius = 12

MAC.kRepairHealthPerSecond = 50
MAC.kHealth = kMACHealth
MAC.kArmor = kMACArmor
MAC.kMoveSpeed = 6

-MAC.kHoverHeight = .5 +MAC.kHoverHeight = 0.5

MAC.kStartDistance = 3
MAC.kWeldDistance = 2
MAC.kBuildDistance = 2     -- Distance at which bot can start building a structure.

@@ -93,8 +93,8 @@ MAC.kWeldPositionCheckInterval = 1

-- how fast the MAC rolls out of the ARC factory. Standard speed is just too fast.
MAC.kRolloutSpeed = 2

-MAC.kCapsuleHeight = .2 -MAC.kCapsuleRadius = .5 +MAC.kCapsuleHeight = 0.2 +MAC.kCapsuleRadius = 0.5

-- Greetings
MAC.kGreetingUpdateInterval = 1

@@ -228,6 +228,9 @@ function MAC:OnInitialized()

        self.jetsSound:SetAsset(kJetsSound)
        self.jetsSound:SetParent(self)

+ self.leashedPosition = nil + self.autoReturning = false +

    elseif Client then
    
        InitMixin(self, UnitStatusMixin)     

@@ -539,7 +542,7 @@ local function GetBackPosition(self, target)

    local weldPos = None    
    local dot = targetViewAxis:DotProduct(fromTarget)    
    -- if we are in front or not sufficiently away from the target, we calculate a new weldPos

- if dot > 0 or targetDist < MAC.kWeldDistance - 0.5 then + if dot > 0.866 or targetDist < MAC.kWeldDistance - 0.5 then

        -- we are in front, find out back positon
        local obstacleSize = 0
        if HasMixin(target, "Extents") then

@@ -549,7 +552,7 @@ local function GetBackPosition(self, target)

        -- left or right
        local targetPos = target:GetOrigin()
        local toMidPos = targetViewAxis * (obstacleSize + MAC.kWeldDistance - 0.1)

- local midWeldPos = targetPos - targetViewAxis * (obstacleSize + MAC.kWeldDistance - 0.1) + local midWeldPos = targetPos - targetViewAxis * (obstacleSize + MAC.kWeldDistance - 0.4)

        local leftV = Vector(-targetViewAxis.z, targetViewAxis.y, targetViewAxis.x)
        local rightV = Vector(targetViewAxis.z, targetViewAxis.y, -targetViewAxis.x)
        local leftWeldPos = midWeldPos + leftV * 2

@@ -572,8 +575,6 @@ local function GetBackPosition(self, target)

end
local function CheckBehindBackPosition(self, orderTarget)

- local toTarget = (orderTarget:GetOrigin() - self:GetOrigin()) - local distanceToTarget = toTarget:GetLength()

    if not self.timeOfLastBackPositionCheck or Shared.GetTime() > self.timeOfLastBackPositionCheck + MAC.kWeldPositionCheckInterval then
 

@@ -591,76 +592,70 @@ function MAC:ProcessWeldOrder(deltaTime, orderTarget, orderLocation, autoWeld)

    local canBeWeldedNow = false
    local orderStatus = kOrderStatus.InProgress

- if self.timeOfLastWeld == 0 or time > self.timeOfLastWeld + MAC.kWeldRate then - - -- Not allowed to weld after taking damage recently. - if Shared.GetTime() - self:GetTimeLastDamageTaken() <= 1.0 then + -- It is possible for the target to not be weldable at this point. + -- This can happen if a damaged Marine becomes Commander for example. + -- The Commander is not Weldable but the Order correctly updated to the + -- new entity Id of the Commander. In this case, the order will simply be completed. + if orderTarget and HasMixin(orderTarget, "Weldable") then

- return kOrderStatus.InProgress - + local toTarget = (orderLocation - self:GetOrigin()) + local distanceToTarget = toTarget:GetLength() + canBeWeldedNow = orderTarget:GetCanBeWelded(self) + + local obstacleSize = 0 + if HasMixin(orderTarget, "Extents") then + obstacleSize = orderTarget:GetExtents():GetLengthXZ()

        end

- - -- It is possible for the target to not be weldable at this point. - -- This can happen if a damaged Marine becomes Commander for example. - -- The Commander is not Weldable but the Order correctly updated to the - -- new entity Id of the Commander. In this case, the order will simply be completed. - if orderTarget and HasMixin(orderTarget, "Weldable") then - - local toTarget = (orderLocation - self:GetOrigin()) - local distanceToTarget = toTarget:GetLength() - canBeWeldedNow = orderTarget:GetCanBeWelded(self) - - local obstacleSize = 0 - if HasMixin(orderTarget, "Extents") then - obstacleSize = orderTarget:GetExtents():GetLengthXZ() + + local tooFarFromLeash = self.leashedPosition and Vector(self.leashedPosition - self:GetOrigin()):GetLength() > 30 or false + + if autoWeld and (distanceToTarget > 15 or tooFarFromLeash) then + orderStatus = kOrderStatus.Cancelled + elseif not canBeWeldedNow then + orderStatus = kOrderStatus.Completed + else + local forceMove = false + local targetPosition = orderTarget:GetOrigin() + + local closeEnoughToWeld = distanceToTarget - obstacleSize < MAC.kWeldDistance + 0.5 + local shouldMoveCloser = distanceToTarget - obstacleSize > MAC.kWeldDistance + + if closeEnoughToWeld then + local backPosition = CheckBehindBackPosition(self, orderTarget) + if backPosition then + forceMove = true + targetPosition = backPosition + end

            end

- - if autoWeld and distanceToTarget > 15 then - orderStatus = kOrderStatus.Cancelled - elseif not canBeWeldedNow then - orderStatus = kOrderStatus.Completed + + if shouldMoveCloser or forceMove then + -- otherwise move towards it + local hoverAdjustedLocation = GetHoverAt(self, targetPosition) + local doneMoving = self:MoveToTarget(PhysicsMask.AIMovement, hoverAdjustedLocation, self:GetMoveSpeed(), deltaTime) + self.moving = not doneMoving

            else

- local forceMove = false - local targetPosition = orderTarget:GetOrigin() - - local closeEnoughToWeld = distanceToTarget - obstacleSize < MAC.kWeldDistance - - if closeEnoughToWeld then - local backPosition = CheckBehindBackPosition(self, orderTarget) - if backPosition then - forceMove = true - targetPosition = backPosition - end - end - - -- If we're close enough to weld, weld (unless we must move to behind the player) - if not forceMove and closeEnoughToWeld and not GetIsVortexed(self) then + self.moving = false + end + + -- Not allowed to weld after taking damage recently. + if Shared.GetTime() - self:GetTimeLastDamageTaken() <= 1.0 then + return kOrderStatus.InProgress + end + + -- Weld target if we're close enough to weld and enough time has passed since last weld + if closeEnoughToWeld and not GetIsVortexed(self) and (time > self.timeOfLastWeld + MAC.kWeldRate) then + orderTarget:OnWeld(self, MAC.kWeldRate) + self.timeOfLastWeld = time + end

- orderTarget:OnWeld(self, MAC.kWeldRate) - self.timeOfLastWeld = time - self.moving = false - - else - - -- otherwise move towards it - local hoverAdjustedLocation = GetHoverAt(self, targetPosition) - local doneMoving = self:MoveToTarget(PhysicsMask.AIMovement, hoverAdjustedLocation, self:GetMoveSpeed(), deltaTime) - self.moving = not doneMoving - if doneMoving then - self.weldPosition = None - end - end - - end - - else - orderStatus = kOrderStatus.Cancelled

        end

- + + else + orderStatus = kOrderStatus.Cancelled

    end
    
    -- Continuously turn towards the target. But don't mess with path finding movement if it was done.

- if not self.moving and orderLocation then + if orderLocation then

        local toOrder = (orderLocation - self:GetOrigin())
        self:SmoothTurn(deltaTime, GetNormalizedVector(toOrder), 0)

@@ -804,10 +799,26 @@ function MAC:ProcessConstruct(deltaTime, orderTarget, orderLocation)

end

local function FindSomethingToDo(self)

- +

    local target, orderType = GetAutomaticOrder(self)

+

    if target and orderType then

- return self:GiveOrder(orderType, target:GetId(), target:GetOrigin(), nil, false, false) ~= kTechId.None + if self.leashedPosition then + local tooFarFromLeash = Vector(self.leashedPosition - target:GetOrigin()):GetLength() > 15 + if tooFarFromLeash then + --DebugPrint("Strayed too far!") + return false + end + else + self.leashedPosition = GetHoverAt(self, self:GetOrigin()) + --DebugPrint("return position set "..ToString(self.leashedPosition)) + end + self.autoReturning = false + return self:GiveOrder(orderType, target:GetId(), target:GetOrigin(), nil, true, true) ~= kTechId.None + elseif self.leashedPosition and not self.autoReturning then + self.autoReturning = true + self:GiveOrder(kTechId.Move, nil, self.leashedPosition, nil, true, true) + --DebugPrint("returning to "..ToString(self.leashedPosition))

    end
    
    return false

@@ -1016,6 +1027,14 @@ function MAC:OnUpdate(deltaTime)

end

+function MAC:OnOrderComplete(order) + if self.autoReturning then + self.leashedPosition = nil + self.autoReturning = false + --DebugPrint("Arrived") + end +end +

function MAC:PerformActivation(techId, position, normal, commander)

    if techId == kTechId.MACEMP then

diff --git a/ns2/lua/MainMenu.lua b/ns2/lua/MainMenu.lua index 9dc0909c6..ed74532a8 100644 --- a/ns2/lua/MainMenu.lua +++ b/ns2/lua/MainMenu.lua @@ -135,15 +135,23 @@ end

function MainMenu_GetSelectedIsFull()
    
    if gSelectedServerNum and gSelectedServerData then

- local numReservedSlots = GetNumServerReservedSlots(gSelectedServerData.serverId) + local numReservedSlots = GetNumServerReservedSlots(gSelectedServerNum)

        return Client.GetServerNumPlayers(gSelectedServerNum) >= Client.GetServerMaxPlayers(gSelectedServerNum) - numReservedSlots
    end
    
end

+function MainMenu_GetSelectedHasSpectatorSlots() + + if gSelectedServerNum and gSelectedServerData then + return Client.GetServerNumSpectators(gSelectedServerNum) < Client.GetServerMaxSpectators(gSelectedServerNum) + end + +end +

function MainMenu_GetSelectedIsFullWithNoRS()

- - if gSelectedServerNum and gSelectedServerNum >0 then + + if gSelectedServerNum and gSelectedServerData then

        return Client.GetServerNumPlayers(gSelectedServerNum) >= Client.GetServerMaxPlayers(gSelectedServerNum)
    end
    

@@ -271,6 +279,17 @@ function MainMenu_GetAlertMessage()

end

+function MainMenu_Preload() + + if not gMainMenu then + gMainMenu = GetGUIManager():CreateGUIScript("menu/GUIMainMenu") + gMainMenu:SetIsVisible(true) + gMainMenu:Update(0) + LeaveMenu() + end + +end +

function MainMenu_Open()

    -- Don't load or open main menu while debugging (too slow).

diff --git a/ns2/lua/Matchmaking.lua b/ns2/lua/Matchmaking.lua index a2cd8663c..0ebb2a46f 100644 --- a/ns2/lua/Matchmaking.lua +++ b/ns2/lua/Matchmaking.lua @@ -71,9 +71,10 @@ function Matchmaking_GetNumInGlobalLobby()

    return gShowMatchmakingCount and gActiveLobby and Client.GetNumLobbyMembers( gActiveLobby )
end
    

-Event.Hook("LobbyListReady", Matchmaking_JoinGlobalLobby_RebuildListResponse ) -Event.Hook("LobbyCreated", Matchmaking_JoinGlobalLobby_JoinedResponse ) -Event.Hook("LobbyEntered", Matchmaking_JoinGlobalLobby_JoinedResponse ) +Event.Hook("OnLobbyListResults", Matchmaking_JoinGlobalLobby_RebuildListResponse ) +Event.Hook("OnLobbyCreated", Matchmaking_JoinGlobalLobby_JoinedResponse ) +Event.Hook("OnLobbyMessage", function() end ) +Event.Hook("OnLobbyClientEnter", Matchmaking_JoinGlobalLobby_JoinedResponse )

Event.Hook("ClientDisconnected", Matchmaking_LeaveGlobalLobby )

--[[

diff --git a/ns2/lua/NS2ConsoleCommands_Server.lua b/ns2/lua/NS2ConsoleCommands_Server.lua index 3b31fd832..a350bae3a 100644 --- a/ns2/lua/NS2ConsoleCommands_Server.lua +++ b/ns2/lua/NS2ConsoleCommands_Server.lua @@ -15,17 +15,9 @@ local gLastPosition = nil

local function JoinTeam(player, teamIndex)

- if player ~= nil and player:GetTeamNumber() == kTeamReadyRoom then - - -- Auto team balance checks. - local allowed, reason = GetGamerules():GetCanJoinTeamNumber(player, teamIndex) - - if allowed or Shared.GetCheatsEnabled() then - return GetGamerules():JoinTeam(player, teamIndex) - else - Server.SendNetworkMessage(player, "JoinError", BuildJoinErrorMessage(reason), true) - return false - end + if player and player:GetTeamNumber() == kTeamReadyRoom then + + return GetGamerules():JoinTeam(player, teamIndex)

    end
    

@@ -48,8 +40,7 @@ end

local function ReadyRoom(player)

    if player and not player:isa("ReadyRoomPlayer") then

- - player:SetCameraDistance(0) +

        return GetGamerules():JoinTeam(player, kTeamReadyRoom)
        
    end

diff --git a/ns2/lua/NS2Gamerules.lua b/ns2/lua/NS2Gamerules.lua index e4e9cd4f7..38d0dd9ad 100644 --- a/ns2/lua/NS2Gamerules.lua +++ b/ns2/lua/NS2Gamerules.lua @@ -1046,16 +1046,7 @@ if Server then

    end

    function NS2Gamerules:GetReservedSlots()

- local tags = { } - Server.GetTags(tags) - - for t = 1, #tags do - if string.find(tags[t], "R_S") then - return tonumber(string.sub(tags[t], 4)) - end - end - - return 0 + return Server.GetReservedSlotLimit()

    end

    function NS2Gamerules:SetRookieMode(state)

@@ -1088,8 +1079,10 @@ if Server then

        local kTime = Shared.GetTime()
        if not self.nextTimeUpdateNumPlayersForScoreboard or self.nextTimeUpdateNumPlayersForScoreboard < kTime then

- +

            local numPlayersTotal = Server.GetNumPlayersTotal and Server.GetNumPlayersTotal() or 0

+ local numSpectator = Server.GetNumSpectators and Server.GetNumSpectators() or 0 + numPlayersTotal = numPlayersTotal + numSpectator

            self.gameInfo:SetNumBots( #gServerBots )
            self.gameInfo:SetNumPlayers( Server.GetNumPlayers() )

@@ -1099,44 +1092,6 @@ if Server then

        end

    end

- - local kTickCount = 0 - local kTickTimeSum = 0 - function NS2Gamerules:UpdatePerfTags(timePassed) - - UpdateTag("tickrate_", math.floor(Server.GetFrameRate())) - - kTickCount = kTickCount + 1 - kTickTimeSum = kTickTimeSum + timePassed - - local kTime = Shared.GetTime() - if not self.nextServerTickrateUpdate or self.nextServerTickrateUpdate < kTime then - - --kNextServerTickrateUpdate = Shared.GetTime() + 10 - kTickCount = kTickCount / kTickTimeSum - kTickTimeSum = 1 - --round it - local avgTickrate = string.format("%.0f", kTickCount) - UpdateTag("ServerTickrate", avgTickrate) - - self.nextServerTickrateUpdate = kTime + 10 - end - - end - - function NS2Gamerules:UpdateCustomNetworkSettings() - - local kTime = Shared.GetTime() - if not self.nextTimeUpdateCustomNetworkSettings or self.nextTimeCustomNetworkSettings < kTime then - if Server.GetSendrate() ~= 20 or Server.GetTickrate() ~= 30 or Shared.GetSettingsVariable( "mr" ) ~= "26.000000" or Shared.GetSettingsVariable("interp") ~= "0.100000" then - UpdateTag("custom_network_settings", "") - else - RemoveTag("custom_network_settings") - end - self.nextTimeUpdateNetworkSettingsModded = kTime + 10 - end - - end

    function NS2Gamerules:UpdateConcedeSequence()
        local seq = GetConcedeSequence()

@@ -1194,8 +1149,6 @@ if Server then

                self:UpdatePlayerSkill()
                self:UpdateNumPlayersForScoreboard()

- self:UpdatePerfTags(timePassed) - self:UpdateCustomNetworkSettings()

            end

@@ -1353,24 +1306,34 @@ if Server then

    -- No enforced balanced teams on join as the auto team balance system balances teams.
    function NS2Gamerules:GetCanJoinTeamNumber(player, teamNumber)

+ local team1Players = self.team1:GetNumPlayers() + local team2Players = self.team2:GetNumPlayers() + + local team1Number = self.team1:GetTeamNumber() + local team2Number = self.team2:GetTeamNumber() + + -- Every check below is disabled with cheats enabled + if not Shared.GetCheatsEnabled() then + return true + end +

        local forceEvenTeams = Server.GetConfigSetting("force_even_teams_on_join")
        if forceEvenTeams then

- - local team1Players = self.team1:GetNumPlayers() - local team2Players = self.team2:GetNumPlayers()

- if (team1Players > team2Players) and (teamNumber == self.team1:GetTeamNumber()) then - return false, 0 - elseif (team2Players > team1Players) and (teamNumber == self.team2:GetTeamNumber()) then - return false, 0 + if (team1Players > team2Players) and (teamNumber == team1Number) then + Server.SendNetworkMessage(player, "JoinError", BuildJoinErrorMessage(0), true) + return false + elseif (team2Players > team1Players) and (teamNumber == team2Number) then + Server.SendNetworkMessage(player, "JoinError", BuildJoinErrorMessage(0), true) + return false

            end

        end

- if not Shared.GetCheatsEnabled() and Server.IsDedicated() and - not self.botTraining and player:GetPlayerLevel() ~= -1 then + if Server.IsDedicated() and not self.botTraining and player:GetPlayerLevel() ~= -1 then

            if self.gameInfo:GetRookieMode() and player:GetPlayerLevel() >= kRookieAllowedLevel then

- return false, 2 + Server.SendNetworkMessage(player, "JoinError", BuildJoinErrorMessage(2), true) + return false

            end
        end
        

@@ -1406,6 +1369,14 @@ if Server then

        local client = Server.GetOwner(player)
        if not client then return end

+ + if newTeamNumber ~= kSpectatorIndex and not self:GetCanJoinPlayingTeam(player) then + return false + end + + if not force and not self:GetCanJoinTeamNumber(player, newTeamNumber) then + return false + end

        local success = false
        local newPlayer

@@ -1503,12 +1474,23 @@ if Server then

                elseif oldTeamNumber == kTeam1Index or oldTeamNumber == kTeam2Index then
                    self.playerRanking:SetExitTime( newPlayer, oldTeamNumber ) --Hive2 added team param
                end

- - Server.SendNetworkMessage(newPlayerClient, "SetClientTeamNumber", { teamNumber = newPlayer:GetTeamNumber() }, true) - +

                if newTeamNumber == kSpectatorIndex then
                    newPlayer:SetSpectatorMode(kSpectatorMode.Overhead)

+ + -- add player to list of spectators if there are spectator lots available + local numSpecs = Server.GetNumSpectators() + if numSpecs < Server.GetMaxSpectators() and newPlayer:GetIsSpectator() == false then + newPlayer:SetIsSpectator(true) + end + else + --remove player from spectator list + if newPlayer:GetIsSpectator() then + newPlayer:SetIsSpectator(false) + end

                end

+ + Server.SendNetworkMessage(newPlayerClient, "SetClientTeamNumber", { teamNumber = newPlayer:GetTeamNumber() }, true)

                self.botTeamController:UpdateBots()
            end

diff --git a/ns2/lua/NetworkMessages.lua b/ns2/lua/NetworkMessages.lua index 8560d128f..30fb2295a 100644 --- a/ns2/lua/NetworkMessages.lua +++ b/ns2/lua/NetworkMessages.lua @@ -300,7 +300,7 @@ Shared.RegisterNetworkMessage( "AbilityResult", kAbilityResultMessage )

-- Tell players WHY they can't join a team
local kJoinErrorMessage =
{

- reason = "integer (0 to 2)" + reason = "integer (0 to 3)"

}
function BuildJoinErrorMessage( reason )
    return { reason = reason }

diff --git a/ns2/lua/NetworkMessages_Client.lua b/ns2/lua/NetworkMessages_Client.lua index 9711b2c72..fa0f7955e 100644 --- a/ns2/lua/NetworkMessages_Client.lua +++ b/ns2/lua/NetworkMessages_Client.lua @@ -198,6 +198,8 @@ function OnCommandJoinError(message)

        MainMenu_Open()
        GetGUIMainMenu():CreateRookieOnlyNagWindow()

+ elseif message.reason == 3 then + ChatUI_AddSystemMessage( Locale.ResolveString("JOIN_ERROR_NO_PLAYER_SLOT_LEFT") )

    end
end

diff --git a/ns2/lua/Order.lua b/ns2/lua/Order.lua index bc4c2bdb3..3cf4ba28d 100644 --- a/ns2/lua/Order.lua +++ b/ns2/lua/Order.lua @@ -74,14 +74,24 @@ function Order:OnEntityChange(oldId, newId)

    -- The orderParam represents a kTechId when the order type is
    -- kTechId.Build so ignore the entity change event in this case.
    if self.orderType ~= kTechId.Build and self.orderParam == oldId then

- +

        if newId == nil then
            newId = Entity.invalidId

+ else + local newTarget = Shared.GetEntity(newId) + local oldTarget = newTarget and Shared.GetEntity(oldId) + local oldTeam = newTarget and newTarget.GetTeamNumber and newTarget:GetTeamNumber() + local newTeam = oldTarget and oldTarget.GetTeamNumber and oldTarget:GetTeamNumber() + + if oldTeam ~= newTeam then + newId = Entity.invalidId + end +

        end

+

        self.orderParam = newId
        
    end

-

end

function Order:tostring()

diff --git a/ns2/lua/Player_Server.lua b/ns2/lua/Player_Server.lua index aeafecfa7..3792ea810 100644 --- a/ns2/lua/Player_Server.lua +++ b/ns2/lua/Player_Server.lua @@ -22,6 +22,26 @@ function Player:GetClient()

    return self.client
end

+function Player:GetIsSpectator() + return self.client and self.client:GetIsSpectator() +end + +function Player:SetIsSpectator(isSpec) + if type(isSpec) ~= "boolean" or not self.client then return end --sanity check of inputs + + if self:GetIsVirtual() then return end --ignore bots + + if not self.client:SetIsSpectator(isSpec) then return end + + --Move spectating player to the spectator team + if not isSpec then return end + + local gamerules = GetGamerules() + if gamerules and self:GetTeamNumber() ~= kSpectatorIndex then + gamerules:JoinTeam(self, kSpectatorIndex, true) + end +end +

function Player:GetIsVirtual()
    return self.isVirtual
end

diff --git a/ns2/lua/RepositioningMixin.lua b/ns2/lua/RepositioningMixin.lua index 52ca1ff73..c7652bd16 100644 --- a/ns2/lua/RepositioningMixin.lua +++ b/ns2/lua/RepositioningMixin.lua @@ -23,10 +23,12 @@ local kToleranzDistance = 0.15

local kRepositioningSpeed = 3
local kRepositioningTime = 0.7
local kPositionCheckIntervall = 0.8

-local kDefaultExtents = Vector(0.25, 0.25, 0.25) +local kDefaultExtents = Vector(0.50, 0.50, 0.50)

local kGroupOrderCompleteRange = 6

+local random = math.random +

RepositioningMixin.expectedCallbacks =
{
    GetCanReposition = "Should be true if repositioning is allowed",

@@ -65,8 +67,8 @@ function RepositioningMixin:GetRepositioningSpeed()

    if self.OverrideRepositioningSpeed then
        return self:OverrideRepositioningSpeed()
    end

- - return kRepositioningSpeed + + return kRepositioningSpeed

end

@@ -75,8 +77,8 @@ function RepositioningMixin:GetRepositioningDistance()

    if self.OverrideRepositioningDistance then
        return self:OverrideRepositioningDistance()
    end

- - return kRepositiongDistance + + return kRepositiongDistance

end

@@ -96,7 +98,7 @@ function RepositioningMixin:_AdjustRepositioningHeight()

    local trace = Shared.TraceRay(startPointGround, endPointGround, CollisionRep.Move, PhysicsMask.AllButPCs, EntityFilterMixinAndSelf(self, "Repositioning"))
    
    if trace.entity == nil or not trace.entity:isa("TechPoint") then

- +

        local traceGround = Shared.TraceRay(startPointGround, endPointGround, CollisionRep.Move, PhysicsMask.AllButPCs, EntityFilterMixinAndSelf(self, "Repositioning"))
        
        if endPointGround ~= traceGround.endPoint then

@@ -129,9 +131,9 @@ function RepositioningMixin:ToggleRepositioning()

    local baseYaw = 0

    for i, entity in ipairs(entitiesInRange) do

- - if entity:GetCanReposition() and entity ~= self then

+ if entity:GetCanReposition() and entity ~= self then +

            entity.isRepositioning = true
            entity.timeLeftForReposition = self:GetRepositioningTime()
            

@@ -142,7 +144,7 @@ function RepositioningMixin:ToggleRepositioning()

            end
            
        end

- +

    end
    
    return true

@@ -163,18 +165,20 @@ function RepositioningMixin:FindBetterPosition(yaw, baseYaw, calls)

    local coords = angles:GetCoords(self:GetOrigin())
    local startPoint = self:GetOrigin()

- local endPoint = startPoint + coords.zAxis * 10 + local randRads = random() * math.pi * 2 + local direction = Vector(math.cos(randRads), 0, math.sin(randRads)) + local endPoint = startPoint + direction * 5

    local trace = Shared.TraceRay(startPoint, endPoint, CollisionRep.Move, PhysicsMask.AllButPCs, EntityFilterMixinAndSelf(self, "Repositioning"))
    
    local validPos = false
    
    if trace.fraction ~= 1 then

- endPoint = trace.endPoint + coords.zAxis * -0.5 + endPoint = trace.endPoint + direction * -0.5

    end
    
    if (endPoint - startPoint):GetLength() >= self:GetRepositioningDistance() then

- endPoint = startPoint + coords.zAxis * self:GetRepositioningDistance() + endPoint = startPoint + direction * self:GetRepositioningDistance()

        validPos = true
    end
    

@@ -195,16 +199,16 @@ end

function RepositioningMixin:PerformRepositioning(deltaTime)

    if self.targetPos ~= nil then

- +

        local direction = self.targetPos - self:GetOrigin()
        self.timeLeftForReposition = Clamp(self.timeLeftForReposition - deltaTime, 0, self:GetRepositioningTime())
        
        if direction:GetLength() < kToleranzDistance then

- +

            if HasMixin(self, "Pathing") then
                self:SetCurrentPositionValid(self.targetPos)
            end

- +

            self.isRepositioning = false
            self.targetPos = nil
            self:_AdjustRepositioningHeight()

@@ -215,7 +219,7 @@ function RepositioningMixin:PerformRepositioning(deltaTime)

            end
            
            return

- +

        end
        
        -- in the case we are out of time we simply failed, it could be too risky to trigger repositioning

@@ -230,7 +234,7 @@ function RepositioningMixin:PerformRepositioning(deltaTime)

            self:_AdjustRepositioningHeight()
            
            return

- +

        end
        
        direction:Normalize()

@@ -239,7 +243,7 @@ function RepositioningMixin:PerformRepositioning(deltaTime)

        if Pathing.GetIsFlagSet(newOrigin, kDefaultExtents, Pathing.PolyFlag_Walk) then        
            self:SetOrigin(newOrigin)
        end

- +

    else
        self.isRepositioning = false
    end

@@ -249,10 +253,10 @@ end

function RepositioningMixin:_GetShouldCheckPosition()

    if (self.timeLastPositionCheck == nil) or (self.timeLastPositionCheck + kPositionCheckIntervall < Shared.GetTime()) then

- +

        self.timeLastPositionCheck = Shared.GetTime()
        return true

- +

    end
    
    return not self.initialSpaceChecked

@@ -266,7 +270,7 @@ end

function RepositioningMixin:OnUpdate(deltaTime)
    PROFILE("RepositioningMixin:OnUpdate")
    if self.isRepositioning then

- +

        self:PerformRepositioning(deltaTime)
        
    elseif HasMixin(self, "Orders") and (self:GetHasOrder() or not self.initialSpaceChecked) then

@@ -274,7 +278,7 @@ function RepositioningMixin:OnUpdate(deltaTime)

        --self:_AdjustRepositioningHeight()
        
        if self:_GetShouldCheckPosition() then

- +

            if self.GetShouldRepositionDuringMove and not self:GetShouldRepositionDuringMove() then
                return
            end

@@ -291,29 +295,29 @@ function RepositioningMixin:OnOrderComplete(currentOrder)

    if HasMixin(self, "Orders") then
        --[[ disabled, here is not the right place for group orders

- if(currentOrder:GetType() == kTechId.Move) then + if(currentOrder:GetType() == kTechId.Move) then

            local entitiesInRange = GetEntitiesWithMixinForTeamWithinRange("Repositioning", self:GetTeamNumber(), self:GetOrigin(), kGroupOrderCompleteRange)
            
            for index, entity in ipairs(entitiesInRange) do
            

- if HasMixin(entity, "Orders") and entity ~= self then - - local entityOrder = entity:GetCurrentOrder() - if entityOrder and entityOrder:GetType() == kTechId.Move and currentOrder:GetLocation() == entityOrder:GetLocation() then - entity:ClearOrders() - end - - end + if HasMixin(entity, "Orders") and entity ~= self then + + local entityOrder = entity:GetCurrentOrder() + if entityOrder and entityOrder:GetType() == kTechId.Move and currentOrder:GetLocation() == entityOrder:GetLocation() then + entity:ClearOrders() + end + + end + + end

            end

- - end

        --]]

- +

        if currentOrder then
            self:ToggleRepositioning()
        end
    end

-end \ No newline at end of file +end diff --git a/ns2/lua/SabotCoreServer.lua b/ns2/lua/SabotCoreServer.lua index dfeb4d6e5..5c5df4392 100644 --- a/ns2/lua/SabotCoreServer.lua +++ b/ns2/lua/SabotCoreServer.lua @@ -63,4 +63,4 @@ local function UpdateGatherServer()

end

-Event.Hook("UpdateServer", UpdateGatherServer) \ No newline at end of file +-- Event.Hook("UpdateServer", UpdateGatherServer) \ No newline at end of file diff --git a/ns2/lua/ServerAdminCommands.lua b/ns2/lua/ServerAdminCommands.lua index a09d7e7cf..5b4e0ed9b 100644 --- a/ns2/lua/ServerAdminCommands.lua +++ b/ns2/lua/ServerAdminCommands.lua @@ -311,6 +311,14 @@ end

    Also we check if a player is banned here.
]]

+local spectorIds = {} + +local reservedSlotIds = {} + +function GetHasReservedSlotAccess(userId) + return reservedSlotIds[userId] +end +

local function OnCheckConnectionAllowed(userId)

    --check if the user is banned

@@ -342,35 +350,67 @@ local function OnCheckConnectionAllowed(userId)

    --check the reserved slots
    local reservedSlots = Server.GetReservedSlotsConfig()

- if not reservedSlots or not reservedSlots.amount or not reservedSlots.ids then - return true - end + if reservedSlots and reservedSlots.ids then + reservedSlotIds[userId] = nil

- local newPlayerIsReserved = false - for name, id in pairs(reservedSlots.ids) do + for _, id in pairs(reservedSlots.ids) do

- if id == userId then + if id == userId then

- newPlayerIsReserved = true - break + reservedSlotIds[userId] = true + break

- end + end

+ end

    end

- local numPlayers = Server.GetNumPlayersTotal() - 1 --NumTotal includes currently check player + local numPlayers = Server.GetNumPlayersTotal() - 1 -- NumTotal includes currently check player + local numSpectator = Server.GetNumSpectators()

    local maxPlayers = Server.GetMaxPlayers()

+ local maxSpectators = Server.GetMaxSpectators() + local numRes = Server.GetReservedSlotLimit() + + --check for free player slots + if numPlayers < (maxPlayers - numRes) then + return true + end + + --check for free reserved slots + if GetHasReservedSlotAccess(userId) and numPlayers < maxPlayers then + return true + end

- if (numPlayers < (maxPlayers - reservedSlots.amount)) or (newPlayerIsReserved and (numPlayers < maxPlayers)) then + spectorIds[userId] = nil + + --check for free spectator slots + if numSpectator < maxSpectators then + spectorIds[userId] = true

        return true
    end

    Shared.Message("Rejected connection to client %s as there are no free slots avaible.", userId)

- return false, "Server full" + return false, "Server is currently full."

end
Event.Hook("CheckConnectionAllowed", OnCheckConnectionAllowed)

+ +local function OnConnect(client) + local clientId = client:GetUserId() + + if not client:GetIsSpectator() and not spectorIds[clientId] then + return + end + + local player = client:GetControllingPlayer() + if player then + player:SetIsSpectator(true) + else + client:SetIsSpectator(true) + end +end +Event.Hook("ClientConnect", OnConnect)

--[[
 * Duration is specified in minutes. Pass in 0 or nil to ban forever.
 * A reason string may optionally be provided.

@@ -584,20 +624,8 @@ function SetReservedSlotAmount(client, amount)

    if reservedSlots and amount and amount >= 0 and amount <= Server.GetMaxPlayers() then
    
        reservedSlots.amount = amount

- - -- We are using tags for the reserved slots. - -- First clear out the old tag. - local tags = { } - Server.GetTags(tags) - for t = 1, #tags do - - if string.find(tags[t], "R_S") then - Server.RemoveTag(tags[t]) - end - - end - - Server.AddTag("R_S" .. reservedSlots.amount) + + Server.SetReservedSlotLimit(reservedSlots.amount)

        Shared.Message("Reserved slot amount set to " .. reservedSlots.amount)
        Server.SaveReservedSlotsConfig()

diff --git a/ns2/lua/ServerBrowser.lua b/ns2/lua/ServerBrowser.lua index e5c30b788..20d4fa95a 100644 --- a/ns2/lua/ServerBrowser.lua +++ b/ns2/lua/ServerBrowser.lua @@ -71,7 +71,7 @@ local function GetServerTagValue(serverIndex, tagName, asString)

end

function GetNumServerReservedSlots(serverIndex)

- return math.abs(GetServerTagValue(serverIndex, "R_S") or 0) + return Client.GetServerNumReservedSlots(serverIndex)

end

function GetServerPlayerSkill(serverIndex)

@@ -153,6 +153,15 @@ local kServerRankingFunctions =

	CalculateSeverRankingForArcade --Arcade
}

+local function GetServerHasCustomNetVars(serverIndex) + local tickrate = Client.GetServerPerformanceTickrate(serverIndex) + local sendrate = Client.GetServerPerformanceSendrate(serverIndex) + local moverate = Client.GetServerPerformanceMoverate(serverIndex) + local interp = Client.GetServerPerformanceInterpMs(serverIndex) + + return tickrate ~= 30 or sendrate ~= 20 or moverate ~= 26 or interp ~= 100 +end +

function BuildServerEntry(serverIndex)

    -- local mods = Client.GetServerKeyValue(serverIndex, "mods")

@@ -163,14 +172,16 @@ function BuildServerEntry(serverIndex)

    serverEntry.mode = FormatGameMode(Client.GetServerGameMode(serverIndex), serverEntry.maxPlayers)
    serverEntry.map = GetTrimmedMapName(Client.GetServerMapName(serverIndex))
    serverEntry.numPlayers = Client.GetServerNumPlayers(serverIndex)

+ serverEntry.numSpectators = Client.GetServerNumSpectators(serverIndex) + serverEntry.maxSpectators = Client.GetServerMaxSpectators(serverIndex)

    serverEntry.numRS = GetNumServerReservedSlots(serverIndex)
    serverEntry.ping = Client.GetServerPing(serverIndex)
    serverEntry.address = GetServerAddress(serverIndex)
    serverEntry.requiresPassword = Client.GetServerRequiresPassword(serverIndex)
    serverEntry.playerSkill = GetServerPlayerSkill(serverIndex)
    serverEntry.rookieOnly = Client.GetServerHasTag(serverIndex, "rookie_only")

- serverEntry.ignorePlayNow = Client.GetServerHasTag(serverIndex, "ignore_playnow") - serverEntry.gatherServer = Client.GetServerHasTag(serverIndex, "gather_server") + serverEntry.ignorePlayNow = Client.GetServerIsQuickPlayReady(serverIndex) + -- serverEntry.gatherServer = Client.GetServerHasTag(serverIndex, "gather_server")

    serverEntry.friendsOnServer = Client.GetServerContainsFriends(serverIndex)
    serverEntry.lanServer = false
    serverEntry.tickrate = GetServerTickRate(serverIndex)

@@ -182,7 +193,7 @@ function BuildServerEntry(serverIndex)

    serverEntry.ranked = GetServerIsRanked(serverEntry.address)
    serverEntry.favorite = GetServerIsFavorite(serverEntry.address)
    serverEntry.history, serverEntry.lastConnect = GetServerIsHistory(serverEntry.address)

- serverEntry.customNetworkSettings = Client.GetServerHasTag(serverIndex, "custom_network_settings") + serverEntry.customNetworkSettings = GetServerHasCustomNetVars(serverIndex)

    serverEntry.name = FormatServerName(serverEntry.name, serverEntry.rookieOnly)

diff --git a/ns2/lua/ServerSponitor.lua b/ns2/lua/ServerSponitor.lua index 4c630c985..103379125 100644 --- a/ns2/lua/ServerSponitor.lua +++ b/ns2/lua/ServerSponitor.lua @@ -21,7 +21,7 @@ local kPerfCheckPeriod = 1.0 -- Every 1 second, we MIGHT report performance i

local function CollectActiveModIds()

- modIds = {} + local modIds = {}

    for modNum = 1, Server.GetNumActiveMods() do
        modIds[modNum] = Server.GetActiveModId(modNum)
    end

@@ -68,15 +68,15 @@ local function SendSponitorRequest(url, requestType, data, callback)

    -- Don't send any data when this server has bots connected.
    -- We don't want to track any bot data.
    if not GetServerContainsBots() then

- +

        if callback then
            Shared.SendHTTPRequest(url, requestType, data, callback)
        else
            Shared.SendHTTPRequest(url, requestType, data)
        end

- +

    end

- +

end

local function SendHiveRequest(url, requestType, data, callback)

@@ -89,9 +89,9 @@ local function SendHiveRequest(url, requestType, data, callback)

        else
            Shared.SendHTTPRequest(url, requestType, data)
        end

- +

    end

- +

end

------------------------------------------

@@ -138,35 +138,35 @@ end

function ServerSponitor:ListenToTeam(team)

    team:AddListener("OnResearchComplete",

- function(structure, researchId) + function(structure, researchId)

- local node = team:GetTechTree():GetTechNode(researchId) + local node = team:GetTechTree():GetTechNode(researchId)

- if node:GetIsResearch() or node:GetIsUpgrade() then - self:OnTechEvent("DONE "..TechIdToString(researchId)) - end + if node:GetIsResearch() or node:GetIsUpgrade() then + self:OnTechEvent("DONE "..TechIdToString(researchId)) + end

- end ) + end )

    team:AddListener("OnCommanderAction",

- function(techId) - self:OnTechEvent("CMDR "..TechIdToString(techId)) - end ) + function(techId) + self:OnTechEvent("CMDR "..TechIdToString(techId)) + end )

    team:AddListener("OnConstructionComplete",

- function(structure) - self:OnTechEvent("BUILT "..TechIdToString(structure:GetTechId())) - end ) + function(structure) + self:OnTechEvent("BUILT "..TechIdToString(structure:GetTechId())) + end )

    team:AddListener("OnEvolved",

- function(techId) - self:OnTechEvent("EVOL "..TechIdToString(techId)) - end ) - + function(techId) + self:OnTechEvent("EVOL "..TechIdToString(techId)) + end ) +

    team:AddListener("OnBought",

- function(techId) - self:OnTechEvent("BUY "..TechIdToString(techId)) - end ) + function(techId) + self:OnTechEvent("BUY "..TechIdToString(techId)) + end )

    self.teamStats[team:GetTeamType()] = {}
    ResetTeamStats( self.teamStats[ team:GetTeamType() ], team )

@@ -179,17 +179,17 @@ end

function ServerSponitor:OnMatchStartResponse(response)

    local data, pos, err = json.decode(response)

- +

    if err then
        Shared.Message("Could not parse match start response. Error: " .. ToString(err) .. ". Response: " .. response)
    else

- +

        if IsNumber(data.matchId) then
            self.matchId = data.matchId
        else
            self.matchId = nil
        end

- +

        if IsBoolean(data.reportDetails) then
            self.reportDetails = data.reportDetails
        else

@@ -208,41 +208,41 @@ end

function ServerSponitor:OnServerPerfResponse(response)

    local data, pos, err = json.decode(response)

- +

    if err then
        Shared.Message("Could not parse server perf response. Error: " .. ToString(err) .. ". Response: " .. response)
    else

- +

        -- We don't necessarily expect this, so don't reset to default if it was not provided.
        if IsNumber(data.serverPerfThrottle) then
            self.serverPerfThrottle = data.serverPerfThrottle
        end

- +

    end

- +

end

function ServerSponitor:OnStartMatch()

    local jsonData = json.encode(

- { - startTime = Shared.GetGMTString(false), - version = Shared.GetBuildNumber(), - map = Shared.GetMapName(), - serverIp = Server.GetIpAddress(), - isRookieServer = Server.GetIsRookieFriendly(), - modIds = CollectActiveModIds(), - tournamentMode = self.tournamentMode, - averageSkill = GetGameInfoEntity():GetAveragePlayerSkill() - }) - + { + startTime = Shared.GetGMTString(false), + version = Shared.GetBuildNumber(), + map = Shared.GetMapName(), + serverIp = Server.GetIpAddress(), + isRookieServer = Server.GetIsRookieFriendly(), + modIds = CollectActiveModIds(), + tournamentMode = self.tournamentMode, + averageSkill = GetGameInfoEntity():GetAveragePlayerSkill() + }) +

    SendSponitorRequest(kSponitor2Url.."matchStart", "POST", { data = jsonData },
        function(response) self:OnMatchStartResponse(response) end)

- +

    -- Reset check timers
    self.sincePlayerCountCheck = kPlayerCountCheckPeriod
    self.sincePerfCheck = kPerfCheckPeriod

- +

end

------------------------------------------

@@ -270,53 +270,54 @@ end

function ServerSponitor:OnEndMatch(winningTeam)

    if self.matchId or gDebugAlwaysPost then

- +

        local startHiveTech = "None"

- +

        if self.game.initialHiveTechId then
            startHiveTech = EnumToString(kTechId, self.game.initialHiveTechId)
        end

- +

        local stats1 = self.teamStats[kMarineTeamType]
        local stats2 = self.teamStats[kAlienTeamType]

- +

        local jsonData = json.encode(

- { - matchId = self.matchId, - endTime = Shared.GetGMTString(false), - winner = winningTeam:GetTeamType(), - start_location1 = self.game.startingLocationNameTeam1, - start_location2 = self.game.startingLocationNameTeam2, - start_path_distance = self.game.startingLocationsPathDistance, - start_hive_tech = startHiveTech, - - pvpKills1 = stats1.pvpKills, - pvpKills2 = stats2.pvpKills, - minPlayers1 = stats1.minNumPlayers, - minPlayers2 = stats2.minNumPlayers, - maxPlayers1 = stats1.maxNumPlayers, - maxPlayers2 = stats2.maxNumPlayers, - avgPlayers1 = stats1.avgNumPlayersSum / stats1.numPlayerCountSamples, - avgPlayers2 = stats2.avgNumPlayersSum / stats2.numPlayerCountSamples, - avgRookies1 = stats1.avgNumRookiesSum / stats1.numPlayerCountSamples, - avgRookies2 = stats2.avgNumRookiesSum / stats2.numPlayerCountSamples, - totalTResMined1 = stats1.team:GetTotalTeamResources(), - totalTResMined2 = stats2.team:GetTotalTeamResources(), - averageSkill = GetGameInfoEntity():GetAveragePlayerSkill() - }) - + { + matchId = self.matchId, + endTime = Shared.GetGMTString(false), + winner = winningTeam:GetTeamType(), + start_location1 = self.game.startingLocationNameTeam1, + start_location2 = self.game.startingLocationNameTeam2, + start_path_distance = self.game.startingLocationsPathDistance, + start_hive_tech = startHiveTech, + + pvpKills1 = stats1.pvpKills, + pvpKills2 = stats2.pvpKills, + minPlayers1 = stats1.minNumPlayers, + minPlayers2 = stats2.minNumPlayers, + maxPlayers1 = stats1.maxNumPlayers, + maxPlayers2 = stats2.maxNumPlayers, + avgPlayers1 = stats1.avgNumPlayersSum / stats1.numPlayerCountSamples, + avgPlayers2 = stats2.avgNumPlayersSum / stats2.numPlayerCountSamples, + avgRookies1 = stats1.avgNumRookiesSum / stats1.numPlayerCountSamples, + avgRookies2 = stats2.avgNumRookiesSum / stats2.numPlayerCountSamples, + totalTResMined1 = stats1.team:GetTotalTeamResources(), + totalTResMined2 = stats2.team:GetTotalTeamResources(), + averageSkill = GetGameInfoEntity():GetAveragePlayerSkill() + }) +

        SendSponitorRequest(kSponitor2Url .. "matchEnd", "POST", { data = jsonData })

- +

        self.matchId = nil

- +

    end

- +

    -- Reset team stats here instead of OnStartMatch. This is because there is data we want to track
    -- before the match actually starts, such as players joining the team.

- for teamType, stats in pairs(self.teamStats) do + for i = 1, 2 do + local stats = self.teamStats[i]

        ResetTeamStats(stats, stats.team)
    end

- +

end

------------------------------------------

@@ -327,13 +328,13 @@ function ServerSponitor:OnEntityKilled(target, attacker, weapon)

    if not attacker or not target or not weapon then
        return
    end

- +

    local targetWeapon = "None"

- +

    if target.GetActiveWeapon and target:GetActiveWeapon() then
        targetWeapon = target:GetActiveWeapon():GetClassName()
    end

- +

    local attackerOrigin = attacker:GetOrigin()
    local targetOrigin = target:GetOrigin()
    local attackerTeamType = ((HasMixin(attacker, "Team") and attacker:GetTeamType()) or kNeutralTeamType)

@@ -343,48 +344,47 @@ function ServerSponitor:OnEntityKilled(target, attacker, weapon)

    if (self.matchId and self.reportDetails) or gDebugAlwaysPost then
        local jsonData, jsonError = json.encode(

- { - matchId = self.matchId, - time = Shared.GetGMTString(false), - attackerClass = attacker:GetClassName(), - attackerTeam = attackerTeamType, - attackerWeapon = weapon:GetClassName(), - attackerX = string.format("%.2f", attackerOrigin.x), - attackerY = string.format("%.2f", attackerOrigin.y), - attackerZ = string.format("%.2f", attackerOrigin.z), - attackerAttrs = GetUpgradeAttribsString(attacker), - targetClass = target:GetClassName(), - targetTeam = target:GetTeamType(), - targetWeapon = targetWeapon, - targetX = string.format("%.2f", targetOrigin.x), - targetY = string.format("%.2f", targetOrigin.y), - targetZ = string.format("%.2f", targetOrigin.z), - targetAttrs = GetUpgradeAttribsString(target), - targetLifeTime = string.format("%.2f", ((target.GetCreationTime and Shared.GetTime() - target:GetCreationTime()) or 0)), - attackerShoulderPad = attackerPads, - targetShoulderPad = targetPads, - mapName = Shared.GetMapName() - }) - + { + matchId = self.matchId, + time = Shared.GetGMTString(false), + attackerClass = attacker:GetClassName(), + attackerTeam = attackerTeamType, + attackerWeapon = weapon:GetClassName(), + attackerX = string.format("%.2f", attackerOrigin.x), + attackerY = string.format("%.2f", attackerOrigin.y), + attackerZ = string.format("%.2f", attackerOrigin.z), + attackerAttrs = GetUpgradeAttribsString(attacker), + targetClass = target:GetClassName(), + targetTeam = target:GetTeamType(), + targetWeapon = targetWeapon, + targetX = string.format("%.2f", targetOrigin.x), + targetY = string.format("%.2f", targetOrigin.y), + targetZ = string.format("%.2f", targetOrigin.z), + targetAttrs = GetUpgradeAttribsString(target), + targetLifeTime = string.format("%.2f", ((target.GetCreationTime and Shared.GetTime() - target:GetCreationTime()) or 0)), + attackerShoulderPad = attackerPads, + targetShoulderPad = targetPads, + mapName = Shared.GetMapName() + }) +

        if jsonData then
            SendSponitorRequest(kSponitor2Url .. "kill", "POST", { data = jsonData })

- --SendHiveRequest(kHiveUrl .. "post/promoKill", "POST", { data = jsonData })

        else

- +

            -- the encoder returned nil, so there was an error. Post it to Spon2.
            jsonData = json.encode(

- { - launchId = -1, - time = Shared.GetGMTString(false), - type = "server killpost", - text = jsonError, - }) + { + launchId = -1, + time = Shared.GetGMTString(false), + type = "server killpost", + text = jsonError, + })

            SendSponitorRequest(kSponitor2Url .. "error", "POST", { data = jsonData })

- +

        end

- +

        if attacker:isa("Player") and target:isa("Player") then

- +

            local tstats = self.teamStats[attackerTeamType]

            if tstats then

@@ -392,49 +392,11 @@ function ServerSponitor:OnEntityKilled(target, attacker, weapon)

            end

        end

- +

    else

- local jsonData, jsonError = json.encode( - { - time = Shared.GetGMTString(false), - attackerClass = attacker:GetClassName(), - attackerTeam = attackerTeamType, - attackerWeapon = weapon:GetClassName(), - attackerX = string.format("%.2f", attackerOrigin.x), - attackerY = string.format("%.2f", attackerOrigin.y), - attackerZ = string.format("%.2f", attackerOrigin.z), - attackerAttrs = GetUpgradeAttribsString(attacker), - targetClass = target:GetClassName(), - targetTeam = target:GetTeamType(), - targetWeapon = targetWeapon, - targetX = string.format("%.2f", targetOrigin.x), - targetY = string.format("%.2f", targetOrigin.y), - targetZ = string.format("%.2f", targetOrigin.z), - targetAttrs = GetUpgradeAttribsString(target), - targetLifeTime = string.format("%.2f", ((target.GetCreationTime and Shared.GetTime() - target:GetCreationTime()) or 0)), - attackerShoulderPad = attackerPads, - targetShoulderPad = targetPads, - mapName = Shared.GetMapName() - })

- if jsonData then - --SendHiveRequest(kHiveUrl .. "post/promoKill", "POST", { data = jsonData }) - else - - -- the encoder returned nil, so there was an error. Post it to Spon2. - jsonData = json.encode( - { - launchId = -1, - time = Shared.GetGMTString(false), - type = "server killpost", - text = jsonError, - }) - --SendHiveRequest(kHiveUrl .. "post/promoKill" .. "error", "POST", { data = jsonData }) - - end -

        if attacker:isa("Player") and target:isa("Player") then

- +

            local tstats = self.teamStats[attackerTeamType]

            if tstats then

@@ -453,18 +415,18 @@ end

function ServerSponitor:OnTechEvent(name)

    if (self.matchId and self.reportDetails) or gDebugAlwaysPost then

- +

        local jsonData = json.encode(

- { - matchId = self.matchId, - time = Shared.GetGMTString(false), - name = name, - }) - + { + matchId = self.matchId, + time = Shared.GetGMTString(false), + name = name, + }) +

        SendSponitorRequest(kSponitor2Url .. "tech", "POST", { data = jsonData })

- +

    end

- +

end

------------------------------------------

@@ -473,39 +435,40 @@ end

local function UpdatePerformanceReporting(self, dt)

    if self.matchId or gDebugAlwaysPost then

- +

        self.sincePerfCheck = self.sincePerfCheck + dt

- +

        if self.sincePerfCheck >= kPerfCheckPeriod then

- +

            self.sincePerfCheck = 0.0

- +

            if math.random() < self.serverPerfThrottle then

- +

                local totalNumPlayers = 0

- - for teamType, stats in pairs(self.teamStats) do + + for i = 1, 2 do + local stats = self.teamStats[i]

                    totalNumPlayers = totalNumPlayers + stats.currNumPlayers
                end

- +

                local jsonData = json.encode(

- { - matchId = self.matchId, - time = Shared.GetGMTString(false), - tickRate = Server.GetFrameRate(), - numEntities = Shared.GetEntitiesWithClassname("Entity"):GetSize(), - numPlayers = totalNumPlayers - }) - + { + matchId = self.matchId, + time = Shared.GetGMTString(false), + tickRate = Server.GetFrameRate(), + numEntities = Shared.GetEntitiesWithClassname("Entity"):GetSize(), + numPlayers = totalNumPlayers + }) +

                SendSponitorRequest(kSponitor2Url .. "serverPerformance", "POST", { data = jsonData },
                    function(response) self:OnServerPerfResponse(response) end)

- +

            end

- +

        end

- +

    end

- +

end

------------------------------------------

@@ -525,7 +488,8 @@ function ServerSponitor:Update(dt)

            self.sincePlayerCountCheck = 0.0

- for teamType, stats in pairs(self.teamStats) do + for i = 1, 2 do + local stats = self.teamStats[i]

                local numPlayers, numRookies = stats.team:GetNumPlayers()   -- only call this once - it does some computation
                stats.currNumPlayers = numPlayers
                stats.minNumPlayers = math.min( stats.minNumPlayers, numPlayers )

@@ -540,6 +504,4 @@ function ServerSponitor:Update(dt)

    end

-end - - +end \ No newline at end of file diff --git a/ns2/lua/TeamJoin.lua b/ns2/lua/TeamJoin.lua index 6c430cd5a..24941b888 100644 --- a/ns2/lua/TeamJoin.lua +++ b/ns2/lua/TeamJoin.lua @@ -102,7 +102,7 @@ if Server then

        local playerEnts = GetEntities("Player")
        local players = {}
        for _,player in ipairs( playerEnts ) do

- if player:GetClient() then -- don't try to balance ragdolls + if not player:GetIsSpectator() then -- don't try to balance ragdolls or spectators

                players[#players+1] = player
            end
        end

diff --git a/ns2/lua/UnsortedSet.lua b/ns2/lua/UnsortedSet.lua index f90913308..30312d644 100644 --- a/ns2/lua/UnsortedSet.lua +++ b/ns2/lua/UnsortedSet.lua @@ -86,6 +86,12 @@ function US_GetSize(set)

end

+function US_GetElementExists(set, element) + + return set.d[element] ~= nil + +end +

-- Returns the element at the given index in the unsorted set.
function US_GetElement(set, index)
    

diff --git a/ns2/lua/Utility.lua b/ns2/lua/Utility.lua index 2423c17fa..4a9f54a35 100644 --- a/ns2/lua/Utility.lua +++ b/ns2/lua/Utility.lua @@ -189,7 +189,7 @@ function StringSplit(str, delim, maxNb)

    local nb = 0
    local lastPos
    

- for part, pos in string.gfind(str, pat) do + for part, pos in string.gmatch(str, pat) do

        nb = nb + 1
        result[nb] = part

@@ -350,7 +350,7 @@ function WordWrap( label, text, xpos, maxWidth, maxLines )

        i = i + 1
    end

- return TableConcat( lines, "\n" ), maxLines and TableConcat(words, " ", startIndex) + return TableConcat( lines, "\n" ), maxLines and TableConcat(words, " ", startIndex), #lines

end

-- Returns nil if it doesn't hit

@@ -2759,4 +2759,44 @@ end

function IsValid(obj)
    return debug.isvalid(obj)

-end \ No newline at end of file +end + +function HexToColor(hx, alpha) + + if #hx ~= 6 then + error("Not a valid hexadecimal color string!") + return Color(1,1,1,1) + end + + alpha = alpha or 1.0 + + -- Remove 0x or # prefix... yea, not bulletproof but should work well enough. + hx = string.gsub(hx, "0x", "") + hx = string.gsub(hx, "#", "") + + hx = "0x"..hx -- add 0x prefix (ensuring it's only there once.) + + local dec = tonumber(hx) + if not dec then + error("Not a valid hexadecimal color string!") + return Color(1,1,1,1) + end + local red = bit.rshift(dec, 16) / 255 + local green = bit.band(bit.rshift(dec, 8),255) / 255 + local blue = bit.band(dec, 255) / 255 + + return Color(red, green, blue, alpha) + +end + +local kSpaceAdditions = {'Num','Pad','Left','Right','Page','App','Mouse','Button','Wheel','Print', 'Joystick', 'Rotation', 'Pov', 'Slider'} +function GetFriendlyBindingName(name) + + local newName = name + for i=1, #kSpaceAdditions do + newName, _ = string.gsub(newName, kSpaceAdditions[i], kSpaceAdditions[i]..' ') + end + + return newName + +end diff --git a/ns2/lua/Voting.lua b/ns2/lua/Voting.lua index d6ba1bc53..f68c50460 100644 --- a/ns2/lua/Voting.lua +++ b/ns2/lua/Voting.lua @@ -15,7 +15,7 @@ Shared.RegisterNetworkMessage("SendVote", { voteId = "integer", choice = "boolea

kVoteState = enum( { 'InProgress', 'Passed', 'Failed' } )
Shared.RegisterNetworkMessage("VoteResults", { voteId = "integer", yesVotes = "integer (0 to 255)", noVotes = "integer (0 to 255)", requiredVotes = "integer (0 to 255)", state = "enum kVoteState" })
Shared.RegisterNetworkMessage("VoteComplete", { voteId = "integer" })

-kVoteCannotStartReason = enum( { 'VoteAllowedToStart', 'VoteInProgress', 'Waiting', 'Spam', 'DisabledByAdmin', 'GameInProgress', 'TooLate', 'UnsupportedGamemode' } ) +kVoteCannotStartReason = enum( { 'VoteAllowedToStart', 'VoteInProgress', 'Waiting', 'Spam', 'DisabledByAdmin', 'GameInProgress', 'TooEarly', 'TooLate', 'UnsupportedGamemode' } )

Shared.RegisterNetworkMessage("VoteCannotStart", { reason = "enum kVoteCannotStartReason" })

local kVoteCannotStartReasonStrings = { }

@@ -24,6 +24,7 @@ kVoteCannotStartReasonStrings[kVoteCannotStartReason.Waiting] = "VOTE_WAITING"

kVoteCannotStartReasonStrings[kVoteCannotStartReason.Spam] = "VOTE_SPAM"
kVoteCannotStartReasonStrings[kVoteCannotStartReason.GameInProgress] = "VOTE_GAME_IN_PROGRESS"
kVoteCannotStartReasonStrings[kVoteCannotStartReason.DisabledByAdmin] = "VOTE_DISABLED_BY_ADMIN"

+kVoteCannotStartReasonStrings[kVoteCannotStartReason.TooEarly] = "VOTE_TOO_EARLY"

kVoteCannotStartReasonStrings[kVoteCannotStartReason.TooLate] = "VOTE_TOO_LATE"
kVoteCannotStartReasonStrings[kVoteCannotStartReason.UnsupportedGamemode] = "VOTE_GAMEMODE_NOT_SUPPORTED"

@@ -32,9 +33,10 @@ local hookedVoteTypes = {}

if Server then

+ -- Allow reset between Countdown and kMaxTimeBeforeReset

    function VotingResetGameAllowed()
        local gameRules = GetGamerules()

- return not gameRules:GetGameStarted() or Shared.GetTime() - gameRules:GetGameStartTime() < kMaxTimeBeforeReset + return gameRules:GetGameState() == kGameState.Countdown or (gameRules:GetGameStarted() and Shared.GetTime() - gameRules:GetGameStartTime() < kMaxTimeBeforeReset)

    end
    
    local activeVoteName = nil

@@ -83,7 +85,11 @@ if Server then

        if voteName == "VoteResetGame" then
            if not VotingResetGameAllowed() then

- return kVoteCannotStartReason.TooLate + if GetGamerules():GetGameState() < kGameState.Countdown then + return kVoteCannotStartReason.TooEarly + else + return kVoteCannotStartReason.TooLate + end

            end
        end

diff --git a/ns2/lua/VotingChangeMap.lua b/ns2/lua/VotingChangeMap.lua index cce56e107..14503a4b4 100644 --- a/ns2/lua/VotingChangeMap.lua +++ b/ns2/lua/VotingChangeMap.lua @@ -45,7 +45,28 @@ if Client then

end

if Server then

- + + -- finds every map with "ns2_" as a prefix, and, attempts to replace it with "[prefix]_" in order to add + -- more game modes via different map names (for the same level data). The map will only be added to the + -- vote menu if the map (with altered prefix) is included in the server's MapCycle. + local function AddMapPrefixesToVoteMenu(prefix, mapIndex) + + mapIndex = mapIndex or (Server.GetNumMaps() + 1) + + for i=1, Server.GetNumMaps() do + + local mapName = prefix .. "_" .. string.sub( Server.GetMapName(i), string.len( "ns2_" ) + 1 ) + if MapCycle_GetMapIsInCycle(mapName) then + Server.SendNetworkMessage( client, "AddVoteMap", {name = mapName, index = mapIndex}, true) + mapIndex = mapIndex + 1 + end + + end + + return mapIndex + + end +

    -- Send new Clients the map list.
    local function OnClientConnect(client)
    

@@ -58,11 +79,49 @@ if Server then

        end
        

+ -- Add official game mode mods to the player's map list. (For example, infested marines uses vanilla maps, and the + -- game mode is activated by the map previx being "infested" or "infected". + local mapCount = nil + mapCount = AddMapPrefixesToVoteMenu("infest", mapCount) + mapCount = AddMapPrefixesToVoteMenu("infect", mapCount) +

    end
    Event.Hook("ClientConnect", OnClientConnect)
    

+ local function CheckForMapPrefix(prefix, data, mapIndex) + + mapIndex = mapIndex or (Server.GetNumMaps() + 1) + for i=1, Server.GetNumMaps() do + local mapName = prefix .. "_" .. string.sub( Server.GetMapName(i), string.len( "ns2_") + 1) + if MapCycle_GetMapIsInCycle(mapName) then + if mapIndex == data.map_index then + MapCycle_ChangeMap(mapName) + return mapIndex, true + end + mapIndex = mapIndex + 1 + end + end + + return mapIndex, false + + end + + + local checkPrefixes = {"infest", "infect"}

    local function OnChangeMapVoteSuccessful(data)

+ + if data.map_index > Server.GetNumMaps() then + local result = false + local mapIndex = nil + for i=1, #checkPrefixes do + mapIndex, result = CheckForMapPrefix(checkPrefixes[i], data, mapIndex) + if result then + return + end + end + end

        MapCycle_ChangeMap(Server.GetMapName(data.map_index))

+

    end
    SetVoteSuccessfulCallback("VoteChangeMap", kExecuteVoteDelay, OnChangeMapVoteSuccessful)
    

diff --git a/ns2/lua/Weapons/Alien/Ability.lua b/ns2/lua/Weapons/Alien/Ability.lua index c5f84fae7..fb3071244 100644 --- a/ns2/lua/Weapons/Alien/Ability.lua +++ b/ns2/lua/Weapons/Alien/Ability.lua @@ -180,13 +180,16 @@ function Ability:GetEffectParams(tableParams)

end

function Ability:DoAbilityFocusCooldown(player, animationDuration)

- local veilLevel = 0 +

    if player:GetHasUpgrade( kTechId.Focus ) then

- veilLevel = GetVeilLevel( kTeam2Index ) - local cooldown = animationDuration * (1 + kFocusAttackSlowAtMax) + local veilLevel = GetVeilLevel( kTeam2Index ) + local focusCooldown = veilLevel > 0 and kFocusAttackSlowAtMax or 0 + + local cooldown = animationDuration * (1 + focusCooldown)

        -- factor in effects like enzyme and pulse grenade hits
        local attackPeriodFactor = 1.0

+

        -- general attack speed modifications by self
        if player.ModifyAttackSpeed then
            local attackSpeedTable = { attackSpeed = attackPeriodFactor }

diff --git a/ns2/lua/Weapons/Alien/Shockwave.lua b/ns2/lua/Weapons/Alien/Shockwave.lua index 75a56d501..b706c63c2 100644 --- a/ns2/lua/Weapons/Alien/Shockwave.lua +++ b/ns2/lua/Weapons/Alien/Shockwave.lua @@ -16,7 +16,8 @@ class 'Shockwave' (ScriptActor)

Shockwave.kMapName = "Shockwave"
-- Shockwave.kModelName = PrecacheAsset("models/marine/rifle/rifle_grenade.model") -- for debugging

-Shockwave.kRadius = 0.06 +Shockwave.kRadius = 0.025 +Shockwave.tFirst = true

local kShockwaveCrackMaterial = PrecacheAsset("cinematics/vfx_materials/decals/shockwave_crack.material")

local networkVars = { }

@@ -49,7 +50,7 @@ local kRotationCoords =

local function CreateEffect(self)

    local coords = self:GetCoords()

- local groundTrace = Shared.TraceRay(coords.origin, coords.origin - Vector.yAxis * 7, CollisionRep.Move, PhysicsMask.Movement, EntityFilterAllButIsa("Tunnel")) + local groundTrace = Shared.TraceRay(coords.origin, coords.origin - Vector.yAxis * 3, CollisionRep.Move, PhysicsMask.Movement, EntityFilterAllButIsa("Tunnel"))

    if groundTrace.fraction ~= 1 then
    

@@ -63,7 +64,7 @@ local function CreateEffect(self)

        coords.yAxis = coords.zAxis:CrossProduct(coords.xAxis)

- self:TriggerEffects("shockwave_trail", { effecthostcoords = coords }) + --self:TriggerEffects("shockwave_trail", { effecthostcoords = coords })

        Client.CreateTimeLimitedDecal(kShockwaveCrackMaterial, coords * kRotationCoords[math.random(1, #kRotationCoords)], 2.7, 6)
        
    end    

@@ -82,7 +83,7 @@ function Shockwave:OnCreate()

    if Server then    
    
        self:AddTimedCallback(Shockwave.TimeUp, kShockwaveLifeTime)    

- self:AddTimedCallback(Shockwave.Detonate, 0.05) + self:AddTimedCallback(Shockwave.Detonate, 0.1)

        self.damagedEntIds = {}
   
    end

@@ -126,44 +127,47 @@ end

-- called in on processmove server side by stompmixin
function Shockwave:UpdateShockwave(deltaTime)

- - if not self.endPoint then + + local bestEndPoint = nil + local bestFraction = 0

- local bestEndPoint = nil - local bestFraction = 0 + if self.tFirst == true then + self.tFirst = false + self:Detonate() + end

- for i = 1, 11 do + for i = 1, 4 do

- local offset = Vector.yAxis * (i-1) * 0.3 - local trace = Shared.TraceRay(self:GetOrigin() + offset, self:GetOrigin() + self:GetCoords().zAxis * kShockWaveVelocity * kShockwaveLifeTime + offset, CollisionRep.Damage, PhysicsMask.Bullets, EntityFilterAllButIsa("Tunnel")) - - --DebugLine(self:GetOrigin() + offset, trace.endPoint, 2, 1, 1, 1, 1) + local offset = Vector.yAxis * (i-1) * 0.3 + local trace = Shared.TraceRay(self:GetOrigin() + offset, self:GetOrigin() + self:GetCoords().zAxis * 3.5 + offset, CollisionRep.Move, PhysicsMask.Movement, EntityFilterAllButIsa("Tunnel")) + if Shared.GetTestsEnabled() then + DebugLine(self:GetOrigin() + offset, trace.endPoint, 2, 1, 1, 1, 1) + end

- if trace.fraction == 1 then + if trace.fraction == 1 then

- bestEndPoint = trace.endPoint - break + bestEndPoint = trace.endPoint + break

- elseif trace.fraction > bestFraction then + elseif trace.fraction > bestFraction then

- bestEndPoint = trace.endPoint - bestFraction = trace.fraction + bestEndPoint = trace.endPoint + bestFraction = trace.fraction

- end + end

- end - - self.endPoint = bestEndPoint - local origin = self:GetOrigin() - origin.y = self.endPoint.y - self:SetOrigin(origin) + end

- --DebugLine(origin, self.endPoint, 2, 1, 0, 0, 1) + self.endPoint = bestEndPoint + local origin = self:GetOrigin() + origin.y = self.endPoint.y + self:SetOrigin(origin) + + --DebugLine(origin, self.endPoint, 2, 1, 0, 0, 1)

- end

     local newPosition = SlerpVector(self:GetOrigin(), self.endPoint, self:GetCoords().zAxis * kShockWaveVelocity * deltaTime)

- +

     if (newPosition - self.endPoint):GetLength() < 0.1 then
        DestroyShockwave(self)
     else

@@ -179,6 +183,10 @@ function Shockwave:Detonate()

    local groundTrace = Shared.TraceRay(origin, origin - Vector.yAxis * 3, CollisionRep.Move, PhysicsMask.Movement, EntityFilterAllButIsa("Tunnel"))
    local enemies = GetEntitiesWithMixinWithinRange("Live", groundTrace.endPoint, 2.2)
    

+ if Shared.GetTestsEnabled() then + DebugLine(origin,groundTrace.endPoint, 2, 1, 0, 1, 1) + end +

    -- never damage the owner
    local owner = self:GetOwner()
    if owner then

@@ -186,7 +194,13 @@ function Shockwave:Detonate()

    end
    
    if groundTrace.fraction < 1 then

- + + if Shared.GetTestsEnabled() then + DebugBox(groundTrace.endPoint, groundTrace.endPoint, Vector(2.2,0.8,2.2), 5, 0, 0, 1, 1 ) + end + + self:SetOrigin(groundTrace.endPoint + (Vector.yAxis * .5) ) +

        for _, enemy in ipairs(enemies) do
        
            local enemyId = enemy:GetId()

@@ -207,7 +221,9 @@ function Shockwave:Detonate()

        end
    

- end + else + DestroyShockwave(self) + end

    return true

@@ -217,4 +233,4 @@ function Shockwave:GetDeathIconIndex()

    return kDeathMessageIcon.Stomp
end

-Shared.LinkClassToMap("Shockwave", Shockwave.kMapName, networkVars) \ No newline at end of file +Shared.LinkClassToMap("Shockwave", Shockwave.kMapName, networkVars) diff --git a/ns2/lua/Weapons/Marine/Grenade.lua b/ns2/lua/Weapons/Marine/Grenade.lua index 43b2f330a..1300d3d9a 100644 --- a/ns2/lua/Weapons/Marine/Grenade.lua +++ b/ns2/lua/Weapons/Marine/Grenade.lua @@ -107,7 +107,7 @@ if Server then

        -- full damage on direct impact
        if targetHit then
            table.removevalue(hitEntities, targetHit)

- self:DoDamage(kGrenadeLauncherGrenadeDamage, targetHit, targetHit:GetOrigin(), GetNormalizedVector(targetHit:GetOrigin() - self:GetOrigin()), "none") + self:DoDamage(kGrenadeLauncherGrenadeDamage, targetHit, self:GetOrigin(), GetNormalizedVector(targetHit:GetOrigin() - self:GetOrigin()), "none")

        end

        RadiusDamage(hitEntities, self:GetOrigin(), kGrenadeLauncherGrenadeDamageRadius, kGrenadeLauncherGrenadeDamage, self)

diff --git a/ns2/lua/Whip.lua b/ns2/lua/Whip.lua index 6506780cb..daeea2cb9 100644 --- a/ns2/lua/Whip.lua +++ b/ns2/lua/Whip.lua @@ -62,18 +62,19 @@ Whip.kBombardRange = 20

Whip.kBombSpeed = 20

local networkVars =

-{ - attackYaw = "interpolated integer (0 to 360)", - - slapping = "boolean", -- true if we have started a slap attack - bombarding = "boolean", -- true if we have started a bombard attack - - rooted = "boolean", - move_speed = "float", -- used for animation speed - - -- used for rooting/unrooting - unblockTime = "time", -} + { + attackYaw = "interpolated integer (0 to 360)", + + slapping = "boolean", -- true if we have started a slap attack + bombarding = "boolean", -- true if we have started a bombard attack + lastAttackStart = "compensated time", -- Time of the last attack start + + rooted = "boolean", + move_speed = "float", -- used for animation speed + + -- used for rooting/unrooting + unblockTime = "time", + }

AddMixinNetworkVars(UpgradableMixin, networkVars)
AddMixinNetworkVars(OrdersMixin, networkVars)

@@ -105,19 +106,21 @@ function Whip:OnCreate()

    self.slapping = false
    self.bombarding = false

+ self.lastAttackStart = 0 +

    self.rooted = true
    self.moving = false
    self.move_speed = 0
    self.unblockTime = 0

- -- to prevent collision with whip bombs + -- to prevent collision with whip bombs

    self:SetPhysicsGroup(PhysicsGroup.WhipGroup)
    
    if Server then

        self.targetId = Entity.invalidId
        self.nextAttackTime = 0

- +

    end

end

@@ -127,7 +130,7 @@ function Whip:OnInitialized()

    AlienStructure.OnInitialized(self, Whip.kModelName, Whip.kAnimationGraph)
    
    if Server then

- +

        InitMixin(self, RepositioningMixin)
        InitMixin(self, SupplyUserMixin)
        InitMixin(self, TargetCacheMixin)

@@ -142,6 +145,8 @@ function Whip:OnInitialized()

    InitMixin(self, IdleMixin)
    
    self:SetUpdates(true)

+ self.nextSlapStartTime = 0 + self.nextBombardStartTime = 0

end

@@ -246,11 +251,18 @@ function Whip:OnUpdateAnimationInput(modelMixin)

    PROFILE("Whip:OnUpdateAnimationInput")  
    
    local activity = "none"

- - if self.slapping then - activity = "primary" - elseif self.bombarding then - activity = "secondary" + local timeFromLastAttack = 0 + local outSyncedBy = Server and 0 or (Shared.GetTime() - self.lastAttackStart) + + -- 0.10s is a good value, you have to set net_lag=700 and net_loss=40 to start seeing + -- the animation not playing, and even then only once in a while. It's still a permissive. + -- However, when it plays, it is sync with the hit of the tentacle. + if outSyncedBy <= 0.10 then + if self.slapping then + activity = "primary" + elseif self.bombarding then + activity = "secondary" + end

    end
    
    -- use the back attack animation (both slap and bombard) for this range of yaw

@@ -283,7 +295,7 @@ end

-- CQ: EyePos seems to be somewhat hackish; used in several places but not owned anywhere... predates Mixins
function Whip:GetEyePos()

- return self:GetOrigin() + self:GetCoords().yAxis * 1.8 + return self:GetOrigin() + Vector(0, 1.8, 0) -- self:GetCoords().yAxis * 1.8

end

-- CQ: Predates Mixins, somewhat hackish

@@ -356,7 +368,7 @@ function Whip:OnUpdate(deltaTime)

    AlienStructure.OnUpdate(self, deltaTime)
    
    if Server then 

- +

        self:UpdateRootState()           
        self:UpdateOrders(deltaTime)
        

@@ -386,4 +398,4 @@ if Client then

end

-Shared.LinkClassToMap("Whip", Whip.kMapName, networkVars, true) \ No newline at end of file +Shared.LinkClassToMap("Whip", Whip.kMapName, networkVars, true) diff --git a/ns2/lua/Whip_Server.lua b/ns2/lua/Whip_Server.lua index 62521cf5a..690b52b88 100644 --- a/ns2/lua/Whip_Server.lua +++ b/ns2/lua/Whip_Server.lua @@ -8,22 +8,71 @@


-- reset attack if we don't get an end-tag from the animation inside this time

-Whip.kAttackTimeout = 10

local kWhipAttackScanInterval = 0.33

-local kSlapAfterBombardTimeout = 2 -local kBombardAfterBombardTimeout = 5.3 -local kAttackYawTurnRate = 120 -- degrees/sec +local kSlapAfterBombardTimeout = Shared.GetAnimationLength(Whip.kModelName, "attack") +local kBombardAfterBombardTimeout = Shared.GetAnimationLength(Whip.kModelName, "bombard") + +-- Delay between the animation start and the "hit" tagName. Values here are hardcoded and +-- will be replaced with the more accurate, real one at the first whip "hit" tag recorded. +local kAnimationHitTagAtSet = { slap = false, bombard = false } +local kSlapAnimationHitTagAt = kSlapAfterBombardTimeout / 2.5 +local kBombardAnimationHitTagAt = kBombardAfterBombardTimeout / 11.5 + +local kRangeSquared = Whip.kRange^2 +local kBombardRangeSquared = Whip.kBombardRange^2 +

Script.Load("lua/Ballistics.lua")

+function Whip:IsEntBlockingLos(ent, target) + local entOrigin = ent:GetOrigin() + local targetOrigin = target:GetOrigin() + local eyePos = self:GetOrigin() + local angle = math.rad(285/2) + + local toEntity = Vector(0, 0, 0) + + -- Reuse vector + toEntity.x = entOrigin.x - eyePos.x + toEntity.y = entOrigin.y - eyePos.y + toEntity.z = entOrigin.z - eyePos.z + + -- Normalize vector + local toEntityLength = math.sqrt(toEntity.x * toEntity.x + toEntity.y * toEntity.y + toEntity.z * toEntity.z) + if toEntityLength > kEpsilon then + toEntity.x = toEntity.x / toEntityLength + toEntity.y = toEntity.y / toEntityLength + toEntity.z = toEntity.z / toEntityLength + end + + local normViewVec = (eyePos - targetOrigin):GetUnit() + local dotProduct = Math.DotProduct(toEntity, normViewVec) + + local s = math.acos(dotProduct) + + -- Log("S = " .. tostring(s) .. " angle = " .. tostring(angle) .. " blocking ent: " .. EntityToString(ent)) + local isVisible = (s > angle) + return (isVisible) +end + + +-- ValidateTarget() checks if we can attack and hit the target via a traceray between +-- our eyePos and the target:GetEngagementPoint(). +-- Todo Fix: +-- Due to that Whips ignore any target with an engagement point behind an obstacle +-- like a railing even if the rest of the target is clearly visible. +function Whip:GetCanAttackTarget(selector, target, range) + return selector:ValidateTarget(target) +end +

function Whip:UpdateOrders(deltaTime)

    if GetIsUnitActive(self) then

- +

        self:UpdateAttack(deltaTime)

- +

    end

- +

end


@@ -31,9 +80,9 @@ function Whip:SetBlockTime(interval)

    assert(type(interval) == "number")
    assert(interval > 0)

- +

    self.unblockTime = Shared.GetTime() + interval

- +

end


@@ -43,51 +92,51 @@ function Whip:OnTeleport()

    if self.rooted then
        self:Unroot()
    end

- +

end

function Whip:UpdateRootState()

- +

    local infested = self:GetGameEffectMask(kGameEffect.OnInfestation)
    local moveOrdered = self:GetCurrentOrder() and self:GetCurrentOrder():GetType() == kTechId.Move
    -- unroot if we have a move order or infestation recedes
    if self.rooted and (moveOrdered or not infested) then
        self:Unroot()
    end

- +

    -- root if on infestation and not moving/teleporting
    if not self.rooted and infested and not (moveOrdered or self:GetIsTeleporting()) then
        self:Root()
    end

- +

end

function Whip:Root()

    StartSoundEffectOnEntity(Whip.kRootedSound, self)

- +

    self:AttackerMoved() -- reset target sel

    self.rooted = true
    self:SetBlockTime(0.5)

- +

    self:EndAttack()

- + self.targetId = Entity.invalidId +

    return true

- +

end

function Whip:Unroot()

    StartSoundEffectOnEntity(Whip.kUnrootSound, self)

- +

    self.rooted = false
    self:SetBlockTime(0.5)
    self:EndAttack()

- self.attackStartTime = nil - +

    return true

- +

end

-- handle the targetId

@@ -95,19 +144,19 @@ function Whip:OnEntityChange(oldId, newId)

    -- Check if an entity was destroyed.
    if oldId ~= nil and newId == nil then

- +

        if oldId == self.targetId then
            self.targetId = Entity.invalidId
        end

- - end - + + end +

end

function Whip:OnMaturityComplete()

    self:GiveUpgrade(kTechId.WhipBombard)

- +

end


@@ -115,22 +164,22 @@ function Whip:OnTeleportEnd()

    self:AttackerMoved() -- reset target sel
    self:ResetPathing()

- +

end

function Whip:PerformAction(techNode, position)

    local success = false

- +

    if techNode:GetTechId() == kTechId.Cancel or techNode:GetTechId() == kTechId.Stop then

- +

        self:ClearOrders()
        success = true

    end

- +

    return success

- +

end


@@ -149,18 +198,13 @@ function Whip:UpdateAttack(deltaTime)

    if not self.nextAttackScanTime or now > self.nextAttackScanTime then
        self:UpdateAttacks()
    end

- - if self.attackStartTime and now > self.attackStartTime + Whip.kAttackTimeout then - Log("%s: started attack more than %s seconds ago, anim graph bug? Reset...", self, Whip.kAttackTimeout) - self:EndAttack() - end - +

end

function Whip:UpdateAttacks()

    if self:GetCanStartSlapAttack() then

- local newTarget = self:TryAttack(self.slapTargetSelector) + local newTarget = self:TryAttack(self.slapTargetSelector, true, kRangeSquared)

        self.nextAttackScanTime = Shared.GetTime() + kWhipAttackScanInterval
        if newTarget then
            self:FaceTarget(newTarget)

@@ -169,9 +213,9 @@ function Whip:UpdateAttacks()

            self.bombarding = false
        end
    end

- - if self:GetCanStartBombardAttack() then - local newTarget = self:TryAttack(self.bombardTargetSelector) + + if not self.slapping and self:GetCanStartBombardAttack() then + local newTarget = self:TryAttack(self.bombardTargetSelector, false, kBombardRangeSquared)

        self.nextAttackScanTime = Shared.GetTime() + kWhipAttackScanInterval
        if newTarget then
            self:FaceTarget(newTarget)

@@ -180,22 +224,14 @@ function Whip:UpdateAttacks()

            self.slapping = false;
        end
    end

-

end


function Whip:GetCanStartSlapAttack()

- if self.slapping or self.bombarding or not self.rooted or self:GetIsOnFire() then + if not self.rooted or self:GetIsOnFire() then

        return false
    end

- - -- if we are in the aftermath of a long attack (ie, bombarding) and enough time has passed, we can try slapping - if self.waitingForEndAttack and self.attackStartTime and Shared.GetTime() > self.attackStartTime + kSlapAfterBombardTimeout then - return true - end - - return not self.waitingForEndAttack - + return Shared.GetTime() > self.nextSlapStartTime

end

function Whip:GetCanStartBombardAttack()

@@ -204,35 +240,39 @@ function Whip:GetCanStartBombardAttack()

        return false
    end

- if self.slapping or self.bombarding or not self.rooted or self:GetIsOnFire() then - return false - end - - if self.waitingForEndAttack or self.bombarding or self.slapping then + if not self.rooted or self:GetIsOnFire() then

        return false
    end

- - -- because bombard attacks can be terminated early, we have a second check to avoid premature bombardment - if self.bombardAttackStartTime and Shared.GetTime() < self.bombardAttackStartTime + kBombardAfterBombardTimeout then - return false - end - - return true - + return Shared.GetTime() > self.nextBombardStartTime

end

+function Whip:TryAttack(selector, keepTarget, maxRangeSquared) + local target + + if keepTarget and self.targetId ~= Entity.invalidId then + target = Shared.GetEntity(self.targetId) + local isPlayer = target and target:isa("Player") + local canAttack = target and self:GetCanAttackTarget(selector, target, maxRangeSquared) + local whipOrig = Vector(self:GetOrigin().x, 0, self:GetOrigin().z) + local targetOrig = target and Vector(target:GetOrigin().x, 0, target:GetOrigin().z) + local isInRange = target and (whipOrig - targetOrig):GetLengthSquared() < maxRangeSquared + + -- Only remember player targets, otherwise we could be locked attacking a building and not switch + -- to a player we could hit (but keep hitting the same player, priority target) + if not isPlayer or not canAttack or not isInRange then + target = nil + self.targetId = Entity.invalidId + end + end

-function Whip:TryAttack(selector) - - -- prioritize hitting the already targeted entity, if possible - local target = Shared.GetEntity(self.targetId) - if target and selector:ValidateTarget(target) then - return target - else - self.targetId = Entity.invalidId + if not target then + target = selector:AcquireTarget() + if target and not self:GetCanAttackTarget(selector, target, maxRangeSquared) then + target = nil + end

    end

- return selector:AcquireTarget() + return target

end

@@ -264,43 +304,43 @@ function Whip:CalcTargetYaws(target)

    -- bombard_back : covers the 135-225 degree area using poseParams 225-315
    -- No valid attack animation covers the 90-135 and 225-270 angles - they are "dead"
    -- To avoid the dead angles, we lerp the view angle at half the attack yaw rate

- +

    -- the attack_yaw we calculate here is the actual angle to be attacked. The pose_params
    -- attack_yaw will be transformed to cover it correctly. OnUpdateAnimationInput handles
    -- switching animations by use_back

    -- Update our attackYaw to aim at our current target
    local attackDir = GetNormalizedVector(point - self:GetModelOrigin())

- +

    -- the animation rotates the wrong way, mathemathically speaking
    local attackYawRadians = -math.atan2(attackDir.x, attackDir.z)

- +

    -- Factor in the orientation of the whip.
    attackYawRadians = attackYawRadians + self:GetAngles().yaw

- +

    --[[

- local angles2 = self:GetAngles() - local p1 = self:GetModelOrigin() - local c = angles2:GetCoords() - DebugLine(p1, p1 + c.zAxis * 2, 5, 0, 1, 0, 1) - angles2.yaw = self:GetAngles().yaw - attackYawRadians - c = angles2:GetCoords() - DebugLine(p1, p1 + c.zAxis * 2, 5, 1, 0, 0, 1) + local angles2 = self:GetAngles() + local p1 = self:GetModelOrigin() + local c = angles2:GetCoords() + DebugLine(p1, p1 + c.zAxis * 2, 5, 0, 1, 0, 1) + angles2.yaw = self:GetAngles().yaw - attackYawRadians + c = angles2:GetCoords() + DebugLine(p1, p1 + c.zAxis * 2, 5, 1, 0, 0, 1)

    --]]

- +

    local attackYawDegrees = DegreesTo360(math.deg(attackYawRadians), true)
    --Log("%s: attackYawDegrees %s, view angle deg %s", self, attackYawDegrees, DegreesTo360(math.deg(self:GetAngles().yaw)))

- +

    -- now figure out any adjustments needed in viewYaw to keep out of the bad animation zones
    local viewYawAdjust = AvoidSector(attackYawDegrees, 90,135)

- if viewYawAdjust == 0 then + if viewYawAdjust == 0 then

        viewYawAdjust = AvoidSector(attackYawDegrees, 225, 270)
    end

- +

    attackYawDegrees = attackYawDegrees - viewYawAdjust
    viewYawAdjust = math.rad(viewYawAdjust)

- - + +

    return  viewYawAdjust, attackYawDegrees

end

@@ -313,20 +353,20 @@ function Whip:TrackTarget(target, deltaTime)

    -- we can't adjust attack yaw after the attack has started, as that will change what animation is run and thus screw
    -- the generation of hit tags. Instead, we rotate the whole whip so the attack will be towards the target

- +

    local dir2Target = GetNormalizedVector(point - self:GetModelOrigin())

- +

    local yaw2Target = -math.atan2(dir2Target.x, dir2Target.z)

- +

    local attackYaw = math.rad(self.attackYaw)
    local desiredYaw = yaw2Target - attackYaw

- +

    local angles = self:GetAngles()
    angles.yaw = desiredYaw
    -- think about slerping later
    Log("%s: Tracking to %s", self, desiredYaw)
    -- self:SetAngles(angles)

- +

end


@@ -337,9 +377,9 @@ function Whip:FaceTarget(target)

    angles.yaw = angles.yaw + viewYawAdjust
    self:SetAngles(angles)

- +

    self.attackYaw = attackYaw

- +

end


@@ -356,16 +396,22 @@ end

function Whip:SlapTarget(target)
    self:FaceTarget(target)
    -- where we hit

+ local now = Shared.GetTime()

    local targetPoint = target:GetEngagementPoint()
    local attackOrigin = self:GetEyePos()
    local hitDirection = targetPoint - attackOrigin
    hitDirection:Normalize()
    -- fudge a bit - put the point of attack 0.5m short of the target
    local hitPosition = targetPoint - hitDirection * 0.5

- +

    self:DoDamage(Whip.kDamage, target, hitPosition, hitDirection, nil, true)
    self:TriggerEffects("whip_attack")

+ local nextSlapStartTime = now + (kSlapAfterBombardTimeout - kSlapAnimationHitTagAt) + local nextBombardStartTime = now + (kSlapAfterBombardTimeout - kSlapAnimationHitTagAt) + + self.nextSlapStartTime = math.max(nextSlapStartTime, self.nextSlapStartTime) + self.nextBombardStartTime = math.max(nextBombardStartTime, self.nextBombardStartTime)

end

--

@@ -374,38 +420,44 @@ end

function Whip:BombardTarget(target)
    self:FaceTarget(target)
    -- This seems to fail completly; we get really weird values from the Whip_Ball point,

+ local now = Shared.GetTime()

    local bombStart,success = self:GetAttachPointOrigin("Whip_Ball")
    if not success then
        Log("%s: no Whip_Ball point?", self)
        bombStart = self:GetOrigin() + Vector(0,1,0);
    end

- +

    local targetPos = target:GetEngagementPoint()

- +

    local direction = Ballistics.GetAimDirection(bombStart, targetPos, Whip.kBombSpeed)
    if direction then
        self:FlingBomb(bombStart, targetPos, direction, Whip.kBombSpeed)
    end

+ local nextSlapStartTime = now + (kSlapAfterBombardTimeout - kBombardAnimationHitTagAt) + local nextBombardStartTime = now + (kBombardAfterBombardTimeout - kBombardAnimationHitTagAt) + + self.nextSlapStartTime = math.max(nextSlapStartTime, self.nextSlapStartTime) + self.nextBombardStartTime = math.max(nextBombardStartTime, self.nextBombardStartTime)

end

function Whip:FlingBomb(bombStart, targetPos, direction, speed)

    local bomb = CreateEntity(WhipBomb.kMapName, bombStart, self:GetTeamNumber())

- +

    -- For callback purposes so we can adjust our aim
    bomb.intendedTargetPosition = targetPos
    bomb.shooter = self
    bomb.shooterEntId = self:GetId()

- +

    SetAnglesFromVector(bomb, direction)

    local startVelocity = direction * speed
    bomb:Setup( self:GetOwner(), startVelocity, true, nil, self)

- +

    -- we set the lifetime so that if the bomb does not hit something, it still explodes in the general area. Good for hunting jetpackers.
    bomb:SetLifetime(self:CalcLifetime(bombStart, targetPos, startVelocity))

- +

end

function Whip:CalcLifetime(bombStart, targetPos, startVelocity)

@@ -415,12 +467,12 @@ function Whip:CalcLifetime(bombStart, targetPos, startVelocity)

    xzVelocity.y = 0
    xzVelocity:Normalize()
    xzVelocity = xzVelocity:DotProduct(startVelocity)

- +

    -- Lifetime is enough to reach target + small random amount.

- local lifetime = xzRange / xzVelocity + math.random() * 0.2 - + local lifetime = xzRange / xzVelocity + math.random() * 0.2 +

    return lifetime

- +

end


@@ -428,91 +480,146 @@ end

-- --- Attack animation handling

-function Whip:OnAttackStart() +function Whip:OnAttackStart()

    -- attack animation has started, so the attack has started
    if HasMixin(self, "Cloakable") then

- self:TriggerUncloak() + self:TriggerUncloak()

    end

    if self.bombarding then
        self:TriggerEffects("whip_bombard")
    end

- self.attackStartTime = Shared.GetTime() -

end

-function Whip:OnAttackHit(target) +-- +-- Check if an other target is reachable in the same attack cone, so we don't 'miss' +-- if someone block the slap or if current target is out of range. +-- Instead, hit the new target in the attack cone +function Whip:OnAttackHitBlockedTarget(target) + local targets = GetEntitiesForTeamWithinRange("Player", + GetEnemyTeamNumber(self:GetTeamNumber()), + self:GetOrigin(), Whip.kRange) + + Shared.SortEntitiesByDistance(target:GetOrigin(), targets) + for _, newTarget in ipairs(targets) do + + if newTarget ~= target then + self.targetId = newTarget:GetId() + newTarget = self:TryAttack(self.slapTargetSelector, true, kRangeSquared)

- if target and self.slapping then - if not self:GetIsOnFire() and self.slapTargetSelector:ValidateTarget(target) then - self:SlapTarget(target) + if newTarget and newTarget:isa("Player") and self:IsEntBlockingLos(newTarget, target) + then + -- Log("New target blocking the way validated, hitting him") + return true, newTarget + end

        end

+

    end

- - if target and self.bombarding then - if not self:GetIsOnFire() and self.bombardTargetSelector:ValidateTarget(target) then - self:BombardTarget(target) - end + + return false, nil +end + +function Whip:OnAttackHit() + + -- Prevent OnAttackHit to be called multiple times in a raw + if self.attackStarted and (self.slapping or self.bombarding) and not self:GetIsOnFire() then + + local success = false + local target = Shared.GetEntity(self.targetId) + local eyePos = GetEntityEyePos(self) + local selector = (self.slapping and self.slapTargetSelector or self.bombardTargetSelector) + + -- Log("OnAttackHit() : " .. EntityToString(target) .. tostring(self.slapping and " slapping" or " bombard")) + + if target then + + local targetValidated = self.slapping and self:GetCanAttackTarget(selector, target, kRangeSquared) or true + + if not targetValidated then + -- Log("OnAttackHit() : Target not validated, is anything blocking us ?") + targetValidated, target = self:OnAttackHitBlockedTarget(target) + -- Log("OnAttackHit() : New Target validated ? " .. tostring(targetValidated)) + end + + if targetValidated then + if self.slapping then + if self:GetCanAttackTarget(selector, target, kRangeSquared) then + self:SlapTarget(target) + success = true + end + else + self:BombardTarget(target) + success = true + end + end + + end + + if not success then + self.targetId = Entity.invalidId + end

    end

- -- Stop trigger new attacks - self.slapping = false - self.bombarding = false - -- mark that we are waiting for the end of an attack - self.waitingForEndAttack = true - + + self.attackStarted = false + self:EndAttack() +end + +function Whip:OnAttackEnd() + -- Empty placeholder function that can be used for modding

end

function Whip:EndAttack()

    -- unblock the next attack

- self.attackStartTime = nil - self.targetId = Entity.invalidId - self.slapping = false - self.bombarding = false - self.waitingForEndAttack = false; - - self:UpdateAttacks() + -- self.targetId = Entity.invalidId + if self.slapping or self.bombarding then + self:OnAttackEnd()

+ self.slapping = false + self.bombarding = false + self.attackStarted = false + end

end

-

function Whip:OnTag(tagName)

    PROFILE("Whip:OnTag")

- - local target = Shared.GetEntity(self.targetId) - - --[[ - if tagName ~= "start" and tagName ~= "end" then - Log("%s : %s for target %s, slapping %s, bombarding %s", self, tagName, target, self.slapping, self.bombarding) - end - --]] - if tagName == "hit" then - self:OnAttackHit(target) - end

- if tagName == "slap_start" then - self:OnAttackStart(target) + if tagName == "hit" and self.attackStarted then + + if not kAnimationHitTagAtSet.slap and self.slapping then + kAnimationHitTagAtSet.slap = true + kSlapAnimationHitTagAt = (Shared.GetTime() - self.lastAttackStart) + -- Log("%s : Setting slap hit tag at %s", self, tostring(kBombardAnimationHitTagAt)) + end + + if not kAnimationHitTagAtSet.bombard and self.bombarding then + kAnimationHitTagAtSet.bombard = true + kBombardAnimationHitTagAt = (Shared.GetTime() - self.lastAttackStart) + -- Log("%s : Setting bombard hit tag at %s", self, tostring(kBombardAnimationHitTagAt)) + end + + self:OnAttackHit()

    end

- if tagName == "slap_end" then - self:EndAttack() + -- The 'tagName == "hit"' is not reliable and sometime is not triggered at all (obscure reasons). + -- To fix that we use a manual callback that is reliable, so each time a whip has an animation, + -- it is guaranted it will hit if the target is still in range and in sight. + if (tagName == "slap_start" or tagName == "bombard_start") and not self.attackStarted then + local animationLength = (tagName == "slap_start" and kSlapAnimationHitTagAt or kBombardAnimationHitTagAt) + + self.attackStarted = true + self.lastAttackStart = Shared.GetTime() + self:OnAttackStart() + self:AddTimedCallback(Whip.OnAttackHit, animationLength)

    end

- if tagName == "bombard_start" then - self.bombardAttackStartTime = Shared.GetTime() - self:OnAttackStart(target) - end + if tagName == "slap_end" or tagName == "bombard_end" then + self:OnAttackEnd() + end

- if tagName == "bombard_end" then - -- we are only allowed to end our own attack - if a slap-attack has started, we must not terminate it early - if self.bombardAttackStartTime == self.attackStartTime and not self.slapping then - self:EndAttack() - end - end +end

- end -

-- --- End attack animation

\ No newline at end of file diff --git a/ns2/lua/challenge/GUIChallengeButton.lua b/ns2/lua/challenge/GUIChallengeButton.lua new file mode 100644 index 000000000..b28ae234b --- /dev/null +++ b/ns2/lua/challenge/GUIChallengeButton.lua @@ -0,0 +1,248 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengeButton.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Base class for a button for use in the challenge modes popups. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/GUIAssets.lua") +Script.Load("lua/UnsortedSet.lua") + +class 'GUIChallengeButton' (GUIScript) + +GUIChallengeButton.kFontName = Fonts.kAgencyFB_Medium +GUIChallengeButton.kFontSize = 20 -- desired font size, at 1-1 scaling. +GUIChallengeButton.kFontActualSize = 22 -- size font texture actually is +GUIChallengeButton.kTextColor = Color(0,0,0,1) +GUIChallengeButton.kButtonSize = Vector(221, 71, 0) +GUIChallengeButton.kButtonSpacing = 40 + +GUIChallengeButton.kDefaultLayer = 40 + +GUIChallengeButton.kTextLayerOffset = 0 + +-- Over size = regular size scaled up proportionally so that it is kButtonSpacing-wider than normal. +GUIChallengeButton.kButtonOverSize = Vector(GUIChallengeButton.kButtonSize.x + GUIChallengeButton.kButtonSpacing, ((GUIChallengeButton.kButtonSize.x + GUIChallengeButton.kButtonSpacing) / GUIChallengeButton.kButtonSize.x) * GUIChallengeButton.kButtonSize.y, 0) + +GUIChallengeButton.kHoverSound = PrecacheAsset("sound/NS2.fev/common/hovar") +GUIChallengeButton.kClickSound = PrecacheAsset("sound/NS2.fev/common/button_click") + +function GUIChallengeButton:UpdateTransform() + + local size = self.kButtonSize * self.scale + self.minCorner = self.position - (size * 0.5) + self.maxCorner = self.minCorner + size + self.realSize = size -- useful for child classes to not have to redo these calculations + + self.text:SetPosition(self.position) + self.text:SetScale(self.fontScale) + +end + +function GUIChallengeButton:CreateGUIItem() + + local item = GUI.CreateItem() + US_Add(self.items, item) + + return item + +end + +function GUIChallengeButton:InitGUI() + + self.text = self:CreateGUIItem() + self.text:SetOptionFlag(GUIItem.ManageRender) + self.text:SetTextAlignmentX(GUIItem.Align_Center) + self.text:SetTextAlignmentY(GUIItem.Align_Center) + self.text:SetFontName(self.kFontName) + +end + +function GUIChallengeButton:UpdateFontScale() + + local fontScale = (self.kFontSize / self.kFontActualSize) * self.scale.y + self.fontScale = Vector(fontScale, fontScale, 0) + +end + +function GUIChallengeButton:UpdateLayers() + + self.text:SetLayer(self.layer + self.kTextLayerOffset) + +end + +function GUIChallengeButton:UpdateColor() + + local textColor = Color(self.kTextColor) + textColor.a = textColor.a * self.opacity + self.text:SetColor(textColor) + +end + +-- Sets the parent script of this button. The parent script is used to determine if the buttons +-- should be active or not. +function GUIChallengeButton:SetParentScript(script) + + self.parentScript = script + +end + +function GUIChallengeButton:Initialize() + + self.items = US_Create() + + self.layer = self.kDefaultLayer + self.opacity = 0.0 -- start faded out. + + self.position = Vector(0,0,0) + self.scale = Vector(1,1,0) + + self.minCorner = Vector(0,0,0) + self.maxCorner = Vector(0,0,0) + + self.over = false + self.mouseDown = false + + self:InitGUI() + self:UpdateFontScale() + self:UpdateColor() + self:UpdateLayers() + self:UpdateTransform() + self:Update(0) + + MouseTracker_SetIsVisible(true, nil, true) + + self.updateInterval = 0 + +end + +function GUIChallengeButton:Uninitialize() + + for i=1, #self.items.a do + GUI.DestroyItem(self.items.a[i]) + end + + MouseTracker_SetIsVisible(false) + +end + +function GUIChallengeButton:SetText(text) + + self.text:SetText(text) + +end + +function GUIChallengeButton:SetCallback(callback) + + self.callback = callback + +end + +function GUIChallengeButton:CheckForMouseOver() + + if not self.parentScript or not self.parentScript:GetIsWindowActive(self) then + return false + end + + local mousePos = Vector(0,0,0) + mousePos.x, mousePos.y = Client.GetCursorPosScreen() + + if mousePos.x >= self.minCorner.x and + mousePos.x < self.maxCorner.x and + mousePos.y >= self.minCorner.y and + mousePos.y < self.maxCorner.y then + return true + end + + return false + +end + +function GUIChallengeButton:Update(deltaTime) + + local newOver = self:CheckForMouseOver() + if not self.over and newOver then + -- Play sound effect + StartSoundEffect(self.kHoverSound) + end + + if self.over ~= newOver then + self.over = newOver + self:UpdateTransform() + end + +end + +-- We only receive SendKeyEvent from the parent GUIScript because we want the option of consuming all events +-- (eg prevent main menu from being opened while this screen is visible). +function GUIChallengeButton:SendKeyEventFromParent(input, down) + + -- We only care about the left mouse button. + if input ~= InputKey.MouseButton0 then + return false + end + + -- ensure mouse button was not being held down. + if self.mouseDown and down then + return false + end + + -- keep track of mouse's previous "down" state + self.mouseDown = down + + -- nothing else to do if the button isn't down. + if not down then + return false + end + + -- determine if the mouse cursor is over the button. It will also return false if the button is disabled, + -- even if the mouse is over the button. + local over = self:CheckForMouseOver() + if not over then + return false + end + + -- if we made it this far, must be a valid click. + StartSoundEffect(self.kClickSound) + if self.callback then + self.callback(self) + end + + return true + +end + +function GUIChallengeButton:SetPosition(position) + + self.position = position + self:UpdateTransform() + +end + +function GUIChallengeButton:SetScale(scale) + + self.scale = scale + self:UpdateFontScale() + self:UpdateTransform() + +end + +function GUIChallengeButton:SetOpacity(opacity) + + self.opacity = opacity + self:UpdateColor() + +end + +function GUIChallengeButton:SetLayer(layer) + + self.layer = layer + self:UpdateLayers() + +end + + + diff --git a/ns2/lua/challenge/GUIChallengeButtonAlien.lua b/ns2/lua/challenge/GUIChallengeButtonAlien.lua new file mode 100644 index 000000000..6e1116d7d --- /dev/null +++ b/ns2/lua/challenge/GUIChallengeButtonAlien.lua @@ -0,0 +1,106 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengeButtonAlien.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Alien-themed button for use in the many popups in the challenge mode. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/challenge/GUIChallengeButton.lua") + +class 'GUIChallengeButtonAlien' (GUIChallengeButton) + +GUIChallengeButtonAlien.kTextLayerOffset = 2 +GUIChallengeButtonAlien.kVeinsLayerOffset = 1 +GUIChallengeButtonAlien.kBackgroundLayerOffset = 0 + +GUIChallengeButtonAlien.kTexture = PrecacheAsset("ui/alien_buymenu.dds") +GUIChallengeButtonAlien.kTextureCoords = {396, 428, 706, 511} +GUIChallengeButtonAlien.kVeinsTextureCoords = { 600, 350, 915, 419} +GUIChallengeButtonAlien.kVeinsMargin = 4 +GUIChallengeButtonAlien.kVeinsPulsePeriod = math.pi -- pulse once every two seconds. + +GUIChallengeButtonAlien.kHoverSound = PrecacheAsset("sound/NS2.fev/alien/common/alien_menu/hover") +GUIChallengeButtonAlien.kClickSound = PrecacheAsset("sound/NS2.fev/alien/common/alien_menu/close_menu") + +function GUIChallengeButtonAlien:UpdateLayers() + + GUIChallengeButton.UpdateLayers(self) + + self.back:SetLayer(self.layer + self.kBackgroundLayerOffset) + self.veins:SetLayer(self.layer + self.kVeinsLayerOffset) + +end + +function GUIChallengeButtonAlien:Update(deltaTime) + + GUIChallengeButton.Update(self, deltaTime) + + if self.over then + self.veinsPulse = 0.0 + else + self.veinsPulse = self.veinsPulse + deltaTime + end + + local pulse = math.cos(self.veinsPulse * self.kVeinsPulsePeriod) * 0.5 + 0.5 + self.veins:SetColor(Color(1,1,1,self.opacity * pulse)) + +end + +function GUIChallengeButtonAlien:InitGUI() + + GUIChallengeButton.InitGUI(self) + + self.back = self:CreateGUIItem() + self.back:SetTexture(self.kTexture) + self.back:SetTexturePixelCoordinates(unpack(self.kTextureCoords)) + + self.veins = self:CreateGUIItem() + self.veins:SetTexture(self.kTexture) + self.veins:SetTexturePixelCoordinates(unpack(self.kVeinsTextureCoords)) + + self.veinsPulse = 0.0 + +end + +function GUIChallengeButtonAlien:UpdateColor() + + GUIChallengeButton.UpdateColor(self) + + self.back:SetColor(Color(1,1,1,self.opacity)) + +end + +function GUIChallengeButtonAlien:UpdateTransform() + + GUIChallengeButton.UpdateTransform(self) + + local size + if self.over then + size = Vector(self.kButtonOverSize) + else + size = Vector(self.kButtonSize) + end + + local veinsSize = size - (Vector(self.kVeinsMargin, self.kVeinsMargin, 0) * 2.0) + + size = size * self.scale + veinsSize = veinsSize * self.scale + + local backPos = self.position - (size * 0.5) + local veinsPos = self.position - (veinsSize * 0.5) + + self.back:SetPosition(backPos) + self.back:SetSize(size) + + self.veins:SetPosition(veinsPos) + self.veins:SetSize(veinsSize) + +end + + + + + diff --git a/ns2/lua/challenge/GUIChallengeLeaderboard.lua b/ns2/lua/challenge/GUIChallengeLeaderboard.lua new file mode 100644 index 000000000..14f78ac9e --- /dev/null +++ b/ns2/lua/challenge/GUIChallengeLeaderboard.lua @@ -0,0 +1,2161 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengeLeaderboard.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- An abstract GUIScript class for displaying the leaderboards for challenge modes. This is +-- extended by the GUIChallengeLeaderboardAlien class, for alien-themed leaderboards. (At the +-- time of writing, there are no marine-related challenges... but it's nice to plan ahead.) +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +local kSteamProfileURL = "http://steamcommunity.com/profiles/" + +Script.Load("lua/GUIAssets.lua") +Script.Load("lua/UnsortedSet.lua") + +class 'GUIChallengeLeaderboard' (GUIScript) + +local kDefaultLayer = 40 + +-- All of the below member-constants are encouraged to be overwritten by extended classes, where desired. +GUIChallengeLeaderboard.kNumRows = 10 + +GUIChallengeLeaderboard.kColor = Color(1,1,1,1) +GUIChallengeLeaderboard.kShadowColor = Color(0,0,0,0.5) +GUIChallengeLeaderboard.kHighlightedColor = Color(1,1,1,1) + +GUIChallengeLeaderboard.kPanelWidth = 512 + +GUIChallengeLeaderboard.kTitleFontName = Fonts.kAgencyFB_Huge +local kAgencyHugeActualSize = 66 +GUIChallengeLeaderboard.kTitleFontSize = 42 + +GUIChallengeLeaderboard.kFontName = Fonts.kAgencyFB_Large +local kAgencyLargeActualSize = 28 +GUIChallengeLeaderboard.kFontSize = 24 + +GUIChallengeLeaderboard.kTooltipTextLayerOffset = 8 +GUIChallengeLeaderboard.kTooltipTextShadowLayerOffset = 7 +GUIChallengeLeaderboard.kTooltipBackgroundLayerOffset = 6 +GUIChallengeLeaderboard.kTooltipBackgroundShadowLayerOffset = 5 +GUIChallengeLeaderboard.kButtonHighlightLayerOffset = 4 +GUIChallengeLeaderboard.kContentLayerOffset = 3 +GUIChallengeLeaderboard.kContentShadowLayerOffset = 2 +GUIChallengeLeaderboard.kHighlightLayerOffset = 1 +GUIChallengeLeaderboard.kAnimationStencilLayerOffset = 1 +GUIChallengeLeaderboard.kBackgroundLayerOffset = 0 + +GUIChallengeLeaderboard.kShadowOffset = Vector(2, 2, 0) + +GUIChallengeLeaderboard.kHeaderYOffset = 74 +GUIChallengeLeaderboard.kPlayerHeaderXOffset = 102 +GUIChallengeLeaderboard.kDividerYOffset = 103 +GUIChallengeLeaderboard.kDividerThickness = 8 +GUIChallengeLeaderboard.kRowYOffset = 144 +GUIChallengeLeaderboard.kRankXOffset = 47 +GUIChallengeLeaderboard.kIconSize = 32 +GUIChallengeLeaderboard.kCommonMargin = 8 +GUIChallengeLeaderboard.kRowSpacing = 40 +GUIChallengeLeaderboard.kArrowSize = Vector(48, 96, 0) +GUIChallengeLeaderboard.kArrowXOffset = 380 -- should be overidden +GUIChallengeLeaderboard.kHighlightWidth = 430 -- should be overidden +GUIChallengeLeaderboard.kPlayerButtonWidth = 160 -- will likely be overidden + +GUIChallengeLeaderboard.kFriendsIcon = PrecacheAsset("ui/challenge/friends_icon.dds") +GUIChallengeLeaderboard.kGlobalIcon = PrecacheAsset("ui/challenge/globe_icon.dds") +GUIChallengeLeaderboard.kArrowIcon = PrecacheAsset("models/dev/dev_sphere.dds") -- should be overridden +GUIChallengeLeaderboard.kHighlightGraphic = PrecacheAsset("ui/challenge/leaderboard_personal_highlight.dds") +GUIChallengeLeaderboard.kHighlightColor = Color(1,1,1,1) -- should be overridden. +GUIChallengeLeaderboard.kHighlightSize = Vector(588, 67, 0) +GUIChallengeLeaderboard.kHighlightPosition = Vector(-31, 139, 0) +GUIChallengeLeaderboard.kAvatarHighlightStrength = 0.75 +GUIChallengeLeaderboard.kButtonDisabledColor = Color(0.25, 0.25, 0.25, 1.0) +GUIChallengeLeaderboard.kMissingAvatarTexture = PrecacheAsset("ui/missing_avatar.dds") + +GUIChallengeLeaderboard.kTooltipFontSize = 15 +GUIChallengeLeaderboard.kTooltipFontName = Fonts.kAgencyFB_Small +local kAgencySmallActualSize = 18 +local kAgencySmallLineSpan = 27 +GUIChallengeLeaderboard.kTooltipDelayTime = 0.5 +GUIChallengeLeaderboard.kTooltipFadeInTime = 0.5 +GUIChallengeLeaderboard.kTooltipPersistTime = 1.0 +GUIChallengeLeaderboard.kTooltipBackColor = Color(0.5, 0.5, 0.5, 0.9) -- should be overridden +GUIChallengeLeaderboard.kTooltipMargin = 4.0 +GUIChallengeLeaderboard.kTooltipMarginBottom = 12.0 +GUIChallengeLeaderboard.kTooltipMaxWidth = 140 +GUIChallengeLeaderboard.kTooltipOffset = Vector(0,32,0) -- so mouse cursor doesn't overlap it as much + +GUIChallengeLeaderboard.kLeaderboardPosition = Vector(0, 0, 0) + +GUIChallengeLeaderboard.kInvalidHandle = "18446744073709551615" -- (2^64)-1 + +GUIChallengeLeaderboard.kWipeTime = 0.1 -- each row's wipe takes 0.1 seconds from start to finish +GUIChallengeLeaderboard.kWipeDelay = 0.016667 -- each row's wipe is delayed by this amount from the previous row. + +GUIChallengeLeaderboard.kFadeTime = 1.0 + +GUIChallengeLeaderboard.kButtonHoverSound = PrecacheAsset("sound/NS2.fev/common/hovar") +GUIChallengeLeaderboard.kButtonClickSound = PrecacheAsset("sound/NS2.fev/common/button_click") + +function GUIChallengeLeaderboard:UpdateRowTransform(rowIndex) + + local shadowOffset = self.kShadowOffset * self.scale + local rowOffset = Vector(0, self.kRowSpacing * (rowIndex-1) * self.scale.y, 0) + + local row = self.rows[rowIndex] + + -- Rank + local rankPosition = Vector(self.kRankXOffset, self.kRowYOffset, 0) + rankPosition = (rankPosition * self.scale) + self.position + rowOffset + row.rankItem:SetPosition(rankPosition) + row.rankShadowItem:SetPosition(rankPosition + shadowOffset) + row.rankItem:SetScale(self.fontScale) + row.rankShadowItem:SetScale(self.fontScale) + + -- Player Info (avatar button, name text button) + local iconPosition = Vector(rankPosition.x + (self.kCommonMargin * self.scale.x), rankPosition.y - (self.kIconSize * 0.5 * self.scale.y), 0) + row.playerItemTable.iconButton.realPos = Vector(iconPosition) + local iconSize = Vector(self.scale.x * self.kIconSize, self.scale.y * self.kIconSize, 0) + row.playerItemTable.iconButton.realSize = Vector(iconSize) + row.playerItemTable.iconButton:SetPosition(iconPosition, shadowOffset) + row.playerItemTable.iconButton:SetSize(iconSize) + + local namePosition = Vector(rankPosition.x + ((self.kIconSize + self.kCommonMargin * 3.0) * self.scale.x), rankPosition.y, 0) + local nameButtonPosition = namePosition - Vector(0, self.kFontSize * 0.5 * self.scale.y, 0) + local nameSize = Vector(self.scale.x * self.kPlayerButtonWidth, self.scale.y * self.kIconSize, 0) + row.playerItemTable.nameButton.realPos = Vector(nameButtonPosition) + row.playerItemTable.nameButton.realSize = Vector(nameSize) + row.playerItemTable.nameButton:SetPosition(namePosition, shadowOffset) + row.playerItemTable.nameButton:SetSize(nameSize) + + -- Animation wiper (mostly just to get the y-coordinates set correctly, x is handled by animation whenever + -- animation is running) + row.wipePos = Vector(self.position.x, rankPosition.y - (self.kRowSpacing * 0.5 * self.scale.y) , 0.0) + row.wipeSize = Vector(self:GetRowWidth(), self.kRowSpacing, 0.0) * self.scale + row.wiper:SetPosition(row.wipePos) + row.wiper:SetSize(row.wipeSize) + +end + +function GUIChallengeLeaderboard:UpdateRowStencilFunc(rowIndex, sFunc) + + local row = self.rows[rowIndex] + + row.rankItem:SetStencilFunc(sFunc) + row.rankShadowItem:SetStencilFunc(sFunc) + row.playerItemTable:SetStencilFunc(sFunc) + +end + +-- Called to update the positioning and scaling of all items in the leaderboard. +function GUIChallengeLeaderboard:UpdateTransform() + + local shadowOffset = self.kShadowOffset * self.scale + + -- Header + -- Title + local titlePosition = Vector(self.kPanelWidth * 0.5, self.kTitleFontSize * 0.5, 0) + titlePosition = (titlePosition * self.scale) + self.position + self.titleItem:SetPosition(titlePosition) + self.titleShadowItem:SetPosition(titlePosition + shadowOffset) + self.titleItem:SetScale(self.titleFontScale) + self.titleShadowItem:SetScale(self.titleFontScale) + + -- Rank + local rankPosition = Vector(0, self.kHeaderYOffset, 0) + rankPosition = (rankPosition * self.scale) + self.position + self.rankHeaderItem:SetPosition(rankPosition) + self.rankHeaderShadowItem:SetPosition(rankPosition + shadowOffset) + self.rankHeaderItem:SetScale(self.fontScale) + self.rankHeaderShadowItem:SetScale(self.fontScale) + + -- Player + local playerPosition = Vector(self.kPlayerHeaderXOffset, self.kHeaderYOffset, 0) + playerPosition = (playerPosition * self.scale) + self.position + self.playerHeaderItem:SetPosition(playerPosition) + self.playerHeaderShadowItem:SetPosition(playerPosition + shadowOffset) + self.playerHeaderItem:SetScale(self.fontScale) + self.playerHeaderShadowItem:SetScale(self.fontScale) + + -- Divider + local dividerPosition = Vector(0, self.kDividerYOffset, 0) + dividerPosition = (dividerPosition * self.scale) + self.position + self.dividerItem:SetPosition(dividerPosition) + self.dividerShadowItem:SetPosition(dividerPosition + shadowOffset) + local dividerSize = Vector(self.kPanelWidth * self.scale.x, self.kDividerThickness * self.scale.y, 0) + self.dividerItem:SetSize(dividerSize) + self.dividerShadowItem:SetSize(dividerSize) + + -- Row Items + for i=1, #self.rows do + self:UpdateRowTransform(i) + end + + -- Buttons + local regularButtonSize = Vector(self.kIconSize, self.kIconSize, 0) * self.scale + + local friendPos = Vector(self.kIconSize, self.kCommonMargin, 0) + friendPos = (friendPos * self.scale) + self.position + self.friendsButton.realPos = Vector(friendPos) + self.friendsButton.realSize = Vector(regularButtonSize) + self.friendsButton:SetPosition(friendPos, shadowOffset) + self.friendsButton:SetSize(regularButtonSize) + + local globalPos = Vector(self.kIconSize * 2.0 + self.kCommonMargin, self.kCommonMargin, 0) + globalPos = (globalPos * self.scale) + self.position + self.globalButton.realPos = Vector(globalPos) + self.globalButton.realSize = Vector(regularButtonSize) + self.globalButton:SetPosition(globalPos, shadowOffset) + self.globalButton:SetSize(regularButtonSize) + + local upArrowPos = Vector(self.kArrowXOffset, self.kRowYOffset - self.kIconSize * 0.5, 0) + local downArrowPos = upArrowPos + Vector(0, (self.kRowSpacing * (self.kNumRows - 1)) + self.kIconSize - self.kArrowSize.y, 0) + upArrowPos = (upArrowPos * self.scale) + self.position + downArrowPos = (downArrowPos * self.scale) + self.position + local arrowSize = self.kArrowSize * self.scale + self.upArrowButton.realSize = arrowSize + self.downArrowButton.realSize = arrowSize + self.upArrowButton.realPos = upArrowPos + self.downArrowButton.realPos = downArrowPos + self.upArrowButton:SetPosition(upArrowPos, shadowOffset) + self.downArrowButton:SetPosition(downArrowPos, shadowOffset) + self.upArrowButton:SetSize(arrowSize) + self.downArrowButton:SetSize(arrowSize) + +end + +-- Returns the entry data for the given row index, or nil if not found. +function GUIChallengeLeaderboard:GetEntryDisplayedAtIndex(rowIndex) + + if not self.displayedTopIndex then + return nil + end + + local entryTable = self:GetActiveData() + + return entryTable[self.displayedTopIndex + rowIndex - 1] + +end + +function GUIChallengeLeaderboard:OnProfileButtonClicked(rowIndex) + + local entry = self:GetEntryDisplayedAtIndex(rowIndex) + if not entry then + return + end + + local steamId = entry.steamId + Client.ShowWebpage(string.format("%s[U:1:%s]", kSteamProfileURL, steamId)) + +end + +function GUIChallengeLeaderboard:GetTextureNameForAvatar(rowIndex) + + return "*avatar"..tostring(rowIndex) + +end + +function GUIChallengeLeaderboard:CreateButtonCommon(onClick, tooltip, disabledTooltip) + + local newButton = {} + newButton.active = false + newButton.over = false + newButton.board = self + newButton.enabled = true + newButton.visible = true + + -- function called when button is (successfully) clicked on. Will not fire if + -- user clicks on button that was already "active", or if item is not visible. + newButton.onClick = onClick + + if tooltip then + newButton.tooltip = Locale.ResolveString(tooltip) + end + + if disabledTooltip then + newButton.disabledTooltip = Locale.ResolveString(disabledTooltip) + end + + newButton.SetIsEnabled = function(button, state) + button.enabled = state + end + + US_Add(self.buttons, newButton) + + return newButton + +end + +function GUIChallengeLeaderboard:CreateSimpleButton(graphic, onClick, tooltip, disabledTooltip) + + local newButton = self:CreateButtonCommon(onClick, tooltip, disabledTooltip) + + local item = self:CreateGUIItem() + item:SetColor(self.kColor) + local shadowItem = self:CreateGUIItem() + shadowItem:SetColor(self.kShadowColor) + + newButton.color = Color(self.kColor) + newButton.shadowColor = Color(self.kShadowColor) + newButton.item = item + newButton.shadowItem = shadowItem + newButton.type = "icon" + + if graphic then + newButton.item:SetTexture(graphic) + newButton.shadowItem:SetTexture(graphic) + end + + newButton.board = self + + newButton.UpdateLayers = function(button) + button.item:SetLayer(self.layer + self.kContentLayerOffset) + button.shadowItem:SetLayer(self.layer + self.kContentShadowLayerOffset) + end + + newButton.SetOpacity = function(button, opacity) + local color = Color(button.color) + color.a = color.a * opacity + button.item:SetColor(color) + + local shadowColor = Color(button.shadowColor) + shadowColor.a = shadowColor.a * opacity + button.shadowItem:SetColor(shadowColor) + end + + newButton.SetStencilFunc = function(button, sFunc) + button.item:SetStencilFunc(sFunc) + button.shadowItem:SetStencilFunc(sFunc) + end + + newButton.SetIsVisible = function(button, state) + button.visible = state + button:UpdateVisibility() + end + + newButton.UpdateVisibility = function(button) + local vis = button.visible and self.visible + button.item:SetIsVisible(vis) + button.shadowItem:SetIsVisible(vis) + end + + newButton.GetIsVisible = function(button, state) + return button.item:GetIsVisible() + end + + local old_Button_SetIsEnabled = newButton.SetIsEnabled + newButton.SetIsEnabled = function(button, state) + old_Button_SetIsEnabled(button, state) + + if state then + button.item:SetColor(self.kColor) + button.color = Color(self.kColor) + else + button.item:SetColor(self.kButtonDisabledColor) + button.color = Color(self.kButtonDisabledColor) + end + end + + newButton.Highlight = function(button) + button.item:SetColor(self.kHighlightedColor) + button.color = Color(self.kHighlightedColor) + end + + newButton.UnHighlight = function(button) + button.item:SetColor(self.kColor) + button.color = Color(self.kColor) + end + + newButton.SetPosition = function(button, pos, shadowOffset) + button.item:SetPosition(pos) + button.shadowItem:SetPosition(pos + shadowOffset) + end + + newButton.SetSize = function(button, size) + button.item:SetSize(size) + button.shadowItem:SetSize(size) + end + + return newButton + +end + +function GUIChallengeLeaderboard:CreateIconButton(rowIndex) + + local newButton = self:CreateSimpleButton(self.kMissingAvatarTexture, + function(button) + button.board:OnProfileButtonClicked(button.rowIndex) + end, "LEADERBOARD_TOOLTIP_PROFILE") + newButton.item:SetColor(Color(1,1,1,1)) + newButton.color = Color(1,1,1,1) + newButton.type = "player_icon" + newButton.overlayItem = self:CreateGUIItem() + newButton.overlayItem:SetIsVisible(false) + newButton.overlayItem:SetBlendTechnique(GUIItem.Add) + newButton.overlayItem:SetColor(Color(1,1,1,self.kAvatarHighlightStrength)) + newButton.highlightColor = Color(1,1,1, self.kAvatarHighlightStrength) + newButton.overlayItem:SetTexture(self.kMissingAvatarTexture) + + local oldUpdateLayers = newButton.UpdateLayers + newButton.UpdateLayers = function(button) + oldUpdateLayers(button) + button.overlayItem:SetLayer(self.layer + self.kButtonHighlightLayerOffset) + end + + local oldSetOpacity = newButton.SetOpacity + newButton.SetOpacity = function(button, opacity) + oldSetOpacity(button, opacity) + + local color = Color(button.highlightColor) + color.a = color.a * opacity + button.overlayItem:SetColor(color) + end + + local oldSetStencilFunc = newButton.SetStencilFunc + newButton.SetStencilFunc = function(button, sFunc) + oldSetStencilFunc(button, sFunc) + button.overlayItem:SetStencilFunc(sFunc) + end + + newButton.SetTexture = function(button, texture) + button.overlayItem:SetTexture(texture) + button.item:SetTexture(texture) + end + + newButton.Highlight = function(button) + button.overlayItem:SetIsVisible(true) + end + + newButton.UnHighlight = function(button) + button.overlayItem:SetIsVisible(false) + end + + local oldSetPosition = newButton.SetPosition + newButton.SetPosition = function(button, pos, shadowOffset) + oldSetPosition(button, pos, shadowOffset) + button.overlayItem:SetPosition(pos) + end + + local oldSetSize = newButton.SetSize + newButton.SetSize = function(button, size) + oldSetSize(button, size) + button.overlayItem:SetSize(size) + end + + return newButton + +end + +function GUIChallengeLeaderboard:CreateTextButton(text, onClick, tooltip, disabledTooltip) + + local newButton = self:CreateButtonCommon(onClick, tooltip) + + local item, shadowItem = self:CreateTextItem(true) + newButton.item = item + newButton.color = Color(newButton.item:GetColor()) + newButton.shadowItem = shadowItem + newButton.shadowColor = Color(newButton.shadowItem:GetColor()) + newButton.type = "text" + + newButton.UpdateLayers = function(button) + button.item:SetLayer(self.layer + self.kContentLayerOffset) + button.shadowItem:SetLayer(self.layer + self.kContentShadowLayerOffset) + end + + newButton.SetOpacity = function(button, opacity) + local color = Color(button.color) + color.a = color.a * opacity + button.item:SetColor(color) + + local shadowColor = Color(button.shadowColor) + shadowColor.a = shadowColor.a * opacity + button.shadowItem:SetColor(shadowColor) + end + + newButton.SetStencilFunc = function(button, sFunc) + button.item:SetStencilFunc(sFunc) + button.shadowItem:SetStencilFunc(sFunc) + end + + newButton.Highlight = function(button) + button.item:SetColor(self.kHighlightedColor) + button.color = Color(self.kHighlightedColor) + end + + newButton.UnHighlight = function(button) + button.item:SetColor(self.kColor) + button.color = Color(self.kColor) + end + + newButton.SetPosition = function(button, pos, shadowOffset) + button.item:SetPosition(pos) + button.shadowItem:SetPosition(pos + shadowOffset) + end + + newButton.SetSize = function(button, size) + button.item:SetScale(self.fontScale) + button.shadowItem:SetScale(self.fontScale) + end + + newButton.SetText = function(button, text) + button.item:SetText(text) + button.shadowItem:SetText(text) + end + + newButton.SetIsVisible = function(button, state) + button.visible = state + button:UpdateVisibility() + end + + newButton.UpdateVisibility = function(button) + local vis = button.visible and self.visible + button.item:SetIsVisible(vis) + button.shadowItem:SetIsVisible(vis) + end + + newButton.GetIsVisible = function(button) + return button.item:GetIsVisible() + end + + return newButton + +end + +function GUIChallengeLeaderboard:CreateGUIItem() + + local item = GUI.CreateItem() + US_Add(self.items, item) + + return item + +end + +function GUIChallengeLeaderboard:CreateTextItem(createShadow) + + local item = self:CreateGUIItem() + item:SetOptionFlag(GUIItem.ManageRender) + item:SetTextAlignmentY(GUIItem.Align_Center) + item:SetFontName(self.kFontName) + item:SetColor(self.kColor) + + if createShadow then + + local shadowItem = self:CreateGUIItem() + shadowItem:SetOptionFlag(GUIItem.ManageRender) + shadowItem:SetTextAlignmentY(GUIItem.Align_Center) + shadowItem:SetFontName(self.kFontName) + shadowItem:SetColor(self.kShadowColor) + + return item, shadowItem + + end + + return item + +end + +function GUIChallengeLeaderboard:CreatePlayerItem(rowIndex) + + local playerNameButton = self:CreateTextButton("", + function(button) + button.board:OnProfileButtonClicked(button.rowIndex) + end, "LEADERBOARD_TOOLTIP_PROFILE") + playerNameButton.rowIndex = rowIndex + playerNameButton.item:SetTextAlignmentX(GUIItem.Align_Min) + playerNameButton.shadowItem:SetTextAlignmentX(GUIItem.Align_Min) + + local playerIconButton = self:CreateIconButton(rowIndex) + playerIconButton.rowIndex = rowIndex + + local newPlayerEntry = {} + newPlayerEntry.nameButton = playerNameButton + newPlayerEntry.iconButton = playerIconButton + + newPlayerEntry.UpdateLayers = function(entry) + entry.nameButton:UpdateLayers() + entry.iconButton:UpdateLayers() + end + + newPlayerEntry.SetOpacity = function(entry, opacity) + entry.nameButton:SetOpacity(opacity) + entry.iconButton:SetOpacity(opacity) + end + + newPlayerEntry.SetStencilFunc = function(entry, sFunc) + entry.nameButton:SetStencilFunc(sFunc) + entry.iconButton:SetStencilFunc(sFunc) + end + + return newPlayerEntry + +end + +function GUIChallengeLeaderboard:DestroyGUIItem(item) + + GUI.DestroyItem(item) + US_Remove(self.items, item) + US_Remove(self.buttons, item) -- just in case. + +end + +function GUIChallengeLeaderboard:InitializeRow(rowIndex) + + local row = self.rows[rowIndex] + row.visible = true + + row.rankItem, row.rankShadowItem = self:CreateTextItem(true) + row.rankItem:SetTextAlignmentX(GUIItem.Align_Max) + row.rankShadowItem:SetTextAlignmentX(GUIItem.Align_Max) + row.rankItem:SetIsVisible(false) + row.rankShadowItem:SetIsVisible(false) + + row.playerItemTable = self:CreatePlayerItem(rowIndex) + row.playerItemTable.nameButton:SetIsVisible(false) + row.playerItemTable.iconButton:SetIsVisible(false) + + -- initialize the animation wipe + row.wiper = self:CreateGUIItem() + row.wiper:SetIsStencil(true) + row.wiper:SetClearsStencilBuffer(false) + +end + +function GUIChallengeLeaderboard:UpdateFontScales() + + -- we assume scale.y is the more pertinant scaling factor here. They should be the same anyways, but just in case... + local titleScale = (self.kTitleFontSize / kAgencyHugeActualSize) * self.scale.y + self.titleFontScale = Vector(titleScale, titleScale, 0) + + local regularScale = (self.kFontSize / kAgencyLargeActualSize) * self.scale.y + self.fontScale = Vector(regularScale, regularScale, 0) + + local tooltipScale = (self.kTooltipFontSize / kAgencySmallActualSize) * self.scale.y + self.tooltipFontScale = Vector(tooltipScale, tooltipScale, 0) + +end + +function GUIChallengeLeaderboard:InitGUI() + + -- Initialize title graphic + self.titleItem, self.titleShadowItem = self:CreateTextItem(true) + self.titleItem:SetTextAlignmentX(GUIItem.Align_Center) + self.titleShadowItem:SetTextAlignmentX(GUIItem.Align_Center) + self.titleItem:SetFontName(self.kTitleFontName) + self.titleShadowItem:SetFontName(self.kTitleFontName) + self.titleItem:SetText(Locale.ResolveString("LEADERBOARD")) + self.titleShadowItem:SetText(Locale.ResolveString("LEADERBOARD")) + + -- Every leaderboard table consists of at least two columns (rank, player info) followed by + -- whatever score data is used. + + -- Initialize "rank" column heading text. + self.rankHeaderItem, self.rankHeaderShadowItem = self:CreateTextItem(true) + self.rankHeaderItem:SetText(Locale.ResolveString("RANK")) + self.rankHeaderShadowItem:SetText(Locale.ResolveString("RANK")) + + -- Initialize "player" column heading text. + self.playerHeaderItem, self.playerHeaderShadowItem = self:CreateTextItem(true) + self.playerHeaderItem:SetTextAlignmentX(GUIItem.Align_Min) -- left-aligned text + self.playerHeaderShadowItem:SetTextAlignmentX(GUIItem.Align_Min) + self.playerHeaderItem:SetText(Locale.ResolveString("LEADERBOARD_PLAYER")) + self.playerHeaderShadowItem:SetText(Locale.ResolveString("LEADERBOARD_PLAYER")) + + -- Initialize per-row items + for i=1, #self.rows do + self:InitializeRow(i) + end + + -- Initialize divider between header and contents + self.dividerItem = self:CreateGUIItem() + self.dividerShadowItem = self:CreateGUIItem() + self.dividerItem:SetColor(self.kColor) + self.dividerShadowItem:SetColor(self.kShadowColor) + + -- Initialize buttons. + self.friendsButton = self:CreateSimpleButton(self.kFriendsIcon, + function(button) + button.board:SetBoardFilter("friends") + end, "LEADERBOARD_TOOLTIP_FRIENDS") + self.friendsButton.active = true + self.friendsButton:Highlight() + self.globalButton = self:CreateSimpleButton(self.kGlobalIcon, + function(button) + button.board:SetBoardFilter("global") + end, "LEADERBOARD_TOOLTIP_GLOBAL") + self.upArrowButton = self:CreateSimpleButton(self.kArrowIcon, + function(button) + button.board:OnArrowClicked("up") + end, "LEADERBOARD_TOOLTIP_ARROW") + self.upArrowButton:SetIsVisible(false) + self.downArrowButton = self:CreateSimpleButton(self.kArrowIcon, + function(button) + button.board:OnArrowClicked("down") + end, "LEADERBOARD_TOOLTIP_ARROW") + self.downArrowButton.item:SetTextureCoordinates(1.0, 1.0, 0.0, 0.0) -- rotate 180 degrees. + self.downArrowButton.shadowItem:SetTextureCoordinates(1.0, 1.0, 0.0, 0.0) -- rotate 180 degrees. + self.downArrowButton:SetIsVisible(false) + + -- Initialize personal row highlight + self.highlightItem = self:CreateGUIItem() + self.highlightItem:SetTexture(self.kHighlightGraphic) + self.highlightItem:SetBlendTechnique(GUIItem.Add) + self.highlightItem:SetIsVisible(false) -- hide until we know player is actually on board. + self.highlightItem:SetColor(self.kHighlightColor) + +end + +function GUIChallengeLeaderboard:UpdateRowLayers(index, row) + + local contentLayer = self.layer + self.kContentLayerOffset + local shadowLayer = self.layer + self.kContentShadowLayerOffset + + row.rankItem:SetLayer(contentLayer) + row.rankShadowItem:SetLayer(shadowLayer) + + row.playerItemTable:UpdateLayers() + + row.wiper:SetLayer(self.layer + self.kAnimationStencilLayerOffset) + +end + +function GUIChallengeLeaderboard:UpdateRowsLayers() + + for i=1, #self.rows do + self:UpdateRowLayers(i, self.rows[i]) + end + +end + +function GUIChallengeLeaderboard:UpdateLayers() + + local contentLayer = self.layer + self.kContentLayerOffset + local shadowLayer = self.layer + self.kContentShadowLayerOffset + + self.titleItem:SetLayer(contentLayer) + self.titleShadowItem:SetLayer(shadowLayer) + + self.rankHeaderItem:SetLayer(contentLayer) + self.rankHeaderShadowItem:SetLayer(shadowLayer) + + self.playerHeaderItem:SetLayer(contentLayer) + self.playerHeaderShadowItem:SetLayer(shadowLayer) + + self.dividerItem:SetLayer(contentLayer) + self.dividerShadowItem:SetLayer(shadowLayer) + + self:UpdateRowsLayers() + + self.friendsButton:UpdateLayers() + self.globalButton:UpdateLayers() + self.upArrowButton:UpdateLayers() + self.downArrowButton:UpdateLayers() + + self.highlightItem:SetLayer(self.layer + self.kHighlightLayerOffset) + + if self.tooltip then + + self.tooltip.back:SetLayer(self.layer + self.kTooltipBackgroundLayerOffset) + self.tooltip.backShadowBottom:SetLayer(self.layer + self.kTooltipBackgroundShadowLayerOffset) + self.tooltip.backShadowRight:SetLayer(self.layer + self.kTooltipBackgroundShadowLayerOffset) + + self.tooltip.textItem:SetLayer(self.layer + self.kTooltipTextLayerOffset) + self.tooltip.textShadowItem:SetLayer(self.layer + self.kTooltipTextShadowLayerOffset) + + end + +end + +function GUIChallengeLeaderboard:SetLayer(layer) + + self.layer = layer + self:UpdateLayers() + +end + +function GUIChallengeLeaderboard:AddSiblingScript(script) + + if US_Add(self.siblingScripts, script) then + -- we just added them to our list of siblings, make sure we're added to theirs. + if script.AddSiblingScript then + script:AddSiblingScript(self) + end + end + +end + +function GUIChallengeLeaderboard:RemoveSiblingScript(script) + + if US_Remove(self.siblingScripts, script) then + -- we just removed them from our set, make sure they remove us. + if script.RemoveSiblingScript then + script:RemoveSiblingScript(self) + end + end + + self:SetWindowActive(script, true) -- just in case this script was preventing this window from being active. + +end + +function GUIChallengeLeaderboard:SetIsVisible(state) + + self.visible = state + self:UpdateVisibility() + +end + +function GUIChallengeLeaderboard:UpdateRowVisibility(rowIndex) + + local row = self.rows[rowIndex] + local vis = self.visible and row.visible + + row.rankItem:SetIsVisible(vis) + row.rankShadowItem:SetIsVisible(vis) + row.playerItemTable.nameButton:SetIsVisible(row.visible) + row.playerItemTable.iconButton:SetIsVisible(row.visible) + +end + +function GUIChallengeLeaderboard:UpdateVisibility() + + self.titleItem:SetIsVisible(self.visible) + self.titleShadowItem:SetIsVisible(self.visible) + + self.rankHeaderItem:SetIsVisible(self.visible) + self.rankHeaderShadowItem:SetIsVisible(self.visible) + + self.playerHeaderItem:SetIsVisible(self.visible) + self.playerHeaderShadowItem:SetIsVisible(self.visible) + + self.dividerItem:SetIsVisible(self.visible) + self.dividerShadowItem:SetIsVisible(self.visible) + + for i=1, #self.rows do + self:UpdateRowVisibility(i) + end + + self.highlightItem:SetIsVisible(self.visible and self.highlightVis) + + -- update button visibilities (their update function takes into account the leaderboard's visibility) + local buttonsArray = US_GetArray(self.buttons) + for i=1, #buttonsArray do + buttonsArray[i]:UpdateVisibility() + end + +end + +function GUIChallengeLeaderboard:Initialize() + + self.filterType = "friends" -- only display friend's scores. + + -- stores entries in an array where indices are equal to rank... therefore table array will have + -- holes in the data. These holes will be filled as requested. + self.globalData = {} -- stores entries in a table associated by rank. + self.globalDataMaxEntry = -1 + + -- stores entries in a sorted order, but indices are unrelated to rank, as the friends score + -- entries will likely have holes in it (eg player is friends with rank #1, 2, 3, 5, but not 4.) + -- we initialize to nil instead of an empty table because the way steam works for fetching + -- friends-only data is all or nothing. There is no way to specify only a range of friends. + -- Therefore if friendsData is not nil, it is filled. + self.friendsData = nil + + -- the index of the entry that is displayed at the top of the list. If the filter is global, + -- this means the global rank that is displayed in row 1 of the leaderboard. If the filter is + -- friends, this means the index in the friends entry table that is displayed in row 1. If nil, + -- we revert to some default behavior. + self.displayedTopIndex = nil + self.displayedBottomIndex = nil -- will never be nil unless displayedTopIndex is also nil. + + -- the ROW INDEX of the entry that is displayed that is highlighted. NOT the entry index. + self.highlightedIndex = nil + self.highlightVis = true + + -- the global player rank of the player's entry. "nil" indicates it has not been retrieved, + -- 0 indicates the player does not have a score entry. + self.playerRank = nil + + -- table of items that make up the tooltip. + self.tooltip = nil + + -- a better name for this might be "state". + self.animation = "hidden" -- completely invisible, waiting for the go-ahead to fade-in. + self.animationTime = nil + + -- true when the next set of row data is ready to be animated on. (Sometimes it can be faster than the animation, so + -- we need to wait.) + self.nextDataReady = false + + -- tooltip hover is for the whole board, not per-button, to allow the user to hover over one button, then quickly + -- inspect the other buttons without having to wait again, which would be frustrating. + self.tooltipHoverTime = 0.0 + + -- To make cleanup easier, we keep track of which items belong to this script. + self.items = US_Create() + self.buttons = US_Create() + self.siblingScripts = US_Create() + + -- Initialize important values + self.position = Vector(0,0,0) + self.scale = Vector(1,1,1) + self.layer = kDefaultLayer + + self.windowDisabled = {} + self.windowDisabledCount = 0 + + -- Initialize empty tables, one for each row. + self.rows = {} + for i=1, self.kNumRows do + self.rows[i] = {} + end + + self:InitGUI() + -- setup stencils for row items + for i=1, #self.rows do + self:UpdateRowStencilFunc(i, GUIItem.Equal) + end + + self:SetIsVisible(true) + self:UpdateLeaderboardTransform() + self:UpdateFontScales() + self:UpdateTransform() + self:UpdateButtonsRollovers() + self:UpdateActiveData() + self:UpdateLayers() + + MouseTracker_SetIsVisible(true, nil, true) + + self.updateInterval = 0 + +end + +function GUIChallengeLeaderboard:DoWipeOutAnimation(callback) + + self.animationCallback = callback + self.animation = "out" + self.animationTime = 0.0 + + -- setup all elements of the row to only render inside the wiper, so they will be hidden + -- as the wiper slides right + for i=1, #self.rows do + self:UpdateRowStencilFunc(i, GUIItem.NotEqual) + end + +end + +function GUIChallengeLeaderboard:DoWipeInAnimation(callback) + + self.animationCallback = callback + self.animation = "in" + self.animationTime = 0.0 + + -- setup all elements of the row to be wiped out by the wiper, so they will be revealed + -- as the wiper slides right. + for i=1, #self.rows do + self:UpdateRowStencilFunc(i, GUIItem.Equal) + end + +end + +function GUIChallengeLeaderboard:OnArrowClicked(direction) + + self:DoWipeOutAnimation( + function(self) + self.animation = "waitingForDownload" + end) + + if direction == "up" then + self.displayedTopIndex = self.displayedTopIndex - self.kNumRows + elseif direction == "down" then + self.displayedTopIndex = self.displayedTopIndex + self.kNumRows + end + + self.displayedBottomIndex = self.displayedTopIndex + self.kNumRows - 1 + + self:UpdateActiveData() + + self.upArrowButton.item:SetIsVisible(false) + self.upArrowButton.shadowItem:SetIsVisible(false) + self.downArrowButton.item:SetIsVisible(false) + self.downArrowButton.shadowItem:SetIsVisible(false) + + self:Update(0) + +end + +function GUIChallengeLeaderboard:Uninitialize() + + -- Cleanup is easy because every item created by the system is in one + -- convenient set. + for i=1, #self.items.a do + GUI.DestroyItem(self.items.a[i]) + end + + MouseTracker_SetIsVisible(false) + + -- Sever our connection with any sibling scripts. + while US_GetSize(self.siblingScripts) > 0 do + self:RemoveSiblingScript(US_GetElement(self.siblingScripts, 1)) + end + +end + +function GUIChallengeLeaderboard:OnResolutionChanged() + + -- side effect: recalculates transforms for everything -- invalidating wiper positions. + self:UpdateLeaderboardTransform() + + if self.animation == "done" then + -- ensure rows are not hidden by wipers + for i=1, #self.rows do + self:UpdateRowStencilFunc(i, GUIItem.NotEqual) + end + + elseif self.animation == "waitingForDownload" then + -- ensure rows are hidden by wipers + for i=1, #self.rows do + self:UpdateRowStencilFunc(i, GUIItem.Equal) + end + + end + +end + +-- Sets the absolute screen position of the upper-left corner of this panel, in pixels. +-- (Not scaled 1080p pixels or any of that funkery... I've learned my lesson...) +-- Since it is the upper-left corner of the panel, this is not affected by scaling in any +-- way. +function GUIChallengeLeaderboard:SetPosition(position) + + self.position = position + self:UpdateTransform() + +end + +-- Sets the scaling value of this panel. Measurements provided are taken from a mockup +-- done at 1920x1080, so scale values should be calculated with this in mind. +function GUIChallengeLeaderboard:SetScale(scale) + + self.scale = scale + self:UpdateFontScales() + self:UpdateTransform() + +end + +-- Hides any graphical elements that belong to the row. +function GUIChallengeLeaderboard:HideRow(rowIndex) + + local row = self.rows[rowIndex] + row.visible = false + self:UpdateRowVisibility(rowIndex) + +end + +-- Returns the steam name associated with this steamId, or nil if it's not yet known. +function GUIChallengeLeaderboard:GetPlayerNameForSteamId(steamId) + + local result + result = GetSteamLeaderboardManager():GetSteamName(steamId) + + if result then + return result + end + + -- try again, as sometimes the name will be made available immediately + result = GetSteamLeaderboardManager():GetSteamName(steamId) + + return result -- name or nil + +end + +-- Clears the row at the given index and fills it with the supplied data. +function GUIChallengeLeaderboard:SetRowData(rowIndex, data) + + local row = self.rows[rowIndex] + + row.playerSteamId = data.steamId + if row.playerSteamId then + row.playerName = self:GetPlayerNameForSteamId(row.playerSteamId) + end + + row.rankItem:SetText(tostring(data.globalRank)) + row.rankShadowItem:SetText(tostring(data.globalRank)) + + if row.playerName then -- we might still be waiting on Steam for the player name. + row.playerItemTable.nameButton:SetText(row.playerName) + row.showName = true + else + row.showName = false + end + + row.visible = true + self:UpdateRowVisibility(rowIndex) + + row.playerItemTable.iconButton:SetTexture(self.kMissingAvatarTexture) + Client.RequestAvatarImageForPlayer(row.playerSteamId, self:GetTextureNameForAvatar(rowIndex)) + +end + +-- Clear all stored data for the leaderboard. UpdateActiveData() should be called +-- afterwards, otherwise GUI will be outdated. +function GUIChallengeLeaderboard:ClearData() + + self.globalData = {} + self.friendsData = nil + +end + +function GUIChallengeLeaderboard:SetBoardFilter(type) + + if type == "friends" then + self.friendsButton.active = true + self.friendsButton:Highlight() + self.globalButton.active = false + self.globalButton:UnHighlight() + elseif type == "global" then + self.globalButton.active = true + self.globalButton:Highlight() + self.friendsButton.active = false + self.friendsButton:UnHighlight() + end + + self:DoWipeOutAnimation(function(self) + self.animation = "waitingForDownload" + end) + + self.filterType = type + + self.upArrowButton.item:SetIsVisible(false) + self.upArrowButton.shadowItem:SetIsVisible(false) + self.downArrowButton.item:SetIsVisible(false) + self.downArrowButton.shadowItem:SetIsVisible(false) + + -- Changing filter type (or clicking the same button that's active) causes the view range to reset to default range. + self.displayedTopIndex = nil + + -- also pull double-duty and have switching types act as a flush/refresh. + self:ClearData() + self:UpdateActiveData() + + self:Update(0) + +end + +function GUIChallengeLeaderboard:CheckForButtonClicks() + + self:UpdateButtonsRollovers() + local buttonsArray = US_GetArray(self.buttons) + local button + for i=1, #buttonsArray do + if buttonsArray[i].over and buttonsArray[i].enabled and buttonsArray[i]:GetIsVisible() then + button = buttonsArray[i] + break + end + end + + if not button then + return false + end + + StartSoundEffect(self.kButtonClickSound) + button.onClick(button) + return true + +end + +function GUIChallengeLeaderboard:SendKeyEvent(input, down) + + if not self:GetIsWindowActive() then + return false + end + + -- take control of mouse movement, so they player isn't also moving their view around with the mouse visible. + -- This *should* be handled by InputHandler.lua... but... it doesn't always catch things... :( + if input == InputKey.MouseX or input == InputKey.MouseY then + return true + end + + if input == InputKey.MouseButton0 and down then + if self:CheckForButtonClicks() then + return true + end + end + + return false + +end + +-- Is anything external preventing this window from working? +function GUIChallengeLeaderboard:GetIsWindowActive() + return self.windowDisabledCount == 0 and self.visible +end + +-- keep track of things that prevent this window from being active. +function GUIChallengeLeaderboard:SetWindowActive(label, state) + + assert(label) -- label doesn't have to be a string, it can be anything unique (eg a pointer) + + if state == true and self.windowDisabled[label] then + self.windowDisabled[label] = nil + self.windowDisabledCount = self.windowDisabledCount - 1 + elseif state == false and not self.windowDisabled[label] then + self.windowDisabled[label] = true + self.windowDisabledCount = self.windowDisabledCount + 1 + end + +end + +function GUIChallengeLeaderboard:UpdateButtonRollover(button, mousePos) + + local over = false + if button.realPos + and self:GetIsWindowActive() + and self.animation == "done" + and button.item:GetIsVisible() + and mousePos.x >= button.realPos.x + and mousePos.y >= button.realPos.y + and mousePos.x <= button.realPos.x + button.realSize.x + and mousePos.y <= button.realPos.y + button.realSize.y then + over = true + end + + if button.enabled and not button.active then -- do not do any rollover effects if button is disabled. + if button.over and not over then + -- on -> off + button:UnHighlight() + elseif not button.over and over then + -- off -> on + button:Highlight() + StartSoundEffect(self.kButtonHoverSound) + end + end + + button.over = over + +end + +function GUIChallengeLeaderboard:CreateTooltip() + + local tooltip = {} + tooltip.back = self:CreateGUIItem() + tooltip.backShadowBottom = self:CreateGUIItem() + tooltip.backShadowRight = self:CreateGUIItem() + tooltip.textItem = self:CreateTextItem() + tooltip.textShadowItem = self:CreateTextItem() + + -- initialize everything invisible, we will deal with this later during the fade-in. + tooltip.back:SetColor(Color(0,0,0,0)) + tooltip.backShadowBottom:SetColor(Color(0,0,0,0)) + tooltip.backShadowRight:SetColor(Color(0,0,0,0)) + + tooltip.textItem:SetFontName(self.kTooltipFontName) + tooltip.textShadowItem:SetFontName(self.kTooltipFontName) + + tooltip.textItem:SetTextAlignmentY(GUIItem.Align_Min) + tooltip.textShadowItem:SetTextAlignmentY(GUIItem.Align_Min) + + tooltip.opacity = 0.0 + tooltip.visible = self.visible + + self.tooltip = tooltip + + self:UpdateLayers() + +end + +function GUIChallengeLeaderboard:SetTooltipText(tooltip, text) + + tooltip.rawText = text + + local maxWidth = self.kTooltipMaxWidth * self.scale.x + + tooltip.textItem:SetScale(self.tooltipFontScale) + tooltip.textShadowItem:SetScale(self.tooltipFontScale) + + local wrappedText + local numLines + wrappedText, ___, numLines = WordWrap(tooltip.textItem, text, 0, maxWidth) + tooltip.textItem:SetText(wrappedText) + tooltip.textShadowItem:SetText(wrappedText) + + -- adjust background size to fit the text. + local backSize = Vector(0,0,0) + backSize.x = (tooltip.textItem:GetTextWidth(wrappedText) * self.tooltipFontScale.x) + (self.kTooltipMargin * 2.0 * self.scale.x) + backSize.y = (self.kTooltipFontSize + (math.max(numLines - 1, 0)) * kAgencySmallLineSpan + self.kTooltipMargin + self.kTooltipMarginBottom) * self.scale.y + + local shadowOffset = self.kShadowOffset * self.scale + local bottomShadowSize = Vector(backSize.x, shadowOffset.y, 0) + local rightShadowSize = Vector(shadowOffset.x, backSize.y - shadowOffset.y, 0) + + tooltip.back:SetSize(backSize) + tooltip.backShadowBottom:SetSize(bottomShadowSize) + tooltip.backShadowRight:SetSize(rightShadowSize) + +end + +function GUIChallengeLeaderboard:SetTooltipPosition(tooltip, pos) + + -- ensure we don't move off the screen. + local screenSize = Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0) + + local shadowOffset = self.kShadowOffset * self.scale + local backSize = tooltip.back:GetSize() + + local anchor = Vector(0,0,0) + + local bottomRightCorner = pos + backSize + if bottomRightCorner.x >= screenSize.x then + anchor.x = 1 + end + + if bottomRightCorner.y >= screenSize.y then + anchor.y = 1 + end + + local offset = -(backSize * anchor) - (((anchor * 2.0) - 1.0) * self.kTooltipOffset) + + local backPos = pos + offset + tooltip.back:SetPosition(backPos) + tooltip.backShadowBottom:SetPosition(backPos + Vector(shadowOffset.x, backSize.y, 0)) + tooltip.backShadowRight:SetPosition(backPos + Vector(backSize.x, shadowOffset.y, 0)) + + local textOffset = Vector(self.kTooltipMargin, self.kTooltipMargin, 0) * self.scale + tooltip.textItem:SetPosition(backPos + textOffset) + tooltip.textShadowItem:SetPosition(backPos + textOffset + shadowOffset) + +end + +function GUIChallengeLeaderboard:UpdateTooltipColor(tooltip, opacity) + + local backColor = Color(self.kTooltipBackColor.r, self.kTooltipBackColor.g, self.kTooltipBackColor.b, self.kTooltipBackColor.a * opacity) + tooltip.back:SetColor(backColor) + + local shadowColor = Color(self.kShadowColor.r, self.kShadowColor.g, self.kShadowColor.b, self.kShadowColor.a * opacity) + tooltip.backShadowBottom:SetColor(shadowColor) + tooltip.backShadowRight:SetColor(shadowColor) + tooltip.textShadowItem:SetColor(shadowColor) + + local textColor = Color(self.kColor.r, self.kColor.g, self.kColor.b, self.kColor.a * opacity) + tooltip.textItem:SetColor(textColor) + +end + +function GUIChallengeLeaderboard:DestroyTooltip() + + local tooltip = self.tooltip + + if not tooltip then + return + end + + if tooltip.back then + self:DestroyGUIItem(tooltip.back) + tooltip.back = nil + end + + if tooltip.backShadowBottom then + self:DestroyGUIItem(tooltip.backShadowBottom) + tooltip.backShadowBottom = nil + end + + if tooltip.backShadowRight then + self:DestroyGUIItem(tooltip.backShadowRight) + tooltip.backShadowRight = nil + end + + if tooltip.textItem then + self:DestroyGUIItem(tooltip.textItem) + tooltip.textItem = nil + end + + if tooltip.textShadowItem then + self:DestroyGUIItem(tooltip.textShadowItem) + tooltip.textShadowItem = nil + end + + self.tooltip = nil + +end + +function GUIChallengeLeaderboard:UpdateTooltip(button, deltaTime, mousePos) + + -- If user is hovering over a button, start accumulating the time they've hovered, or subtract if they're + -- not hovering over a button. + if button then + local totalPersistTime = self.kTooltipDelayTime + self.kTooltipFadeInTime + self.kTooltipPersistTime + self.tooltipHoverTime = math.min(self.tooltipHoverTime + deltaTime, totalPersistTime) + else + self.tooltipHoverTime = math.max(self.tooltipHoverTime - deltaTime, 0.0) + end + + if not self.tooltip then + self:CreateTooltip() + end + + local tooltip = self.tooltip + + local tooltipText + if button then + tooltipText = button.tooltip + if button.disabledTooltip and not button.enabled then + tooltipText = button.disabledTooltip + end + end + + if tooltipText and tooltip.rawText ~= tooltipText then + self:SetTooltipText(tooltip, tooltipText) + end + + self:SetTooltipPosition(tooltip, mousePos) + + local opacity = Clamp((self.tooltipHoverTime - self.kTooltipDelayTime) / self.kTooltipFadeInTime, 0.0, 1.0) + self:UpdateTooltipColor(tooltip, opacity) + + if self.visible ~= tooltip.visible then + tooltip.visible = self.visible + tooltip.back:SetIsVisible(self.visible) + tooltip.backShadowBottom:SetIsVisible(self.visible) + tooltip.backShadowRight:SetIsVisible(self.visible) + tooltip.textItem:SetIsVisible(self.visible) + tooltip.textShadowItem:SetIsVisible(self.visible) + end + +end + +function GUIChallengeLeaderboard:UpdateButtonsRollovers(mousePos) + + if not mousePos then + mousePos = Vector(0,0,0) + mousePos.x, mousePos.y = Client.GetCursorPosScreen() + end + + local buttonsArray = US_GetArray(self.buttons) + for i=1, #buttonsArray do + self:UpdateButtonRollover(buttonsArray[i], mousePos) + end + +end + +function GUIChallengeLeaderboard:UpdatePlayerItems() + + for i=1, self.kNumRows do + + -- Check that all the player names that are needed, are loaded. + local row = self.rows[i] + local playerItems = row.playerItemTable + if not row.playerName and row.playerSteamId and playerItems.nameButton:GetIsVisible() then + row.playerName = self:GetPlayerNameForSteamId(row.playerSteamId) + if row.playerName then + playerItems.nameButton:SetText(row.playerName) + end + end + + -- Ensure garbage data isn't being displayed as the player avatar. + local avatarName = self:GetTextureNameForAvatar(i) + if Client.GetIsAvatarActiveByTextureName(avatarName) then + + playerItems.iconButton:SetTexture(avatarName) + else + + playerItems.iconButton:SetTexture(self.kMissingAvatarTexture) + end + + end + +end + +-- Update the transform of the "wiper" object to move from left to right, from totally covering row, to not at all. +-- The difference between wipe-in and wipe-out is simply the stencil mode of the row (eg is it being knocked out by +-- the wiper, or being exclusively included by the wiper?) +function GUIChallengeLeaderboard:UpdateWipeAnimationForRow(rowIndex, wipeFraction) + + local row = self.rows[rowIndex] + local wiper = row.wiper + + local newPosX = (row.wipeSize.x * wipeFraction) + row.wipePos.x + local newSizeX = row.wipeSize.x * (1.0 - wipeFraction) + + wiper:SetPosition(Vector(newPosX, row.wipePos.y, 0)) + wiper:SetSize(Vector(newSizeX, row.wipeSize.y, 0)) + +end + +function GUIChallengeLeaderboard:UpdateRowOpacity(rowIndex, opacity) + + local row = self.rows[rowIndex] + + local modifiedColor = Color(self.kColor) + modifiedColor.a = modifiedColor.a * opacity + + local modifiedShadowColor = Color(self.kShadowColor) + modifiedShadowColor.a = modifiedShadowColor.a * opacity + + row.rankItem:SetColor(modifiedColor) + row.rankShadowItem:SetColor(modifiedShadowColor) + + row.playerItemTable:SetOpacity(opacity) + +end + +function GUIChallengeLeaderboard:UpdateOpacity(opacity) + + local modifiedColor = Color(self.kColor) + modifiedColor.a = modifiedColor.a * opacity + + local modifiedShadowColor = Color(self.kShadowColor) + modifiedShadowColor.a = modifiedShadowColor.a * opacity + + self.titleItem:SetColor(modifiedColor) + self.titleShadowItem:SetColor(modifiedShadowColor) + + self.rankHeaderItem:SetColor(modifiedColor) + self.rankHeaderShadowItem:SetColor(modifiedShadowColor) + + self.playerHeaderItem:SetColor(modifiedColor) + self.playerHeaderShadowItem:SetColor(modifiedShadowColor) + + self.dividerItem:SetColor(modifiedColor) + self.dividerShadowItem:SetColor(modifiedShadowColor) + + self.friendsButton:SetOpacity(opacity) + self.globalButton:SetOpacity(opacity) + + for i=1, #self.rows do + self:UpdateRowOpacity(i, opacity) + end + + local highlightColor = Color(self.kHighlightColor) + highlightColor.a = highlightColor.a * opacity + self.highlightItem:SetColor(highlightColor) + + self.upArrowButton:SetOpacity(opacity) + self.downArrowButton:SetOpacity(opacity) + +end + +function GUIChallengeLeaderboard:UpdateAnimation(deltaTime) + + assert(self.animation ~= nil) + + if self.animation == "done" or self.animation == "hidden" then + -- nothing to do + return + + elseif self.animation == "waitingForDownload" then + + if not self.nextDataReady then + -- still waiting for downloaded data... + return + end + + self:RevealNextGUI() + + elseif self.animation == "in" or self.animation == "out" then + + assert(self.animationTime ~= nil) + + self.animationTime = self.animationTime + deltaTime + + -- total amount of time it will take for the entire animation to complete + local totalAnimationDuration = self.kWipeTime + (self.kWipeDelay * #self.rows) + + -- Update the rows for the animation + for i=1, #self.rows do + local index = i-1 + local wipeFraction = Clamp((self.animationTime - (self.kWipeDelay * index)) / self.kWipeTime, 0, 1) + + self:UpdateWipeAnimationForRow(i, wipeFraction) + end + + -- Update the highlight fade in/out for the animation + local highlightFraction = Clamp(self.animationTime / totalAnimationDuration, 0, 1) + local opacity = 1.0 + if self.animation == "in" then + opacity = highlightFraction + elseif self.animation == "out" then + opacity = 1.0 - highlightFraction + end + + local color = Color(self.kHighlightColor) + color.a = color.a * opacity + self.highlightItem:SetColor(color) + + -- Check if the animation has finished + if self.animationTime >= totalAnimationDuration then + -- the animation has completed for all rows + self.animation = "done" -- might be set to something else in the callback function. + if self.animationCallback then + self.animationCallback(self) + end + end + + elseif self.animation == "fadeIn" or self.animation == "fadeOut" then + + assert(self.animationTime ~= nil) + + self.animationTime = self.animationTime + deltaTime + + local animFraction = Clamp(self.animationTime / self.kFadeTime, 0.0, 1.0) + + local opacity + if self.animation == "fadeIn" then + opacity = animFraction + else + opacity = 1.0 - animFraction + end + + self:UpdateOpacity(opacity) + + -- Check if the animation has finished + if animFraction >= 1.0 then + self.animation = "done" -- might be set to something else in the callback function. + if self.animationCallback then + self.animationCallback(self) + return + end + + end + + end + +end + +function GUIChallengeLeaderboard:Update(deltaTime) + + -- Hide if main menu is open + local vis = not MainMenu_GetIsOpened() + if vis ~= self.visible then + self:SetIsVisible(vis) + end + + -- Player names and avatars + self:UpdatePlayerItems() + + local mousePos = Vector(0,0,0) + mousePos.x, mousePos.y = Client.GetCursorPosScreen() + + -- Update button rollovers + self:UpdateButtonsRollovers(mousePos) + + -- Update tooltip + local buttonsArray = US_GetArray(self.buttons) + local button + for i=1, #buttonsArray do + if buttonsArray[i].over then + button = buttonsArray[i] + break + end + end + + self:UpdateTooltip(button, deltaTime, mousePos) + + -- Update animations, if any. + self:UpdateAnimation(deltaTime) + +end + +-- Returns the first index of the entry with the given steamId. The maximum table size can be +-- provided to account for tables with holes in the data. +function GUIChallengeLeaderboard:FindSteamIdInEntryTable(steamId, entryTable, tableSize) + + -- Optionally provide maximum index of table (the # operator is fooled by holes in the array). + local range = tableSize + if not range then + range = #entryTable + end + + for i=1, range do + local entry = entryTable[i] + if entry and entry.steamId == steamId then + return i + end + end + + return -1 + +end + +-- Attempts to fit a window of values (values between and including startIndex and endIndex) within 1..range. +-- The window will be shifted in-bounds. (eg if startIndex is -1 and endIndex is 3, both values have 2 added to +-- them, making the range 1 and 5. If the window is too large for the range, it will be cropped to the range. +function GUIChallengeLeaderboard:ValidateWindowRange(startIndex, endIndex, range) + + if (endIndex - startIndex) + 1 > range then + return 1, range + end + + if startIndex < 1 then + endIndex = endIndex - (startIndex - 1) + startIndex = 1 + end + + if endIndex > range then + startIndex = startIndex - (endIndex - range) + endIndex = range + end + + return startIndex, endIndex + +end + +-- Calculates the displayedTopIndex based on the player's index and how many surrounding entries there +-- are. Ideally, we have half above player, and half below player (with possible leftover entry +-- sent below player), but we might not have enough entries for that. +function GUIChallengeLeaderboard:CalculateDisplayedRangeWindow(playerIndex, entryTable, tableSize) + + -- Optionally provide maximum index of table (the # operator is fooled by holes in the array). + local range = tableSize + if not range then + range = #entryTable + end + + local idealAbove = math.floor((self.kNumRows - 1) * 0.5) + local idealBelow = math.ceil((self.kNumRows - 1) * 0.5) + + local idealTop = playerIndex - idealAbove + local idealBot = playerIndex + idealBelow + + self.displayedTopIndex, self.displayedBottomIndex = self:ValidateWindowRange(idealTop, idealBot, range) + +end + +function GUIChallengeLeaderboard:GetActiveData() + + if self.filterType == "friends" then + return self.friendsData + elseif self.filterType == "global" then + return self.globalData + end + + return {} + +end + +function GUIChallengeLeaderboard:GetEntryCount() + + if self.filterType == "friends" then + return self.friendsData and #self.friendsData or 0 + elseif self.filterType == "global" then + if self.boardName and self.boardName ~= "" then + return GetSteamLeaderboardManager():GetEntryCount(self.boardName) + else + return 0 + end + end + + return 0 + +end + +function GUIChallengeLeaderboard:UpdateHighlight() + + local playerRow -- player's row index + local localSteamId = Client.GetSteamId() + + for i=1, #self.rows do + if self.rows[i].playerSteamId == localSteamId then + playerRow = i + break + end + end + + -- Update highlight + if playerRow then + self.highlightItem:SetIsVisible(self.visible) + self.highlightVis = true + local pos = ((self.kHighlightPosition + (Vector(0, self.kRowSpacing, 0) * playerRow)) - Vector(0, self.kHighlightSize.y, 0)) * self.scale + self.position + local size = self.kHighlightSize * self.scale + self.highlightItem:SetPosition(pos) + self.highlightItem:SetSize(size) + else + self.highlightItem:SetIsVisible(false) + self.highlightVis = false + end + +end + +-- Called when the wipe out animation is finished, and the next data is ready. +function GUIChallengeLeaderboard:RevealNextGUI() + + self.nextDataReady = false + + -- Update rows + local activeData = self:GetActiveData() + local numDisplayedRows = math.min((self.displayedBottomIndex - self.displayedTopIndex) + 1,10) + for i=1, numDisplayedRows do + local entryIndex = self.displayedTopIndex + i - 1 + self:SetRowData(i, activeData[entryIndex]) + end + + self:UpdateHighlight() + + -- Clear rows that do not contain data + for i=numDisplayedRows + 1, self.kNumRows do + self:HideRow(i) + end + + self:DoWipeInAnimation( + function(self) + -- Update scroll arrows + self.upArrowButton:SetIsVisible(self.displayedTopIndex > 1) + self.downArrowButton:SetIsVisible(self.displayedBottomIndex < self:GetEntryCount()) + end) + + self:Update(0) + +end + +-- Called when the displayed data has been updated, and the GUI needs to be updated to reflect this. +function GUIChallengeLeaderboard:UpdateGUI() + + self.nextDataReady = true + +end + +function GUIChallengeLeaderboard:UpdateActiveData_Friends() + + if not self.boardName then + -- Can't do anything until we know which leaderboard we're querying. + return + end + + if self.friendsData == nil then + -- Request friends data from Steam. + GetSteamLeaderboardManager():RequestFriendScores(self.boardName, + function(success, entryTable) + if success then + self.friendsData = entryTable + self:UpdateActiveData() + else + Log("ERROR: Unable to retrieve friends' scores.") + end + end) + + -- Don't do anything now, we're waiting to get back something from Steam. + return + end + + if self.displayedTopIndex == nil then + -- search for current player in list of entries. + local steamId = Client.GetSteamId() + local index = self:FindSteamIdInEntryTable(steamId, self.friendsData) + if index > 0 then + -- player's index found + self:CalculateDisplayedRangeWindow(index, self.friendsData) + self.highlightedIndex = (index - self.displayedTopIndex) + 1 + else + -- player's index not found + -- display the top scoring friends + self.displayedTopIndex = 1 + self.displayedBottomIndex = math.min(#self.friendsData, self.kNumRows) + self.highlightedIndex = nil + end + end + + -- ensure arrows haven't pushed us off the bottom of the list. + self.displayedTopIndex, self.displayedBottomIndex = self:ValidateWindowRange(self.displayedTopIndex, self.displayedBottomIndex, #self.friendsData) + + self:UpdateGUI() + +end + +function GUIChallengeLeaderboard:EntriesAreEqual(a, b) + + if a.steamId ~= b.steamId then + return false + end + + if a.globalRank ~= b.globalRank then + return false + end + + if a.score ~= b.score then + return false + end + + if a.ugcHandle ~= b.ugcHandle then + return false + end + + return true + +end + +function GUIChallengeLeaderboard:AddEntryListToGlobalList(entryList) + + local minIndex + local maxIndex + local clearAllOthers = false + for i=1, #entryList do + local newEntry = entryList[i] + local newEntryIndex = newEntry.globalRank + + minIndex = (minIndex and math.min(minIndex, newEntryIndex)) or newEntryIndex + maxIndex = (maxIndex and math.max(maxIndex, newEntryIndex)) or newEntryIndex + + local oldEntry = self.globalData + if not clearAllOthers and oldEntry ~= nil and not self:EntriesAreEqual(newEntry, oldEntry) then + clearAllOthers = true -- we found a conflict when updating some of the entries... indicating the + -- leaderboard data has changed independent of us. We'll finish copying over the data we JUST + -- received, but afterwards we'll clear everything else out so it's forced to refresh. + end + + self.globalData[newEntryIndex] = newEntry + end + + if clearAllOthers then + for i=1, minIndex - 1 do + self.globalData[i] = nil + end + + for i=maxIndex+1, self.globalDataMaxEntry do + self.globalData[i] = nil + end + + self.globalDataMaxEntry = maxIndex + + -- Clear the cached replays too, just in case a new replay was created and uploaded. + GetReplayManager():ClearCachedReplaysFromLeaderboard() + + else + self.globalDataMaxEntry = math.max(self.globalDataMaxEntry, maxIndex) + end + +end + +function GUIChallengeLeaderboard:DoGetPlayerRank() + + GetSteamLeaderboardManager():RequestPlayerScore(self.boardName, + function(success, entryTable) + if success then + if #entryTable == 0 then + -- player entry was not found + self.playerRank = 0 -- not found (ranks start at 1) + self:UpdateActiveData() + else + -- player was found + self.playerRank = entryTable[1].globalRank + self:UpdateActiveData() + end + else + Log("ERROR: Unable to retrieve user's score! (Does NOT indicate the score does not exist, but rather there was a problem with the request.)") + end + end) + +end + +function GUIChallengeLeaderboard:DoGetTopScores() + + GetSteamLeaderboardManager():RequestRangeOfScores(self.boardName, 1, self.kNumRows, + function(success, entryTable) + if success then + self:AddEntryListToGlobalList(entryTable) + + -- Don't act on the data we just received -- it may be out of date. Instead, + -- just call the update function again. It'll get everything sorted out. + self:UpdateActiveData() + return + else + Log("ERROR: Unable to retrieve top scores!") + return + end + end) + +end + +function GUIChallengeLeaderboard:ShowTopScores() + + -- See if we need to download the top scores, or if we already have them. + local entryCount = GetSteamLeaderboardManager():GetEntryCount(self.boardName) + + if not self:GetIsRangeContiguous(self.globalData, 1, math.min(self.kNumRows, entryCount)) then + -- we need to download the top scores + self:DoGetTopScores() + return + end + + -- we've already downloaded the top entries, let's display those. + self.displayedTopIndex = math.min(1, entryCount) + self.displayedBottomIndex = math.min(entryCount, self.kNumRows) + self:UpdateGUI() + +end + +function GUIChallengeLeaderboard:DoGetDisplayRangeOfScores() + + GetSteamLeaderboardManager():RequestRangeOfScores(self.boardName, self.displayedTopIndex, self.displayedBottomIndex, + function(success, entryTable) + if success then + self:AddEntryListToGlobalList(entryTable) + + -- Don't act on the data we just received -- it may be out of date. Instead, + -- just call the update function again. It'll get everything sorted out. + self:UpdateActiveData() + else + Log("ERROR: Unable to retrieve scores around player!") + end + end) + +end + +function GUIChallengeLeaderboard:ShowScoresAroundPlayer() + + local entryCount = GetSteamLeaderboardManager():GetEntryCount(self.boardName) + self:CalculateDisplayedRangeWindow(self.playerRank, self.globalData, entryCount) + if not self:GetIsRangeContiguous(self.globalData, self.displayedTopIndex, + self.displayedBottomIndex) then + -- There were holes in the data, we need to request the data from steam. + self:DoGetDisplayRangeOfScores() + return + end + + -- We've got all the entries we needed to display. + self:UpdateGUI() + +end + +function GUIChallengeLeaderboard:UpdateActiveData_GlobalInitial() + + -- we aren't displaying anything currently! Let's see if the player is on the scoreboard, + -- and if so, we'll display some entries around them. + if self.playerRank == nil then + -- we need to query Steam for the player's rank. + self:DoGetPlayerRank() + return + end + + -- Ensure the board actually has some entries to show on it + local entryCount = GetSteamLeaderboardManager():GetEntryCount(self.boardName) + if entryCount == 0 then + -- board is empty. + self.displayedTopIndex = 0 + self.displayedBottomIndex = 0 + self:UpdateGUI() + return + end + + -- We have the player's rank (or at least know that they are not present on the board) + if self.playerRank == 0 then + -- Player does not have an entry on the board, let's just show them the top scores + -- in that case. + self:ShowTopScores() + return + end + + self:ShowScoresAroundPlayer() + +end + +function GUIChallengeLeaderboard:UpdateActiveData_Global() + + if not self.boardName then + -- Can't do anything until we know which leaderboard we're querying. + return + end + + if self.displayedTopIndex == nil then + -- Setup our initial viewing position on the leaderboard. + -- (splitting this out into many functions, otherwise it becomes a huge, nightmarish "if" pyramid.) + self:UpdateActiveData_GlobalInitial() + return + end + + -- Validate the range of the viewing window, shifting it into valid space if it is out of bounds. + local entryCount = GetSteamLeaderboardManager():GetEntryCount(self.boardName) + self.displayedTopIndex, self.displayedBottomIndex = self:ValidateWindowRange(self.displayedTopIndex, self.displayedBottomIndex, entryCount) + + -- Check if we need to download any of these entries. + if not self:GetIsRangeContiguous(self.globalData, self.displayedTopIndex, self.displayedBottomIndex) then + self:DoGetDisplayRangeOfScores() + return + end + + -- We've got all the entries we needed to display. + self:UpdateGUI() + +end + +function GUIChallengeLeaderboard:GetIsRangeContiguous(entryTable, startIndex, endIndex) + + for i=startIndex, endIndex do + if entryTable[i] == nil then + return false + end + end + + return true + +end + +-- Ensure we have the data needed to display in the GUI. If so, pass it along to the GUI, +-- otherwise take whatever actions necessary to get the data. +function GUIChallengeLeaderboard:UpdateActiveData() + + if self.filterType == "friends" then + self:UpdateActiveData_Friends() + elseif self.filterType == "global" then + self:UpdateActiveData_Global() + else + Log("ERROR: invalid filter type!") + end + +end + +function GUIChallengeLeaderboard:SetSteamLeaderboardName(name) + + self.boardName = name + self:UpdateActiveData() + +end + +function GUIChallengeLeaderboard:UpdateLeaderboardTransform() + + local pos, scale = Fancy_Transform(Vector(0,0,0), 1.0) + self:SetScale(Vector(scale, scale, 1.0)) + self:SetPosition(self.kLeaderboardPosition * scale + pos) + self:UpdateHighlight() + + -- ensure we re-scale the tooltip text if applicable + if self.tooltip and self.tooltip.rawText then + self:SetTooltipText(self.tooltip, self.tooltip.rawText) + end + +end + +-- the width of the row, before scaling. +-- should be overridden +function GUIChallengeLeaderboard:GetRowWidth() + + return 0 + +end + +function GUIChallengeLeaderboard:DoFadeInAnimation(callback) + + self.animationCallback = callback + self.animation = "fadeIn" + self.animationTime = 0.0 + self:Update(0) + +end + +function GUIChallengeLeaderboard:DoFadeOutAnimation(callback) + + self.animationCallback = callback + self.animation = "fadeOut" + self.animationTime = 0.0 + self:Update(0) + +end + +function GUIChallengeLeaderboard:GetParentEntity() + return self.parentEnt +end + +-- The entity that controls the flow of the game (eg the "SkulkChallenge" entity). +-- Necessary in order to deliver callbacks to the entity so it can coordinate between the many different +-- gui scripts. +function GUIChallengeLeaderboard:SetParentEntity(ent) + self.parentEnt = ent +end diff --git a/ns2/lua/challenge/GUIChallengeLeaderboardAlien.lua b/ns2/lua/challenge/GUIChallengeLeaderboardAlien.lua new file mode 100644 index 000000000..c2172d03c --- /dev/null +++ b/ns2/lua/challenge/GUIChallengeLeaderboardAlien.lua @@ -0,0 +1,97 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengeLeaderboardAlien.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- An abstract GUIScript class that extends GUIChallengeLeaderboard to provide an alien-themed +-- leaderboard for the alien-based challenge modes. At the time of writing, this would be the +-- hive challenge, and the skulk challenge. This class should not be instantiated, but should +-- be extended to suit the specific needs of the specific challenge. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/challenge/GUIChallengeLeaderboard.lua") + +class 'GUIChallengeLeaderboardAlien' (GUIChallengeLeaderboard) + +GUIChallengeLeaderboardAlien.kBackgroundShader = "shaders/GUISmokeAlpha.surface_shader" +GUIChallengeLeaderboardAlien.kBackgroundTexture = PrecacheAsset("ui/challenge/leaderboard_background_alien.dds") +GUIChallengeLeaderboardAlien.kBackgroundNoiseTexture = PrecacheAsset("ui/alien_commander_bg_smoke.dds") +GUIChallengeLeaderboardAlien.kBackgroundCorrectionFactor = 2.5 +GUIChallengeLeaderboardAlien.kBackgroundOffset = Vector(-115, -49, 0) +GUIChallengeLeaderboardAlien.kArrowIcon = PrecacheAsset("ui/challenge/alien_arrow.dds") +GUIChallengeLeaderboardAlien.kHighlightColor = Color(234/255, 198/255, 128/255, 1) +GUIChallengeLeaderboardAlien.kColor = Color(219/255, 157/255, 35/255, 1) + +GUIChallengeLeaderboardAlien.kButtonHoverSound = PrecacheAsset("sound/NS2.fev/alien/common/alien_menu/hover") +GUIChallengeLeaderboardAlien.kButtonClickSound = PrecacheAsset("sound/NS2.fev/alien/common/alien_menu/buy_upgrade") + +GUIChallengeLeaderboardAlien.kTooltipBackColor = Color(109/255, 78/255, 17/255, 0.9) + +function GUIChallengeLeaderboardAlien:UpdateVisibility() + + GUIChallengeLeaderboard.UpdateVisibility(self) + + self.backgroundItem:SetIsVisible(self.visible) + +end + +function GUIChallengeLeaderboardAlien:UpdateTransform() + + GUIChallengeLeaderboard.UpdateTransform(self) + + local backgroundPosition = (self.kBackgroundOffset * self.scale) + self.position + self.backgroundItem:SetPosition(backgroundPosition) + local backgroundSize = Vector(self.backgroundItem:GetTextureWidth(), self.backgroundItem:GetTextureHeight(), 0) * self.scale + self.backgroundItem:SetSize(backgroundSize) + +end + +function GUIChallengeLeaderboardAlien:UpdateLayers() + + GUIChallengeLeaderboard.UpdateLayers(self) + + self.backgroundItem:SetLayer(self.layer + self.kBackgroundLayerOffset) + +end + +function GUIChallengeLeaderboardAlien:InitGUI() + + GUIChallengeLeaderboard.InitGUI(self) + + -- Background + self.backgroundItem = self:CreateGUIItem() + self.backgroundItem:SetShader(self.kBackgroundShader) + self.backgroundItem:SetTexture(self.kBackgroundTexture) + self.backgroundItem:SetAdditionalTexture("noise", self.kBackgroundNoiseTexture) + self.backgroundItem:SetFloatParameter("correctionX", self.kBackgroundCorrectionFactor * self.scale.x) + self.backgroundItem:SetFloatParameter("correctionY", self.kBackgroundCorrectionFactor * self.scale.y) + self.backgroundItem:SetFloatParameter("timeOffset", math.random() * 20) + + -- Start out completely invisible. + self.backgroundItem:SetFloatParameter("fadeStartTime", -1) + self.backgroundItem:SetFloatParameter("fadeEndTime", 0) + self.backgroundItem:SetFloatParameter("fadeTarget", 0) + +end + +function GUIChallengeLeaderboardAlien:DoFadeInAnimation(callback) + + GUIChallengeLeaderboard.DoFadeInAnimation(self, callback) + + self.backgroundItem:SetFloatParameter("fadeStartTime", Shared.GetTime()) + self.backgroundItem:SetFloatParameter("fadeEndTime", Shared.GetTime() + self.kFadeTime) + self.backgroundItem:SetFloatParameter("fadeTarget", 1.0) + +end + +function GUIChallengeLeaderboardAlien:DoFadeOutAnimation(callback) + + GUIChallengeLeaderboard.DoFadeOutAnimation(self, callback) + + self.backgroundItem:SetFloatParameter("fadeStartTime", Shared.GetTime()) + self.backgroundItem:SetFloatParameter("fadeEndTime", Shared.GetTime() + self.kFadeTime) + self.backgroundItem:SetFloatParameter("fadeTarget", 0.0) + +end \ No newline at end of file diff --git a/ns2/lua/challenge/GUIChallengeMedal.lua b/ns2/lua/challenge/GUIChallengeMedal.lua new file mode 100644 index 000000000..d19aaf743 --- /dev/null +++ b/ns2/lua/challenge/GUIChallengeMedal.lua @@ -0,0 +1,173 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengeMedal.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- An abstract GUIScript class for displaying the medals awarded for completing challenges with a good +-- enough score. Specific medals are defined in their own classes, extending from this one (defined at +-- the bottom of this file). +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +local kBlankURL = "temptemp" + +class 'GUIChallengeMedal' (GUIScript) + +-- Override for other medal types +GUIChallengeMedal.kVideoName = nil +GUIChallengeMedal.kVideoLength = 0.01 -- very short default, so it's easy to spot errors. + +-- Can override for other medal types... but probably shouldn't... +GUIChallengeMedal.kShaderName = "shaders/GUISideBySideRGBAVideo.surface_shader" +GUIChallengeMedal.kTextureName = "*medal_texture" +GUIChallengeMedal.kViewURL = "file:///ns2/web/client_game/fullscreenvideo_widget_html5.html" +GUIChallengeMedal.kVideoSize = Vector(1024, 512, 0) + +GUIChallengeMedal.kSoundEffect = "sound/NS2.fev/skulk_challenge/medal_spin" +Client.PrecacheLocalSound(GUIChallengeMedal.kSoundEffect) + +function GUIChallengeMedal:Initialize() + + self.item = GUI.CreateItem() + self.item:SetColor(Color(1,1,1,1)) + self.item:SetTexture(self.kTextureName) + self.item:SetShader(self.kShaderName) + self.item:SetIsVisible(false) + + self.webView = Client.CreateWebView(self.kVideoSize.x, self.kVideoSize.y) + self.webView:SetTargetTexture(self.kTextureName) + self.webView:SetIsVisible(false) + + self.state = "hidden-waiting" + + self.visible = true + self.itemVis = false -- keep it invisible until it's loaded + +end + +function GUIChallengeMedal:Uninitialize() + + if self.item then + GUI.DestroyItem(self.item) + self.item = nil + end + + if self.webView then + self.webView:LoadUrl(kBlankURL) + Client.DestroyWebView(self.webView) + self.webView = nil + end + + self.state = "done" + +end + +function GUIChallengeMedal:LoadAndPlay() + + self.state = "loading" + local vidJson = + { + videoUrl = self.kVideoName, + volume = 0.0, + videoWidth = self.kVideoSize.x, + videoHeight = self.kVideoSize.y, + } + self.webView:LoadUrl(self.kViewURL.."?"..json.encode(vidJson)) + +end + +-- Callback function will be called when video is done loading and begins playing. +-- Useful for playing sound effects along with the video. +function GUIChallengeMedal:SetStartCallback(callback) + + self.videoBeginCallback = callback + +end + +function GUIChallengeMedal:SetEndCallback(callback) + + self.videoEndCallback = callback + +end + +function GUIChallengeMedal:SetLayer(layer) + + self.item:SetLayer(layer) + +end + +function GUIChallengeMedal:SetPosition(pos) + + self.item:SetPosition(pos) + +end + +function GUIChallengeMedal:SetSize(size) + + self.item:SetSize(size) + +end + +function GUIChallengeMedal:SetOpacity(opacity) + + local color = Color(1,1,1,opacity) + + self.item:SetColor(color) + +end + +function GUIChallengeMedal:SetIsVisible(state) + + self.visible = state + self.item:SetIsVisible(self.visible and self.itemVis) + self.webView:SetIsVisible(self.visible and self.itemVis) + +end + +function GUIChallengeMedal:Update(deltaTime) + + if self.state == "hidden-waiting" or self.state == "done" then + return + + elseif self.state == "loading" then + if self.webView:GetUrlLoaded() then + if self.videoBeginCallback then + self.videoBeginCallback() + end + self.state = "playing" + self.itemVis = true + self.item:SetIsVisible(self.visible and self.itemVis) + self.webView:SetIsVisible(self.visible and self.itemVis) + self.playtimeRemaining = self.kVideoLength + Shared.PlaySound(nil, self.kSoundEffect) + end + + elseif self.state == "playing" then + self.playtimeRemaining = self.playtimeRemaining - deltaTime + if self.playtimeRemaining <= 0.0 then + if self.videoEndCallback then + self.videoEndCallback() + end + self.state = "done" + end + end + +end + +class 'GUIChallengeMedal_AlienBronze' (GUIChallengeMedal) +GUIChallengeMedal_AlienBronze.kVideoName = "file:///ns2/videos/challenge/medal_alien_bronze.webm" +GUIChallengeMedal_AlienBronze.kVideoLength = 70 / 24.0 -- 70 frames @ 24fps + +class 'GUIChallengeMedal_AlienSilver' (GUIChallengeMedal) +GUIChallengeMedal_AlienSilver.kVideoName = "file:///ns2/videos/challenge/medal_alien_silver.webm" +GUIChallengeMedal_AlienSilver.kVideoLength = 70 / 24.0 -- 70 frames @ 24fps + +class 'GUIChallengeMedal_AlienGold' (GUIChallengeMedal) +GUIChallengeMedal_AlienGold.kVideoName = "file:///ns2/videos/challenge/medal_alien_gold.webm" +GUIChallengeMedal_AlienGold.kVideoLength = 70 / 24.0 -- 70 frames @ 24fps + +class 'GUIChallengeMedal_AlienShadow' (GUIChallengeMedal) +GUIChallengeMedal_AlienShadow.kVideoName = "file:///ns2/videos/challenge/medal_alien_shadow.webm" +GUIChallengeMedal_AlienShadow.kVideoLength = 70 / 24.0 -- 70 frames @ 24fps + diff --git a/ns2/lua/challenge/GUIChallengePrompt.lua b/ns2/lua/challenge/GUIChallengePrompt.lua new file mode 100644 index 000000000..b322bfc65 --- /dev/null +++ b/ns2/lua/challenge/GUIChallengePrompt.lua @@ -0,0 +1,473 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengePrompt.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Nag screen to try to get players to enable their Steam Cloud settings, or report if it cannot be changed +-- due to their settings. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/UnsortedSet.lua") +Script.Load("lua/challenge/GUIChallengeButton.lua") +Script.Load("lua/menu/FancyUtilities.lua") + +class 'GUIChallengePrompt' (GUIScript) + +GUIChallengePrompt.kButtonClass = "GUIChallengeButton" + +GUIChallengePrompt.kIcons = {} -- no icons defined in base class. + +GUIChallengePrompt.kPanelSize = Vector(800, 375, 0) + +GUIChallengePrompt.kIconSpaceSize = Vector(240, 227, 0) +GUIChallengePrompt.kIconMargin = 40 -- amount of space inset into the "IconSpace" that the icon must be within. + +-- The size of the icon space inset with the margins. +GUIChallengePrompt.kIconBounds = GUIChallengePrompt.kIconSpaceSize - (Vector(GUIChallengePrompt.kIconMargin, GUIChallengePrompt.kIconMargin, 0) * 2.0) + +GUIChallengePrompt.kFontName = Fonts.kAgencyFB_Large +GUIChallengePrompt.kFontSize = 24 +GUIChallengePrompt.kFontActualSize = 28 +GUIChallengePrompt.kFontColor = Color(1,1,1,1) +GUIChallengePrompt.kFontLineSpan = 41 + +GUIChallengePrompt.kShadowColor = Color(0,0,0,0.5) +GUIChallengePrompt.kShadowOffset = Vector(2,2,0) + +GUIChallengePrompt.kDescriptionTextSpaceSize = Vector(560, 188, 0) +GUIChallengePrompt.kDescriptionTextSpacePosition = Vector(240, 0, 0) +GUIChallengePrompt.kDescriptionTextMargin = 26 + +GUIChallengePrompt.kPromptTextYOffset = 24 -- text is centered in panel with this much y-offset from center. + +-- buttons are positioned with their centers at this y-offset from the center of the panel +GUIChallengePrompt.kButtonAnchorYOffset = 130 + +GUIChallengePrompt.kTextLayerOffset = 2 +GUIChallengePrompt.kTextShadowLayerOffset = 1 +GUIChallengePrompt.kButtonLayerOffset = 1 +GUIChallengePrompt.kIconLayerOffset = 1 +GUIChallengePrompt.kDimmerLayerOffset = 0 +GUIChallengePrompt.kDefaultLayer = 40 + +GUIChallengePrompt.kFadeTime = 0.5 + +GUIChallengePrompt.kDimmerOpacity = 0.5 + +function GUIChallengePrompt:CreateGUIItem() + + local item = GUI.CreateItem() + US_Add(self.items, item) + + return item + +end + +function GUIChallengePrompt:UpdateFontScale() + + self.fontScale = (self.kFontSize / self.kFontActualSize) * self.scale.y + self.fontSize = self.kFontSize * self.fontScale + self.fontLineSpan = self.fontScale * self.kFontLineSpan + +end + +function GUIChallengePrompt:UpdateLayers() + + self.descText:SetLayer(self.layer + self.kTextLayerOffset) + self.descTextShadow:SetLayer(self.layer + self.kTextShadowLayerOffset) + self.promptText:SetLayer(self.layer + self.kTextLayerOffset) + self.promptTextShadow:SetLayer(self.layer + self.kTextShadowLayerOffset) + + if self.icon then + self.icon:SetLayer(self.layer + self.kIconLayerOffset) + end + + for i=1, #self.buttons do + self.buttons[i]:SetLayer(self.layer + self.kButtonLayerOffset) + end + + self.dimmer:SetLayer(self.layer + self.kDimmerLayerOffset) + +end + +function GUIChallengePrompt:UpdateIconTransform() + + if not self.icon then + return + end + + -- fit the texture inside the "icon bounds" + local iconBounds = self.kIconBounds * self.scale + local size = Vector(self.icon:GetTextureWidth(), self.icon:GetTextureHeight(), 0) + + -- scale down until it fits horizontally + if iconBounds.x < size.x then + size = size * (iconBounds.x / size.x) + end + + -- scale down until it fits vertically + if iconBounds.y < size.y then + size = size * (iconBounds.y / size.y) + end + + self.icon:SetSize(size) + + -- center icon in the space provided. + local centerPos = self.kIconSpaceSize * 0.5 * self.scale + self.position + self.icon:SetPosition(Vector(centerPos.x - (size.x * 0.5), centerPos.y - (size.y * 0.5), 0)) + +end + +function GUIChallengePrompt:UpdateButtonTransforms() + + local buttonsOrigin = (self.kPanelSize * 0.5 + Vector(0, self.kButtonAnchorYOffset, 0)) * self.scale + self.position + local buttonSpacing = Vector(_G[self.kButtonClass].kButtonOverSize.x * self.scale.x, 0, 0) + + local buttonFirstPos = buttonsOrigin - (buttonSpacing * 0.5 * (#self.buttons - 1)) + for i=1, #self.buttons do + self.buttons[i]:SetPosition(buttonFirstPos + (buttonSpacing * (i-1))) + self.buttons[i]:SetScale(self.scale) + end + +end + +function GUIChallengePrompt:UpdateTransform() + + local shadowOffset = self.kShadowOffset * self.scale + + self.numLines = self.numLines or 1 + local blockHeight = math.max(self.numLines - 1, 0) * self.fontLineSpan + self.fontSize + local emptySpace = self.kDescriptionTextSpaceSize.y * self.scale.y - blockHeight + local descPos = self.kDescriptionTextSpacePosition * self.scale + Vector(self.kDescriptionTextMargin * self.scale.x, emptySpace * 0.5, 0) + self.position + self.descText:SetPosition(descPos) + self.descTextShadow:SetPosition(descPos + shadowOffset) + self.descText:SetScale(Vector(self.fontScale, self.fontScale, 0)) + self.descTextShadow:SetScale(Vector(self.fontScale, self.fontScale, 0)) + + local promptPos = ((self.kPanelSize * 0.5) + Vector(0, self.kPromptTextYOffset, 0)) * self.scale + self.position + self.promptText:SetPosition(promptPos) + self.promptTextShadow:SetPosition(promptPos + shadowOffset) + self.promptText:SetScale(Vector(self.fontScale, self.fontScale, 0)) + self.promptTextShadow:SetScale(Vector(self.fontScale, self.fontScale, 0)) + + self.dimmer:SetSize(Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0)) + self.dimmer:SetPosition(Vector(0,0,0)) + + self:UpdateIconTransform() + self:UpdateButtonTransforms() + +end + +function GUIChallengePrompt:SetLayer(layer) + + self.layer = layer + self:UpdateLayers() + +end + +function GUIChallengePrompt:InitGUI() + + -- Create description text + self.descText = self:CreateGUIItem() + self.descTextShadow = self:CreateGUIItem() + + self.descText:SetText("") + self.descTextShadow:SetText("") + + self.descText:SetOptionFlag(GUIItem.ManageRender) + self.descTextShadow:SetOptionFlag(GUIItem.ManageRender) + + self.descText:SetFontName(self.kFontName) + self.descTextShadow:SetFontName(self.kFontName) + + -- Create prompt text + self.promptText = self:CreateGUIItem() + self.promptTextShadow = self:CreateGUIItem() + + self.promptText:SetOptionFlag(GUIItem.ManageRender) + self.promptTextShadow:SetOptionFlag(GUIItem.ManageRender) + + self.promptText:SetText("") + self.promptTextShadow:SetText("") + + self.promptText:SetFontName(self.kFontName) + self.promptTextShadow:SetFontName(self.kFontName) + + self.promptText:SetTextAlignmentX(GUIItem.Align_Center) + self.promptTextShadow:SetTextAlignmentX(GUIItem.Align_Center) + self.promptText:SetTextAlignmentY(GUIItem.Align_Center) + self.promptTextShadow:SetTextAlignmentY(GUIItem.Align_Center) + + self.dimmer = self:CreateGUIItem() + +end + +function GUIChallengePrompt:UpdateColor() + + local textColor = Color(self.kFontColor) + textColor.a = textColor.a * self.opacity + + self.descText:SetColor(textColor) + self.promptText:SetColor(textColor) + + local shadowColor = Color(self.kShadowColor) + shadowColor.a = shadowColor.a * self.opacity + + self.descTextShadow:SetColor(shadowColor) + self.promptTextShadow:SetColor(shadowColor) + + for i=1, #self.buttons do + self.buttons[i]:SetOpacity(self.opacity) + end + + if self.icon then + self.icon:SetColor(Color(1,1,1,self.opacity)) + end + + self.dimmer:SetColor(Color(0, 0, 0, self.opacity * self.kDimmerOpacity)) + +end + +function GUIChallengePrompt:Initialize() + + self.items = US_Create() + self.buttons = {} -- table of button scripts + + -- start faded-out + self.opacity = 0.0 + self.visState = "invisible" + self.layer = self.kDefaultLayer + self.windowDisabled = {} + self.windowDisabledCount = 0 + + self:InitGUI() + + self:ResizeForScreen() -- also calls UpdateTransform() + self:UpdateColor() + +end + +function GUIChallengePrompt:Uninitialize() + + -- Destroy button scripts + for i=1, #self.buttons do + GetGUIManager():DestroyGUIScript(self.buttons[i]) + end + + -- Destroy gui items + for i=1, #self.items.a do + GUI.DestroyItem(self.items.a[i]) + end + +end + +function GUIChallengePrompt:OnResolutionChanged() + + self:ResizeForScreen() + +end + +function GUIChallengePrompt:ClearButtons() + + for i=1, #self.buttons do + GetGUIManager():DestroyGUIScript(self.buttons[i]) + end + self.buttons = {} + + self:UpdateButtonTransforms() + +end + +function GUIChallengePrompt:AddButton(localeString, callback) + + local newButton = GetGUIManager():CreateGUIScript(self.kButtonClass) + newButton:SetText(Locale.ResolveString(localeString)) + newButton:SetCallback(callback) + newButton:SetParentScript(self) + table.insert(self.buttons, newButton) + + self:UpdateButtonTransforms() + self:UpdateLayers() + +end + +-- Disable the window with a label. Multiple things can disable the window at once, and the window will only +-- ever be active again once all those things have set the window to active again. +function GUIChallengePrompt:SetWindowActive(label, state) + + assert(label) -- label doesn't have to be a string, it can be anything unique (eg a pointer) + + if state == true and self.windowDisabled[label] then + self.windowDisabled[label] = nil + self.windowDisabledCount = self.windowDisabledCount - 1 + elseif state == false and not self.windowDisabled[label] then + self.windowDisabled[label] = true + self.windowDisabledCount = self.windowDisabledCount + 1 + end + +end + +-- Is anything preventing this window from working? +function GUIChallengePrompt:GetIsWindowActive() + return self.windowDisabledCount == 0 and self.visState == "visible" +end + +function GUIChallengePrompt:ResizeForScreen() + + _, self.scale = Fancy_Transform(Vector(0,0,0), Vector(1,1,1)) + + local screenSize = Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0) + local panelSize = self.kPanelSize * self.scale + + self.position = ((screenSize - panelSize) * 0.5) + + self:UpdateFontScale() + self:UpdateTransform() + +end + +function GUIChallengePrompt:DoCallback() + + if not self.callback then + return + end + + local tempCallback = self.callback + self.callback = nil + tempCallback(self) + +end + +function GUIChallengePrompt:UpdateVisibility(deltaTime) + + if self.visState == "fadingIn" then + + self.opacity = math.min(self.opacity + (deltaTime / self.kFadeTime), 1.0) + self:UpdateColor() + + if self.opacity == 1.0 then + self.visState = "visible" + self:DoCallback() + end + + elseif self.visState == "fadingOut" then + + self.opacity = math.max(self.opacity - (deltaTime / self.kFadeTime), 0.0) + self:UpdateColor() + + if self.opacity == 0.0 then + self.visState = "invisible" + self:DoCallback() + end + + end + +end + +function GUIChallengePrompt:Update(deltaTime) + + self:UpdateVisibility(deltaTime) + +end + +function GUIChallengePrompt:Hide(callback) + + self.callback = callback + + if self.visState ~= "visible" then + self:DoCallback() + return + end + + self.visState = "fadingOut" + +end + +function GUIChallengePrompt:Show(callback) + + self.callback = callback + + if self.visState ~= "invisible" then + self:DoCallback() + return + end + + self.visState = "fadingIn" + +end + +function GUIChallengePrompt:SetPromptTextLiteral(stringLiteral) + + self.promptText:SetText(stringLiteral) + self.promptTextShadow:SetText(stringLiteral) + +end + +function GUIChallengePrompt:SetPromptText(localeString) + + local promptResolved = Locale.ResolveString(localeString) + self.promptText:SetText(promptResolved) + self.promptTextShadow:SetText(promptResolved) + +end + +function GUIChallengePrompt:SetDescriptionText(localeString) + + local descriptionResolved = Locale.ResolveString(localeString) + local maxWidth = self.kDescriptionTextSpaceSize.x - (self.kDescriptionTextMargin * 2.0) + + local descriptionWrapped + local numLines + descriptionWrapped, _, numLines = WordWrap(self.descText, descriptionResolved, 0, maxWidth * self.scale.x) + self.descText:SetText(descriptionWrapped) + self.descTextShadow:SetText(descriptionWrapped) + self.numLines = numLines + + self:UpdateTransform() + +end + +function GUIChallengePrompt:SetIcon(name) + + if self.kIcons[name] == nil then + Log("Icon '%s' not found for prompt screen!") + return + end + + if not self.icon then + self.icon = self:CreateGUIItem() + end + + self.icon:SetTexture(self.kIcons[name]) + self:UpdateIconTransform() + self:UpdateLayers() + self:UpdateColor() + +end + +function GUIChallengePrompt:SendKeyEvent(input, down) + + -- take control of mouse movement, so they player isn't also moving their view around with the mouse visible. + -- This *should* be handled by InputHandler.lua... but... it doesn't always catch things... :( + if input == InputKey.MouseX or input == InputKey.MouseY then + return true + end + + for i=1, #self.buttons do + + if self.buttons[i]:SendKeyEventFromParent(input, down) then + return true + end + + end + + -- consume all events (don't allow main menu to open while this screen is open). + return true + +end + + + + diff --git a/ns2/lua/challenge/GUIChallengePromptAlien.lua b/ns2/lua/challenge/GUIChallengePromptAlien.lua new file mode 100644 index 000000000..b09698360 --- /dev/null +++ b/ns2/lua/challenge/GUIChallengePromptAlien.lua @@ -0,0 +1,112 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengePromptAlien.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Alien theme for the cloud nag screen. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/challenge/GUIChallengePrompt.lua") +Script.Load("lua/challenge/GUIChallengeButtonAlien.lua") + +class 'GUIChallengePromptAlien' (GUIChallengePrompt) + +GUIChallengePromptAlien.kButtonClass = "GUIChallengeButtonAlien" + +GUIChallengePromptAlien.kFontColor = Color(219/255, 157/255, 35/255, 1) + +GUIChallengePromptAlien.kIcons = GUIChallengePromptAlien.kIcons or {} +GUIChallengePromptAlien.kIcons["choice"] = PrecacheAsset("ui/challenge/sad_babbler.dds") +GUIChallengePromptAlien.kIcons["halt"] = PrecacheAsset("ui/challenge/halt_gorge.dds") + +GUIChallengePromptAlien.kBackgroundPosition = Vector(-61, -77, 0) +GUIChallengePromptAlien.kBackgroundSize = Vector(911, 536, 0) +GUIChallengePromptAlien.kBackgroundShader = "shaders/GUISmokeAlpha.surface_shader" +GUIChallengePromptAlien.kBackgroundTexture = PrecacheAsset("ui/challenge/prompt_screen_background_alien.dds") +GUIChallengePromptAlien.kBackgroundNoiseTexture = PrecacheAsset("ui/alien_commander_bg_smoke.dds") +GUIChallengePromptAlien.kBackgroundCorrectionFactor = 0.01 + +GUIChallengePromptAlien.kTextLayerOffset = 3 +GUIChallengePromptAlien.kTextShadowLayerOffset = 2 +GUIChallengePromptAlien.kButtonLayerOffset = 2 +GUIChallengePromptAlien.kIconLayerOffset = 2 +GUIChallengePromptAlien.kBackgroundLayerOffset = 1 +GUIChallengePromptAlien.kDimmerLayerOffset = 0 + +function GUIChallengePromptAlien:UpdateColor() + + GUIChallengePrompt.UpdateColor(self) + + self.back:SetColor(Color(1,1,1,self.opacity)) + +end + +function GUIChallengePromptAlien:UpdateLayers() + + GUIChallengePrompt.UpdateLayers(self) + + self.back:SetLayer(self.layer + self.kBackgroundLayerOffset) + +end + +function GUIChallengePromptAlien:UpdateTransform() + + GUIChallengePrompt.UpdateTransform(self) + + self.back:SetPosition(self.kBackgroundPosition * self.scale + self.position) + self.back:SetSize(self.kBackgroundSize * self.scale) + + local texSize = Vector(self.back:GetTextureWidth(), self.back:GetTextureHeight(), 0) + self.back:SetFloatParameter("correctionX", self.kBackgroundCorrectionFactor * texSize.x) + self.back:SetFloatParameter("correctionY", self.kBackgroundCorrectionFactor * texSize.y) + +end + +function GUIChallengePromptAlien:InitGUI() + + GUIChallengePrompt.InitGUI(self) + + self.back = self:CreateGUIItem() + self.back:SetShader(self.kBackgroundShader) + self.back:SetTexture(self.kBackgroundTexture) + self.back:SetAdditionalTexture("noise", self.kBackgroundNoiseTexture) + self.back:SetFloatParameter("timeOffset", math.random() * 20) + + -- Start out completely invisible. + self.back:SetFloatParameter("fadeStartTime", -1) + self.back:SetFloatParameter("fadeEndTime", 0) + self.back:SetFloatParameter("fadeTarget", 0) + +end + +function GUIChallengePromptAlien:Show(callback) + + local visState = self.visState + + GUIChallengePrompt.Show(self, callback) + + if visState ~= "visible" then + self.back:SetFloatParameter("fadeStartTime", Shared.GetTime()) + self.back:SetFloatParameter("fadeEndTime", Shared.GetTime() + self.kFadeTime) + self.back:SetFloatParameter("fadeTarget", 1) + end + +end + +function GUIChallengePromptAlien:Hide(callback) + + local visState = self.visState + + GUIChallengePrompt.Hide(self, callback) + + if visState ~= "invisible" then + self.back:SetFloatParameter("fadeStartTime", Shared.GetTime()) + self.back:SetFloatParameter("fadeEndTime", Shared.GetTime() + self.kFadeTime) + self.back:SetFloatParameter("fadeTarget", 0) + end + +end + + diff --git a/ns2/lua/challenge/GUIChallengeResults.lua b/ns2/lua/challenge/GUIChallengeResults.lua new file mode 100644 index 000000000..b3b52715e --- /dev/null +++ b/ns2/lua/challenge/GUIChallengeResults.lua @@ -0,0 +1,1139 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengeResults.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- An abstract GUIScript class for displaying the end-game results of a challenge mode. This is +-- extended by the GUIChallengeResultsAlien class, for alien-themed results screens. (At the time +-- of writing, there are no marine-related challenges... but it's nice to plan ahead.) +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/GUIAssets.lua") +Script.Load("lua/UnsortedSet.lua") +Script.Load("lua/challenge/GUIChallengeMedal.lua") + +class 'GUIChallengeResults' (GUIScript) + +local kDefaultLayer = 40 + +-- All of the below member-constants are encouraged to be overwritten by extended classes, where desired. +GUIChallengeResults.kColor = Color(1,1,1,1) +GUIChallengeResults.kShadowColor = Color(0,0,0,0.5) +GUIChallengeResults.kHighlightedColor = Color(1,1,1,1) + +GUIChallengeResults.kTitleFontName = Fonts.kAgencyFB_Huge +local kAgencyHugeActualSize = 66 +GUIChallengeResults.kTitleFontSize = 42 + +GUIChallengeResults.kFontName = Fonts.kAgencyFB_Large +local kAgencyLargeActualSize = 28 +GUIChallengeResults.kFontSize = 24 + +GUIChallengeResults.kButtonTextLayerOffset = 3 +GUIChallengeResults.kContentLayerOffset = 2 +GUIChallengeResults.kContentShadowLayerOffset = 1 +GUIChallengeResults.kWiperLayerOffset = 0 +GUIChallengeResults.kBackgroundLayerOffset = 0 + +GUIChallengeResults.kShadowOffset = Vector(2, 2, 0) + +GUIChallengeResults.kCommonMargin = 8 +GUIChallengeResults.kRowSpacing = 40 +GUIChallengeResults.kTitleSpacing = 20 +GUIChallengeResults.kDividerThickness = 8 + +GUIChallengeResults.kPanelSize = Vector(815, 490, 0) +GUIChallengeResults.kMedalBorderSize = 366 +GUIChallengeResults.kMedalSize = 330 + +GUIChallengeResults.kButtonSize = Vector(221, 71, 0) +GUIChallengeResults.kButtonFontSize = 20 +GUIChallengeResults.kButtonFontName = Fonts.kAgencyFB_Medium +local kAgencyMediumActualSize = 22 +GUIChallengeResults.kButtonTextColor = Color(0,0,0,1) +GUIChallengeResults.kButtonSpacing = 40 +GUIChallengeResults.kFirstButtonPosition = Vector(738, 557, 0) + +-- Over size = regular size scaled up proportionally so that it is kButtonSpacing-wider than normal. +GUIChallengeResults.kButtonOverSize = Vector(GUIChallengeResults.kButtonSize.x + GUIChallengeResults.kButtonSpacing, ((GUIChallengeResults.kButtonSize.x + GUIChallengeResults.kButtonSpacing) / GUIChallengeResults.kButtonSize.x) * GUIChallengeResults.kButtonSize.y, 0) + +GUIChallengeResults.kMedalGraphic = PrecacheAsset("ui/challenge/medal_outline.dds") +GUIChallengeResults.kMedalNameToClassName = {} -- empty here, filled in extending classes + +GUIChallengeResults.kWipeTime = 0.1 -- each row's wipe takes 0.1 seconds from start to finish +GUIChallengeResults.kWipeDelay = 0.016667 -- each row's wipe is delayed by this amount from the previous row. + +GUIChallengeResults.kFadeTime = 1.0 + +GUIChallengeResults.kResultsScreenPosition = Vector(186, 271, 0) + +GUIChallengeResults.kButtonHoverSound = PrecacheAsset("sound/NS2.fev/common/hovar") +GUIChallengeResults.kButtonClickSound = PrecacheAsset("sound/NS2.fev/common/button_click") + +GUIChallengeResults.kMedalNameLocale = +{ + bronze = "CHALLENGE_MEDAL_BRONZE", + silver = "CHALLENGE_MEDAL_SILVER", + gold = "CHALLENGE_MEDAL_GOLD", + shadow = "CHALLENGE_MEDAL_SHADOW", +} + +function GUIChallengeResults:CreateGUIItem() + + local item = GUI.CreateItem() + US_Add(self.items, item) + + return item + +end + +function GUIChallengeResults:DestroyGUIItem(item) + + GUI.DestroyItem(item) + US_Remove(self.items, item) + +end + +function GUIChallengeResults:CreateTextItem(createShadow) + + local item = self:CreateGUIItem() + item:SetOptionFlag(GUIItem.ManageRender) + item:SetTextAlignmentY(GUIItem.Align_Center) + item:SetFontName(self.kFontName) + item:SetColor(self.kColor) + + if createShadow then + + local shadowItem = self:CreateGUIItem() + shadowItem:SetOptionFlag(GUIItem.ManageRender) + shadowItem:SetTextAlignmentY(GUIItem.Align_Center) + shadowItem:SetFontName(self.kFontName) + shadowItem:SetColor(self.kShadowColor) + + return item, shadowItem + + end + + return item + +end + +-- Clears all the contents of the results rows. +function GUIChallengeResults:ClearRows() + + for i=1, #self.rows do + if self.rows[i].transformType == "content" then + self:DestroyGUIItem(self.rows[i].name.text) + self:DestroyGUIItem(self.rows[i].name.textShadow) + self:DestroyGUIItem(self.rows[i].content.text) + self:DestroyGUIItem(self.rows[i].content.textShadow) + elseif self.rows[i].transformType == "divider" then + self:DestroyGUIItem(self.rows[i].item) + end + end + + self.rows = {} + +end + +function GUIChallengeResults:SetRowStencilFunc(row, func) + + if row.name then + row.name.text:SetStencilFunc(func) + row.name.textShadow:SetStencilFunc(func) + end + + if row.content then + row.content.text:SetStencilFunc(func) + row.content.textShadow:SetStencilFunc(func) + end + + if row.item then + row.item:SetStencilFunc(func) + end + +end + +-- Adds a row to the results screen that displays a name on the left, and then some content +-- (a string) on the right. +function GUIChallengeResults:AddRow(name, content) + + local newRow = {} + newRow.name = {} + newRow.name.text, newRow.name.textShadow = self:CreateTextItem(true) + newRow.content = {} + newRow.content.text, newRow.content.textShadow = self:CreateTextItem(true) + newRow.content.text:SetTextAlignmentX(GUIItem.Align_Max) + newRow.content.textShadow:SetTextAlignmentX(GUIItem.Align_Max) + + local nameResolved = Locale.ResolveString(name) + newRow.name.text:SetText(nameResolved) + newRow.name.textShadow:SetText(nameResolved) + newRow.content.text:SetText(content) + newRow.content.textShadow:SetText(content) + + local wiper = self:CreateGUIItem() + wiper:SetIsStencil(true) + wiper:SetClearsStencilBuffer(false) + newRow.wiper = wiper + newRow.wiperVis = true + + newRow.transformType = "content" + + table.insert(self.rows, newRow) + + self:UpdateVisibility() + +end + +function GUIChallengeResults:AddButton_InitGUI(localeString, newButton) + + newButton.text = self:CreateTextItem() + newButton.text:SetFontName(self.kButtonFontName) + newButton.text:SetTextAlignmentX(GUIItem.Align_Center) + newButton.text:SetTextAlignmentY(GUIItem.Align_Center) + newButton.text:SetColor(self.kButtonTextColor) + newButton.text:SetText(Locale.ResolveString(localeString)) + +end + +function GUIChallengeResults:SetButtonText(luaName, localeString) + + self.namedButtons[luaName].text:SetText(Locale.ResolveString(localeString)) + +end + +function GUIChallengeResults:GetButtonByName(name) + + return self.namedButtons[name] + +end + +-- Adds a button to the bottom of the results screen. Should not be overridden by extending classes. +-- To add/modify buttons' appearance, override AddButton_InitGUI instead. +function GUIChallengeResults:AddButton(localeString, callback, luaName) + + local newButton = {} + newButton.over = false + newButton.enabled = true + + self:AddButton_InitGUI(localeString, newButton) + + newButton.callback = callback + newButton.luaName = luaName + + table.insert(self.buttons, newButton) + if luaName then + self.namedButtons[luaName] = newButton + end + + self:UpdateButtonTransform(#self.buttons, newButton) + + return newButton + +end + +function GUIChallengeResults:UpdateFontScales() + + local titleScale = (self.kTitleFontSize / kAgencyHugeActualSize) * self.scale.y + self.titleFontScale = Vector(titleScale, titleScale, 0) + + local regularFontScale = (self.kFontSize / kAgencyLargeActualSize) * self.scale.y + self.fontScale = Vector(regularFontScale, regularFontScale, 0) + + local buttonFontScale = (self.kButtonFontSize / kAgencyMediumActualSize) * self.scale.y + self.buttonFontScale = Vector(buttonFontScale, buttonFontScale, 0) + +end + +function GUIChallengeResults:UpdateButtonTransforms() + + for i=1, #self.buttons do + self:UpdateButtonTransform(i, self.buttons[i]) + end + +end + +function GUIChallengeResults:UpdateButtonTransform(index, button) + + -- Button Text + local pos = self.kFirstButtonPosition - Vector(((index - 1) * (self.kButtonSpacing + self.kButtonSize.x)), 0, 0) + pos.x = pos.x * self.scale.x + pos.y = pos.y * self.scale.y + pos = pos + self.position + button.text:SetPosition(pos) + button.text:SetScale(self.buttonFontScale) + + -- store this for easier over detection + button.position = pos + button.halfExtents = Vector(0,0,0) + button.halfExtents.x = self.kButtonSize.x * 0.5 * self.scale.x + button.halfExtents.y = self.kButtonSize.y * 0.5 * self.scale.y + + -- Button graphics are handled via extending classes + +end + +function GUIChallengeResults:UpdateMedalTransform() + + if not self.medalScript then + return + end + + local size = Vector(self.scale.x * self.kMedalSize, self.scale.y * self.kMedalSize, 0) + self.medalScript:SetSize(size) + + local pos = Vector(0,0,0) + pos.x = self.kMedalBorderSize * 0.5 + pos.y = self.kMedalBorderSize * 0.5 + self.kTitleSpacing + self.kTitleFontSize + pos.x = pos.x * self.scale.x + pos.y = pos.y * self.scale.y + pos = pos + self.position - (size * 0.5) + self.medalScript:SetPosition(pos) + +end + +function GUIChallengeResults:UpdateButtonVisibility(button) + + button.text:SetIsVisible(self.visible) + +end + +function GUIChallengeResults:UpdateVisibility() + + self.titleItem:SetIsVisible(self.visible) + self.titleShadowItem:SetIsVisible(self.visible) + + self.medalNameItem:SetIsVisible(self.visible) + self.medalNameShadowItem:SetIsVisible(self.visible) + self.medalNameWiper:SetIsVisible(self.visible) + + self.medalBorderItem:SetIsVisible(self.visible) + + if self.medalScript then + self.medalScript:SetIsVisible(self.visible) + end + + for i=1, #self.rows do + + local row = self.rows[i] + + if row.name then + row.name.text:SetIsVisible(self.visible) + row.name.textShadow:SetIsVisible(self.visible) + end + + if row.content then + row.content.text:SetIsVisible(self.visible) + row.content.textShadow:SetIsVisible(self.visible) + end + + if row.item then + row.item:SetIsVisible(self.visible) + end + + row.wiper:SetIsVisible(self.visible and row.wiperVis) + + end + + for i=1, #self.buttons do + self:UpdateButtonVisibility(self.buttons[i]) + end + +end + +function GUIChallengeResults:UpdateTransform() + + local shadowOffset = self.kShadowOffset * self.scale + + -- Title + local titlePosition = Vector(self.kPanelSize.x * 0.5, 0, 0) * self.scale + self.position + self.titleItem:SetPosition(titlePosition) + self.titleShadowItem:SetPosition(titlePosition + shadowOffset) + self.titleItem:SetScale(self.titleFontScale) + self.titleShadowItem:SetScale(self.titleFontScale) + + -- Medal Name + local mNamePos = Vector(self.kMedalBorderSize * 0.5, self.kPanelSize.y, 0) * self.scale + self.position + self.medalNameItem:SetPosition(mNamePos) + self.medalNameShadowItem:SetPosition(mNamePos + shadowOffset) + self.medalNameItem:SetScale(self.titleFontScale) + self.medalNameShadowItem:SetScale(self.titleFontScale) + + -- Medal Name Wiper + local mnWiperPos = Vector(0.0, self.kPanelSize.y - self.kTitleFontSize - self.kTitleSpacing, 0.0) * self.scale + self.position + local mnWiperSize = Vector(self.kMedalBorderSize, self.kTitleFontSize + self.kTitleSpacing * 2.0, 0) * self.scale + self.medalNameWiper:SetPosition(mnWiperPos) + self.medalNameWiper:SetSize(mnWiperSize) + + -- Medal Border + local mBorderPos = Vector(0.0, self.kTitleFontSize + self.kTitleSpacing, 0.0) * self.scale + self.position + self.medalBorderItem:SetPosition(mBorderPos) + self.medalBorderItem:SetSize(Vector(self.kMedalBorderSize * self.scale.x, self.kMedalBorderSize * self.scale.y, 0)) + + -- Medal Graphic + self:UpdateMedalTransform() + + -- Content Rows + local rowLeftX = self.kMedalBorderSize + self.kCommonMargin * 4.0 + local rowRightX = self.kPanelSize.x + local rowMiddleY = self.kTitleFontSize + self.kTitleSpacing + self.kRowSpacing + self.kCommonMargin * 2.0 + for i=1, #self.rows do + + local row = self.rows[i] + + if self.rows[i].transformType == "content" then + + local namePos = Vector(rowLeftX, rowMiddleY, 0) * self.scale + self.position + row.name.text:SetPosition(namePos) + row.name.textShadow:SetPosition(namePos + shadowOffset) + + local contentPos = Vector(rowRightX, rowMiddleY, 0) * self.scale + self.position + row.content.text:SetPosition(contentPos) + row.content.textShadow:SetPosition(contentPos + shadowOffset) + + row.name.text:SetScale(self.fontScale) + row.name.textShadow:SetScale(self.fontScale) + row.content.text:SetScale(self.fontScale) + row.content.textShadow:SetScale(self.fontScale) + + elseif self.rows[i].transformType == "divider" then + + local left = Vector(rowLeftX, rowMiddleY - self.kDividerThickness * 0.5, 0) * self.scale + self.position + local right = Vector(rowRightX, rowMiddleY + self.kDividerThickness * 0.5, 0) * self.scale + self.position + row.item:SetPosition(left) + row.item:SetSize(right - left) + + end + + -- update the wiper + local wiperLeftX = rowLeftX - self.kCommonMargin + local wiperRightX = rowRightX + self.kCommonMargin + local wiperPos = Vector(wiperLeftX, rowMiddleY - self.kRowSpacing * 0.5, 0) * self.scale + self.position + local wiperSize = Vector(wiperRightX - wiperLeftX, self.kRowSpacing, 0) * self.scale + row.wiper:SetPosition(wiperPos) + row.wiper:SetSize(wiperSize) + + rowMiddleY = rowMiddleY + self.kRowSpacing + + end + + -- Buttons + self:UpdateButtonTransforms() + + -- Full screen fader + self.fullscreenFade:SetPosition(Vector(0,0,0)) + self.fullscreenFade:SetSize(Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0)) + +end + +function GUIChallengeResults:ClearMedalScript() + + if self.medalScript then + GetGUIManager():DestroyGUIScript(self.medalScript) + self.medalScript = nil + end + +end + +function GUIChallengeResults:SetMedalScript(scriptName) + + if self.medalScript then + self:ClearMedalScript() + end + + self.medalScript = GetGUIManager():CreateGUIScript(scriptName) + self.medalScript:SetIsVisible(self.visible) + self:UpdateMedalTransform() + self.medalScript:SetLayer(self.layer + self.kContentLayerOffset) + + return self.medalScript + +end + +function GUIChallengeResults:SetTitle(text) + + self.titleItem:SetText(text) + self.titleShadowItem:SetText(text) + +end + +function GUIChallengeResults:UpdateButtonLayers(index, button) + + button.text:SetLayer(self.layer + self.kButtonTextLayerOffset) + +end + +function GUIChallengeResults:UpdateButtonsLayers() + + for i=1, #self.buttons do + self:UpdateButtonLayers(i, self.buttons[i]) + end + +end + +function GUIChallengeResults:UpdateLayers() + + -- start by setting everything to default content layer. + local contentLayer = self.layer + self.kContentLayerOffset + local shadowLayer = self.layer + self.kContentShadowLayerOffset + self.titleItem:SetLayer(contentLayer) + self.titleShadowItem:SetLayer(shadowLayer) + + self.medalBorderItem:SetLayer(contentLayer) + + self.medalNameItem:SetLayer(contentLayer) + self.medalNameShadowItem:SetLayer(shadowLayer) + + self.medalNameWiper:SetLayer(self.layer + self.kWiperLayerOffset) + + self:UpdateButtonsLayers() + + -- content rows + for i=1, #self.rows do + local row = self.rows[i] + + if row.transformType == "content" then + row.name.text:SetLayer(contentLayer) + row.name.textShadow:SetLayer(shadowLayer) + row.content.text:SetLayer(contentLayer) + row.content.textShadow:SetLayer(shadowLayer) + elseif row.transformType == "divider" then + row.item:SetLayer(contentLayer) + end + + row.wiper:SetLayer(self.layer + self.kWiperLayerOffset) + end + + if self.medalScript then + self.medalScript:SetLayer(contentLayer) + end + +end + +function GUIChallengeResults:SetLayer(layer) + + self.layer = layer + self:UpdateLayers() + +end + +function GUIChallengeResults:InitGUI() + + -- Initialize title graphic + self.titleItem, self.titleShadowItem = self:CreateTextItem(true) + self.titleItem:SetTextAlignmentX(GUIItem.Align_Center) + self.titleItem:SetTextAlignmentY(GUIItem.Align_Min) + self.titleShadowItem:SetTextAlignmentX(GUIItem.Align_Center) + self.titleShadowItem:SetTextAlignmentY(GUIItem.Align_Min) + self.titleItem:SetFontName(self.kTitleFontName) + self.titleShadowItem:SetFontName(self.kTitleFontName) + self.titleItem:SetText("") + self.titleShadowItem:SetText("") + + -- Initialize medal border graphic + self.medalBorderItem = self:CreateGUIItem() + self.medalBorderItem:SetTexture(self.kMedalGraphic) + self.medalBorderItem:SetColor(self.kColor) + + -- Initialize medal name text + self.medalNameItem, self.medalNameShadowItem = self:CreateTextItem(true) + self.medalNameItem:SetTextAlignmentX(GUIItem.Align_Center) + self.medalNameItem:SetTextAlignmentY(GUIItem.Align_Max) + self.medalNameShadowItem:SetTextAlignmentX(GUIItem.Align_Center) + self.medalNameShadowItem:SetTextAlignmentY(GUIItem.Align_Max) + self.medalNameItem:SetFontName(self.kTitleFontName) + self.medalNameShadowItem:SetFontName(self.kTitleFontName) + self.medalNameItem:SetText("") + self.medalNameShadowItem:SetText("") + + -- Add wiper for medal name text + local wiper = self:CreateGUIItem() + wiper:SetIsStencil(true) + wiper:SetClearsStencilBuffer(false) + self.medalNameWiper = wiper + + -- Add "quit" button + local quitButton = self:AddButton("QUIT", + function() + self:GetParentEntity():OnQuitClicked() + end) + + -- Create a black, fullscreen object for fading in/out. + -- Don't use our self-cleaning GUI method, we want this to persist for that brief frame or two between the gui being + -- destroyed, and the game changing to the loading screen. Otherwise, we'll see a brief flash from black, to game, to + -- loading screen. + self.fullscreenFade = GUI.CreateItem() + self.fullscreenFade:SetColor(Color(0,0,0,1)) + self.fullscreenFade:SetIsVisible(false) + +end + +function GUIChallengeResults:GetParentEntity() + return self.parentEnt +end + +-- The entity that controls the flow of the game (eg the "SkulkChallenge" entity). +-- Necessary in order to deliver callbacks to the entity so it can coordinate between the many different +-- gui scripts. +function GUIChallengeResults:SetParentEntity(ent) + self.parentEnt = ent +end + +function GUIChallengeResults:SetIsVisible(state) + + self.visible = state + self:UpdateVisibility() + +end + +function GUIChallengeResults:Initialize() + + -- To make cleanup easier, we keep track of which items belong to this script. + self.items = US_Create() + self.rows = {} + self.buttons = {} + self.namedButtons = {} -- easier access to buttons. + self.siblingScripts = US_Create() + + -- Initialize important values + self.position = Vector(0,0,0) + self.scale = Vector(1,1,1) + self.state = "hidden" -- waiting for fade in. + self.animationTime = 0.0 + self.animationCallback = nil + + self.windowDisabled = {} + self.windowDisabledCount = 0 + + self.layer = kDefaultLayer + + self:UpdateFontScales() + self:InitGUI() + self:UpdateResultsScreenTransform() + self:UpdateTransform() + self:SetIsVisible(true) + self:Update(0) + + MouseTracker_SetIsVisible(true, nil, true) + + self.updateInterval = 0 + +end + +function GUIChallengeResults:Uninitialize() + + for i=1, #self.items.a do + GUI.DestroyItem(self.items.a[i]) + end + + self:ClearMedalScript() + + MouseTracker_SetIsVisible(false) + + -- Sever our connection with any sibling scripts. + while US_GetSize(self.siblingScripts) > 0 do + self:RemoveSiblingScript(US_GetElement(self.siblingScripts, 1)) + end + +end + +function GUIChallengeResults:OnResolutionChanged() + + self:UpdateResultsScreenTransform() + +end + +function GUIChallengeResults:SetPosition(position) + + self.position = position + self:UpdateTransform() + +end + +function GUIChallengeResults:SetScale(scale) + + self.scale = scale + self:UpdateFontScales() + self:UpdateTransform() + +end + +function GUIChallengeResults:AddRow_Divider() + + local newDivider = {} + newDivider.item = self:CreateGUIItem() + newDivider.item:SetColor(self.kColor) + + local wiper = self:CreateGUIItem() + wiper:SetIsStencil(true) + wiper:SetClearsStencilBuffer(false) + newDivider.wiper = wiper + + newDivider.transformType = "divider" + + newDivider.wiperVis = true + + table.insert(self.rows, newDivider) + +end + +function GUIChallengeResults:AddRow_Time(row) + + if row.value then + self:AddRow(row.name, ConvertSecondsToString(row.value)) + else + self:AddRow(row.name, "--:--.--") + end + +end + +function GUIChallengeResults:AddRow_Speed(row) + + if row.value then + self:AddRow(row.name, string.format("%.2f m/s", row.value)) + else + self:AddRow(row.name, "-------") + end + +end + +function GUIChallengeResults:AddRow_Integer(row) + + if row.value then + self:AddRow(row.name, tostring(row.value)) + else + self:AddRow(row.name, "???") + end + +end + +-- Adds the requested rows to the gui. +function GUIChallengeResults:SetupResults(resultsTable) + + local rows = resultsTable.rows + -- Call the appropriate "AddRow_" function depending on the "type" of the row, as specified in the table. + for i=1, #rows do + local funcName = "AddRow_" .. rows[i].type + self[funcName](self, rows[i]) + end + + -- Ensure all newly created rows of GUI elements are hidden by the wipers + for i=1, #self.rows do + self:SetRowStencilFunc(self.rows[i], GUIItem.Equal) + end + + -- Set medal item text, and create a wiper for this too. + local medalNameResolved + if resultsTable.medalAwarded then + medalNameResolved = self.kMedalNameLocale[resultsTable.medalAwarded] + if medalNameResolved then + medalNameResolved = Locale.ResolveString(medalNameResolved) + end + end + + medalNameResolved = medalNameResolved or Locale.ResolveString("NO_MEDAL") + + self.medalNameItem:SetText(medalNameResolved) + self.medalNameShadowItem:SetText(medalNameResolved) + + self:SetTitle(Locale.ResolveString(resultsTable.title)) + + self:UpdateTransform() + self:UpdateLayers() + +end + +-- Shows the results screen (presumed to be hidden before now), and displays the results stored in the table. +function GUIChallengeResults:ShowWithResults(resultsTable, finishCallback) + + assert(resultsTable) + + self:SetupResults(resultsTable) + + self:DoFadeIn( + function(self) + self:DoWipeIn( + function(self) + if resultsTable.medalAwarded then + self:DisplayMedal(resultsTable.medalAwarded, + function() + self:DoWipeInMedalName(finishCallback) + end, nil) + else + self:DoWipeInMedalName(finishCallback) + end + end) + end) + +end + +function GUIChallengeResults:SetButtonEnabled(button, state) + + button.enabled = (state ~= false) -- default nil to true + +end + +function GUIChallengeResults:UpdateButtonRollover(button, mousePos) + + local oldState = button.over or false + + local diff = (button.position - mousePos) + if math.abs(diff.x) <= button.halfExtents.x + and math.abs(diff.y) <= button.halfExtents.y + and self.state == "done" + and self:GetIsWindowActive() + and button.enabled == true then + button.over = true + else + button.over = false + end + + if oldState == false and button.over == true then + StartSoundEffect(self.kButtonHoverSound) + end + +end + +function GUIChallengeResults:UpdateButtonRollovers() + + local mousePos = Vector(0,0,0) + mousePos.x, mousePos.y = Client.GetCursorPosScreen() + + for i=1, #self.buttons do + self:UpdateButtonRollover(self.buttons[i], mousePos) + end + +end + +function GUIChallengeResults:UpdateButtonOpacity(button, opacity) + + local fadeColor = Color(self.kButtonTextColor) + fadeColor.a = fadeColor.a * opacity + + button.text:SetColor(fadeColor) + +end + +function GUIChallengeResults:UpdateRowOpacity(row, opacity) + + local fadeColor = Color(self.kColor) + fadeColor.a = fadeColor.a * opacity + + local shadowColor = Color(self.kShadowColor) + shadowColor.a = shadowColor.a * opacity + + if row.transformType == "content" then + + row.name.text:SetColor(fadeColor) + row.name.textShadow:SetColor(shadowColor) + + row.content.text:SetColor(fadeColor) + row.content.textShadow:SetColor(shadowColor) + + elseif row.transformType == "divider" then + + row.item:SetColor(fadeColor) + + end + +end + +function GUIChallengeResults:UpdateOpacity(opacity) + + local fadeColor = Color(self.kColor) + fadeColor.a = fadeColor.a * opacity + + local shadowColor = Color(self.kShadowColor) + shadowColor.a = shadowColor.a * opacity + + self.titleItem:SetColor(fadeColor) + self.titleShadowItem:SetColor(shadowColor) + + self.medalNameItem:SetColor(fadeColor) + self.medalNameShadowItem:SetColor(shadowColor) + + self.medalBorderItem:SetColor(fadeColor) + + if self.medalScript then + self.medalScript:SetOpacity(opacity) + end + + -- update rows + for i=1, #self.rows do + self:UpdateRowOpacity(self.rows[i], opacity) + end + + -- update buttons + for i=1, #self.buttons do + self:UpdateButtonOpacity(self.buttons[i], opacity) + end + +end + +function GUIChallengeResults:UpdateWiperPosition(rowIndex, fraction) + + local rowLeftX = self.kMedalBorderSize + self.kCommonMargin * 3.0 + local rowRightX = self.kPanelSize.x + self.kCommonMargin + local rowMiddleY = self.kTitleFontSize + self.kTitleSpacing + self.kRowSpacing + self.kCommonMargin * 2.0 + rowMiddleY = rowMiddleY + (self.kRowSpacing * (rowIndex - 1)) + + local row = self.rows[rowIndex] + local wiper = row.wiper + + local wiperPos = Vector(0, rowMiddleY - self.kRowSpacing * 0.5, 0) + wiperPos.x = rowLeftX * (1.0 - fraction) + rowRightX * fraction + wiperPos = wiperPos * self.scale + self.position + local wiperSize = Vector(0, self.kRowSpacing, 0) + wiperSize.x = (rowRightX - rowLeftX) * (1.0 - fraction) + wiperSize = wiperSize * self.scale + wiper:SetPosition(wiperPos) + wiper:SetSize(wiperSize) + +end + +function GUIChallengeResults:UpdateAnimation(deltaTime) + + if self.state == "hidden" or self.state == "done" then + + return -- nothing to do + + elseif self.state == "fadeIn" or self.state == "fadeOut" then + + -- do fade animations + self.animationTime = self.animationTime + deltaTime + + if self.state == "fadeIn" then + self:UpdateOpacity(Clamp(self.animationTime / self.kFadeTime, 0, 1)) + else + self:UpdateOpacity(1.0 - Clamp(self.animationTime / self.kFadeTime, 0, 1)) + end + + -- check if animation is done + if self.animationTime >= self.kFadeTime then + self.state = "done" -- might be changed by animationCallback + if self.animationCallback then + self.animationCallback(self) + end + end + + elseif self.state == "wipeIn" then + + -- do wipe animation + self.animationTime = self.animationTime + deltaTime + + for i=1, #self.rows do + + local index = i-1 + local fraction = Clamp((self.animationTime - self.kWipeDelay * index) / self.kWipeTime, 0.0, 1.0) + self:UpdateWiperPosition(i, fraction) + + end + + -- check if animation is done + local totalAnimationTime = self.kWipeTime + (#self.rows * self.kWipeDelay) + if self.animationTime >= totalAnimationTime then + + -- hide the wipers + for i=1, #self.rows do + self.rows[i].wiper:SetIsVisible(false) + self.rows[i].wiperVis = false + end + + self.state = "done" -- might be changed by animationCallback + if self.animationCallback then + self.animationCallback(self) + end + end + + elseif self.state == "wipeInMedalName" then + + self.animationTime = self.animationTime + deltaTime + + local fraction = Clamp(self.animationTime / self.kWipeTime, 0.0, 1.0) + + local mnWiperPos = Vector(self.kMedalBorderSize * fraction, self.kPanelSize.y - self.kTitleFontSize - self.kTitleSpacing, 0.0) * self.scale + self.position + local mnWiperSize = Vector(self.kMedalBorderSize * (1.0 - fraction), self.kTitleFontSize + self.kTitleSpacing * 2.0, 0) * self.scale + self.medalNameWiper:SetPosition(mnWiperPos) + self.medalNameWiper:SetSize(mnWiperSize) + + -- check if animation is done + if self.animationTime >= self.kWipeTime then + self.state = "done" + + self.medalNameWiper:SetIsVisible(false) + + if self.animationCallback then + self.animationCallback(self) + end + end + + end + +end + +function GUIChallengeResults:Update(deltaTime) + + -- ensure this screen is hidden if menu is open + local vis = not MainMenu_GetIsOpened() + if vis ~= self.visible then + self:SetIsVisible(vis) + end + + self:UpdateButtonRollovers() + self:UpdateButtonTransforms() + self:UpdateAnimation(deltaTime) + +end + +function GUIChallengeResults:DisplayMedal(medalName, playbackStartCallback, playbackEndCallback) + + local medalGUIClassName = self.kMedalNameToClassName[medalName] + if medalGUIClassName == nil then + Log("no medal class name found for medal named \"%s\"", medalName) + return + end + + local script = self:SetMedalScript(medalGUIClassName) + script:SetStartCallback(playbackStartCallback) + script:SetEndCallback(playbackEndCallback) + script:LoadAndPlay() + +end + +-- Fades in the entire results screen (at this point, the rows would not exist yet, so just the background and medal +-- stuff). +function GUIChallengeResults:DoFadeIn(callback) + + self.state = "fadeIn" + self.animationCallback = callback + self.animationTime = 0.0 + self:Update(0) + +end + +-- Fades out everything in the results screen. +function GUIChallengeResults:DoFadeOut(callback) + + self.state = "fadeOut" + self.animationCallback = callback + self.animationTime = 0.0 + self:Update(0) + +end + +-- Does a per-row wipe-in animation from left to right. +function GUIChallengeResults:DoWipeIn(callback) + + self.state = "wipeIn" + self.animationCallback = callback + self.animationTime = 0.0 + +end + +-- Wipes in the name of the medal (presumably) just awarded. +function GUIChallengeResults:DoWipeInMedalName(callback) + + self.state = "wipeInMedalName" + self.animationCallback = callback + self.animationTime = 0.0 + +end + +function GUIChallengeResults:UpdateResultsScreenTransform() + + local pos, scale = Fancy_Transform(Vector(0,0,0), 1.0) + self:SetScale(Vector(scale, scale, 0)) + self:SetPosition(self.kResultsScreenPosition * scale + pos) + +end + +function GUIChallengeResults:RemoveSiblingScript(script) + + if US_Remove(self.siblingScripts, script) then + -- we just removed them from our set, make sure they remove us. + if script.RemoveSiblingScript then + script:RemoveSiblingScript(self) + end + end + + self:SetWindowActive(script, true) -- just in case this script was preventing this window from being active. + +end + +function GUIChallengeResults:AddSiblingScript(script) + + if US_Add(self.siblingScripts, script) then + -- we just added them to our list of siblings, make sure we're added to theirs. + if script.AddSiblingScript then + script:AddSiblingScript(self) + end + end + +end + +-- Disable the window with a label. Multiple things can disable the window at once, and the window will only +-- ever be active again once all those things have set the window to active again. +function GUIChallengeResults:SetWindowActive(label, state) + + assert(label) -- label doesn't have to be a string, it can be anything unique (eg a pointer) + + if state == true and self.windowDisabled[label] then + self.windowDisabled[label] = nil + self.windowDisabledCount = self.windowDisabledCount - 1 + elseif state == false and not self.windowDisabled[label] then + self.windowDisabled[label] = true + self.windowDisabledCount = self.windowDisabledCount + 1 + end + +end + +-- Is anything preventing this window from working? +function GUIChallengeResults:GetIsWindowActive() + return self.windowDisabledCount == 0 and self.visible == true +end + +function GUIChallengeResults:CheckForButtonClicks() + + self:UpdateButtonRollovers() + local hoverButton + for i=1, #self.buttons do + if self.buttons[i].over then + hoverButton = self.buttons[i] + end + end + + if not hoverButton then + return false + end + + StartSoundEffect(self.kButtonClickSound) + hoverButton.callback(hoverButton) + + return true + +end + +function GUIChallengeResults:SendKeyEvent(input, down) + + if not self:GetIsWindowActive() then + return false + end + + -- take control of mouse movement, so they player isn't also moving their view around with the mouse visible. + -- This *should* be handled by InputHandler.lua... but... it doesn't always catch things... :( + if input == InputKey.MouseX or input == InputKey.MouseY then + return true + end + + if input == InputKey.MouseButton0 and down then + if self:CheckForButtonClicks() then + return true + end + end + + return false + +end diff --git a/ns2/lua/challenge/GUIChallengeResultsAlien.lua b/ns2/lua/challenge/GUIChallengeResultsAlien.lua new file mode 100644 index 000000000..e24f1b2f7 --- /dev/null +++ b/ns2/lua/challenge/GUIChallengeResultsAlien.lua @@ -0,0 +1,239 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIChallengeResultsAlien.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Extension of GUIChallengeResults for an alien-themed results screen. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/challenge/GUIChallengeResults.lua") + +class 'GUIChallengeResultsAlien' (GUIChallengeResults) + +GUIChallengeResultsAlien.kButtonTexture = PrecacheAsset("ui/alien_buymenu.dds") +GUIChallengeResultsAlien.kButtonTextureCoords = {396, 428, 706, 511} +GUIChallengeResultsAlien.kButtonVeinsTextureCoords = { 600, 350, 915, 419} +local kVeinsMargin = 4 +GUIChallengeResultsAlien.kVeinsPulsePeriod = math.pi -- pulse once every two seconds. + +GUIChallengeResultsAlien.kButtonHoverSound = PrecacheAsset("sound/NS2.fev/alien/common/alien_menu/hover") +GUIChallengeResultsAlien.kButtonClickSound = PrecacheAsset("sound/NS2.fev/alien/common/alien_menu/close_menu") + +GUIChallengeResultsAlien.kButtonBackgroundLayerOffset = GUIChallengeResults.kContentLayerOffset +GUIChallengeResultsAlien.kVeinsLayerOffset = GUIChallengeResultsAlien.kButtonBackgroundLayerOffset + 1 +GUIChallengeResultsAlien.kButtonTextLayerOffset = GUIChallengeResultsAlien.kButtonBackgroundLayerOffset + 2 + +GUIChallengeResultsAlien.kButtonShader = "shaders/GUIBasicSaturation.surface_shader" + +GUIChallengeResultsAlien.kMedalNameToClassName = +{ + bronze = "GUIChallengeMedal_AlienBronze", + silver = "GUIChallengeMedal_AlienSilver", + gold = "GUIChallengeMedal_AlienGold", + shadow = "GUIChallengeMedal_AlienShadow", +} + +GUIChallengeResultsAlien.kBackgroundShader = "shaders/GUISmokeAlpha.surface_shader" +GUIChallengeResultsAlien.kBackgroundTexture = PrecacheAsset("ui/challenge/results_background_alien.dds") +GUIChallengeResultsAlien.kBackgroundNoiseTexture = PrecacheAsset("ui/alien_commander_bg_smoke.dds") +GUIChallengeResultsAlien.kBackgroundCorrectionFactor = 0.0025 +GUIChallengeResultsAlien.kBackgroundOffset = Vector(-112, -86, 0) + +GUIChallengeResultsAlien.kColor = Color(219/255, 157/255, 35/255, 1) + +function GUIChallengeResultsAlien:UpdateButtonLayers(index, button) + + GUIChallengeResults.UpdateButtonLayers(self, index, button) + + button.graphic:SetLayer(self.layer + self.kButtonBackgroundLayerOffset) + button.veins:SetLayer(self.layer + self.kVeinsLayerOffset) + +end + +function GUIChallengeResultsAlien:UpdateLayers() + + GUIChallengeResults.UpdateLayers(self) + + self.bgItem:SetLayer(self.layer + self.kBackgroundLayerOffset) + +end + +function GUIChallengeResultsAlien:UpdateButtonTransform(index, button) + + GUIChallengeResults.UpdateButtonTransform(self, index, button) + + local size + if button.over then + size = Vector(self.kButtonOverSize) + else + size = Vector(self.kButtonSize) + end + + local veinsSize = size - (Vector(kVeinsMargin, kVeinsMargin, 0) * 2.0) + + size.x = size.x * self.scale.x + size.y = size.y * self.scale.y + veinsSize.x = veinsSize.x * self.scale.x + veinsSize.y = veinsSize.y * self.scale.y + + local graphicPos = button.position - (size * 0.5) + local veinsPos = button.position - (veinsSize * 0.5) + + button.graphic:SetPosition(graphicPos) + button.graphic:SetSize(size) + + button.veins:SetPosition(veinsPos) + button.veins:SetSize(veinsSize) + +end + +function GUIChallengeResultsAlien:UpdateButtonVisibility(button) + + GUIChallengeResults.UpdateButtonVisibility(self, button) + + button.graphic:SetIsVisible(self.visible) + button.veins:SetIsVisible(self.visible) + +end + +function GUIChallengeResultsAlien:UpdateVisibility() + + GUIChallengeResults.UpdateVisibility(self) + + self.bgItem:SetIsVisible(self.visible) + +end + +function GUIChallengeResultsAlien:UpdateTransform() + + GUIChallengeResults.UpdateTransform(self) + + local pos = Vector(self.kBackgroundOffset) + pos.x = pos.x * self.scale.x + pos.y = pos.y * self.scale.y + pos = pos + self.position + self.bgItem:SetPosition(pos) + + local size = Vector(self.bgItem:GetTextureWidth(), self.bgItem:GetTextureHeight(), 0) + size.x = size.x * self.scale.x + size.y = size.y * self.scale.y + self.bgItem:SetSize(size) + + local texSize = Vector(self.bgItem:GetTextureWidth(), self.bgItem:GetTextureHeight(), 0) + self.bgItem:SetFloatParameter("correctionX", self.kBackgroundCorrectionFactor * texSize.x ) + self.bgItem:SetFloatParameter("correctionY", self.kBackgroundCorrectionFactor * texSize.y ) + +end + +function GUIChallengeResultsAlien:InitGUI() + + GUIChallengeResults.InitGUI(self) + + -- Smokey background + self.bgItem = self:CreateGUIItem() + self.bgItem:SetShader(self.kBackgroundShader) + self.bgItem:SetTexture(self.kBackgroundTexture) + self.bgItem:SetAdditionalTexture("noise", self.kBackgroundNoiseTexture) + + local texSize = Vector(self.bgItem:GetTextureWidth(), self.bgItem:GetTextureHeight(), 0) + self.bgItem:SetFloatParameter("correctionX", self.kBackgroundCorrectionFactor * texSize.x ) + self.bgItem:SetFloatParameter("correctionY", self.kBackgroundCorrectionFactor * texSize.y ) + self.bgItem:SetFloatParameter("timeOffset", math.random() * 20) + + -- Start out completely invisible + self.bgItem:SetFloatParameter("fadeStartTime", -1) + self.bgItem:SetFloatParameter("fadeEndTime", 0) + self.bgItem:SetFloatParameter("fadeTarget", 0) + +end + +function GUIChallengeResultsAlien:SetButtonEnabled(button, state) + + GUIChallengeResults.SetButtonEnabled(self, button, state) + + button.graphic:SetFloatParameter("saturation", button.enabled and 1.0 or 0.0) + button.veins:SetFloatParameter("saturation", button.enabled and 1.0 or 0.0) + +end + +function GUIChallengeResultsAlien:AddButton_InitGUI(localeString, newButton) + + GUIChallengeResults.AddButton_InitGUI(self, localeString, newButton) + + local graphic = self:CreateGUIItem() + graphic:SetTexture(self.kButtonTexture) + graphic:SetTexturePixelCoordinates(unpack(self.kButtonTextureCoords)) + graphic:SetShader(self.kButtonShader) + graphic:SetFloatParameter("saturation", 1.0) + newButton.graphic = graphic + + local veins = self:CreateGUIItem() + veins:SetTexture(self.kButtonTexture) + veins:SetTexturePixelCoordinates(unpack(self.kButtonVeinsTextureCoords)) + veins:SetShader(self.kButtonShader) + veins:SetFloatParameter("saturation", 1.0) + newButton.veins = veins + + newButton.veinsPulse = 0 + +end + +function GUIChallengeResultsAlien:Update(deltaTime) + + GUIChallengeResults.Update(self, deltaTime) + + for i=1, #self.buttons do + + local button = self.buttons[i] + + button.veinsPulse = self.buttons[i].veinsPulse + deltaTime + + if button.over then + button.veinsPulse = 0 + elseif not button.enabled then + button.veinsPulse = self.kVeinsPulsePeriod * 0.5 + end + + local opacity = math.cos(button.veinsPulse * self.kVeinsPulsePeriod) * 0.5 + 0.5 + button.veins:SetColor(Color(1,1,1,opacity)) + + end + +end + +function GUIChallengeResultsAlien:UpdateButtonOpacity(button, opacity) + + GUIChallengeResults.UpdateButtonOpacity(self, button, opacity) + + local fadeColor = Color(1,1,1,opacity) + + button.graphic:SetColor(fadeColor) + button.veins:SetColor(fadeColor) + +end + +function GUIChallengeResultsAlien:DoFadeIn(callback) + + GUIChallengeResults.DoFadeIn(self, callback) + + self.bgItem:SetFloatParameter("fadeStartTime", Shared.GetTime()) + self.bgItem:SetFloatParameter("fadeEndTime", Shared.GetTime() + self.kFadeTime) + self.bgItem:SetFloatParameter("fadeTarget", 1.0) + +end + +function GUIChallengeResultsAlien:DoFadeOut(callback) + + GUIChallengeResults.DoFadeOut(self, callback) + + self.bgItem:SetFloatParameter("fadeStartTime", Shared.GetTime()) + self.bgItem:SetFloatParameter("fadeEndTime", Shared.GetTime() + self.kFadeTime) + self.bgItem:SetFloatParameter("fadeTarget", 0.0) + +end + + + + diff --git a/ns2/lua/challenge/GUIReplayDownloader.lua b/ns2/lua/challenge/GUIReplayDownloader.lua new file mode 100644 index 000000000..5c7955268 --- /dev/null +++ b/ns2/lua/challenge/GUIReplayDownloader.lua @@ -0,0 +1,569 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIReplayDownloader.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- An abstract GUIScript class that downloads a replay for a challenge mode, and displays the download +-- progress. Extended by GUIReplayDownloaderAlien, replay downloader with alien theme. Most +-- of the functionality is defined in here, though.. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/challenge/SteamLeaderboardManager.lua") +Script.Load("lua/UnsortedSet.lua") +Script.Load("lua/GUIAssets.lua") + +local kDefaultLayer = 40 + +class 'GUIReplayDownloader' (GUIScript) + +GUIReplayDownloader.kColor = Color(1,1,1,1) +GUIReplayDownloader.kShadowColor = Color(0,0,0,0.5) +GUIReplayDownloader.kDarkenColor = Color(0,0,0,0.5) + +GUIReplayDownloader.kShadowOffset = Vector(2, 2, 0) + +GUIReplayDownloader.kTitleFontName = Fonts.kAgencyFB_Huge +GUIReplayDownloader.kTitleFontActualSize = 66 +GUIReplayDownloader.kTitleFontSize = 42 + +GUIReplayDownloader.kButtonFontName = Fonts.kAgencyFB_Medium +GUIReplayDownloader.kButtonFontActualSize = 22 +GUIReplayDownloader.kButtonFontSize = 20 +GUIReplayDownloader.kButtonFontColor = Color(1,1,1,1) + +GUIReplayDownloader.kButtonTextLayerOffset = 0 +GUIReplayDownloader.kButtonLayerOffset = 3 + +GUIReplayDownloader.kProgressBarLayerOffset = 2 + +GUIReplayDownloader.kStatusTextLayerOffset = 3 +GUIReplayDownloader.kStatusTextShadowLayerOffset = 2 + +GUIReplayDownloader.kBackgroundLayerOffset = 1 +GUIReplayDownloader.kDarkOverlayLayerOffset = 0 + +GUIReplayDownloader.kPanelSize = Vector(626, 288, 0) +GUIReplayDownloader.kTitleYOffset = 62 +GUIReplayDownloader.kButtonCenterYOffset = 220 + +GUIReplayDownloader.kButtonSize = Vector(233, 86, 0) +GUIReplayDownloader.kButtonSpacing = 40 + +-- Over size = regular size scaled up proportionally so that it is kButtonSpacing-wider than normal. +GUIReplayDownloader.kButtonOverSize = Vector(GUIReplayDownloader.kButtonSize.x + GUIReplayDownloader.kButtonSpacing, ((GUIReplayDownloader.kButtonSize.x + GUIReplayDownloader.kButtonSpacing) / GUIReplayDownloader.kButtonSize.x) * GUIReplayDownloader.kButtonSize.y, 0) + +GUIReplayDownloader.kFadeTime = 0.5 + +GUIReplayDownloader.kButtonHoverSound = PrecacheAsset("sound/NS2.fev/common/hovar") +GUIReplayDownloader.kButtonClickSound = PrecacheAsset("sound/NS2.fev/common/button_click") + +function GUIReplayDownloader:UpdateFontScales() + + self.titleFontScale = self.scale * (self.kTitleFontSize / self.kTitleFontActualSize) + self.buttonFontScale = self.scale * (self.kButtonFontSize / self.kButtonFontActualSize) + +end + +function GUIReplayDownloader:UpdateButtonTransform(button) + + button.textItem:SetPosition(button.position) + button.textItem:SetScale(self.buttonFontScale) + + -- store this for easier rollover detection + button.halfExtents = self.kButtonSize * 0.5 * self.scale + +end + +function GUIReplayDownloader:UpdateProgressFraction() + + if not self.ugcHandle then + self.progressFraction = 0.0 + return + end + + if self.downloadedFileSize then + -- an indication that download has completed + self.progressFraction = 1.0 + end + + local fraction = Client.GetUGCDownloadProgress(self.ugcHandle) + + if fraction >= 0.0 then + self.progressFraction = fraction + end + +end + +function GUIReplayDownloader:UpdateProgressBarFill() + -- Will be extended for themeing... +end + +function GUIReplayDownloader:UpdateProgressBarTransform() + -- Will be extended for themeing... +end + +function GUIReplayDownloader:UpdateLayers() + + self.statusTextItem:SetLayer(self.layer + self.kStatusTextLayerOffset) + self.statusTextShadowItem:SetLayer(self.layer + self.kStatusTextShadowLayerOffset) + + self.progressBar:SetLayer(self.layer + self.kProgressBarLayerOffset) + + self.darkenItem:SetLayer(self.layer + self.kDarkOverlayLayerOffset) + + for i=1, #self.buttons do + self.buttons[i]:SetLayer(self.layer + self.kButtonLayerOffset) + end + +end + +function GUIReplayDownloader:UpdateTransform() + + local shadowOffset = self.kShadowOffset * self.scale + + -- Update buttons + local buttonCenterPos = Vector(self.kPanelSize.x * 0.5, self.kButtonCenterYOffset, 0) + local offsetFraction = -0.5 * (#self.buttons - 1) -- buttons are positioned by their centers. + local xOffset = offsetFraction * self.kButtonOverSize.x + + for i=1, #self.buttons do + local index = i-1 + local pos = ((buttonCenterPos + Vector(xOffset + (self.kButtonOverSize.x * index), 0, 0)) * self.scale) + self.position + self.buttons[i].position = pos + self:UpdateButtonTransform(self.buttons[i]) + end + + -- Update status text + local titlePos = (Vector(self.kPanelSize.x * 0.5, self.kTitleYOffset, 0) * self.scale) + self.position + self.statusTextItem:SetPosition(titlePos) + self.statusTextShadowItem:SetPosition(titlePos + shadowOffset) + self.statusTextItem:SetScale(self.titleFontScale) + self.statusTextShadowItem:SetScale(self.titleFontScale) + + self.darkenItem:SetSize(Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0)) + self.darkenItem:SetPosition(Vector(0, 0, 0)) + + self:UpdateProgressBarTransform() + +end + +function GUIReplayDownloader:GetOpacity() + + if self.fadeIn ~= nil then + return Clamp(self.fadeIn / self.kFadeTime, 0, 1) + elseif self.fadeOut ~= nil then + return Clamp(self.fadeOut / self.kFadeTime, 0, 1) + elseif self.status == "active" then + return 1 + end + + return 0 + +end + +function GUIReplayDownloader:UpdateColor() + + local opacity = self:GetOpacity() + + local shadowColor = Color(self.kShadowColor.r, self.kShadowColor.g, self.kShadowColor.b, self.kShadowColor.a * opacity) + local color = Color(self.kColor.r, self.kColor.g, self.kColor.b, self.kColor.a * opacity) + local darkenColor = Color(self.kDarkenColor.r, self.kDarkenColor.g, self.kDarkenColor.b, self.kDarkenColor.a * opacity) + + self.statusTextItem:SetColor(color) + self.statusTextShadowItem:SetColor(shadowColor) + self.darkenItem:SetColor(darkenColor) + + local buttonTextColor = Color(self.kButtonFontColor.r, self.kButtonFontColor.g, self.kButtonFontColor.b, self.kButtonFontColor.a * opacity) + + for i=1, #self.buttons do + self.buttons[i].textItem:SetColor(buttonTextColor) + end + +end + +function GUIReplayDownloader:InitProgressBarGUI() + -- Will be extended for themeing... +end + +function GUIReplayDownloader:CreateProgressBar() + + self.progressBar = {} + + self:InitProgressBarGUI() + +end + +function GUIReplayDownloader:InitButton(newButton, buttonText) + + local text = self:CreateTextItem() + text:SetColor(self.kButtonFontColor) + text:SetFontName(self.kButtonFontName) + text:SetText(Locale.ResolveString(buttonText)) + newButton.textItem = text + + newButton.SetLayer = function(button, layer) + button.textItem:SetLayer(layer + self.kButtonTextLayerOffset) + end + +end + +function GUIReplayDownloader:CreateButton(buttonText, onClick) + + local newButton = {} + newButton.over = false + newButton.onClick = onClick + + self:InitButton(newButton, buttonText) + + table.insert(self.buttons, 1, newButton) -- add new buttons on the left. + + self:UpdateTransform() + self:UpdateLayers() + self:UpdateColor() + + return newButton + +end + +function GUIReplayDownloader:SetupCallback(name, callback) + + self.callbacks[name] = callback + +end + +function GUIReplayDownloader:DoFadeIn() + + self.status = "fadingIn" + self.fadeIn = 0.0 + +end + +function GUIReplayDownloader:DoFadeOut() + + self.status = "fadingOut" + self.fadeOut = self.kFadeTime + +end + +function GUIReplayDownloader:OnCancelClicked() + + self:DoFadeOut() + self.result = "cancel" + +end + +function GUIReplayDownloader:OnOkClicked() + + self.result = "ok" + if self.callbacks.ok then + self.callbacks.ok() + end + +end + +function GUIReplayDownloader:InitializeGUI() + + self.statusTextItem, self.statusTextShadowItem = self:CreateTextItem(true) + local status = Locale.ResolveString("DOWNLOADING_REPLAY") + self.statusTextItem:SetText(status) + self.statusTextShadowItem:SetText(status) + + self:CreateProgressBar() + + self.darkenItem = self:CreateGUIItem() + self.darkenItem:SetColor(self.kDarkenColor) + + self.cancelButton = self:CreateButton("CANCEL", function() self:OnCancelClicked() end) + +end + +function GUIReplayDownloader:Initialize() + + self.items = US_Create() + self.buttons = {} + + self.callbacks = {} + + self.status = "inactive" + self.result = "cancel" + self.okButtonText = "OK" -- can be changed, eg to "View Replay" + + self.position = Vector(0,0,0) + self.scale = Vector(1,1,1) + self.layer = kDefaultLayer + + self:UpdateFontScales() + + self:InitializeGUI() + + self:UpdateTransform() + self:UpdateLayers() + self:UpdateColor() + self:UpdateProgressFraction() + + self.updateInterval = 0 + +end + +function GUIReplayDownloader:DestroyButton(button) + + local index + for i=1, #self.buttons do + if self.buttons[i] == button then + index = i + break + end + end + + if index then + table.remove(self.buttons, index) + end + + self:DestroyGUIItem(button.textItem) + +end + +function GUIReplayDownloader:OnRetryClicked() + + self:DestroyButton(self.retryButton) + self:BeginDownloadingUGC(self.ugcHandle) + +end + +function GUIReplayDownloader:SetOkButtonText(text) + + self.okButtonText = text + +end + +function GUIReplayDownloader:OnDownloadComplete(success, handleOrError, fileSize) + + if success then + + self.downloadedFileSize = fileSize + + local status = Locale.ResolveString("DOWNLOAD_COMPLETE") + self.statusTextItem:SetText(status) + self.statusTextShadowItem:SetText(status) + + if self.callbacks.ok then + self:CreateButton(self.okButtonText, function() self:OnOkClicked() end) + end + + else + Log("UGC download failed! (Error code = %s)", handleOrError) + local status = Locale.ResolveString("DOWNLOAD_FAILED") + self.statusTextItem:SetText(status) + self.statusTextShadowItem:SetText(status) + + if not self.retryButton then + self:CreateButton("RETRY", function() self:OnRetry() end) + end + + end + +end + +function GUIReplayDownloader:BeginDownloadingUGC(handle) + + self.ugcHandle = handle + GetSteamLeaderboardManager():DownloadUGC(handle, function(success, handleOrError, fileSize) self:OnDownloadComplete(success, handleOrError, fileSize) end) + self:DoFadeIn() + +end + +function GUIReplayDownloader:Terminate() + + self.status = "terminated" + + if self.callbacks.terminated then + self.callbacks.terminated() + end + + GetGUIManager():DestroyGUIScript(self) + +end + +function GUIReplayDownloader:Uninitialize() + + -- Cleanup is easy because every item created by the system is in one + -- convenient set. + for i=1, #self.items.a do + GUI.DestroyItem(self.items.a[i]) + end + + self.buttons = {} + +end + +function GUIReplayDownloader:SendKeyEvent(key, down) + + -- take control of mouse movement, so they player isn't also moving their view around with the mouse visible. + -- This *should* be handled by InputHandler.lua... but... it doesn't always catch things... :( + if input == InputKey.MouseX or input == InputKey.MouseY then + return true + end + + self:UpdateButtonRollovers() + if key == InputKey.MouseButton0 and down then + for i=1, #self.buttons do + if self.buttons[i].over then + StartSoundEffect(self.kButtonClickSound) + self.buttons[i].onClick() + self:UpdateButtonRollovers() -- to disable buttons if status changed. + return true + end + end + end + + return true -- consume everything + +end + +function GUIReplayDownloader:UpdateButtonRollover(button, mousePos) + + if self.status ~= "active" then + button.over = false + return + end + + local oldOver = button.over + + local diff = (button.position - mousePos) + if math.abs(diff.x) <= button.halfExtents.x and math.abs(diff.y) <= button.halfExtents.y then + button.over = true + if not oldOver then + StartSoundEffect(self.kButtonHoverSound) + end + else + button.over = false + end + + if oldOver ~= button.over then + self:UpdateButtonTransform(button) + end + +end + +function GUIReplayDownloader:UpdateButtonRollovers() + + local mousePos = Vector(0,0,0) + mousePos.x, mousePos.y = Client.GetCursorPosScreen() + + for i=1, #self.buttons do + self:UpdateButtonRollover(self.buttons[i], mousePos) + end + +end + +function GUIReplayDownloader:Update(deltaTime) + + self:UpdateButtonRollovers() + self:UpdateProgressFraction() + self:UpdateProgressBarFill() + + if self.status == "fadingOut" then + + self.fadeOut = self.fadeOut - deltaTime + + self:UpdateColor() + + if self.fadeOut <= 0.0 then + self:Terminate() + end + + elseif self.status == "fadingIn" then + + self.fadeIn = self.fadeIn + deltaTime + + self:UpdateColor() + + if self.fadeIn >= self.kFadeTime then + self.status = "active" + self.fadeIn = nil + end + + end + +end + +function GUIReplayDownloader:CreateGUIItem() + + local item = GUI.CreateItem() + US_Add(self.items, item) + + return item + +end + +function GUIReplayDownloader:CreateTextItem(createShadow) + + local item = self:CreateGUIItem() + item:SetOptionFlag(GUIItem.ManageRender) + item:SetTextAlignmentX(GUIItem.Align_Center) + item:SetTextAlignmentY(GUIItem.Align_Center) + item:SetFontName(self.kTitleFontName) + item:SetColor(self.kColor) + + if createShadow then + + local shadowItem = self:CreateGUIItem() + shadowItem:SetOptionFlag(GUIItem.ManageRender) + shadowItem:SetTextAlignmentX(GUIItem.Align_Center) + shadowItem:SetTextAlignmentY(GUIItem.Align_Center) + shadowItem:SetFontName(self.kTitleFontName) + shadowItem:SetColor(self.kShadowColor) + + return item, shadowItem + + end + + return item + +end + +function GUIReplayDownloader:DestroyGUIItem(item) + + GUI.DestroyItem(item) + US_Remove(self.items, item) + +end + +function GUIReplayDownloader:CenterOnScreen() + + local size = self.kPanelSize * self.scale + local screenSize = Vector(Client.GetScreenWidth(), Client.GetScreenHeight(), 0) + + local pos = (screenSize - size) * 0.5 + + self:SetPosition(pos) + +end + +-- Sets the absolute screen position of the upper-left corner of this panel, in pixels. +function GUIReplayDownloader:SetPosition(position) + + self.position = position + self:UpdateTransform() + +end + +-- Sets the scaling value of this panel. Measurements provided are taken from a mockup +-- done at 1920x1080, so scale values should be calculated with this in mind. +function GUIReplayDownloader:SetScale(scale) + + self.scale = scale + self:UpdateFontScales() + self:CenterOnScreen() + +end + +function GUIReplayDownloader:SetLayer(layer) + + self.layer = layer + self:UpdateLayers() + +end + diff --git a/ns2/lua/challenge/GUIReplayDownloaderAlien.lua b/ns2/lua/challenge/GUIReplayDownloaderAlien.lua new file mode 100644 index 000000000..9d31bed12 --- /dev/null +++ b/ns2/lua/challenge/GUIReplayDownloaderAlien.lua @@ -0,0 +1,295 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua\challenge\GUIReplayDownloader.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Extends GUIReplayDownloader to provide the alien theme. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +Script.Load("lua/challenge/GUIReplayDownloader.lua") + +class 'GUIReplayDownloaderAlien' (GUIReplayDownloader) + +GUIReplayDownloaderAlien.kColor = Color(219/255, 157/255, 35/255, 1) +GUIReplayDownloaderAlien.kButtonFontColor = Color(0,0,0,1) + +GUIReplayDownloaderAlien.kButtonTexture = PrecacheAsset("ui/alien_buymenu.dds") +GUIReplayDownloaderAlien.kButtonTextureCoords = {396, 428, 706, 511} +GUIReplayDownloaderAlien.kButtonVeinsTextureCoords = { 600, 350, 915, 419} +local kVeinsMargin = 4 +GUIReplayDownloaderAlien.kVeinsPulsePeriod = math.pi -- pulse once every two seconds. + +GUIReplayDownloaderAlien.kButtonTextLayerOffset = 2 +GUIReplayDownloaderAlien.kButtonVeinsLayerOffset = 1 +GUIReplayDownloaderAlien.kButtonGraphicLayerOffset = 0 + +GUIReplayDownloaderAlien.kForegroundBarLayerOffset = 2 +GUIReplayDownloaderAlien.kBarOuterLayerOffset = 1 +GUIReplayDownloaderAlien.kBackgroundBarLayerOffset = 0 + +GUIReplayDownloaderAlien.kInfestedGraphicPosition = Vector(36, 95, 0) +GUIReplayDownloaderAlien.kInfestedGraphicSize = Vector(568, 60, 0) +GUIReplayDownloaderAlien.kBarPosition = Vector(46, 111, 0) +GUIReplayDownloaderAlien.kBarSize = Vector(540, 31, 0) +GUIReplayDownloaderAlien.kInfestedGraphicTexture = PrecacheAsset("ui/infested_marines/air_quality_bar_infestation.dds") +GUIReplayDownloaderAlien.kBarGraphicTexture = PrecacheAsset("ui/infested_marines/air_quality_bar_blue.dds") +GUIReplayDownloaderAlien.kFrontBarOpacity = 0.15 + +GUIReplayDownloaderAlien.kBackgroundShader = "shaders/GUISmokeAlpha.surface_shader" +GUIReplayDownloaderAlien.kBackgroundNoiseTexture = PrecacheAsset("ui/alien_commander_bg_smoke.dds") +GUIReplayDownloaderAlien.kBackgroundCorrectionFactor = 0.0025 +GUIReplayDownloaderAlien.kBackgroundTexture = PrecacheAsset("ui/challenge/downloader_background_alien.dds") +GUIReplayDownloaderAlien.kBackgroundPosition = Vector( -92, -127, 0) +GUIReplayDownloaderAlien.kBackgroundSize = Vector(834, 496, 0) + +GUIReplayDownloaderAlien.kButtonHoverSound = PrecacheAsset("sound/NS2.fev/alien/common/alien_menu/hover") +GUIReplayDownloaderAlien.kButtonClickSound = PrecacheAsset("sound/NS2.fev/alien/common/alien_menu/close_menu") + +function GUIReplayDownloaderAlien:InitButton(newButton, buttonText) + + GUIReplayDownloader.InitButton(self, newButton, buttonText) + + local graphic = self:CreateGUIItem() + graphic:SetTexture(self.kButtonTexture) + graphic:SetTexturePixelCoordinates(unpack(self.kButtonTextureCoords)) + newButton.graphic = graphic + + local veins = self:CreateGUIItem() + veins:SetTexture(self.kButtonTexture) + veins:SetTexturePixelCoordinates(unpack(self.kButtonVeinsTextureCoords)) + newButton.veins = veins + + newButton.veinsPulse = 0 + + local old_newButton_SetLayer = newButton.SetLayer + newButton.SetLayer = function(button, layer) + old_newButton_SetLayer(button, layer) + button.graphic:SetLayer(layer + self.kButtonGraphicLayerOffset) + button.veins:SetLayer(layer + self.kButtonVeinsLayerOffset) + end + +end + +function GUIReplayDownloaderAlien:InitProgressBarGUI() + + GUIReplayDownloader.InitProgressBarGUI(self) + + local pb = self.progressBar + + local frame = self:CreateGUIItem() + frame:SetTexture(self.kInfestedGraphicTexture) + pb.frame = frame + + local backBar = self:CreateGUIItem() + backBar:SetTexture(self.kBarGraphicTexture) + backBar:SetIsVisible(false) + pb.backBar = backBar + + local frontBar = self:CreateGUIItem() + frontBar:SetTexture(self.kBarGraphicTexture) + frontBar:SetColor(Color(1,1,1,self.kFrontBarOpacity)) + frontBar:SetIsVisible(false) + pb.frontBar = frontBar + + pb.SetLayer = function(bar, layer) + bar.backBar:SetLayer(layer + self.kBackgroundBarLayerOffset) + bar.frame:SetLayer(layer + self.kBarOuterLayerOffset) + bar.frontBar:SetLayer(layer + self.kForegroundBarLayerOffset) + end + + pb.SetOpacity = function(bar, opacity) + bar.backBar:SetColor(Color(1,1,1,opacity)) + bar.frame:SetColor(Color(1,1,1,opacity)) + bar.frontBar:SetColor(Color(1,1,1, self.kFrontBarOpacity * opacity)) + end + +end + +function GUIReplayDownloaderAlien:UpdateColor() + + GUIReplayDownloader.UpdateColor(self) + + local opacity = self:GetOpacity() + + local fullWhite = Color(1,1,1,opacity) + if self.backItem then + self.backItem:SetColor(fullWhite) + end + + if self.progressBar then + self.progressBar:SetOpacity(opacity) + end + + for i=1, #self.buttons do + self.buttons[i].graphic:SetColor(fullWhite) + end + + self:UpdateVeinsOpacity(0) + +end + +function GUIReplayDownloaderAlien:UpdateVeinsOpacity(deltaTime) + + local opacity = self:GetOpacity() + + for i=1, #self.buttons do + local button = self.buttons[i] + button.veinsPulse = button.veinsPulse + deltaTime + if button.over then + button.veinsPulse = 0 + end + + local veinOpacity = (math.cos(button.veinsPulse * self.kVeinsPulsePeriod) * 0.5 + 0.5) * opacity + button.veins:SetColor(Color(1,1,1,veinOpacity)) + end + +end + +function GUIReplayDownloaderAlien:Update(deltaTime) + + GUIReplayDownloader.Update(self, deltaTime) + + self:UpdateVeinsOpacity(deltaTime) + +end + +function GUIReplayDownloaderAlien:DestroyButton(button) + + GUIReplayDownloader.DestroyButton(self, button) + + self:DestroyGUIItem(button.graphic) + self:DestroyGUIItem(button.veins) + +end + +function GUIReplayDownloaderAlien:UpdateProgressBarFill() + + GUIReplayDownloader.UpdateProgressBarFill(self) + + self:UpdateProgressFraction() + local fill = self.progressFraction or 0.0 + + if fill <= 0.0 then + self.progressBar.frontBar:SetIsVisible(false) + self.progressBar.backBar:SetIsVisible(false) + else + self.progressBar.frontBar:SetIsVisible(true) + self.progressBar.backBar:SetIsVisible(true) + local barSize = self.kBarSize * self.scale * Vector(fill, 1, 0) + self.progressBar.frontBar:SetSize(barSize) + self.progressBar.backBar:SetSize(barSize) + self.progressBar.frontBar:SetTextureCoordinates(0, 0, fill, 1) + self.progressBar.backBar:SetTextureCoordinates(0, 0, fill, 1) + end + +end + +function GUIReplayDownloaderAlien:UpdateProgressBarTransform() + + GUIReplayDownloader.UpdateProgressBarTransform(self) + + local framePos = (self.kInfestedGraphicPosition * self.scale) + self.position + local frameSize = self.kInfestedGraphicSize * self.scale + self.progressBar.frame:SetPosition(framePos) + self.progressBar.frame:SetSize(frameSize) + + local barPos = (self.kBarPosition * self.scale) + self.position + self.progressBar.frontBar:SetPosition(barPos) + self.progressBar.backBar:SetPosition(barPos) + + -- Update progress bar fill + self:UpdateProgressBarFill() + +end + +function GUIReplayDownloaderAlien:UpdateButtonTransform(button) + + GUIReplayDownloader.UpdateButtonTransform(self, button) + + local buttonSize = self.kButtonSize + if button.over then + buttonSize = self.kButtonOverSize + end + + local veinsSize = buttonSize - (Vector(kVeinsMargin, kVeinsMargin, 0) * 2.0) + + buttonSize = buttonSize * self.scale + veinsSize = veinsSize * self.scale + + local graphicPos = button.position - (buttonSize * 0.5) + local veinsPos = button.position - (veinsSize * 0.5) + + button.graphic:SetPosition(graphicPos) + button.graphic:SetSize(buttonSize) + + button.veins:SetPosition(veinsPos) + button.veins:SetSize(veinsSize) + +end + +function GUIReplayDownloaderAlien:UpdateLayers() + + GUIReplayDownloader.UpdateLayers(self) + + if self.backItem then + self.backItem:SetLayer(self.layer + self.kBackgroundLayerOffset) + end + +end + +function GUIReplayDownloaderAlien:UpdateTransform() + + GUIReplayDownloader.UpdateTransform(self) + + if self.backItem then + self.backItem:SetPosition((self.kBackgroundPosition * self.scale) + self.position) + self.backItem:SetSize(self.kBackgroundSize * self.scale) + + local texSize = Vector(self.backItem:GetTextureWidth(), self.backItem:GetTextureHeight(), 0) + self.backItem:SetFloatParameter("correctionX", self.kBackgroundCorrectionFactor * texSize.x) + self.backItem:SetFloatParameter("correctionY", self.kBackgroundCorrectionFactor * texSize.y) + end + +end + +function GUIReplayDownloaderAlien:InitializeGUI() + + GUIReplayDownloader.InitializeGUI(self) + + self.backItem = self:CreateGUIItem() + self.backItem:SetShader(self.kBackgroundShader) + self.backItem:SetTexture(self.kBackgroundTexture) + self.backItem:SetAdditionalTexture("noise", self.kBackgroundNoiseTexture) + self.backItem:SetFloatParameter("timeOffset", math.random() * 20) + + local texSize = Vector(self.backItem:GetTextureWidth(), self.backItem:GetTextureHeight(), 0) + self.backItem:SetFloatParameter("correctionX", self.kBackgroundCorrectionFactor * texSize.x) + self.backItem:SetFloatParameter("correctionY", self.kBackgroundCorrectionFactor * texSize.y) + +end + +function GUIReplayDownloaderAlien:DoFadeIn() + + GUIReplayDownloader.DoFadeIn(self) + + self.backItem:SetFloatParameter("fadeStartTime", Shared.GetTime()) + self.backItem:SetFloatParameter("fadeEndTime", Shared.GetTime() + self.kFadeTime) + self.backItem:SetFloatParameter("fadeTarget", 1.0) + +end + +function GUIReplayDownloaderAlien:DoFadeOut() + + GUIReplayDownloader.DoFadeOut(self) + + self.backItem:SetFloatParameter("fadeStartTime", Shared.GetTime()) + self.backItem:SetFloatParameter("fadeEndTime", Shared.GetTime() + self.kFadeTime) + self.backItem:SetFloatParameter("fadeTarget", 0.0) + +end + + + + diff --git a/ns2/lua/challenge/ReplayManager.lua b/ns2/lua/challenge/ReplayManager.lua new file mode 100644 index 000000000..c94ab9b0a --- /dev/null +++ b/ns2/lua/challenge/ReplayManager.lua @@ -0,0 +1,85 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ======= +-- +-- lua/challenge/ReplayManager.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- Manages a cache of downloaded replays for the challenge game modes. +-- +-- ========= For more information, visit us at http://www.unknownworlds.com ===================== + +local kMaxEntries = 10 -- max other player entries to cache before discarding the oldest. + +local manager +function GetReplayManager() + + if not manager then + manager = ReplayManager() + manager:Initialize() + end + + return manager + +end + +class 'ReplayManager' + +function ReplayManager:Initialize() + + self.playerReplayEntry = nil + self.replayEntries = {} + +end + +-- clears all replays except the player's local copy +function ReplayManager:ClearCachedReplaysFromLeaderboard() + + self.replayEntries = {} + +end + +-- Store a replay for the given steam id. For the player's last run (not necessarily the one they have on the leaderboard, +-- pass nil for steamId to have this replay stored separately). +function ReplayManager:AddReplay(replayData, steamId) + + local replayEntry = {} + replayEntry.data = replayData + replayEntry.id = steamId + + -- store player's last run separately -- so it is never overwritten. + if steamId == nil then + self.playerReplayEntry = replayEntry + return + end + + -- remove other entries with the same id from the list + for i=#self.replayEntries, 1, -1 do + if self.replayEntries[i].id == steamId then + table.remove(self.replayEntries, i) + end + end + + -- remove oldest entry to make room, if needed + while #self.replayEntries > kMaxEntries do + table.remove(self.replayEntries, 1) + end + + table.insert(self.replayEntries, replayEntry) + +end + +function ReplayManager:GetReplay(steamId) + + if steamId == nil and self.playerReplayEntry then + return self.playerReplayEntry.data + end + + for i=1, #self.replayEntries do + if self.replayEntries[i].id == steamId then + return self.replayEntries[i].data + end + end + + return nil + +end diff --git a/ns2/lua/challenge/SteamLeaderboardManager.lua b/ns2/lua/challenge/SteamLeaderboardManager.lua new file mode 100644 index 000000000..ba1219ed1 --- /dev/null +++ b/ns2/lua/challenge/SteamLeaderboardManager.lua @@ -0,0 +1,657 @@ +-- ======= Copyright (c) 2017, Unknown Worlds Entertainment, Inc. All rights reserved. ===== +-- +-- lua/challenge/SteamLeaderboardManager.lua +-- +-- Created by: Trevor Harris (trevor@naturalselection2.com) +-- +-- A helpful class for dealing with Steam's asynchronous leaderboard functions. Manages a queue of requests, +-- and sends out callbacks when operations complete. +-- +-- ========= For more information, visit us at http:\\www.unknownworlds.com ===================== + +assert(Client) -- should only be loaded on client. + +class 'SteamLeaderboardManager' + +-- if an operation fails, but might work if we try again, give it this many attempts. +SteamLeaderboardManager.kMaxAttempts = 5 + +-- wait 1 second after failures to retry. +SteamLeaderboardManager.kFailTimeout = 1 + +-------------------------------------------------------------------------------- +-------------------------------- PUBLIC METHODS -------------------------------- +-------------------------------------------------------------------------------- + +-- Static global accessor for manager class. Should really never need more than one instance of this class. +local manager +function GetSteamLeaderboardManager() + + if not manager then + manager = SteamLeaderboardManager() + manager:Init() + end + + return manager + +end + +local function OnSteamNameReceived(steamId, name) + + local self = GetSteamLeaderboardManager() + US_Remove(self.requestedNames, steamId) + self.knownSteamNames[steamId] = name + +end +Event.Hook("SteamNameReceived", OnSteamNameReceived) +-- Returns the steam name of the given steam id. If the name is not yet known, a request is +-- sent to Steam, and nil is returned. You can keep polling this function to get the name if +-- nil is returned. +function SteamLeaderboardManager:GetSteamName(steamId) + + if self.knownSteamNames[steamId] then + return self.knownSteamNames[steamId] + end + + -- Ensure the name is being requested by steam + if not US_GetElementExists(self.requestedNames, steamId) then + local result = {} + if Client.RequestSteamName(steamId, result) then + -- returned true, meaning we have a result immediately available. + OnSteamNameReceived(steamId, result.steamName) + else + -- we've put out a request for the name, and must now wait for Steam to return. Make note of requested + -- names we're waiting on so we don't spam Steam. + US_Add(self.requestedNames, steamId) + end + end + + return nil + +end + +-- Removes all queued requests (excluding any waiting for a reply from Steam) of the given type. +-- Useful to allow players to scroll through scoreboard quickly without having to wait for 8 billion requests to +-- finish. Allows the most recent request to preempt the others. +function SteamLeaderboardManager:CancelPendingRequestsOfType(typeName) + + assert(typeName) + assert(typeName ~= "") + + for i = #self.requestQueue, 2, -1 do -- iterate backwards from the end to the second element. + if typeName == self.requestQueue[i].type then + table.remove(self.requestQueue, i) + end + end + +end + +-- Whenever Steam returns with a result of a request, it also carries with it the total number of entries +-- in the leaderboard. This is an accessor for that value. +function SteamLeaderboardManager:GetEntryCount(boardName) + + assert(boardName) + assert(boardName ~= "") + + local boardHandle = self.boardNameToHandle[boardName] + if boardHandle == nil then + Log("No board handle found for board named '%s'.", boardName) + return nil + end + + return self.entryCount[boardHandle] + +end + +-- Sends a request to Steam to retrieve this player's entry for the leaderboard. Returns true if +-- request is enqueued successfully, false if there was an issue. When the reply is received, +-- callbackFunc is called with two parameters: a boolean, and a table of leaderboard entries. +-- The boolean is true if successful, or false if the request failed. The second parameter is an +-- array-like table (nil if success == false), with each element being another table containing +-- the following fields: +-- number steamId +-- number globalRank (eg #1 is player with best score) +-- number score +-- string ugcHandle (User Generated Content handle, eg the skulk challenge replays) +-- table details (array of numbers for extra information about player's score, +-- eg max speed, number of kills, etc.) +function SteamLeaderboardManager:RequestPlayerScore(boardName, callbackFunc) + + assert(boardName) + assert(boardName ~= "") + assert(callbackFunc) + + local newRequest = {} + newRequest.type = "GetPlayerScore" + newRequest.boardName = boardName + newRequest.callbackFunc = callbackFunc + self:EnqueueRequest(newRequest) + +end + +-- Sends a request to Steam to retrieve leaderboard entries within the given range of rankings. +-- Steam can only return what exists. If no entries exist within the specified range, an empty +-- list will be returned. +-- See RequestPlayerScore for format of callbackFunc parameter. +function SteamLeaderboardManager:RequestRangeOfScores(boardName, rangeStart, rangeEnd, callbackFunc) + + assert(rangeStart) + assert(rangeEnd) + assert(rangeStart > 0) + assert(rangeEnd >= rangeStart) + assert(boardName) + assert(boardName ~= "") + assert(callbackFunc) + + local newRequest = {} + newRequest.type = "GetRangeOfScores" + newRequest.boardName = boardName + newRequest.rangeStart = rangeStart + newRequest.rangeEnd = rangeEnd + newRequest.callbackFunc = callbackFunc + self:EnqueueRequest(newRequest) + +end + +-- Sends a request to Steam to retrieve leaderboard entries surrounding the player's own leaderboard entry. +-- For example, if frontCount is 4, and backCount is 5, this will (potentially) yield a list of 10 entries +-- (the user is included in this list). Also, Steam will automatically adjust the frontCount and backCount +-- if the requested counts are not possible, to preserve the total entry count. For example, if frontCount +-- is 4, and backCount is 5, that's 10 total entries, but if player is rank 2, then we cannot retrieve 4 +-- in front of player. The frontCount is adjusted to 1, and backCount is adjusted to 8. +-- See RequestPlayerScore for format of callbackFunc parameter. +function SteamLeaderboardManager:RequestScoresAroundPlayer(boardName, frontCount, backCount, callbackFunc) + + assert(boardName) + assert(boardName ~= "") + assert(callbackFunc) + assert(frontCount >= 0) + assert(backCount >= 0) + + local newRequest = {} + newRequest.type = "GetScoresAroundPlayer" + newRequest.boardName = boardName + newRequest.frontCount = frontCount + newRequest.backCount = backCount + newRequest.callbackFunc = callbackFunc + self:EnqueueRequest(newRequest) + +end + +-- Sends a request to Steam to download all leaderboard entries associated with friends of the user for this +-- board. +-- See RequestPlayerScore for format of callbackFunc parameter. +function SteamLeaderboardManager:RequestFriendScores(boardName, callbackFunc) + + assert(boardName) + assert(boardName ~= "") + assert(callbackFunc) + + local newRequest = {} + newRequest.type = "GetFriendScores" + newRequest.boardName = boardName + newRequest.callbackFunc = callbackFunc + self:EnqueueRequest(newRequest) + +end + +-- Sends the score to Steam. Steam automatically decides based on the leaderboard's settings whether or not the +-- score is better or should be discarded, so no need to check this ahead of time. Parameter score is just a number +-- value (it should be an integer). Parameter scoreExtras should be a table of integers, but can be nil. Extra values +-- are extra information about how a player obtained the score (eg number of kills, max speed, etc.) +-- The callbackFunc function is called upon receipt of successful confirmation from Steam, and will have two parameters: +-- success and uploadResult. Success is a boolean, true if the score uploaded successfully, false if the operation +-- failed. Note that the SteamLeaderboardManager class already attempts several retries. The callback is only called +-- once it gives up and fails for good. +-- Parameter uploadResult is a table (nil if success == false) that holds 5 named values: +-- string handle the handle of the leaderboard this score was just added to. +-- number score the score that was added (or attempted to be added) to the leaderboard. +-- boolean changed true if this score was better than the previous one, or false if it was worse or the same. +-- number globalRank the player's new ranking (only valid if changed == true) +-- number prevGlobalRank the player's old ranking (only valid if changed == true) +-- NOTE: It is tempting to use globalRank and prevGlobalRank even when changed == false, as they may be nonzero and appear +-- valid. This is UNRELIABLE! It sometimes shows 0 for both. You should instead request the player's score from the +-- leaderboard again to ensure the data is correct. +function SteamLeaderboardManager:UploadScore(boardName, score, scoreExtras, callbackFunc) + + assert(boardName) + assert(boardName ~= "") + assert(score) + assert(callbackFunc) + + scoreExtras = scoreExtras or {} -- default to empty table + + local newRequest = {} + newRequest.type = "UploadScore" + newRequest.boardName = boardName + newRequest.callbackFunc = callbackFunc + newRequest.score = score + newRequest.scoreExtras = scoreExtras + self:EnqueueRequest(newRequest) + +end + +-- Attempts to convert the given replayTable into binary data, then upload that to steam cloud under the given fileName. +-- As usual, first parameter with callbackFunc is true/false for if the operation was successful or not. Then, if true, +-- the next 2 parameters are ugcHandle and fileName. Parameter ugcHandle is a unique identifier (64-bit unsigned integer, +-- expressed as a string) steam uses to keep track of the file. Parameter fileName will match what was given. +function SteamLeaderboardManager:UploadReplay(fileName, replayTable, replayType, callbackFunc) + + assert(fileName) + assert(fileName ~= "") + assert(replayTable) + assert(replayType) + assert(callbackFunc) + + local newRequest = {} + newRequest.type = "UploadReplay" + newRequest.callbackFunc = callbackFunc + newRequest.fileName = fileName + newRequest.replayTable = replayTable + newRequest.replayType = replayType + self:EnqueueRequest(newRequest) + +end + +-- Attempts to associate the given UGC handle with the player's entry on the given leaderboard. +-- As usual, first parameter given to callbackFunc is true if successful, false if failed. If +-- it failed, the second parameter is an error code for why the failure occurred. There is no +-- second parameter for success. +function SteamLeaderboardManager:AttachUGCToLeaderboard(ugcHandle, boardName, callbackFunc) + + assert(ugcHandle) + assert(ugcHandle ~= "") + assert(boardName) + assert(boardName ~= "") + assert(callbackFunc) + + local newRequest = {} + newRequest.type = "AttachUGC" + newRequest.callbackFunc = callbackFunc + newRequest.ugcHandle = ugcHandle + newRequest.boardName = boardName + self:EnqueueRequest(newRequest) + +end + +-- Tells Steam to begin downloading the UGC. The download progress can be checked with +-- Client.GetUGCDownloadProgress (see api docs for details). +-- Parameter callbackFunc is called when download completes, first parameter is true if download was successful, +-- false if it failed. If true, second parameter is the ugcFileHandle that finished downloading (should match +-- input), and third parameter is the file size, in bytes, of the file downloaded. If download was unsuccessful, +-- the second parameter is an error code. +function SteamLeaderboardManager:DownloadUGC(ugcHandle, callbackFunc) + + assert(ugcHandle) + assert(ugcHandle ~= "") + assert(callbackFunc) + + local newRequest = {} + newRequest.type = "DownloadUGC" + newRequest.callbackFunc = callbackFunc + newRequest.ugcHandle = ugcHandle + self:EnqueueRequest(newRequest) + +end + +-------------------------------------------------------------------------------- +-------------------------------- PRIVATE METHODS ------------------------------- +-------------------------------------------------------------------------------- + +-- Request type names +-- LeaderboardHandle -- retrieve the leaderboard handle value from steam for the leaderboard of a given name. +-- GetPlayerScore -- retrieve only the current player's score, and no one else's. +-- GetRangeOfScores -- retrieve a range of scores based on rank. +-- GetScoresAroundPlayer -- retrieve a set of score around the player's entry +-- GetFriendScores -- retrieve the scores of ALL steam friends. +-- UploadScore -- upload a score to the leaderboard. +-- UploadReplay -- converts a replay table to binary data (VERY strict format), and uploads it to steam +-- cloud. +-- AttachUGC -- associates the given UGC handle with the player's entry on the given leaderboard. +-- DownloadUGC -- downloads the steam cloud file associated with the given UGC handle. + +-- Constructor. +function SteamLeaderboardManager:Init() + + self.boardNameToHandle = {} -- name -> handle lookup table + self.badBoardNames = {} -- leaderboard names that have definitively failed, and should not be re-queried. + + self.requestedNames = US_Create() -- names requested from Steam. + self.knownSteamNames = {} -- id -> name string lookup table. + + self.requestQueue = {} -- stores pending steam requests of various types. + self.requestRetryDelay = nil -- either nil or a delay for when we should re-attempt our last failed request. + self.attemptNumber = 1 -- stores the attempt number for the current request. + + self.entryCount = {} -- stores the number of entries in the leaderboard. Unknown until steam returns after a query. + +end + +function SteamLeaderboardManager:UpdateEntryCount(boardHandle) + + self.entryCount[boardHandle] = Client.GetNumLeaderboardEntries(boardHandle) + +end + +-- Sets up the manager to pause for a certain amount of time before retrying a request, and keeps track +-- of the number of attempts that have been made. Returns true if another attempt will be made, or false +-- if all attempts have been exhausted. +function SteamLeaderboardManager:DelayReattempt() + + if self.attemptNumber >= self.kMaxAttempts then + self.attemptNumber = 1 + return false + end + + self.attemptNumber = self.attemptNumber + 1 + self.requestRetryDelay = self.kFailTimeout + return true + +end + +function SteamLeaderboardManager:Update(deltaTime) + + if self.requestRetryDelay ~= nil then + + self.requestRetryDelay = self.requestRetryDelay - deltaTime + + if self.requestRetryDelay <= 0.0 then + self.requestRetryDelay = nil + self:ProcessNextRequest() + end + + end + +end +local function OnUpdateClient(deltaTime) + GetSteamLeaderboardManager():Update(deltaTime) +end +Event.Hook("UpdateClient", OnUpdateClient) + +function SteamLeaderboardManager:ProcessRequest_LeaderboardHandle(request) + Client.GetLeaderboardHandleByName(request.boardName) + return true +end + +function SteamLeaderboardManager:ProcessRequest_GetPlayerScore(request) + return Client.DownloadGlobalLeaderboardEntriesAroundPlayer(self.boardNameToHandle[request.boardName], 0, 0) +end + +function SteamLeaderboardManager:ProcessRequest_GetRangeOfScores(request) + return Client.DownloadGlobalLeaderboardEntries(self.boardNameToHandle[request.boardName], request.rangeStart, request.rangeEnd) +end + +function SteamLeaderboardManager:ProcessRequest_GetScoresAroundPlayer(request) + return Client.DownloadGlobalLeaderboardEntries(self.boardNameToHandle[request.boardName], request.frontCount, request.backCount) +end + +function SteamLeaderboardManager:ProcessRequest_GetFriendScores(request) + return Client.DownloadFriendsLeaderboardEntries(self.boardNameToHandle[request.boardName]) +end + +function SteamLeaderboardManager:ProcessRequest_UploadScore(request) + Client.UploadScoreToLeaderboard(self.boardNameToHandle[request.boardName], request.score, request.scoreExtras) + return true +end + +function SteamLeaderboardManager:ProcessRequest_UploadReplay(request) + return Client.UploadReplay(request.fileName, request.replayTable, request.replayType) +end + +function SteamLeaderboardManager:ProcessRequest_AttachUGC(request) + return Client.AttachUGCToLeaderboard(request.ugcHandle, self.boardNameToHandle[request.boardName]) +end + +function SteamLeaderboardManager:ProcessRequest_DownloadUGC(request) + return Client.DownloadUGC(request.ugcHandle) +end + +-- Performs the next request in the queue. +function SteamLeaderboardManager:ProcessNextRequest() + + local request = self.requestQueue[1] + if not request then + -- no more requests queued up. + return + end + + assert(request.type ~= nil) + + if request.type ~= "LeaderboardHandle" and request.boardName ~= nil then + -- ensure we have a valid board handle for this board name... otherwise we gotta do that one first. + if not self:CheckBoardName(request.boardName) then + -- board name is invalid (not just missing). Discard request. + assert(request == self.requestQueue[1]) + table.remove(self.requestQueue, 1) + self:ProcessNextRequest() + return + end + + if request ~= self.requestQueue[1] then + -- our request is no longer the most urgent... do the most recent request first. + self:ProcessNextRequest() + return + end + + end + + -- If we make it to here, we're good to go, and should process request now. We process each type with a different + -- function. The name of the function is derived from the type of request. + local functionName = "ProcessRequest_" .. request.type + if self[functionName](self, request) == false then + -- Something is wrong with the request, we cannot retry. + Log("Unable to process request '%s'!", request.type) + table.remove(self.requestQueue, 1) + self:ProcessNextRequest() + return + end + +end + +-- Adds the request (table) to the request queue. +function SteamLeaderboardManager:EnqueueRequest(newRequest) + + self.requestQueue[#self.requestQueue+1] = newRequest + + if #self.requestQueue == 1 then + self:ProcessNextRequest() + end + +end + +-- Adds the request (table) to the front of the request queue. +function SteamLeaderboardManager:EnqueueRequestImmediate(newRequest) + + table.insert(self.requestQueue, 1, newRequest) + + if #self.requestQueue == 1 then + self:ProcessNextRequest() + end + +end + +-- Checks to ensure the board name has a handle associated with it. If not, it sends the request to +-- steam to check the name. Returns false if the board name is invalid (definitively missing, not just IO failure), +-- true if all is well. +function SteamLeaderboardManager:CheckBoardName(boardName) + if self.boardNameToHandle[boardName] then + -- This board name already has a handle, we're good to proceed. + return true + end + + if self.badBoardNames[boardName] then + -- board is definitively missing, we should discard any requests using this board name. + return false + end + + local newRequest = {} + newRequest.type = "LeaderboardHandle" + newRequest.boardName = boardName + self:EnqueueRequestImmediate(newRequest) + + -- We don't have a handle yet, but we've just put in the request, so it should be ready by the time + -- the other request is put through. + return true + +end + +-- Handles which behavior needs to occur when an asynchronous request comes back successful. +function SteamLeaderboardManager:OnSuccess() + + -- Update the amount of entries this steam leaderboard has. + local request = self.requestQueue[1] + if request.boardName then + local boardHandle = self.boardNameToHandle[request.boardName] + if boardHandle then + self:UpdateEntryCount(boardHandle) + end + end + + self.attemptNumber = 1 + table.remove(self.requestQueue, 1) + + self:ProcessNextRequest() + +end + +-- Handles which behavior needs to occur when an asynchronous request fails. +-- errorCode is optional, and will only be used if it fails for good. +function SteamLeaderboardManager:OnFail(errorCode) + + if not self:DelayReattempt() then + local request = self.requestQueue[1] + if request.callbackFunc then + request.callbackFunc(false, errorCode) + end + + table.remove(self.requestQueue, 1) + self:ProcessNextRequest() + end + +end + +local function OnLeaderboardFindSuccess(boardHandle) + + local self = GetSteamLeaderboardManager() + local request = self.requestQueue[1] + + self.boardNameToHandle[request.boardName] = boardHandle + + self:OnSuccess() + +end +Event.Hook("LeaderboardFindSuccess", OnLeaderboardFindSuccess) + +local function OnLeaderboardFindFail(canRetry) + + local self = GetSteamLeaderboardManager() + + if not canRetry then + self.attemptNumber = 99999 -- disallow reattempts if there's no point in retrying. + end + + self:OnFail() + +end +Event.Hook("LeaderboardFindFail", OnLeaderboardFindFail) + +local function OnDownloadScoresSuccess(entries) + + local self = GetSteamLeaderboardManager() + local request = self.requestQueue[1] + + request.callbackFunc(true, entries) + + self:OnSuccess() + +end +Event.Hook("DownloadScoresSuccess", OnDownloadScoresSuccess) + +local function OnDownloadScoresFailed() + + local self = GetSteamLeaderboardManager() + self:OnFail() + +end +Event.Hook("DownloadScoresFailed", OnDownloadScoresFailed) + +local function OnLeaderboardUploadSuccess(uploadResult) + + local self = GetSteamLeaderboardManager() + local request = self.requestQueue[1] + + request.callbackFunc(true, uploadResult) + + self:OnSuccess() + +end +Event.Hook("LeaderboardUploadSuccess", OnLeaderboardUploadSuccess) + +local function OnLeaderboardUploadFail() + + local self = GetSteamLeaderboardManager() + self:OnFail() + +end +Event.Hook("LeaderboardUploadFail", OnLeaderboardUploadFail) + +local function OnShareFileSuccess(ugcHandle, fileName) + + local self = GetSteamLeaderboardManager() + local request = self.requestQueue[1] + + request.callbackFunc(true, ugcHandle, fileName) + + self:OnSuccess() + +end +Event.Hook("ShareFileSuccess", OnShareFileSuccess) + +local function OnShareFileFailed() + + local self = GetSteamLeaderboardManager() + self:OnFail() + +end +Event.Hook("ShareFileFailed", OnShareFileFailed) + +local function OnUGCAttachSuccess() + + local self = GetSteamLeaderboardManager() + local request = self.requestQueue[1] + + request.callbackFunc(true) + + self:OnSuccess() + +end +Event.Hook("UGCAttachSuccess", OnUGCAttachSuccess) + +local function OnUGCAttachFail(errorCode) + + local self = GetSteamLeaderboardManager() + self:OnFail(errorCode) + +end +Event.Hook("UGCAttachFail", OnUGCAttachFail) + +local function OnDownloadUGCSuccess(ugcHandle, fileSize) + + local self = GetSteamLeaderboardManager() + local request = self.requestQueue[1] + + request.callbackFunc(true, ugcHandle, fileSize) + + self:OnSuccess() + +end +Event.Hook("DownloadUGCSuccess", OnDownloadUGCSuccess) + +local function OnDownloadUGCFailed(errorCode) + + local self = GetSteamLeaderboardManager() + self:OnFail(errorCode) + +end +Event.Hook("DownloadUGCFailed", OnDownloadUGCFailed) + diff --git a/ns2/lua/menu/FancyUtilities.lua b/ns2/lua/menu/FancyUtilities.lua index 7b8214477..8bb865c0b 100644 --- a/ns2/lua/menu/FancyUtilities.lua +++ b/ns2/lua/menu/FancyUtilities.lua @@ -93,6 +93,8 @@ function Fancy_GetBestFont(font1, font2, text)

end

+-- Returns a string that has been split into substrings by a delimiter character, and returned as a table of +-- these sub strings. Empty strings caused by two delimiter characters in a row are discarded.

function Fancy_SplitStringIntoTable(text, delimiter)
    
    local output = {}

diff --git a/ns2/lua/menu/GUIHoverTooltip.lua b/ns2/lua/menu/GUIHoverTooltip.lua index 1c4cb6853..4c0b3841f 100644 --- a/ns2/lua/menu/GUIHoverTooltip.lua +++ b/ns2/lua/menu/GUIHoverTooltip.lua @@ -101,7 +101,7 @@ function GUIHoverTooltip:Update(deltaTime)

    end
end

-local function UpdateBorders(self) +function GUIHoverTooltip:UpdateBorders()

    local borderSize = Vector(self.background:GetSize())
    borderSize.x = borderSize.x + 2 * kBorderWidth
    borderSize.y = borderSize.y

@@ -135,8 +135,8 @@ function GUIHoverTooltip:SetText(string)

    GUIMakeFontScale(self.tooltip)
    
    self.background:SetSize(Vector(width, height, 0))

- - UpdateBorders(self) + + self:UpdateBorders()

end

diff --git a/ns2/lua/menu/GUIMainMenu.lua b/ns2/lua/menu/GUIMainMenu.lua index 139a2dcb4..ae85e2103 100644 --- a/ns2/lua/menu/GUIMainMenu.lua +++ b/ns2/lua/menu/GUIMainMenu.lua @@ -122,7 +122,7 @@ function GUIMainMenu:TriggerOpenAnimation(window)

end

function GUIMainMenu:Initialize()

- +

    GUIAnimatedScript.Initialize(self, 0)

    Shared.Message("Main Menu Initialized at Version: " .. Shared.GetBuildNumber())

@@ -224,8 +224,8 @@ function GUIMainMenu:Initialize()

        self.optionWindow:SetIsVisible(false)
    end
    

- self.scanLine = CreateMenuElement(self.mainWindow, "Image") - self.scanLine:SetCSSClass("scanline") + --self.scanLine = CreateMenuElement(self.mainWindow, "Image") + --self.scanLine:SetCSSClass("scanline")

    --self.logo = CreateMenuElement(self.mainWindow, "Image")
    --self.logo:SetCSSClass("logo")

@@ -370,11 +370,11 @@ end

function GUIMainMenu:CreateMainLink(text, linkNum, OnClick)
    

- local playRoomOffset = MainMenu_IsInGame() and 0 or 1 + local playRoomOffset = MainMenu_IsInGame() and 0 or 0.5

    local cssClass = MainMenu_IsInGame() and "ingame" or "mainmenu"
    local elementName = "Link"
    

- -- play button takes up two slots. + -- play button takes up one and a half slots.

    if text == "MENU_PLAY" then
        playRoomOffset = 0
        elementName = "BigLink"

@@ -574,12 +574,14 @@ local function AddFavoritesToServerList(serverList)

        serverEntry.map = "?"
        serverEntry.numPlayers = 0
        serverEntry.maxPlayers = currentFavorite.maxPlayers or 24

+ serverEntry.numSpectators = currentFavorite.numSpectators or 0 + serverEntry.maxSpectators = currentFavorite.maxSpectators or 0

        serverEntry.ping = 999
        serverEntry.address = currentFavorite.address or "127.0.0.1:27015"
        serverEntry.requiresPassword = currentFavorite.requiresPassword or false
        serverEntry.playerSkill = currentFavorite.playerSkill or 0
        serverEntry.rookieOnly = currentFavorite.rookieOnly or false

- serverEntry.gatherServer = currentFavorite.gatherServer or false + -- serverEntry.gatherServer = currentFavorite.gatherServer or false

        serverEntry.friendsOnServer = false
        serverEntry.lanServer = false
        serverEntry.tickrate = 30

@@ -623,24 +625,36 @@ function GUIMainMenu:UpdateServerList()

    end
end

-local function JoinServer(self) +function GUIMainMenu:JoinServer()

    local selectedServer = MainMenu_GetSelectedServer()

- if selectedServer ~= nil then - + if selectedServer then

        if selectedServer < 0 then

+

            MainMenu_JoinSelected()
            
        elseif MainMenu_GetSelectedIsFull() and not MainMenu_ForceJoin() then
            
            self.autoJoinWindow:SetIsVisible(true)
            self.autoJoinText:SetText(ToString(MainMenu_GetSelectedServerName()))

- - if MainMenu_GetSelectedIsFullWithNoRS() then - self.autoJoinWindow.forceJoin:SetIsVisible(false) - else + + if MainMenu_GetSelectedHasSpectatorSlots() then + if MainMenu_GetSelectedIsFullWithNoRS() then + self.autoJoinTooltip:SetText(Locale.ResolveString("AUTOJOIN_JOIN_TOOLTIP_SPEC")) + self.autoJoinWindow.forceJoin:SetText(Locale.ResolveString("AUTOJOIN_SPEC")) + else + self.autoJoinTooltip:SetText(Locale.ResolveString("AUTOJOIN_JOIN_TOOLTIP_SPEC_AND_RS")) + self.autoJoinWindow.forceJoin:SetText(Locale.ResolveString("AUTOJOIN_SPEC_AND_RS")) + end + + self.autoJoinWindow.forceJoin:SetIsVisible(true) + elseif not MainMenu_GetSelectedIsFullWithNoRS() then + self.autoJoinWindow.forceJoin:SetText(Locale.ResolveString("AUTOJOIN"))

                self.autoJoinWindow.forceJoin:SetIsVisible(true)

+ self.autoJoinTooltip:SetText(Locale.ResolveString("AUTOJOIN_JOIN_TOOLTIP")) + else + self.autoJoinWindow.forceJoin:SetIsVisible(false)

            end
            
        else

@@ -677,8 +691,8 @@ function GUIMainMenu:ProcessJoinServer(pastWarning)

            self.passwordPromptWindow:SetIsVisible(true)
            
        else

- - JoinServer(self) + + self:JoinServer()

        end
    end

@@ -865,9 +879,9 @@ function GUIMainMenu:CreateAutoJoinWindow()

    text:SetCSSClass("auto_join_text")
    text:SetText(Locale.ResolveString("AUTOJOIN_JOIN"))
    

- local autoJoinTooltip = CreateMenuElement(self.autoJoinWindow, "Font") - autoJoinTooltip:SetCSSClass("auto_join_text_tooltip") - autoJoinTooltip:SetText(Locale.ResolveString("AUTOJOIN_JOIN_TOOLTIP")) + self.autoJoinTooltip = CreateMenuElement(self.autoJoinWindow, "Font") + self.autoJoinTooltip:SetCSSClass("auto_join_text_tooltip") + self.autoJoinTooltip:SetText(Locale.ResolveString("AUTOJOIN_JOIN_TOOLTIP"))

    self.autoJoinText = CreateMenuElement(self.autoJoinWindow, "Font")
    self.autoJoinText:SetCSSClass("auto_join_text_servername")

@@ -915,7 +929,6 @@ function GUIMainMenu:CreatePasswordPromptWindow()

    passwordPromptWindow:DisableSlideBar()
    passwordPromptWindow:DisableContentBox()
    passwordPromptWindow:SetCSSClass("passwordprompt_window")

- passwordPromptWindow:DisableCloseButton()

    passwordPromptWindow:SetLayer(kGUILayerMainMenuDialogs)
        
    local passwordForm = CreateMenuElement(passwordPromptWindow, "Form", false)

@@ -923,16 +936,22 @@ function GUIMainMenu:CreatePasswordPromptWindow()

    local textinput = passwordForm:CreateFormElement(Form.kElementType.TextInput, "PASSWORD", "")
    textinput:SetCSSClass("serverpassword")    

- textinput:AddEventCallbacks({ - OnEscape = function(self) - passwordPromptWindow:SetIsVisible(false) - end - }) + textinput:SetIsSecret(true)

    local descriptionText = CreateMenuElement(passwordPromptWindow.titleBar, "Font", false)
    descriptionText:SetCSSClass("passwordprompt_title")
    descriptionText:SetText(Locale.ResolveString("PASSWORD"))
    

+ local togglePasswordVisible = CreateMenuElement(passwordForm, "MenuButton") + togglePasswordVisible:SetCSSClass("displaypassword_toggle") + + local function TogglePassword() + textinput:SetIsSecret(not textinput:GetIsSecret()) + GetWindowManager():HandleFocusBlur(passwordPromptWindow, textinput) + end + + togglePasswordVisible:AddEventCallbacks({ OnClick = TogglePassword }) +

    local joinServer = CreateMenuElement(passwordPromptWindow, "MenuButton")
    joinServer:SetCSSClass("bottomcenter")
    joinServer:SetText(Locale.ResolveString("JOIN"))

@@ -941,7 +960,7 @@ function GUIMainMenu:CreatePasswordPromptWindow()

        local formData = passwordForm:GetFormData()
        MainMenu_SetSelectedServerPassword(formData.PASSWORD)
        passwordPromptWindow:SetIsVisible(false)

- JoinServer(self) + self:JoinServer()

    end
    
    joinServer:AddEventCallbacks({ OnClick = SubmitPassword })

@@ -950,7 +969,13 @@ function GUIMainMenu:CreatePasswordPromptWindow()

        OnBlur = function(self) self:SetIsVisible(false) end,        
        OnEnter = SubmitPassword,
        OnShow = function(self) GetWindowManager():HandleFocusBlur(self, textinput) end,

- + OnHide = function(self) + textinput:SetValue("") + textinput:SetIsSecret(true) + end, + OnEscape = function(self) + passwordPromptWindow:SetIsVisible(false) + end

    })
    
end

@@ -1230,6 +1255,10 @@ function GUIMainMenu:CreateServerDetailsWindow()

    self.serverDetailsWindow:AddEventCallbacks({
        OnBlur = function(self)
            self:SetIsVisible(false)

+ end, + OnEscape = function(self) + self:SetIsVisible(false) + return true

        end
    })
    

@@ -1240,27 +1269,30 @@ function GUIMainMenu:CreateServerDetailsWindow()

    self.serverDetailsWindow.playerCount = CreateMenuElement(self.serverDetailsWindow, "Font")
    self.serverDetailsWindow.playerCount:SetTopOffset(64)

+ + self.serverDetailsWindow.spectatorCount = CreateMenuElement(self.serverDetailsWindow, "Font") + self.serverDetailsWindow.spectatorCount:SetTopOffset(96)

    self.serverDetailsWindow.ping = CreateMenuElement(self.serverDetailsWindow, "Font")

- self.serverDetailsWindow.ping:SetTopOffset(96) + self.serverDetailsWindow.ping:SetTopOffset(128)

    self.serverDetailsWindow.gameMode = CreateMenuElement(self.serverDetailsWindow, "Font")

- self.serverDetailsWindow.gameMode:SetTopOffset(128) + self.serverDetailsWindow.gameMode:SetTopOffset(160)

    self.serverDetailsWindow.map = CreateMenuElement(self.serverDetailsWindow, "Font")

- self.serverDetailsWindow.map:SetTopOffset(160) + self.serverDetailsWindow.map:SetTopOffset(192)

    self.serverDetailsWindow.performance = CreateMenuElement(self.serverDetailsWindow, "Font")

- self.serverDetailsWindow.performance:SetTopOffset(192) + self.serverDetailsWindow.performance:SetTopOffset(224)

    self.serverDetailsWindow.modsDesc = CreateMenuElement(self.serverDetailsWindow, "Font")

- self.serverDetailsWindow.modsDesc:SetTopOffset(224) + self.serverDetailsWindow.modsDesc:SetTopOffset(256)

    self.serverDetailsWindow.modsDesc:SetText("Installed Mods:")
    
    local windowWidth = self.serverDetailsWindow.background.guiItem:GetSize().x - 16
    
    self.serverDetailsWindow.modList = CreateMenuElement(self.serverDetailsWindow, "Font")

- self.serverDetailsWindow.modList:SetTopOffset(256) + self.serverDetailsWindow.modList:SetTopOffset(288)

    self.serverDetailsWindow.modList:SetCSSClass("serverdetails_modlist")
    self.serverDetailsWindow.modList.text:SetTextClipped(true, windowWidth, 70)

@@ -1369,6 +1401,7 @@ function GUIMainMenu:CreateServerDetailsWindow()

        self.serverName:SetText("")
        self.serverAddress:SetText(Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_ADDRESS"))
        self.playerCount:SetText(Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_PLAYERS"))

+ self.spectatorCount:SetText(Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_SPECTATORS"))

        self.ping:SetText(Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_PING"))
        self.gameMode:SetText(Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_GAME"))
        self.map:SetText(Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_MAP"))

@@ -1380,8 +1413,9 @@ function GUIMainMenu:CreateServerDetailsWindow()

            self.serverName:SetText(serverData.name)
            self.serverAddress:SetText(string.format("%s %s", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_ADDRESS"), ToString(serverData.address)))

- local numReservedSlots = GetNumServerReservedSlots(serverData.serverId) + local numReservedSlots = serverData.numRS or 0

            self.playerCount:SetText(string.format("%s %d / %d", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_PLAYERS"), serverData.numPlayers, (serverData.maxPlayers - numReservedSlots)))

+ self.spectatorCount:SetText(string.format("%s %d / %d", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_SPECTATORS"), serverData.numSpectators, serverData.maxSpectators))

            self.ping:SetText(string.format("%s %d", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_PING"), serverData.ping))
            self.gameMode:SetText(string.format("%s %s", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_GAME"), serverData.mode))
            self.map:SetText(string.format("%s %s", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_MAP"), serverData.map))

@@ -1408,6 +1442,7 @@ function GUIMainMenu:CreateServerDetailsWindow()

            local numReservedSlots = GetNumServerReservedSlots(self.serverIndex)
            self.playerCount:SetText(string.format("%s %d / %d", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_PLAYERS"), Client.GetServerNumPlayers(self.serverIndex), (Client.GetServerMaxPlayers(self.serverIndex) - numReservedSlots)))

+ self.spectatorCount:SetText(string.format("%s %d / %d", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_SPECTATORS"), Client.GetServerNumSpectators(self.serverIndex), Client.GetServerMaxSpectators(self.serverIndex)))

            self.ping:SetText(string.format("%s %d", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_PING"), Client.GetServerPing(self.serverIndex)))
            self.gameMode:SetText(string.format("%s %s", Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_GAME"), FormatGameMode(Client.GetServerGameMode(self.serverIndex), Client.GetServerMaxPlayers(self.serverIndex))))
            self.map:SetText(string.format("%s %s",Locale.ResolveString("SERVERBROWSER_SERVER_DETAILS_MAP"), GetTrimmedMapName(Client.GetServerMapName(self.serverIndex))))

@@ -1459,11 +1494,12 @@ function GUIMainMenu:CreateServerListWindow()

        "game",
        "map",
        "players",

+ "spectators",

        "rate",
        "ping"
    }
    

- local rowNames = { { Locale.ResolveString("SERVERBROWSER_RANK"), Locale.ResolveString("SERVERBROWSER_FAVORITE"), Locale.ResolveString("SERVERBROWSER_PRIVATE"), Locale.ResolveString("SERVERBROWSER_NAME"), Locale.ResolveString("SERVERBROWSER_GAME"), Locale.ResolveString("SERVERBROWSER_MAP"), Locale.ResolveString("SERVERBROWSER_PLAYERS"), Locale.ResolveString("SERVERBROWSER_PERF"), Locale.ResolveString("SERVERBROWSER_PING") } } + local rowNames = { { Locale.ResolveString("SERVERBROWSER_RANK"), Locale.ResolveString("SERVERBROWSER_FAVORITE"), Locale.ResolveString("SERVERBROWSER_PRIVATE"), Locale.ResolveString("SERVERBROWSER_NAME"), Locale.ResolveString("SERVERBROWSER_GAME"), Locale.ResolveString("SERVERBROWSER_MAP"), Locale.ResolveString("SERVERBROWSER_PLAYERS"), Locale.ResolveString("SERVERBROWSER_SPECTATORS"), Locale.ResolveString("SERVERBROWSER_PERF"), Locale.ResolveString("SERVERBROWSER_PING") } }

    local serverList = self.serverList

@@ -1476,8 +1512,9 @@ function GUIMainMenu:CreateServerListWindow()

        { OnClick = function() serverList:SetComparator(SortByMode, nil, 5) end },
        { OnClick = function() serverList:SetComparator(SortByMap, nil, 6) end },
        { OnClick = function() serverList:SetComparator(SortByPlayers, nil, 7) end },

- { OnClick = function() serverList:SetComparator(SortByPerformance, nil, 8) end }, - { OnClick = function() serverList:SetComparator(SortByPing, nil, 9) end } + { OnClick = function() serverList:SetComparator(SortBySpectators, nil, 8) end }, + { OnClick = function() serverList:SetComparator(SortByPerformance, nil, 9) end }, + { OnClick = function() serverList:SetComparator(SortByPing, nil, 10) end }

    }

    --Default sorting

@@ -1491,7 +1528,7 @@ function GUIMainMenu:CreateServerListWindow()

    self.serverRowNames:AddCSSClass("server_list_names")
    self.serverRowNames:SetColumnClassNames(columnClassNames)
    self.serverRowNames:SetEntryCallbacks(entryCallbacks)

- self.serverRowNames:SetRowPattern( { SERVERBROWSER_RANK, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, } ) + self.serverRowNames:SetRowPattern( { SERVERBROWSER_RANK, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, RenderServerNameEntry, } )

    self.serverRowNames:SetTableData(rowNames)
    
    self.serverBrowserWindow:AddEventCallbacks({

@@ -3433,10 +3470,6 @@ end

function GUIMainMenu:OnAnimationsEnd(item)
    

- if item == self.scanLine:GetBackground() then - self.scanLine:SetCSSClass("scanline") - end -

end

function GUIMainMenu:OnAnimationCompleted(animatedItem, animationName, itemHandle)

@@ -4169,4 +4202,32 @@ local function OnCommandDebugTrainingGlow()

end
Event.Hook("Console_debugtrainingglow", OnCommandDebugTrainingGlow)

+--[[ +-- DEBUG cloud nag +Script.Load("lua/challenge/GUIChallengePromptAlien.lua") +local nag +local function OnCommandDebugNag(iconName) + + nag = GetGUIManager():CreateGUIScript("GUIChallengePromptAlien") + nag:SetLayer(999) + nag:SetPromptText("STEAM_CLOUD_NAG_PROMPT") + nag:SetDescriptionText("STEAM_CLOUD_NAG_DESC") + nag:AddButton("YES", function() Log("Yes clicked") end) + nag:AddButton("JUST_THIS_TIME", function() Log("Just this time clicked") end) + nag:AddButton("NO", function() Log("No clicked") end) + nag:SetIcon(iconName or "choice") + +end +Event.Hook("Console_debugnag", OnCommandDebugNag) -- icon name + +local function OnCommandDebugNagShow() + nag:Show(function() Log("hidden now!") end) +end +Event.Hook("Console_debugnagshow", OnCommandDebugNagShow) + +local function OnCommandDebugNagHide() + nag:Hide(function() Log("hidden now!") end) +end +Event.Hook("Console_debugnaghide", OnCommandDebugNagHide) +--]]

diff --git a/ns2/lua/menu/GUIMainMenu_Training.lua b/ns2/lua/menu/GUIMainMenu_Training.lua index d191e4f79..719e85952 100644 --- a/ns2/lua/menu/GUIMainMenu_Training.lua +++ b/ns2/lua/menu/GUIMainMenu_Training.lua @@ -466,6 +466,44 @@ local function CreateTutorialPage(self)

                end
            end,
        })

+ + self.skulkChallengeButton = CreateMenuElement(self.menu, "TutorialMenuButton") + self.skulkChallengeButton:SetCSSClass("play_skulk_challenge") + self.skulkChallengeButtonHighlight = CreateMenuElement(self.skulkChallengeButton, "Image") + self.skulkChallengeButtonHighlight:SetCSSClass("medium_highlight") + self.skulkChallengeButtonHighlight:SetIgnoreEvents(true) + self.skulkChallengeButton.highlight_element = self.skulkChallengeButtonHighlight + self.skulkChallengeButtonText = CreateMenuElement(self.skulkChallengeButton, "Font") + self.skulkChallengeButtonText:SetCSSClass("play_skulk_challenge") + self.skulkChallengeButtonText:SetText(Locale.ResolveString("PLAY_SKULK_CHALLENGE")) + + self.skulkChallengeButton:AddEventCallbacks({ + OnClick = function(self, _, doubleclick) + local onClick = function(self) + Analytics.RecordEvent("training_skulk") + self.scriptHandle:StartSkulkChallenge() + end + + if doubleclick then + onClick(self) + else + self.scriptHandle:SetTutorialPage(self, Locale.ResolveString("TUT_SKULK_CHALLENGE_TOOLTIP"), onClick, "ui/menu/training/skulk_challenge_banner.dds") + end + end, + + OnMouseOver = function(self) + if self.highlight_element then + + self.highlight_element:SetBackgroundColor(Color(1,1,1,1)) + end + end, + + OnMouseOut = function(self) + if self.highlight_element and not self.highlight_element.activePage then + self.highlight_element:SetBackgroundColor(Color(0,0,0,0)) + end + end, + })

    local buttonOrder = {
        self.replayIntroButton,

@@ -475,6 +513,7 @@ local function CreateTutorialPage(self)

        self.playTutorial4Button,
        self.sandboxButton,
        self.hiveChallengeButton,

+ self.skulkChallengeButton,

    }

    for i, button in ipairs(buttonOrder) do

@@ -522,6 +561,28 @@ function GUIMainMenu:StartHiveChallenge()

end

+function GUIMainMenu:StartSkulkChallenge() + + local modIndex = Client.GetLocalModId("challenges/skulk_challenge") + + if modIndex == -1 then + Shared.Message("Hive Challenge mod does not exist!") + return + end + + local password = "dummypassword"..ToString(math.random()) + local port = 27015 + local maxPlayers = 1 -- leaving room for bots + local serverName = "private skulk-challenge server" + local mapName = "ns2_skulk_challenge_1" + Client.SetOptionString("lastServerMapName", mapName) + + if Client.StartServer(modIndex, mapName, serverName, password, port, maxPlayers, true, true) then + LeaveMenu() + end + +end +

function GUIMainMenu:StartAlienTutorial()

    local modIndex = Client.GetLocalModId("bootcamp/alien_1")

@@ -851,10 +912,10 @@ function GUIMainMenu:SelectNextTutorialPage()

        local onClick = function(self)
            Analytics.RecordEvent("training_hive")

- self.scriptHandle:StartHiveChallenge() + self.scriptHandle:StartSkulkChallenge()

        end

- self:SetTutorialPage(self.hiveChallengeButton, Locale.ResolveString("TUT_HIVE_CHALLENGE_TOOLTIP"), onClick, "ui/menu/training/hive_challenge_banner.dds") + self:SetTutorialPage(self.skulkChallengeButton, Locale.ResolveString("TUT_SKULK_CHALLENGE_TOOLTIP"), onClick, "ui/menu/training/skulk_challenge_banner.dds")

    end
end

@@ -866,6 +927,10 @@ function GUIMainMenu:CreateTrainingWindow()

    self:SetupWindow(self.trainingWindow, "TRAINING")
    self.trainingWindow:SetCSSClass("tutorial_window")
    

+ -- for whatever reason, the height specified in main_menu.css is just completely disregarded... so we'll hard + -- code it here... 964 = (1080 * 80%) + 100 (the height of a button) + self.trainingWindow:SetHeight(964, false) +

    if not self.videoPlayer then
        self.videoPlayer = GetGUIManager():CreateGUIScriptSingle("GUITipVideo")
    end

diff --git a/ns2/lua/menu/ServerEntry.lua b/ns2/lua/menu/ServerEntry.lua index 38bfc3459..6d99c97a9 100644 --- a/ns2/lua/menu/ServerEntry.lua +++ b/ns2/lua/menu/ServerEntry.lua @@ -83,6 +83,9 @@ function ServerEntry:Initialize()

    self.playerCount = CreateTextItem(self, true)
    self.playerCount:SetTextAlignmentX(GUIItem.Align_Center)

+ self.spectatorCount = CreateTextItem(self, true) + self.spectatorCount:SetTextAlignmentX(GUIItem.Align_Center) +

    self.rank = CreateTextItem(self, true)
    self.rank:SetTextAlignmentX(GUIItem.Align_Center)
    

@@ -210,6 +213,8 @@ function ServerEntry:SetFontName(fontName)

    self.modName:SetScale(GetScaledVector())
    self.playerCount:SetFontName(fontName)
    self.playerCount:SetScale(GetScaledVector())

+ self.spectatorCount:SetFontName(fontName) + self.spectatorCount:SetScale(GetScaledVector())

    self.rank:SetFontName(fontName)
    self.rank:SetScale(GetScaledVector())

@@ -221,6 +226,7 @@ function ServerEntry:SetTextColor(color)

    self.mapName:SetColor(color)
    self.modName:SetColor(color)
    self.playerCount:SetColor(color)

+ self.spectatorCount:SetColor(color)

    self.rank:SetColor(color)

end

@@ -254,7 +260,7 @@ function ServerEntry:SetServerData(serverData)

    if self.serverData ~= serverData then
    

- local numReservedSlots = GetNumServerReservedSlots(serverData.serverId) + local numReservedSlots = serverData.numRS or 0

        self.playerCount:SetText(string.format("%d/%d", serverData.numPlayers, (serverData.maxPlayers - numReservedSlots)))
        if serverData.numPlayers >= serverData.maxPlayers then
            self.playerCount:SetColor(kRed)

@@ -264,6 +270,13 @@ function ServerEntry:SetServerData(serverData)

            self.playerCount:SetColor(kWhite)
        end

+ self.spectatorCount:SetText(string.format("%d/%d", serverData.numSpectators, serverData.maxSpectators )) + if serverData.numSpectators >= serverData.maxSpectators then + self.spectatorCount:SetColor(kRed) + else + self.spectatorCount:SetColor(kWhite) + end +

        self.rank:SetText(tostring(serverData.rank or 0))
     
        self.serverName:SetText(serverData.name)

@@ -360,9 +373,14 @@ function ServerEntry:SetWidth(width, isPercentage, time, animateFunc, callBack)

        self.mapName:SetPosition(Vector((currentPos + currentPercentage/2 - currentWidth/2), 0, 0))
        
        currentPos = currentPos + currentPercentage + kPaddingSize

- currentPercentage = width * 0.14 + currentPercentage = width * 0.07

        currentWidth = GUIScale(self.playerCount:GetTextWidth(self.playerCount:GetText()))

- self.playerCount:SetPosition(Vector((currentPos + currentPercentage/2 - currentWidth/2), 0, 0)) + self.playerCount:SetPosition(Vector((currentPos + currentPercentage/2 - currentWidth/2), 0, 0)) + + currentPos = currentPos + currentPercentage + kPaddingSize + currentPercentage = width * 0.07 + currentWidth = GUIScale(self.spectatorCount:GetTextWidth(self.spectatorCount:GetText())) + self.spectatorCount:SetPosition(Vector((currentPos + currentPercentage/2 - currentWidth/2), 0, 0))

        currentPos = currentPos + currentPercentage + kPaddingSize
        currentPercentage = width * 0.07

diff --git a/ns2/lua/menu/ServerList.lua b/ns2/lua/menu/ServerList.lua index c03c102d9..4a54f25e0 100644 --- a/ns2/lua/menu/ServerList.lua +++ b/ns2/lua/menu/ServerList.lua @@ -68,6 +68,19 @@ function SortByPlayers(a, b)

end

+function SortBySpectators(a, b) + + local aNumSpectators = tonumber(a.numSpectators) + local bNumSpectators = tonumber(b.numSpectators) + + if not gSortReversed then + return aNumSpectators > bNumSpectators + else + return aNumSpectators < bNumSpectators + end + +end +

function SortByPrivate(a, b)

    local aValue = a.requiresPassword and 1 or 0

diff --git a/ns2/lua/menu/TextInput.lua b/ns2/lua/menu/TextInput.lua index d49ee66d0..b840426ea 100644 --- a/ns2/lua/menu/TextInput.lua +++ b/ns2/lua/menu/TextInput.lua @@ -19,6 +19,8 @@ local kDefaultMaxLength = 32 -- This is the max steam profile length

local kDefaultSize = Vector(300, 48, 0)

+local kHiddenChar = "•" +

class 'TextInput' (FormElement)

local function UpdateCursorPosition(self)

@@ -141,12 +143,33 @@ function TextInput:SetFieldColor(color)

    self:SetBackgroundColor(color)
end

-local function IsANumber(self, character) +function TextInput:SetIsSecret(isSecret) + if isSecret == self.isSecret then return end + + self.isSecret = isSecret + + self:SetValue(self:GetValue()) +end + +function TextInput:GetIsSecret() + return self.isSecret or false +end + +function TextInput:GetValue(obscure) + local value = FormElement.GetValue(self)

- local oldValue = self.text:GetText() - self.text:SetWideText(character) - local characterString = self.text:GetText() - self.text:SetText(oldValue) + if type(value) ~= "string" then + value = ConvertWideStringToString(value) + end + + if obscure and self:GetIsSecret() then + return kHiddenChar:rep(#value) + end + + return value +end + +local function IsANumber(self, characterString)

    return characterString == "0" or characterString == "1" or characterString == "2" or characterString == "3" or characterString == "4" or 
           characterString == "5" or characterString == "6" or characterString == "7" or characterString == "8" or characterString == "9" or

@@ -155,30 +178,29 @@ local function IsANumber(self, character)

end

function TextInput:AddCharacter(character)

+ character = ConvertWideStringToString(character)

    if not self.numbersOnly or IsANumber(self, character) then

- self:SetValue(self.text:GetWideText() .. character) + self:SetValue(self:GetValue() .. character)

    end
    
end

function TextInput:RemoveCharacter()

- local currentText = self.text:GetWideText() + local currentText = FormElement.GetValue(self)

    local length = #currentText

+

    self:SetValue(currentText:sub(1, length - 1))
end

function TextInput:SetValue(value)

- - if type(value) == "string" then - self.text:SetText(value) - else - self.text:SetWideText(value) - end + FormElement.SetValue(self, value) + + value = self:GetValue(true) --value may be obscured in case we are set to be a secret + + self.text:SetText(value)

    UpdateCursorPosition(self)

- - FormElement.SetValue(self, self.text:GetText())

end

diff --git a/ns2/lua/menu/main_menu.css b/ns2/lua/menu/main_menu.css index 8d347f07a..ae4eb1c31 100644 --- a/ns2/lua/menu/main_menu.css +++ b/ns2/lua/menu/main_menu.css @@ -727,7 +727,7 @@ window.main_frame image.menu_bg_show

    left: 0px; 
    width: 630; 

- height: 655; + height: 700;

    vertical-align: center; 
    top: -60%px; 

@@ -1226,9 +1226,13 @@ window.passwordprompt_window

    left: -200px; 
    top: -100px; 
    vertical-align: center; 

- horizontal-align: center; - border-width: 0px; - background-color: rgba(0.3, 0.3, 0.3, 0.75); + horizontal-align: center; + + background-color: #0c0F10; + opacity: 0.85; + + border-width: 1px; + border-color: #4F7E91;

} 
  
window.serverdetails_window font 

@@ -1252,8 +1256,7 @@ window.serverdetails_window

    left: -300px; 
    top: -400px; 
    vertical-align: center; 

- horizontal-align: center; - border-width: 0px; + horizontal-align: center;

    background-color: #0c0F10;
    opacity: 0.85;

@@ -1266,7 +1269,7 @@ window.serverdetails_window

window.autojoin_window 
{ 
    scaling: true; 

- width: 400px; + width: 500px;

    height: 280px; 
    left: -200px; 
    top: -100px; 

@@ -1515,7 +1518,13 @@ font.dontshowagain

window.passwordprompt_window titlebar 
{ 

- height: 36px; + height: 36px; + + background-image: path(ui/menu/serverbrowser/rownamesbackground.dds); + opacity: 1; + + border-width: 1px; + border-color: #4F7E91;

} 
  
window.autojoin_window titlebar 

@@ -1529,29 +1538,31 @@ window.passwordprompt_window form

    top: 32px; 
} 
  

-window.passwordprompt_window titlebar button -{ - - height: 32px; - width: 32px; - border-color: #FFFFFF; - -} +window.passwordprompt_window titlebar button.close { + background-image: path(ui/menu/serverbrowser/close.dds); + background-color: #ebebeb; + opacity: 1; + + border-width: 0px; + width: 32px; + height: 32px; + right: 0 +}

form textinput.serverpassword 
{ 
    top: 30px; 

- width: 300px; - horizontal-align: center; - left: -150px; + width: 350px; + left: 25px;

} 
  
font.passwordprompt_title 
{ 

- text-color: #CCCCCC; - font-name: fonts/AgencyFB_small.fnt; - opacity: 0; - left: 30px; + text-color: #8bb4c1; + font-name: fonts/AgencyFB_medium.fnt; + opacity: 0; + text-padding: 2px; + left:10px;

} 
  
window.main_menu_window titlebar 

@@ -1638,7 +1649,7 @@ button.bottomcenter

    text-color: #8bb4c1; 
    text-padding: 16px; 
      

- bottom: 0px; + bottom: 20px;

    left: -128px; 
} 
  

@@ -1789,7 +1800,27 @@ button.getmods

    bottom: 0px; 
    left: 0px; 
} 

- + +button.displaypassword_toggle +{ + height: 48px; + width: 48px; + + border-width: 0px; + border-color: #9E9E9E; + + top: 30px; + right: 24px; + + background-image: path(ui/menu/serverbrowser/eye.dds); + background-color: #4F7E91; + hover-background-color: #8bb4c1; + + font-name: fonts/AgencyFB_large.fnt; + text-color: #8bb4c1; + text-padding: 16px; +} +

window content 
{ 
    ignore-margin: false; 

@@ -1805,15 +1836,15 @@ window content

} 
  
window.serverdetails_window content 

-{ - top: 275px; +{ + top: 307px;

    border-width: 1px; 
    border-color: rgb(0.2, 0.2, 0.2); 
    opacity: 0.6; 
      
    left: 20px;
    width: 530px;

- height: 352px; + height: 320px;

}

@@ -2057,12 +2088,17 @@ entry.map

{ 
    width: 15%; 
} 

- -entry.players -{ - width: 14%; -} - + +entry.players +{ + width: 7%; +} + +entry.spectators +{ + width: 7%; +} +

entry.rate
{ 
    width: 7%;

@@ -3015,7 +3051,7 @@ font.bot_note

.tutorial_menu {
    width: 350px;

- height: 672px; + height: 772px;

    background-color: #0c0F10;
    opacity: 0.90;

@@ -3027,7 +3063,7 @@ font.bot_note

.tutorial_body {
    left: 350px;
    width: 600px;

- height: 672px; + height: 772px;

    background-color: #0c0F10;
    opacity: 0.85;

@@ -3078,7 +3114,7 @@ font.bot_note

}

-button.play_tutorial1, button.play_tutorial2, button.play_tutorial3, button.play_tutorial4, button.play_sandbox, button.replay_intro_video, button.play_hive_challenge +button.play_tutorial1, button.play_tutorial2, button.play_tutorial3, button.play_tutorial4, button.play_sandbox, button.replay_intro_video, button.play_hive_challenge, button.play_skulk_challenge

{
    font-name: fonts/AgencyFB_medium.fnt; 
    text-color: #4F7E91;

@@ -3125,6 +3161,11 @@ button.play_hive_challenge

    background-image: path(ui/menu/hive_challenge_button.dds)
}

+button.play_skulk_challenge +{ + background-image: path(ui/menu/skulk_challenge_button.dds) +} +

font.play_tutorial, font.play_tutorial2, font.play_tutorial3, font.play_tutorial4
{

@@ -3189,7 +3230,7 @@ form.tutorial_footer_bg

{
    width: 100%;
    height: 64px;

- top: 672px; + top: 772px;

    border-color: #4F7E91;
    border-width: 1px;

@@ -3264,6 +3305,24 @@ image.small_highlight

    width: 300px;
}

+font.play_skulk_challenge +{ + font-name: fonts/AgencyFB_medium.fnt; + text-color: #8bb4c1; + text-align: center; + background-color: rgba(0,0,0,0); + + text-padding: 0px; + height: 100px; + width: 400px; + left: 0px; + top: -16px; + border-width: 0px; + + vertical-align: center; + horizontal-align: center; +} +

font.play_hive_challenge
{
    font-name: fonts/AgencyFB_medium.fnt;

</source>