3817 lines
104 KiB
Lua
3817 lines
104 KiB
Lua
print('[how_progression] server.lua loaded')
|
|
|
|
local vinnyFenceSchemaReady = false
|
|
|
|
local function isVinnyFenceUnlocked(value)
|
|
return value == true or tonumber(value) == 1
|
|
end
|
|
|
|
MySQL.ready(function()
|
|
local ok, err = pcall(function()
|
|
MySQL.query.await([[
|
|
ALTER TABLE how_progression
|
|
ADD COLUMN IF NOT EXISTS vinny_fence_unlocked TINYINT(1) NOT NULL DEFAULT 0
|
|
]])
|
|
|
|
MySQL.query.await([[
|
|
ALTER TABLE how_progression
|
|
ADD COLUMN IF NOT EXISTS vinny_fence_day INT NOT NULL DEFAULT 0
|
|
]])
|
|
|
|
MySQL.query.await([[
|
|
ALTER TABLE how_progression
|
|
ADD COLUMN IF NOT EXISTS vinny_fence_count TINYINT UNSIGNED NOT NULL DEFAULT 0
|
|
]])
|
|
end)
|
|
|
|
if not ok then
|
|
print(('[how_progression] Vinny fence schema migration failed: %s')
|
|
:format(tostring(err)))
|
|
return
|
|
end
|
|
|
|
vinnyFenceSchemaReady = true
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- Progression
|
|
-- ============================================================
|
|
|
|
local function getProgression(citizenid)
|
|
if not citizenid then return nil end
|
|
|
|
local row = MySQL.single.await(
|
|
'SELECT * FROM how_progression WHERE citizenid = ?',
|
|
{ citizenid }
|
|
)
|
|
|
|
if row then
|
|
return row
|
|
end
|
|
|
|
MySQL.insert.await(
|
|
'INSERT INTO how_progression (citizenid) VALUES (?)',
|
|
{ citizenid }
|
|
)
|
|
|
|
return MySQL.single.await(
|
|
'SELECT * FROM how_progression WHERE citizenid = ?',
|
|
{ citizenid }
|
|
)
|
|
end
|
|
|
|
local function getPlayerProgression(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return nil, nil end
|
|
|
|
return getProgression(player.PlayerData.citizenid), player
|
|
end
|
|
|
|
local function setCrowbarProgression(citizenid, stage, rep)
|
|
if not citizenid then return false end
|
|
|
|
local result = MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
crowbar_stage = GREATEST(crowbar_stage, ?),
|
|
crowbar_rep = GREATEST(crowbar_rep, ?)
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ stage, rep, citizenid }
|
|
)
|
|
|
|
return result ~= nil
|
|
end
|
|
|
|
local function advanceCrowbar(src, stageIncrease, repIncrease)
|
|
local progression, player = getPlayerProgression(src)
|
|
|
|
if not progression or not player then
|
|
return nil
|
|
end
|
|
|
|
local currentStage = progression.crowbar_stage or 0
|
|
local currentRep = progression.crowbar_rep or 0
|
|
|
|
local newStage = currentStage + (stageIncrease or 0)
|
|
local newRep = math.min(
|
|
currentRep + (repIncrease or 0),
|
|
100
|
|
)
|
|
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
crowbar_stage = ?,
|
|
crowbar_rep = ?
|
|
WHERE citizenid = ?
|
|
]],
|
|
{
|
|
newStage,
|
|
newRep,
|
|
player.PlayerData.citizenid
|
|
}
|
|
)
|
|
|
|
return newStage, newRep
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Crew / party helpers
|
|
-- ============================================================
|
|
|
|
-- Returns a snapshot of the players participating in a mission.
|
|
--
|
|
-- A solo player gets one participant.
|
|
-- A player in a bs_groupsystem party gets the current party
|
|
-- members at the moment the mission begins.
|
|
--
|
|
-- Progression belongs to each character individually.
|
|
-- Mission state belongs to the shared mission.
|
|
local function getMissionParticipants(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return nil, nil end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
local partyId = exports['bs_groupsystem']:getPlayerPartyId(citizenid)
|
|
|
|
local participants = {}
|
|
|
|
if partyId then
|
|
exports['bs_groupsystem']:sendToPartyMembers(
|
|
partyId,
|
|
function(_, memberCitizenId)
|
|
if memberCitizenId then
|
|
-- bs_groupsystem may retain an old FiveM source ID.
|
|
-- Use the citizenid to resolve the character's current
|
|
-- live source through Qbox instead.
|
|
local member =
|
|
exports.qbx_core:GetPlayerByCitizenId(
|
|
memberCitizenId
|
|
)
|
|
|
|
if member then
|
|
participants[#participants + 1] = {
|
|
source = member.PlayerData.source,
|
|
citizenid = memberCitizenId
|
|
}
|
|
end
|
|
end
|
|
end
|
|
)
|
|
else
|
|
participants[1] = {
|
|
source = src,
|
|
citizenid = citizenid
|
|
}
|
|
end
|
|
|
|
return participants, partyId
|
|
end
|
|
|
|
local function getCitizenIdFromSource(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return nil end
|
|
|
|
return player.PlayerData.citizenid
|
|
end
|
|
|
|
local function participantIsValid(participant)
|
|
if not participant then return false end
|
|
|
|
local player = exports.qbx_core:GetPlayer(participant.source)
|
|
if not player then return false end
|
|
|
|
return player.PlayerData.citizenid == participant.citizenid
|
|
end
|
|
|
|
local function rewardCrowbarParticipants(
|
|
participants,
|
|
requiredStage,
|
|
nextStage,
|
|
repReward
|
|
)
|
|
for _, participant in ipairs(participants) do
|
|
if participantIsValid(participant) then
|
|
local progression, player = getPlayerProgression(participant.source)
|
|
|
|
if progression and player then
|
|
local currentStage = progression.crowbar_stage or 0
|
|
local currentRep = progression.crowbar_rep or 0
|
|
|
|
-- Only advance players who were actually at the
|
|
-- expected mission stage when the mission began.
|
|
if currentStage == requiredStage then
|
|
local newRep = math.min(
|
|
currentRep + repReward,
|
|
100
|
|
)
|
|
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
crowbar_stage = ?,
|
|
crowbar_rep = ?
|
|
WHERE citizenid = ?
|
|
]],
|
|
{
|
|
nextStage,
|
|
newRep,
|
|
participant.citizenid
|
|
}
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarReward',
|
|
participant.source,
|
|
repReward,
|
|
newRep,
|
|
nextStage
|
|
)
|
|
|
|
print((
|
|
'[how_progression] Crowbar progression advanced for %s | stage %s | REP %s'
|
|
):format(
|
|
participant.citizenid,
|
|
nextStage,
|
|
newRep
|
|
))
|
|
else
|
|
print((
|
|
'[how_progression] %s did not advance: expected stage %s, current stage %s'
|
|
):format(
|
|
participant.citizenid,
|
|
requiredStage,
|
|
currentStage
|
|
))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Mack progression helpers
|
|
-- ============================================================
|
|
|
|
local function rewardMackParticipants(
|
|
participants,
|
|
requiredStage,
|
|
nextStage,
|
|
repReward,
|
|
cashReward
|
|
)
|
|
for _, participant in ipairs(participants) do
|
|
if participantIsValid(participant) then
|
|
local progression, player =
|
|
getPlayerProgression(participant.source)
|
|
|
|
if progression and player then
|
|
local currentStage = progression.mack_stage or 0
|
|
local currentRep = progression.mack_rep or 0
|
|
|
|
if currentStage == requiredStage then
|
|
local newRep = math.min(
|
|
currentRep + repReward,
|
|
100
|
|
)
|
|
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
mack_stage = ?,
|
|
mack_rep = ?
|
|
WHERE citizenid = ?
|
|
]],
|
|
{
|
|
nextStage,
|
|
newRep,
|
|
participant.citizenid
|
|
}
|
|
)
|
|
|
|
if cashReward and cashReward > 0 then
|
|
player.Functions.AddMoney(
|
|
'cash',
|
|
cashReward,
|
|
'mack-mission-reward'
|
|
)
|
|
end
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:mackReward',
|
|
participant.source,
|
|
repReward,
|
|
newRep,
|
|
nextStage,
|
|
cashReward or 0
|
|
)
|
|
|
|
print((
|
|
'[how_progression] Mack progression advanced for %s | stage %s | REP %s'
|
|
):format(
|
|
participant.citizenid,
|
|
nextStage,
|
|
newRep
|
|
))
|
|
else
|
|
print((
|
|
'[how_progression] %s did not advance Mack: expected stage %s, current stage %s'
|
|
):format(
|
|
participant.citizenid,
|
|
requiredStage,
|
|
currentStage
|
|
))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- UNKNOWN introduction
|
|
-- ============================================================
|
|
|
|
local function sendSmsToPlayer(src, senderNumber, message)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return false end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
local phone = player.PlayerData.charinfo.phone
|
|
|
|
if not phone then
|
|
print((
|
|
'[how_progression] SMS failed for %s: no phone number'
|
|
):format(citizenid))
|
|
return false
|
|
end
|
|
|
|
local ok, err = pcall(function()
|
|
exports.npwd:emitMessage({
|
|
senderNumber = senderNumber,
|
|
targetNumber = phone,
|
|
message = message
|
|
})
|
|
end)
|
|
|
|
if not ok then
|
|
print((
|
|
'[how_progression] SMS failed for %s: %s'
|
|
):format(citizenid, tostring(err)))
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local function introduceUnknown(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then
|
|
print((
|
|
'[how_progression] Could not find Qbox player for source %s'
|
|
):format(tostring(src)))
|
|
return false
|
|
end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
local progression = getProgression(citizenid)
|
|
|
|
if not progression then
|
|
print((
|
|
'[how_progression] Could not create/load progression for %s'
|
|
):format(citizenid))
|
|
return false
|
|
end
|
|
|
|
if progression.unknown_introduced == 1 then
|
|
return false
|
|
end
|
|
|
|
local phone = player.PlayerData.charinfo.phone
|
|
|
|
if not phone then
|
|
print((
|
|
'[how_progression] No phone number for %s'
|
|
):format(citizenid))
|
|
return false
|
|
end
|
|
|
|
local message =
|
|
"Heard you're new in town. Keep this number. Call me whatever you want. " ..
|
|
"Online, I go by UNKNOWN.\n\n" ..
|
|
"If you're looking to make some money, find Crowbar in Sandy. " ..
|
|
"He's near the Ammu-Nation. He'll have something for you.\n\n" ..
|
|
"Don't ask him about me. I'll be in touch."
|
|
|
|
local ok, err = pcall(function()
|
|
exports.npwd:emitMessage({
|
|
senderNumber = '555-0100',
|
|
targetNumber = phone,
|
|
message = message
|
|
})
|
|
end)
|
|
|
|
if not ok then
|
|
print((
|
|
'[how_progression] UNKNOWN SMS failed for %s: %s'
|
|
):format(citizenid, tostring(err)))
|
|
return false
|
|
end
|
|
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
unknown_introduced = 1,
|
|
crowbar_stage = GREATEST(crowbar_stage, 1)
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:unlockCrowbar',
|
|
src
|
|
)
|
|
|
|
print((
|
|
'[how_progression] UNKNOWN introduced %s to Crowbar'
|
|
):format(citizenid))
|
|
|
|
return true
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Persistent progression state sync
|
|
-- Restores unlocked world state whenever a character loads.
|
|
-- ============================================================
|
|
|
|
local function syncProgressionState(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then
|
|
print(('[how_progression] State sync: no player for source %s')
|
|
:format(tostring(src)))
|
|
return false
|
|
end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
local progression = getProgression(citizenid)
|
|
|
|
if not progression then
|
|
print(('[how_progression] State sync: no progression for %s')
|
|
:format(citizenid))
|
|
return false
|
|
end
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:syncProgression',
|
|
src,
|
|
{
|
|
unknown_introduced = progression.unknown_introduced or 0,
|
|
crowbar_stage = progression.crowbar_stage or 0,
|
|
crowbar_rep = progression.crowbar_rep or 0,
|
|
mack_stage = progression.mack_stage or 0,
|
|
vinny_fence_unlocked = progression.vinny_fence_unlocked or 0,
|
|
mack_rep = progression.mack_rep or 0,
|
|
tina_stage = progression.tina_stage or 0,
|
|
tina_rep = progression.tina_rep or 0,
|
|
story_stage = progression.story_stage or 0
|
|
}
|
|
)
|
|
|
|
print(('[how_progression] Restored persistent state for %s')
|
|
:format(citizenid))
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent('QBCore:Server:OnPlayerLoaded')
|
|
AddEventHandler('QBCore:Server:OnPlayerLoaded', function()
|
|
local src = source
|
|
|
|
print(('[how_progression] OnPlayerLoaded received from source %s')
|
|
:format(tostring(src)))
|
|
|
|
CreateThread(function()
|
|
Wait(3000)
|
|
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
|
|
if player then
|
|
print(('[how_progression] Player ready: source=%s citizenid=%s')
|
|
:format(
|
|
tostring(src),
|
|
tostring(player.PlayerData.citizenid)
|
|
))
|
|
else
|
|
print(('[how_progression] Player NOT ready for source %s')
|
|
:format(tostring(src)))
|
|
end
|
|
|
|
syncProgressionState(src)
|
|
end)
|
|
end)
|
|
|
|
-- Appearance save is our initial character hook.
|
|
RegisterNetEvent(
|
|
'illenium-appearance:server:saveAppearance',
|
|
function()
|
|
local src = source
|
|
|
|
CreateThread(function()
|
|
Wait(3000)
|
|
introduceUnknown(src)
|
|
end)
|
|
end
|
|
)
|
|
|
|
-- ============================================================
|
|
-- Development: UNKNOWN
|
|
-- ============================================================
|
|
|
|
RegisterCommand('testunknown', function(source)
|
|
if source == 0 then
|
|
print('[how_progression] /testunknown must be run by a player')
|
|
return
|
|
end
|
|
|
|
local success = introduceUnknown(source)
|
|
|
|
if success then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'UNKNOWN introduction sent.',
|
|
type = 'success'
|
|
})
|
|
else
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'UNKNOWN introduction was not sent.',
|
|
type = 'error'
|
|
})
|
|
end
|
|
end, false)
|
|
|
|
-- ============================================================
|
|
-- Crowbar Mission 1
|
|
-- The Introduction
|
|
-- ============================================================
|
|
|
|
local crowbarMission1 = nil
|
|
local crowbarMission1ByCitizenId = {}
|
|
|
|
local function clearCrowbarMission1()
|
|
if not crowbarMission1 then
|
|
return
|
|
end
|
|
|
|
if crowbarMission1.vehicle
|
|
and DoesEntityExist(crowbarMission1.vehicle) then
|
|
DeleteEntity(crowbarMission1.vehicle)
|
|
end
|
|
|
|
for _, participant in ipairs(crowbarMission1.participants) do
|
|
crowbarMission1ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
crowbarMission1 = nil
|
|
end
|
|
|
|
local function startCrowbarMission1(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return false end
|
|
|
|
local progression = getProgression(
|
|
player.PlayerData.citizenid
|
|
)
|
|
|
|
if not progression or progression.crowbar_stage ~= 1 then
|
|
print((
|
|
'[how_progression] Mission 1 rejected for %s: invalid stage'
|
|
):format(player.PlayerData.citizenid))
|
|
return false
|
|
end
|
|
|
|
local participants, partyId = getMissionParticipants(src)
|
|
|
|
if not participants or #participants == 0 then
|
|
print((
|
|
'[how_progression] Could not determine Mission 1 participants for %s'
|
|
):format(src))
|
|
return false
|
|
end
|
|
|
|
-- Only one Crowbar Mission 1 can be active at a time.
|
|
clearCrowbarMission1()
|
|
|
|
local vehicle = CreateVehicleServerSetter(
|
|
`emperor`,
|
|
'automobile',
|
|
911.40,
|
|
3589.50,
|
|
33.25,
|
|
91.36
|
|
)
|
|
|
|
if not vehicle or vehicle == 0 then
|
|
print(
|
|
'[how_progression] Failed to spawn Crowbar Mission 1 vehicle'
|
|
)
|
|
return false
|
|
end
|
|
|
|
while not DoesEntityExist(vehicle) do
|
|
Wait(0)
|
|
end
|
|
|
|
local snapshot = {}
|
|
|
|
for _, participant in ipairs(participants) do
|
|
snapshot[#snapshot + 1] = {
|
|
source = participant.source,
|
|
citizenid = participant.citizenid
|
|
}
|
|
end
|
|
|
|
crowbarMission1 = {
|
|
missionNumber = 1,
|
|
objective = 'retrieve_vehicle',
|
|
vehicle = vehicle,
|
|
netId = NetworkGetNetworkIdFromEntity(vehicle),
|
|
participants = snapshot,
|
|
partyId = partyId,
|
|
completed = false
|
|
}
|
|
|
|
for _, participant in ipairs(snapshot) do
|
|
crowbarMission1ByCitizenId[participant.citizenid] = crowbarMission1
|
|
|
|
exports.qbx_vehiclekeys:GiveKeys(
|
|
participant.source,
|
|
vehicle
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:startCrowbarMission1',
|
|
participant.source,
|
|
crowbarMission1.netId
|
|
)
|
|
end
|
|
|
|
if partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
partyId,
|
|
{
|
|
{
|
|
name = "Steal Crowbar's target vehicle",
|
|
status = 'current'
|
|
},
|
|
{
|
|
name = "Bring the vehicle back to Crowbar",
|
|
status = 'pending'
|
|
}
|
|
}
|
|
)
|
|
|
|
exports['bs_groupsystem']:sendPartyNotification(
|
|
partyId,
|
|
{
|
|
title = 'Crowbar',
|
|
description = 'Your crew is on the job.',
|
|
icon = 'car'
|
|
}
|
|
)
|
|
end
|
|
|
|
print((
|
|
'[how_progression] Crowbar Mission 1 started | owner: %s | participants: %s | party: %s | netId: %s'
|
|
):format(
|
|
player.PlayerData.citizenid,
|
|
#snapshot,
|
|
tostring(partyId),
|
|
tostring(crowbarMission1.netId)
|
|
))
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:startCrowbarMission1',
|
|
function()
|
|
startCrowbarMission1(source)
|
|
end
|
|
)
|
|
|
|
|
|
local function setCrowbarMission1ReturnPhase(src)
|
|
local mission = crowbarMission1
|
|
if not mission or mission.completed then
|
|
return false
|
|
end
|
|
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
|
|
if not citizenid
|
|
or crowbarMission1ByCitizenId[citizenid] ~= mission
|
|
then
|
|
return false
|
|
end
|
|
|
|
mission.objective = 'return_vehicle'
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission1Return',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = "Steal Crowbar's target vehicle",
|
|
status = 'done'
|
|
},
|
|
{
|
|
name = "Bring the vehicle back to Crowbar",
|
|
status = 'current'
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:crowbarMission1VehicleAcquired',
|
|
function()
|
|
setCrowbarMission1ReturnPhase(source)
|
|
end
|
|
)
|
|
|
|
local function completeCrowbarMission1(src, netId, devOverride)
|
|
local mission = crowbarMission1
|
|
|
|
if not mission or mission.completed then
|
|
print(
|
|
'[how_progression] Mission 1 completion rejected: no active mission'
|
|
)
|
|
return false
|
|
end
|
|
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
|
|
if not citizenid
|
|
or crowbarMission1ByCitizenId[citizenid] ~= mission
|
|
then
|
|
print((
|
|
'[how_progression] Mission 1 completion rejected for source %s: not a participant'
|
|
):format(src))
|
|
return false
|
|
end
|
|
|
|
if not devOverride then
|
|
local submittedVehicle =
|
|
NetworkGetEntityFromNetworkId(netId)
|
|
|
|
if submittedVehicle ~= mission.vehicle then
|
|
print((
|
|
'[how_progression] Mission 1 completion rejected for source %s: vehicle mismatch'
|
|
):format(src))
|
|
return false
|
|
end
|
|
|
|
if not DoesEntityExist(mission.vehicle) then
|
|
print(
|
|
'[how_progression] Mission 1 completion rejected: vehicle no longer exists'
|
|
)
|
|
return false
|
|
end
|
|
|
|
if mission.objective ~= 'return_vehicle' then
|
|
print(
|
|
'[how_progression] Mission 1 completion rejected: vehicle not acquired'
|
|
)
|
|
return false
|
|
end
|
|
end
|
|
|
|
mission.completed = true
|
|
mission.objective = 'complete'
|
|
|
|
if DoesEntityExist(mission.vehicle) then
|
|
DeleteEntity(mission.vehicle)
|
|
end
|
|
|
|
local owner = exports.qbx_core:GetPlayer(src)
|
|
|
|
if not owner then
|
|
mission.completed = false
|
|
return false
|
|
end
|
|
|
|
local cashReward = 500
|
|
local repReward = 5
|
|
|
|
owner.Functions.AddMoney(
|
|
'cash',
|
|
cashReward,
|
|
'crowbar-mission-1'
|
|
)
|
|
|
|
rewardCrowbarParticipants(
|
|
mission.participants,
|
|
1,
|
|
2,
|
|
repReward
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission1Reward',
|
|
src,
|
|
cashReward,
|
|
repReward
|
|
)
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = "Steal Crowbar's target vehicle",
|
|
status = 'done'
|
|
},
|
|
{
|
|
name = "Bring the vehicle back to Crowbar",
|
|
status = 'done'
|
|
}
|
|
}
|
|
)
|
|
|
|
exports['bs_groupsystem']:sendPartyNotification(
|
|
mission.partyId,
|
|
{
|
|
title = 'Crowbar',
|
|
description = 'Job done. You proved you can follow directions.',
|
|
icon = 'car'
|
|
}
|
|
)
|
|
end
|
|
|
|
print((
|
|
'[how_progression] Crowbar Mission 1 complete | owner: %s | $%s | participants: %s'
|
|
):format(
|
|
owner.PlayerData.citizenid,
|
|
cashReward,
|
|
#mission.participants
|
|
))
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:cleanupCrowbarMission1',
|
|
participant.source
|
|
)
|
|
end
|
|
|
|
crowbarMission1ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
crowbarMission1 = nil
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:completeCrowbarMission1',
|
|
function(netId)
|
|
completeCrowbarMission1(
|
|
source,
|
|
netId,
|
|
false
|
|
)
|
|
end
|
|
)
|
|
|
|
-- ============================================================
|
|
-- Development cleanup for Mission 1
|
|
-- ============================================================
|
|
|
|
CreateThread(function()
|
|
Wait(2000)
|
|
|
|
local locations = {
|
|
vec3(911.40, 3589.50, 33.25),
|
|
vec3(1705.50, 3771.76, 34.49)
|
|
}
|
|
|
|
local removed = 0
|
|
|
|
for _, vehicle in ipairs(GetAllVehicles()) do
|
|
if DoesEntityExist(vehicle)
|
|
and GetEntityModel(vehicle) == `emperor` then
|
|
|
|
local coords = GetEntityCoords(vehicle)
|
|
|
|
for _, location in ipairs(locations) do
|
|
if #(coords - location) < 50.0 then
|
|
DeleteEntity(vehicle)
|
|
removed = removed + 1
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
print((
|
|
'[how_progression] Cleaned up %s leftover Crowbar Mission 1 Emperor(s)'
|
|
):format(removed))
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- Crowbar Mission 2
|
|
-- Get Some Wheels
|
|
-- ============================================================
|
|
|
|
local crowbarMission2 = nil
|
|
local crowbarMission2ByCitizenId = {}
|
|
|
|
local function clearCrowbarMission2()
|
|
if not crowbarMission2 then
|
|
return
|
|
end
|
|
|
|
for _, participant in ipairs(crowbarMission2.participants) do
|
|
crowbarMission2ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
if crowbarMission2.vehicle
|
|
and DoesEntityExist(crowbarMission2.vehicle) then
|
|
DeleteEntity(crowbarMission2.vehicle)
|
|
end
|
|
|
|
crowbarMission2 = nil
|
|
end
|
|
|
|
local function startCrowbarMission2(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return false end
|
|
|
|
local progression = getProgression(
|
|
player.PlayerData.citizenid
|
|
)
|
|
|
|
if not progression or progression.crowbar_stage ~= 2 then
|
|
print((
|
|
'[how_progression] Mission 2 rejected for %s: invalid stage'
|
|
):format(player.PlayerData.citizenid))
|
|
return false
|
|
end
|
|
|
|
local participants, partyId = getMissionParticipants(src)
|
|
|
|
if not participants or #participants == 0 then
|
|
print((
|
|
'[how_progression] Could not determine Mission 2 participants for %s'
|
|
):format(src))
|
|
return false
|
|
end
|
|
|
|
clearCrowbarMission2()
|
|
|
|
local vehicle = CreateVehicleServerSetter(
|
|
`premier`,
|
|
'automobile',
|
|
2684.30,
|
|
3292.18,
|
|
54.75,
|
|
240.63
|
|
)
|
|
|
|
if not vehicle or vehicle == 0 then
|
|
print(
|
|
'[how_progression] Failed to spawn Crowbar Mission 2 vehicle'
|
|
)
|
|
return false
|
|
end
|
|
|
|
while not DoesEntityExist(vehicle) do
|
|
Wait(0)
|
|
end
|
|
|
|
SetVehicleDoorsLocked(vehicle, 2)
|
|
|
|
local snapshot = {}
|
|
|
|
for _, participant in ipairs(participants) do
|
|
snapshot[#snapshot + 1] = {
|
|
source = participant.source,
|
|
citizenid = participant.citizenid
|
|
}
|
|
end
|
|
|
|
crowbarMission2 = {
|
|
missionNumber = 2,
|
|
objective = 'steal_vehicle',
|
|
vehicle = vehicle,
|
|
netId = NetworkGetNetworkIdFromEntity(vehicle),
|
|
participants = snapshot,
|
|
partyId = partyId,
|
|
ownerSource = src,
|
|
completed = false
|
|
}
|
|
|
|
for _, participant in ipairs(snapshot) do
|
|
crowbarMission2ByCitizenId[participant.citizenid] =
|
|
crowbarMission2
|
|
|
|
exports.ox_inventory:AddItem(
|
|
participant.source,
|
|
'lockpick',
|
|
3
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:startCrowbarMission2',
|
|
participant.source,
|
|
crowbarMission2.netId
|
|
)
|
|
end
|
|
|
|
if partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
partyId,
|
|
{
|
|
{
|
|
name = "Find the car Crowbar told you about",
|
|
status = 'current'
|
|
},
|
|
{
|
|
name = 'Break into the car',
|
|
status = 'pending'
|
|
},
|
|
{
|
|
name = 'Steal the car',
|
|
status = 'pending'
|
|
}
|
|
}
|
|
)
|
|
|
|
exports['bs_groupsystem']:sendPartyNotification(
|
|
partyId,
|
|
{
|
|
title = 'Crowbar',
|
|
description = 'Three lockpicks each. Go get yourselves some wheels.',
|
|
icon = 'key'
|
|
}
|
|
)
|
|
end
|
|
|
|
print((
|
|
'[how_progression] Crowbar Mission 2 started | owner: %s | participants: %s | party: %s | netId: %s'
|
|
):format(
|
|
player.PlayerData.citizenid,
|
|
#snapshot,
|
|
tostring(partyId),
|
|
tostring(crowbarMission2.netId)
|
|
))
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:startCrowbarMission2',
|
|
function()
|
|
startCrowbarMission2(source)
|
|
end
|
|
)
|
|
|
|
|
|
local function completeCrowbarMission2(src, netId, props, devOverride)
|
|
local mission = crowbarMission2
|
|
|
|
if not mission or mission.completed then
|
|
print(
|
|
'[how_progression] Mission 2 completion rejected: no active mission'
|
|
)
|
|
return false
|
|
end
|
|
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
|
|
if not citizenid
|
|
or crowbarMission2ByCitizenId[citizenid] ~= mission
|
|
then
|
|
print((
|
|
'[how_progression] Mission 2 completion rejected for source %s: not a participant'
|
|
):format(src))
|
|
return false
|
|
end
|
|
|
|
-- Normal gameplay validates the submitted vehicle and its properties.
|
|
-- Development completion uses the authoritative server mission vehicle
|
|
-- and creates a stock owned Premier without requiring client streaming.
|
|
if not devOverride then
|
|
local submittedVehicle =
|
|
NetworkGetEntityFromNetworkId(netId)
|
|
|
|
if submittedVehicle ~= mission.vehicle then
|
|
print((
|
|
'[how_progression] Mission 2 completion rejected for source %s: vehicle mismatch'
|
|
):format(src))
|
|
return false
|
|
end
|
|
|
|
if not DoesEntityExist(mission.vehicle) then
|
|
print(
|
|
'[how_progression] Mission 2 completion rejected: vehicle no longer exists'
|
|
)
|
|
return false
|
|
end
|
|
|
|
if GetEntityModel(mission.vehicle) ~= `premier` then
|
|
print(
|
|
'[how_progression] Mission 2 completion rejected: wrong vehicle model'
|
|
)
|
|
return false
|
|
end
|
|
|
|
if type(props) ~= 'table' then
|
|
print(
|
|
'[how_progression] Mission 2 completion rejected: missing vehicle properties'
|
|
)
|
|
return false
|
|
end
|
|
end
|
|
|
|
mission.completed = true
|
|
mission.objective = 'complete'
|
|
|
|
local owner =
|
|
exports.qbx_core:GetPlayerByCitizenId(
|
|
mission.participants[1].citizenid
|
|
)
|
|
|
|
-- Prefer the original mission initiator when still connected.
|
|
local originalOwner =
|
|
exports.qbx_core:GetPlayer(mission.ownerSource)
|
|
|
|
if originalOwner then
|
|
owner = originalOwner
|
|
end
|
|
|
|
if not owner then
|
|
mission.completed = false
|
|
print(
|
|
'[how_progression] Mission 2 completion rejected: owner unavailable'
|
|
)
|
|
return false
|
|
end
|
|
|
|
local ownerCitizenId =
|
|
owner.PlayerData.citizenid
|
|
|
|
local request = {
|
|
model = 'premier',
|
|
citizenid = ownerCitizenId
|
|
}
|
|
|
|
if not devOverride and type(props) == 'table' then
|
|
request.props = props
|
|
end
|
|
|
|
local vehicleId, err =
|
|
exports.qbx_vehicles:CreatePlayerVehicle(request)
|
|
|
|
if not vehicleId then
|
|
print((
|
|
'[how_progression] Failed to create owned Premier for %s: %s'
|
|
):format(
|
|
ownerCitizenId,
|
|
json.encode(err)
|
|
))
|
|
|
|
mission.completed = false
|
|
mission.objective = 'steal_vehicle'
|
|
return false
|
|
end
|
|
|
|
rewardCrowbarParticipants(
|
|
mission.participants,
|
|
2,
|
|
3,
|
|
5
|
|
)
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = "Find the car Crowbar told you about",
|
|
status = 'done'
|
|
},
|
|
{
|
|
name = 'Break into the car',
|
|
status = 'done'
|
|
},
|
|
{
|
|
name = 'Steal the car',
|
|
status = 'done'
|
|
}
|
|
}
|
|
)
|
|
|
|
exports['bs_groupsystem']:sendPartyNotification(
|
|
mission.partyId,
|
|
{
|
|
title = 'Crowbar',
|
|
description = 'Job done. The Premier is yours.',
|
|
icon = 'car'
|
|
}
|
|
)
|
|
end
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
sendSmsToPlayer(
|
|
participant.source,
|
|
'555-7384',
|
|
"Hey, it's Crowbar. Save my contact. " ..
|
|
"And keep the car—you earned it. " ..
|
|
"I'll be in touch when I've got more work."
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:cleanupCrowbarMission2',
|
|
participant.source
|
|
)
|
|
end
|
|
|
|
crowbarMission2ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
print((
|
|
'[how_progression] Crowbar Mission 2 complete | owner: %s | vehicleId: %s | participants: %s'
|
|
):format(
|
|
ownerCitizenId,
|
|
tostring(vehicleId),
|
|
#mission.participants
|
|
))
|
|
|
|
-- Do not delete the physical mission Premier.
|
|
crowbarMission2 = nil
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:completeCrowbarMission2',
|
|
function(netId, props)
|
|
completeCrowbarMission2(
|
|
source,
|
|
netId,
|
|
props,
|
|
false
|
|
)
|
|
end
|
|
)
|
|
|
|
-- ============================================================
|
|
-- Crowbar Mission 3: Quick Score
|
|
-- ============================================================
|
|
|
|
local crowbarMission3 = nil
|
|
local crowbarMission3ByCitizenId = {}
|
|
|
|
local function clearCrowbarMission3()
|
|
if not crowbarMission3 then return end
|
|
|
|
for _, participant in ipairs(crowbarMission3.participants) do
|
|
crowbarMission3ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
crowbarMission3 = nil
|
|
end
|
|
|
|
local function startCrowbarMission3(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return false end
|
|
|
|
local progression = getProgression(
|
|
player.PlayerData.citizenid
|
|
)
|
|
|
|
if not progression or progression.crowbar_stage ~= 3 then
|
|
print((
|
|
'[how_progression] Mission 3 rejected for %s: invalid stage'
|
|
):format(player.PlayerData.citizenid))
|
|
return false
|
|
end
|
|
|
|
local participants, partyId = getMissionParticipants(src)
|
|
|
|
if not participants or #participants == 0 then
|
|
print((
|
|
'[how_progression] Could not determine Mission 3 participants for %s'
|
|
):format(src))
|
|
return false
|
|
end
|
|
|
|
clearCrowbarMission3()
|
|
|
|
local snapshot = {}
|
|
|
|
for _, participant in ipairs(participants) do
|
|
snapshot[#snapshot + 1] = {
|
|
source = participant.source,
|
|
citizenid = participant.citizenid
|
|
}
|
|
end
|
|
|
|
crowbarMission3 = {
|
|
missionNumber = 3,
|
|
objective = 'rob_register',
|
|
participants = snapshot,
|
|
partyId = partyId,
|
|
ownerSource = src,
|
|
targetStoreIndex = 13,
|
|
registerRobbed = false,
|
|
completed = false
|
|
}
|
|
|
|
for _, participant in ipairs(snapshot) do
|
|
crowbarMission3ByCitizenId[participant.citizenid] =
|
|
crowbarMission3
|
|
|
|
print((
|
|
'[how_progression] Mission 3 client target: initiator=%s participant=%s citizenid=%s'
|
|
):format(
|
|
tostring(src),
|
|
tostring(participant.source),
|
|
tostring(participant.citizenid)
|
|
))
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:startCrowbarMission3',
|
|
participant.source
|
|
)
|
|
end
|
|
|
|
-- One unloaded pistol for the crew.
|
|
exports.ox_inventory:AddItem(
|
|
src,
|
|
'weapon_pistol',
|
|
1
|
|
)
|
|
|
|
if partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
partyId,
|
|
{
|
|
{
|
|
name = 'Rob the Harmony 24/7 register',
|
|
status = false
|
|
},
|
|
{
|
|
name = 'Lose the cops',
|
|
status = false
|
|
}
|
|
}
|
|
)
|
|
|
|
exports['bs_groupsystem']:sendPartyNotification(
|
|
partyId,
|
|
'Crowbar: Quick Score'
|
|
)
|
|
end
|
|
|
|
print((
|
|
'[how_progression] Crowbar Mission 3 started by %s with %s participant(s)'
|
|
):format(
|
|
player.PlayerData.citizenid,
|
|
#snapshot
|
|
))
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:startCrowbarMission3',
|
|
function()
|
|
startCrowbarMission3(source)
|
|
end
|
|
)
|
|
|
|
AddEventHandler(
|
|
'how_progression:server:storeRegisterRobbed',
|
|
function(src, storeIndex)
|
|
local mission = crowbarMission3
|
|
|
|
if not mission or mission.completed then return end
|
|
if mission.registerRobbed then return end
|
|
if storeIndex ~= mission.targetStoreIndex then return end
|
|
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
|
|
print((
|
|
'[how_progression] M3 robbery validation | src=%s citizenid=%s store=%s member=%s'
|
|
):format(
|
|
tostring(src),
|
|
tostring(citizenid),
|
|
tostring(storeIndex),
|
|
tostring(crowbarMission3ByCitizenId[citizenid] == mission)
|
|
))
|
|
|
|
if crowbarMission3ByCitizenId[citizenid] ~= mission then return end
|
|
|
|
mission.registerRobbed = true
|
|
mission.objective = 'lose_cops'
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = 'Rob the Harmony 24/7 register',
|
|
status = true
|
|
},
|
|
{
|
|
name = 'Lose the cops',
|
|
status = false
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission3Escape',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
|
|
print((
|
|
'[how_progression] Crowbar Mission 3 register robbed | store %s'
|
|
):format(storeIndex))
|
|
end
|
|
)
|
|
|
|
local function completeCrowbarMission3(src, devOverride)
|
|
local mission = crowbarMission3
|
|
|
|
if not mission or mission.completed then
|
|
return false
|
|
end
|
|
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
if not citizenid then
|
|
return false
|
|
end
|
|
|
|
if crowbarMission3ByCitizenId[citizenid] ~= mission then
|
|
return false
|
|
end
|
|
|
|
-- Normal gameplay requires the register robbery first.
|
|
-- /prog X.2 may bypass the objective for development testing.
|
|
if not devOverride and not mission.registerRobbed then
|
|
return false
|
|
end
|
|
|
|
mission.completed = true
|
|
mission.objective = 'complete'
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = 'Rob the Harmony 24/7 register',
|
|
status = true
|
|
},
|
|
{
|
|
name = 'Lose the cops',
|
|
status = true
|
|
}
|
|
}
|
|
)
|
|
|
|
exports['bs_groupsystem']:sendPartyNotification(
|
|
mission.partyId,
|
|
'Quick Score complete'
|
|
)
|
|
end
|
|
|
|
rewardCrowbarParticipants(
|
|
mission.participants,
|
|
3,
|
|
4,
|
|
5
|
|
)
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
sendSmsToPlayer(
|
|
participant.source,
|
|
'555-7384',
|
|
"Not bad. One problem—the cash is marked. " ..
|
|
"Don't spend it. Come see me. " ..
|
|
"I know a guy who can clean it."
|
|
)
|
|
end
|
|
|
|
crowbarMission3ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
crowbarMission3 = nil
|
|
|
|
print(
|
|
'[how_progression] Crowbar Mission 3 complete | stage 4'
|
|
)
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:completeCrowbarMission3',
|
|
function()
|
|
completeCrowbarMission3(source, false)
|
|
end
|
|
)
|
|
|
|
-- ============================================================
|
|
-- Crowbar Mission 4: Wash the Cash
|
|
-- ============================================================
|
|
|
|
local crowbarMission4 = nil
|
|
local crowbarMission4ByCitizenId = {}
|
|
|
|
local function clearCrowbarMission4()
|
|
if not crowbarMission4 then return end
|
|
|
|
for _, participant in ipairs(crowbarMission4.participants) do
|
|
crowbarMission4ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
crowbarMission4 = nil
|
|
end
|
|
|
|
local function startCrowbarMission4(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return false end
|
|
|
|
local progression = getProgression(
|
|
player.PlayerData.citizenid
|
|
)
|
|
|
|
if not progression or progression.crowbar_stage ~= 4 then
|
|
return false
|
|
end
|
|
|
|
local participants, partyId = getMissionParticipants(src)
|
|
|
|
if not participants or #participants == 0 then
|
|
return false
|
|
end
|
|
|
|
clearCrowbarMission4()
|
|
|
|
local snapshot = {}
|
|
|
|
for _, participant in ipairs(participants) do
|
|
snapshot[#snapshot + 1] = {
|
|
source = participant.source,
|
|
citizenid = participant.citizenid
|
|
}
|
|
end
|
|
|
|
crowbarMission4 = {
|
|
missionNumber = 4,
|
|
objective = 'meet_mack',
|
|
participants = snapshot,
|
|
partyId = partyId,
|
|
ownerSource = src,
|
|
completed = false
|
|
}
|
|
|
|
for _, participant in ipairs(snapshot) do
|
|
crowbarMission4ByCitizenId[participant.citizenid] =
|
|
crowbarMission4
|
|
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:startCrowbarMission4',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
|
|
if partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
partyId,
|
|
{
|
|
{
|
|
name = 'Meet Mack at the scrapyard',
|
|
status = false
|
|
},
|
|
{
|
|
name = 'Take the marked cash to Gabe',
|
|
status = false
|
|
},
|
|
{
|
|
name = 'Wash the marked cash',
|
|
status = false
|
|
}
|
|
}
|
|
)
|
|
|
|
exports['bs_groupsystem']:sendPartyNotification(
|
|
partyId,
|
|
'Crowbar: Wash the Cash'
|
|
)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:startCrowbarMission4',
|
|
function()
|
|
startCrowbarMission4(source)
|
|
end
|
|
)
|
|
|
|
lib.callback.register(
|
|
'how_progression:server:canTalkToMack',
|
|
function(source)
|
|
local progression, player = getPlayerProgression(source)
|
|
if not progression or not player then return false end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
|
|
return (progression.mack_stage or 0) >= 1
|
|
or crowbarMission4ByCitizenId[citizenid] ~= nil
|
|
end
|
|
)
|
|
|
|
lib.callback.register(
|
|
'how_progression:server:getCrowbarMission4State',
|
|
function(source)
|
|
local progression, player = getPlayerProgression(source)
|
|
if not progression or not player then return nil end
|
|
|
|
local mission =
|
|
crowbarMission4ByCitizenId[
|
|
player.PlayerData.citizenid
|
|
]
|
|
|
|
if mission and not mission.completed then
|
|
return mission.objective
|
|
end
|
|
|
|
if (progression.mack_stage or 0) >= 1 then
|
|
return 'unlocked'
|
|
end
|
|
|
|
return nil
|
|
end
|
|
)
|
|
|
|
local function crowbarMission4TalkedToMack(src)
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
if not citizenid then return false end
|
|
|
|
local mission = crowbarMission4ByCitizenId[citizenid]
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'meet_mack'
|
|
then
|
|
return false
|
|
end
|
|
|
|
mission.objective = 'meet_gabe'
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET mack_stage = GREATEST(mack_stage, 1)
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ participant.citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission4Mack',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = 'Meet Mack at the scrapyard',
|
|
status = true
|
|
},
|
|
{
|
|
name = 'Take the marked cash to Gabe',
|
|
status = false
|
|
},
|
|
{
|
|
name = 'Wash the marked cash',
|
|
status = false
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:crowbarMission4TalkedToMack',
|
|
function()
|
|
crowbarMission4TalkedToMack(source)
|
|
end
|
|
)
|
|
|
|
local function crowbarMission4TalkedToGabe(src)
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
if not citizenid then return false end
|
|
|
|
local mission = crowbarMission4ByCitizenId[citizenid]
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'meet_gabe'
|
|
then
|
|
return false
|
|
end
|
|
|
|
mission.objective = 'wash_cash'
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission4Gabe',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = 'Meet Mack at the scrapyard',
|
|
status = true
|
|
},
|
|
{
|
|
name = 'Take the marked cash to Gabe',
|
|
status = true
|
|
},
|
|
{
|
|
name = 'Wash the marked cash',
|
|
status = false
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
AddEventHandler(
|
|
'how_progression:server:moneywashStarted',
|
|
function(src)
|
|
crowbarMission4TalkedToGabe(src)
|
|
end
|
|
)
|
|
|
|
local function completeCrowbarMission4(src, devOverride)
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
if not citizenid then return false end
|
|
|
|
local mission = crowbarMission4ByCitizenId[citizenid]
|
|
|
|
if not mission or mission.completed then
|
|
return false
|
|
end
|
|
|
|
if not devOverride and mission.objective ~= 'wash_cash' then
|
|
return false
|
|
end
|
|
|
|
mission.completed = true
|
|
mission.objective = 'complete'
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = 'Meet Mack at the scrapyard',
|
|
status = true
|
|
},
|
|
{
|
|
name = 'Take the marked cash to Gabe',
|
|
status = true
|
|
},
|
|
{
|
|
name = 'Wash the marked cash',
|
|
status = true
|
|
}
|
|
}
|
|
)
|
|
|
|
exports['bs_groupsystem']:sendPartyNotification(
|
|
mission.partyId,
|
|
'Wash the Cash complete'
|
|
)
|
|
end
|
|
|
|
rewardCrowbarParticipants(
|
|
mission.participants,
|
|
4,
|
|
5,
|
|
5
|
|
)
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
sendSmsToPlayer(
|
|
participant.source,
|
|
'555-7384',
|
|
"Good. Clean money spends clean. " ..
|
|
"Come back and see me. " ..
|
|
"I got something new for ya."
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:cleanupCrowbarMission4',
|
|
participant.source
|
|
)
|
|
end
|
|
|
|
crowbarMission4ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
crowbarMission4 = nil
|
|
|
|
print(
|
|
'[how_progression] Crowbar Mission 4 complete | stage 5'
|
|
)
|
|
|
|
return true
|
|
end
|
|
|
|
AddEventHandler(
|
|
'how_progression:server:moneyWashed',
|
|
function(src)
|
|
completeCrowbarMission4(src, false)
|
|
end
|
|
)
|
|
|
|
-- ============================================================
|
|
-- Mack Mission 1: Easy Rider
|
|
-- ============================================================
|
|
|
|
local mackMission1 = nil
|
|
local mackMission1ByCitizenId = {}
|
|
|
|
local function clearMackMission1()
|
|
if not mackMission1 then return end
|
|
|
|
for _, participant in ipairs(mackMission1.participants) do
|
|
mackMission1ByCitizenId[participant.citizenid] = nil
|
|
end
|
|
|
|
mackMission1 = nil
|
|
end
|
|
|
|
local function getMackMission1ForSource(src)
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
if not citizenid then return nil end
|
|
|
|
return mackMission1ByCitizenId[citizenid]
|
|
end
|
|
|
|
local function broadcastMackMission1(mission, eventName, ...)
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
eventName,
|
|
participant.source,
|
|
...
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function startMackMission1(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return false end
|
|
|
|
local progression =
|
|
getProgression(player.PlayerData.citizenid)
|
|
|
|
if not progression
|
|
or (progression.mack_stage or 0) ~= 1
|
|
then
|
|
return false
|
|
end
|
|
|
|
if mackMission1 then
|
|
TriggerClientEvent(
|
|
'ox_lib:notify',
|
|
src,
|
|
{
|
|
title = 'Mack',
|
|
description = 'I already have somebody handling that delivery.',
|
|
type = 'error'
|
|
}
|
|
)
|
|
|
|
return false
|
|
end
|
|
|
|
local participants, partyId =
|
|
getMissionParticipants(src)
|
|
|
|
if not participants or #participants == 0 then
|
|
return false
|
|
end
|
|
|
|
local snapshot = {}
|
|
|
|
for _, participant in ipairs(participants) do
|
|
snapshot[#snapshot + 1] = {
|
|
source = participant.source,
|
|
citizenid = participant.citizenid
|
|
}
|
|
end
|
|
|
|
mackMission1 = {
|
|
missionNumber = 1,
|
|
objective = 'leave_scrapyard',
|
|
participants = snapshot,
|
|
partyId = partyId,
|
|
ownerSource = src,
|
|
bikeCount = math.min(#snapshot, 5),
|
|
bikesDelivered = {},
|
|
deliveredCount = 0,
|
|
completed = false
|
|
}
|
|
|
|
for _, participant in ipairs(snapshot) do
|
|
mackMission1ByCitizenId[
|
|
participant.citizenid
|
|
] = mackMission1
|
|
end
|
|
|
|
for _, participant in ipairs(mackMission1.participants) do
|
|
if participantIsValid(participant) then
|
|
local added = exports.ox_inventory:AddItem(
|
|
participant.source,
|
|
'WEAPON_BAT',
|
|
1
|
|
)
|
|
|
|
if not added then
|
|
TriggerClientEvent('ox_lib:notify', participant.source, {
|
|
title = 'Mack',
|
|
description = 'Could not carry the bat. Make room in your inventory.',
|
|
type = 'error'
|
|
})
|
|
end
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:startMackMission1',
|
|
participant.source,
|
|
participant.source == mackMission1.ownerSource,
|
|
mackMission1.bikeCount
|
|
)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:startMackMission1',
|
|
function()
|
|
startMackMission1(source)
|
|
end
|
|
)
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:mackMission1BikeSpawned',
|
|
function(bikeIndex, netId)
|
|
local src = source
|
|
local mission = getMackMission1ForSource(src)
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or src ~= mission.ownerSource
|
|
then
|
|
return
|
|
end
|
|
|
|
bikeIndex = tonumber(bikeIndex)
|
|
netId = tonumber(netId)
|
|
|
|
if not bikeIndex
|
|
or bikeIndex < 1
|
|
or bikeIndex > mission.bikeCount
|
|
or not netId
|
|
then
|
|
return
|
|
end
|
|
|
|
CreateThread(function()
|
|
local vehicle = 0
|
|
|
|
-- Network ownership can reach the server slightly after
|
|
-- the spawning client sends us the network ID.
|
|
for attempt = 1, 20 do
|
|
vehicle = NetworkGetEntityFromNetworkId(netId)
|
|
|
|
if vehicle and vehicle ~= 0
|
|
and DoesEntityExist(vehicle)
|
|
then
|
|
break
|
|
end
|
|
|
|
Wait(100)
|
|
end
|
|
|
|
if not vehicle
|
|
or vehicle == 0
|
|
or not DoesEntityExist(vehicle)
|
|
then
|
|
print((
|
|
'[how_progression] M1 failed to resolve bike %d netId %d'
|
|
):format(bikeIndex, netId))
|
|
return
|
|
end
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
exports.qbx_vehiclekeys:GiveKeys(
|
|
participant.source,
|
|
vehicle,
|
|
true
|
|
)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
)
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:mackMission1Detour',
|
|
function()
|
|
local mission =
|
|
getMackMission1ForSource(source)
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'leave_scrapyard'
|
|
then
|
|
return
|
|
end
|
|
|
|
mission.objective = 'trash_car'
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
sendSmsToPlayer(
|
|
participant.source,
|
|
'555-6842',
|
|
"Hold up. Got something else for you while you're out. Sending you an address."
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:mackMission1TrashCar',
|
|
participant.source,
|
|
participant.source == mission.ownerSource
|
|
)
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:mackMission1AtDetour',
|
|
function()
|
|
local mission =
|
|
getMackMission1ForSource(source)
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'trash_car'
|
|
then
|
|
return
|
|
end
|
|
|
|
if mission.detourExplained then
|
|
return
|
|
end
|
|
|
|
mission.detourExplained = true
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
sendSmsToPlayer(
|
|
participant.source,
|
|
'555-6842',
|
|
"See that car? Guy owes me money. I'm tired of asking. Don't kill anybody. Just make sure his next ride is on a tow truck."
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:mackMission1DetourExplained',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:mackMission1CarTrashed',
|
|
function()
|
|
local mission =
|
|
getMackMission1ForSource(source)
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'trash_car'
|
|
or not mission.detourExplained
|
|
or source ~= mission.ownerSource
|
|
then
|
|
return
|
|
end
|
|
|
|
mission.objective = 'deliver_bikes'
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
sendSmsToPlayer(
|
|
participant.source,
|
|
'555-6842',
|
|
"Yeah. That'll get the point across. Get those bikes to Vinny."
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:mackMission1DeliverBikes',
|
|
participant.source,
|
|
mission.deliveredCount,
|
|
mission.bikeCount
|
|
)
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:mackMission1BikeDelivered',
|
|
function(bikeIndex)
|
|
local mission =
|
|
getMackMission1ForSource(source)
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'deliver_bikes'
|
|
or source ~= mission.ownerSource
|
|
then
|
|
return
|
|
end
|
|
|
|
bikeIndex = tonumber(bikeIndex)
|
|
|
|
if not bikeIndex
|
|
or bikeIndex < 1
|
|
or bikeIndex > mission.bikeCount
|
|
or mission.bikesDelivered[bikeIndex]
|
|
then
|
|
return
|
|
end
|
|
|
|
mission.bikesDelivered[bikeIndex] = true
|
|
mission.deliveredCount =
|
|
mission.deliveredCount + 1
|
|
|
|
broadcastMackMission1(
|
|
mission,
|
|
'how_progression:client:mackMission1DeliveryCount',
|
|
mission.deliveredCount,
|
|
mission.bikeCount
|
|
)
|
|
|
|
if mission.deliveredCount >= mission.bikeCount then
|
|
mission.objective = 'talk_vinny'
|
|
|
|
broadcastMackMission1(
|
|
mission,
|
|
'how_progression:client:mackMission1TalkVinny'
|
|
)
|
|
end
|
|
end
|
|
)
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:completeMackMission1',
|
|
function()
|
|
local mission =
|
|
getMackMission1ForSource(source)
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'talk_vinny'
|
|
then
|
|
return
|
|
end
|
|
|
|
mission.completed = true
|
|
mission.objective = 'complete'
|
|
|
|
-- First Mack mission:
|
|
-- $300 clean cash + 5 Mack REP for every snapshotted participant.
|
|
rewardMackParticipants(
|
|
mission.participants,
|
|
1,
|
|
2,
|
|
5,
|
|
300
|
|
)
|
|
|
|
broadcastMackMission1(
|
|
mission,
|
|
'how_progression:client:mackMission1Complete'
|
|
)
|
|
|
|
-- Vinny follows up after the crew has left.
|
|
local participants = mission.participants
|
|
|
|
SetTimeout(math.random(60000, 90000), function()
|
|
for _, participant in ipairs(participants) do
|
|
if participantIsValid(participant) then
|
|
local smsSent = sendSmsToPlayer(
|
|
participant.source,
|
|
'555-3176',
|
|
"Hey. Vinny. Told ya you'd like this.\n\n" ..
|
|
"I buy Sanchezes. Don't care where you find 'em.\n\n" ..
|
|
"Bring 'em around back. I handle the VIN and paperwork, you get paid.\n\n" ..
|
|
"Marked bills. Don't ask.\n\n" ..
|
|
"I can only move so many a day, so don't get greedy."
|
|
)
|
|
|
|
if smsSent then
|
|
getProgression(participant.citizenid)
|
|
local saved = MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET vinny_fence_unlocked = 1
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ participant.citizenid }
|
|
)
|
|
|
|
local progression = saved ~= nil
|
|
and getProgression(participant.citizenid)
|
|
|
|
if progression and isVinnyFenceUnlocked(
|
|
progression.vinny_fence_unlocked
|
|
) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:unlockVinnyFence',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
for _, participant in ipairs(participants) do
|
|
mackMission1ByCitizenId[
|
|
participant.citizenid
|
|
] = nil
|
|
end
|
|
|
|
mackMission1 = nil
|
|
|
|
print(
|
|
'[how_progression] Mack Mission 1 complete | stage 2'
|
|
)
|
|
end
|
|
)
|
|
|
|
local vinnyFenceBusy = {}
|
|
local vinnyFenceCoords = vec3(1356.79, 3617.37, 34.88)
|
|
|
|
lib.callback.register('how_progression:server:fenceSanchez', function(source, netId, gameDay)
|
|
if not vinnyFenceSchemaReady then
|
|
return false, 'Vinny is not ready yet. Try again shortly.'
|
|
end
|
|
|
|
local player = exports.qbx_core:GetPlayer(source)
|
|
if not player then return false, 'Player unavailable.' end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
if vinnyFenceBusy[citizenid] then return false, 'Vinny is handling your last bike.' end
|
|
vinnyFenceBusy[citizenid] = true
|
|
|
|
local function finish(success, message)
|
|
vinnyFenceBusy[citizenid] = nil
|
|
return success, message
|
|
end
|
|
|
|
local progression = getProgression(citizenid)
|
|
if not progression or not isVinnyFenceUnlocked(
|
|
progression.vinny_fence_unlocked
|
|
) then
|
|
return finish(false, 'Vinny has not offered to buy bikes yet.')
|
|
end
|
|
|
|
gameDay = tonumber(gameDay)
|
|
if not gameDay or gameDay ~= math.floor(gameDay) then
|
|
return finish(false, 'Could not verify the in-game day.')
|
|
end
|
|
|
|
local year = math.floor(gameDay / 10000)
|
|
local month = math.floor(gameDay / 100) % 100
|
|
local day = gameDay % 100
|
|
if year < 1900 or year > 2100 or month < 0 or month > 12 or day < 1 or day > 31
|
|
or gameDay < (progression.vinny_fence_day or 0)
|
|
then
|
|
return finish(false, 'Could not verify the in-game day.')
|
|
end
|
|
|
|
local vehicle = NetworkGetEntityFromNetworkId(tonumber(netId) or 0)
|
|
local ped = GetPlayerPed(source)
|
|
if not vehicle or vehicle == 0 or not DoesEntityExist(vehicle)
|
|
or GetEntityModel(vehicle) ~= `sanchez`
|
|
or not ped or ped == 0
|
|
or GetVehiclePedIsIn(ped, false) ~= vehicle
|
|
or GetPedInVehicleSeat(vehicle, -1) ~= ped
|
|
or #(GetEntityCoords(vehicle) - vinnyFenceCoords) > 3.0
|
|
or #(GetEntityCoords(ped) - vinnyFenceCoords) > 4.0
|
|
or Entity(vehicle).state.howMackMission1Bike
|
|
then
|
|
return finish(false, 'Bring an unowned Sanchez into Vinny\'s marker.')
|
|
end
|
|
|
|
local plate = GetVehicleNumberPlateText(vehicle)
|
|
if Entity(vehicle).state.vehicleid
|
|
or exports.qbx_vehicles:GetVehicleIdByPlate(plate)
|
|
then
|
|
return finish(false, 'Vinny does not buy player-owned bikes.')
|
|
end
|
|
|
|
if not exports.ox_inventory:CanCarryItem(source, 'markedbills', 1, { worth = 200 }) then
|
|
return finish(false, 'Make room for the marked bills first.')
|
|
end
|
|
|
|
local reserved = MySQL.update.await([[
|
|
UPDATE how_progression
|
|
SET vinny_fence_count = IF(vinny_fence_day = ?, vinny_fence_count + 1, 1),
|
|
vinny_fence_day = ?
|
|
WHERE citizenid = ? AND vinny_fence_unlocked = 1
|
|
AND (vinny_fence_day < ? OR vinny_fence_count < 3)
|
|
]], { gameDay, gameDay, citizenid, gameDay })
|
|
|
|
if reserved ~= 1 then
|
|
return finish(false, 'Vinny has bought three Sanchezes from you today.')
|
|
end
|
|
|
|
local function releaseQuota()
|
|
MySQL.update.await([[
|
|
UPDATE how_progression
|
|
SET vinny_fence_count = GREATEST(vinny_fence_count - 1, 0)
|
|
WHERE citizenid = ? AND vinny_fence_day = ?
|
|
]], { citizenid, gameDay })
|
|
end
|
|
|
|
TriggerClientEvent('how_progression:client:leaveFenceBike', source, netId)
|
|
local timeout = GetGameTimer() + 4000
|
|
while GetVehiclePedIsIn(ped, false) == vehicle and GetGameTimer() < timeout do
|
|
Wait(100)
|
|
end
|
|
|
|
if not DoesEntityExist(vehicle)
|
|
or GetVehiclePedIsIn(ped, false) == vehicle
|
|
or #(GetEntityCoords(vehicle) - vinnyFenceCoords) > 3.0
|
|
or #(GetEntityCoords(ped) - vinnyFenceCoords) > 5.0
|
|
then
|
|
releaseQuota()
|
|
return finish(false, 'Leave the Sanchez parked in the marker.')
|
|
end
|
|
|
|
DeleteEntity(vehicle)
|
|
Wait(100)
|
|
if DoesEntityExist(vehicle) then
|
|
releaseQuota()
|
|
return finish(false, 'Vinny could not take that bike. Try again.')
|
|
end
|
|
|
|
local added = exports.ox_inventory:AddItem(source, 'markedbills', 1, { worth = 200 })
|
|
if not added then
|
|
releaseQuota()
|
|
return finish(false, 'The marked bills could not be added to your inventory.')
|
|
end
|
|
|
|
return finish(true, 'Vinny paid you $200 in marked bills.')
|
|
end)
|
|
|
|
lib.callback.register(
|
|
'how_progression:server:getMackMission1State',
|
|
function(source)
|
|
local progression, player =
|
|
getPlayerProgression(source)
|
|
|
|
if not progression or not player then
|
|
return nil
|
|
end
|
|
|
|
local mission =
|
|
mackMission1ByCitizenId[
|
|
player.PlayerData.citizenid
|
|
]
|
|
|
|
if mission and not mission.completed then
|
|
return {
|
|
objective = mission.objective,
|
|
deliveredCount = mission.deliveredCount,
|
|
bikeCount = mission.bikeCount
|
|
}
|
|
end
|
|
|
|
if (progression.mack_stage or 0) == 1 then
|
|
return {
|
|
objective = 'available',
|
|
deliveredCount = 0
|
|
}
|
|
end
|
|
|
|
if (progression.mack_stage or 0) >= 2 then
|
|
return {
|
|
objective = 'complete',
|
|
deliveredCount = 3
|
|
}
|
|
end
|
|
|
|
return nil
|
|
end
|
|
)
|
|
|
|
-- ============================================================
|
|
-- Crowbar Mission 5: Buy In
|
|
-- ============================================================
|
|
|
|
local crowbarMission5 = nil
|
|
local crowbarMission5ByCitizenId = {}
|
|
|
|
local function clearCrowbarMission5()
|
|
if not crowbarMission5 then return end
|
|
|
|
for _, participant in ipairs(crowbarMission5.participants) do
|
|
crowbarMission5ByCitizenId[
|
|
participant.citizenid
|
|
] = nil
|
|
end
|
|
|
|
crowbarMission5 = nil
|
|
end
|
|
|
|
local function startCrowbarMission5(src)
|
|
local player = exports.qbx_core:GetPlayer(src)
|
|
if not player then return false end
|
|
|
|
local progression = getProgression(
|
|
player.PlayerData.citizenid
|
|
)
|
|
|
|
if not progression
|
|
or progression.crowbar_stage ~= 5
|
|
then
|
|
return false
|
|
end
|
|
|
|
local participants, partyId =
|
|
getMissionParticipants(src)
|
|
|
|
if not participants or #participants == 0 then
|
|
return false
|
|
end
|
|
|
|
clearCrowbarMission5()
|
|
|
|
local snapshot = {}
|
|
|
|
for _, participant in ipairs(participants) do
|
|
snapshot[#snapshot + 1] = {
|
|
source = participant.source,
|
|
citizenid = participant.citizenid
|
|
}
|
|
end
|
|
|
|
local stashId =
|
|
exports.ox_inventory:CreateTemporaryStash({
|
|
label = "Crowbar's Package",
|
|
slots = 1,
|
|
maxWeight = 5000,
|
|
items = {
|
|
{
|
|
'crowbar_package',
|
|
1,
|
|
{
|
|
description =
|
|
"A sealed package. Crowbar wants it unopened."
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
if not stashId then
|
|
return false
|
|
end
|
|
|
|
crowbarMission5 = {
|
|
missionNumber = 5,
|
|
objective = 'get_package',
|
|
participants = snapshot,
|
|
partyId = partyId,
|
|
ownerSource = src,
|
|
stashId = stashId,
|
|
completed = false
|
|
}
|
|
|
|
for _, participant in ipairs(snapshot) do
|
|
crowbarMission5ByCitizenId[
|
|
participant.citizenid
|
|
] = crowbarMission5
|
|
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:startCrowbarMission5',
|
|
participant.source,
|
|
stashId
|
|
)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:startCrowbarMission5',
|
|
function()
|
|
startCrowbarMission5(source)
|
|
end
|
|
)
|
|
|
|
local function crowbarMission5PackageTaken(src)
|
|
local citizenid =
|
|
getCitizenIdFromSource(src)
|
|
|
|
if not citizenid then return false end
|
|
|
|
local mission =
|
|
crowbarMission5ByCitizenId[citizenid]
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'get_package'
|
|
then
|
|
return false
|
|
end
|
|
|
|
mission.objective = 'escape_ambush'
|
|
mission.carrierSource = src
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
sendSmsToPlayer(
|
|
participant.source,
|
|
'555-7384',
|
|
"Get out of there! You're about to have company! Lose em!"
|
|
)
|
|
end
|
|
end
|
|
|
|
print((
|
|
'[how_progression] M5 package taken | carrier=%s | participants=%s'
|
|
):format(
|
|
tostring(src),
|
|
tostring(#mission.participants)
|
|
))
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
local valid = participantIsValid(participant)
|
|
|
|
print((
|
|
'[how_progression] M5 participant | source=%s citizenid=%s valid=%s ambushOwner=%s'
|
|
):format(
|
|
tostring(participant.source),
|
|
tostring(participant.citizenid),
|
|
tostring(valid),
|
|
tostring(participant.source == src)
|
|
))
|
|
|
|
if valid then
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission5PackageTaken',
|
|
participant.source,
|
|
participant.source == src
|
|
)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:crowbarMission5Escaped',
|
|
function()
|
|
local src = source
|
|
local citizenid = getCitizenIdFromSource(src)
|
|
|
|
if not citizenid then return end
|
|
|
|
local mission =
|
|
crowbarMission5ByCitizenId[citizenid]
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or mission.objective ~= 'escape_ambush'
|
|
or mission.carrierSource ~= src
|
|
then
|
|
return
|
|
end
|
|
|
|
mission.objective = 'return_package'
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission5Escaped',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
end
|
|
)
|
|
|
|
exports.ox_inventory:registerHook(
|
|
'swapItems',
|
|
function(payload)
|
|
if not crowbarMission5 then
|
|
return
|
|
end
|
|
|
|
if crowbarMission5.objective ~= 'get_package' then
|
|
return
|
|
end
|
|
|
|
local fromInventory = payload.fromInventory
|
|
local fromId = fromInventory
|
|
|
|
if type(fromInventory) == 'table' then
|
|
fromId =
|
|
fromInventory.id
|
|
or fromInventory.name
|
|
end
|
|
|
|
if tostring(fromId)
|
|
~= tostring(crowbarMission5.stashId)
|
|
then
|
|
return
|
|
end
|
|
|
|
-- Moving the package from Crowbar's stash into a
|
|
-- player's inventory starts the ambush.
|
|
if payload.toType ~= 'player' then
|
|
return
|
|
end
|
|
|
|
local itemName
|
|
|
|
if type(payload.fromSlot) == 'table' then
|
|
itemName = payload.fromSlot.name
|
|
end
|
|
|
|
if itemName ~= 'crowbar_package' then
|
|
return
|
|
end
|
|
|
|
-- swapItems is a pre-hook. Let ox_inventory finish
|
|
-- moving the item, then advance the mission.
|
|
SetTimeout(250, function()
|
|
crowbarMission5PackageTaken(
|
|
payload.source
|
|
)
|
|
end)
|
|
end,
|
|
{
|
|
itemFilter = {
|
|
crowbar_package = true
|
|
}
|
|
}
|
|
)
|
|
|
|
local function completeCrowbarMission5(
|
|
src,
|
|
devOverride
|
|
)
|
|
local citizenid =
|
|
getCitizenIdFromSource(src)
|
|
|
|
if not citizenid then return false end
|
|
|
|
local mission =
|
|
crowbarMission5ByCitizenId[citizenid]
|
|
|
|
if not mission or mission.completed then
|
|
return false
|
|
end
|
|
|
|
if not devOverride
|
|
and mission.objective ~= 'return_package'
|
|
then
|
|
return false
|
|
end
|
|
|
|
if not devOverride then
|
|
local count =
|
|
exports.ox_inventory:GetItemCount(
|
|
src,
|
|
'crowbar_package'
|
|
)
|
|
|
|
if not count or count < 1 then
|
|
TriggerClientEvent(
|
|
'ox_lib:notify',
|
|
src,
|
|
{
|
|
title = 'Crowbar',
|
|
description =
|
|
"You don't have my package.",
|
|
type = 'error'
|
|
}
|
|
)
|
|
|
|
return false
|
|
end
|
|
|
|
exports.ox_inventory:RemoveItem(
|
|
src,
|
|
'crowbar_package',
|
|
1
|
|
)
|
|
end
|
|
|
|
mission.completed = true
|
|
mission.objective = 'complete'
|
|
|
|
rewardCrowbarParticipants(
|
|
mission.participants,
|
|
5,
|
|
6,
|
|
5
|
|
)
|
|
|
|
-- The carrier gets the cash payment.
|
|
local carrier =
|
|
exports.qbx_core:GetPlayer(src)
|
|
|
|
if carrier then
|
|
carrier.Functions.AddMoney(
|
|
'cash',
|
|
750,
|
|
'crowbar-buy-in'
|
|
)
|
|
end
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:cleanupCrowbarMission5',
|
|
participant.source
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission5Complete',
|
|
participant.source
|
|
)
|
|
end
|
|
|
|
crowbarMission5ByCitizenId[
|
|
participant.citizenid
|
|
] = nil
|
|
end
|
|
|
|
crowbarMission5 = nil
|
|
|
|
print(
|
|
'[how_progression] Crowbar Mission 5 complete | stage 6'
|
|
)
|
|
|
|
return true
|
|
end
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:completeCrowbarMission5',
|
|
function()
|
|
completeCrowbarMission5(
|
|
source,
|
|
false
|
|
)
|
|
end
|
|
)
|
|
|
|
-- ============================================================
|
|
-- Crowbar active mission state
|
|
-- ============================================================
|
|
|
|
local function getCrowbarActiveMission(citizenid)
|
|
local checks = {
|
|
{ mission = crowbarMission1, members = crowbarMission1ByCitizenId },
|
|
{ mission = crowbarMission2, members = crowbarMission2ByCitizenId },
|
|
{ mission = crowbarMission3, members = crowbarMission3ByCitizenId },
|
|
{ mission = crowbarMission4, members = crowbarMission4ByCitizenId },
|
|
{ mission = crowbarMission5, members = crowbarMission5ByCitizenId }
|
|
}
|
|
|
|
for _, check in ipairs(checks) do
|
|
local mission = check.mission
|
|
|
|
if mission
|
|
and not mission.completed
|
|
and check.members[citizenid] == mission
|
|
then
|
|
return {
|
|
missionNumber = mission.missionNumber,
|
|
objective = mission.objective
|
|
}
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Crowbar progression callback
|
|
-- ============================================================
|
|
|
|
lib.callback.register(
|
|
'how_progression:server:getCrowbarMission',
|
|
function(source)
|
|
local progression = getPlayerProgression(source)
|
|
|
|
if not progression then
|
|
return 0
|
|
end
|
|
|
|
return progression.crowbar_stage or 0
|
|
end
|
|
)
|
|
|
|
|
|
lib.callback.register(
|
|
'how_progression:server:isCrowbarMissionActive',
|
|
function(source)
|
|
local citizenid = getCitizenIdFromSource(source)
|
|
if not citizenid then return false end
|
|
|
|
return getCrowbarActiveMission(citizenid) ~= nil
|
|
end
|
|
)
|
|
|
|
-- ============================================================
|
|
-- Development: skip Crowbar Mission 1
|
|
-- ============================================================
|
|
|
|
RegisterCommand('skipcrowbar1', function(source)
|
|
if source == 0 then
|
|
print(
|
|
'[how_progression] /skipcrowbar1 must be run by a player'
|
|
)
|
|
return
|
|
end
|
|
|
|
local progression, player =
|
|
getPlayerProgression(source)
|
|
|
|
if not progression or not player then
|
|
return
|
|
end
|
|
|
|
local currentRep =
|
|
progression.crowbar_rep or 0
|
|
|
|
local newRep =
|
|
math.min(currentRep + 5, 100)
|
|
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
unknown_introduced = 1,
|
|
crowbar_stage = GREATEST(crowbar_stage, 2),
|
|
crowbar_rep = GREATEST(crowbar_rep, ?)
|
|
WHERE citizenid = ?
|
|
]],
|
|
{
|
|
newRep,
|
|
player.PlayerData.citizenid
|
|
}
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarReward',
|
|
source,
|
|
5,
|
|
newRep,
|
|
2
|
|
)
|
|
|
|
print((
|
|
'[how_progression] %s skipped Crowbar Mission 1 | stage 2 | REP %s'
|
|
):format(
|
|
player.PlayerData.citizenid,
|
|
newRep
|
|
))
|
|
end, false)
|
|
|
|
-- ============================================================
|
|
-- Development: progression control
|
|
-- Usage:
|
|
-- /prog crowbar 0
|
|
-- /prog crowbar 3
|
|
-- /prog mack 3
|
|
-- /prog tina 5
|
|
-- ============================================================
|
|
|
|
RegisterCommand('prog', function(source, args)
|
|
if source == 0 then
|
|
print('[how_progression] /prog must be run by a player')
|
|
return
|
|
end
|
|
|
|
local contact = args[1] and string.lower(args[1])
|
|
local stageInput = args[2]
|
|
|
|
-- Complete development reset.
|
|
if contact == 'reset' then
|
|
local player = exports.qbx_core:GetPlayer(source)
|
|
|
|
if not player then
|
|
return
|
|
end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
|
|
-- Abort all currently active Crowbar missions.
|
|
clearCrowbarMission1()
|
|
clearCrowbarMission2()
|
|
clearCrowbarMission3()
|
|
clearCrowbarMission4()
|
|
clearCrowbarMission5()
|
|
|
|
-- Reset all custom progression, including UNKNOWN.
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
unknown_introduced = 0,
|
|
crowbar_stage = 0,
|
|
crowbar_rep = 0,
|
|
mack_stage = 0,
|
|
vinny_fence_unlocked = 0,
|
|
vinny_fence_day = 0,
|
|
vinny_fence_count = 0,
|
|
mack_rep = 0,
|
|
tina_stage = 0,
|
|
tina_rep = 0,
|
|
story_stage = 0
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
-- Development reset intentionally removes everything
|
|
-- currently carried by this character.
|
|
-- Reset inventory to the standard development baseline.
|
|
exports.ox_inventory:ClearInventory(source)
|
|
|
|
exports.ox_inventory:AddItem(
|
|
source,
|
|
'phone',
|
|
1
|
|
)
|
|
|
|
exports.ox_inventory:AddItem(
|
|
source,
|
|
'money',
|
|
500
|
|
)
|
|
|
|
-- Clear local mission state, temporary routes/blips,
|
|
-- and locked contact blips.
|
|
TriggerClientEvent(
|
|
'how_progression:client:resetProgression',
|
|
source
|
|
)
|
|
|
|
print((
|
|
'[how_progression] Full progression reset for %s'
|
|
):format(citizenid))
|
|
|
|
return
|
|
end
|
|
|
|
-- UNKNOWN development control.
|
|
--
|
|
-- /prog unknown 0 = reset UNKNOWN introduction state
|
|
-- /prog unknown 1 = perform the real UNKNOWN introduction
|
|
if contact == 'unknown' then
|
|
local player = exports.qbx_core:GetPlayer(source)
|
|
|
|
if not player then
|
|
return
|
|
end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
|
|
if stageInput == '0' then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
unknown_introduced = 0,
|
|
crowbar_stage = 0
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:resetProgression',
|
|
source
|
|
)
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'UNKNOWN reset.',
|
|
type = 'success'
|
|
})
|
|
|
|
return
|
|
|
|
elseif stageInput == '1' then
|
|
if not introduceUnknown(source) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'UNKNOWN introduction was not sent.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'UNKNOWN introduction triggered.',
|
|
type = 'success'
|
|
})
|
|
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Use /prog unknown 0 or /prog unknown 1.',
|
|
type = 'error'
|
|
})
|
|
|
|
return
|
|
end
|
|
|
|
if not contact or not stageInput then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Usage: /prog crowbar 0-10 or /prog crowbar 1.1',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
-- Development mission shortcuts:
|
|
-- X = mission available
|
|
-- X.Y = mission X, phase Y
|
|
--
|
|
-- Each mission defines what its phases mean. The final
|
|
-- phase always performs the real mission completion path.
|
|
local missionInput, phaseInput =
|
|
stageInput:match('^(%d+)%.(%d+)$')
|
|
|
|
local stage
|
|
local phase
|
|
|
|
if missionInput then
|
|
stage = tonumber(missionInput)
|
|
phase = tonumber(phaseInput)
|
|
|
|
if not phase or phase < 1 then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Mission phase must be 1 or greater.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
else
|
|
if not stageInput:match('^%d+$') then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Use X or X.Y, for example 1, 1.1, or 1.2.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
stage = tonumber(stageInput)
|
|
end
|
|
|
|
if not stage or stage < 0 or stage > 10 then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Stage must be between 0 and 10.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
local player = exports.qbx_core:GetPlayer(source)
|
|
|
|
if not player then
|
|
return
|
|
end
|
|
|
|
local citizenid = player.PlayerData.citizenid
|
|
local column
|
|
|
|
if contact == 'crowbar' then
|
|
column = 'crowbar_stage'
|
|
elseif contact == 'mack' then
|
|
column = 'mack_stage'
|
|
elseif contact == 'tina' then
|
|
column = 'tina_stage'
|
|
else
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Valid contacts: crowbar, mack, tina',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
-- Mission phase shortcuts.
|
|
if phase then
|
|
if contact ~= 'crowbar' then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Mission phase shortcuts are currently only implemented for Crowbar.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
-- ====================================================
|
|
-- Crowbar Mission 1
|
|
-- 1.1 = accepted / retrieve Emperor
|
|
-- 1.2 = Emperor acquired / return to Crowbar
|
|
-- 1.3 = completion
|
|
-- ====================================================
|
|
|
|
if stage == 1 then
|
|
if phase == 1 then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
crowbar_stage = 1,
|
|
unknown_introduced = 1
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:unlockCrowbar',
|
|
source
|
|
)
|
|
|
|
if not startCrowbarMission1(source) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Could not start Crowbar Mission 1.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 1 started.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
|
|
elseif phase == 2 then
|
|
if not setCrowbarMission1ReturnPhase(source) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 1 is not currently active.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 1 advanced to return phase.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
|
|
elseif phase == 3 then
|
|
if not completeCrowbarMission1(
|
|
source,
|
|
nil,
|
|
true
|
|
) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 1 could not be completed.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 1 completed.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
end
|
|
|
|
-- ====================================================
|
|
-- Crowbar Mission 2
|
|
-- 2.1 = accepted / steal Premier
|
|
-- 2.2 = completion
|
|
-- ====================================================
|
|
|
|
elseif stage == 2 then
|
|
if phase == 1 then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
crowbar_stage = 2,
|
|
unknown_introduced = 1
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:unlockCrowbar',
|
|
source
|
|
)
|
|
|
|
if not startCrowbarMission2(source) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Could not start Crowbar Mission 2.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 2 started.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
|
|
elseif phase == 2 then
|
|
if not completeCrowbarMission2(
|
|
source,
|
|
nil,
|
|
nil,
|
|
true
|
|
) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 2 could not be completed.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 2 completed.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
end
|
|
|
|
-- ====================================================
|
|
-- Crowbar Mission 3
|
|
-- 3.1 = accepted / rob Harmony register
|
|
-- 3.2 = register robbed / lose cops
|
|
-- 3.3 = completion
|
|
-- ====================================================
|
|
|
|
elseif stage == 3 then
|
|
if phase == 1 then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
crowbar_stage = 3,
|
|
unknown_introduced = 1
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:unlockCrowbar',
|
|
source
|
|
)
|
|
|
|
if not startCrowbarMission3(source) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Could not start Crowbar Mission 3.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 3 started.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
|
|
elseif phase == 2 then
|
|
local mission = crowbarMission3
|
|
|
|
if not mission
|
|
or mission.completed
|
|
or crowbarMission3ByCitizenId[citizenid] ~= mission
|
|
then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 3 is not currently active.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
mission.registerRobbed = true
|
|
mission.objective = 'lose_cops'
|
|
|
|
for _, participant in ipairs(mission.participants) do
|
|
if participantIsValid(participant) then
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission3Escape',
|
|
participant.source
|
|
)
|
|
end
|
|
end
|
|
|
|
if mission.partyId then
|
|
exports['bs_groupsystem']:updatePartyTasks(
|
|
mission.partyId,
|
|
{
|
|
{
|
|
name = 'Rob the Harmony 24/7 register',
|
|
status = 'done'
|
|
},
|
|
{
|
|
name = 'Lose the cops',
|
|
status = 'current'
|
|
}
|
|
}
|
|
)
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 3 advanced to escape phase.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
|
|
elseif phase == 3 then
|
|
if not completeCrowbarMission3(source, true) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 3 could not be completed.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 3 completed.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
end
|
|
end
|
|
|
|
-- ====================================================
|
|
-- Crowbar Mission 4: Wash the Cash
|
|
-- 4.1 = accepted / meet Mack
|
|
-- 4.2 = Mack spoken to / meet Gabe
|
|
-- 4.3 = Gabe contacted / wash cash
|
|
-- 4.4 = completion
|
|
-- ====================================================
|
|
|
|
if stage == 4 then
|
|
if phase == 1 then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
crowbar_stage = 4,
|
|
unknown_introduced = 1
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:unlockCrowbar',
|
|
source
|
|
)
|
|
|
|
if not startCrowbarMission4(source) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Could not start Crowbar Mission 4.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 4 started.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
|
|
elseif phase == 2 then
|
|
if not crowbarMission4TalkedToMack(source) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Mission 4 is not waiting for Mack.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Mission 4 advanced to Gabe.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
|
|
elseif phase == 3 then
|
|
if not crowbarMission4TalkedToGabe(source) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Mission 4 is not waiting for Gabe.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Mission 4 advanced to money washing.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
|
|
elseif phase == 4 then
|
|
if not completeCrowbarMission4(source, true) then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 4 could not be completed.',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Crowbar Mission 4 completed.',
|
|
type = 'success'
|
|
})
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Crowbar Mission 5: Buy In
|
|
if stage == 5 then
|
|
if phase == 1 then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
crowbar_stage = 5,
|
|
unknown_introduced = 1
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:unlockCrowbar',
|
|
source
|
|
)
|
|
|
|
if not startCrowbarMission5(source) then
|
|
TriggerClientEvent(
|
|
'ox_lib:notify',
|
|
source,
|
|
{
|
|
title = 'Development',
|
|
description = 'Could not start Crowbar Mission 5.',
|
|
type = 'error'
|
|
}
|
|
)
|
|
end
|
|
|
|
return
|
|
|
|
elseif phase == 2 then
|
|
if not crowbarMission5PackageTaken(source) then
|
|
TriggerClientEvent(
|
|
'ox_lib:notify',
|
|
source,
|
|
{
|
|
title = 'Development',
|
|
description = 'Mission 5 could not advance to the ambush.',
|
|
type = 'error'
|
|
}
|
|
)
|
|
end
|
|
|
|
return
|
|
|
|
elseif phase == 3 then
|
|
-- Fight or escape: there is deliberately no
|
|
-- kill requirement. This represents reaching
|
|
-- the return-package phase.
|
|
local mission =
|
|
crowbarMission5ByCitizenId[citizenid]
|
|
|
|
if mission then
|
|
mission.objective =
|
|
'return_package'
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:crowbarMission5PackageTaken',
|
|
source
|
|
)
|
|
end
|
|
|
|
return
|
|
|
|
elseif phase == 4 then
|
|
completeCrowbarMission5(
|
|
source,
|
|
true
|
|
)
|
|
|
|
return
|
|
end
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = ('Crowbar Mission %s phase %s is not implemented.'):format(
|
|
stage,
|
|
phase
|
|
),
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
-- Normal progression stage.
|
|
MySQL.update.await(
|
|
('UPDATE how_progression SET %s = ? WHERE citizenid = ?'):format(column),
|
|
{ stage, citizenid }
|
|
)
|
|
|
|
-- Setting Crowbar to 0 also resets his introduction and REP.
|
|
if contact == 'crowbar' and stage == 0 then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET
|
|
unknown_introduced = 0,
|
|
crowbar_rep = 0
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
end
|
|
|
|
-- Active Crowbar stages require the character to have been introduced.
|
|
if contact == 'crowbar' and stage > 0 then
|
|
MySQL.update.await(
|
|
[[
|
|
UPDATE how_progression
|
|
SET unknown_introduced = 1
|
|
WHERE citizenid = ?
|
|
]],
|
|
{ citizenid }
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'how_progression:client:unlockCrowbar',
|
|
source
|
|
)
|
|
end
|
|
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = (
|
|
'%s progression set to stage %s.'
|
|
):format(
|
|
contact:gsub("^%l", string.upper),
|
|
stage
|
|
),
|
|
type = 'success'
|
|
})
|
|
|
|
print((
|
|
'[how_progression] %s set %s progression to stage %s'
|
|
):format(
|
|
citizenid,
|
|
contact,
|
|
stage
|
|
))
|
|
end, false)
|
|
|
|
-- ============================================================
|
|
-- Resource cleanup
|
|
-- ============================================================
|
|
|
|
AddEventHandler('onResourceStop', function(resourceName)
|
|
if resourceName ~= GetCurrentResourceName() then
|
|
return
|
|
end
|
|
|
|
if crowbarMission1 then
|
|
if crowbarMission1.vehicle
|
|
and DoesEntityExist(crowbarMission1.vehicle) then
|
|
DeleteEntity(crowbarMission1.vehicle)
|
|
end
|
|
end
|
|
|
|
if crowbarMission2 then
|
|
if crowbarMission2.vehicle
|
|
and DoesEntityExist(crowbarMission2.vehicle) then
|
|
DeleteEntity(crowbarMission2.vehicle)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Development helper: give properly formatted marked bills.
|
|
lib.addCommand('givemarked', {
|
|
help = 'Give yourself marked bills for development testing',
|
|
params = {
|
|
{
|
|
name = 'amount',
|
|
type = 'number',
|
|
help = 'Dollar value of the marked bills'
|
|
}
|
|
},
|
|
restricted = false
|
|
}, function(source, args)
|
|
local amount = math.floor(tonumber(args.amount) or 0)
|
|
|
|
if amount <= 0 then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = 'Usage: /givemarked <amount>',
|
|
type = 'error'
|
|
})
|
|
return
|
|
end
|
|
|
|
local success = exports.ox_inventory:AddItem(
|
|
source,
|
|
'markedbills',
|
|
1,
|
|
{ worth = amount }
|
|
)
|
|
|
|
if success then
|
|
TriggerClientEvent('ox_lib:notify', source, {
|
|
title = 'Development',
|
|
description = ('$%s in marked bills added.'):format(amount),
|
|
type = 'success'
|
|
})
|
|
end
|
|
end)
|
|
|
|
RegisterNetEvent(
|
|
'how_progression:server:crowbarPackageCarrierDied',
|
|
function()
|
|
local src = source
|
|
|
|
local count = exports.ox_inventory:Search(
|
|
src,
|
|
'count',
|
|
'crowbar_package'
|
|
) or 0
|
|
|
|
if count > 0 then
|
|
exports.ox_inventory:RemoveItem(
|
|
src,
|
|
'crowbar_package',
|
|
count
|
|
)
|
|
|
|
TriggerClientEvent(
|
|
'ox_lib:notify',
|
|
src,
|
|
{
|
|
title = 'Buy In',
|
|
description = "Crowbar's package was lost.",
|
|
type = 'error'
|
|
}
|
|
)
|
|
end
|
|
end
|
|
)
|
|
|
|
-- Temporary NPWD contact test
|
|
RegisterCommand('testcontact', function(source)
|
|
if source == 0 then return end
|
|
|
|
local number = ('555-%04d'):format(math.random(0, 9999))
|
|
local contact = exports.npwd:addContact(source, {
|
|
display = 'Michael',
|
|
number = number,
|
|
avatar = 'https://fivemedia.howlerdesign.com/media/6801a2d6-659f-46e3-b0ee-028a9a4ac215.png'
|
|
})
|
|
|
|
if not contact then
|
|
print('[how_progression] Failed to add NPWD test contact')
|
|
return
|
|
end
|
|
|
|
TriggerClientEvent('how_progression:client:addPhoneContact', source, contact)
|
|
|
|
SetTimeout(500, function()
|
|
sendSmsToPlayer(source, number, 'Hey. Testing the new contact system.')
|
|
end)
|
|
end, false)
|