├── .gitignore ├── README.md ├── ctf-gamemode ├── README.md ├── ctf_client.lua ├── ctf_config.lua ├── ctf_rendering.lua ├── ctf_server.lua ├── ctf_shared.lua ├── fxmanifest.lua └── loadscreen │ ├── css │ ├── bankgothic.ttf │ └── loadscreen.css │ ├── index.html │ ├── js │ └── loadscreen.js │ └── loadscreen.jpg └── tdm-gamemode ├── README.md ├── fxmanifest.lua ├── tdm_client.lua ├── tdm_config.lua ├── tdm_server.lua └── tdm_shared.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gamemode Examples 2 | 3 | Welcome to the Gamemode Examples Repository. Here, you'll find resources for a variety of gamemodes. Our primary goal is to provide documented official methods for creating gamemodes, thoroughly reviewed by our team. Please note that these resources may evolve to incorporate technological advancements and community feedback. 4 | 5 | Additionally, we may introduce new gamemodes in the future. 6 | 7 | Explore, learn, and stay tuned for updates as we continue to expand our repository! 8 | 9 | ### These resources cover the following topics 10 | 11 | - Usage of natives 12 | - Use of events 13 | - Use of spawn manager 14 | - State bags 15 | 16 | 17 | ## Resources 18 | 19 | In FiveM, a 'resource' is essentially a structured folder containing various files that contribute to modifying or enhancing gameplay on a FiveM server. 20 | 21 | These resources can include scripts for gameplay mechanics, assets like models and textures for new objects or vehicles, and other files that enhance the overall gaming experience on a FiveM server. 22 | 23 | Server owners and developers can use these resources to customize and tailor their server to their preferences and the desires of their community. 24 | 25 | - [TDM Gamemode](./tdm-gamemode): A simple team death match game mode where players are put in teams and are allowed to compete against each other. 26 | - [CTF Gamemode](./ctf-gamemode): A game mode that involves capturing an objective by taking it from point A to B. 27 | 28 | ## Getting Started 29 | 30 | We recommend checking out this [guide](https://docs.fivem.net/docs/scripting-manual/introduction/creating-your-first-script/) *(Creating your first script in Lua)*, as a starting point to set up the game modes. This guide assumes you already have a server set up, if not, you may follow one of [these guides](https://docs.fivem.net/docs/server-manual/setting-up-a-server/) *(Setting up a server)*. 31 | 32 | ### The Resource Manifest 33 | 34 | The manifest file (`fxmanifest.lua`) is used to define what files/scripts are used by the resource. More about it can be found in [Introduction to resources](https://docs.fivem.net/docs/scripting-manual/introduction/introduction-to-resources/). 35 | 36 | ### Natives 37 | 38 | It's important to note that when you browse the files mentioned above, you will see many function calls that don't seem to be declared anywhere; those are most likely natives, i.e., [`SetEntityCoords`](https://docs.fivem.net/natives/?_0xDF70B41B). 39 | 40 | Natives are used to call in-game function methods that execute larger chunks of game logic within the game. It's how the game mode communicates with the game-client (when called via client scripts such as `client.lua` or `shared.lua`). 41 | 42 | If called on the server side, it may call server-related natives and not client natives, but client natives can still be called by the server via RPC (Remote Procedure Call). 43 | 44 | The full list of natives and their corresponding documentation can be found [here](https://docs.fivem.net/natives/). 45 | 46 | ### Events 47 | 48 | The game mode makes use of events (server and client) to communicate data back and forth. Documentation regarding events can be found [here](https://docs.fivem.net/docs/scripting-manual/working-with-events/triggering-events/). 49 | -------------------------------------------------------------------------------- /ctf-gamemode/README.md: -------------------------------------------------------------------------------- 1 | # Capture The Flag (CTF) Game Mode 2 | 3 | Welcome! 4 | 5 | This directory contains the source code and resources for implementing a CTF game mode in your game project. 6 | 7 | Whether you're building a multiplayer game, experimenting with game mechanics, or enhancing an existing project, this CTF game mode provides a starting point to get you started. 8 | 9 | ## Server/Client Logic 10 | 11 | - **ctf_client.lua:** Handles flag logic and team selection. 12 | - **ctf_rendering.lua:** Handles drawing on the client. 13 | - **ctf_server.lua:** Handles most of the CTF logic (this involves capturing, dropping and taking the flag). 14 | - **ctf_shared.lua:** Has some structs (and lua tables) and a single function that is shared by the server and the client. Documentation about the `shared_script` directive can be found [here](https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/#shared_script). 15 | 16 | ### Multiple classes are present 17 | 18 | For the server logic, team and flag objects 'classes' are used, Lua supports such in the form of metatables under [chapter 16.1](https://www.lua.org/pil/16.1.html). 19 | 20 | Classes used in this game-mode are detailed down below: 21 | 22 | - **CTFGame:** The main class that simply holds a `CTFGame:Update` method, its `constructor` (to initialize flags and teams) and a `CTFGame:shutDown` method that is used to 'dispose' of any flag or team instances once `onResourceStop` gets called. 23 | - **Flag:** Used for each `Flag` instance, each `Flag` can have different states and they're tied to a team. 24 | - **Team:** An instance of `Team` stores the base position and each team color, teams (referred by `CTFGame` as `self.teams`) are **Blue**, **Red** and **Spectator** (where **Spectator** is simply a placeholder at the moment). 25 | 26 | ## Features 27 | 28 | This game mode utilizes the following FiveM features: 29 | 30 | - [**State Bags**](https://docs.fivem.net/docs/scripting-manual/networking/state-bags): To keep track of any entity states between client and server. 31 | 32 | ## Small Todos 33 | 34 | - Put the bases farther away from each other, mainly close for testing. 35 | -------------------------------------------------------------------------------- /ctf-gamemode/ctf_client.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File: ctf_client.lua 3 | Description: 4 | This is the main client file and its main purpose is: 5 | - To handle the following client related logic executed via our main thread: (see Citizen.CreateThread). 6 | - To perform team selection and camera manipulation if the user is in camera selection: (see boolean bInTeamSelection). 7 | 8 | - To handle certain client-side flag logic: 9 | - processFlagLogic: Used to update the state of a flag from the client, i.e. 10 | - If the client dies (their health is below 1) the flagStatus is set to EFlagStatuses.CARRIER_DIED. 11 | - The server then takes note of this and updates everyone telling them the flag was dropped. 12 | 13 | - To receive team data from the server via events: (see receiveTeamData). 14 | 15 | Event handlers: 16 | - playerSpawned: Dispatched by FiveM resource spawnManager when the player spawns (https://docs.fivem.net/docs/resources/spawnmanager/events/playerSpawned/) 17 | We use this event to set the player's position to the base position on first spawn (or respawn after death). 18 | 19 | Variables used by this script: 20 | - bInTeamSelection: Stores whether the user is in camera selection or not, set by method: setIntoTeamSelection. 21 | - bShouldShowTeamScreen: Boolean that stores whether the team screen used to pick a team should be shown. 22 | It is set to true on script initialization and false once a team is picked. 23 | 24 | - receivedServerTeams: A table assigned by receiveTeamData, holding data sent from the server. 25 | It contains information about the teams the user may pick (Red and Blue). 26 | 27 | - lastTeamSelKeyPress: Used to introduce a cooldown period 'on left/right click' when cycling through team selection. 28 | Without this cooldown, rapid clicks could lead to unintended and fast team cycling. 29 | 30 | - localTeamSelection: Stores the local team, mainly used to decide if the client should go into team/camera selection. 31 | It is also used to spawn the client at their respective base (see 'playerSpawned' event). 32 | 33 | - activeCameraHandle: Stores the created camera handle (set by CreateCam in setIntoTeamSelection) for later use. 34 | ]] 35 | 36 | -- Declare the variables this script will use 37 | local bInTeamSelection = false 38 | local bIsAttemptingToSwitchTeams = false 39 | local bShouldShowTeamScreen = true -- Show it on first spawn 40 | local receivedServerTeams = nil 41 | local lastTeamSelKeyPress = -1 42 | local localTeamSelection = 0 43 | local activeCameraHandle = -1 44 | local entityBlipHandles = {} -- Keeps handles of any blips (just two, one for each flag) 45 | local spawnPoints = {} 46 | 47 | --[[ 48 | Define controller variables 49 | These controller IDs are gathered from: https://docs.fivem.net/docs/game-references/controls/#controls 50 | ]] 51 | 52 | local CONTROL_LMB = 329 -- Left mouse button 53 | local CONTROL_RMB = 330 -- Right mouse button 54 | local CONTROL_LSHIFT = 209 -- Left shift 55 | 56 | local BLIP_COLOR_BLUE = 4 57 | local BLIP_COLOR_RED = 1 58 | 59 | local spawnmanager = exports.spawnmanager -- Caching the spawnmanager export 60 | 61 | -- Set the teamID to spectator on script initialization 62 | LocalPlayer.state:set('teamID', TeamType.TEAM_SPECTATOR, true) 63 | 64 | -- This event is first called when the resource starts 65 | AddEventHandler('onClientResourceStart', function(resourceName) 66 | if (GetCurrentResourceName() ~= resourceName) then 67 | return 68 | end 69 | -- Let's request team data from the server when we join. 70 | TriggerServerEvent("requestTeamData") 71 | -- Send a console message showing that the resource has been started 72 | print('The resource ' .. resourceName .. ' has been started.') 73 | end) 74 | 75 | RegisterNetEvent("SetObjectiveVisible") 76 | -- The event handler function follows after registering the event first. 77 | AddEventHandler("SetObjectiveVisible", function(flagEntityNetID, bVisible) 78 | if NetworkDoesNetworkIdExist(flagEntityNetID) then 79 | -- Call NetToEnt to get the entity handle from our net handle (flagEntityNetID), which is sent by the server. 80 | local flagEntity = NetToEnt(flagEntityNetID) 81 | 82 | print("SetObjectiveVisible: " .. GetEntityArchetypeName(flagEntity) .. " to our player, owner is: " .. GetPlayerName(NetworkGetEntityOwner(flagEntity))) 83 | 84 | SetEntityVisible(flagEntity, bVisible) 85 | else 86 | print("AttachFlagToPlayer: Something terrible happened, where's our flag?") 87 | end 88 | end) 89 | 90 | RegisterNetEvent("PlaySoundFrontEnd") 91 | -- The event handler function follows after registering the event first. 92 | AddEventHandler("PlaySoundFrontEnd", function(soundName, soundSetName) 93 | PlaySoundFrontend(-1, soundName, soundSetName, false) 94 | end) 95 | 96 | -- This event gets called every time the player spawns. 97 | -- We use it to decide whether or not they should should go into team selection during first spawn 98 | AddEventHandler('playerSpawned', function() 99 | if shouldGoIntoTeamSelection() then 100 | setIntoTeamSelection(TeamType.TEAM_BLUE, true) 101 | end 102 | end) 103 | 104 | -- Register the event handler to receive team data 105 | RegisterNetEvent("receiveTeamData") 106 | AddEventHandler("receiveTeamData", function(teamsData) 107 | receivedServerTeams = teamsData 108 | 109 | for _, team in ipairs(receivedServerTeams) do 110 | spawnPoints[team.id] = spawnmanager:addSpawnPoint({ 111 | x = team.basePosition.x, 112 | y = team.basePosition.y, 113 | z = team.basePosition.z, 114 | heading = team.playerHeading, 115 | model = team.playerModel, 116 | skipFade = false 117 | }) 118 | end 119 | end) 120 | 121 | ---------------------------------------------- Commands ---------------------------------------------- 122 | 123 | --- A command to go into camera selection (was mainly used for test purposes during development). 124 | -- Additional documentation about RegisterCommand can be found by following the URL provided down below. 125 | -- https://docs.fivem.net/docs/scripting-manual/migrating-from-deprecated/creating-commands/ 126 | RegisterCommand("switchteam", function(source, args, rawCommand) 127 | TriggerServerEvent("sendTeamDataToClient", GetPlayerServerId(PlayerId())) 128 | bIsAttemptingToSwitchTeams = true 129 | LocalPlayer.state:set('teamID', TeamType.TEAM_SPECTATOR, true) 130 | SetEntityHealth(PlayerPedId(), 0) 131 | end) 132 | 133 | RegisterCommand("kill", function(source, args, rawCommand) 134 | SetEntityHealth(PlayerPedId(), 0) 135 | end, false) 136 | 137 | ---------------------------------------------- Functions ---------------------------------------------- 138 | 139 | --- Handles player input for team selection. 140 | -- This function allows players to navigate through available teams using mouse clicks and to confirm their selection by pressing the left shift key. 141 | -- Mouse click on the left button (LMB) decreases the team selection index by one, while a click on the right button (RMB) increases it by one. 142 | -- The left shift key confirms the selected team and spawns the player character at the designated spawn point. 143 | function handleTeamSelectionControl() 144 | local teamSelDirection = 0 145 | local bPressedSpawnKey = false 146 | 147 | -- Determine the direction of team selection based on mouse clicks 148 | if IsControlPressed(0, CONTROL_LMB) then 149 | teamSelDirection = -1 -- Previous team 150 | elseif IsControlPressed(0, CONTROL_RMB) then 151 | teamSelDirection = 1 -- Next team 152 | elseif IsControlPressed(0, CONTROL_LSHIFT) then -- Left Shift 153 | -- Let's spawn! 154 | bInTeamSelection = false -- We're no longer in team/camera selection 155 | bIsAttemptingToSwitchTeams = false -- We're no longer trying to switch teams 156 | bPressedSpawnKey = true 157 | 158 | -- Spawn the player 159 | spawnmanager:spawnPlayer( 160 | spawnPoints[LocalPlayer.state.teamID], 161 | onPlayerSpawnCallback 162 | ) 163 | end 164 | 165 | -- Determine the direction of team selection based on mouse clicks 166 | if teamSelDirection ~= 0 or bPressedSpawnKey then 167 | local newTeamID = LocalPlayer.state.teamID + teamSelDirection 168 | if newTeamID >= 1 and newTeamID <= #receivedServerTeams then 169 | LocalPlayer.state:set('teamID', newTeamID, true) 170 | lastTeamSelKeyPress = GetGameTimer() + 500 171 | end 172 | setIntoTeamSelection(LocalPlayer.state.teamID, bInTeamSelection) 173 | end 174 | end 175 | 176 | --- Our callback method for the autoSpawnCallback down below, we give ourselves guns here. 177 | function onPlayerSpawnCallback() 178 | -- Cache the player ped 179 | local ped = PlayerPedId() 180 | 181 | -- Spawn the player via an export at the player team's spawn point. 182 | spawnmanager:spawnPlayer( 183 | spawnPoints[LocalPlayer.state.teamID] 184 | ) 185 | 186 | -- Let's use compile-time jenkins hashes to give ourselves an assault rifle. 187 | GiveWeaponToPed(ped, `weapon_assaultrifle`, 300, false, true) 188 | 189 | -- Disable friendly fire. 190 | NetworkSetFriendlyFireOption(false) 191 | 192 | -- Clear any previous blood damage 193 | ClearPedBloodDamage(ped) 194 | 195 | -- Make us visible again 196 | SetEntityVisible(ped, true) 197 | 198 | local TEAM_BLUE_REL_GROUP, TEAM_RED_REL_GROUP = nil, nil 199 | 200 | -- Add any relationship groups 201 | TEAM_BLUE_REL_GROUP = AddRelationshipGroup('TEAM_BLUE') 202 | TEAM_RED_REL_GROUP = AddRelationshipGroup('TEAM_RED') 203 | 204 | -- Set the relationship to hate 205 | -- This is done so we can allow players to shoot each other if they are in different teams. 206 | SetRelationshipBetweenGroups(5, `TEAM_BLUE`, `TEAM_RED`) 207 | SetRelationshipBetweenGroups(5, `TEAM_RED`, `TEAM_BLUE`) 208 | 209 | if LocalPlayer.state.teamID == TeamType.TEAM_BLUE then 210 | SetPedRelationshipGroupHash(ped, `TEAM_BLUE`) 211 | else 212 | SetPedRelationshipGroupHash(ped, `TEAM_RED`) 213 | end 214 | end 215 | 216 | -- Define a function to format the team name 217 | function formatTeamName(receivedServerTeams, teamID) 218 | -- Check if receivedServerTeams is valid and contains the teamID 219 | if receivedServerTeams and receivedServerTeams[teamID] then 220 | -- Concatenate the team name with " Team" suffix 221 | return receivedServerTeams[teamID].name .. " Team" 222 | else 223 | -- Return a default message if the team name cannot be formatted 224 | return "Unknown Team" 225 | end 226 | end 227 | 228 | function shouldGoIntoTeamSelection() 229 | -- If we're on spectator team and we're not in team selection, then we should. 230 | return LocalPlayer.state.teamID == TeamType.TEAM_SPECTATOR and not bInTeamSelection 231 | end 232 | 233 | function setIntoTeamSelection(team, bIsInTeamSelection) 234 | -- Sets the player into camera selection 235 | -- Main camera handle only gets created once in order to manipulate it later 236 | local ped = PlayerPedId() -- Let's cache the Player Ped ID so we're not constantly calling PlayerPedId() 237 | 238 | LocalPlayer.state:set('teamID', team, true) 239 | bInTeamSelection = bIsInTeamSelection 240 | local origCamCoords = receivedServerTeams[team].basePosition 241 | local camFromCoords = vector3(origCamCoords.x, origCamCoords.y + 2.0, origCamCoords.z + 2.0) 242 | if activeCameraHandle == -1 then 243 | activeCameraHandle = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) 244 | end 245 | 246 | -- Set the ped where the camera should be at to allow streaming. 247 | SetEntityCoords(ped, origCamCoords.x, origCamCoords.y, origCamCoords.z, false, false, false, true) 248 | 249 | -- Set the camera coordinates 250 | SetCamCoord(activeCameraHandle, camFromCoords) 251 | PointCamAtCoord(activeCameraHandle, origCamCoords) 252 | 253 | -- Let the game know whether we should enable or disable rendering of scripted cameras. 254 | -- Our scripted camera is the one created by CreateCam, we keep a reference to that camera 255 | -- see: activeCameraHandle 256 | RenderScriptCams(bInTeamSelection) 257 | 258 | -- Set the ped entity visible if they are not in team selection, invisible otherwise. 259 | SetEntityVisible(ped, not bIsInTeamSelection) 260 | end 261 | 262 | function tryCreateBlipForEntity(teamID, entity, spriteId) 263 | local blipHandle = entityBlipHandles[teamID] 264 | if DoesBlipExist(blipHandle) then 265 | RemoveBlip(blipHandle) 266 | end 267 | local newBlipHandle = AddBlipForEntity(entity) 268 | SetBlipSprite(newBlipHandle, spriteId) 269 | SetBlipColour(newBlipHandle, teamID == TeamType.TEAM_RED and BLIP_COLOR_RED or BLIP_COLOR_BLUE) 270 | return newBlipHandle 271 | end 272 | 273 | --- Process logic related to the flag state 274 | -- This method is used to process client-side logic related to any of the flags. 275 | -- It will also inform the server if the carrier died to take proper action on the server. 276 | function processFlagLogic(flagEntityNetID) 277 | -- Check if it exists before processing. 278 | -- It may not exist if we're far away from the entity 279 | if not NetworkDoesNetworkIdExist(flagEntityNetID) then return end 280 | 281 | -- Cast/convert the network ID to an entity 282 | local ent = NetToEnt(flagEntityNetID) 283 | 284 | -- Don't process any logic until we have our object 285 | if not DoesEntityExist(ent) then return end 286 | 287 | -- Code to lerp the intensity https://en.wikipedia.org/wiki/Linear_interpolation 288 | -- This line gets the current time in the game and converts it to seconds 289 | local time = GetGameTimer() / 1000 290 | 291 | -- This line calculates the intensity using a mathematical function 292 | -- It involves a sine wave, which creates a smooth up-and-down motion over time 293 | -- The "math.sin(time)" part calculates the sine of the time, which creates a wave pattern 294 | -- The "* 3" part makes the wave's peaks and valleys higher, increasing the intensity 295 | local intensity = 3 * (1 + math.sin(time)) 296 | 297 | -- This line ensures that the intensity stays within a specific range (0 to 6) 298 | -- If the intensity is below 0, it sets it to 0 299 | -- If the intensity is above 6, it sets it to 6 300 | intensity = math.max(0, math.min(6, intensity)) 301 | 302 | local bFreezeInPosition = false 303 | 304 | -- Get the entity state data for this entity 305 | local es = Entity(ent) 306 | 307 | if es.state.flagStatus == EFlagStatuses.DROPPED then 308 | local coords = GetEntityCoords(ent) 309 | Draw3DText(coords.x, coords.y, coords.z, 0.5, screenCaptions.DefendAndGrabThePackage) 310 | 311 | DrawLightWithRangeAndShadow( 312 | coords.x, coords.y, coords.z, 313 | es.state.flagColor[1], 314 | es.state.flagColor[2], 315 | es.state.flagColor[3], 316 | 5.0, 317 | intensity, 318 | 1.0 319 | ) 320 | 321 | -- Add a blip for the entity 322 | entityBlipHandles[es.state.teamID] = tryCreateBlipForEntity( 323 | es.state.teamID, 324 | ent, 325 | 309 326 | ) 327 | 328 | elseif es.state.flagStatus == EFlagStatuses.TAKEN then 329 | -- Draw a light around the player carrying the flag 330 | local carrierPed = GetPlayerPed(GetPlayerFromServerId(es.state.carrierId)) 331 | local carrierCoords = GetEntityCoords(carrierPed) 332 | DrawLightWithRangeAndShadow( 333 | carrierCoords.x, carrierCoords.y, carrierCoords.z, 334 | es.state.flagColor[1], 335 | es.state.flagColor[2], 336 | es.state.flagColor[3], 337 | 4.0, 338 | intensity, 339 | 1.0 340 | ) 341 | 342 | -- Add a blip for the ped carrying the flag 343 | entityBlipHandles[es.state.teamID] = tryCreateBlipForEntity( 344 | es.state.teamID, 345 | carrierPed, 346 | 309 347 | ) 348 | 349 | -- If the carrier dies, update the status and send a request to the server to let it know 350 | if IsEntityDead(carrierPed) then 351 | es.state:set('flagStatus', EFlagStatuses.CARRIER_DIED, true) 352 | TriggerServerEvent("requestFlagUpdate") 353 | return 354 | end 355 | end 356 | 357 | if es.state.flagStatus ~= EFlagStatuses.TAKEN then 358 | if not IsEntityAttachedToAnyPed(ent) then 359 | local playerPed = PlayerPedId() 360 | if entityHasEntityInRadius(ent, playerPed) and not IsEntityDead(playerPed) then 361 | TriggerServerEvent("requestFlagUpdate") 362 | Citizen.Wait(500) 363 | end 364 | end 365 | end 366 | 367 | if es.state.flagStatus == EFlagStatuses.AT_BASE then 368 | if DoesBlipExist(entityBlipHandles[es.state.teamID]) then 369 | RemoveBlip(entityBlipHandles[es.state.teamID]) 370 | end 371 | 372 | -- Freeze when the flag is at base 373 | bFreezeInPosition = true 374 | end 375 | 376 | -- Actually freeze the entity by calling the native 377 | FreezeEntityPosition(ent, bFreezeInPosition) 378 | end 379 | 380 | function processBasesForTeams() 381 | --- Processes the bases for teams by drawing lights and freezing their positions. 382 | -- @usage This function should be called per tick in a thread after receiving server teams. 383 | for _, team in ipairs(receivedServerTeams) do 384 | DrawLightWithRangeAndShadow( 385 | team.basePosition.x, team.basePosition.y, team.basePosition.z, 386 | team.flagColor[1] --[[ integer ]], 387 | team.flagColor[2] --[[ integer ]], 388 | team.flagColor[3] --[[ integer ]], 389 | 10.0 --[[ number ]], 390 | 2.0 --[[intensity]], 391 | 1.0 392 | ) 393 | 394 | if NetworkDoesNetworkIdExist(team.baseNetworkId) then 395 | -- Cast/convert the network ID to an entity 396 | local ent = NetToEnt(team.baseNetworkId) 397 | if not IsEntityPositionFrozen(ent) then 398 | -- Freezes the entity position to keep the base in place. 399 | FreezeEntityPosition(ent, true) 400 | end 401 | end 402 | end 403 | end 404 | 405 | -- Define a function to get the received teams from other files 406 | function getReceivedServerTeams() 407 | return receivedServerTeams 408 | end 409 | 410 | -- Getter to determine whether the player is in team selection or not 411 | function isInTeamSelection() 412 | return bInTeamSelection 413 | end 414 | 415 | ---------------------------------------------- Callbacks ---------------------------------------------- 416 | 417 | --- This handles player auto spawning after death. 418 | -- See spawnmanager's documentation for more: https://docs.fivem.net/docs/resources/spawnmanager/ 419 | spawnmanager:setAutoSpawnCallback(onPlayerSpawnCallback) 420 | spawnmanager:setAutoSpawn(true) 421 | 422 | ----------------------------------------------- Threads ----------------------------------------------- 423 | 424 | -- Process the client flag logic for each flag every tick 425 | Citizen.CreateThread(function() 426 | while true do 427 | if not bInTeamSelection then 428 | if receivedServerTeams and #receivedServerTeams > 0 then 429 | for _, team in ipairs(receivedServerTeams) do 430 | -- Run flag logic if they are not in camera/team selection 431 | if team.id ~= TeamType.TEAM_SPECTATOR then 432 | processFlagLogic(team.flagNetworkedID) 433 | end 434 | end 435 | end 436 | end 437 | Citizen.Wait(0) 438 | end 439 | end) 440 | 441 | --- Our main thread. 442 | -- We use this thread to handle team selection and to process any flag related logic. 443 | Citizen.CreateThread(function() 444 | while true do 445 | if receivedServerTeams and #receivedServerTeams > 0 then 446 | if shouldGoIntoTeamSelection() and not bIsAttemptingToSwitchTeams then 447 | setIntoTeamSelection(TeamType.TEAM_BLUE, true) 448 | end 449 | 450 | if bInTeamSelection then 451 | if GetGameTimer() > lastTeamSelKeyPress then 452 | -- Determine if the user pressed one of the mouse buttons or SHIFT 453 | -- Sets LocalPlayer.state.teamID if so 454 | handleTeamSelectionControl() 455 | end 456 | end 457 | -- Render base lights for teams and freezes them each frame. 458 | processBasesForTeams() 459 | end 460 | -- No wanted level 461 | ClearPlayerWantedLevel(PlayerId()) 462 | 463 | -- Make it night time so we can see our lights 464 | NetworkOverrideClockTime(23, 0, 0) 465 | 466 | -- Process all rendering logic for this frame 467 | ctfRenderingRenderThisFrame() 468 | 469 | Citizen.Wait(0) 470 | end 471 | end) 472 | -------------------------------------------------------------------------------- /ctf-gamemode/ctf_config.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File: ctf_config.lua 3 | Description: 4 | This file contains configurations for the Team Deathmatch (TDM) game mode, 5 | including properties for each team such as team ID, base position, and player model. 6 | 7 | Configuration Format: 8 | Each team configuration is represented as a table with the following properties: 9 | - id: The ID of the team. 10 | - basePosition: The base position of the team. 11 | - playerModel: The player model associated with the team. 12 | - playerHeading: The desired player heading on spawn. 13 | ]] 14 | 15 | ctfConfig = {} 16 | 17 | ctfConfig.teams = { 18 | { 19 | id = TeamType.TEAM_RED, 20 | flagColor = {255, 0, 0}, 21 | basePosition = vector3(2555.1860, -333.1058, 92.9928), 22 | playerModel = 'a_m_y_beachvesp_01', 23 | playerHeading = 90.0 24 | }, 25 | { 26 | id = TeamType.TEAM_BLUE, 27 | flagColor = {0, 0, 255}, 28 | basePosition = vector3(2574.9807, -342.9044, 92.9928), 29 | playerModel = 's_m_m_armoured_02', 30 | playerHeading = 90.0 31 | }, 32 | { 33 | id = TeamType.TEAM_SPECTATOR, 34 | flagColor = {255, 255, 255}, 35 | basePosition = vector3(2574.9807, -342.9044, 92.9928), 36 | playerModel = 's_m_m_armoured_02', 37 | playerHeading = 90.0 38 | }, 39 | -- Add more team configurations as needed 40 | } 41 | 42 | -- Flags 43 | ctfConfig.flags = { 44 | { 45 | teamID = TeamType.TEAM_RED, 46 | model = "w_am_case", 47 | position = vector3(2555.1860, -333.1058, 92.9928) 48 | }, 49 | { 50 | teamID = TeamType.TEAM_BLUE, 51 | model = "w_am_case", 52 | position = vector3(2574.9807, -342.9044, 92.9928) 53 | } 54 | } 55 | 56 | ctfConfig.UI = { 57 | btnCaptions = { 58 | Spawn = "Spawn", 59 | NextTeam = "Next Team", 60 | PreviousTeam = "Previous Team" 61 | }, 62 | teamTxtProperties = { 63 | x = 1.0, -- Screen X coordinate 64 | y = 0.9, -- Screen Y coordinate 65 | width = 0.4, -- Width of the text 66 | height = 0.070, -- Height of the text 67 | scale = 1.0, -- Text scaling 68 | text = "", -- Text content 69 | color = { -- Color components 70 | r = 255, -- Red color component 71 | g = 255, -- Green color component 72 | b = 255, -- Blue color component 73 | a = 255 -- Alpha (transparency) value 74 | } 75 | }, 76 | screenCaptions = { 77 | DefendAndGrabThePackage = "Defend and grab the ~y~package~w~.", 78 | TeamFlagAction = "The %s team's flag has been %s." 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /ctf-gamemode/ctf_rendering.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File: ctf_client.lua 3 | Description: 4 | - Handles rendering for UI elements via ctfRenderingRenderThisFrame: 5 | - Text/Sprite Rendering: (see renderFlagScores). 6 | - Draws instructional UI on the screen: (see drawScaleFormUI). 7 | - Contains helper methods to draw text on the screen. 8 | Event handlers: 9 | - SendClientHudNotification: Dispatched by the server to display simple 'toast' UI notifications on the client. 10 | ]] 11 | 12 | -- Store UI configuration from ctfConfig.UI. 13 | local UIConfig = ctfConfig.UI 14 | 15 | -- Store properties for team text. 16 | local UITeamTxtProps = UIConfig.teamTxtProperties 17 | 18 | -- Store captions for buttons. 19 | local btnCaptions = UIConfig.btnCaptions 20 | 21 | -- Store captions for screens. 22 | -- Global since it's used by ctf_client.lua 23 | screenCaptions = UIConfig.screenCaptions 24 | 25 | -- Used for the instructional buttons in drawScaleFormUI 26 | local buttonsHandle = nil 27 | 28 | 29 | AddEventHandler('onClientResourceStart', function(resourceName) 30 | if (GetCurrentResourceName() ~= resourceName) then 31 | return 32 | end 33 | -- Let's create our entry for text rendering. 34 | -- '~a~' is a placeholder for a substring 'text component', such as ADD_TEXT_COMPONENT_SUBSTRING_TEXT_LABEL. 35 | -- More here: 36 | -- https://docs.fivem.net/docs/game-references/text-formatting/#content-formatting-codes 37 | AddTextEntry("textRenderingEntry", "~a~") 38 | -- Request the texture dictionary for our instructional buttons 39 | RequestStreamedTextureDict("commonmenutu", false) 40 | buttonsHandle = RequestScaleformMovie('INSTRUCTIONAL_BUTTONS') -- Request the scaleform to be loaded 41 | end) 42 | 43 | -- Event used to draw toast notifications on the screen 44 | RegisterNetEvent("SendClientHudNotification") 45 | AddEventHandler("SendClientHudNotification", function(message) 46 | BeginTextCommandThefeedPost("STRING") 47 | AddTextComponentSubstringPlayerName(message) 48 | EndTextCommandThefeedPostTicker(true, true) 49 | end) 50 | 51 | -- Our main rendering method. 52 | -- We use this method to perform Text/Sprite Rendering, handling drawing of instructional UI, team selection and camera manipulation. 53 | function ctfRenderingRenderThisFrame() 54 | drawScaleFormUI(buttonsHandle) 55 | 56 | local teams = nil 57 | 58 | -- We need to wait for these since they arrive via an event from the server 59 | if not teams then 60 | teams = getReceivedServerTeams() 61 | end 62 | 63 | -- If we got the teams we can render 64 | if teams and #teams > 0 then 65 | if HasStreamedTextureDictLoaded("commonmenutu") then 66 | renderFlagScores(teams) 67 | end 68 | 69 | if isInTeamSelection() then 70 | DisableRadarThisFrame() 71 | HideHudAndRadarThisFrame() 72 | DrawScaleformMovieFullscreen(buttonsHandle, 255, 255, 255, 255, 1) -- Draw the instructional buttons this frame 73 | 74 | -- Draw the text on the screen for this specific team 75 | -- This will use the properties from our ctf_config.lua file 76 | if LocalPlayer.state.teamID and LocalPlayer.state.teamID <= #teams then 77 | drawTxt( 78 | UITeamTxtProps.x, 79 | UITeamTxtProps.y, 80 | UITeamTxtProps.width, 81 | UITeamTxtProps.height, 82 | UITeamTxtProps.scale, 83 | formatTeamName(teams, LocalPlayer.state.teamID), 84 | UITeamTxtProps.color.r, 85 | UITeamTxtProps.color.g, 86 | UITeamTxtProps.color.b, 87 | UITeamTxtProps.color.a 88 | ) 89 | end 90 | end 91 | end 92 | end 93 | 94 | function renderFlagScore(flagData, screenPos, colorRgba) 95 | local statusID = flagData.flagStatus 96 | 97 | -- Draw the flag 98 | DrawSprite( 99 | "commonmenutu" --[[ string ]], 100 | "race" --[[ string ]], 101 | screenPos.x --[[ number ]], 102 | screenPos.y --[[ number ]], 103 | 0.06 --[[ scale on x ]], 104 | 0.1 --[[ scale on y ]], 105 | 0.0 --[[ heading ]], 106 | table.unpack(colorRgba) 107 | ) 108 | 109 | -- Draws the score and the flag description 110 | drawTxt(screenPos.x, screenPos.y, -0.08, 0.08, 1.0, tostring(flagData.score), table.unpack(colorRgba)) 111 | drawTxt(screenPos.x, screenPos.y, 0.03, -0.04, 0.5, getDescriptionForFlagStatus(statusID), table.unpack(colorRgba)) 112 | end 113 | 114 | function renderFlagScores(teams) 115 | -- If we received any news from the server after script startup, run our render 116 | if teams ~= nil then 117 | renderFlagScore(teams[TeamType.TEAM_RED], {x = 0.025, y = 0.5}, {255, 0, 0, 180}) 118 | renderFlagScore(teams[TeamType.TEAM_BLUE], {x = 0.025, y = 0.6}, {0, 0, 255, 180}) 119 | end 120 | end 121 | 122 | function buttonMessage(text) 123 | BeginTextCommandScaleformString("STRING") 124 | AddTextComponentScaleform(text) 125 | EndTextCommandScaleformString() 126 | end 127 | 128 | --- Draws the scaleform UI displaying controller buttons and associated messages for player instructions. 129 | -- 130 | -- @param buttonsHandle (number) The handle for the scaleform movie. 131 | function drawScaleFormUI(buttonsHandle) 132 | while not HasScaleformMovieLoaded(buttonsHandle) do -- Wait for the scaleform to be fully loaded 133 | Wait(0) 134 | end 135 | 136 | CallScaleformMovieMethod(buttonsHandle, 'CLEAR_ALL') -- Clear previous buttons 137 | 138 | PushScaleformMovieFunction(buttonsHandle, "SET_DATA_SLOT") 139 | PushScaleformMovieFunctionParameterInt(2) 140 | ScaleformMovieMethodAddParamPlayerNameString("~INPUT_SPRINT~") 141 | buttonMessage(btnCaptions.Spawn) 142 | PopScaleformMovieFunctionVoid() 143 | 144 | PushScaleformMovieFunction(buttonsHandle, "SET_DATA_SLOT") 145 | PushScaleformMovieFunctionParameterInt(1) 146 | ScaleformMovieMethodAddParamPlayerNameString("~INPUT_ATTACK~") 147 | buttonMessage(btnCaptions.PreviousTeam) 148 | PopScaleformMovieFunctionVoid() 149 | 150 | PushScaleformMovieFunction(buttonsHandle, "SET_DATA_SLOT") 151 | PushScaleformMovieFunctionParameterInt(0) 152 | ScaleformMovieMethodAddParamPlayerNameString("~INPUT_AIM~") -- The button to display 153 | buttonMessage(btnCaptions.NextTeam) -- the message to display next to it 154 | PopScaleformMovieFunctionVoid() 155 | 156 | CallScaleformMovieMethod(buttonsHandle, 'DRAW_INSTRUCTIONAL_BUTTONS') -- Sets buttons ready to be drawn 157 | end 158 | 159 | ------------------------------------------ Helper methods ------------------------------------------ 160 | 161 | --- Used to draw text on the screen. 162 | -- Multiple natives are called for drawing. 163 | -- Documentation for those natives can be found at http://docs.fivem.net/natives 164 | -- 165 | -- @param x (number) Where on the screen to draw text (horizontal axis). 166 | -- @param y (number) Where on the screen to draw text (vertical axis). 167 | -- @param width (number) The width for the text. 168 | -- @param height (number) The height for the text. 169 | -- @param scale (number) The scale for the text. 170 | -- @param text (string) The actual text value to display. 171 | -- @param r (number) The value for red (0-255). 172 | -- @param g (number) The value for green (0-255). 173 | -- @param b (number) The value for blue (0-255). 174 | -- @param alpha (number) The value for alpha/opacity (0-255). 175 | function drawTxt(x,y,width,height,scale, text, r,g,b,a) 176 | SetTextFont(2) 177 | SetTextProportional(0) 178 | SetTextScale(scale, scale) 179 | SetTextColour(r, g, b, a) 180 | SetTextDropShadow(0, 0, 0, 0,255) 181 | SetTextEdge(1, 0, 0, 0, 255) 182 | SetTextDropShadow() 183 | SetTextOutline() 184 | -- Let's use our previously created text entry 'textRenderingEntry' 185 | SetTextEntry("textRenderingEntry") 186 | AddTextComponentString(text) 187 | DrawText(x - width/2, y - height/2 + 0.005) 188 | end 189 | 190 | --- https://forum.cfx.re/t/draw-3d-text-as-marker/2643565/2 191 | function Draw3DText(x, y, z, scl_factor, text) 192 | local onScreen, _x, _y = World3dToScreen2d(x, y, z) 193 | local p = GetGameplayCamCoords() 194 | local distance = GetDistanceBetweenCoords(p.x, p.y, p.z, x, y, z, 1) 195 | local scale = (1 / distance) * 2 196 | local fov = (1 / GetGameplayCamFov()) * 100 197 | local scale = scale * fov * scl_factor 198 | if onScreen then 199 | SetTextScale(0.0, scale) 200 | SetTextFont(0) 201 | SetTextProportional(1) 202 | SetTextColour(255, 255, 255, 215) 203 | SetTextDropshadow(0, 0, 0, 0, 255) 204 | SetTextEdge(2, 0, 0, 0, 150) 205 | SetTextDropShadow() 206 | SetTextOutline() 207 | -- Let's use our previously created text entry 'textRenderingEntry' 208 | SetTextEntry("textRenderingEntry") 209 | SetTextCentre(1) 210 | AddTextComponentString(text) 211 | DrawText(_x, _y) 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /ctf-gamemode/ctf_server.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File: ctf_server.lua 3 | Description: 4 | This file handles server-side logic for the Capture the Flag (CTF) game mode. 5 | It includes functions for team balancing, player-team assignments, flag management, and game state updates. 6 | 7 | Functions: 8 | - GetCTFGame: Retrieves or creates an instance of the CTFGame class. 9 | - CTFGame.new: Constructs a new CTFGame instance. 10 | - CTFGame:start: Initializes the CTF game by creating teams and flags. 11 | - CTFGame:update: Updates the game state including flag interactions, player scores, and flag status changes. 12 | - CTFGame:shutDown: Handles cleanup tasks upon game shutdown, including flag and team destruction. 13 | 14 | Event Handlers: 15 | - playerJoining: Handles player joining events, triggering team data updates for the client. 16 | - requestTeamData: Responds to client requests for team data. 17 | - sendTeamDataToClient: Sends team data to clients for UI and game state updates. 18 | - assignPlayerTeam: Assigns a team to a player. 19 | 20 | Classes: 21 | - Team: Represents a team in the CTF game mode. 22 | - new: Creates a new Team instance. 23 | - createBaseObject: Creates the base object associated with the team. 24 | - getName: Retrieves the name of the team. 25 | - updateScore: Updates the team's score. 26 | - goalBaseHasEntityInRadius: Checks if the base has a given entity in range. 27 | - destroy: Destroys the team object. 28 | 29 | - Flag: Represents a flag in the CTF game mode. 30 | - new: Creates a new Flag instance. 31 | - spawn: Spawns the flag in the game world. 32 | - getFlagStatus: Retrieves the current status of the flag. 33 | - getFlagNetworkedID: Retrieves the networked ID of the flag. 34 | - carrierDied: Checks if the carrier of the flag has died. 35 | - isBeingCarried: Checks if the flag is being carried by a player. 36 | - isTaken: Checks if the flag has been taken. 37 | - isCaptured: Checks if the flag has been captured. 38 | - isDropped: Checks if the flag has been dropped. 39 | - isAtBase: Checks if the flag is at its base. 40 | - setNextCooldown: Sets the next cooldown time for the flag. 41 | - isPastCooldown: Checks if the flag's cooldown has elapsed. 42 | - setStatus: Sets the status of the flag. 43 | - hasEntityInRadius: Checks if an entity is within the flag's radius. 44 | - setPosition: Sets the position of the flag. 45 | - sendBackToBase: Sends the flag back to its base. 46 | - destroy: Destroys the flag object. 47 | 48 | - CTFGame: Manages the overall game state and logic for the CTF game mode. 49 | - new: Creates a new CTFGame instance. 50 | - start: Initializes the game by creating teams and flags. 51 | - update: Updates the game state based on player interactions and flag status changes. 52 | - shutDown: Cleans up resources and stops the game. 53 | 54 | Notes: 55 | - The CTF game mode relies on consistent team assignments, flag status updates, and player interactions to function correctly. 56 | - Adjust game parameters and logic as needed to maintain balance and fairness. 57 | ]] 58 | 59 | -- UI related variables. 60 | -- Create a local reference to ctfConfig.UI 61 | local UIConfig = ctfConfig.UI 62 | 63 | -- Access 'screenCaptions' properties from the 'ctfConfig.UI' table referenced by 'UIConfig'. 64 | local screenCaptions = UIConfig.screenCaptions 65 | 66 | -- Define the Team class 67 | Team = {} 68 | Team.__index = Team 69 | 70 | --- Creates a new Team instance. 71 | -- Example: 72 | -- @see CTFGame.new 73 | 74 | -- @param id (number) The unique identifier for the team. 75 | -- @param flagColor (table) The RGB color values representing the team's flag color. 76 | -- @param basePosition (vector3) The base position where the team's flag is located. 77 | -- @return Team A new Team instance. 78 | -- 79 | function Team.new(id, flagColor, basePosition, playerModel, playerHeading) 80 | -- Set the metatable for the self table to the Team table. 81 | -- This allows instances of Team to inherit properties and methods. 82 | local self = setmetatable({}, Team) 83 | 84 | -- Assign values to the properties of the new Team instance. 85 | self.id = id 86 | self.flagColor = flagColor 87 | self.basePosition = basePosition 88 | self.entity = nil 89 | self.score = 0 90 | self.playerModel = playerModel 91 | self.playerHeading = playerHeading -- The desired player heading on spawn. 92 | 93 | -- Return the newly created Team instance. 94 | return self 95 | end 96 | 97 | --- Creates the base object entity associated with the team. 98 | -- 99 | -- Example: 100 | -- @see CTFGame:start 101 | -- 102 | function Team:createBaseObject() 103 | -- Calls server setter CREATE_OBJECT_NO_OFFSET, to create an entity on the server 104 | -- Adjust z-coord, so its flushed to the ground. 105 | local baseEntity = CreateObjectNoOffset( 106 | `xs_propint2_stand_thin_02_ring`, 107 | self.basePosition.x, self.basePosition.y, self.basePosition.z - 0.85 108 | ) 109 | 110 | -- wait until it has been created 111 | while not DoesEntityExist(baseEntity) do 112 | Citizen.Wait(1) 113 | end 114 | 115 | -- Now that it's created we can set its state 116 | self.entity = Entity(baseEntity) 117 | 118 | -- Assign a networked ID 119 | self.networkedID = NetworkGetNetworkIdFromEntity(self.entity) 120 | end 121 | 122 | function Team:goalBaseHasEntityInRadius(targetEntity, radius) 123 | radius = radius or 2.5 -- Default radius is 2.5 if not provided 124 | local targetEntityPos = GetEntityCoords(targetEntity) 125 | local distance = #(self.basePosition - targetEntityPos) 126 | return distance < radius -- Adjust the distance as needed 127 | end 128 | 129 | --- Retrieves the name of the team. 130 | -- 131 | -- Example: 132 | -- ```lua 133 | -- local blueTeam = Team.new(1, {0, 0, 255}, vector3(100.0, 0.0, 50.0)) 134 | -- blueTeam:getName() 135 | -- ``` 136 | function Team:getName() 137 | if self.id == TeamType.TEAM_BLUE then 138 | return "Blue" 139 | end 140 | if self.id == TeamType.TEAM_RED then 141 | return "Red" 142 | end 143 | return "Spectator" 144 | end 145 | 146 | --- Update's the team's score. 147 | -- Example: 148 | -- ```lua 149 | -- local blueTeam = Team.new(1, {0, 0, 255}, vector3(100.0, 0.0, 50.0)) 150 | -- blueTeam:updateScore(1) 151 | -- ``` 152 | -- @param score (number) How much to update the score by. 153 | -- 154 | function Team:updateScore(score) 155 | self.score = self.score + score 156 | end 157 | 158 | --- Destroy the Team's instanced object. 159 | -- It will first destroy the team's entity (the base object), it will also reset certain properties of the class. 160 | -- Finally, it will set the metatable to nil, CTFGame:shutDown provides an example of this using iteration. 161 | -- 162 | -- Example: 163 | -- ```lua 164 | -- local blueTeam = Team.new(1, {0, 0, 255}, vector3(100.0, 0.0, 50.0)) 165 | -- blueTeam:destroy() 166 | -- ``` 167 | function Team:destroy() 168 | DeleteEntity(self.entity) 169 | 170 | self.id = -1 171 | self.flagColor = {0, 0, 0} 172 | self.basePosition = nil 173 | self.entity = nil 174 | 175 | setmetatable(self, nil) 176 | end 177 | 178 | -- Define the Flag class 179 | Flag = {} 180 | Flag.__index = Flag 181 | 182 | --- Creates a new Flag instance. 183 | -- Example: 184 | -- ```lua 185 | -- local blueFlag = Flag.new(1, `prop_flag_ls`, blueTeam, vector3(100.0, 0.0, 50.0)) 186 | -- ``` 187 | -- @param id (number) The unique identifier for the flag. 188 | -- @param modelHash (number) The model hash of the flag. 189 | -- @param team (Team) The Team instance to which the flag belongs. 190 | -- @param spawnPosition (vector3) The initial spawn position of the flag. 191 | -- @return Flag A new Flag instance. 192 | -- 193 | function Flag.new(id, modelHash, team, spawnPosition) 194 | local self = setmetatable({}, Flag) 195 | self.id = id 196 | self.modelHash = modelHash 197 | self.entity = nil 198 | self.team = team 199 | self.spawnPosition = spawnPosition 200 | self.hasBeenCaptured = false 201 | self.networkedID = -1 202 | return self 203 | end 204 | 205 | --- Spawns a new flag entity. 206 | -- This method spawns a new flag entity at the specified position. 207 | -- It creates an object on the server using the CreateObjectNoOffset function, 208 | -- waits until the entity has been created, and then sets its initial state properties. 209 | -- You may refer to CTFGame:start to see how this method is implemented (example code). 210 | -- @see CTFGame:start 211 | -- 212 | -- @return (Flag) A new Flag instance. 213 | -- 214 | -- @see CreateObjectNoOffset 215 | -- @see DoesEntityExist 216 | -- @see NetworkGetNetworkIdFromEntity 217 | -- @see EFlagStatuses 218 | function Flag:spawn() 219 | print('Spawning flag at: ' .. self.spawnPosition) 220 | -- Calls server setter CREATE_OBJECT_NO_OFFSET, to create an entity on the server 221 | local flagEntity = CreateObjectNoOffset(self.modelHash, self.spawnPosition) 222 | 223 | while not DoesEntityExist(flagEntity) do -- wait until it has been created 224 | Citizen.Wait(1) 225 | end 226 | 227 | -- Make the object fall so it doesn't stay still in the air by setting the z-velocity 228 | SetEntityVelocity(flagEntity, 0, 0, -1.0) 229 | 230 | -- Now that it's created we can set its state 231 | self.entity = Entity(flagEntity) 232 | self.networkedID = NetworkGetNetworkIdFromEntity(self.entity) 233 | 234 | --local ent = Entity(NetworkGetEntityFromNetworkId(self.networkedID)) 235 | 236 | self.entity.state.networkedID = self.networkedID 237 | self.entity.state.teamID = self.team.id 238 | self.entity.state.position = self.spawnPosition 239 | self.entity.state.flagColor = self.team.flagColor 240 | self.entity.state.flagStatus = EFlagStatuses.AT_BASE 241 | self.entity.state.carrierId = -1 -- The player that is carrying the flag 242 | self.entity.state.lastCooldown = GetGameTimer() 243 | self.entity.state.autoReturnTime = GetGameTimer() 244 | end 245 | 246 | 247 | --- This method returns the current status of the flag by accessing its flagStatus property from the entity state. 248 | -- Example: 249 | -- ```lua 250 | -- local flagStatus = blueFlag:getFlagStatus() 251 | -- ``` 252 | -- @return (number) The status of the flag represented by an enum value. 253 | -- @see EFlagStatuses 254 | function Flag:getFlagStatus() 255 | return self.entity.state.flagStatus 256 | end 257 | 258 | --- Returns the Networked ID of the flag, set by Flag:spawn 259 | -- @return (number) The status of the flag represented by an enum value. 260 | function Flag:getFlagNetworkedID() 261 | return self.networkedID 262 | end 263 | 264 | function Flag:carrierDied() 265 | return self:getFlagStatus() == EFlagStatuses.CARRIER_DIED 266 | end 267 | 268 | function Flag:isBeingCarried() 269 | return self.entity.state.carrierId ~= -1 270 | end 271 | 272 | function Flag:isFlagCarrier(playerId) 273 | return self.entity.state.carrierId == playerId 274 | end 275 | 276 | function Flag:isTaken() 277 | return self:getFlagStatus() == EFlagStatuses.TAKEN 278 | end 279 | 280 | function Flag:isCaptured() 281 | return self:getFlagStatus() == EFlagStatuses.CAPTURED 282 | end 283 | 284 | function Flag:isDropped() 285 | return self:getFlagStatus() == EFlagStatuses.DROPPED 286 | end 287 | 288 | -- Method to check if the flag is at its base (spawn position) 289 | function Flag:isAtBase() 290 | local distance = #(GetEntityCoords(self.entity) - self.spawnPosition) 291 | return distance < 5.0 292 | end 293 | 294 | 295 | --- Sets the next cooldown time for the flag. 296 | -- 297 | -- @param timeMs (number) The time duration in milliseconds for the cooldown. 298 | function Flag:setNextCooldown(timeMs) 299 | self.entity.state.lastCooldown = GetGameTimer() + timeMs 300 | end 301 | 302 | --- Sets the automatic return time for the flag (if it's dropped). 303 | -- 304 | -- @param timeMs (number) The time duration in milliseconds for the return time. 305 | function Flag:setAutoReturnTime(timeMs) 306 | self.entity.state.autoReturnTime = GetGameTimer() + timeMs 307 | end 308 | 309 | --- Checks if the flag's cooldown period has elapsed. 310 | -- This function evaluates whether the current time exceeds the lastCooldown time of the flag instance. 311 | -- It serves as a check within the Capture The Flag (CTF) game logic to determine if the flag is available for interaction after a certain cooldown period has passed. 312 | -- By returning true when the cooldown period is over, it indicates that the flag is ready to be captured or interacted with again. 313 | -- 314 | -- @return (boolean) Returns true if the cooldown period has elapsed, otherwise false. 315 | function Flag:isPastCooldown() 316 | -- Check if the current time (in milliseconds) is greater than the last cooldown time 317 | return GetGameTimer() > self.entity.state.lastCooldown 318 | end 319 | 320 | --- Checks if the flag's auto return time period has elapsed. 321 | -- This function evaluates whether the current time exceeds the autoReturnTime time of the flag instance. 322 | -- It serves as a check within the Capture The Flag (CTF) game logic to determine if the flag should be returned to base after a certain time period has passed. 323 | -- By returning true when the period is over, it indicates that the flag should be returned to base. 324 | -- 325 | -- @return (boolean) Returns true if the auto-return time period has elapsed, otherwise false. 326 | function Flag:isPastAutoReturnTime() 327 | -- Check if the current time (in milliseconds) is greater than the last cooldown time 328 | return GetGameTimer() > self.entity.state.autoReturnTime 329 | end 330 | 331 | --- Sets the flag status 332 | -- @see EFlagStatuses 333 | function Flag:setStatus(status) 334 | self.entity.state.flagStatus = status 335 | end 336 | 337 | --- Gets the flag status 338 | -- @see EFlagStatuses 339 | function Flag:getStatus() 340 | return self.entity.state.flagStatus 341 | end 342 | 343 | --- Checks if an entity is in radius 344 | -- @return (boolean) Returns true if the entity is in radius of the targetEntity 345 | function Flag:hasEntityInRadius(targetEntity, radius) 346 | return entityHasEntityInRadius(self.entity, targetEntity, radius) 347 | end 348 | 349 | --- Sets the position of the flag entity. 350 | -- @param position (vector3) The new position to set for the flag. 351 | function Flag:setPosition(position) 352 | print("setPosition: " .. position .. " entity: " .. tostring(self.entity)) 353 | SetEntityCoords(self.entity, position.x, position.y, position.z, true, true, true, true) 354 | end 355 | 356 | --- Sends the flag back to its base position. 357 | -- This function resets the flag's status to 'at base', and sets its position back to the 358 | -- spawn position. 359 | -- 360 | -- @usage Call this function to return the flag to its base position after certain events, 361 | -- such as when it's dropped or captured. 362 | -- 363 | function Flag:sendBackToBase() 364 | self:setNextCooldown(500) 365 | -- self.entity.state.carrierId 366 | 367 | self:setPosition(self.spawnPosition) 368 | 369 | self:setStatus(EFlagStatuses.AT_BASE) 370 | 371 | -- Make the object fall so it doesn't stay still in the air by setting the z-velocity 372 | SetEntityVelocity(self.entity, 0, 0, -1.0) 373 | 374 | self.entity.state.carrierId = -1 375 | 376 | TriggerClientEvent("SetObjectiveVisible", -1, self:getFlagNetworkedID(), true) 377 | 378 | print(string.format("Sent %s flag back to %f, %f, %f\n", self.team:getName(), self.spawnPosition.x, self.spawnPosition.y, self.spawnPosition.z)) 379 | end 380 | 381 | --- 382 | -- Sets the flag as dropped and performs necessary actions. 383 | -- This function is used to mark the flag as dropped, resetting its status, setting its 384 | -- position to the carrier's current position, and triggering client events. 385 | -- 386 | -- @usage Call this function when the flag needs to be dropped. 387 | -- 388 | function Flag:setAsDropped() 389 | local carrierId = self.entity.state.carrierId 390 | self:setStatus(EFlagStatuses.DROPPED) 391 | self:setPosition(GetEntityCoords(GetPlayerPed(carrierId))) 392 | self.entity.state.carrierId = -1 393 | self:setNextCooldown(5000) -- Set a cooldown so we can't perform any logic once it's dropping 394 | self:setAutoReturnTime(30000) -- Set auto return time to 30 seconds 395 | TriggerClientEvent("SetObjectiveVisible", -1, self:getFlagNetworkedID(), true) 396 | end 397 | 398 | --- Method to destroy the flag entity. 399 | function Flag:destroy() 400 | print("Flag:destroy\n") 401 | DeleteEntity(self.entity) 402 | 403 | self.id = -1 404 | self.modelHash = modelHash 405 | self.entity = nil 406 | self.team = team 407 | self.spawnPosition = spawnPosition 408 | self.hasBeenCaptured = false 409 | self.networkedID = -1 410 | 411 | setmetatable(self, nil) 412 | end 413 | 414 | -- Define the CTFGame class 415 | CTFGame = {} 416 | CTFGame.__index = CTFGame 417 | 418 | --- Constructs a new instance of the Capture The Flag (CTF) game. 419 | -- This method initializes the game state, including teams and flags, 420 | -- and returns the initialized CTF game object. 421 | -- 422 | -- The CTF game consists of teams and flags, each with specific properties and positions within the game world. 423 | -- Teams represent player factions, while flags denote objectives to be captured by opposing teams. 424 | -- 425 | -- @return (CTFGame) A new CTFGame instance representing a CTF game environment. 426 | function CTFGame.new() 427 | local self = setmetatable({}, CTFGame) 428 | self.teams = {} -- Initialize teams as an empty table 429 | -- Initialize each team with their respective position, player model and heading 430 | -- These are loaded from ctf_config.lua 431 | for _, teamConfig in ipairs(ctfConfig.teams) do 432 | self.teams[teamConfig.id] = Team.new( 433 | teamConfig.id, 434 | teamConfig.flagColor, 435 | teamConfig.basePosition, 436 | teamConfig.playerModel, 437 | teamConfig.playerHeading 438 | ) 439 | end 440 | -- Create flags based on the configuration 441 | self.flags = {} -- Initialize flags as an empty table 442 | for _, flagConfig in ipairs(ctfConfig.flags) do 443 | self.flags[flagConfig.teamID] = Flag.new( 444 | flagConfig.teamID, 445 | flagConfig.model, 446 | self.teams[flagConfig.teamID], 447 | flagConfig.position 448 | ) 449 | end 450 | self.leadingTeam = nil 451 | return self 452 | end 453 | 454 | --- Method to start the CTF game. 455 | function CTFGame:start() 456 | for _, team in ipairs(self.teams) do 457 | team:createBaseObject() 458 | end 459 | 460 | for _, flag in ipairs(self.flags) do 461 | print('Spawning flag owned by team: ' .. flag.team.id) 462 | flag:spawn() 463 | end 464 | end 465 | 466 | --- Method to create or retrieve the CTFGame instance. 467 | -- @return (CTFGame) The CTFGame instance. 468 | function GetCTFGame() 469 | if not ctfGame then 470 | ctfGame = CTFGame.new() 471 | end 472 | return ctfGame 473 | end 474 | 475 | --- Retrieves the team of a player based on their source ID. 476 | -- If the player's team ID is not found, it defaults to the spectator team. 477 | -- 478 | -- @param source (number) The source ID of the player. 479 | -- @return Team The team of the player, defaults to the spectator team if not found. 480 | -- 481 | function CTFGame:getPlayerTeam(source) 482 | local playerState = Player(source).state 483 | local teamID = playerState.teamID 484 | local playerTeam = self.teams[teamID] or self.teams[TeamType.TEAM_SPECTATOR] 485 | return playerTeam 486 | end 487 | 488 | --- Retrieves the flag based on the player's source ID and a boolean (`bGetEnemyFlag`). 489 | -- If the boolean is set to retrieve the enemy flag (`bGetEnemyFlag`), 490 | -- it returns the flag of the opposing team. Otherwise, it returns the flag of the player's team. 491 | -- 492 | -- @param source (number) The source ID of the player. 493 | -- @param bGetEnemyFlag (boolean) Indicates whether to retrieve the enemy flag (true) or the player's team flag (false). 494 | -- @return Flag The flag object corresponding to the specified criteria, or nil if not found. 495 | -- 496 | function CTFGame:getFlag(source, bGetEnemyFlag) 497 | local playerTeam = self:getPlayerTeam(source) 498 | 499 | if bGetEnemyFlag then 500 | -- Assuming there are only two teams (TEAM_BLUE and TEAM_RED) 501 | local enemyTeamIndex = (playerTeam.id == TeamType.TEAM_BLUE) and TeamType.TEAM_RED or TeamType.TEAM_BLUE 502 | return self.flags[enemyTeamIndex] or nil 503 | else 504 | return self.flags[playerTeam.id] or nil 505 | end 506 | return nil 507 | end 508 | 509 | --- Return an instance of a flag, based on the teamID. 510 | -- @return Flag The flag object corresponding to the specified criteria, or nil if not found. 511 | function CTFGame:getFlagByTeamID(teamID) 512 | return self.flags[teamID] or nil 513 | end 514 | 515 | -- Helper functions 516 | 517 | --- Captures the flag. 518 | function CTFGame:captureFlag(flagToCapture, ourFlag, playerId) 519 | local playerTeam = self:getPlayerTeam(playerId) 520 | playerTeam:updateScore(1) 521 | SendClientHudNotification( 522 | -1, 523 | string.format( 524 | "The %s team's flag has been captured.~n~Scores are %d-%d", 525 | flagToCapture.team:getName(), 526 | flagToCapture.team.score, 527 | ourFlag.team.score 528 | ) 529 | ) 530 | PlaySoundForEveryone("BASE_JUMP_PASSED", "HUD_AWARDS") 531 | flagToCapture:sendBackToBase() 532 | TriggerEvent("sendTeamDataToClient", -1) 533 | end 534 | 535 | --- Takes the flag. 536 | function CTFGame:attemptToTakeFlag(flagToCapture, playerPed, playerId) 537 | flagToCapture:setStatus(EFlagStatuses.TAKEN) 538 | TriggerClientEvent("SetObjectiveVisible", NetworkGetEntityOwner(flagToCapture.entity), flagToCapture:getFlagNetworkedID(), false) 539 | --TriggerClientEvent("AttachFlagToPlayer", NetworkGetEntityOwner(flagToCapture.entity), flagToCapture:getFlagNetworkedID(), playerId) 540 | flagToCapture.entity.state.carrierId = playerId 541 | flagToCapture:setNextCooldown(2000) 542 | PlaySoundForEveryone("CHALLENGE_UNLOCKED", "HUD_AWARDS") 543 | SendClientHudNotification( 544 | -1, 545 | string.format( 546 | screenCaptions.TeamFlagAction, 547 | flagToCapture.team:getName(), 548 | "taken" 549 | ) 550 | ) 551 | end 552 | 553 | --- Returns the flag. 554 | function CTFGame:returnFlag(ourFlag) 555 | ourFlag:sendBackToBase() 556 | TriggerEvent("sendTeamDataToClient", -1) 557 | SendClientHudNotification( 558 | -1, 559 | string.format( 560 | screenCaptions.TeamFlagAction, 561 | ourFlag.team:getName(), 562 | "returned" 563 | ) 564 | ) 565 | end 566 | 567 | -- Method to update the CTF game state 568 | function CTFGame:update() 569 | -- Check if any flags have been dropped 570 | for _, flagInstance in ipairs(ctfGame.flags) do 571 | if flagInstance:carrierDied() then 572 | flagInstance:setAsDropped() 573 | SendClientHudNotification( 574 | -1, 575 | string.format( 576 | screenCaptions.TeamFlagAction, 577 | flagInstance.team:getName(), 578 | "dropped" 579 | ) 580 | ) 581 | 582 | elseif flagInstance:isDropped() then 583 | -- -- If any flags have been dropped, check if they haven't been picked up for a while 584 | if flagInstance:isPastAutoReturnTime() then 585 | self:returnFlag(flagInstance) 586 | end 587 | end 588 | end 589 | end 590 | 591 | function SendClientHudNotification(source, message) 592 | TriggerClientEvent("SendClientHudNotification", source, message) 593 | -- Update the client's team data and hud status 594 | TriggerEvent("sendTeamDataToClient", source) 595 | end 596 | 597 | --- Call this method to end the game mode 598 | -- Will destroy all flag and team instances. 599 | function CTFGame:shutDown() 600 | -- 'Dispose' on shutdown 601 | for _, flag in ipairs(self.flags) do 602 | flag:destroy() 603 | end 604 | for _, team in ipairs(self.teams) do 605 | team:destroy() 606 | end 607 | end 608 | 609 | -- Instantiate the CTFGame 610 | ctfGame = CTFGame.new() 611 | 612 | -- Start the CTF game 613 | ctfGame:start() 614 | 615 | function PlaySoundForEveryone(soundName, soundSetName) 616 | TriggerClientEvent("PlaySoundFrontEnd", -1, soundName, soundSetName) 617 | end 618 | 619 | --- Gets called by the client when flag data is requested. 620 | -- See ctf_client.lua, this gets called from processFlagLogic. 621 | -- Most of the flag logic on the server is handled here. 622 | RegisterServerEvent('requestFlagUpdate') 623 | AddEventHandler('requestFlagUpdate', function() 624 | print("requestFlagUpdate from: " .. source .. "\n") 625 | local playerPed = GetPlayerPed(source) 626 | local playerTeam = ctfGame:getPlayerTeam(source) 627 | 628 | -- Check if any flags have been taken, dropped or captured. 629 | if playerTeam.id ~= TeamType.TEAM_SPECTATOR then 630 | if GetEntityHealth(playerPed) < 1 then return end 631 | local ourFlag = ctfGame:getFlag(source, false) 632 | local flagToCapture = ctfGame:getFlag(source, true) 633 | if flagToCapture:isPastCooldown() and ourFlag:isPastCooldown() then 634 | if (flagToCapture:isDropped() or flagToCapture:isAtBase()) and flagToCapture:hasEntityInRadius(playerPed) then 635 | ctfGame:attemptToTakeFlag(flagToCapture, playerPed, source) 636 | elseif flagToCapture:isFlagCarrier(source) and ourFlag:isAtBase() then 637 | if playerTeam:goalBaseHasEntityInRadius(playerPed) then 638 | ctfGame:captureFlag(flagToCapture, ourFlag, source) 639 | end 640 | elseif ourFlag:hasEntityInRadius(playerPed) and ourFlag:isDropped() then 641 | ctfGame:returnFlag(ourFlag) 642 | end 643 | end 644 | end 645 | end) 646 | 647 | --- The playerJoining event. 648 | -- This is an event provided by FiveM. 649 | -- It's triggered when a player connects to the server and has a finally-assigned NetID. 650 | -- We use this method to send the team data to the client. 651 | -- We trigger a local event (registered on the server) named 'sendTeamDataToClient'. 652 | -- 653 | -- @param source (number) The player's NetID (a number in Lua/JS). 654 | -- @param oldID (number) The original TempID for the connecting player, as specified during playerConnecting. 655 | RegisterServerEvent('playerJoining') 656 | AddEventHandler('playerJoining', function(source, oldID) 657 | Player(source).state.teamID = TeamType.TEAM_RED -- Initialize to team red 658 | TriggerEvent("sendTeamDataToClient", source) 659 | end) 660 | 661 | --- The requestTeamData event. 662 | -- This is triggered by the client via TriggerServerEvent. 663 | -- This event is used to call another event that sends the team data to the client. 664 | -- We trigger a local event (registered on the server) named 'sendTeamDataToClient'. 665 | -- 666 | -- @param source (number) The player's NetID (a number in Lua/JS). 667 | RegisterServerEvent('requestTeamData') 668 | AddEventHandler('requestTeamData', function() 669 | TriggerEvent("sendTeamDataToClient", source) 670 | end) 671 | 672 | --- The sendTeamDataToClient event. 673 | -- This event can be triggered by the client or the server. 674 | -- This event is used to set up our teamsDataArray table and send it over to the client for parsing. 675 | -- 676 | -- @param source (number) The player's NetID (a number in Lua/JS). 677 | RegisterServerEvent("sendTeamDataToClient") 678 | AddEventHandler("sendTeamDataToClient", function(source) 679 | local teamsDataArray = {} 680 | -- We go through the gamemode's teams and add everything, but TEAM_SPECTATOR (so TEAM_BLUE and TEAM_RED only). 681 | for _, team in ipairs(ctfGame.teams) do 682 | if team.id ~= TeamType.TEAM_SPECTATOR then 683 | print("flag status is " .. ctfGame:getFlagByTeamID(team.id):getFlagStatus()) 684 | local teamData = { 685 | id = team.id, 686 | name = team:getName(), 687 | basePosition = team.basePosition, 688 | flagColor = team.flagColor, 689 | flagNetworkedID = ctfGame:getFlagByTeamID(team.id):getFlagNetworkedID(), 690 | flagStatus = ctfGame:getFlagByTeamID(team.id):getFlagStatus(), 691 | playerModel = team.playerModel, 692 | playerHeading = team.playerHeading, 693 | baseNetworkId = team.networkedID, 694 | score = team.score 695 | } 696 | teamsDataArray[#teamsDataArray+1] = teamData 697 | end 698 | end 699 | -- Finally trigger the client event receiveTeamData declared in ctf_client.lua 700 | TriggerClientEvent("receiveTeamData", source, teamsDataArray) 701 | end) 702 | 703 | RegisterCommand("shutdown", function(source, args, rawCommand) 704 | ctfGame:shutDown() 705 | end, true) 706 | 707 | --- Triggered when the resource is stopping. 708 | -- 709 | -- @param resourceName (string) The resource name, i.e. ctf_server. 710 | AddEventHandler('onResourceStop', function(resourceName) 711 | if (GetCurrentResourceName() ~= resourceName) then 712 | return 713 | end 714 | -- The gamemode is over, call our shutdown method which will remove the flags 715 | ctfGame:shutDown() 716 | print('The resource ' .. resourceName .. ' was stopped.') 717 | end) 718 | 719 | AddEventHandler('playerDropped', function (reason) 720 | local flagToCapture = ctfGame:getFlag(source, true) 721 | if flagToCapture:isFlagCarrier(source) then 722 | flagToCapture:setAsDropped() 723 | end 724 | end) 725 | 726 | -- Main game loop 727 | Citizen.CreateThread(function() 728 | while true do 729 | ctfGame:update() 730 | Citizen.Wait(500) -- Adjust the interval as needed 731 | end 732 | end) 733 | -------------------------------------------------------------------------------- /ctf-gamemode/ctf_shared.lua: -------------------------------------------------------------------------------- 1 | TeamType = { 2 | TEAM_BLUE = 1, 3 | TEAM_RED = 2, 4 | TEAM_SPECTATOR = 3, 5 | } 6 | 7 | EFlagStatuses = { 8 | AT_BASE = 1, 9 | BACK_TO_BASE = 2, 10 | DROPPED = 3, 11 | TAKEN = 4, 12 | CAPTURED = 5, 13 | CARRIER_DIED = 6 14 | } 15 | 16 | FlagStatuses = { 17 | { status = AT_BASE, description = "At Base" }, 18 | { status = BACK_TO_BASE, description = "Warping Back" }, 19 | { status = DROPPED, description = "Dropped" }, 20 | { status = TAKEN, description = "Taken" }, 21 | { status = CAPTURED, description = "Captured" }, 22 | { status = CARRIER_DIED, description = "Carrier Died" } 23 | } 24 | 25 | function getDescriptionForFlagStatus(statusID) 26 | return FlagStatuses[statusID] and FlagStatuses[statusID].description or "Unknown Status" 27 | end 28 | 29 | function entityHasEntityInRadius(entity, targetEntity, radius) 30 | radius = radius or 2.5 -- Default radius is 2.5 if not provided 31 | local flagPosition = GetEntityCoords(entity) 32 | local targetEntityPos = GetEntityCoords(targetEntity) 33 | local distance = #(flagPosition - targetEntityPos) 34 | return distance < radius -- Adjust the distance as needed 35 | end -------------------------------------------------------------------------------- /ctf-gamemode/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'bodacious' 2 | game 'gta5' 3 | 4 | author 'You' 5 | version '1.0.0' 6 | 7 | client_script { 8 | 'ctf_client.lua', 9 | 'ctf_rendering.lua' 10 | } 11 | server_script 'ctf_server.lua' 12 | shared_script { 13 | 'ctf_shared.lua', 14 | 'ctf_config.lua' 15 | } 16 | 17 | files { 18 | 'loadscreen/index.html', 19 | 'loadscreen/css/loadscreen.css', 20 | 'loadscreen/js/loadscreen.js', 21 | 'loadscreen/css/bankgothic.ttf', 22 | 'loadscreen/loadscreen.jpg' 23 | } 24 | 25 | loadscreen 'loadscreen/index.html' -------------------------------------------------------------------------------- /ctf-gamemode/loadscreen/css/bankgothic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citizenfx/example-resources/12d98b62e77468a078eebe5f44b9c3e198ad7ae1/ctf-gamemode/loadscreen/css/bankgothic.ttf -------------------------------------------------------------------------------- /ctf-gamemode/loadscreen/css/loadscreen.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'BankGothic'; 3 | src: url('bankgothic.ttf') format('truetype'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | body { 9 | margin: 0px; 10 | padding: 0px; 11 | } 12 | 13 | .backdrop { 14 | position: relative; 15 | top: 0px; 16 | left: 0px; 17 | width: 100%; 18 | height: 100%; 19 | 20 | background-image: url(../loadscreen.jpg); 21 | background-size: cover; 22 | } 23 | 24 | .top { 25 | color: #fff; 26 | 27 | position: relative; 28 | top: 5vh; 29 | margin-left: 5vw; 30 | margin-right: 5vw; 31 | } 32 | 33 | .top h1 { 34 | font-family: BankGothic; 35 | font-size: 60px; 36 | 37 | margin: 0px; 38 | } 39 | 40 | .top h2 { 41 | font-family: BankGothic; 42 | font-size: 40px; 43 | 44 | margin: 0px; 45 | 46 | color: #ddd; 47 | } 48 | 49 | .bottom { 50 | position: absolute; 51 | left: 50%; 52 | transform: translateX(-50%); 53 | width: 90vw; 54 | bottom: 5vh; 55 | } 56 | 57 | h1, h2 { 58 | position: relative; 59 | background: transparent; 60 | z-index: 0; 61 | } 62 | 63 | /* add a single stroke */ 64 | h1:before, h2:before { 65 | content: attr(title); 66 | position: absolute; 67 | -webkit-text-stroke: 0.1em #000; 68 | left: 0; 69 | z-index: -1; 70 | } 71 | 72 | .loading-container { 73 | z-index: 5; 74 | color: #fff; 75 | font-family: "Segoe UI"; 76 | } 77 | 78 | .loading-container .message-tip { 79 | font-family: BankGothic; 80 | text-transform: uppercase; 81 | font-size: 15px; 82 | margin: 0px; 83 | display: inline-block; 84 | opacity: 0; 85 | transition: opacity 0.5s ease-in-out; /* Adjust the transition duration and timing function as needed */ 86 | } 87 | 88 | .loadbar { 89 | display: flex; /* Use flexbox to align loading bars */ 90 | justify-content: space-between; /* Evenly distribute space between loading bars */ 91 | align-items: stretch; /* Ensure loading bars stretch to fill the height */ 92 | background-color: rgba(151, 151, 151, 0.3); 93 | height: 20px; 94 | margin-left: 2px; 95 | margin-right: 3px; 96 | margin-top: 5px; 97 | margin-bottom: 5px; 98 | position: relative; 99 | } 100 | 101 | .progress-wrapper { 102 | flex: 1; /* Each loading bar takes up equal space */ 103 | height: 100%; 104 | position: relative; 105 | text-align: center; 106 | font-size: 10px; 107 | 108 | display: flex; 109 | align-items: center; 110 | display: inline-block; 111 | } 112 | 113 | 114 | /* Property for all the progress bars */ 115 | .progress-bar { 116 | width: 0%; 117 | height: 20px; 118 | left: 0%; 119 | } 120 | 121 | .blue { 122 | background-color: rgba(3, 0, 153, 0.4); 123 | } 124 | 125 | .white { 126 | background-color: rgba(252, 252, 252, 0.4); 127 | } 128 | 129 | .red { 130 | background-color: rgba(255, 0, 0, 0.4); 131 | } 132 | 133 | /* Hide scrollbar for Chrome/CEF */ 134 | .log-line-msg::-webkit-scrollbar { 135 | display: none; 136 | } 137 | 138 | .log-line-msg { 139 | height: 35vh; 140 | width: auto; 141 | position: relative; 142 | margin-top: 10vh; 143 | margin-left: 5vw; 144 | margin-right: 5vw; 145 | padding: 10px; 146 | overflow-y: auto; /* Enable vertical scrolling */ 147 | background-color: rgba(151, 151, 151, 0.236); /* Default background color */ 148 | border-radius: 5px; /* Add border radius for a rounded appearance */ 149 | } 150 | 151 | /* Style for individual message divs */ 152 | .log-line-msg > .message { 153 | padding: 5px; 154 | margin-bottom: 5px; 155 | border-radius: 5px; 156 | color: #fff; /* Text color */ 157 | background-color: rgba(160, 0, 0, 0.1); /* Default background color */ 158 | font-family: "Segoe UI"; 159 | } 160 | -------------------------------------------------------------------------------- /ctf-gamemode/loadscreen/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 |

Capture The Flag

9 |

CTF

10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /ctf-gamemode/loadscreen/js/loadscreen.js: -------------------------------------------------------------------------------- 1 | // This is a very basic example of a load screen. 2 | 3 | // If you would like to see an advanced example, check out the following: 4 | // https://github.com/citizenfx/fivem/blob/master/ext/ui-build/loadscreen/code.jsx 5 | 6 | var intervalId; // Variable to store the interval ID 7 | var loadState = {} 8 | 9 | // Load phases used for progress bars 10 | const loadPhases = { 11 | INIT_CORE: ['Init Core'], 12 | INIT_BEFORE_MAP_LOADED: ['Before Map Loaded'], 13 | MAP: ['MAP'], 14 | INIT_AFTER_MAP_LOADED: ['After Map Loaded'], 15 | INIT_SESSION: ['Session'] 16 | }; 17 | 18 | // Hint message array 19 | var messages = [ 20 | "Grab the enemy's package and capture it.", 21 | "You can spawn by pressing SHIFT.", 22 | "Cycle through teams by using your left and right mouse button." 23 | ]; 24 | 25 | // Start index for cycling through hint messages 26 | var currentIndex = 0; 27 | 28 | // Hide the main element until messages are shown, see logMessage function 29 | document.querySelector('.log-line-msg').style.display = 'none'; 30 | 31 | // Load screen handlers 32 | // See LoadingScreens.cpp for more detailed information: 33 | // https://github.com/citizenfx/fivem/blob/master/code/components/loading-screens-five/src/LoadingScreens.cpp#L586 34 | const handlers = { 35 | startInitFunction(data) { 36 | // Check if loadState for the data type is uninitialized 37 | if (loadState[data.type] === undefined) { 38 | // Initialize loadState for the data type with count and processed properties 39 | loadState[data.type] = { count: 0, processed: 0 }; 40 | 41 | // Start the progress update interval if it's not already running 42 | if (!intervalId) { 43 | intervalId = setInterval(updateProgressBars, 100); 44 | } 45 | } 46 | }, 47 | 48 | startInitFunctionOrder(data) { 49 | if(loadState[data.type] !== undefined) { 50 | loadState[data.type].count += data.count; 51 | } 52 | }, 53 | 54 | initFunctionInvoked(data) { 55 | if(loadState[data.type] !== undefined) { 56 | loadState[data.type].processed++; 57 | } 58 | 59 | logMessage({ message: `Invoked: ${data.type} ${data.name}!` }); 60 | }, 61 | 62 | startDataFileEntries(data) { 63 | loadState["MAP"] = {}; 64 | loadState["MAP"].count = data.count; 65 | loadState["MAP"].processed = 0; 66 | }, 67 | 68 | performMapLoadFunction(data) { 69 | loadState["MAP"].processed++; 70 | }, 71 | 72 | onLogLine(data) { 73 | logMessage(data); 74 | } 75 | }; 76 | 77 | window.addEventListener('message', function (e) { 78 | /* 79 | Call each handler i.e. 80 | startInitFunction, startInitFunctionOrder, initFunctionInvoked, etc. 81 | */ 82 | (handlers[e.data.eventName] || function () { })(e.data); 83 | }); 84 | 85 | function logMessage(data) { 86 | /* 87 | Log game related messages. 88 | i.e. Function invoked messages among others. 89 | */ 90 | const logLineMsg = document.querySelector('.log-line-msg'); 91 | 92 | logLineMsg.style.display = 'block'; // Show the main element 93 | 94 | // Create a div for our message 95 | const newMessage = document.createElement('div'); 96 | 97 | // Set the text content to the message 98 | newMessage.textContent = data.message; 99 | 100 | // Add the 'message' class for styling 101 | newMessage.classList.add('message'); 102 | 103 | // Append newMessage to logLineMsg 104 | logLineMsg.appendChild(newMessage); 105 | 106 | // Scroll to the bottom to show the latest message 107 | logLineMsg.scrollTop = logLineMsg.scrollHeight; 108 | } 109 | 110 | function updateProgressBars() { 111 | // Iterate through all progress bars, updating each. 112 | for (const phaseName in loadPhases) { 113 | if (loadState[phaseName] != null){ 114 | console.log(`${phaseName}, Processed: ${loadState[phaseName].processed}, Total: ${loadState[phaseName].count}`); 115 | updateProgressBar(phaseName, loadState[phaseName].processed, loadState[phaseName].count); 116 | } 117 | } 118 | } 119 | 120 | function updateProgressBar(type, idx, count) { 121 | /* 122 | Update each individual progress bar, passed by type 123 | Those are: 124 | - INIT_BEFORE_MAP_LOADED 125 | - MAP 126 | - INIT_SESSION 127 | 128 | Count is the max count of items we're processing 129 | idx is the current count up until count 130 | */ 131 | // Replace underscores (_) with hyphens (-) and convert to lowercase 132 | const progressBarName = type.replace(/_/g, '-').toLowerCase(); 133 | 134 | // Find the element with the class name generated above 135 | const progressBar = document.querySelector(`.${progressBarName}`); 136 | 137 | // Calculate the width based on the index (idx) and total count (count) 138 | const progressBarWidth = ((idx / count) * 100).toFixed(2); 139 | 140 | // Set the width of the progress bar element 141 | progressBar.style.width = `${progressBarWidth}%`; 142 | 143 | var parentOfProgressBar = progressBar.parentNode; 144 | 145 | // Check if the span element already exists, we will use these spans to show a description for each progress bar 146 | var spanElement = parentOfProgressBar.querySelector("span"); 147 | if (!spanElement) { 148 | // Create a new span element 149 | spanElement = document.createElement("span"); 150 | // Append the new span to the parent element 151 | parentOfProgressBar.appendChild(spanElement); 152 | } 153 | 154 | // Set the text content for the progress bar span element 155 | spanElement.textContent = `${loadPhases[type]} (${progressBarWidth}%)` || ''; 156 | } 157 | 158 | function displayRandomHintMessage() { 159 | // Get the .message-tip span element 160 | var messageTipElement = document.querySelector('.message-tip'); 161 | // Fade out the existing message 162 | messageTipElement.style.opacity = 0; 163 | 164 | // Schedule a function to execute after a short delay 165 | setTimeout(function() { 166 | // Set the text content of the .message-tip span element to the message 167 | messageTipElement.textContent = messages[currentIndex]; 168 | // Fade in the new message 169 | messageTipElement.style.opacity = 1; 170 | }, 500); // Delay set to 500 milliseconds 171 | 172 | // Move to the next index for the next message 173 | currentIndex = (currentIndex + 1) % messages.length; 174 | } 175 | 176 | // Function to set timeout for printing random messages 177 | function displayHintMessage(intervalInSeconds) { 178 | // Print a random message immediately 179 | displayRandomHintMessage(); 180 | 181 | // Set timeout to print a random message every intervalInSeconds seconds 182 | setInterval(displayRandomHintMessage, intervalInSeconds * 1000); 183 | } 184 | 185 | // Call the function with the desired interval in seconds (e.g., 2 seconds) 186 | displayHintMessage(2); 187 | -------------------------------------------------------------------------------- /ctf-gamemode/loadscreen/loadscreen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citizenfx/example-resources/12d98b62e77468a078eebe5f44b9c3e198ad7ae1/ctf-gamemode/loadscreen/loadscreen.jpg -------------------------------------------------------------------------------- /tdm-gamemode/README.md: -------------------------------------------------------------------------------- 1 | # Team Deathmatch (TDM) Game Mode 2 | 3 | Welcome! 4 | 5 | This repository contains the source code and resources for implementing a TDM game mode in your game project. 6 | 7 | Whether you're building a multiplayer game, experimenting with game mechanics, or enhancing an existing project, this TDM game mode provides a starting point to get you started. 8 | 9 | ## Server/Client Logic 10 | 11 | - **client.lua:** Handles drawing on the client as well as team selection. 12 | - **server.lua:** Handles most of the TDM logic. 13 | - **shared.lua:** Has some structs (and lua tables) and a single function that is shared by the server and the client. Documentation about the `shared_script` directive can be found [here](https://docs.fivem.net/docs/scripting-reference/resource-manifest/resource-manifest/#shared_script). 14 | 15 | ### Multiple classes are present 16 | 17 | For the server logic and teams 'classes' are used, Lua supports such in the form of metatables under [chapter 16.1](https://www.lua.org/pil/16.1.html). 18 | 19 | Classes used in this game-mode are detailed down below: 20 | 21 | - **TDMGame:** The main class used to interact with teams, its `constructor` (to initialize teams) and a `TDMGame:shutDown` method that is used to 'dispose' of any team instances once `onResourceStop` gets called. 22 | - **Team:** An instance of `Team` stores the base position and each team color, teams (referred by `TDMGame` as `self.teams`) are **Blue**, **Red** and **Spectator** (where **Spectator** is simply a placeholder at the moment). 23 | 24 | ## Features 25 | 26 | This game mode utilizes the following FiveM features: 27 | 28 | - [**State Bags**](https://docs.fivem.net/docs/scripting-manual/networking/state-bags): To keep track of any entity states between client and server. 29 | -------------------------------------------------------------------------------- /tdm-gamemode/fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'bodacious' 2 | game 'gta5' 3 | 4 | author 'You' 5 | version '1.0.0' 6 | 7 | client_script 'tdm_client.lua' 8 | server_script 'tdm_server.lua' 9 | shared_script { 10 | 'tdm_shared.lua', 11 | 'tdm_config.lua' 12 | } 13 | -------------------------------------------------------------------------------- /tdm-gamemode/tdm_client.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File: tdm_client.lua 3 | Description: 4 | This is the main client file and its main purpose is: 5 | - To handle the following client related logic executed via our main thread: (see Citizen.CreateThread). 6 | - To draw any instructional UI on the screen: (see drawScaleFormUI). 7 | - To perform team selection and camera manipulation if the user is in camera selection: (see boolean bInTeamSelection). 8 | 9 | - To receive team data from the server via events: (see receiveTeamData). 10 | 11 | Event handlers: 12 | - SendClientHudNotification: Dispatched by the server to display simple 'toast' UI notifications on the client. 13 | - playerSpawned: Dispatched by FiveM resource spawnManager when the player spawns (https://docs.fivem.net/docs/resources/spawnmanager/events/playerSpawned/) 14 | We use this event to set the player's position to the base position on first spawn (or respawn after death). 15 | - gameEventTriggered: Used in conjunction with CEventNetworkEntityDamage to check if the player was killed. 16 | Information is then relayed to the server via an event (tdm:onPlayerKilled). 17 | 18 | Variables used by this script: 19 | - bInTeamSelection: Stores whether the user is in camera selection or not, set by method: setIntoTeamSelection. 20 | It is set to true on script initialization and false once a team is picked. 21 | 22 | - receivedServerTeams: A table assigned by receiveTeamData, holding data sent from the server. 23 | It contains information about the teams the user may pick (Red and Blue). 24 | 25 | - lastTeamSelKeyPress: Used to introduce a cooldown period 'on left/right click' when cycling through team selection. 26 | Without this cooldown, rapid clicks could lead to unintended and fast team cycling. 27 | 28 | - teamID: Stores the local team, mainly used to decide if the client should go into team/camera selection. 29 | It is also used to spawn the client at their respective base (see 'playerSpawned' event). 30 | This is a state bag and it's shared with the server 31 | 32 | - activeCameraHandle: Stores the created camera handle (set by CreateCam in setIntoTeamSelection) for later use. 33 | ]] 34 | 35 | -- Declare the variables this script will use 36 | local bInTeamSelection = false 37 | local receivedServerTeams = nil 38 | local lastTeamSelKeyPress = -1 39 | local activeCameraHandle = -1 40 | local spawnPoints = {} 41 | local enemyBlips = {} 42 | 43 | -- Define controller variables 44 | local CONTROL_LMB = 329 -- Left mouse button 45 | local CONTROL_RMB = 330 -- Right mouse button 46 | local CONTROL_LSHIFT = 209 -- Left shift 47 | 48 | -- UI related variables. 49 | -- Create a local reference to tdmConfig.UI 50 | local UIConfig = tdmConfig.UI 51 | 52 | -- Access 'teamTxtProperties' from the 'tdmConfig.UI' table referenced by 'UIConfig'. 53 | local UITeamTxtProps = UIConfig.teamTxtProperties 54 | 55 | -- Access 'btnCaptions' properties from the 'tdmConfig.UI' table referenced by 'UIConfig'. 56 | local btnCaptions = UIConfig.btnCaptions 57 | 58 | -- Set the teamID to spectator on script initialization 59 | LocalPlayer.state:set('teamID', TeamType.TEAM_SPECTATOR, true) 60 | 61 | -- Caching the spawnmanager export 62 | local spawnmanager = exports.spawnmanager 63 | 64 | ---------------------------------------------- Functions ---------------------------------------------- 65 | 66 | --- Our callback method for the autoSpawnCallback down below, we give ourselves guns here. 67 | function onPlayerSpawnCallback() 68 | local ped = PlayerPedId() -- 'Cache' our ped so we're not invoking the native multiple times. 69 | 70 | -- Spawn the player via an export at the player team's spawn point. 71 | spawnmanager:spawnPlayer( 72 | spawnPoints[LocalPlayer.state.teamID] 73 | ) 74 | 75 | -- Let's use compile-time jenkins hashes to give ourselves an assault rifle. 76 | GiveWeaponToPed(ped, `weapon_assaultrifle`, 300, false, true) 77 | 78 | -- Enable player vs player so players can target and shoot each other 79 | NetworkSetFriendlyFireOption(true) 80 | SetCanAttackFriendly(ped, true, true) 81 | 82 | -- Clear any previous blood damage 83 | ClearPedBloodDamage(ped) 84 | 85 | -- Make us visible again 86 | SetEntityVisible(ped, true) 87 | end 88 | 89 | function setIntoTeamSelection(team, bIsInTeamSelection) 90 | -- Sets the player into camera selection 91 | -- Main camera handle only gets created once in order to manipulate it later 92 | local ped = PlayerPedId() -- Let's cache the Player Ped ID so we're not constantly calling PlayerPedId() 93 | 94 | LocalPlayer.state:set('teamID', team, true) 95 | bInTeamSelection = bIsInTeamSelection 96 | local origCamCoords = receivedServerTeams[team].basePosition 97 | local camFromCoords = vector3(origCamCoords.x, origCamCoords.y + 2.0, origCamCoords.z + 2.0) 98 | if activeCameraHandle == -1 then 99 | activeCameraHandle = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) 100 | end 101 | 102 | SetEntityCoords(ped, origCamCoords.x, origCamCoords.y, origCamCoords.z, false, false, false, true) 103 | SetCamCoord(activeCameraHandle, camFromCoords) 104 | PointCamAtCoord(activeCameraHandle, origCamCoords) 105 | RenderScriptCams(bInTeamSelection) 106 | SetEntityVisible(ped, not bIsInTeamSelection) 107 | end 108 | 109 | function buttonMessage(text) 110 | BeginTextCommandScaleformString("STRING") 111 | AddTextComponentScaleform(text) 112 | EndTextCommandScaleformString() 113 | end 114 | 115 | --- Draws the scaleform UI displaying controller buttons and associated messages for player instructions. 116 | -- 117 | -- @param buttonsHandle (number) The handle for the scaleform movie. 118 | function drawScaleFormUI(buttonsHandle) 119 | while not HasScaleformMovieLoaded(buttonsHandle) do -- Wait for the scaleform to be fully loaded 120 | Wait(0) 121 | end 122 | 123 | CallScaleformMovieMethod(buttonsHandle, 'CLEAR_ALL') -- Clear previous buttons 124 | 125 | PushScaleformMovieFunction(buttonsHandle, "SET_DATA_SLOT") 126 | PushScaleformMovieFunctionParameterInt(2) 127 | ScaleformMovieMethodAddParamPlayerNameString("~INPUT_SPRINT~") 128 | buttonMessage(btnCaptions.Spawn) 129 | PopScaleformMovieFunctionVoid() 130 | 131 | PushScaleformMovieFunction(buttonsHandle, "SET_DATA_SLOT") 132 | PushScaleformMovieFunctionParameterInt(1) 133 | ScaleformMovieMethodAddParamPlayerNameString("~INPUT_ATTACK~") 134 | buttonMessage(btnCaptions.PreviousTeam) 135 | PopScaleformMovieFunctionVoid() 136 | 137 | PushScaleformMovieFunction(buttonsHandle, "SET_DATA_SLOT") 138 | PushScaleformMovieFunctionParameterInt(0) 139 | ScaleformMovieMethodAddParamPlayerNameString("~INPUT_AIM~") -- The button to display 140 | buttonMessage(btnCaptions.NextTeam) -- the message to display next to it 141 | PopScaleformMovieFunctionVoid() 142 | 143 | CallScaleformMovieMethod(buttonsHandle, 'DRAW_INSTRUCTIONAL_BUTTONS') -- Sets buttons ready to be drawn 144 | end 145 | 146 | function removePlayerBlips() 147 | for blipTableIdx, blipHandle in ipairs(enemyBlips) do 148 | local blipOwningEntity = GetBlipInfoIdEntityIndex(blipHandle) 149 | local playerId = GetPlayerServerId(NetworkGetPlayerIndexFromPed(blipOwningEntity)) 150 | if not DoesEntityExist(blipOwningEntity) or Player(playerId).state.teamID == LocalPlayer.state.teamID then 151 | print("Removed orphan blip (" .. GetPlayerName(NetworkGetPlayerIndexFromPed(blipOwningEntity)) .. ")") 152 | RemoveBlip(blipHandle) 153 | table.remove(enemyBlips, blipTableIdx) 154 | end 155 | end 156 | end 157 | 158 | function tryCreateBlips() 159 | for _, player in ipairs(GetActivePlayers()) do 160 | local ped = GetPlayerPed(player) 161 | if GetBlipFromEntity(ped) == 0 then 162 | if Player(GetPlayerServerId(player)).state.teamID ~= LocalPlayer.state.teamID then 163 | print('Added ' .. GetPlayerName(player)) 164 | enemyBlips[#enemyBlips+1] = AddBlipForEntity(ped) -- Store the blip handle in the table 165 | end 166 | end 167 | end 168 | end 169 | 170 | --- Used to draw text on the screen. 171 | -- Multiple natives are called for drawing. 172 | -- Documentation for those natives can be found at http://docs.fivem.net/natives 173 | -- 174 | -- @param x (number) Where on the screen to draw text (horizontal axis). 175 | -- @param y (number) Where on the screen to draw text (vertical axis). 176 | -- @param width (number) The width for the text. 177 | -- @param height (number) The height for the text. 178 | -- @param scale (number) The scale for the text. 179 | -- @param text (string) The actual text value to display. 180 | -- @param r (number) The value for red (0-255). 181 | -- @param g (number) The value for green (0-255). 182 | -- @param b (number) The value for blue (0-255). 183 | -- @param alpha (number) The value for alpha/opacity (0-255). 184 | function drawTxt(x, y, width, height, scale, text, r, g, b, a) 185 | SetTextFont(2) 186 | SetTextProportional(0) 187 | SetTextScale(scale, scale) 188 | SetTextColour(r, g, b, a) 189 | SetTextDropShadow(0, 0, 0, 0,255) 190 | SetTextEdge(1, 0, 0, 0, 255) 191 | SetTextDropShadow() 192 | SetTextOutline() 193 | -- Let's use our previously created text entry 'textRenderingEntry' 194 | SetTextEntry("textRenderingEntry") 195 | AddTextComponentString(text) 196 | DrawText(x - width/2, y - height/2 + 0.005) 197 | end 198 | 199 | --- Handles player input for team selection. 200 | -- This function allows players to navigate through available teams using mouse clicks and to confirm their selection by pressing the left shift key. 201 | -- Mouse click on the left button (LMB) decreases the team selection index by one, while a click on the right button (RMB) increases it by one. 202 | -- The left shift key confirms the selected team and spawns the player character at the designated spawn point. 203 | function handleTeamSelectionControl() 204 | local teamSelDirection = 0 205 | local bPressedSpawnKey = false 206 | 207 | -- Determine the direction of team selection based on mouse clicks 208 | if IsControlPressed(0, CONTROL_LMB) then 209 | teamSelDirection = -1 -- Previous team 210 | elseif IsControlPressed(0, CONTROL_RMB) then 211 | teamSelDirection = 1 -- Next team 212 | elseif IsControlPressed(0, CONTROL_LSHIFT) then -- Left Shift 213 | -- Let's spawn! 214 | bInTeamSelection = false -- We're no longer in team/camera selection 215 | bPressedSpawnKey = true 216 | 217 | -- Spawn the player 218 | exports.spawnmanager:spawnPlayer( 219 | spawnPoints[LocalPlayer.state.teamID], 220 | onPlayerSpawnCallback 221 | ) 222 | end 223 | 224 | -- Determine the direction of team selection based on mouse clicks 225 | if teamSelDirection ~= 0 or bPressedSpawnKey then 226 | local newTeamID = LocalPlayer.state.teamID + teamSelDirection 227 | if newTeamID >= 1 and newTeamID <= #receivedServerTeams then 228 | LocalPlayer.state:set('teamID', newTeamID, true) 229 | lastTeamSelKeyPress = GetGameTimer() + 500 230 | end 231 | setIntoTeamSelection(LocalPlayer.state.teamID, bInTeamSelection) 232 | end 233 | end 234 | 235 | -- Define a function to format the team name 236 | function formatTeamName(receivedServerTeams, teamID) 237 | -- Check if receivedServerTeams is valid and contains the teamID 238 | if receivedServerTeams and receivedServerTeams[teamID] then 239 | -- Concatenate the team name with " Team" suffix 240 | return receivedServerTeams[teamID].name .. " Team" 241 | else 242 | -- Return a default message if the team name cannot be formatted 243 | return "Unknown Team" 244 | end 245 | end 246 | 247 | function shouldGoIntoCameraSelection() 248 | return LocalPlayer.state.teamID == TeamType.TEAM_SPECTATOR and not bInTeamSelection 249 | end 250 | 251 | 252 | ---------------------------------------------- Event handlers ---------------------------------------------- 253 | 254 | RegisterNetEvent("SendClientHudNotification") 255 | AddEventHandler("SendClientHudNotification", function(message) 256 | BeginTextCommandThefeedPost("STRING") 257 | AddTextComponentSubstringPlayerName(message) 258 | EndTextCommandThefeedPostTicker(true, true) 259 | end) 260 | 261 | -- Register the event handler to receive team data 262 | RegisterNetEvent("receiveTeamData") 263 | AddEventHandler("receiveTeamData", function(teamsData) 264 | receivedServerTeams = teamsData 265 | 266 | for _, team in ipairs(receivedServerTeams) do 267 | spawnPoints[team.id] = spawnmanager:addSpawnPoint({ 268 | x = team.basePosition.x, 269 | y = team.basePosition.y, 270 | z = team.basePosition.z, 271 | heading = team.playerHeading, 272 | model = team.playerModel, 273 | skipFade = false 274 | }) 275 | end 276 | end) 277 | 278 | --- Sets the player health to 0 (kills the player) 279 | RegisterNetEvent("killPlayer") 280 | AddEventHandler("killPlayer", function() 281 | -- Over here 'ped' isn't cached since it's only called once 282 | SetEntityHealth(PlayerPedId(), 0) 283 | end) 284 | 285 | --- Here we handle the CEventNetworkEntityDamage event. 286 | -- Documentation on gameEventTriggered can be found here: https://docs.fivem.net/docs/scripting-reference/events/list/gameEventTriggered/ 287 | -- The full list of events can be found linked on the forementioned URL as well. 288 | AddEventHandler("gameEventTriggered", function(name, args) 289 | if not (name == "CEventNetworkEntityDamage") then return end 290 | 291 | local victimID = GetPlayerServerId(NetworkGetPlayerIndexFromPed(args[1])) 292 | local killerID = GetPlayerServerId(NetworkGetPlayerIndexFromPed(args[2])) 293 | 294 | if IsEntityDead(args[1]) then 295 | if GetPlayerServerId(PlayerId()) == killerID then 296 | TriggerServerEvent("tdm:onPlayerKilled", killerID, victimID) 297 | end 298 | end 299 | end) 300 | 301 | --- 302 | -- Event handler triggered when a resource starts. 303 | -- Requests team data from the server when the resource starts. 304 | -- For more information regarding onClientResourceStart, visit the following link: 305 | -- https://docs.fivem.net/docs/scripting-reference/events/list/onClientResourceStart/ 306 | -- @param resourceName The name of the resource that started. 307 | AddEventHandler('onClientResourceStart', function(resourceName) 308 | if (GetCurrentResourceName() ~= resourceName) then 309 | return 310 | end 311 | -- Let's create our entry for text rendering. 312 | -- '~a~' is a placeholder for a substring 'text component', such as ADD_TEXT_COMPONENT_SUBSTRING_TEXT_LABEL. 313 | -- More here: 314 | -- https://docs.fivem.net/docs/game-references/text-formatting/#content-formatting-codes 315 | AddTextEntry("textRenderingEntry", "~a~") 316 | -- Let's request team data from the server when we join. 317 | TriggerServerEvent("requestTeamData") 318 | -- Send a console message showing that the resource has been started 319 | print('The resource ' .. resourceName .. ' has been started.') 320 | end) 321 | 322 | --- This event is dispatched by spawnmanager once the player spawns (URL at the top of the file). 323 | AddEventHandler('playerSpawned', function() 324 | if shouldGoIntoCameraSelection() then 325 | setIntoTeamSelection(TeamType.TEAM_BLUE, true) 326 | end 327 | end) 328 | 329 | --- Used to switch teams 330 | -- For more information on RegisterCommand, see the following link: 331 | -- https://docs.fivem.net/natives/?_0x5FA79B0F 332 | RegisterCommand("switchteam", function(source, args, rawCommand) 333 | setIntoTeamSelection(TeamType.TEAM_BLUE, true) 334 | end) 335 | 336 | ---------------------------------------------- Callbacks ---------------------------------------------- 337 | 338 | --- This handles player auto spawning after death. 339 | -- See spawnmanager's documentation for more: https://docs.fivem.net/docs/resources/spawnmanager/ 340 | spawnmanager:setAutoSpawnCallback(onPlayerSpawnCallback) 341 | spawnmanager:setAutoSpawn(true) 342 | 343 | ---------------------------------------------- Threads ---------------------------------------------- 344 | 345 | -- Threads are used to perform tasks asynchronously. 346 | -- They are based on lua's coroutines 347 | -- Lua's coroutines basics can be found here: https://www.lua.org/pil/9.1.html 348 | 349 | -- Refresh blips every two seconds in case new players join in 350 | Citizen.CreateThread(function() 351 | while true do 352 | -- Cleanup any old blips 353 | removePlayerBlips() 354 | 355 | -- Recreate blips 356 | tryCreateBlips() 357 | Citizen.Wait(2000) 358 | end 359 | end) 360 | 361 | --- Our main thread. 362 | -- We use this thread to perform Text/Sprite Rendering, handling drawing of instructional UI, team selection and camera manipulation. 363 | Citizen.CreateThread(function() 364 | local buttonsHandle = RequestScaleformMovie('INSTRUCTIONAL_BUTTONS') -- Request the scaleform to be loaded 365 | drawScaleFormUI(buttonsHandle) 366 | 367 | while true do 368 | if receivedServerTeams ~= nil and #receivedServerTeams > 0 then 369 | -- Our spectator team is not in use, it's only there for team selection purposes. 370 | -- So if we're in that team and we're not in camera selection, we initiate the team selection process. 371 | if shouldGoIntoCameraSelection() then 372 | setIntoTeamSelection(TeamType.TEAM_BLUE, true) 373 | end 374 | 375 | -- Run the logic for picking a team 376 | if bInTeamSelection then 377 | DisableRadarThisFrame() 378 | HideHudAndRadarThisFrame() 379 | 380 | -- Draw the instructional buttons this frame 381 | DrawScaleformMovieFullscreen(buttonsHandle, 255, 255, 255, 255, 1) 382 | 383 | if GetGameTimer() > lastTeamSelKeyPress then 384 | -- Determine if the user pressed one of the mouse buttons or SHIFT 385 | -- Sets LocalPlayer.state.teamID if so 386 | handleTeamSelectionControl() 387 | 388 | if LocalPlayer.state.teamID and LocalPlayer.state.teamID <= #receivedServerTeams then 389 | -- Draw the text on the screen for this specific team 390 | -- This will use the properties from our tdm_config.lua file 391 | drawTxt( 392 | UITeamTxtProps.x, 393 | UITeamTxtProps.y, 394 | UITeamTxtProps.width, 395 | UITeamTxtProps.height, 396 | UITeamTxtProps.scale, 397 | formatTeamName(receivedServerTeams, LocalPlayer.state.teamID), 398 | UITeamTxtProps.color.r, 399 | UITeamTxtProps.color.g, 400 | UITeamTxtProps.color.b, 401 | UITeamTxtProps.color.a 402 | ) 403 | end 404 | end 405 | end 406 | end 407 | -- No wanted level 408 | ClearPlayerWantedLevel(PlayerId()) 409 | 410 | Citizen.Wait(0) 411 | end 412 | end) 413 | -------------------------------------------------------------------------------- /tdm-gamemode/tdm_config.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File: tdm_config.lua 3 | Description: 4 | This file contains configurations for the Team Deathmatch (TDM) game mode, 5 | including properties for each team such as team ID, base position, player model and UI. 6 | 7 | Configuration Format: 8 | Each team configuration is represented as a table with the following properties: 9 | - id: The ID of the team. 10 | - basePosition: The base position of the team. 11 | - playerModel: The player model associated with the team. 12 | - playerHeading: The desired player heading on spawn. 13 | ]] 14 | 15 | tdmConfig = {} 16 | 17 | tdmConfig.teams = { 18 | { 19 | id = TeamType.TEAM_RED, 20 | basePosition = vector3(2555.1860, -333.1058, 92.9928), 21 | playerModel = 'a_m_y_beachvesp_01', 22 | playerHeading = 90.0 23 | }, 24 | { 25 | id = TeamType.TEAM_BLUE, 26 | basePosition = vector3(2574.9807, -342.9044, 92.9928), 27 | playerModel = 's_m_m_armoured_02', 28 | playerHeading = 90.0 29 | }, 30 | { 31 | id = TeamType.TEAM_SPECTATOR, 32 | basePosition = vector3(2574.9807, -342.9044, 92.9928), 33 | playerModel = 's_m_m_armoured_02', 34 | playerHeading = 90.0 35 | }, 36 | -- Add more team configurations as needed 37 | } 38 | 39 | tdmConfig.UI = { 40 | btnCaptions = { 41 | Spawn = "Spawn", 42 | NextTeam = "Next Team", 43 | PreviousTeam = "Previous Team" 44 | }, 45 | teamTxtProperties = { 46 | x = 1.0, -- Screen X coordinate 47 | y = 0.9, -- Screen Y coordinate 48 | width = 0.4, -- Width of the text 49 | height = 0.070, -- Height of the text 50 | scale = 1.0, -- Text scaling 51 | text = "", -- Text content 52 | color = { -- Color components 53 | r = 255, -- Red color component 54 | g = 255, -- Green color component 55 | b = 255, -- Blue color component 56 | a = 255 -- Alpha (transparency) value 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tdm-gamemode/tdm_server.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File: tdm_server.lua 3 | Description: 4 | This file handles server-side logic for the Team Deathmatch (TDM) game mode. 5 | It includes functions for team management, player kills tracking, team data updates, and game state maintenance. 6 | 7 | Functions: 8 | - Team.new: Constructs a new Team object representing a team in the TDM game mode. 9 | - Team:destroy: Destroys the Team object, resetting its properties. 10 | - Team:getName: Retrieves the name of the team. 11 | - Team:incrementKills: Increments the kill count for the team. 12 | 13 | - TDMGame.new: Initializes a new instance of the TDMGame class. 14 | - TDMGame:shutDown: Cleans up resources and stops the TDM game. 15 | - TDMGame:getPlayerTeam: Retrieves the team instance for a player. 16 | - TDMGame:getLeadingTeam: Determines the leading team based on kills count. 17 | 18 | Event Handlers: 19 | - tdm:onPlayerKilled: Handles player kill events, updating team kills count and notifying clients. 20 | 21 | Variables: 22 | - teamAssignments: Stores player assignments to different teams for balancing purposes. 23 | - g_PlayerTeams: Maps player IDs to their assigned team IDs. 24 | 25 | Classes: 26 | - Team: Represents a team in the TDM game mode. 27 | - TDMGame: Manages the overall game state and logic for the TDM game mode. 28 | ]] 29 | 30 | 31 | -- Define the Team class 32 | Team = {} 33 | Team.__index = Team 34 | 35 | --- Constructs a new Team object. 36 | -- @param teamID The ID of the team. 37 | -- @param basePosition The base position of the team. 38 | -- @param playerModel The player model associated with the team. 39 | -- @param playerHeading The desired player heading on spawn. 40 | -- @return Team The newly created Team object. 41 | function Team.new(teamID, basePosition, playerModel, playerHeading) 42 | local self = setmetatable({}, Team) 43 | self.id = teamID -- The ID of the team. 44 | self.kills = 0 -- The number of kills for the team. 45 | self.basePosition = basePosition -- The base position of the team. 46 | self.playerModel = playerModel -- The player model associated with the team. 47 | self.playerHeading = playerHeading -- The desired player heading on spawn. 48 | return self 49 | end 50 | 51 | --- Destroy the Team's instanced object. 52 | -- It will first destroy the team's entity (the base object), it will also reset certain properties of the class. 53 | -- Finally, it will set the metatable to nil, TDMGame:shutDown provides an example of this using iteration. 54 | -- 55 | -- Example: 56 | -- ```lua 57 | -- local blueTeam = Team.new(TeamType.TEAM_RED, vector3(2555.1860, -333.1058, 92.9928), 'a_m_y_beachvesp_01') 58 | -- blueTeam:destroy() 59 | -- ``` 60 | function Team:destroy() 61 | self.id = -1 62 | self.kills = 0 63 | self.basePosition = nil 64 | self.playerModel = '' 65 | 66 | setmetatable(self, nil) 67 | end 68 | 69 | --- Retrieves the name of the team. 70 | -- 71 | -- Example: 72 | -- ```lua 73 | -- local blueTeam = Team.new(1, {0, 0, 255}, vector3(100.0, 0.0, 50.0), 'a_m_y_beachvesp_01') 74 | -- blueTeam:getName() 75 | -- ``` 76 | function Team:getName() 77 | if self.id == TeamType.TEAM_BLUE then 78 | return "Blue" 79 | end 80 | if self.id == TeamType.TEAM_RED then 81 | return "Red" 82 | end 83 | return "Spectator" 84 | end 85 | 86 | --- Increments the kill count for the created team instance 87 | function Team:incrementKills(byNum) 88 | self.kills = self.kills + byNum 89 | end 90 | 91 | -- Define the TDMGame class 92 | TDMGame = {} 93 | TDMGame.__index = TDMGame 94 | 95 | --- Creates a new instance of the TDMGame class. 96 | -- This function initializes a new TDMGame object with default values. 97 | -- It sets up the teams for the Team Deathmatch (TDM) game mode, including the team locations and player models. 98 | -- The leading team is initialized as nil. 99 | -- @return table A new instance of the TDMGame class. 100 | function TDMGame.new() 101 | local self = setmetatable({}, TDMGame) 102 | self.teams = {} -- Initialize teams as an empty table 103 | -- Initialize each team with their respective position, player model and heading 104 | -- These are loaded from tdm_config.lua 105 | for _, teamConfig in ipairs(tdmConfig.teams) do 106 | self.teams[teamConfig.id] = Team.new( 107 | teamConfig.id, 108 | teamConfig.basePosition, 109 | teamConfig.playerModel, 110 | teamConfig.playerHeading 111 | ) 112 | end 113 | self.leadingTeam = nil 114 | return self 115 | end 116 | 117 | --- Call this method to end the game mode 118 | -- Will destroy all team instances. 119 | function TDMGame:shutDown() 120 | -- 'Dispose' on shutdown 121 | for _, team in ipairs(self.teams) do 122 | team:destroy() 123 | end 124 | end 125 | 126 | -- Define a method to get the team instance for a player 127 | function TDMGame:getPlayerTeam(playerID) 128 | local playerState = Player(playerID).state 129 | local teamID = playerState.teamID 130 | if teamID then 131 | return self.teams[teamID] 132 | else 133 | return nil 134 | end 135 | end 136 | 137 | -- Define the TDMGame method to get the leading team 138 | function TDMGame:getLeadingTeam() 139 | for _, team in pairs(self.teams) do 140 | if not self.leadingTeam or team.kills > self.leadingTeam.kills then 141 | self.leadingTeam = team 142 | end 143 | end 144 | return self.leadingTeam 145 | end 146 | 147 | -- Creating a TDM game 148 | local tdmGame = TDMGame.new() 149 | 150 | -------------------------------------- Event Handlers -------------------------------------- 151 | 152 | -- Event handler for player kills 153 | RegisterNetEvent("tdm:onPlayerKilled") 154 | AddEventHandler("tdm:onPlayerKilled", function(killerID, victimID) 155 | -- The player wasn't killed by another player. 156 | if killerID == 0 or victimID == 0 then return end 157 | 158 | -- We cannot trust the client, clients can lie to us and fabricate information. 159 | -- We're the server and we have state-awareness over what's happening. 160 | -- Let's do a simple check to verify that they died by who they said they did. 161 | local victimDeathSource = GetPedSourceOfDeath(GetPlayerPed(victimID)) 162 | if victimDeathSource ~= GetPlayerPed(killerID) then 163 | -- They lied to us, so we give them nothing. 164 | print(string.format("%s is possibly sending fake events.", GetPlayerName(killerID))) 165 | return 166 | end 167 | 168 | -- Looks good, let's continue by retrieving the killer's team instance 169 | local killerTeam = tdmGame:getPlayerTeam(killerID) 170 | local victimTeam = tdmGame:getPlayerTeam(victimID) 171 | 172 | if killerTeam.id == victimTeam.id then 173 | TriggerClientEvent("killPlayer", killerID) 174 | -- Color coding can also be added to strings (~r~), for more check: https://docs.fivem.net/docs/game-references/text-formatting/ 175 | TriggerClientEvent("SendClientHudNotification", killerID, "~r~Friendly fire won't be tolerated!") 176 | return 177 | end 178 | 179 | if killerTeam then 180 | -- Increment the team kills 181 | killerTeam:incrementKills(1) 182 | 183 | -- Notify clients about the leading team 184 | local leadingTeam = tdmGame:getLeadingTeam() 185 | if leadingTeam then 186 | local message = string.format("Team %s is leading with %s kill(s)", leadingTeam:getName(), leadingTeam.kills) 187 | -- Send a notification to every client, indicating by sending -1 as the source parameter 188 | TriggerClientEvent("SendClientHudNotification", -1, message) 189 | end 190 | else 191 | print("Player killed. Killer's team not found (error).") 192 | end 193 | end) 194 | 195 | --- A server-side event that is triggered when a player has a finally-assigned NetID. 196 | -- See the following link for more: 197 | -- https://docs.fivem.net/docs/scripting-reference/events/server-events/ 198 | RegisterServerEvent('playerJoining') 199 | AddEventHandler('playerJoining', function(source, oldID) 200 | Player(source).state.teamID = TeamType.TEAM_RED -- Initialize to team red 201 | TriggerEvent("sendTeamDataToClient", source) -- Trigger the event locally (on the server) 202 | end) 203 | 204 | -- Register a server event to send team data to clients 205 | RegisterServerEvent("sendTeamDataToClient") 206 | 207 | -- Event handler for sending team data to clients 208 | AddEventHandler("sendTeamDataToClient", function(source) 209 | -- Create an array to store team data 210 | local teamsDataArray = {} 211 | -- Iterate through each team in tdmGame.teams 212 | for _, team in ipairs(tdmGame.teams) do 213 | -- Exclude TEAM_SPECTATOR from the data 214 | if team.id ~= TeamType.TEAM_SPECTATOR then 215 | -- Create a table containing team information 216 | local teamData = { 217 | id = team.id, 218 | name = team:getName(), 219 | basePosition = team.basePosition, 220 | playerModel = team.playerModel, 221 | playerHeading = team.playerHeading 222 | } 223 | -- Insert the team data table into teamsDataArray 224 | teamsDataArray[#teamsDataArray+1] = teamData 225 | end 226 | end 227 | -- Trigger the "receiveTeamData" event on the client with the team data array 228 | TriggerClientEvent("receiveTeamData", source, teamsDataArray) 229 | end) 230 | 231 | --- The requestTeamData event. 232 | -- This is triggered by the client via TriggerServerEvent. 233 | -- This event is used to call another event that sends the team data to the client. 234 | -- We trigger a local event (registered on the server) named 'sendTeamDataToClient'. 235 | -- 236 | -- @param source (number) The player's NetID (a number in Lua/JS). 237 | RegisterServerEvent('requestTeamData') 238 | AddEventHandler('requestTeamData', function() 239 | TriggerEvent("sendTeamDataToClient", source) 240 | end) 241 | 242 | --- Triggered when the resource is stopping. 243 | -- This event can be triggered by the client via TriggerServerEvent. 244 | -- 245 | -- @param resourceName (string) The resource name, i.e. tdm_server. 246 | AddEventHandler('onResourceStop', function(resourceName) 247 | if (GetCurrentResourceName() ~= resourceName) then 248 | return 249 | end 250 | -- The gamemode is over, call our shutdown method which will remove the flags 251 | tdmGame:shutDown() 252 | print('The resource ' .. resourceName .. ' was stopped.') 253 | end) 254 | 255 | -------------------------------------- Commands -------------------------------------- 256 | 257 | --- Shutdown command used to clean-up the game mode (removes teams) 258 | -- For more information on RegisterCommand, see the following link: 259 | -- https://docs.fivem.net/natives/?_0x5FA79B0F 260 | RegisterCommand("shutdown", function(source, args, rawCommand) 261 | tdmGame:shutDown() 262 | end, true) 263 | -------------------------------------------------------------------------------- /tdm-gamemode/tdm_shared.lua: -------------------------------------------------------------------------------- 1 | TeamType = { 2 | TEAM_BLUE = 1, 3 | TEAM_RED = 2, 4 | TEAM_SPECTATOR = 3, 5 | } --------------------------------------------------------------------------------