diff --git a/[gamemodes]/[deathmatch]/deathmatch/client/hud.lua b/[gamemodes]/[deathmatch]/deathmatch/client/hud.lua
index be363ede8..a9988c5ea 100644
--- a/[gamemodes]/[deathmatch]/deathmatch/client/hud.lua
+++ b/[gamemodes]/[deathmatch]/deathmatch/client/hud.lua
@@ -48,22 +48,13 @@ _hud.scoreDisplay.update = function()
)
end
--- respawn screen
-_hud.respawnScreen = {}
--- respawn counter (You will respawn in x seconds)
-_hud.respawnScreen.respawnCounter = dxText:create("", 0.5, 0.5, true, "beckett", 4)
-_hud.respawnScreen.respawnCounter:type("stroke", 6)
-_hud.respawnScreen.respawnCounter:color(255, 225, 225)
-_hud.respawnScreen.respawnCounter:visible(false)
-_hud.respawnScreen.setVisible = function(_, visible)
- _hud.respawnScreen.respawnCounter:visible(visible)
-end
-_hud.respawnScreen.startCountdown = function()
- if _respawnTime > 0 then
- startCountdown(_respawnTime)
- else
- _hud.respawnScreen.respawnCounter:text("Wasted")
- end
+-- wasted screen
+_hud.wastedScreen = {}
+_hud.wastedScreen.text = dxText:create("Wasted", 0.5, 0.5, true, "beckett", 4)
+_hud.wastedScreen.text:type("border", 2)
+_hud.wastedScreen.text:color(255, 0, 0)
+_hud.wastedScreen.setVisible = function(_, visible)
+ _hud.wastedScreen.text:visible(visible)
end
-- end screen
@@ -93,49 +84,22 @@ _hud.endScreen.update = function(_, winner, draw, aborted)
playSound("client/audio/mission_accomplished.mp3")
end
end
+
+-- spectate screen
+_hud.spectateScreen = {}
+-- spectating info label
+_hud.spectateScreen.infoLabel = dxText:create("You are currently spectating.\nUse left and right arrow to switch players.", 0, 0, false, "default-bold", 2)
+_hud.spectateScreen.infoLabel:color(225, 225, 225, 225)
+_hud.spectateScreen.infoLabel:boundingBox(0, 0.8, 1, 1, true)
+_hud.spectateScreen.infoLabel:align("bottom", "center")
+_hud.spectateScreen.infoLabel:type("border", 2)
+_hud.spectateScreen.setVisible = function(_, visible)
+ _hud.spectateScreen.infoLabel:visible(visible)
+end
+
-- hide all HUD elements by default
_hud.loadingScreen:setVisible(false)
_hud.scoreDisplay:setVisible(false)
-_hud.respawnScreen:setVisible(false)
+_hud.wastedScreen:setVisible(false)
_hud.endScreen:setVisible(false)
-
--- TODO: clean this junk up
-local function dxSetAlpha ( dx, a )
- local r,g,b = dx:color()
- dx:color(r,g,b,a)
-end
-
-local countdownCR
-local function countdown(time)
- for i=time,0,-1 do
- _hud.respawnScreen.respawnCounter:text("Wasted\n"..i)
- setTimer ( countdownCR, 1000, 1 )
- coroutine.yield()
- end
-end
-
-local function hideCountdown()
- setTimer (
- function()
- _hud.respawnScreen:setVisible(false)
- end,
- 600, 1
- )
- Animation.createAndPlay(
- _hud.respawnScreen.respawnCounter,
- {{ from = 225, to = 0, time = 400, fn = dxSetAlpha }}
- )
- removeEventHandler ( "onClientPlayerSpawn", localPlayer, hideCountdown )
-end
-
-function startCountdown(time)
- Animation.createAndPlay(
- _hud.respawnScreen.respawnCounter,
- {{ from = 0, to = 225, time = 600, fn = dxSetAlpha }}
- )
- addEventHandler ( "onClientPlayerSpawn", localPlayer, hideCountdown )
- _hud.respawnScreen:setVisible(true)
- time = math.floor(time/1000)
- countdownCR = coroutine.wrap(countdown)
- countdownCR(time)
-end
+_hud.spectateScreen:setVisible(false)
\ No newline at end of file
diff --git a/[gamemodes]/[deathmatch]/deathmatch/client/main.lua b/[gamemodes]/[deathmatch]/deathmatch/client/main.lua
index b514199bc..cc36300c0 100644
--- a/[gamemodes]/[deathmatch]/deathmatch/client/main.lua
+++ b/[gamemodes]/[deathmatch]/deathmatch/client/main.lua
@@ -1,3 +1,5 @@
+local _wastedTimer, _respawnTimer
+
--
-- startGamemodeClient: initializes the gamemode client
--
@@ -9,6 +11,8 @@ local function startGamemodeClient()
exports.scoreboard:scoreboardSetSortBy("Rank")
-- fade out camera
fadeCamera(false, 0)
+ -- disable zone name HUD element
+ setPlayerHudComponentVisible("area_name", false)
-- if a game is in progress, apply the loading camera matrix
if getElementData(resourceRoot, "gameState") == GAME_IN_PROGRESS then
setCameraMatrix(unpack(calculateLoadingCameraMatrix()))
@@ -25,6 +29,8 @@ local function stopGamemodeClient()
exports.scoreboard:scoreboardRemoveColumn("Rank")
-- hide scoreboard
exports.scoreboard:setScoreboardForced(false)
+ -- re-enable zone name HUD element
+ setPlayerHudComponentVisible("area_name", true)
end
addEventHandler("onClientResourceStop", resourceRoot, stopGamemodeClient)
@@ -68,10 +74,15 @@ addEventHandler("onClientGamemodeMapStop", resourceRoot, stopGamemodeMap)
-- startGamemodeRound: triggered when a round begins
--
local function startGamemodeRound()
- -- attach player wasted handler
- addEventHandler("onClientPlayerWasted", localPlayer, _hud.respawnScreen.startCountdown)
+ -- attach player spawn and wasted handler
+ addEventHandler("onClientPlayerSpawn", localPlayer, localPlayerSpawn)
+ addEventHandler("onClientPlayerWasted", localPlayer, localPlayerWasted)
-- attach element data change handler
addEventHandler("onClientElementDataChange", root, elementDataChange)
+ -- stop spectating
+ if isSpectating() then
+ stopSpectating(true)
+ end
-- hide end/loading screens and scoreboard
_hud.loadingScreen:setVisible(false)
_hud.endScreen:setVisible(false)
@@ -87,30 +98,73 @@ addEventHandler("onClientGamemodeRoundStart", resourceRoot, startGamemodeRound)
-- stopGamemodeRound: triggered when a round ends
--
local function stopGamemodeRound(winner, draw, aborted)
- -- remove player wasted handler and hide respawn screen if active
- removeEventHandler("onClientPlayerWasted", localPlayer, _hud.respawnScreen.startCountdown)
- _hud.respawnScreen.setVisible(false)
+ -- remove player spawn & wasted handler and hide respawn screen if active
+ removeEventHandler("onClientPlayerWasted", localPlayer, localPlayerWasted)
+ removeEventHandler("onClientPlayerSpawn", localPlayer, localPlayerSpawn)
-- remove element data change handler
removeEventHandler("onClientElementDataChange", root, elementDataChange)
-- hide score display
_hud.scoreDisplay:setVisible(false)
+ -- hide wasted screen and cancel the wasted and respawn timers
+ _hud.wastedScreen:setVisible(false)
+ if isTimer(_wastedTimer) then
+ killTimer(_wastedTimer)
+ end
+ if isElement(_respawnTimer) then
+ destroyElement(_respawnTimer)
+ end
-- spectate the winner
if winner and player ~= winner then
- if winner then
- setCameraTarget(winner)
+ startSpectating(winner)
+ end
+ -- exit spectate mode and go to black if round was aborted
+ if aborted then
+ if isSpectating() then
+ stopSpectating(true)
end
- toggleAllControls(true, true, false)
+ fadeCamera(false, 0)
+ else
+ -- begin fading out the screen
+ fadeCamera(false, ROUND_START_DELAY/1000)
+ -- show end screen and scoreboard
+ _hud.endScreen:update(winner, draw, aborted)
+ _hud.endScreen:setVisible(true)
+ exports.scoreboard:setScoreboardForced(true)
end
- -- begin fading out the screen
- fadeCamera(false, CAMERA_LOAD_DELAY/1000)
- -- show end screen and scoreboard
- _hud.endScreen:update(winner, draw, aborted)
- _hud.endScreen:setVisible(true)
- exports.scoreboard:setScoreboardForced(true)
end
addEvent("onClientGamemodeRoundEnd", true)
addEventHandler("onClientGamemodeRoundEnd", resourceRoot, stopGamemodeRound)
+--
+-- localPlayerWasted: triggered when local player is killed
+--
+function localPlayerWasted()
+ -- show the wasted screen
+ _hud.wastedScreen:setVisible(true)
+
+ -- set timer to show the spectate screen
+ _wastedTimer = setTimer(startSpectating, WASTED_CAMERA_DURATION, 1)
+
+ -- create a respawn timer is repawn is enabled
+ if _respawnTime > 0 then
+ _respawnTimer = exports.missionTimer:createMissionTimer(WASTED_CAMERA_DURATION + _respawnTime, true, "You will respawn in %s seconds", 0.5, 50, true, "default-bold", 1)
+ end
+end
+
+--
+-- localPlayerSpawn: triggered when local player is spawned
+--
+function localPlayerSpawn()
+ -- if we're spectating, stop spectating
+ if isSpectating() then
+ stopSpectating()
+ end
+ -- kill the respawn timer if it exists
+ if isElement(_respawnTimer) then
+ destroyElement(_respawnTimer)
+ end
+end
+
--
-- elementDataChange: triggered when element data changes - used to track score changes
--
diff --git a/[gamemodes]/[deathmatch]/deathmatch/client/spectate.lua b/[gamemodes]/[deathmatch]/deathmatch/client/spectate.lua
new file mode 100644
index 000000000..008d72b16
--- /dev/null
+++ b/[gamemodes]/[deathmatch]/deathmatch/client/spectate.lua
@@ -0,0 +1,168 @@
+local _spectating = false
+local _currentTarget
+local _validTargets = {}
+
+--
+-- startSpectating([target]): starts spectating the targted player, or a random one if target == nil
+--
+function startSpectating(target)
+ -- fade camera out, hide radar hud and score screen
+ fadeCamera(false, 0)
+ setPlayerHudComponentVisible("radar", false)
+ _hud.scoreDisplay:setVisible(false)
+
+ -- hide wasted screen and destroy wasted timer
+ _hud.wastedScreen:setVisible(false)
+ if isElement(_wastedTimer) then
+ destroyElement(_wastedTimer)
+ end
+
+ -- if target is nil, pick a random player
+ if not target then
+ target = _validTargets[math.random(1, #_validTargets)]
+ end
+
+ -- if there still isn't a target, error out
+ if not target then
+ -- TODO: handle this more gracefully
+ error("no valid spectate target", 2)
+ end
+
+ -- set camera target and disable controls
+ iprint(target)
+ setCameraTarget(target)
+ toggleAllControls(false, true, false)
+
+ -- show spectate screen
+ _hud.spectateScreen:setVisible(true)
+
+ -- bind left and right arrow keys to cycle spectate target
+ bindKey("left", "down", cycleSpectateTarget, true)
+ bindKey("right", "down", cycleSpectateTarget)
+
+ -- fade camera in next frame
+ setTimer(fadeCamera, 50, 1, true, 1)
+
+ _spectating = true
+ _currentTarget = nil
+end
+
+--
+-- stopSpectating([fadeOut]): exits spectate mode. if fadeOut == true camera will not fade back in
+--
+function stopSpectating(fadeOut)
+ -- fade camera out, restore radar hud and score screen
+ fadeCamera(false, 0)
+ setPlayerHudComponentVisible("radar", true)
+ _hud.scoreDisplay:setVisible(true)
+
+ -- reset camera target and controls
+ setCameraTarget(localPlayer)
+ toggleAllControls(true, true, false)
+
+ -- hide spectate screen
+ _hud.spectateScreen:setVisible(false)
+
+ -- bind left and right arrow keys to cycle spectate target
+ unbindKey("left", "down", cycleSpectateTarget)
+ unbindKey("right", "down", cycleSpectateTarget)
+
+ -- fade camera in next frame
+ if not fadeOut then
+ setTimer(fadeCamera, 50, 1, true, 1)
+ end
+
+ _spectating = false
+ _currentTarget = nil
+end
+
+--
+-- isSpectating(): returns true if local player is spectating, false otherwise
+--
+function isSpectating()
+ return _spectating
+end
+
+--
+-- setSpectateTarget(target): updates spectate target
+--
+function setSpectateTarget(target)
+ if not _spectating then
+ error("local player is not spectating", 2)
+ end
+
+ if target == _currentTarget then
+ return
+ end
+
+ _currentTarget = target
+ setCameraTarget(target)
+end
+
+--
+-- cycleSpectateTarget(): cycles to next or previous target while in spectate mode
+--
+function cycleSpectateTarget(previous)
+ if not _spectating then
+ error("local player is not spectating", 2)
+ end
+
+ local index = 1
+ for i, validTarget in ipairs(_validTargets) do
+ if validTarget == _currentTarget then
+ index = i
+ break
+ end
+ end
+
+ if previous then
+ index = index - 1
+ if index < 1 then
+ index = #_validTargets
+ end
+ else
+ index = index + 1
+ if index > #_validTargets then
+ index = 1
+ end
+ end
+
+ setSpectateTarget(_validTargets[index])
+end
+
+--
+-- functions to update target list on player spawn, death, and resource start
+--
+local function addValidTarget(playerTeam)
+ if source == localPlayer then
+ return
+ end
+
+ table.insert(_validTargets, source)
+end
+addEventHandler("onClientPlayerSpawn", root, addValidTarget)
+
+local function removeValidTarget()
+ if source == localPlayer then
+ return
+ end
+
+ for i, target in ipairs(_validTargets) do
+ table.remove(_validTargets, i)
+ end
+end
+addEventHandler("onClientPlayerWasted", root, removeValidTarget)
+
+local function refreshValidTargets()
+ local players = getElementsByType("player", root)
+
+ -- remove local player and dead players from list
+ for i, player in ipairs(players) do
+ if player == localPlayer or isPedDead(player) then
+ table.remove(players, i)
+ end
+ end
+
+ _validTargets = players
+end
+addEventHandler("onClientResourceStart", resourceRoot, refreshValidTargets)
\ No newline at end of file
diff --git a/[gamemodes]/[deathmatch]/deathmatch/meta.xml b/[gamemodes]/[deathmatch]/deathmatch/meta.xml
index fb1d35f7e..55ea4e3ff 100644
--- a/[gamemodes]/[deathmatch]/deathmatch/meta.xml
+++ b/[gamemodes]/[deathmatch]/deathmatch/meta.xml
@@ -1,5 +1,5 @@
-
+
@@ -25,6 +25,7 @@
+
diff --git a/[gamemodes]/[deathmatch]/deathmatch/server/main.lua b/[gamemodes]/[deathmatch]/deathmatch/server/main.lua
index 9fbab16f1..0507f1d05 100644
--- a/[gamemodes]/[deathmatch]/deathmatch/server/main.lua
+++ b/[gamemodes]/[deathmatch]/deathmatch/server/main.lua
@@ -3,9 +3,9 @@ _respawnTimers = {} -- lookup table for respawn timers
-- default map settings
local defaults = {
- fragLimit = 10, -- TODO: this should be 10
+ fragLimit = 10,
timeLimit = 600, --10 minutes
- respawnTime = 10,
+ respawnTime = 10, -- 10 seconds
spawnWeapons = "22:100", -- "weaponID:ammo,weaponID:ammmo"
}
@@ -37,10 +37,10 @@ addEventHandler("onGamemodeStop", resourceRoot, stopGamemode)
--
-- startGamemodeMap: initializes a gamemode map
--
-local function startGamemodeMap(resource)
+local function startGamemodeMap(mapResource)
-- load map settings
- _mapResource = resource
- local resourceName = getResourceName(resource)
+ _mapResource = mapResource
+ local resourceName = getResourceName(mapResource)
_fragLimit = tonumber(get(resourceName..".frag_limit")) and math.floor(tonumber(get(resourceName..".frag_limit"))) or defaults.fragLimit
_timeLimit = (tonumber(get(resourceName..".time_limit")) and math.floor(tonumber(get(resourceName..".time_limit"))) or defaults.timeLimit)*1000
_respawnTime = (tonumber(get(resourceName..".respawn_time")) and math.floor(tonumber(get(resourceName..".respawn_time"))) or defaults.respawnTime)*1000
@@ -60,11 +60,11 @@ local function startGamemodeMap(resource)
end
end
-- if the map title is not defined in the map's meta.xml, use the resource name
- _mapTitle = getResourceInfo(resource, "name")
+ _mapTitle = getResourceInfo(mapResource, "name")
if not _mapTitle then
_mapTitle = resourceName
end
- _mapAuthor = getResourceInfo(resource, "author")
+ _mapAuthor = getResourceInfo(mapResource, "author")
-- update game state
setElementData(resourceRoot, "gameState", GAME_STARTING)
-- inform all ready players that the game is about to start
@@ -74,14 +74,19 @@ local function startGamemodeMap(resource)
end
end
-- schedule round to begin
- setTimer(beginRound, CAMERA_LOAD_DELAY, 1)
+ _startTimer = exports.missiontimer:createMissionTimer(ROUND_START_DELAY, true, "Next round begins in %s seconds", 0.5, 20, true, "default-bold", 1)
+ addEventHandler("onMissionTimerElapsed", _startTimer, beginRound)
end
addEventHandler("onGamemodeMapStart", root, startGamemodeMap)
--
-- stopGamemodeMap: cleans up a gamemode map
--
-local function stopGamemodeMap(resource)
+local function stopGamemodeMap(mapResource)
+ -- kill start timer, if it exists
+ if isElement(_startTimer) then
+ destroyElement(_startTimer)
+ end
-- end the round
endRound(false, false, true)
-- update game state
@@ -117,3 +122,42 @@ function calculatePlayerRanks()
end
end
end
+
+--
+-- checkElementData(): secures element data against unauthorized changes
+--
+function checkElementData(key, oldValue, newValue)
+ -- if the change was server-side, ignore it
+ if not client then
+ return
+ end
+
+ local revert = true
+
+ -- if the change by the client was on resourceRoot, revert it
+ if source == resourceRoot then
+ revert = true
+ end
+
+ -- if the change by the client was a player's rank or score, revert it
+ if getElementType(source) == "player" and (key == "Rank" or key == "Score") then
+ revert = true
+ end
+
+ if not revert then
+ return
+ end
+
+ -- revert the change and output a warning
+ setElementData(source, key, oldValue)
+ local warning = string.format(
+ "Unauthorized element data change detected: client = %s, element = %s, key = %s, oldValue = %s, newValue = %s",
+ getPlayerName(client),
+ getElementType(source) == "player" and getPlayerName(source) or tostring(source),
+ tostring(key),
+ tostring(oldValue),
+ tostring(newValue)
+ )
+ outputDebugString(warning, 2)
+end
+addEventHandler("onElementDataChange", resourceRoot, checkElementData)
\ No newline at end of file
diff --git a/[gamemodes]/[deathmatch]/deathmatch/server/player.lua b/[gamemodes]/[deathmatch]/deathmatch/server/player.lua
index 569630954..12dfa9cea 100644
--- a/[gamemodes]/[deathmatch]/deathmatch/server/player.lua
+++ b/[gamemodes]/[deathmatch]/deathmatch/server/player.lua
@@ -3,6 +3,8 @@
--
local function processPlayerJoin()
_playerStates[source] = PLAYER_JOINED
+ -- begin protecting player element data
+ addEventHandler("onElementDataChange", source, checkElementData)
-- initialize player score data
setElementData(source, "Score", 0)
setElementData(source, "Rank", "-")
@@ -28,7 +30,10 @@ addEventHandler("onPlayerQuit", root, processPlayerQuit)
-- gamemodePlayerReady: triggered when a client is ready to play
--
-- triggered by the client post-onClientResourceStart
- function gamemodePlayerReady()
+ local function gamemodePlayerReady(loadedResource)
+ if loadedResource ~= resource then
+ return
+ end
-- inform client of current game state by triggering certain events
local gameState = getElementData(resourceRoot, "gameState")
if gameState == GAME_STARTING then
@@ -104,6 +109,6 @@ function processPlayerWasted(totalAmmo, killer, killerWeapon, bodypart)
calculatePlayerRanks()
-- set timer to respawn player
if _respawnTime > 0 then
- _respawnTimers[source] = setTimer(spawnGamemodePlayer, _respawnTime, 1, source)
+ _respawnTimers[source] = setTimer(spawnGamemodePlayer, _respawnTime + WASTED_CAMERA_DURATION, 1, source)
end
end
diff --git a/[gamemodes]/[deathmatch]/deathmatch/server/round.lua b/[gamemodes]/[deathmatch]/deathmatch/server/round.lua
index 778f48e7e..d20540763 100644
--- a/[gamemodes]/[deathmatch]/deathmatch/server/round.lua
+++ b/[gamemodes]/[deathmatch]/deathmatch/server/round.lua
@@ -2,9 +2,11 @@
-- beginRound: begins the round
--
function beginRound()
+ -- destroy start timer
+ destroyElement(_startTimer)
-- start round timer
if _timeLimit > 0 then
- _missionTimer = exports.missiontimer:createMissionTimer(_timeLimit, true, true, 0.5, 20, true, "default-bold", 1)
+ _missionTimer = exports.missiontimer:createMissionTimer(_timeLimit, true, "%m:%s", 0.5, 20, true, "default-bold", 1)
addEventHandler("onMissionTimerElapsed", _missionTimer, onTimeElapsed)
end
-- attach player wasted handler
@@ -76,6 +78,6 @@ function endRound(winner, draw, aborted)
if mapcycler and getResourceState(mapcycler) == "running" then
triggerEvent("onRoundFinished", resourceRoot)
else
- setTimer(beginRound, CAMERA_LOAD_DELAY * 2, 1)
+ setTimer(beginRound, ROUND_START_DELAY * 2, 1)
end
end
diff --git a/[gamemodes]/[deathmatch]/deathmatch/shared/shared.lua b/[gamemodes]/[deathmatch]/deathmatch/shared/shared.lua
index ab84255bf..908627f85 100644
--- a/[gamemodes]/[deathmatch]/deathmatch/shared/shared.lua
+++ b/[gamemodes]/[deathmatch]/deathmatch/shared/shared.lua
@@ -1,4 +1,5 @@
-CAMERA_LOAD_DELAY = 5000 -- delay used at beginning and end of round (ms)
+ROUND_START_DELAY = 5000 -- delay used at beginning and end of round (ms)
+WASTED_CAMERA_DURATION = 3000 -- duration of the wasted camera (ms)
--
-- enum: creates a c-style enum