ADD - introduce SkyRide taxi platform

This commit is contained in:
Leon.Schmidt
2026-08-09 19:10:36 +02:00
parent 550982f847
commit e3de5cd747
18 changed files with 6730 additions and 0 deletions
+97
View File
@@ -320,3 +320,100 @@ Config.Calendar = {
FutureSeconds = 5 * 365 * 24 * 60 * 60,
ReminderPollSeconds = 15,
}
Config.SkyRide = {
PaymentAccount = "bank",
Currency = "$",
DistanceUnit = "kilometer", -- kilometer or mile
QuoteLifetimeSeconds = 120,
HistoryLimit = 50,
ReadsPerMinute = 120,
ActionsPerMinute = 30,
QuotesPerMinute = 20,
RecoveryIntervalSeconds = 15,
PaymentPendingRecoverySeconds = 30,
PickupSelectionRadius = 150.0,
ArrivalRadius = 40.0,
CompletionRadius = 80.0,
MinimumDistanceMeters = 250,
MaximumDistanceMeters = 40000,
RouteDistanceMultiplier = 1.25,
AverageSpeedMetersPerSecond = 12.0,
DriverPayoutPercent = 85,
RatingCommentMaxLength = 300,
MaximumTip = 10000,
CustomFare = {
Enabled = true,
MinimumPrice = 5,
MaximumPrice = 100000,
MinimumCalculatedMultiplier = 0.5,
MaximumCalculatedMultiplier = 3.0,
},
DriverJobs = {
taxi = 0,
},
Services = {
{
Id = "taxi",
Seats = 4,
EtaMinutes = 3,
BaseFare = 12,
PricePerKilometer = 18,
PricePerMile = 29,
PricePerMinute = 1,
MinimumFare = 15,
},
{
Id = "comfort",
Seats = 4,
EtaMinutes = 5,
BaseFare = 16,
PricePerKilometer = 24,
PricePerMile = 39,
PricePerMinute = 2,
MinimumFare = 22,
},
{
Id = "xl",
Seats = 6,
EtaMinutes = 7,
BaseFare = 20,
PricePerKilometer = 30,
PricePerMile = 48,
PricePerMinute = 2,
MinimumFare = 28,
},
{
Id = "premium",
Seats = 4,
EtaMinutes = 8,
BaseFare = 28,
PricePerKilometer = 40,
PricePerMile = 64,
PricePerMinute = 3,
MinimumFare = 38,
},
},
QuickLocations = {
{
Id = "legion-square",
Label = "Legion Square",
Coords = { x = -265.1, y = -960.2, z = 31.2 },
},
{
Id = "diamond-casino",
Label = "Diamond Casino",
Coords = { x = 925.2, y = 46.4, z = 81.1 },
},
{
Id = "airport",
Label = "Los Santos Airport",
Coords = { x = -1037.7, y = -2737.8, z = 20.2 },
},
{
Id = "vinewood",
Label = "Vinewood",
Coords = { x = -594.4, y = -929.9, z = 23.9 },
},
},
}
+89
View File
@@ -396,6 +396,95 @@ Locales["en"] = {
request_failed = "The garage request failed.", default = "The garage request failed.",
},
},
skyride = {
name = "SkyRide", loading = "Loading SkyRide", unavailable = "SkyRide unavailable", tryAgain = "Try Again",
mode = { rider = "Ride", driver = "Drive" },
riderEyebrow = "Ride anywhere", whereTo = "Where to?", pickup = "Pickup", destination = "Destination",
currentLocation = "Current location", notSelected = "Not selected", location = "Choose a location",
chooseLocation = { pickup = "Set pickup", destination = "Choose destination" },
chooseOnMap = "Choose on map", chooseOnMapBody = "Tap a precise point on the Los Santos map.",
pickOnMap = { pickup = "Tap your pickup point", destination = "Tap your destination" },
tapMapHint = "Drag and zoom the map, then tap to place the pin.", mapPoint = "Map pin", map = "Map", useGps = "Use your live position",
zoomIn = "Zoom in", zoomOut = "Zoom out", resetMap = "Reset map view",
savedPlaces = "Saved places", savedPlace = "Saved place", quickDestinations = "Quick destinations",
quickLocations = { ["legion-square"] = "Legion Square", ["diamond-casino"] = "Diamond Casino", airport = "Los Santos Airport", vinewood = "Vinewood" },
viewRides = "View Ride Options", change = "Change", requestRide = "Request SkyRide",
serviceMeta = "{eta} min away · {seats} seats", distanceMeters = "{distance} m",
distanceKilometers = "{distance} km", distanceMiles = "{distance} mi", durationMinutes = "{minutes} min",
fare = "Trip fare", calculatedFare = "Calculated", customFare = "Own price",
calculatedFareBody = "Distance, time, and selected service included.",
customFareRange = "Allowed range: {minimum} to {maximum}", applyCustomFare = "Apply Own Price",
perKilometer = "{price} / km", perMile = "{price} / mi", rideStatus = "Ride status",
services = {
taxi = { name = "Taxi" }, comfort = { name = "Comfort" }, xl = { name = "XL" }, premium = { name = "Premium" },
},
ride = "SkyRide",
driver = {
online = "You are online", offline = "You are offline",
onlineBody = "New ride requests can reach you now.", offlineBody = "Go online when you are ready to drive.",
toggleStatus = "Toggle driver status", today = "Today", requests = "Nearby requests", accept = "Accept Ride",
noRequests = "No nearby requests", noRequestsBody = "New requests will appear here automatically.",
goOnline = "Go online to receive rides", goOnlineBody = "Switch your driver status on to see nearby requests.",
statusBody = {
searching = "A passenger is waiting for a driver.", accepted = "Navigate to the passenger pickup.",
driver_arriving = "You are heading to the passenger.", arrived = "Confirm when the passenger is in the vehicle.",
in_progress = "Follow the route to the destination.", completed = "The ride has been completed.",
cancelled = "This ride was cancelled.",
},
actions = { arrive = "I Have Arrived", start = "Start Ride", complete = "Complete Ride" },
},
passenger = "Passenger", vehicle = "Vehicle", navigate = "Navigate", call = "Call", message = "Message",
cancelRide = "Cancel Ride", cancelTitle = "Cancel this ride?",
cancelBody = "Your driver or passenger will be notified. A cancellation fee may apply.",
safetyNote = "Trip details are shared only while this ride is active.", waypointSet = "Route added to your GPS.",
status = {
searching = "Finding a driver", accepted = "Driver found", driver_arriving = "Driver en route",
arrived = "Driver arrived", in_progress = "On the way", completed = "Completed", cancelled = "Cancelled",
},
statusBody = {
searching = "We are matching you with a nearby driver.", accepted = "Your driver is preparing to pick you up.",
driver_arriving = "Your driver is on the way to your pickup.", arrived = "Your driver is waiting at the pickup point.",
in_progress = "You are on the way to your destination.", completed = "You have arrived at your destination.",
cancelled = "This ride is no longer active.",
},
navigation = "SkyRide navigation",
tabs = { home = "Home", rides = "Rides", activity = "Activity", messages = "Messages", profile = "Profile" },
rides = "Your Rides", ridesBody = "Review completed and cancelled rides.", noRides = "No rides yet",
noRidesBody = "Completed and cancelled rides will appear here.", activity = "Activity",
activityBody = "Recent trip updates and milestones.", noActivity = "No recent activity",
noActivityBody = "Ride updates will appear here.", messages = "Messages",
messagesBody = "Contact your active driver or passenger.", noMessages = "No active contact",
noMessagesBody = "Book or accept a ride to contact the other person.", openMessages = "Open Messages",
account = "Account", paymentMethod = "Payment Method", paymentMethods = { bank = "Bank", cash = "Cash", default = "Payment method" }, safety = "Safety Center",
safetyBody = "Help, trip sharing, and emergency information", driverMode = "Driver Mode",
driverModeBody = "Accept ride requests and track your earnings", completedRides = "Completed",
cancelledRides = "Cancelled", acceptance = "Acceptance", rating = "Rating", notAvailable = "N/A",
memberSince = "SkyRide member since {date}", rideComplete = "Ride Complete", rateRide = "Rate your ride",
rateRideBody = "How was your trip? Your feedback helps every rider.", ratingValue = "{rating} stars",
tip = "Add a tip", noTip = "No tip", comment = "Comment", commentPlaceholder = "Share an optional note...",
submitRating = "Submit Rating", notNow = "Not Now", ratingSaved = "Thanks for your feedback.",
errors = {
skyride_unavailable = "SkyRide is currently unavailable.",
invalid_location = "Choose a valid pickup and destination.", quote_not_found = "These ride options are no longer available.",
pickup_too_far = "Move closer to the selected pickup point.", invalid_route = "No suitable route was found for this ride.",
invalid_custom_fare = "Choose an own price inside the allowed range.",
quote_expired = "These ride options expired. Request a new quote.", active_ride_exists = "Finish or cancel your active ride first.",
invalid_driver_status = "That driver status is not available.", driver_not_eligible = "This account is not eligible for Driver Mode.",
driver_offline = "Go online before accepting a ride.", driver_unavailable = "That driver is no longer available.",
driver_vehicle_required = "Sit in the driver's seat of a vehicle before accepting a ride.",
ride_not_found = "This ride is no longer available.", not_ride_participant = "You are not part of this ride.",
invalid_ride_status = "This action is not available for the ride.", invalid_rating = "Choose a rating from one to five stars.",
driver_too_far = "Move closer to the required ride location.", passenger_too_far = "The passenger is not close enough to start.",
destination_too_far = "Move closer to the destination to complete the ride.", payout_failed = "The driver payout could not be completed.",
refund_failed = "The ride refund could not be completed.", invalid_cancel_reason = "Choose a valid cancellation reason.",
invalid_tip = "Choose a valid tip amount.", invalid_comment = "The rating comment is not valid.",
tip_failed = "The tip could not be processed.", insufficient_funds = "Your selected payment method has insufficient funds.",
rate_limited = "Please wait a moment before trying again.", request_failed = "SkyRide could not complete the request.",
device_not_open = "Open the phone again to use SkyRide.", device_not_owned = "This phone does not belong to this character.",
device_locked = "Unlock this phone to use SkyRide.",
default = "SkyRide could not complete the request.",
},
},
calculator = { name = "Calculator" },
snake = {
name = "Snake", backToMenu = "Back to game menu", board = "Snake game board",
+2
View File
@@ -27,6 +27,7 @@ client_scripts {
'source/client/animations.lua',
'source/client/camera.lua',
'source/client/garage.lua',
'source/client/skyride.lua',
'source/bridge/client/radio.lua',
'source/client/main.lua',
'source/client/radio.lua',
@@ -61,6 +62,7 @@ server_scripts {
'source/server/fliptok.lua',
'source/server/picstagram.lua',
'source/server/map.lua',
'source/server/skyride.lua',
'source/server/calendar.lua',
'source/server/radio.lua',
}
+120
View File
@@ -0,0 +1,120 @@
local server_callbacks = {
"skyride:bootstrap",
"skyride:quote",
"skyride:request",
"skyride:set-driver-status",
"skyride:accept",
"skyride:arrive",
"skyride:start",
"skyride:complete",
"skyride:cancel",
"skyride:rate",
"skyride:history",
}
local function normalize_person(person)
if type(person) ~= "table" then
return
end
if person.avatarUrl == false then
person.avatarUrl = json.null
end
end
local function normalize_ride(ride)
if type(ride) ~= "table" then
return
end
normalize_person(ride.passenger)
if ride.driver == false then
ride.driver = json.null
return
end
if type(ride.driver) ~= "table" then
return
end
normalize_person(ride.driver)
local vehicle = ride.driver.vehicle
local model_hash = type(vehicle) == "table" and tonumber(vehicle.model) or nil
if not model_hash then
return
end
local display_name = GetDisplayNameFromVehicleModel(model_hash)
local label = display_name and GetLabelText(display_name) or nil
if label and label ~= "NULL" and label ~= "CARNOTFOUND" then
vehicle.model = label
end
end
local function normalize_rides(rides)
if type(rides) ~= "table" then
return
end
for index = 1, #rides do
normalize_ride(rides[index])
end
end
local function normalize_state(data)
if type(data) ~= "table" then
return
end
if data.activeRide == false then
data.activeRide = json.null
else
normalize_ride(data.activeRide)
end
if data.pendingRating == false then
data.pendingRating = json.null
else
normalize_ride(data.pendingRating)
end
normalize_rides(data.availableRequests)
normalize_rides(data.history)
normalize_rides(data.items)
if type(data.profile) == "table" then
if data.profile.acceptanceRate == false then
data.profile.acceptanceRate = json.null
end
if data.profile.avatarUrl == false then
data.profile.avatarUrl = json.null
end
if data.profile.earningsToday == false then
data.profile.earningsToday = json.null
end
end
end
for index = 1, #server_callbacks do
local callback_name = server_callbacks[index]
RegisterNUICallback(callback_name, function(data, cb)
local result = Bridge.Callbacks.Trigger("sky_phone:" .. callback_name, data or {})
if not result then
cb({ success = false, error = "request_failed" })
return
end
if result.success and type(result.data) == "table" then
normalize_state(result.data)
end
cb(result)
end)
end
RegisterNUICallback("skyride:get-player-coords", function(_, cb)
local coords = GetEntityCoords(PlayerPedId())
cb({
success = true,
data = {
coords = {
x = coords.x,
y = coords.y,
z = coords.z,
},
},
})
end)
RegisterNetEvent("sky_phone:skyride:changed", function(data)
normalize_state(data)
SendNUIMessage({ type = "skyride:changed", data = data })
end)
+119
View File
@@ -1517,6 +1517,125 @@ local schema = {
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skyride_profiles",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{
name = "owner_identifier",
type = "VARCHAR(80) NOT NULL",
characterSet = "ascii",
collation = "ascii_bin",
},
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
},
primaryKey = "id",
uniqueKeys = {
{ name = "uniq_sky_phone_skyride_owner", columns = "(`owner_identifier`)" },
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
{
name = "sky_phone_skyride_rides",
columns = {
{ name = "id", type = "CHAR(36) NOT NULL", characterSet = "ascii", collation = "ascii_bin" },
{
name = "passenger_profile_id",
type = "CHAR(36) NOT NULL",
characterSet = "ascii",
collation = "ascii_bin",
},
{
name = "driver_profile_id",
type = "CHAR(36) NULL",
characterSet = "ascii",
collation = "ascii_bin",
},
{ name = "passenger_name", type = "VARCHAR(80) NOT NULL" },
{ name = "driver_name", type = "VARCHAR(80) NULL" },
{
name = "status",
type = "ENUM('payment_pending','searching','accepted','arrived','in_progress','completing','completed','cancelled') NOT NULL",
},
{
name = "service_class",
type = "ENUM('taxi','comfort','xl','premium') NOT NULL",
},
{ name = "pickup_label", type = "VARCHAR(80) NOT NULL" },
{ name = "pickup_x", type = "DECIMAL(10,3) NOT NULL" },
{ name = "pickup_y", type = "DECIMAL(10,3) NOT NULL" },
{ name = "pickup_z", type = "DECIMAL(10,3) NOT NULL" },
{ name = "destination_label", type = "VARCHAR(80) NOT NULL" },
{ name = "destination_x", type = "DECIMAL(10,3) NOT NULL" },
{ name = "destination_y", type = "DECIMAL(10,3) NOT NULL" },
{ name = "destination_z", type = "DECIMAL(10,3) NOT NULL" },
{ name = "distance_meters", type = "INT UNSIGNED NOT NULL" },
{ name = "duration_seconds", type = "INT UNSIGNED NOT NULL" },
{ name = "price", type = "INT UNSIGNED NOT NULL" },
{ name = "payout_amount", type = "INT UNSIGNED NOT NULL" },
{
name = "currency",
type = "VARCHAR(8) NOT NULL",
characterSet = "ascii",
collation = "ascii_general_ci",
},
{ name = "driver_vehicle_model", type = "VARCHAR(64) NULL" },
{ name = "driver_vehicle_color", type = "VARCHAR(64) NULL" },
{ name = "driver_vehicle_plate", type = "VARCHAR(16) NULL" },
{ name = "cancelled_by", type = "ENUM('passenger','driver','system') NULL" },
{ name = "cancel_reason", type = "VARCHAR(32) NULL" },
{ name = "passenger_rating", type = "TINYINT UNSIGNED NULL" },
{ name = "rating_comment", type = "VARCHAR(300) NULL" },
{ name = "tip_amount", type = "INT UNSIGNED NOT NULL DEFAULT 0" },
{
name = "refund_status",
type = "ENUM('none','pending','processing','completed') NOT NULL DEFAULT 'none'",
},
{
name = "tip_status",
type = "ENUM('none','processing','completed','failed') NOT NULL DEFAULT 'none'",
},
{ name = "created_at", type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP" },
{
name = "updated_at",
type = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",
},
{ name = "accepted_at", type = "DATETIME NULL" },
{ name = "arrived_at", type = "DATETIME NULL" },
{ name = "started_at", type = "DATETIME NULL" },
{ name = "completed_at", type = "DATETIME NULL" },
{ name = "cancelled_at", type = "DATETIME NULL" },
{ name = "refunded_at", type = "DATETIME NULL" },
{ name = "paid_out_at", type = "DATETIME NULL" },
},
primaryKey = "id",
indexes = {
{
name = "idx_sky_phone_skyride_passenger",
columns = "(`passenger_profile_id`, `status`, `updated_at`)",
},
{
name = "idx_sky_phone_skyride_driver",
columns = "(`driver_profile_id`, `status`, `updated_at`)",
},
{ name = "idx_sky_phone_skyride_requests", columns = "(`status`, `created_at`)" },
{
name = "idx_sky_phone_skyride_refunds",
columns = "(`refund_status`, `status`, `updated_at`)",
},
},
foreignKeys = {
{
column = "passenger_profile_id",
references = "`sky_phone_skyride_profiles` (`id`) ON DELETE CASCADE",
},
{
column = "driver_profile_id",
references = "`sky_phone_skyride_profiles` (`id`) ON DELETE SET NULL",
},
},
tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
},
}
Bridge.Database.Migrate("sky_phone", schema)
File diff suppressed because it is too large Load Diff
+57
View File
@@ -614,3 +614,60 @@ CREATE TABLE IF NOT EXISTS `sky_phone_picstagram_moderation_audit` (
KEY `idx_sky_phone_picstagram_audit_target` (`target_type`,`target_id`,`created_at`),
FOREIGN KEY (`report_id`) REFERENCES `sky_phone_picstagram_reports` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skyride_profiles` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`owner_identifier` VARCHAR(80) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_sky_phone_skyride_owner` (`owner_identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `sky_phone_skyride_rides` (
`id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`passenger_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
`driver_profile_id` CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL,
`passenger_name` VARCHAR(80) NOT NULL,
`driver_name` VARCHAR(80) NULL,
`status` ENUM('payment_pending','searching','accepted','arrived','in_progress','completing','completed','cancelled') NOT NULL,
`service_class` ENUM('taxi','comfort','xl','premium') NOT NULL,
`pickup_label` VARCHAR(80) NOT NULL,
`pickup_x` DECIMAL(10,3) NOT NULL,
`pickup_y` DECIMAL(10,3) NOT NULL,
`pickup_z` DECIMAL(10,3) NOT NULL,
`destination_label` VARCHAR(80) NOT NULL,
`destination_x` DECIMAL(10,3) NOT NULL,
`destination_y` DECIMAL(10,3) NOT NULL,
`destination_z` DECIMAL(10,3) NOT NULL,
`distance_meters` INT UNSIGNED NOT NULL,
`duration_seconds` INT UNSIGNED NOT NULL,
`price` INT UNSIGNED NOT NULL,
`payout_amount` INT UNSIGNED NOT NULL,
`currency` VARCHAR(8) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`driver_vehicle_model` VARCHAR(64) NULL,
`driver_vehicle_color` VARCHAR(64) NULL,
`driver_vehicle_plate` VARCHAR(16) NULL,
`cancelled_by` ENUM('passenger','driver','system') NULL,
`cancel_reason` VARCHAR(32) NULL,
`passenger_rating` TINYINT UNSIGNED NULL,
`rating_comment` VARCHAR(300) NULL,
`tip_amount` INT UNSIGNED NOT NULL DEFAULT 0,
`refund_status` ENUM('none','pending','processing','completed') NOT NULL DEFAULT 'none',
`tip_status` ENUM('none','processing','completed','failed') NOT NULL DEFAULT 'none',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`accepted_at` DATETIME NULL,
`arrived_at` DATETIME NULL,
`started_at` DATETIME NULL,
`completed_at` DATETIME NULL,
`cancelled_at` DATETIME NULL,
`refunded_at` DATETIME NULL,
`paid_out_at` DATETIME NULL,
PRIMARY KEY (`id`),
KEY `idx_sky_phone_skyride_passenger` (`passenger_profile_id`,`status`,`updated_at`),
KEY `idx_sky_phone_skyride_driver` (`driver_profile_id`,`status`,`updated_at`),
KEY `idx_sky_phone_skyride_requests` (`status`,`created_at`),
KEY `idx_sky_phone_skyride_refunds` (`refund_status`,`status`,`updated_at`),
FOREIGN KEY (`passenger_profile_id`) REFERENCES `sky_phone_skyride_profiles` (`id`) ON DELETE CASCADE,
FOREIGN KEY (`driver_profile_id`) REFERENCES `sky_phone_skyride_profiles` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;