diff --git a/FiveM/nyx_core/server/tests.lua b/FiveM/nyx_core/server/tests.lua index b05bd78..ba5a707 100644 --- a/FiveM/nyx_core/server/tests.lua +++ b/FiveM/nyx_core/server/tests.lua @@ -198,6 +198,44 @@ function Nyx.Tests.TestRPC() return passes == total end +-- 61. Test Highway Road Degradation & Public Works Resurfacing Repair Contracts +function Nyx.Tests.TestHighwayDegradationAndTransitRepair() + print("^3[Nyx:Tests] Running Highway Road Degradation & Transit Repair Suite...^7") + local passes = 0 + local total = 4 + + local R = Nyx.Vehicles.RoadDegradation + if not R then + print("^1[Nyx:Test:FAIL]^7 Nyx.Vehicles.RoadDegradation shared module not loaded in environment") + return false + end + + -- Sub-test A: Fourth-Power Axle Damage Factor + local carFactor = R.CalculateAxleDamageFactor(2000.0, 2) + local heavyFactor = R.CalculateAxleDamageFactor(20000.0, 5) + local testA_ok = (carFactor == 0.06 or carFactor == 0.06) and (heavyFactor > 1.0) + if assertTest(testA_ok == true, "4th-Power Axle Damage Factor Calculation", "Expected low passenger car factor and high semi factor", string.format("Car: %.2f, Semi: %.2f", carFactor, heavyFactor)) then passes = passes + 1 end + + -- Sub-test B: Road Quality Decay + local q1 = R.CalculateRoadQualityDecay(0, 100.0, 0) + local q2 = R.CalculateRoadQualityDecay(100, 100.0, 10) + local testB_ok = (q1 == 100.0) and (q2 < 90.0) + if assertTest(testB_ok == true, "Road Quality Decay Rate Math", "Expected 100% initial and degraded quality after 100 passages", string.format("Q1: %.2f%%, Q2: %.2f%%", q1, q2)) then passes = passes + 1 end + + -- Sub-test C: Vehicle Handling Penalty Modifiers + local hOpt = R.CalculateHandlingPenalty(90.0) + local hDeg = R.CalculateHandlingPenalty(50.0) + local testC_ok = (hOpt.status == "OPTIMAL" and hOpt.gripMultiplier == 1.0) and (hDeg.status == "DEGRADED" and hDeg.gripMultiplier < 1.0) + if assertTest(testC_ok == true, "Handling & Traction Penalty Modifiers", "Expected OPTIMAL status for 90% and DEGRADED for 50%", string.format("Opt: %s (x%.2f), Deg: %s (x%.2f)", hOpt.status, hOpt.gripMultiplier, hDeg.status, hDeg.gripMultiplier)) then passes = passes + 1 end + + -- Sub-test D: Repair Cost & Asphalt Requirements + local rep = R.CalculateRepairCostAndMaterials(1000.0, 20.0) + local testD_ok = (rep.requiredAsphaltKg == 2000.0) and (rep.treasuryPayout == 5000.0) + if assertTest(testD_ok == true, "Public Works Asphalt & Repair Cost Math", "Expected 2000kg asphalt and $5000.00 payout for 20% loss on 1000m segment", string.format("Asphalt: %.1fkg, Payout: $%.2f", rep.requiredAsphaltKg, rep.treasuryPayout)) then passes = passes + 1 end + + return passes == total +end + -- 2. Test Inventory Mutex Concurrency, Dijkstra Dual Locks & Zero-Duplication function Nyx.Tests.TestInventoryConcurrency() @@ -12640,6 +12678,7 @@ function Nyx.Tests.RunAll() local t59 = Nyx.Tests.TestIndustrialRefiningAndFunctionalPartsManufacturing() local t60 = Nyx.Tests.TestJudicialBailBondsSuretyEscrowAndFugitiveRecovery() local t61 = Nyx.Tests.TestMunicipalTollsAndWeighStations() + local t62 = Nyx.Tests.TestHighwayDegradationAndTransitRepair() print("^5--------------------------------------------------^7") local suites1to50Passed = false @@ -12647,7 +12686,7 @@ function Nyx.Tests.RunAll() suites1to50Passed = true end - if suites1to50Passed and t51 and t52 and t53 and t54 and t55 and t56 and t57 and t58 and t59 and t60 and t61 then + if suites1to50Passed and t51 and t52 and t53 and t54 and t55 and t56 and t57 and t58 and t59 and t60 and t61 and t62 then print("^2>>> ALL NYXFRAMEWORK INTEGRATION TESTS PASSED! <<<^7") else print("^1>>> SOME NYXFRAMEWORK TESTS FAILED! CHECK LOGS. <<<^7") diff --git a/FiveM/nyx_core/sql/migrations/067_highway_degradation_and_transit_repair.sql b/FiveM/nyx_core/sql/migrations/067_highway_degradation_and_transit_repair.sql new file mode 100644 index 0000000..87f455a --- /dev/null +++ b/FiveM/nyx_core/sql/migrations/067_highway_degradation_and_transit_repair.sql @@ -0,0 +1,55 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_services & nyx_treasury +-- Migration: 067_highway_degradation_and_transit_repair.sql +-- Description: Commercial highway infrastructure degradation, heavy vehicle axle stress +-- pavement wear modeling, municipal road quality tracking, and +-- public works asphalt/resurfacing repair contracts. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create highway segments table +CREATE TABLE IF NOT EXISTS `nyx_highway_segments` ( + `segment_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `highway_name` VARCHAR(128) NOT NULL, + `start_coords` JSON NOT NULL, + `end_coords` JSON NOT NULL, + `length_meters` DECIMAL(10, 2) NOT NULL DEFAULT 1000.00, + `quality_pct` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_heavy_axle_passages` INT UNSIGNED NOT NULL DEFAULT 0, + `status` ENUM('OPTIMAL', 'WEAR_WARNING', 'DEGRADED', 'CRITICAL') NOT NULL DEFAULT 'OPTIMAL', + `last_repaired_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_seg_highway` (`highway_name`), + INDEX `idx_seg_quality` (`quality_pct`), + INDEX `idx_seg_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create municipal road repair contracts table +CREATE TABLE IF NOT EXISTS `nyx_road_repair_contracts` ( + `contract_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `segment_id` VARCHAR(64) NOT NULL, + `required_asphalt_kg` DECIMAL(10, 2) NOT NULL DEFAULT 1000.00, + `treasury_payout` DECIMAL(10, 2) NOT NULL DEFAULT 2500.00, + `assigned_faction_id` VARCHAR(64) NULL DEFAULT NULL, + `status` ENUM('OPEN', 'IN_PROGRESS', 'COMPLETED', 'EXPIRED') NOT NULL DEFAULT 'OPEN', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `completed_at` TIMESTAMP NULL DEFAULT NULL, + FOREIGN KEY (`segment_id`) REFERENCES `nyx_highway_segments` (`segment_id`) ON DELETE CASCADE, + INDEX `idx_repair_segment` (`segment_id`), + INDEX `idx_repair_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed major municipal highway transit corridors +INSERT INTO `nyx_highway_segments` + (`segment_id`, `highway_name`, `start_coords`, `end_coords`, `length_meters`, `quality_pct`, `total_heavy_axle_passages`, `status`) +VALUES + ('seg_del_perro_fwy_1', 'Del Perro Freeway - Westbound Corridor', '{"x": -1200.0, "y": -800.0, "z": 15.0}', '{"x": 400.0, "y": -800.0, "z": 25.0}', 1600.00, 100.00, 0, 'OPTIMAL'), + ('seg_la_puerta_fwy_1', 'La Puerta Freeway - Port Access Connector', '{"x": -200.0, "y": -2000.0, "z": 10.0}', '{"x": 400.0, "y": -1500.0, "z": 20.0}', 1200.00, 95.00, 12, 'OPTIMAL'), + ('seg_palomino_fwy_1', 'Palomino Freeway - East Industrial Bypass', '{"x": 1200.0, "y": -1000.0, "z": 30.0}', '{"x": 2200.0, "y": 500.0, "z": 45.0}', 2500.00, 82.00, 48, 'OPTIMAL'), + ('seg_senora_fwy_1', 'Senora Freeway - Desert Logistics Route', '{"x": 1500.0, "y": 2500.0, "z": 50.0}', '{"x": 2500.0, "y": 4500.0, "z": 60.0}', 3200.00, 68.00, 120, 'WEAR_WARNING') +ON DUPLICATE KEY UPDATE `highway_name` = VALUES(`highway_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_core/sql/migrations/index.json b/FiveM/nyx_core/sql/migrations/index.json index e5081f1..4697762 100644 --- a/FiveM/nyx_core/sql/migrations/index.json +++ b/FiveM/nyx_core/sql/migrations/index.json @@ -64,5 +64,6 @@ "063_pharmaceutical_refining_and_player_chemical_labs.sql", "064_industrial_refining_and_functional_parts_manufacturing.sql", "065_judicial_bail_bonds_surety_escrow_and_fugitive_recovery.sql", - "066_municipal_toll_booths_and_commercial_weigh_stations.sql" + "066_municipal_toll_booths_and_commercial_weigh_stations.sql", + "067_highway_degradation_and_transit_repair.sql" ] diff --git a/FiveM/nyx_services/sql/migrations/067_highway_degradation_and_transit_repair.sql b/FiveM/nyx_services/sql/migrations/067_highway_degradation_and_transit_repair.sql new file mode 100644 index 0000000..87f455a --- /dev/null +++ b/FiveM/nyx_services/sql/migrations/067_highway_degradation_and_transit_repair.sql @@ -0,0 +1,55 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_services & nyx_treasury +-- Migration: 067_highway_degradation_and_transit_repair.sql +-- Description: Commercial highway infrastructure degradation, heavy vehicle axle stress +-- pavement wear modeling, municipal road quality tracking, and +-- public works asphalt/resurfacing repair contracts. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create highway segments table +CREATE TABLE IF NOT EXISTS `nyx_highway_segments` ( + `segment_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `highway_name` VARCHAR(128) NOT NULL, + `start_coords` JSON NOT NULL, + `end_coords` JSON NOT NULL, + `length_meters` DECIMAL(10, 2) NOT NULL DEFAULT 1000.00, + `quality_pct` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_heavy_axle_passages` INT UNSIGNED NOT NULL DEFAULT 0, + `status` ENUM('OPTIMAL', 'WEAR_WARNING', 'DEGRADED', 'CRITICAL') NOT NULL DEFAULT 'OPTIMAL', + `last_repaired_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_seg_highway` (`highway_name`), + INDEX `idx_seg_quality` (`quality_pct`), + INDEX `idx_seg_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create municipal road repair contracts table +CREATE TABLE IF NOT EXISTS `nyx_road_repair_contracts` ( + `contract_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `segment_id` VARCHAR(64) NOT NULL, + `required_asphalt_kg` DECIMAL(10, 2) NOT NULL DEFAULT 1000.00, + `treasury_payout` DECIMAL(10, 2) NOT NULL DEFAULT 2500.00, + `assigned_faction_id` VARCHAR(64) NULL DEFAULT NULL, + `status` ENUM('OPEN', 'IN_PROGRESS', 'COMPLETED', 'EXPIRED') NOT NULL DEFAULT 'OPEN', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `completed_at` TIMESTAMP NULL DEFAULT NULL, + FOREIGN KEY (`segment_id`) REFERENCES `nyx_highway_segments` (`segment_id`) ON DELETE CASCADE, + INDEX `idx_repair_segment` (`segment_id`), + INDEX `idx_repair_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed major municipal highway transit corridors +INSERT INTO `nyx_highway_segments` + (`segment_id`, `highway_name`, `start_coords`, `end_coords`, `length_meters`, `quality_pct`, `total_heavy_axle_passages`, `status`) +VALUES + ('seg_del_perro_fwy_1', 'Del Perro Freeway - Westbound Corridor', '{"x": -1200.0, "y": -800.0, "z": 15.0}', '{"x": 400.0, "y": -800.0, "z": 25.0}', 1600.00, 100.00, 0, 'OPTIMAL'), + ('seg_la_puerta_fwy_1', 'La Puerta Freeway - Port Access Connector', '{"x": -200.0, "y": -2000.0, "z": 10.0}', '{"x": 400.0, "y": -1500.0, "z": 20.0}', 1200.00, 95.00, 12, 'OPTIMAL'), + ('seg_palomino_fwy_1', 'Palomino Freeway - East Industrial Bypass', '{"x": 1200.0, "y": -1000.0, "z": 30.0}', '{"x": 2200.0, "y": 500.0, "z": 45.0}', 2500.00, 82.00, 48, 'OPTIMAL'), + ('seg_senora_fwy_1', 'Senora Freeway - Desert Logistics Route', '{"x": 1500.0, "y": 2500.0, "z": 50.0}', '{"x": 2500.0, "y": 4500.0, "z": 60.0}', 3200.00, 68.00, 120, 'WEAR_WARNING') +ON DUPLICATE KEY UPDATE `highway_name` = VALUES(`highway_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_treasury/sql/migrations/067_highway_degradation_and_transit_repair.sql b/FiveM/nyx_treasury/sql/migrations/067_highway_degradation_and_transit_repair.sql new file mode 100644 index 0000000..87f455a --- /dev/null +++ b/FiveM/nyx_treasury/sql/migrations/067_highway_degradation_and_transit_repair.sql @@ -0,0 +1,55 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_services & nyx_treasury +-- Migration: 067_highway_degradation_and_transit_repair.sql +-- Description: Commercial highway infrastructure degradation, heavy vehicle axle stress +-- pavement wear modeling, municipal road quality tracking, and +-- public works asphalt/resurfacing repair contracts. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create highway segments table +CREATE TABLE IF NOT EXISTS `nyx_highway_segments` ( + `segment_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `highway_name` VARCHAR(128) NOT NULL, + `start_coords` JSON NOT NULL, + `end_coords` JSON NOT NULL, + `length_meters` DECIMAL(10, 2) NOT NULL DEFAULT 1000.00, + `quality_pct` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_heavy_axle_passages` INT UNSIGNED NOT NULL DEFAULT 0, + `status` ENUM('OPTIMAL', 'WEAR_WARNING', 'DEGRADED', 'CRITICAL') NOT NULL DEFAULT 'OPTIMAL', + `last_repaired_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_seg_highway` (`highway_name`), + INDEX `idx_seg_quality` (`quality_pct`), + INDEX `idx_seg_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create municipal road repair contracts table +CREATE TABLE IF NOT EXISTS `nyx_road_repair_contracts` ( + `contract_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `segment_id` VARCHAR(64) NOT NULL, + `required_asphalt_kg` DECIMAL(10, 2) NOT NULL DEFAULT 1000.00, + `treasury_payout` DECIMAL(10, 2) NOT NULL DEFAULT 2500.00, + `assigned_faction_id` VARCHAR(64) NULL DEFAULT NULL, + `status` ENUM('OPEN', 'IN_PROGRESS', 'COMPLETED', 'EXPIRED') NOT NULL DEFAULT 'OPEN', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `completed_at` TIMESTAMP NULL DEFAULT NULL, + FOREIGN KEY (`segment_id`) REFERENCES `nyx_highway_segments` (`segment_id`) ON DELETE CASCADE, + INDEX `idx_repair_segment` (`segment_id`), + INDEX `idx_repair_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed major municipal highway transit corridors +INSERT INTO `nyx_highway_segments` + (`segment_id`, `highway_name`, `start_coords`, `end_coords`, `length_meters`, `quality_pct`, `total_heavy_axle_passages`, `status`) +VALUES + ('seg_del_perro_fwy_1', 'Del Perro Freeway - Westbound Corridor', '{"x": -1200.0, "y": -800.0, "z": 15.0}', '{"x": 400.0, "y": -800.0, "z": 25.0}', 1600.00, 100.00, 0, 'OPTIMAL'), + ('seg_la_puerta_fwy_1', 'La Puerta Freeway - Port Access Connector', '{"x": -200.0, "y": -2000.0, "z": 10.0}', '{"x": 400.0, "y": -1500.0, "z": 20.0}', 1200.00, 95.00, 12, 'OPTIMAL'), + ('seg_palomino_fwy_1', 'Palomino Freeway - East Industrial Bypass', '{"x": 1200.0, "y": -1000.0, "z": 30.0}', '{"x": 2200.0, "y": 500.0, "z": 45.0}', 2500.00, 82.00, 48, 'OPTIMAL'), + ('seg_senora_fwy_1', 'Senora Freeway - Desert Logistics Route', '{"x": 1500.0, "y": 2500.0, "z": 50.0}', '{"x": 2500.0, "y": 4500.0, "z": 60.0}', 3200.00, 68.00, 120, 'WEAR_WARNING') +ON DUPLICATE KEY UPDATE `highway_name` = VALUES(`highway_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_ui/web/js/app.js b/FiveM/nyx_ui/web/js/app.js index f5a63cf..a2704e3 100644 --- a/FiveM/nyx_ui/web/js/app.js +++ b/FiveM/nyx_ui/web/js/app.js @@ -6634,6 +6634,13 @@ window.addEventListener('message', (event) => { setPlacementActive(false); } + // Highway Road Condition Telemetry + else if (action === 'updateHighwayRoadCondition' || action === 'HIGHWAY_ROAD_UPDATE') { + if (packet.data) { + NyxState.highwayRoadCondition = packet.data; + } + } + // HUD Telemetry else if (action === 'HUD_UPDATE' || action === 'UPDATE_STATUS') { updateHudStatus(packet.payload); diff --git a/FiveM/nyx_vehicles/client/road_degradation.lua b/FiveM/nyx_vehicles/client/road_degradation.lua new file mode 100644 index 0000000..3f9868d --- /dev/null +++ b/FiveM/nyx_vehicles/client/road_degradation.lua @@ -0,0 +1,70 @@ +--[[ + Nyxframework: nyx_vehicles + File: client/road_degradation.lua + Description: Client-side spatial sampling for highway segments, dynamic vehicle handling/traction + degradation when driving on degraded road segments, and NUI event dispatching. +]] + +Nyx = Nyx or {} +Nyx.Vehicles = Nyx.Vehicles or {} +Nyx.Vehicles.RoadDegradationClient = Nyx.Vehicles.RoadDegradationClient or {} + +local currentSegmentId = nil +local currentHandlingModifier = { gripMultiplier = 1.0, suspensionWearMultiplier = 1.0, status = "OPTIMAL" } + +-- Sample known highway segments +local SegmentCoords = { + { id = "seg_del_perro_fwy_1", x = -400.0, y = -800.0, z = 20.0, radius = 500.0 }, + { id = "seg_la_puerta_fwy_1", x = 100.0, y = -1750.0, z = 15.0, radius = 400.0 }, + { id = "seg_palomino_fwy_1", x = 1700.0, y = -250.0, z = 35.0, radius = 600.0 }, + { id = "seg_senora_fwy_1", x = 2000.0, y = 3500.0, z = 55.0, radius = 800.0 }, +} + +CreateThread(function() + while true do + Wait(2000) + local ped = PlayerPedId() + if IsPedInAnyVehicle(ped, false) then + local veh = GetVehiclePedIsIn(ped, false) + if GetPedInVehicleSeat(veh, -1) == ped then + local pCoords = GetEntityCoords(veh) + local matched = nil + + for _, seg in ipairs(SegmentCoords) do + local dist = #(pCoords - vector3(seg.x, seg.y, seg.z)) + if dist <= seg.radius then + matched = seg.id + break + end + end + + if matched ~= currentSegmentId then + currentSegmentId = matched + if currentSegmentId then + -- Query segment condition from server or trigger pass + TriggerServerEvent("nyx:vehicles:server:recordHighwayPass", currentSegmentId, 2500.0, 2) + end + end + end + else + currentSegmentId = nil + end + end +end) + +RegisterNetEvent("nyx:vehicles:client:highwayPassRecorded", function(payload) + if not payload or not payload.success then return end + local eval = Nyx.Vehicles.RoadDegradation.CalculateHandlingPenalty(payload.newQuality) + currentHandlingModifier = eval + + -- Send NUI notification for UI dashboard overlay + SendNUIMessage({ + action = "updateHighwayRoadCondition", + data = { + segmentId = payload.segmentId, + qualityPct = payload.newQuality, + status = payload.status, + gripMultiplier = eval.gripMultiplier + } + }) +end) diff --git a/FiveM/nyx_vehicles/fxmanifest.lua b/FiveM/nyx_vehicles/fxmanifest.lua index 51aa8f5..5ab503d 100644 --- a/FiveM/nyx_vehicles/fxmanifest.lua +++ b/FiveM/nyx_vehicles/fxmanifest.lua @@ -22,7 +22,8 @@ shared_scripts { 'shared/diagnostics.lua', 'shared/fuel.lua', 'shared/keys.lua', - 'shared/tolls_and_weighing.lua' + 'shared/tolls_and_weighing.lua', + 'shared/road_degradation.lua' } server_scripts { @@ -35,6 +36,7 @@ server_scripts { 'server/diagnostics.lua', 'server/fuel.lua', 'server/tolls_and_weighing.lua', + 'server/road_degradation.lua', 'server/spawner.lua', 'server/main.lua', 'server/exports.lua' @@ -50,6 +52,7 @@ client_scripts { 'client/fuel.lua', 'client/speedometer.lua', 'client/tolls_and_weighing.lua', + 'client/road_degradation.lua', 'client/main.lua', 'client/exports.lua' } diff --git a/FiveM/nyx_vehicles/server/exports.lua b/FiveM/nyx_vehicles/server/exports.lua index 805dc7a..5be793d 100644 --- a/FiveM/nyx_vehicles/server/exports.lua +++ b/FiveM/nyx_vehicles/server/exports.lua @@ -160,3 +160,20 @@ end) exports("CalculateOverweightFine", function(grossWeightKg, maxAllowedKg, baseFinePerExcessKg) return Nyx.TollsAndWeighing.CalculateOverweightFine(grossWeightKg, maxAllowedKg, baseFinePerExcessKg) end) + +-- Highway Degradation & Transit Repairs +exports("RecordHighwayTransitPass", function(segmentId, grossWeightKg, axleCount) + return Nyx.Vehicles.RoadDegradationServer.RecordTransitPass(segmentId, grossWeightKg, axleCount) +end) + +exports("GetHighwaySegmentCondition", function(segmentId) + return Nyx.Vehicles.RoadDegradationServer.GetSegmentCondition(segmentId) +end) + +exports("SubmitRoadRepairBatch", function(segmentId, asphaltKg, factionId) + return Nyx.Vehicles.RoadDegradationServer.SubmitRoadRepair(segmentId, asphaltKg, factionId) +end) + +exports("CalculateAxleDamageFactor", function(grossWeightKg, axleCount) + return Nyx.Vehicles.RoadDegradation.CalculateAxleDamageFactor(grossWeightKg, axleCount) +end) diff --git a/FiveM/nyx_vehicles/server/road_degradation.lua b/FiveM/nyx_vehicles/server/road_degradation.lua new file mode 100644 index 0000000..688ff63 --- /dev/null +++ b/FiveM/nyx_vehicles/server/road_degradation.lua @@ -0,0 +1,160 @@ +--[[ + Nyxframework: nyx_vehicles + File: server/road_degradation.lua + Description: Authoritative server state management for highway road segment degradation, + transit pass recording, municipal public works repair contracts, + and treasury/services integration. +]] + +Nyx = Nyx or {} +Nyx.Vehicles = Nyx.Vehicles or {} +Nyx.Vehicles.RoadDegradationServer = Nyx.Vehicles.RoadDegradationServer or {} + +local RoadSegments = {} + +-- Load active highway segments on resource start +local function loadHighwaySegments() + MySQL.query("SELECT * FROM `nyx_highway_segments`", {}, function(rows) + if rows then + for _, row in ipairs(rows) do + RoadSegments[row.segment_id] = { + segment_id = row.segment_id, + highway_name = row.highway_name, + length_meters = tonumber(row.length_meters) or 1000.0, + quality_pct = tonumber(row.quality_pct) or 100.0, + total_heavy_axle_passages = tonumber(row.total_heavy_axle_passages) or 0, + status = row.status or "OPTIMAL" + } + end + print(string.format("^2[Nyx:Vehicles] Loaded %d highway road segments.^7", #rows)) + end + end) +end + +CreateThread(function() + while GetResourceState("oxmysql") ~= "started" do + Wait(100) + end + loadHighwaySegments() +end) + +--- Records a vehicle transit passage over a highway segment and degrades quality +--- @param segmentId string +--- @param grossWeightKg number +--- @param axleCount number +--- @return table Result payload { success = boolean, newQuality = number, damageFactor = number, status = string } +function Nyx.Vehicles.RoadDegradationServer.RecordTransitPass(segmentId, grossWeightKg, axleCount) + if not segmentId or not RoadSegments[segmentId] then + return { success = false, message = "INVALID_SEGMENT_ID" } + end + + grossWeightKg = tonumber(grossWeightKg) or 1500.0 + axleCount = tonumber(axleCount) or 2 + + local damageFactor = Nyx.Vehicles.RoadDegradation.CalculateAxleDamageFactor(grossWeightKg, axleCount) + local seg = RoadSegments[segmentId] + + -- Heavy vehicles (damageFactor >= 1.5) accumulate heavy axle passages + if damageFactor >= 1.5 then + seg.total_heavy_axle_passages = seg.total_heavy_axle_passages + 1 + end + + -- Deduct quality proportionally + local qualityLoss = damageFactor * 0.05 + seg.quality_pct = math.max(0.00, seg.quality_pct - qualityLoss) + + local eval = Nyx.Vehicles.RoadDegradation.CalculateHandlingPenalty(seg.quality_pct) + seg.status = eval.status + + -- Async DB sync + MySQL.update("UPDATE `nyx_highway_segments` SET `quality_pct` = ?, `total_heavy_axle_passages` = ?, `status` = ? WHERE `segment_id` = ?", { + seg.quality_pct, seg.total_heavy_axle_passages, seg.status, segmentId + }) + + -- Auto-generate repair contract if quality drops below 60% + if seg.quality_pct < 60.0 then + local repairInfo = Nyx.Vehicles.RoadDegradation.CalculateRepairCostAndMaterials(seg.length_meters, 100.0 - seg.quality_pct) + local contractId = "contract_" .. segmentId + MySQL.query("INSERT INTO `nyx_road_repair_contracts` (`contract_id`, `segment_id`, `required_asphalt_kg`, `treasury_payout`, `status`) VALUES (?, ?, ?, ?, 'OPEN') ON DUPLICATE KEY UPDATE `required_asphalt_kg` = VALUES(`required_asphalt_kg`), `treasury_payout` = VALUES(`treasury_payout`)", { + contractId, segmentId, repairInfo.requiredAsphaltKg, repairInfo.treasuryPayout + }) + end + + return { + success = true, + segmentId = segmentId, + newQuality = seg.quality_pct, + damageFactor = damageFactor, + status = seg.status + } +end + +--- Retrieves current highway segment condition +--- @param segmentId string +--- @return table +function Nyx.Vehicles.RoadDegradationServer.GetSegmentCondition(segmentId) + if not segmentId or not RoadSegments[segmentId] then + return { quality_pct = 100.0, status = "OPTIMAL", total_heavy_axle_passages = 0 } + end + return RoadSegments[segmentId] +end + +--- Submits a road repair batch with asphalt to restore segment quality +--- @param segmentId string +--- @param asphaltKg number +--- @param factionId string +--- @return table Result payload { success = boolean, restoredQuality = number, payout = number } +function Nyx.Vehicles.RoadDegradationServer.SubmitRoadRepair(segmentId, asphaltKg, factionId) + if not segmentId or not RoadSegments[segmentId] then + return { success = false, message = "INVALID_SEGMENT" } + end + + asphaltKg = math.max(0.0, tonumber(asphaltKg) or 0.0) + local seg = RoadSegments[segmentId] + local qualityLoss = 100.0 - seg.quality_pct + + if qualityLoss <= 0 then + return { success = false, message = "ROAD_ALREADY_OPTIMAL" } + end + + local repairInfo = Nyx.Vehicles.RoadDegradation.CalculateRepairCostAndMaterials(seg.length_meters, qualityLoss) + local asphaltNeeded = repairInfo.requiredAsphaltKg + + local restoreRatio = asphaltNeeded > 0 and math.min(1.0, asphaltKg / asphaltNeeded) or 1.0 + local restoredPoints = qualityLoss * restoreRatio + seg.quality_pct = math.min(100.0, seg.quality_pct + restoredPoints) + + local eval = Nyx.Vehicles.RoadDegradation.CalculateHandlingPenalty(seg.quality_pct) + seg.status = eval.status + + local payout = repairInfo.treasuryPayout * restoreRatio + + MySQL.update("UPDATE `nyx_highway_segments` SET `quality_pct` = ?, `status` = ?, `last_repaired_at` = CURRENT_TIMESTAMP WHERE `segment_id` = ?", { + seg.quality_pct, seg.status, segmentId + }) + + if seg.quality_pct >= 95.0 then + MySQL.update("UPDATE `nyx_road_repair_contracts` SET `status` = 'COMPLETED', `completed_at` = CURRENT_TIMESTAMP WHERE `segment_id` = ?", { segmentId }) + end + + return { + success = true, + segmentId = segmentId, + restoredQuality = seg.quality_pct, + payout = payout, + status = seg.status + } +end + +-- Server Events +RegisterNetEvent("nyx:vehicles:server:recordHighwayPass", function(segmentId, grossWeightKg, axleCount) + local src = source + local res = Nyx.Vehicles.RoadDegradationServer.RecordTransitPass(segmentId, grossWeightKg, axleCount) + TriggerClientEvent("nyx:vehicles:client:highwayPassRecorded", src, res) +end) + +RegisterNetEvent("nyx:vehicles:server:submitRoadRepair", function(segmentId, asphaltKg, factionId) + local src = source + local res = Nyx.Vehicles.RoadDegradationServer.SubmitRoadRepair(segmentId, asphaltKg, factionId) + TriggerClientEvent("nyx:vehicles:client:roadRepairSubmitted", src, res) +end) diff --git a/FiveM/nyx_vehicles/shared/road_degradation.lua b/FiveM/nyx_vehicles/shared/road_degradation.lua new file mode 100644 index 0000000..e349411 --- /dev/null +++ b/FiveM/nyx_vehicles/shared/road_degradation.lua @@ -0,0 +1,105 @@ +--[[ + Nyxframework: nyx_vehicles + File: shared/road_degradation.lua + Description: Deterministic pure Lua formulas for highway pavement stress (4th-power law approximation), + road quality decay, vehicle handling/traction penalties on degraded roads, + and public works asphalt/resurfacing repair requirements. +]] + +Nyx = Nyx or {} +Nyx.Vehicles = Nyx.Vehicles or {} +Nyx.Vehicles.RoadDegradation = Nyx.Vehicles.RoadDegradation or {} + +-- Pure deterministic rounding helper to 2 decimal places +local function roundToCent(val) + val = tonumber(val) or 0.0 + return math.floor(val * 100.0 + 0.5) / 100.0 +end + +--- Calculates pavement stress / damage factor based on Fourth-Power Law approximation +--- @param grossWeightKg number Gross vehicle weight rating in KG +--- @param axleCount number Number of axles (default 2) +--- @return number Damage factor multiplier (relative to baseline 4,000 kg 2-axle passenger car = 1.0) +function Nyx.Vehicles.RoadDegradation.CalculateAxleDamageFactor(grossWeightKg, axleCount) + grossWeightKg = math.max(100.0, tonumber(grossWeightKg) or 1500.0) + axleCount = math.max(1, tonumber(axleCount) or 2) + + local weightPerAxle = grossWeightKg / axleCount + local baselineAxleWeight = 2000.0 -- 2,000 kg per axle baseline (4,000 kg total for 2 axles) + + local ratio = weightPerAxle / baselineAxleWeight + local factor = (ratio ^ 4.0) * (axleCount / 2.0) + + return roundToCent(factor) +end + +--- Calculates current road segment quality percentage after heavy vehicle passages +--- @param heavyPassages number Total heavy axle passage count recorded +--- @param baselineQuality number Starting road quality percentage (0-100) +--- @param timeElapsedHours number Hours elapsed since last repair +--- @return number Degraded quality percentage (0.00 to 100.00) +function Nyx.Vehicles.RoadDegradation.CalculateRoadQualityDecay(heavyPassages, baselineQuality, timeElapsedHours) + heavyPassages = math.max(0, tonumber(heavyPassages) or 0) + baselineQuality = math.min(100.0, math.max(0.0, tonumber(baselineQuality) or 100.0)) + timeElapsedHours = math.max(0.0, tonumber(timeElapsedHours) or 0.0) + + -- Passage decay: 0.15% loss per heavy passage + local passageLoss = heavyPassages * 0.15 + -- Environmental time decay: 0.05% loss per hour + local timeLoss = timeElapsedHours * 0.05 + + local remaining = baselineQuality - passageLoss - timeLoss + remaining = math.min(100.0, math.max(0.0, remaining)) + + return roundToCent(remaining) +end + +--- Calculates handling / traction penalty factor for vehicles driving on a degraded road +--- @param roadQualityPct number Road quality percentage (0.00 to 100.00) +--- @return table Handling modifiers { gripMultiplier = number, suspensionWearMultiplier = number, status = string } +function Nyx.Vehicles.RoadDegradation.CalculateHandlingPenalty(roadQualityPct) + roadQualityPct = math.min(100.0, math.max(0.0, tonumber(roadQualityPct) or 100.0)) + + local gripMultiplier = 1.00 + local suspensionWearMultiplier = 1.00 + local status = "OPTIMAL" + + if roadQualityPct < 30.0 then + status = "CRITICAL" + gripMultiplier = 0.65 + suspensionWearMultiplier = 2.50 + elseif roadQualityPct < 60.0 then + status = "DEGRADED" + gripMultiplier = 0.80 + suspensionWearMultiplier = 1.75 + elseif roadQualityPct < 85.0 then + status = "WEAR_WARNING" + gripMultiplier = 0.92 + suspensionWearMultiplier = 1.25 + end + + return { + gripMultiplier = roundToCent(gripMultiplier), + suspensionWearMultiplier = roundToCent(suspensionWearMultiplier), + status = status + } +end + +--- Calculates asphalt material & treasury payout required to restore a road segment to 100% quality +--- @param segmentLengthMeters number Segment length in meters +--- @param qualityLossPct number Quality loss percentage (e.g. 100 - quality_pct) +--- @return table { requiredAsphaltKg = number, treasuryPayout = number } +function Nyx.Vehicles.RoadDegradation.CalculateRepairCostAndMaterials(segmentLengthMeters, qualityLossPct) + segmentLengthMeters = math.max(10.0, tonumber(segmentLengthMeters) or 1000.0) + qualityLossPct = math.min(100.0, math.max(0.0, tonumber(qualityLossPct) or 0.0)) + + -- 10 kg asphalt per meter for 100% loss + local requiredAsphaltKg = (segmentLengthMeters * 10.0) * (qualityLossPct / 100.0) + -- $2.50 treasury payout per kg asphalt laid + local treasuryPayout = requiredAsphaltKg * 2.50 + + return { + requiredAsphaltKg = roundToCent(requiredAsphaltKg), + treasuryPayout = roundToCent(treasuryPayout) + } +end diff --git a/FiveM/nyx_vehicles/sql/migrations/067_highway_degradation_and_transit_repair.sql b/FiveM/nyx_vehicles/sql/migrations/067_highway_degradation_and_transit_repair.sql new file mode 100644 index 0000000..87f455a --- /dev/null +++ b/FiveM/nyx_vehicles/sql/migrations/067_highway_degradation_and_transit_repair.sql @@ -0,0 +1,55 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_services & nyx_treasury +-- Migration: 067_highway_degradation_and_transit_repair.sql +-- Description: Commercial highway infrastructure degradation, heavy vehicle axle stress +-- pavement wear modeling, municipal road quality tracking, and +-- public works asphalt/resurfacing repair contracts. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create highway segments table +CREATE TABLE IF NOT EXISTS `nyx_highway_segments` ( + `segment_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `highway_name` VARCHAR(128) NOT NULL, + `start_coords` JSON NOT NULL, + `end_coords` JSON NOT NULL, + `length_meters` DECIMAL(10, 2) NOT NULL DEFAULT 1000.00, + `quality_pct` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_heavy_axle_passages` INT UNSIGNED NOT NULL DEFAULT 0, + `status` ENUM('OPTIMAL', 'WEAR_WARNING', 'DEGRADED', 'CRITICAL') NOT NULL DEFAULT 'OPTIMAL', + `last_repaired_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_seg_highway` (`highway_name`), + INDEX `idx_seg_quality` (`quality_pct`), + INDEX `idx_seg_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create municipal road repair contracts table +CREATE TABLE IF NOT EXISTS `nyx_road_repair_contracts` ( + `contract_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `segment_id` VARCHAR(64) NOT NULL, + `required_asphalt_kg` DECIMAL(10, 2) NOT NULL DEFAULT 1000.00, + `treasury_payout` DECIMAL(10, 2) NOT NULL DEFAULT 2500.00, + `assigned_faction_id` VARCHAR(64) NULL DEFAULT NULL, + `status` ENUM('OPEN', 'IN_PROGRESS', 'COMPLETED', 'EXPIRED') NOT NULL DEFAULT 'OPEN', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `completed_at` TIMESTAMP NULL DEFAULT NULL, + FOREIGN KEY (`segment_id`) REFERENCES `nyx_highway_segments` (`segment_id`) ON DELETE CASCADE, + INDEX `idx_repair_segment` (`segment_id`), + INDEX `idx_repair_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed major municipal highway transit corridors +INSERT INTO `nyx_highway_segments` + (`segment_id`, `highway_name`, `start_coords`, `end_coords`, `length_meters`, `quality_pct`, `total_heavy_axle_passages`, `status`) +VALUES + ('seg_del_perro_fwy_1', 'Del Perro Freeway - Westbound Corridor', '{"x": -1200.0, "y": -800.0, "z": 15.0}', '{"x": 400.0, "y": -800.0, "z": 25.0}', 1600.00, 100.00, 0, 'OPTIMAL'), + ('seg_la_puerta_fwy_1', 'La Puerta Freeway - Port Access Connector', '{"x": -200.0, "y": -2000.0, "z": 10.0}', '{"x": 400.0, "y": -1500.0, "z": 20.0}', 1200.00, 95.00, 12, 'OPTIMAL'), + ('seg_palomino_fwy_1', 'Palomino Freeway - East Industrial Bypass', '{"x": 1200.0, "y": -1000.0, "z": 30.0}', '{"x": 2200.0, "y": 500.0, "z": 45.0}', 2500.00, 82.00, 48, 'OPTIMAL'), + ('seg_senora_fwy_1', 'Senora Freeway - Desert Logistics Route', '{"x": 1500.0, "y": 2500.0, "z": 50.0}', '{"x": 2500.0, "y": 4500.0, "z": 60.0}', 3200.00, 68.00, 120, 'WEAR_WARNING') +ON DUPLICATE KEY UPDATE `highway_name` = VALUES(`highway_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/tests/test_highway_degradation_and_transit_repair.py b/tests/test_highway_degradation_and_transit_repair.py new file mode 100644 index 0000000..4f544b4 --- /dev/null +++ b/tests/test_highway_degradation_and_transit_repair.py @@ -0,0 +1,207 @@ +""" +Tests for Highway Road Degradation, Heavy Freight Transit Wear & Infrastructure Repairs (Migration 067) +Validates SQL migration 067 parity across core, vehicles, services, and treasury folders, index.json registration, +fxmanifest declarations, server exports, in-engine Test 62 registration, and pure Python/Lua deterministic algorithms +for axle damage factor calculation, road quality decay, handling penalties, and repair material requirements. +""" + +import json +import math +import os +import pytest + +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +CORE_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_core", "sql", "migrations", "067_highway_degradation_and_transit_repair.sql" +) +VEHICLES_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "sql", "migrations", "067_highway_degradation_and_transit_repair.sql" +) +SERVICES_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_services", "sql", "migrations", "067_highway_degradation_and_transit_repair.sql" +) +TREASURY_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_treasury", "sql", "migrations", "067_highway_degradation_and_transit_repair.sql" +) +INDEX_JSON_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_core", "sql", "migrations", "index.json" +) +VEHICLES_MANIFEST_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "fxmanifest.lua" +) +SERVER_EXPORTS_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "server", "exports.lua" +) +TESTS_LUA_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_core", "server", "tests.lua" +) + + +class RoadDegradationEngine: + @staticmethod + def round_to_cent(val): + val = float(val or 0.0) + return math.floor(val * 100.0 + 0.5) / 100.0 + + @classmethod + def calculate_axle_damage_factor(cls, gross_weight_kg, axle_count=2): + gross_weight_kg = max(100.0, float(gross_weight_kg or 1500.0)) + axle_count = max(1, int(axle_count or 2)) + + weight_per_axle = gross_weight_kg / axle_count + baseline_axle_weight = 2000.0 + + ratio = weight_per_axle / baseline_axle_weight + factor = (ratio ** 4.0) * (axle_count / 2.0) + return cls.round_to_cent(factor) + + @classmethod + def calculate_road_quality_decay(cls, heavy_passages, baseline_quality=100.0, time_elapsed_hours=0.0): + heavy_passages = max(0, int(heavy_passages or 0)) + baseline_quality = min(100.0, max(0.0, float(baseline_quality or 100.0))) + time_elapsed_hours = max(0.0, float(time_elapsed_hours or 0.0)) + + passage_loss = heavy_passages * 0.15 + time_loss = time_elapsed_hours * 0.05 + + remaining = baseline_quality - passage_loss - time_loss + remaining = min(100.0, max(0.0, remaining)) + return cls.round_to_cent(remaining) + + @classmethod + def calculate_handling_penalty(cls, road_quality_pct): + road_quality_pct = min(100.0, max(0.0, float(road_quality_pct or 100.0))) + + grip_multiplier = 1.00 + suspension_wear_multiplier = 1.00 + status = "OPTIMAL" + + if road_quality_pct < 30.0: + status = "CRITICAL" + grip_multiplier = 0.65 + suspension_wear_multiplier = 2.50 + elif road_quality_pct < 60.0: + status = "DEGRADED" + grip_multiplier = 0.80 + suspension_wear_multiplier = 1.75 + elif road_quality_pct < 85.0: + status = "WEAR_WARNING" + grip_multiplier = 0.92 + suspension_wear_multiplier = 1.25 + + return { + "grip_multiplier": cls.round_to_cent(grip_multiplier), + "suspension_wear_multiplier": cls.round_to_cent(suspension_wear_multiplier), + "status": status, + } + + @classmethod + def calculate_repair_cost_and_materials(cls, segment_length_meters, quality_loss_pct): + segment_length_meters = max(10.0, float(segment_length_meters or 1000.0)) + quality_loss_pct = min(100.0, max(0.0, float(quality_loss_pct or 0.0))) + + required_asphalt_kg = (segment_length_meters * 10.0) * (quality_loss_pct / 100.0) + treasury_payout = required_asphalt_kg * 2.50 + + return { + "required_asphalt_kg": cls.round_to_cent(required_asphalt_kg), + "treasury_payout": cls.round_to_cent(treasury_payout), + } + + +def test_migration_067_files_and_registration(): + """Verify SQL migration 067 exists in core, vehicles, services, treasury and is registered in index.json.""" + assert os.path.exists(CORE_MIGRATION_PATH), "Core migration 067 must exist" + assert os.path.exists(VEHICLES_MIGRATION_PATH), "Vehicles migration 067 must exist" + assert os.path.exists(SERVICES_MIGRATION_PATH), "Services migration 067 must exist" + assert os.path.exists(TREASURY_MIGRATION_PATH), "Treasury migration 067 must exist" + + with open(INDEX_JSON_PATH, "r", encoding="utf-8") as f: + registered = json.load(f) + + assert "067_highway_degradation_and_transit_repair.sql" in registered, ( + "067 migration must be registered in index.json" + ) + + with open(CORE_MIGRATION_PATH, "r", encoding="utf-8") as f: + sql = f.read() + + assert "CREATE TABLE IF NOT EXISTS `nyx_highway_segments`" in sql + assert "CREATE TABLE IF NOT EXISTS `nyx_road_repair_contracts`" in sql + assert "seg_del_perro_fwy_1" in sql + assert "total_heavy_axle_passages" in sql + + +def test_manifest_and_exports_declarations(): + """Verify scripts are registered in fxmanifest.lua and server exports.""" + with open(VEHICLES_MANIFEST_PATH, "r", encoding="utf-8") as f: + manifest = f.read() + + assert "shared/road_degradation.lua" in manifest + assert "server/road_degradation.lua" in manifest + assert "client/road_degradation.lua" in manifest + + with open(SERVER_EXPORTS_PATH, "r", encoding="utf-8") as f: + exports = f.read() + + assert 'exports("RecordHighwayTransitPass"' in exports + assert 'exports("GetHighwaySegmentCondition"' in exports + assert 'exports("SubmitRoadRepairBatch"' in exports + assert 'exports("CalculateAxleDamageFactor"' in exports + + +def test_in_engine_test_62_registration(): + """Verify Test 62 (TestHighwayDegradationAndTransitRepair) is registered in tests.lua.""" + with open(TESTS_LUA_PATH, "r", encoding="utf-8") as f: + tests_lua = f.read() + + assert "function Nyx.Tests.TestHighwayDegradationAndTransitRepair()" in tests_lua + assert "local t62 = Nyx.Tests.TestHighwayDegradationAndTransitRepair()" in tests_lua + + +def test_axle_damage_factor_fourth_power_law(): + """Verify 4th-power law pavement stress calculation.""" + # 2,000 kg 2-axle passenger car -> 1,000 kg per axle vs 2,000 kg baseline -> ratio 0.5^4 * (2/2) = 0.0625 -> 0.06 + car_factor = RoadDegradationEngine.calculate_axle_damage_factor(2000.0, 2) + assert car_factor == 0.06 + + # 20,000 kg 5-axle semi -> 4,000 kg per axle vs 2,000 kg baseline -> ratio 2.0^4 * (5/2) = 16 * 2.5 = 40.0 + semi_factor = RoadDegradationEngine.calculate_axle_damage_factor(20000.0, 5) + assert semi_factor == 40.0 + + +def test_road_quality_decay(): + """Verify quality degradation from heavy axle passages and time elapsed.""" + q_init = RoadDegradationEngine.calculate_road_quality_decay(0, 100.0, 0.0) + assert q_init == 100.0 + + # 100 heavy passages (15.0% loss) + 20 hours (1.0% loss) -> 84.0% + q_decay = RoadDegradationEngine.calculate_road_quality_decay(100, 100.0, 20.0) + assert q_decay == 84.0 + + +def test_handling_penalty_modifiers(): + """Verify traction and suspension modifiers across road quality tiers.""" + opt = RoadDegradationEngine.calculate_handling_penalty(90.0) + assert opt["status"] == "OPTIMAL" + assert opt["grip_multiplier"] == 1.00 + + warn = RoadDegradationEngine.calculate_handling_penalty(75.0) + assert warn["status"] == "WEAR_WARNING" + assert warn["grip_multiplier"] == 0.92 + + deg = RoadDegradationEngine.calculate_handling_penalty(50.0) + assert deg["status"] == "DEGRADED" + assert deg["grip_multiplier"] == 0.80 + + crit = RoadDegradationEngine.calculate_handling_penalty(20.0) + assert crit["status"] == "CRITICAL" + assert crit["grip_multiplier"] == 0.65 + + +def test_repair_cost_and_materials(): + """Verify asphalt requirements and treasury payout calculation.""" + # 1,000m segment with 20% quality loss -> 1,000 * 10 * 0.20 = 2,000 kg asphalt -> $5,000 payout + repair = RoadDegradationEngine.calculate_repair_costAnd_materials = RoadDegradationEngine.calculate_repair_cost_and_materials(1000.0, 20.0) + assert repair["required_asphalt_kg"] == 2000.0 + assert repair["treasury_payout"] == 5000.0