Initial how_progression baseline

This commit is contained in:
2026-09-20 17:12:20 +00:00
commit 890a250270
22 changed files with 7258 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
return {
id = 'cosmic_dave',
displayName = 'Cosmic Dave',
ped = {
model = 'ig_omega',
coords = { x = 2447.63, y = 3775.08, z = 41.43, w = 23.30 },
spawnZOffset = -1.0,
scenario = 'WORLD_HUMAN_PARTYING',
missionEntity = true, invincible = true, frozen = true,
blockNonTemporaryEvents = true,
},
world = { alwaysPresent = true },
missions = {},
}
+37
View File
@@ -0,0 +1,37 @@
return {
id = 'crowbar',
displayName = 'Crowbar',
phone = { number = '555-7384' },
ped = {
model = 'ig_clay',
coords = { x = 1699.67, y = 3775.30, z = 34.74, w = 207.22 },
spawnZOffset = -1.0,
scenario = 'WORLD_HUMAN_SMOKING',
invincible = true,
frozen = true,
blockNonTemporaryEvents = true,
},
world = {
requires = { column = 'crowbar_stage', min = 1 },
vehicles = {
{
id = 'crowbar_daemon', model = 'daemon',
coords = { x = 1699.70, y = 3771.63, z = 34.53, w = 258.21 },
localEntity = true, locked = 2, invincible = true, frozen = true,
},
},
},
blip = {
label = 'Crowbar', sprite = 280, scale = 0.75, shortRange = false,
requires = { column = 'crowbar_stage', min = 1 },
},
interaction = {
name = 'how_progression_crowbar_talk', icon = 'fa-solid fa-comment',
label = 'Talk to Crowbar', distance = 2.0,
gateCallback = 'how_progression:server:isCrowbarMissionActive',
gateAllowsWhen = false,
stageCallback = 'how_progression:server:getCrowbarMission',
},
progression = { stageColumn = 'crowbar_stage', repColumn = 'crowbar_rep' },
missions = { 'crowbar:1', 'crowbar:2', 'crowbar:3', 'crowbar:4', 'crowbar:5' },
}
+18
View File
@@ -0,0 +1,18 @@
return {
id = 'deke',
displayName = 'Deke',
ped = {
model = 's_m_m_autoshop_02',
coords = { x = 2359.89, y = 3136.74, z = 48.21, w = 110.90 },
spawnZOffset = -1.0,
scenario = 'WORLD_HUMAN_STAND_IMPATIENT',
invincible = true, frozen = true, blockNonTemporaryEvents = true,
},
world = { alwaysPresent = true, spawnedWith = 'mack' },
interaction = {
name = 'how_progression_deke_shop', icon = 'fa-solid fa-toolbox',
label = 'Talk to Deke', distance = 2.0,
inventory = { kind = 'shop', type = 'DekeTools', id = 1 },
},
missions = {},
}
+13
View File
@@ -0,0 +1,13 @@
return {
id = 'gabe',
displayName = 'Gabe',
location = { x = 2340.96, y = 3126.11, z = 48.21 },
blip = {
label = 'Money Laundering', sprite = 500, display = 4,
scale = 0.7, shortRange = true,
createdDuring = 'crowbar:4',
},
service = { resource = 'randol_moneywash' },
associatedMissions = { 'crowbar:4' },
missions = {},
}
+27
View File
@@ -0,0 +1,27 @@
-- Passive definitions. Requiring this module does not create world entities.
local byId = {
unknown = require 'shared.contacts.unknown',
crowbar = require 'shared.contacts.crowbar',
mack = require 'shared.contacts.mack',
gabe = require 'shared.contacts.gabe',
deke = require 'shared.contacts.deke',
vinny = require 'shared.contacts.vinny',
cosmic_dave = require 'shared.contacts.cosmic_dave',
}
local phoneOwners = {}
for id, contact in pairs(byId) do
assert(contact.id == id, 'contact ID mismatch: ' .. id)
local number = contact.phone and contact.phone.number
if number then
assert(number:match('^555%-%d%d%d%d$'), 'invalid contact number: ' .. id)
assert(not phoneOwners[number], 'duplicate contact number: ' .. number)
phoneOwners[number] = id
end
end
return {
byId = byId,
get = function(id) return byId[id] end,
phoneOwner = function(number) return phoneOwners[number] end,
}
+67
View File
@@ -0,0 +1,67 @@
return {
id = 'mack',
displayName = 'Mack',
phone = { number = '555-6842' },
ped = {
model = 's_m_y_xmech_01',
coords = { x = 2366.60, y = 3154.91, z = 48.21, w = 17.84 },
spawnZOffset = -1.0,
scenario = 'WORLD_HUMAN_CLIPBOARD',
invincible = true, frozen = true, blockNonTemporaryEvents = true,
},
world = {
alwaysPresent = true,
vehicles = {
{
id = 'festival_bus', model = 'pbus2',
coords = { x = 2349.54, y = 3133.84, z = 48.21, w = 77.73 },
localEntity = true, locked = 2, missionEntity = true,
invincible = true, frozen = true,
},
{
id = 'junk_emperor', model = 'emperor',
coords = { x = 2343.67, y = 3141.72, z = 48.21, w = 211.39 },
localEntity = true, locked = 2, missionEntity = true,
invincible = true, frozen = true,
damage = {
engineHealth = 100.0, bodyHealth = 300.0,
smashedWindows = { 0, 1, 2, 3 }, brokenDoors = { 0 },
burstTyres = { 0, 4 },
},
},
{
id = 'forklift', model = 'forklift',
coords = { x = 2350.42, y = 3142.49, z = 48.21, w = 231.27 },
localEntity = true, locked = 2, missionEntity = true,
invincible = true, frozen = true,
},
},
ambientPeds = {
{ model = 's_m_y_xmech_02', coords = { x = 2355.0, y = 3148.0, z = 48.21, w = 180.0 }, wanderRadius = 12.0 },
{ model = 's_m_m_autoshop_01', coords = { x = 2345.0, y = 3150.0, z = 48.21, w = 90.0 }, wanderRadius = 12.0 },
{ model = 's_m_y_construct_01', coords = { x = 2360.0, y = 3143.0, z = 48.21, w = 270.0 }, wanderRadius = 15.0 },
{ model = 's_m_y_construct_02', coords = { x = 2341.0, y = 3130.0, z = 48.21, w = 0.0 }, wanderRadius = 10.0 },
{ model = 's_m_m_dockwork_01', coords = { x = 2334.0, y = 3125.0, z = 48.21, w = 180.0 }, wanderRadius = 15.0 },
{ model = 's_m_m_gardener_01', coords = { x = 2368.0, y = 3130.0, z = 48.21, w = 90.0 }, wanderRadius = 22.0 },
},
ambientPedBehavior = {
spawnZOffset = -1.0, missionEntity = true, invincible = true,
blockNonTemporaryEvents = true, canRagdoll = false,
fleeAttributes = 0, wanderMinLength = 2.0, wanderTimeBetweenWalks = 10.0,
},
},
blip = {
label = 'Mack', sprite = 280, scale = 0.75, shortRange = false,
requires = { column = 'mack_stage', min = 1 },
createdDuring = 'crowbar:4',
},
interaction = {
name = 'how_progression_mack_talk', icon = 'fa-solid fa-comment',
label = 'Talk to Mack', distance = 2.0,
gateCallback = 'how_progression:server:canTalkToMack',
gateAllowsWhen = true,
missionStateCallback = 'how_progression:server:getMackMission1State',
},
progression = { stageColumn = 'mack_stage', repColumn = 'mack_rep' },
missions = { 'mack:1' },
}
+7
View File
@@ -0,0 +1,7 @@
return {
id = 'unknown',
displayName = 'UNKNOWN',
phone = { number = '555-0100' },
progression = { introductionFlag = 'unknown_introduced' },
missions = {},
}
+46
View File
@@ -0,0 +1,46 @@
return {
id = 'vinny',
displayName = 'Vinny',
phone = { number = '555-3176' },
ped = {
model = 'a_m_m_business_01',
coords = { x = 1362.57, y = 3602.49, z = 34.96, w = 236.75 },
spawnZOffset = -1.0,
scenario = 'WORLD_HUMAN_STAND_IMPATIENT',
missionEntity = true, invincible = true, frozen = true,
blockNonTemporaryEvents = true,
},
world = {
createdDuring = 'mack:1',
restoredWhen = { vinny_fence_unlocked = 1 },
vehicles = {
{ model = 'sanchez', coords = { x = 1368.39, y = 3594.54, z = 34.34, w = 237.54 } },
{ model = 'sanchez', coords = { x = 1366.95, y = 3598.24, z = 34.37, w = 244.92 } },
{ model = 'sanchez', coords = { x = 1368.76, y = 3592.05, z = 34.37, w = 241.99 } },
{ model = 'sanchez', coords = { x = 1357.67, y = 3601.45, z = 34.44, w = 194.85 } },
{ model = 'sanchez', coords = { x = 1364.94, y = 3603.77, z = 34.33, w = 195.30 } },
},
vehicleBehavior = {
localEntity = true, missionEntity = true, locked = 2,
invincible = true, frozen = true,
purchaseEvent = 'qbx_vehicleshop:server:buyShowroomVehicle',
},
},
blip = {
label = 'Vinny', sprite = 226, scale = 0.7, shortRange = true,
createdDuring = 'mack:1', restoredWhen = { vinny_fence_unlocked = 1 },
},
interaction = {
name = 'how_progression_vinny_talk', icon = 'fa-solid fa-comment',
label = 'Talk to Vinny', distance = 2.0,
requiresObjective = 'talk_vinny',
},
fence = {
coords = { x = 1356.79, y = 3617.37, z = 34.88, w = 105.38 },
requires = { vinny_fence_unlocked = 1 },
vehicleModel = 'sanchez', dailyLimit = 3,
payoutItem = 'markedbills', payoutWorth = 200,
},
associatedMissions = { 'mack:1' },
missions = {},
}
+6
View File
@@ -0,0 +1,6 @@
-- /testcontact's temporary NPWD fixture, not a permanent story contact.
-- Its 555 number is intentionally generated by the legacy debug command.
return {
displayName = 'Michael',
avatarUrl = 'https://fivemedia.howlerdesign.com/media/6801a2d6-659f-46e3-b0ee-028a9a4ac215.png',
}
+28
View File
@@ -0,0 +1,28 @@
-- Named mission locations copied from the live scripts. Legacy scripts still
-- own gameplay until each mission is migrated.
return {
crowbar = {
mission1Vehicle = { x = 911.40, y = 3589.50, z = 33.25, w = 91.36 },
mission1Return = { x = 1705.50, y = 3771.76, z = 34.49 },
mission2Vehicle = { x = 2684.30, y = 3292.18, z = 54.75, w = 240.63 },
harmonyRegister = { x = 549.33, y = 2669.57, z = 42.16, storeIndex = 13 },
mission5Package = { x = 1537.56, y = 1700.49, z = 109.68 },
mission5CrateZ = 108.6397,
mission5Ambush = {
{ model = 'rebel', spawn = { x = 1464.0, y = 1760.0, z = 108.0, w = 130.0 }, destination = { x = 1521.0, y = 1715.0, z = 109.0 } },
{ model = 'bodhi2', spawn = { x = 1448.0, y = 1774.0, z = 108.0, w = 130.0 }, destination = { x = 1513.0, y = 1723.0, z = 109.0 } },
},
},
mack = {
easyRiderBikeSpawns = {
{ x = 2360.90, y = 3114.91, z = 48.21, w = 239.14 },
{ x = 2360.28, y = 3113.93, z = 48.21, w = 245.54 },
{ x = 2359.64, y = 3112.59, z = 48.21, w = 265.43 },
{ x = 2359.45, y = 3111.15, z = 48.21, w = 246.34 },
{ x = 2361.34, y = 3117.44, z = 48.21, w = 234.06 },
},
easyRiderDetour = { x = 252.87, y = 2814.15, z = 43.19 },
easyRiderTrashCar = { x = 285.10, y = 2827.15, z = 42.89, w = 116.30 },
easyRiderVinnyDelivery = { x = 1373.86, y = 3615.83, z = 34.89 },
},
}
+62
View File
@@ -0,0 +1,62 @@
-- Mission IDs and debug phases are strings. Existing /prog still parses its
-- own arguments; this module is for future migration of that command.
local definitions = {
['crowbar:1'] = { contact = 'crowbar', number = 1, name = 'The Introduction', phases = { 'retrieve_vehicle', 'return_vehicle', 'complete' } },
['crowbar:2'] = { contact = 'crowbar', number = 2, name = 'Get Some Wheels', phases = { 'steal_vehicle', 'complete' } },
['crowbar:3'] = { contact = 'crowbar', number = 3, name = 'Quick Score', phases = { 'rob_register', 'lose_cops', 'complete' } },
['crowbar:4'] = { contact = 'crowbar', number = 4, name = 'Wash the Cash', phases = { 'meet_mack', 'meet_gabe', 'wash_cash', 'complete' } },
['crowbar:5'] = { contact = 'crowbar', number = 5, name = 'Buy In', phases = { 'get_package', 'escape_ambush', 'return_package', 'complete' } },
['mack:1'] = { contact = 'mack', number = 1, name = 'Easy Rider', phases = {} },
}
local M = { definitions = definitions }
function M.missionId(contact, number)
if type(contact) ~= 'string' or not contact:match('^[a-z][a-z_]*$')
or type(number) ~= 'number' or number % 1 ~= 0
or number < 1 or number > 999
then
return nil
end
return ('%s:%d'):format(contact, number)
end
function M.phaseId(missionId, phaseName)
if not definitions[missionId] or type(phaseName) ~= 'string'
or not phaseName:match('^[a-z][a-z_]*$')
then
return nil
end
return missionId .. ':' .. phaseName
end
function M.parseProgPhase(contact, notation)
if type(notation) ~= 'string' then return nil end
local missionText, phaseText = notation:match('^(%d+)%.(%d+)$')
if not missionText then return nil end
if #missionText > 3 or #phaseText > 3 then return nil end
local missionNumber, phaseNumber = tonumber(missionText), tonumber(phaseText)
if not missionNumber or not phaseNumber or missionNumber < 1 or phaseNumber < 1 then
return nil
end
local id = M.missionId(contact, missionNumber)
local definition = definitions[id]
local phaseName = definition and definition.phases[phaseNumber]
if not phaseName then return nil end
return {
missionId = id,
phaseId = M.phaseId(id, phaseName),
missionNumber = missionNumber,
phaseNumber = phaseNumber,
phaseName = phaseName,
notation = notation,
}
end
return M