├── img
└── driving_test_permit.png
├── fxmanifest.lua
├── readme.md
├── html
├── debounce.min.js
├── questions.js
├── styles.css
├── scripts.js
└── ui.html
├── server
└── main.lua
├── config.lua
└── client
├── gui.lua
└── main.lua
/img/driving_test_permit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/QBLes/qb-drivingschool/HEAD/img/driving_test_permit.png
--------------------------------------------------------------------------------
/fxmanifest.lua:
--------------------------------------------------------------------------------
1 | fx_version 'adamant'
2 | game 'gta5'
3 |
4 | description 'QB Driving School'
5 |
6 | version '1.0.4'
7 |
8 | server_scripts {
9 | 'config.lua',
10 | 'server/main.lua'
11 | }
12 |
13 | client_scripts {
14 |
15 | 'config.lua',
16 | 'client/main.lua',
17 | 'client/gui.lua',
18 | }
19 |
20 | ui_page 'html/ui.html'
21 |
22 | files {
23 | 'html/ui.html',
24 | 'html/logo.png',
25 | 'html/dmv.png',
26 | 'html/styles.css',
27 | 'html/questions.js',
28 | 'html/scripts.js',
29 | 'html/debounce.min.js'
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | qb-core/shaed/main.lua
2 |
3 | remove line 76: ['driver_license'] = { amount = 1, item = 'driver_license' },
4 |
5 |
6 |
7 | qb-core/server/player.lua
8 |
9 | change line 106 from ['driver] = true to false
10 |
11 |
12 |
13 | Add the following to qb-core/shaed/items.lua
14 |
15 | ['driving_test_permit'] = {['name'] = 'driving_test_permit', ['label'] = 'Driving Test Permit', ['weight'] = 0, ['type'] = 'item', ['image'] = 'driving_test_permit.png', ['unique'] = true, ['useable'] = true, ['shouldClose'] = true, ['combinable'] = nil, ['description'] = 'Permite for Driving Test'},
16 |
17 | Add the image from img folder into qb-inventory/html/images
18 |
19 |
20 | Once images added can remove the folder.
21 |
--------------------------------------------------------------------------------
/html/debounce.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery throttle / debounce - v1.1 - 3/7/2010
3 | * http://benalman.com/projects/jquery-throttle-debounce-plugin/
4 | *
5 | * Copyright (c) 2010 "Cowboy" Ben Alman
6 | * Dual licensed under the MIT and GPL licenses.
7 | * http://benalman.com/about/license/
8 | */
9 | (function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this);
10 |
--------------------------------------------------------------------------------
/server/main.lua:
--------------------------------------------------------------------------------
1 | QBCore = exports['qb-core']:GetCoreObject()
2 |
3 |
4 | QBCore.Functions.CreateCallback('qb-drivingschool:server:hasfunds', function(source, cb)
5 | local src = source
6 | local xPlayer = QBCore.Functions.GetPlayer(src)
7 | local bankamount = xPlayer.PlayerData.money["bank"]
8 | if bankamount >= Config.Price then
9 | xPlayer.Functions.RemoveMoney('bank', 500)
10 | cb(true)
11 | else
12 | cb(false)
13 | end
14 | end)
15 |
16 |
17 |
18 | RegisterServerEvent('qb-drivingschool:server:GetLicense', function()
19 | local src = source
20 | local Player = QBCore.Functions.GetPlayer(src)
21 |
22 |
23 | local info = {}
24 | info.firstname = Player.PlayerData.charinfo.firstname
25 | info.lastname = Player.PlayerData.charinfo.lastname
26 | info.birthdate = Player.PlayerData.charinfo.birthdate
27 | info.type = "Class C Driver License"
28 | Player.Functions.RemoveItem('driving_test_permit', 1)
29 | Player.Functions.AddItem('driver_license', 1, nil, info)
30 | -- add the meta to add license to true
31 | local licenses = {["driver"] = true, ['weapon'] = Player.PlayerData.metadata["licences"].weapon, ["business"] = Player.PlayerData.metadata["licences"].business}
32 | Player.Functions.SetMetaData("licences", licenses)
33 | TriggerClientEvent('inventory:client:ItemBox', src, QBCore.Shared.Items['driver_license'], 'add')
34 | end)
35 |
36 | QBCore.Functions.CreateCallback('qb-drivingschool:server:HasLicense', function(source, cb)
37 | local src = source
38 | local Player = QBCore.Functions.GetPlayer(src)
39 | local license = Player.PlayerData.metadata["licences"].driver
40 | if license then
41 | cb(true)
42 | else
43 | cb(false)
44 | end
45 | end)
46 |
47 | QBCore.Functions.CreateCallback('qb-drivingschool:server:HasPermit', function(source, cb)
48 | local src = source
49 | local Player = QBCore.Functions.GetPlayer(src)
50 | local permit = Player.Functions.GetItemByName("driving_test_permit")
51 | if permit ~= nil then
52 | cb(true)
53 | else
54 | cb(false)
55 | end
56 | end)
57 |
58 | RegisterNetEvent('qb-drivingschool:server:givepermit', function()
59 | Player = QBCore.Functions.GetPlayer(source)
60 | Player.Functions.AddItem('driving_test_permit', 1, nil, info)
61 | end)
--------------------------------------------------------------------------------
/html/questions.js:
--------------------------------------------------------------------------------
1 | var tableauQuestion = [
2 | {
3 | question: "If you're going 80 km/h, and you're approaching a residential area you must:",
4 | propositionA: "You accelerate",
5 | propositionB: "You keep your speed, if you do not pass other vehicles",
6 | propositionC: "You slow down",
7 | propositionD: "You keep your speed",
8 | reponse: "C"
9 | },
10 |
11 | {
12 | question: "If you're turning right at a traffic light, but see a pedestrian crossing what do you do:",
13 | propositionA: "You pass the pedestrian",
14 | propositionB: "You check that there is no other vehicles around",
15 | propositionC: "You wait until the pedestrian has crossed",
16 | propositionD: "You shoot the pedestrian and continue to drive",
17 | reponse: "C"
18 | },
19 |
20 | {
21 | question: "Without any prior indication, the speed in a residential area is: __ km/h",
22 | propositionA: "30 km/h",
23 | propositionB: "50 km/h",
24 | propositionC: "40 km/h",
25 | propositionD: "60 km/h",
26 | reponse: "B"
27 | },
28 |
29 | {
30 | question: "Before every lane change you must:",
31 | propositionA: "Check your mirrors",
32 | propositionB: "Check your blind spots",
33 | propositionC: "Signal your intentions",
34 | propositionD: "All of the above",
35 | reponse: "D"
36 | },
37 |
38 | {
39 | question: "What blood alcohol level is classified as driving while intoxicated?",
40 | propositionA: "0.05%",
41 | propositionB: "0.18%",
42 | propositionC: "0.08%",
43 | propositionD: "0.06%",
44 | reponse: "C"
45 | },
46 |
47 | {
48 | question: "When can you continue to drive at a traffic light?",
49 | propositionA: "When it is green",
50 | propositionB: "When there is nobody in the intersection",
51 | propositionC: "You are in a school zone",
52 | propositionD: "When it is green and / or red and you're turning right",
53 | reponse: "D"
54 | },
55 |
56 | {
57 | question: "A pedestrian has a do not cross signal, what do you do?",
58 | propositionA: "You let them pass",
59 | propositionB: "You observe before continuing",
60 | propositionC: "You wave to tell them to cross",
61 | propositionD: "You continue because your traffic light is green",
62 | reponse: "D"
63 | },
64 |
65 | {
66 | question: "What is allowed when passing another vehicle",
67 | propositionA: "You follow it closely to pass it faster",
68 | propositionB: "You pass it without leaving the roadway",
69 | propositionC: "You drive on the opposite side of the road to pass",
70 | propositionD: "You exceed the speed limit to pass them",
71 | reponse: "C"
72 | },
73 |
74 | {
75 | question: "You are driving on a highway which indicates a maximum speed of 120 km/h. But most trafficers drive at 125 km/h, so you should not drive faster than:",
76 | propositionA: "120 km/h",
77 | propositionB: "125 km/h",
78 | propositionC: "130 km/h",
79 | propositionD: "110 km/h",
80 | reponse: "A"
81 | },
82 |
83 | {
84 | question: "When you are overtaken by another vehicle it is important NOT to:",
85 | propositionA: "Slow Down",
86 | propositionB: "Check your mirrors",
87 | propositionC: "Watch other drivers",
88 | propositionD: "Increase your speed",
89 | reponse: "D"
90 | },
91 | ]
92 |
--------------------------------------------------------------------------------
/html/styles.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: hidden;
3 | font-family: 'Open Sans', sans-serif;
4 | }
5 |
6 | .full-screen {
7 | width: 100%;
8 | height: 100%;
9 | display: flex;
10 | align-items: center;
11 | }
12 |
13 | .question-container {
14 | width: 70%;
15 | height: 700px;
16 | background-color: rgba(0, 0, 0, 0.9);
17 | color: #fff;
18 | border: 1px solid #222;
19 | border-radius: 1px;
20 | margin-left: auto;
21 | margin-right: auto;
22 | overflow: hidden;
23 | z-index: 9999999;
24 | display: none;
25 | }
26 |
27 | .header {
28 | width: 100%;
29 | height: 20%;
30 | background-color: rgba(0, 0, 0, 0.8);
31 | color: #fff;
32 | display: flex;
33 | flex-direction: row;
34 | flex-wrap: wrap;
35 | justify-content: center;
36 | }
37 |
38 | .header .logo {
39 | width: 80%;
40 | max-height: 50%;
41 | margin: auto;
42 | margin-bottom: 0;
43 | }
44 |
45 | .header h1 {
46 | font-size: 2em;
47 | text-align: center;
48 | margin: auto;
49 | }
50 |
51 | .body {
52 | width: 100%;
53 | height: 80%;
54 | display: flex;
55 | flex-direction: row;
56 | flex-wrap: wrap;
57 | display: none;
58 | color: #fff;
59 | }
60 |
61 | .content {
62 | display: flex;
63 | flex-direction: column;
64 | align-self: center;
65 | width: 90%;
66 | height: 70%;
67 | margin: auto;
68 | border: none;
69 | }
70 |
71 | .content .logo {
72 | width: auto;
73 | max-height: auto;
74 | margin: auto;
75 | margin-bottom: 0;
76 | }
77 |
78 | .content h2, p {
79 | margin: 5px;
80 | }
81 |
82 | .buttonspot {
83 | display: flex;
84 | flex-direction: column;
85 | align-self: center;
86 | width: auto;
87 | height: 15%;
88 | margin: auto;
89 | border: none;
90 | }
91 |
92 | .buttonspot h2, p {
93 | margin: 5px;
94 | }
95 |
96 | .button {
97 | width: 30%;
98 | height: 40px;
99 | align-self: center;
100 | text-align: center;
101 | margin-left: 40px;
102 | margin-right: 40px;
103 | background: #ffffff;
104 | border-radius: 4px;
105 | color: #000000;
106 | font-size: 1.5em;
107 | text-decoration: none;
108 | }
109 |
110 | .form {
111 | display: flex;
112 | flex-direction: row;
113 | justify-content: center;
114 | flex-wrap: wrap;
115 | width: 100%;
116 | margin-top: 5px;
117 | }
118 |
119 | .form div {
120 | margin: auto;
121 | margin-top: 5px;
122 | margin-bottom: 5px;
123 | width: 100%;
124 | }
125 |
126 | .submit {
127 | align-items: center;
128 | height: 30px;
129 | text-align: center;
130 | width: 30%;
131 | background: #ffffff;
132 | border-radius: 4px;
133 | color: rgb(0, 0, 0);
134 | font-size: 1.2em;
135 | margin-top: 10px;
136 | }
137 |
138 | .barre-progression {
139 | width: 50%;
140 | align-self: center;
141 | height: 10%;
142 | margin: auto;
143 | margin-top: 0;
144 | display: flex;
145 | flex-wrap: wrap;
146 | justify-content: center;
147 | padding: 0;
148 | }
149 |
150 | .barre-progression h2 {
151 | width: 100%;
152 | height: 60%;
153 | text-align: center;
154 | display: flex;
155 | justify-content: center;
156 | align-items: center;
157 | margin: 0;
158 | color: #ffffff;
159 | }
160 |
161 | .progression {
162 | width: 80%;
163 | border: 0.2px solid white;
164 | background: rgba(0, 0, 0, 0.712);
165 | border-radius: 14px;
166 | box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.5), 0px 1px 0px rgba(255, 255, 255, 0.2);
167 | }
168 |
169 | .progression::-moz-progress-bar {
170 | background: rgb(0, 0, 0);
171 | border-radius: 14px;
172 | box-shadow: inset 0 -2px 4px rgba(0, 0, 0, 0.4), 0 2px 5px 0px rgba(0, 0, 0, 0.3);
173 | }
174 |
175 | .progression::-webkit-progress-bar {
176 | background: transparent;
177 | }
178 |
179 | .progression::-webkit-progress-value {
180 | background: #ffffff;
181 | border-radius: 14px;
182 | box-shadow: inset 0 -2px 4px rgba(0, 0, 0, 0.4), 0 2px 5px 0px rgba(0, 0, 0, 0.3);
183 | }
184 |
185 | .bold-text {
186 | font-weight: bold;
187 | font-size: 1.3em;
188 | }
--------------------------------------------------------------------------------
/html/scripts.js:
--------------------------------------------------------------------------------
1 | // question variables
2 | var questionNumber = 1;
3 | var userAnswer = [];
4 | var goodAnswer = [];
5 | var questionUsed = [];
6 | var nbQuestionToAnswer = 10; // don't forget to change the progress bar max value in html
7 | var nbAnswerNeeded = 5; // out of nbQuestionToAnswer
8 | var nbPossibleQuestions = 10; // number of questions in database questions.js
9 | var lastClick = 0;
10 |
11 | function getRandomQuestion() {
12 | var random = Math.floor(Math.random() * nbPossibleQuestions);
13 |
14 | while (true) {
15 | if (questionUsed.indexOf(random) === -1) {
16 | break;
17 | }
18 |
19 | random = Math.floor(Math.random() * nbPossibleQuestions);
20 | }
21 |
22 | questionUsed.push(random);
23 |
24 | return random;
25 | }
26 |
27 | // Partial Functions
28 | function closeMain() {
29 | $(".home").css("display", "none");
30 | }
31 | function openMain() {
32 | $(".home").css("display", "block");
33 | }
34 | function closeAll() {
35 | $(".body").css("display", "none");
36 | }
37 | function openQuestionnaire() {
38 | $(".questionnaire-container").css("display", "block");
39 | var randomQuestion = getRandomQuestion();
40 |
41 | $("#questionNumero").html("Question: " + questionNumber);
42 | $("#question").html(tableauQuestion[randomQuestion].question);
43 | $(".answerA").html(tableauQuestion[randomQuestion].propositionA);
44 | $(".answerB").html(tableauQuestion[randomQuestion].propositionB);
45 | $(".answerC").html(tableauQuestion[randomQuestion].propositionC);
46 | $(".answerD").html(tableauQuestion[randomQuestion].propositionD);
47 | $('input[name=question]').attr('checked', false);
48 |
49 | goodAnswer.push(tableauQuestion[randomQuestion].reponse);
50 | $(".questionnaire-container .progression").val(questionNumber - 1);
51 | }
52 | function openResultGood() {
53 | $(".resultGood").css("display", "block");
54 | }
55 | function openResultBad() {
56 | $(".resultBad").css("display", "block");
57 | }
58 | function openContainer() {
59 | $(".question-container").css("display", "block");
60 | }
61 | function closeContainer() {
62 | $(".question-container").css("display", "none");
63 | }
64 |
65 | // Listen for NUI Events
66 | window.addEventListener('message', function (event) {
67 | var item = event.data;
68 |
69 | // Open & Close main window
70 | if (item.openQuestion == true) {
71 | openContainer();
72 | openMain();
73 | }
74 |
75 | if (item.openQuestion == false) {
76 | closeContainer();
77 | closeMain();
78 | }
79 |
80 | // Open sub-windows / partials
81 | if (item.openSection == "question") {
82 | closeAll();
83 | openQuestionnaire();
84 | }
85 | });
86 |
87 | // Handle Button Presses
88 | $(".btnQuestion").click(function () {
89 | $.post('http://qb-drivingschool/question', JSON.stringify({}));
90 | });
91 |
92 | $(".btnClose").click(function () {
93 | $.post('http://qb-drivingschool/close', JSON.stringify({}));
94 | userAnswer = [];
95 | goodAnswer = [];
96 | questionUsed = [];
97 | questionNumber = 1;
98 | });
99 |
100 | $(".btnKick").click(function () {
101 | $.post('http://qb-drivingschool/kick', JSON.stringify({}));
102 | userAnswer = [];
103 | goodAnswer = [];
104 | questionUsed = [];
105 | questionNumber = 1;
106 | });
107 |
108 | // Handle Form Submits
109 | $("#question-form").submit(function (e) {
110 | e.preventDefault();
111 |
112 | if (questionNumber != nbQuestionToAnswer) {
113 | //question 1 to 9: pushing answer in array
114 | closeAll();
115 | userAnswer.push($('input[name="question"]:checked').val());
116 | questionNumber++;
117 | openQuestionnaire();
118 | } else {
119 | // question 10: comparing arrays and sending number of good answers
120 | userAnswer.push($('input[name="question"]:checked').val());
121 | var nbGoodAnswer = 0;
122 | for (i = 0; i < nbQuestionToAnswer; i++) {
123 | if (userAnswer[i] == goodAnswer[i]) {
124 | nbGoodAnswer++;
125 | }
126 | }
127 |
128 | closeAll();
129 | if (nbGoodAnswer >= nbAnswerNeeded) {
130 | openResultGood();
131 | } else {
132 | openResultBad();
133 | }
134 | }
135 |
136 | return false;
137 | });
138 |
--------------------------------------------------------------------------------
/html/ui.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Welcome to Driving School
20 |
21 |
22 |
All citizens of MUST pass their exam before they can drive.
23 | Take your time, answer with common sense, and do not answer randomly.
24 |
25 | Theory Test
26 | - Don't be afraid, the driving school accepts credit, but be careful not to get into debt.
27 | - If you fail your test the first time, you can retake it.
28 |
29 | Driving Test
30 | - Make sure you stay alert whilst driving, and avoid accidents and the speed limits!
31 |
32 | Total Cost of Theory Test is $500 its the same for price for the Driving Test, this payment will not be refunded if you fail.
33 |
34 |
35 |
38 |
42 |
43 |
44 |
73 |
74 |
75 |
76 |
77 | Good work!
78 |
79 | You did well during the examination.
80 |
81 | You can close this window, and go take your road test.
82 |
83 |
84 |
87 |
91 |
92 |
93 |
94 |
95 | You failed
96 |
97 | You weren't ready for this test, try again later...
98 |
99 |
100 |
101 |
102 |
105 |
106 |
Progress
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/config.lua:
--------------------------------------------------------------------------------
1 | Config = {}
2 | Config.DrawDistance = 100.0
3 | Config.MaxErrors = 5
4 | Config.SpeedMultiplier = 3.6 -- change this to 2.236936 for MPH
5 | Config.Price = 500
6 |
7 | Config.SpeedLimits = {
8 | residence = 50,
9 | town = 80,
10 | freeway = 120
11 | }
12 |
13 |
14 | Config.Vehicles = {
15 | ["blista"] = "blista",
16 | }
17 |
18 | Config.Zones = {
19 | DMVSchool = {
20 | Pos = {x = 239.62, y = -1381.08, z = 33.74},
21 | Size = {x = 0.5, y = 0.3, z = 0.3},
22 | Color = {r = 255, g = 255, b = 255},
23 | Type = 20
24 | },
25 |
26 | }
27 |
28 | Config.CheckPoints = {
29 |
30 | {
31 | Pos = {x = 255.139, y = -1400.731, z = 29.537},
32 | Action = function(playerPed, vehicle, setCurrentZoneType)
33 | QBCore.Functions.Notify("Next Point Speed - " .. Config.SpeedLimits['residence'] .. " ", "success", 5000)
34 |
35 | end
36 | },
37 |
38 | {
39 | Pos = {x = 271.874, y = -1370.574, z = 30.932},
40 | Action = function(playerPed, vehicle, setCurrentZoneType)
41 | QBCore.Functions.Notify('Go to Next Point', "success", 5000)
42 | end
43 | },
44 |
45 | {
46 | Pos = {x = 217.821, y = -1410.520, z = 28.292},
47 | Action = function(playerPed, vehicle, setCurrentZoneType)
48 | setCurrentZoneType('town')
49 |
50 | Citizen.CreateThread(function()
51 | QBCore.Functions.Notify("Stop Look Left - " .. Config.SpeedLimits['town'] .. " ", "error", 5000)
52 | PlaySound(-1, 'RACE_PLACED', 'HUD_AWARDS', false, 0, true)
53 | FreezeEntityPosition(vehicle, true)
54 | Citizen.Wait(6000)
55 |
56 | FreezeEntityPosition(vehicle, false)
57 | QBCore.Functions.Notify('Good Turn Right', "success", 5000)
58 | end)
59 | end
60 | },
61 |
62 | {
63 | Pos = {x = 178.550, y = -1401.755, z = 27.725},
64 | Action = function(playerPed, vehicle, setCurrentZoneType)
65 | QBCore.Functions.Notify('Watch Traffic Light', "error", 5000)
66 | end
67 | },
68 |
69 | {
70 | Pos = {x = 113.160, y = -1365.276, z = 27.725},
71 | Action = function(playerPed, vehicle, setCurrentZoneType)
72 | QBCore.Functions.Notify('Go To Next Point', "success", 5000)
73 | end
74 | },
75 |
76 | {
77 | Pos = {x = -73.542, y = -1364.335, z = 27.789},
78 | Action = function(playerPed, vehicle, setCurrentZoneType)
79 | QBCore.Functions.Notify('Stop For Passing', "error", 5000)
80 | PlaySound(-1, 'RACE_PLACED', 'HUD_AWARDS', false, 0, true)
81 | end
82 | },
83 |
84 | {
85 | Pos = {x = -355.143, y = -1420.282, z = 27.868},
86 | Action = function(playerPed, vehicle, setCurrentZoneType)
87 | QBCore.Functions.Notify('Go To Next Point', "success", 5000)
88 | end
89 | },
90 |
91 | {
92 | Pos = {x = -439.148, y = -1417.100, z = 27.704},
93 | Action = function(playerPed, vehicle, setCurrentZoneType)
94 | QBCore.Functions.Notify('Go To Next Point', "success", 5000)
95 | end
96 | },
97 |
98 | {
99 | Pos = {x = -453.790, y = -1444.726, z = 27.665},
100 | Action = function(playerPed, vehicle, setCurrentZoneType)
101 | setCurrentZoneType('freeway')
102 |
103 |
104 | QBCore.Functions.Notify("Free way time - " .. Config.SpeedLimits['freeway'] .. " ", "error", 5000)
105 | PlaySound(-1, 'RACE_PLACED', 'HUD_AWARDS', false, 0, true)
106 | end
107 | },
108 |
109 | {
110 | Pos = {x = -463.237, y = -1592.178, z = 37.519},
111 | Action = function(playerPed, vehicle, setCurrentZoneType)
112 | QBCore.Functions.Notify('Go To Next Point', "success", 5000)
113 | end
114 | },
115 |
116 | {
117 | Pos = {x = -900.647, y = -1986.28, z = 26.109},
118 | Action = function(playerPed, vehicle, setCurrentZoneType)
119 | QBCore.Functions.Notify('Go To Next Point', "success", 5000)
120 | end
121 | },
122 |
123 | {
124 | Pos = {x = 1225.759, y = -1948.792, z = 38.718},
125 | Action = function(playerPed, vehicle, setCurrentZoneType)
126 | QBCore.Functions.Notify('Go To Next Point', "success", 5000)
127 | end
128 | },
129 |
130 | {
131 | Pos = {x = 1225.759, y = -1948.792, z = 38.718},
132 | Action = function(playerPed, vehicle, setCurrentZoneType)
133 | setCurrentZoneType('town')
134 | QBCore.Functions.Notify("In Town Speed - " .. Config.SpeedLimits['town'] .. " ", "error", 5000)
135 | end
136 | },
137 |
138 | {
139 | Pos = {x = 1163.603, y = -1841.771, z = 35.679},
140 | Action = function(playerPed, vehicle, setCurrentZoneType)
141 | QBCore.Functions.Notify('Stay Alert', "error", 5000)
142 | PlaySound(-1, 'RACE_PLACED', 'HUD_AWARDS', false, 0, true)
143 | end
144 | },
145 |
146 | {
147 | Pos = {x = 235.283, y = -1398.329, z = 28.921},
148 | Action = function(playerPed, vehicle, setCurrentZoneType)
149 | DeleteVehicle(vehicle)
150 | end
151 | }
152 |
153 | }
154 |
--------------------------------------------------------------------------------
/client/gui.lua:
--------------------------------------------------------------------------------
1 | Keys = {
2 | ["ESC"] = 322, ["F1"] = 288, ["F2"] = 289, ["F3"] = 170, ["F5"] = 166, ["F6"] = 167, ["F7"] = 168, ["F8"] = 169, ["F9"] = 56, ["F10"] = 57,
3 | ["~"] = 243, ["1"] = 157, ["2"] = 158, ["3"] = 160, ["4"] = 164, ["5"] = 165, ["6"] = 159, ["7"] = 161, ["8"] = 162, ["9"] = 163, ["-"] = 84, ["="] = 83, ["BACKSPACE"] = 177,
4 | ["TAB"] = 37, ["Q"] = 44, ["W"] = 32, ["E"] = 38, ["R"] = 45, ["T"] = 245, ["Y"] = 246, ["U"] = 303, ["P"] = 199, ["["] = 39, ["]"] = 40, ["ENTER"] = 18,
5 | ["CAPS"] = 137, ["A"] = 34, ["S"] = 8, ["D"] = 9, ["F"] = 23, ["G"] = 47, ["H"] = 74, ["K"] = 311, ["L"] = 182,
6 | ["LEFTSHIFT"] = 21, ["Z"] = 20, ["X"] = 73, ["C"] = 26, ["V"] = 0, ["B"] = 29, ["N"] = 249, ["M"] = 244, [","] = 82, ["."] = 81,
7 | ["LEFTCTRL"] = 36, ["LEFTALT"] = 19, ["SPACE"] = 22, ["RIGHTCTRL"] = 70,
8 | ["HOME"] = 213, ["PAGEUP"] = 10, ["PAGEDOWN"] = 11, ["DELETE"] = 178,
9 | ["LEFT"] = 174, ["RIGHT"] = 175, ["TOP"] = 27, ["DOWN"] = 173,
10 | ["NENTER"] = 201, ["N4"] = 108, ["N5"] = 60, ["N6"] = 107, ["N+"] = 96, ["N-"] = 97, ["N7"] = 117, ["N8"] = 61, ["N9"] = 118
11 | }
12 |
13 | Menu = {}
14 | Menu.GUI = {}
15 | Menu.buttonCount = 0
16 | Menu.selection = 0
17 | Menu.hidden = true
18 | MenuTitle = "Menu"
19 |
20 | function Menu.addButton(name, func,args,extra,damages,bodydamages,fuel)
21 |
22 | local yoffset = 0.25
23 | local xoffset = 0.3
24 | local xmin = 0.0
25 | local xmax = 0.15
26 | local ymin = 0.03
27 | local ymax = 0.03
28 | Menu.GUI[Menu.buttonCount+1] = {}
29 | if extra ~= nil then
30 | Menu.GUI[Menu.buttonCount+1]["extra"] = extra
31 | end
32 | if damages ~= nil then
33 | Menu.GUI[Menu.buttonCount+1]["damages"] = damages
34 | Menu.GUI[Menu.buttonCount+1]["bodydamages"] = bodydamages
35 | Menu.GUI[Menu.buttonCount+1]["fuel"] = fuel
36 | end
37 |
38 |
39 | Menu.GUI[Menu.buttonCount+1]["name"] = name
40 | Menu.GUI[Menu.buttonCount+1]["func"] = func
41 | Menu.GUI[Menu.buttonCount+1]["args"] = args
42 | Menu.GUI[Menu.buttonCount+1]["active"] = false
43 | Menu.GUI[Menu.buttonCount+1]["xmin"] = xmin
44 | Menu.GUI[Menu.buttonCount+1]["ymin"] = ymin * (Menu.buttonCount + 0.01) +yoffset
45 | Menu.GUI[Menu.buttonCount+1]["xmax"] = xmax
46 | Menu.GUI[Menu.buttonCount+1]["ymax"] = ymax
47 | Menu.buttonCount = Menu.buttonCount+1
48 | end
49 |
50 |
51 | function Menu.updateSelection()
52 | if IsControlJustPressed(1, Keys["DOWN"]) then
53 | if(Menu.selection < Menu.buttonCount -1 ) then
54 | Menu.selection = Menu.selection +1
55 | else
56 | Menu.selection = 0
57 | end
58 | PlaySound(-1, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET", 0, 0, 1)
59 | elseif IsControlJustPressed(1, Keys["TOP"]) then
60 | if(Menu.selection > 0)then
61 | Menu.selection = Menu.selection -1
62 | else
63 | Menu.selection = Menu.buttonCount-1
64 | end
65 | PlaySound(-1, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET", 0, 0, 1)
66 | elseif IsControlJustPressed(1, 215) then
67 | MenuCallFunction(Menu.GUI[Menu.selection +1]["func"], Menu.GUI[Menu.selection +1]["args"])
68 | PlaySound(-1, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET", 0, 0, 1)
69 | end
70 | local iterator = 0
71 | for id, settings in ipairs(Menu.GUI) do
72 | Menu.GUI[id]["active"] = false
73 | if(iterator == Menu.selection ) then
74 | Menu.GUI[iterator +1]["active"] = true
75 | end
76 | iterator = iterator +1
77 | end
78 | end
79 |
80 | function Menu.renderGUI()
81 | if not Menu.hidden then
82 | Menu.renderButtons()
83 | Menu.updateSelection()
84 | end
85 | end
86 |
87 | function Menu.renderBox(xMin,xMax,yMin,yMax,color1,color2,color3,color4)
88 | DrawRect(0.7, yMin,0.15, yMax-0.002, color1, color2, color3, color4);
89 | end
90 |
91 | function Menu.renderButtons()
92 |
93 | local yoffset = 0.5
94 | local xoffset = 0
95 |
96 |
97 |
98 | for id, settings in pairs(Menu.GUI) do
99 | local screen_w = 0
100 | local screen_h = 0
101 | screen_w, screen_h = GetScreenResolution(0, 0)
102 |
103 | boxColor = {0, 0, 0, 200}
104 | local movetext = 0.0
105 | if(settings["extra"] == "Garage") then
106 | boxColor = {0, 0, 0, 200}
107 | elseif (settings["extra"] == "In Seizure") then
108 | boxColor = {0, 0, 0, 200}
109 | end
110 |
111 | if(settings["active"]) then
112 | boxColor = {216, 8, 36, 155}
113 | end
114 |
115 |
116 | if settings["extra"] ~= nil then
117 |
118 | SetTextFont(4)
119 |
120 | SetTextScale(0.34, 0.34)
121 | SetTextColour(255, 255, 255, 255)
122 | SetTextEntry("STRING")
123 | AddTextComponentString(settings["name"])
124 | DrawText(0.63, (settings["ymin"] - 0.012 ))
125 |
126 | SetTextFont(4)
127 | SetTextScale(0.26, 0.26)
128 | SetTextColour(255, 255, 255, 255)
129 | SetTextEntry("STRING")
130 | AddTextComponentString(settings["extra"])
131 | DrawText(0.730 + movetext, (settings["ymin"] - 0.011 ))
132 |
133 |
134 | SetTextFont(4)
135 | SetTextScale(0.28, 0.28)
136 | SetTextColour(11, 11, 11, 255)
137 | SetTextEntry("STRING")
138 | AddTextComponentString(settings["damages"])
139 | DrawText(0.778, (settings["ymin"] - 0.012 ))
140 |
141 | SetTextFont(4)
142 | SetTextScale(0.28, 0.28)
143 | SetTextColour(11, 11, 11, 255)
144 | SetTextEntry("STRING")
145 | AddTextComponentString(settings["bodydamages"])
146 | DrawText(0.815, (settings["ymin"] - 0.012 ))
147 |
148 | SetTextFont(4)
149 | SetTextScale(0.28, 0.28)
150 | SetTextColour(11, 11, 11, 255)
151 | SetTextEntry("STRING")
152 | AddTextComponentString(settings["fuel"])
153 | DrawText(0.854, (settings["ymin"] - 0.012 ))
154 |
155 |
156 |
157 | DrawRect(0.832, settings["ymin"], 0.11, settings["ymax"]-0.002, 255,255,255,199)
158 | --Global.DrawRect(x, y, width, height, r, g, b, a)
159 | else
160 | SetTextFont(4)
161 | SetTextScale(0.31, 0.31)
162 | SetTextColour(255, 255, 255, 255)
163 | SetTextCentre(true)
164 | SetTextEntry("STRING")
165 | AddTextComponentString(settings["name"])
166 | DrawText(0.7, (settings["ymin"] - 0.012 ))
167 |
168 | end
169 |
170 | Menu.renderBox(settings["xmin"] ,settings["xmax"], settings["ymin"], settings["ymax"],boxColor[1],boxColor[2],boxColor[3],boxColor[4])
171 | end
172 | end
173 |
174 | --------------------------------------------------------------------------------------------------------------------
175 |
176 | function ClearMenu()
177 | --Menu = {}
178 | Menu.GUI = {}
179 | Menu.buttonCount = 0
180 | Menu.selection = 0
181 | end
182 |
183 | function MenuCallFunction(fnc, arg)
184 | _G[fnc](arg)
185 | end
--------------------------------------------------------------------------------
/client/main.lua:
--------------------------------------------------------------------------------
1 | QBCore = exports['qb-core']:GetCoreObject()
2 | local CurrentAction = nil
3 | local CurrentActionMsg = nil
4 | local CurrentActionData = nil
5 | local Licenses = {}
6 | local CurrentTest = nil
7 | local CurrentTestType = nil
8 | local CurrentVehicle = nil
9 | local CurrentCheckPoint, DriveErrors = 0, 0
10 | local LastCheckPoint = -1
11 | local CurrentBlip = nil
12 | local CurrentZoneType = nil
13 | local IsAboveSpeedLimit = false
14 | local LastVehicleHealth = nil
15 |
16 |
17 | RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function()
18 | --print('Loaded Driving School')
19 | end)
20 | RegisterNetEvent('qb-drivingschool:start', function()
21 | StartTheoryTest()
22 | end)
23 |
24 | function DrawMissionText(msg, time)
25 | ClearPrints()
26 | BeginTextCommandPrint('STRING')
27 | AddTextComponentSubstringPlayerName(msg)
28 | EndTextCommandPrint(time, true)
29 | end
30 |
31 | function StartTheoryTest()
32 | CurrentTest = 'theory'
33 | SendNUIMessage({
34 | openQuestion = true
35 | })
36 | SetTimeout(200, function()
37 | SetNuiFocus(true, true)
38 | end)
39 | end
40 |
41 | function StopTheoryTest(success)
42 | CurrentTest = nil
43 | SendNUIMessage({
44 | openQuestion = false
45 | })
46 | SetNuiFocus(false)
47 | if success then
48 | QBCore.Functions.Notify("You passed theory test , Start your practical test!", "success", 5000)
49 | TriggerServerEvent('qb-drivingschool:server:givepermit')
50 | else
51 | QBCore.Functions.Notify("Failed Theory Test", "error")
52 | end
53 | end
54 |
55 | function StartDriveTest()
56 | local coords = {
57 | x = 231.36,
58 | y = -1394.49,
59 | z = 30.5,
60 | h = 239.94,
61 | }
62 | local plate = "TESTDRIVE" .. math.random(1111, 9999)
63 | QBCore.Functions.SpawnVehicle('blista', function(vehicle)
64 | SetVehicleNumberPlateText(vehicle, "TESTDRIVE" .. tostring(math.random(1000, 9999)))
65 | SetEntityHeading(vehicle, coords.h)
66 | exports['LegacyFuel']:SetFuel(vehicle, 100.0)
67 | Menu.hidden = true
68 | TaskWarpPedIntoVehicle(GetPlayerPed(-1), vehicle, -1)
69 | TriggerEvent("vehiclekeys:client:SetOwner", GetVehicleNumberPlateText(vehicle))
70 | SetVehicleCustomPrimaryColour(vehicle, 0, 0, 0)
71 | SetVehicleEngineOn(vehicle, true, true)
72 | SetVehicleDirtLevel(vehicle)
73 | SetVehicleUndriveable(vehicle, false)
74 | WashDecalsFromVehicle(vehicle, 1.0)
75 | CurrentTest = 'drive'
76 | CurrentTestType = 'drive_test'
77 | CurrentCheckPoint = 0
78 | LastCheckPoint = -1
79 | CurrentZoneType = 'residence'
80 | DriveErrors = 0
81 | IsAboveSpeedLimit = false
82 | CurrentVehicle = vehicle
83 | LastVehicleHealth = GetEntityHealth(vehicle)
84 | end, coords, true)
85 | end
86 |
87 | function StopDriveTest(success)
88 | if success then
89 | QBCore.Functions.Notify("Passed Driving Test", "success")
90 | TriggerServerEvent('qb-drivingschool:server:GetLicense')
91 | else
92 | -- despawn current car and teleport them
93 | ped = PlayerPedId()
94 | RemoveBlip(CurrentBlip)
95 | DeleteVehicle(GetVehiclePedIsUsing(ped))
96 | SetEntityCoords(ped, 235.283, -1398.329, 28.921)
97 | QBCore.Functions.Notify("Failed Driving Test", "error")
98 | end
99 | CurrentTest = nil
100 | CurrentTestType = nil
101 | end
102 |
103 | function SetCurrentZoneType(type)
104 | CurrentZoneType = type
105 | end
106 |
107 | RegisterNUICallback('question', function(data, cb)
108 | SendNUIMessage({
109 | openSection = 'question'
110 | })
111 | cb()
112 | end)
113 |
114 | RegisterNUICallback('close', function(data, cb)
115 | StopTheoryTest(true)
116 | cb()
117 | end)
118 |
119 | RegisterNUICallback('kick', function(data, cb)
120 | StopTheoryTest(false)
121 | cb()
122 | end)
123 |
124 | AddEventHandler('qb-drivingschool:hasEnteredMarker', function(zone)
125 | if zone == 'DMVSchool' then
126 | CurrentAction = 'dmvschool_menu'
127 | CurrentActionMsg = ('Press E to give driving test - $500')
128 | CurrentActionData = {}
129 | end
130 | end)
131 |
132 | AddEventHandler('qb-drivingschool:hasExitedMarker', function(zone)
133 | CurrentAction = nil
134 | end)
135 |
136 |
137 | -- Create Blips
138 | CreateThread(function()
139 | local blip = AddBlipForCoord(Config.Zones.DMVSchool.Pos.x, Config.Zones.DMVSchool.Pos.y, Config.Zones.DMVSchool.Pos.z)
140 | SetBlipSprite(blip, 525)
141 | SetBlipDisplay(blip, 4)
142 | SetBlipScale(blip, 0.7)
143 | SetBlipColour(blip, 4)
144 | SetBlipAsShortRange(blip, true)
145 | BeginTextCommandSetBlipName("STRING")
146 | AddTextComponentString('Driving School')
147 | EndTextCommandSetBlipName(blip)
148 | end)
149 |
150 | -- Display markers
151 | CreateThread(function()
152 | while true do
153 | Wait(0)
154 | local coords = GetEntityCoords(PlayerPedId())
155 | for k, v in pairs(Config.Zones) do
156 | if (v.Type ~= -1 and GetDistanceBetweenCoords(coords, v.Pos.x, v.Pos.y, v.Pos.z, true) < Config.DrawDistance) then
157 | DrawMarker(v.Type, v.Pos.x, v.Pos.y, v.Pos.z, 0.0, 0.0, 0.0, 0, 0.0, 0.0, v.Size.x, v.Size.y, v.Size.z, v.Color.r, v.Color.g, v.Color.b, 100, false, true, 2, false, false, false, false)
158 | end
159 | end
160 | end
161 | end)
162 |
163 | -- Enter / Exit marker events
164 | CreateThread(function()
165 | while true do
166 | Wait(100)
167 | local coords = GetEntityCoords(PlayerPedId())
168 | local isInMarker = false
169 | local currentZone = nil
170 | for k, v in pairs(Config.Zones) do
171 | if (GetDistanceBetweenCoords(coords, v.Pos.x, v.Pos.y, v.Pos.z, true) < v.Size.x) then
172 | isInMarker = true
173 | currentZone = k
174 | end
175 | end
176 | if (isInMarker and not HasAlreadyEnteredMarker) or (isInMarker and LastZone ~= currentZone) then
177 | HasAlreadyEnteredMarker = true
178 | LastZone = currentZone
179 | TriggerEvent('qb-drivingschool:hasEnteredMarker', currentZone)
180 | end
181 | if not isInMarker and HasAlreadyEnteredMarker then
182 | HasAlreadyEnteredMarker = false
183 | TriggerEvent('qb-drivingschool:hasExitedMarker', LastZone)
184 | end
185 | end
186 | end)
187 |
188 | -- Block UI
189 | CreateThread(function()
190 | while true do
191 | Wait(1)
192 | if CurrentTest == 'theory' then
193 | local playerPed = PlayerPedId()
194 | DisableControlAction(0, 1, true)-- LookLeftRight
195 | DisableControlAction(0, 2, true)-- LookUpDown
196 | DisablePlayerFiring(playerPed, true)-- Disable weapon firing
197 | DisableControlAction(0, 142, true)-- MeleeAttackAlternate
198 | DisableControlAction(0, 106, true)-- VehicleMouseControlOverride
199 | else
200 | Wait(500)
201 | end
202 | end
203 | end)
204 |
205 | -- Key Controls
206 | CreateThread(function()
207 | while true do
208 | Wait(0)
209 | if CurrentAction then
210 | helpText(CurrentActionMsg)
211 | if IsControlJustReleased(0, 38) then
212 | TriggerEvent('opendriving')
213 | CurrentAction = nil
214 | end
215 | else
216 | Wait(500)
217 | end
218 | end
219 | end)
220 |
221 | -- Drive test
222 | CreateThread(function()
223 | while true do
224 | Wait(0)
225 | if CurrentTest == 'drive' then
226 | local playerPed = PlayerPedId()
227 | local coords = GetEntityCoords(playerPed)
228 | local nextCheckPoint = CurrentCheckPoint + 1
229 | if Config.CheckPoints[nextCheckPoint] == nil then
230 | if DoesBlipExist(CurrentBlip) then
231 | RemoveBlip(CurrentBlip)
232 | end
233 | CurrentTest = nil
234 | QBCore.Functions.Notify("Driving Test Complete", "error")
235 | if DriveErrors < Config.MaxErrors then
236 | StopDriveTest(true)
237 | end
238 | else
239 | if CurrentCheckPoint ~= LastCheckPoint then
240 | if DoesBlipExist(CurrentBlip) then
241 | RemoveBlip(CurrentBlip)
242 | end
243 | CurrentBlip = AddBlipForCoord(Config.CheckPoints[nextCheckPoint].Pos.x, Config.CheckPoints[nextCheckPoint].Pos.y, Config.CheckPoints[nextCheckPoint].Pos.z)
244 | SetBlipRoute(CurrentBlip, 1)
245 | LastCheckPoint = CurrentCheckPoint
246 | end
247 | local distance = GetDistanceBetweenCoords(coords, Config.CheckPoints[nextCheckPoint].Pos.x, Config.CheckPoints[nextCheckPoint].Pos.y, Config.CheckPoints[nextCheckPoint].Pos.z, true)
248 | if distance <= 100.0 then
249 | DrawMarker(1, Config.CheckPoints[nextCheckPoint].Pos.x, Config.CheckPoints[nextCheckPoint].Pos.y, Config.CheckPoints[nextCheckPoint].Pos.z, 0.0, 0.0, 0.0, 0, 0.0, 0.0, 1.5, 1.5, 1.5, 102, 204, 102, 100, false, true, 2, false, false, false, false)
250 | end
251 | if distance <= 3.0 then
252 | Config.CheckPoints[nextCheckPoint].Action(playerPed, CurrentVehicle, SetCurrentZoneType)
253 | CurrentCheckPoint = CurrentCheckPoint + 1
254 | end
255 | if DriveErrors == Config.MaxErrors then
256 | StopDriveTest(false)
257 | end
258 | end
259 | else
260 | -- not currently taking driver test
261 | Wait(500)
262 | end
263 | end
264 | end)
265 |
266 | -- Speed / Damage control
267 | CreateThread(function()
268 | while true do
269 | Wait(10)
270 | if CurrentTest == 'drive' then
271 | local playerPed = PlayerPedId()
272 | if IsPedInAnyVehicle(playerPed, false) then
273 | local vehicle = GetVehiclePedIsIn(playerPed, false)
274 | local speed = GetEntitySpeed(vehicle) * Config.SpeedMultiplier
275 | local tooMuchSpeed = false
276 | for k, v in pairs(Config.SpeedLimits) do
277 | if CurrentZoneType == k and speed > v then
278 | tooMuchSpeed = true
279 | if not IsAboveSpeedLimit then
280 | DriveErrors = DriveErrors + 1
281 | IsAboveSpeedLimit = true
282 | QBCore.Functions.Notify("Driving Too Fast! Allowed Speed: " .. v .. " ", "error")
283 | QBCore.Functions.Notify("Mistakes - " .. DriveErrors .. " / " .. Config.MaxErrors .. " ", "error")
284 | end
285 | end
286 | end
287 | if not tooMuchSpeed then
288 | IsAboveSpeedLimit = false
289 | end
290 | local health = GetEntityHealth(vehicle)
291 | if health < LastVehicleHealth then
292 | DriveErrors = DriveErrors + 1
293 | QBCore.Functions.Notify("You damaged Vehicle!", "error")
294 | QBCore.Functions.Notify("Mistakes - " .. DriveErrors .. " / " .. Config.MaxErrors .. " ", "error")
295 | -- avoid stacking faults
296 | LastVehicleHealth = health
297 | Wait(1500)
298 | end
299 | end
300 | else
301 | -- not currently taking driver test
302 | Wait(500)
303 | end
304 | end
305 | end)
306 |
307 | helpText = function(msg)
308 | BeginTextCommandDisplayHelp('STRING')
309 | AddTextComponentSubstringPlayerName(msg)
310 | EndTextCommandDisplayHelp(0, false, true, -1)
311 | end
312 |
313 | RegisterNetEvent('opendriving', function()
314 |
315 | -- do a trigger to server toi see if meta is set to true for driver license
316 | QBCore.Functions.TriggerCallback('qb-drivingschool:server:HasLicense', function(HasItem)
317 |
318 | if HasItem then
319 | QBCore.Functions.Notify("Looks like you have already passed your test! Go to City Hall to get a new License", "error", 10000)
320 | else
321 | TriggerEvent('opendriving:theory')
322 | end
323 | end)
324 | end)
325 |
326 |
327 | RegisterNetEvent('opendriving:theory', function()
328 | QBCore.Functions.TriggerCallback('qb-drivingschool:server:hasfunds', function(HasFunds)
329 | if HasFunds then
330 | TriggerEvent('qb-drivingschool:client:starttest')
331 | else
332 | QBCore.Functions.Notify("Looks like you do not have enough funds in your bank!")
333 | end
334 | end)
335 | end)
336 |
337 | RegisterNetEvent('qb-drivingschool:client:starttest', function()
338 | QBCore.Functions.TriggerCallback('qb-drivingschool:server:HasPermit', function(HasItem)
339 | if HasItem then
340 | StartDriveTest()
341 | else
342 | QBCore.Functions.Notify("You cannot close screen until and unless you give theory test", "error")
343 | StartTheoryTest()
344 | end
345 | end)
346 | end)
--------------------------------------------------------------------------------