From 76a1cc7c5ba8fde9c3b3160ea1a7217064a9c1b4 Mon Sep 17 00:00:00 2001 From: Lacikika <114676193+Lacikika@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:31:54 +0000 Subject: [PATCH] feat: agricultural livestock & slaughterhouse processing Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- FiveM/nyx_core/server/tests.lua | 101 +++++++- ...ughterhouse_and_refrigerated_logistics.sql | 73 ++++++ FiveM/nyx_core/sql/migrations/index.json | 3 +- FiveM/nyx_inventory/shared/items.lua | 69 ++++++ ...ughterhouse_and_refrigerated_logistics.sql | 73 ++++++ FiveM/nyx_logistics/shared/contracts.lua | 7 + FiveM/nyx_logistics/shared/nodes.lua | 26 +++ FiveM/nyx_logistics/shared/recipes.lua | 65 ++++++ ...ughterhouse_and_refrigerated_logistics.sql | 73 ++++++ FiveM/nyx_services/client/livestock.lua | 89 ++++++++ FiveM/nyx_services/fxmanifest.lua | 3 + FiveM/nyx_services/server/livestock.lua | 167 ++++++++++++++ FiveM/nyx_services/shared/livestock.lua | 215 ++++++++++++++++++ ...ughterhouse_and_refrigerated_logistics.sql | 73 ++++++ ...ughterhouse_and_refrigerated_logistics.sql | 73 ++++++ ...aughterhouse_and_refrigerated_logistics.py | 213 +++++++++++++++++ 16 files changed, 1321 insertions(+), 2 deletions(-) create mode 100644 FiveM/nyx_core/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql create mode 100644 FiveM/nyx_inventory/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql create mode 100644 FiveM/nyx_logistics/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql create mode 100644 FiveM/nyx_services/client/livestock.lua create mode 100644 FiveM/nyx_services/server/livestock.lua create mode 100644 FiveM/nyx_services/shared/livestock.lua create mode 100644 FiveM/nyx_services/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql create mode 100644 FiveM/nyx_vehicles/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql create mode 100644 tests/test_livestock_slaughterhouse_and_refrigerated_logistics.py diff --git a/FiveM/nyx_core/server/tests.lua b/FiveM/nyx_core/server/tests.lua index b05bd78..54e63b2 100644 --- a/FiveM/nyx_core/server/tests.lua +++ b/FiveM/nyx_core/server/tests.lua @@ -198,6 +198,104 @@ function Nyx.Tests.TestRPC() return passes == total end +-- 62. Integration Test: Agricultural Livestock, Slaughterhouse Processing & Cold-Chain Logistics +function Nyx.Tests.TestLivestockSlaughterhouseAndRefrigeratedLogistics() + print("^4[Nyx:Test]^7 Running Agricultural Livestock, Slaughterhouse Processing & Cold-Chain Logistics Test...") + local passes = 0 + local total = 7 + + -- Sub-test A: Cattle Ranch Growth & Feed Consumption Math + local feed1, health1, gained1 = Nyx.Services.Livestock.CalculateCattleGrowth(100, 200, 100.0, 100.0, 3600) + local testA_ok = (feed1 == 95.0) and (health1 == 100.0) and (gained1 == 1) + + if assertTest(testA_ok == true, + "Cattle Ranch Feed Consumption & Growth Dynamics", + string.format("Growth calculation mismatch: feed=%.1f, health=%.1f, gained=%d", + feed1, health1, gained1)) then + passes = passes + 1 + end + + -- Sub-test B: Carcass Perishable Decay in Cold Storage vs Ambient Stash + local durCold, spCold, tierCold, lossCold = Nyx.Services.Livestock.CalculateCarcassDecay(100.0, 0.12, 3600, "cold_storage", true, 2.0) + local durDrop, spDrop, tierDrop, lossDrop = Nyx.Services.Livestock.CalculateCarcassDecay(100.0, 0.12, 3600, "drop", false, 25.0) + + local testB_ok = (durCold == 98.8 and tierCold == "PRISTINE") and (durDrop == 70.0 and tierDrop == "FRESH") + + if assertTest(testB_ok == true, + "Carcass Decay Preservation under Active Cold Chain vs Exposed Drop", + string.format("Decay mismatch: coldDur=%.1f (tier=%s), dropDur=%.1f (tier=%s)", + durCold, tierCold, durDrop, tierDrop)) then + passes = passes + 1 + end + + -- Sub-test C: Cold Chain Status Evaluation + local statusPristine, _, _ = Nyx.Services.Livestock.EvaluateColdChainStatus(2.0, true) + local statusSpoiled, _, _ = Nyx.Services.Livestock.EvaluateColdChainStatus(15.0, false) + + local testC_ok = (statusPristine == "PRISTINE") and (statusSpoiled == "CRITICAL_SPOILAGE") + + if assertTest(testC_ok == true, + "Cold Chain Compliance & Spoilage Status Thresholds", + string.format("Status evaluation mismatch: pristine=%s, spoiled=%s", + statusPristine, statusSpoiled)) then + passes = passes + 1 + end + + -- Sub-test D: Carcass Butchering Yield + local cutsA, palletsA, gradeA = Nyx.Services.Livestock.CalculateButcheringYield(100.0, 100.0) + local cutsB, palletsB, gradeB = Nyx.Services.Livestock.CalculateButcheringYield(0.0, 100.0) + + local testD_ok = (cutsA == 10 and palletsA == 1 and gradeA == "PRISTINE") and (cutsB == 0 and palletsB == 0 and gradeB == "SPOILED") + + if assertTest(testD_ok == true, + "Slaughterhouse Carcass Butchering Yields & Quality Grading", + string.format("Butchering yield mismatch: cutsA=%d, gradeA=%s, cutsB=%d, gradeB=%s", + cutsA, gradeA, cutsB, gradeB)) then + passes = passes + 1 + end + + -- Sub-test E: Refrigerated Freight Haulage Payout & Spoilage Penalty + local payoutA, bondA, penA, bonA = Nyx.Services.Livestock.CalculateRefrigeratedFreightPayout(1000.0, 95.0, 1200, 1800) + local payoutB, bondB, penB, bonB = Nyx.Services.Livestock.CalculateRefrigeratedFreightPayout(1000.0, 0.0, 1200, 1800) + + local testE_ok = (payoutA == 1250.0 and bondA == 150.0 and bonA == 150.0) and (payoutB == 0.0 and bondB == 0.0 and penB == 150.0) + + if assertTest(testE_ok == true, + "Refrigerated Freight Haulage Payout, Escrow Return & Spoilage Penalty Math", + string.format("Freight payout mismatch: payoutA=%.2f, bonA=%.2f, payoutB=%.2f, penB=%.2f", + payoutA, bonA, payoutB, penB)) then + passes = passes + 1 + end + + -- Sub-test F: Item & Manufacturing Recipe Registrations + local cattleItem = Nyx.Items.Get("livestock_cattle") + local carcassItem = Nyx.Items.Get("carcass_beef_raw") + local slaughterRecipe = Nyx.Logistics.GetRecipe("cattle_slaughtering") + + local testF_ok = (cattleItem ~= nil and cattleItem.isHeavyFreight == true) and + (carcassItem ~= nil and carcassItem.decayRate == 0.12) and + (slaughterRecipe ~= nil and slaughterRecipe.facilityType == "slaughterhouse") + + if assertTest(testF_ok == true, + "Livestock & Slaughterhouse Item and Manufacturing Recipe Declarations", + string.format("Registration mismatch: cattle=%s, carcass=%s, recipe=%s", + tostring(cattleItem ~= nil), tostring(carcassItem ~= nil), tostring(slaughterRecipe ~= nil))) then + passes = passes + 1 + end + + -- Sub-test G: Node and Terminal Registration + local ranchNode = Nyx.Logistics.GetNode("ranch_grapeseed") + local testG_ok = (ranchNode ~= nil and ranchNode.resource == "livestock_cattle") + + if assertTest(testG_ok == true, + "Regional Livestock Ranch Extraction Node Registration", + string.format("Node registration mismatch: ranchNode=%s", tostring(ranchNode ~= nil))) 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 +12738,7 @@ function Nyx.Tests.RunAll() local t59 = Nyx.Tests.TestIndustrialRefiningAndFunctionalPartsManufacturing() local t60 = Nyx.Tests.TestJudicialBailBondsSuretyEscrowAndFugitiveRecovery() local t61 = Nyx.Tests.TestMunicipalTollsAndWeighStations() + local t62 = Nyx.Tests.TestLivestockSlaughterhouseAndRefrigeratedLogistics() print("^5--------------------------------------------------^7") local suites1to50Passed = false @@ -12647,7 +12746,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_livestock_slaughterhouse_and_refrigerated_logistics.sql b/FiveM/nyx_core/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql new file mode 100644 index 0000000..671c59f --- /dev/null +++ b/FiveM/nyx_core/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql @@ -0,0 +1,73 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_logistics, nyx_vehicles & nyx_inventory +-- Migration: 067_livestock_slaughterhouse_and_refrigerated_logistics.sql +-- Description: Agricultural livestock husbandry, slaughterhouse processing, +-- carcass decay, refrigerated cold-chain logistics, and reefer +-- freight haulage tracking. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create livestock ranches table +CREATE TABLE IF NOT EXISTS `nyx_livestock_ranches` ( + `ranch_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `ranch_name` VARCHAR(64) NOT NULL, + `zone_name` VARCHAR(64) NOT NULL, + `cattle_head` INT NOT NULL DEFAULT 50, + `max_capacity` INT NOT NULL DEFAULT 200, + `feed_level` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `health_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_ranch_zone` (`zone_name`), + INDEX `idx_ranch_health` (`health_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create slaughterhouse facilities table +CREATE TABLE IF NOT EXISTS `nyx_slaughterhouses` ( + `facility_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `facility_name` VARCHAR(64) NOT NULL, + `cold_chain_temp_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `cold_storage_capacity` INT NOT NULL DEFAULT 500, + `cleanliness_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_carcasses_processed` INT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_slaughter_temp` (`cold_chain_temp_c`), + INDEX `idx_slaughter_clean` (`cleanliness_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create refrigerated cold-chain telemetry logs table +CREATE TABLE IF NOT EXISTS `nyx_refrigerated_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `container_id` VARCHAR(64) NOT NULL, + `item_name` VARCHAR(64) NOT NULL, + `durability` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `is_reefer_active` TINYINT(1) NOT NULL DEFAULT 1, + `temperature_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `inspection_status` ENUM('PRISTINE', 'FRESH', 'STALE', 'SPOILED') NOT NULL DEFAULT 'PRISTINE', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_reefer_container` (`container_id`), + INDEX `idx_reefer_item` (`item_name`), + INDEX `idx_reefer_status` (`inspection_status`), + INDEX `idx_reefer_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Seed regional livestock ranches +INSERT INTO `nyx_livestock_ranches` + (`ranch_id`, `ranch_name`, `zone_name`, `cattle_head`, `max_capacity`, `feed_level`, `health_index`) +VALUES + ('ranch_grapeseed', 'O\'Neil Grapeseed Livestock Ranch', 'Grapeseed', 120, 200, 100.00, 100.00), + ('ranch_paleto', 'Paleto Bay Coastal Cattle Ranch', 'Paleto Bay', 80, 150, 95.00, 98.00) +ON DUPLICATE KEY UPDATE `ranch_name` = VALUES(`ranch_name`); + +-- 5. Seed regional slaughterhouse facilities +INSERT INTO `nyx_slaughterhouses` + (`facility_id`, `facility_name`, `cold_chain_temp_c`, `cold_storage_capacity`, `cleanliness_index`, `total_carcasses_processed`) +VALUES + ('slaughterhouse_cypress_flats', 'Cypress Flats Industrial Slaughterhouse', 2.0, 500, 100.00, 0), + ('slaughterhouse_paleto', 'Paleto Meat Processing Plant', 1.5, 300, 96.50, 0) +ON DUPLICATE KEY UPDATE `facility_name` = VALUES(`facility_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..7936b77 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_livestock_slaughterhouse_and_refrigerated_logistics.sql" ] diff --git a/FiveM/nyx_inventory/shared/items.lua b/FiveM/nyx_inventory/shared/items.lua index 3e98c6c..c4dd624 100644 --- a/FiveM/nyx_inventory/shared/items.lua +++ b/FiveM/nyx_inventory/shared/items.lua @@ -589,6 +589,75 @@ Nyx.Items.Registry = { attachRot = { x = 0.0, y = 0.0, z = 0.0 } } }, + ['livestock_cattle'] = { + name = 'livestock_cattle', + label = 'Live Cattle Head', + weight = 100000, + stackable = false, + maxSlots = 1, + type = 'freight', + isHeavyFreight = true, + decayRate = 0.00, + description = 'Live cattle livestock head harvested from regional ranches for transit and slaughterhouse processing.', + client = { + model = `a_c_cow`, + animDict = 'anim@heists@box_carry@', + anim = 'idle' + } + }, + ['carcass_beef_raw'] = { + name = 'carcass_beef_raw', + label = 'Whole Raw Beef Carcass', + weight = 45000, + stackable = false, + maxSlots = 1, + type = 'food', + isHeavyFreight = true, + decayRate = 0.12, + description = 'Whole dressed beef carcass. Highly perishable; requires refrigerated storage or rapid butchering.', + client = { + model = `prop_box_wood02a` + } + }, + ['packaged_meat_cuts'] = { + name = 'packaged_meat_cuts', + label = 'Vacuum-Packed Premium Beef Cuts', + weight = 1000, + stackable = true, + maxStack = 20, + type = 'food', + decayRate = 0.05, + nutrition = 50, + description = 'Butchered and vacuum-sealed choice beef cuts for restaurants, supermarkets, and civil pantries.' + }, + ['pallet_refrigerated_beef'] = { + name = 'pallet_refrigerated_beef', + label = 'Chilled Beef Freight Pallet', + weight = 80000, + stackable = false, + maxSlots = 1, + type = 'freight', + isHeavyFreight = true, + decayRate = 0.01, + description = 'Cold-chain insured freight pallet of chilled beef cuts. Requires refrigerated freight transport.', + client = { + model = `prop_boxpile_06a`, + animDict = 'anim@heists@box_carry@', + anim = 'idle', + attachOffset = { x = 0.0, y = 0.48, z = -0.20 }, + attachRot = { x = 0.0, y = 0.0, z = 0.0 } + } + }, + ['livestock_feed_sack'] = { + name = 'livestock_feed_sack', + label = 'Nutritional Livestock Feed Sack (25kg)', + weight = 25000, + stackable = true, + maxStack = 10, + type = 'freight', + decayRate = 0.00, + description = 'Enriched grain and cornmeal livestock feed sack for cattle ranch husbandry.' + }, -- Tools & Valuables ['tool_lockpick'] = { diff --git a/FiveM/nyx_inventory/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql b/FiveM/nyx_inventory/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql new file mode 100644 index 0000000..671c59f --- /dev/null +++ b/FiveM/nyx_inventory/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql @@ -0,0 +1,73 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_logistics, nyx_vehicles & nyx_inventory +-- Migration: 067_livestock_slaughterhouse_and_refrigerated_logistics.sql +-- Description: Agricultural livestock husbandry, slaughterhouse processing, +-- carcass decay, refrigerated cold-chain logistics, and reefer +-- freight haulage tracking. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create livestock ranches table +CREATE TABLE IF NOT EXISTS `nyx_livestock_ranches` ( + `ranch_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `ranch_name` VARCHAR(64) NOT NULL, + `zone_name` VARCHAR(64) NOT NULL, + `cattle_head` INT NOT NULL DEFAULT 50, + `max_capacity` INT NOT NULL DEFAULT 200, + `feed_level` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `health_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_ranch_zone` (`zone_name`), + INDEX `idx_ranch_health` (`health_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create slaughterhouse facilities table +CREATE TABLE IF NOT EXISTS `nyx_slaughterhouses` ( + `facility_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `facility_name` VARCHAR(64) NOT NULL, + `cold_chain_temp_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `cold_storage_capacity` INT NOT NULL DEFAULT 500, + `cleanliness_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_carcasses_processed` INT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_slaughter_temp` (`cold_chain_temp_c`), + INDEX `idx_slaughter_clean` (`cleanliness_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create refrigerated cold-chain telemetry logs table +CREATE TABLE IF NOT EXISTS `nyx_refrigerated_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `container_id` VARCHAR(64) NOT NULL, + `item_name` VARCHAR(64) NOT NULL, + `durability` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `is_reefer_active` TINYINT(1) NOT NULL DEFAULT 1, + `temperature_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `inspection_status` ENUM('PRISTINE', 'FRESH', 'STALE', 'SPOILED') NOT NULL DEFAULT 'PRISTINE', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_reefer_container` (`container_id`), + INDEX `idx_reefer_item` (`item_name`), + INDEX `idx_reefer_status` (`inspection_status`), + INDEX `idx_reefer_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Seed regional livestock ranches +INSERT INTO `nyx_livestock_ranches` + (`ranch_id`, `ranch_name`, `zone_name`, `cattle_head`, `max_capacity`, `feed_level`, `health_index`) +VALUES + ('ranch_grapeseed', 'O\'Neil Grapeseed Livestock Ranch', 'Grapeseed', 120, 200, 100.00, 100.00), + ('ranch_paleto', 'Paleto Bay Coastal Cattle Ranch', 'Paleto Bay', 80, 150, 95.00, 98.00) +ON DUPLICATE KEY UPDATE `ranch_name` = VALUES(`ranch_name`); + +-- 5. Seed regional slaughterhouse facilities +INSERT INTO `nyx_slaughterhouses` + (`facility_id`, `facility_name`, `cold_chain_temp_c`, `cold_storage_capacity`, `cleanliness_index`, `total_carcasses_processed`) +VALUES + ('slaughterhouse_cypress_flats', 'Cypress Flats Industrial Slaughterhouse', 2.0, 500, 100.00, 0), + ('slaughterhouse_paleto', 'Paleto Meat Processing Plant', 1.5, 300, 96.50, 0) +ON DUPLICATE KEY UPDATE `facility_name` = VALUES(`facility_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_logistics/shared/contracts.lua b/FiveM/nyx_logistics/shared/contracts.lua index 6e54695..21fb17b 100644 --- a/FiveM/nyx_logistics/shared/contracts.lua +++ b/FiveM/nyx_logistics/shared/contracts.lua @@ -80,6 +80,13 @@ Nyx.Logistics.FreightConfig = { coords = vector3(2438.0, 4964.0, 46.0), model = `prop_terminal_01`, heading = 180.0 + }, + { + id = "terminal_cypress_slaughterhouse", + name = "Cypress Flats Slaughterhouse & Cold Logistics", + coords = vector3(960.5, -2180.2, 30.5), + model = `prop_terminal_01`, + heading = 90.0 } } } diff --git a/FiveM/nyx_logistics/shared/nodes.lua b/FiveM/nyx_logistics/shared/nodes.lua index f271773..e0fe1d3 100644 --- a/FiveM/nyx_logistics/shared/nodes.lua +++ b/FiveM/nyx_logistics/shared/nodes.lua @@ -165,6 +165,32 @@ Nyx.Logistics.Nodes.Registry = { dict = 'amb@world_human_gardener_plant@male@base', name = 'base' } + }, + ['ranch_grapeseed'] = { + id = 'ranch_grapeseed', + label = 'O\'Neil Grapeseed Livestock Ranch', + resource = 'livestock_cattle', + maxReserve = 200, + regenRatePerHour = 20, + requiredTool = nil, + coords = vector3(2440.5, 4980.2, 45.0), + anim = { + dict = 'amb@world_human_gardener_plant@male@base', + name = 'base' + } + }, + ['ranch_paleto'] = { + id = 'ranch_paleto', + label = 'Paleto Bay Coastal Cattle Ranch', + resource = 'livestock_cattle', + maxReserve = 150, + regenRatePerHour = 15, + requiredTool = nil, + coords = vector3(-380.2, 6180.4, 31.0), + anim = { + dict = 'amb@world_human_gardener_plant@male@base', + name = 'base' + } } } diff --git a/FiveM/nyx_logistics/shared/recipes.lua b/FiveM/nyx_logistics/shared/recipes.lua index 5cfa241..4a2c9df 100644 --- a/FiveM/nyx_logistics/shared/recipes.lua +++ b/FiveM/nyx_logistics/shared/recipes.lua @@ -31,6 +31,71 @@ Nyx.Logistics.Recipes.Registry = { } } }, + ['cattle_slaughtering'] = { + key = 'cattle_slaughtering', + label = 'Livestock Cattle Slaughtering & Dressing', + facilityType = 'slaughterhouse', + processingTime = 30, + powerRequirementKw = 15, + inputs = { + { item = 'livestock_cattle', amount = 1 } + }, + outputs = { + { + item = 'carcass_beef_raw', + amount = 2, + metadata = { + durability = 100.0, + isSpoiled = false + } + } + } + }, + ['carcass_butchering'] = { + key = 'carcass_butchering', + label = 'Cold-Chain Carcass Butchering & Palletizing', + facilityType = 'slaughterhouse', + processingTime = 25, + powerRequirementKw = 20, + inputs = { + { item = 'carcass_beef_raw', amount = 1 } + }, + outputs = { + { + item = 'packaged_meat_cuts', + amount = 10, + metadata = { + durability = 100.0, + isSpoiled = false, + nutrition = 50 + } + }, + { + item = 'pallet_refrigerated_beef', + amount = 1, + metadata = { + durability = 100.0, + temperatureC = 2.0 + } + } + } + }, + ['livestock_feed_mixing'] = { + key = 'livestock_feed_mixing', + label = 'Industrial Livestock Feed Compounding', + facilityType = 'refinery', + processingTime = 20, + powerRequirementKw = 10, + inputs = { + { item = 'bulk_cornmeal', amount = 2 } + }, + outputs = { + { + item = 'livestock_feed_sack', + amount = 1 + } + } + }, ['first_aid_pack'] = { key = 'first_aid_pack', label = 'Trauma First Aid Kit', diff --git a/FiveM/nyx_logistics/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql b/FiveM/nyx_logistics/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql new file mode 100644 index 0000000..671c59f --- /dev/null +++ b/FiveM/nyx_logistics/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql @@ -0,0 +1,73 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_logistics, nyx_vehicles & nyx_inventory +-- Migration: 067_livestock_slaughterhouse_and_refrigerated_logistics.sql +-- Description: Agricultural livestock husbandry, slaughterhouse processing, +-- carcass decay, refrigerated cold-chain logistics, and reefer +-- freight haulage tracking. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create livestock ranches table +CREATE TABLE IF NOT EXISTS `nyx_livestock_ranches` ( + `ranch_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `ranch_name` VARCHAR(64) NOT NULL, + `zone_name` VARCHAR(64) NOT NULL, + `cattle_head` INT NOT NULL DEFAULT 50, + `max_capacity` INT NOT NULL DEFAULT 200, + `feed_level` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `health_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_ranch_zone` (`zone_name`), + INDEX `idx_ranch_health` (`health_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create slaughterhouse facilities table +CREATE TABLE IF NOT EXISTS `nyx_slaughterhouses` ( + `facility_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `facility_name` VARCHAR(64) NOT NULL, + `cold_chain_temp_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `cold_storage_capacity` INT NOT NULL DEFAULT 500, + `cleanliness_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_carcasses_processed` INT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_slaughter_temp` (`cold_chain_temp_c`), + INDEX `idx_slaughter_clean` (`cleanliness_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create refrigerated cold-chain telemetry logs table +CREATE TABLE IF NOT EXISTS `nyx_refrigerated_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `container_id` VARCHAR(64) NOT NULL, + `item_name` VARCHAR(64) NOT NULL, + `durability` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `is_reefer_active` TINYINT(1) NOT NULL DEFAULT 1, + `temperature_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `inspection_status` ENUM('PRISTINE', 'FRESH', 'STALE', 'SPOILED') NOT NULL DEFAULT 'PRISTINE', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_reefer_container` (`container_id`), + INDEX `idx_reefer_item` (`item_name`), + INDEX `idx_reefer_status` (`inspection_status`), + INDEX `idx_reefer_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Seed regional livestock ranches +INSERT INTO `nyx_livestock_ranches` + (`ranch_id`, `ranch_name`, `zone_name`, `cattle_head`, `max_capacity`, `feed_level`, `health_index`) +VALUES + ('ranch_grapeseed', 'O\'Neil Grapeseed Livestock Ranch', 'Grapeseed', 120, 200, 100.00, 100.00), + ('ranch_paleto', 'Paleto Bay Coastal Cattle Ranch', 'Paleto Bay', 80, 150, 95.00, 98.00) +ON DUPLICATE KEY UPDATE `ranch_name` = VALUES(`ranch_name`); + +-- 5. Seed regional slaughterhouse facilities +INSERT INTO `nyx_slaughterhouses` + (`facility_id`, `facility_name`, `cold_chain_temp_c`, `cold_storage_capacity`, `cleanliness_index`, `total_carcasses_processed`) +VALUES + ('slaughterhouse_cypress_flats', 'Cypress Flats Industrial Slaughterhouse', 2.0, 500, 100.00, 0), + ('slaughterhouse_paleto', 'Paleto Meat Processing Plant', 1.5, 300, 96.50, 0) +ON DUPLICATE KEY UPDATE `facility_name` = VALUES(`facility_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_services/client/livestock.lua b/FiveM/nyx_services/client/livestock.lua new file mode 100644 index 0000000..926d450 --- /dev/null +++ b/FiveM/nyx_services/client/livestock.lua @@ -0,0 +1,89 @@ +--[[ + Nyxframework: nyx_services + File: client/livestock.lua + Description: Client-side spatial interaction checks, ranch & slaughterhouse + NUI data bridge, and cold storage status indicators. +]] + +Nyx = Nyx or {} +Nyx.Services = Nyx.Services or {} +Nyx.Services.ClientLivestock = {} + +local RANCH_LOCATIONS = { + { + id = "ranch_grapeseed", + name = "O'Neil Grapeseed Livestock Ranch", + coords = vector3(2440.5, 4980.2, 45.0) + }, + { + id = "ranch_paleto", + name = "Paleto Bay Coastal Cattle Ranch", + coords = vector3(-380.2, 6180.4, 31.0) + } +} + +local SLAUGHTERHOUSE_LOCATIONS = { + { + id = "slaughterhouse_cypress_flats", + name = "Cypress Flats Industrial Slaughterhouse", + coords = vector3(960.5, -2180.2, 30.5) + }, + { + id = "slaughterhouse_paleto", + name = "Paleto Meat Processing Plant", + coords = vector3(-375.0, 6195.0, 31.5) + } +} + +--- Find nearest registered livestock ranch +function Nyx.Services.ClientLivestock.GetNearestRanch(playerCoords) + playerCoords = playerCoords or GetEntityCoords(PlayerPedId()) + for _, ranch in ipairs(RANCH_LOCATIONS) do + if #(playerCoords - ranch.coords) <= 15.0 then + return ranch + end + end + return nil +end + +--- Find nearest registered slaughterhouse +function Nyx.Services.ClientLivestock.GetNearestSlaughterhouse(playerCoords) + playerCoords = playerCoords or GetEntityCoords(PlayerPedId()) + for _, facility in ipairs(SLAUGHTERHOUSE_LOCATIONS) do + if #(playerCoords - facility.coords) <= 15.0 then + return facility + end + end + return nil +end + +-- Spatial Interaction Registration with nyx_interact +CreateThread(function() + while not exports or not exports["nyx_interact"] do + Wait(500) + end + + for _, ranch in ipairs(RANCH_LOCATIONS) do + exports["nyx_interact"]:AddInteractionPoint({ + id = "ranch_feed_" .. ranch.id, + coords = ranch.coords, + radius = 3.0, + label = "Feed Cattle Ranch (" .. ranch.name .. ")", + action = function() + TriggerServerEvent("nyx_services:server:feedRanch", ranch.id) + end + }) + end + + for _, facility in ipairs(SLAUGHTERHOUSE_LOCATIONS) do + exports["nyx_interact"]:AddInteractionPoint({ + id = "slaughterhouse_" .. facility.id, + coords = facility.coords, + radius = 3.0, + label = "Slaughterhouse Processing (" .. facility.name .. ")", + action = function() + TriggerServerEvent("nyx_services:server:slaughterCattle", facility.id) + end + }) + end +end) diff --git a/FiveM/nyx_services/fxmanifest.lua b/FiveM/nyx_services/fxmanifest.lua index d07cc09..782deb0 100644 --- a/FiveM/nyx_services/fxmanifest.lua +++ b/FiveM/nyx_services/fxmanifest.lua @@ -19,6 +19,7 @@ shared_scripts { 'shared/trauma.lua', 'shared/medical.lua', 'shared/agriculture.lua', + 'shared/livestock.lua', 'shared/sabotage.lua', 'shared/metering.lua', 'shared/hospital_beds.lua', @@ -35,6 +36,7 @@ server_scripts { 'server/beds.lua', 'server/chemlabs.lua', 'server/agriculture.lua', + 'server/livestock.lua', 'server/repair_stations.lua', 'server/main.lua', 'server/exports.lua' @@ -50,6 +52,7 @@ client_scripts { 'client/beds.lua', 'client/chemlabs.lua', 'client/agriculture.lua', + 'client/livestock.lua', 'client/main.lua', 'client/exports.lua' } diff --git a/FiveM/nyx_services/server/livestock.lua b/FiveM/nyx_services/server/livestock.lua new file mode 100644 index 0000000..f5692c3 --- /dev/null +++ b/FiveM/nyx_services/server/livestock.lua @@ -0,0 +1,167 @@ +--[[ + Nyxframework: nyx_services + File: server/livestock.lua + Description: Authoritative state management, database queries, slaughterhouse + processing, cattle husbandry, and cold-chain telemetry logging. +]] + +Nyx = Nyx or {} +Nyx.Services = Nyx.Services or {} +Nyx.Services.Ranches = Nyx.Services.Ranches or {} +Nyx.Services.Slaughterhouses = Nyx.Services.Slaughterhouses or {} + +--- Initialize ranches and slaughterhouses from DB +local function loadLivestockFacilities() + local ranchRows = MySQL.query.await("SELECT * FROM `nyx_livestock_ranches`", {}) + if ranchRows then + for _, r in ipairs(ranchRows) do + Nyx.Services.Ranches[r.ranch_id] = { + ranch_id = r.ranch_id, + ranch_name = r.ranch_name, + zone_name = r.zone_name, + cattle_head = tonumber(r.cattle_head) or 50, + max_capacity = tonumber(r.max_capacity) or 200, + feed_level = tonumber(r.feed_level) or 100.0, + health_index = tonumber(r.health_index) or 100.0, + last_updated = os.time() + } + end + print(string.format("^2[Nyx:Services] Loaded %d livestock ranches.^7", #ranchRows)) + end + + local slaughterRows = MySQL.query.await("SELECT * FROM `nyx_slaughterhouses`", {}) + if slaughterRows then + for _, s in ipairs(slaughterRows) do + Nyx.Services.Slaughterhouses[s.facility_id] = { + facility_id = s.facility_id, + facility_name = s.facility_name, + cold_chain_temp_c = tonumber(s.cold_chain_temp_c) or 2.0, + cold_storage_capacity = tonumber(s.cold_storage_capacity) or 500, + cleanliness_index = tonumber(s.cleanliness_index) or 100.0, + total_carcasses_processed = tonumber(s.total_carcasses_processed) or 0 + } + end + print(string.format("^2[Nyx:Services] Loaded %d slaughterhouses.^7", #slaughterRows)) + end +end + +MySQL.ready(function() + loadLivestockFacilities() +end) + +--- Query authoritative ranch state +function Nyx.Services.GetRanchState(ranchId) + local ranch = Nyx.Services.Ranches[ranchId] + if not ranch then return nil end + + local now = os.time() + local elapsed = now - (ranch.last_updated or now) + if elapsed > 60 then + local newFeed, newHealth, gained = Nyx.Services.Livestock.CalculateCattleGrowth( + ranch.cattle_head, ranch.max_capacity, ranch.feed_level, ranch.health_index, elapsed + ) + ranch.feed_level = newFeed + ranch.health_index = newHealth + ranch.cattle_head = math.min(ranch.max_capacity, ranch.cattle_head + gained) + ranch.last_updated = now + + MySQL.update("UPDATE `nyx_livestock_ranches` SET `cattle_head` = ?, `feed_level` = ?, `health_index` = ? WHERE `ranch_id` = ?", { + ranch.cattle_head, ranch.feed_level, ranch.health_index, ranchId + }) + end + + return ranch +end + +--- Feed a cattle ranch to replenish feed level +function Nyx.Services.FeedRanch(ranchId, amount) + local ranch = Nyx.Services.Ranches[ranchId] + if not ranch then return false end + + ranch.feed_level = math.min(100.0, ranch.feed_level + (tonumber(amount) or 25.0)) + ranch.last_updated = os.time() + + MySQL.update("UPDATE `nyx_livestock_ranches` SET `feed_level` = ? WHERE `ranch_id` = ?", { + ranch.feed_level, ranchId + }) + return true +end + +--- Harvest live cattle head from ranch +function Nyx.Services.HarvestCattle(ranchId, count) + local ranch = Nyx.Services.GetRanchState(ranchId) + if not ranch then return false, 0 end + + count = math.max(1, math.floor(tonumber(count) or 1)) + if ranch.cattle_head < count then + return false, 0 + end + + ranch.cattle_head = ranch.cattle_head - count + ranch.last_updated = os.time() + + MySQL.update("UPDATE `nyx_livestock_ranches` SET `cattle_head` = ? WHERE `ranch_id` = ?", { + ranch.cattle_head, ranchId + }) + + return true, count +end + +--- Query authoritative slaughterhouse state +function Nyx.Services.GetSlaughterhouseState(facilityId) + return Nyx.Services.Slaughterhouses[facilityId] +end + +--- Authoritatively slaughter cattle into carcasses +function Nyx.Services.SlaughterCattle(facilityId, cattleAmount) + local facility = Nyx.Services.Slaughterhouses[facilityId] + if not facility then return false, "FACILITY_NOT_FOUND", 0 end + + cattleAmount = math.max(1, math.floor(tonumber(cattleAmount) or 1)) + facility.total_carcasses_processed = facility.total_carcasses_processed + (cattleAmount * 2) + + MySQL.update("UPDATE `nyx_slaughterhouses` SET `total_carcasses_processed` = ? WHERE `facility_id` = ?", { + facility.total_carcasses_processed, facilityId + }) + + return true, "SUCCESS", cattleAmount * 2 +end + +--- Authoritatively butcher raw carcass into packaged meat cuts and cold-chain pallets +function Nyx.Services.ButcherCarcass(facilityId, carcassDurability) + local facility = Nyx.Services.Slaughterhouses[facilityId] + if not facility then return false, 0, 0, "SPOILED" end + + local cuts, pallets, grade = Nyx.Services.Livestock.CalculateButcheringYield(carcassDurability, facility.cleanliness_index) + return true, cuts, pallets, grade +end + +--- Log cold-chain telemetry record to database +function Nyx.Services.LogRefrigeratedTelemetry(containerId, itemName, durability, isReeferActive, temperatureC) + local status, _, _ = Nyx.Services.Livestock.EvaluateColdChainStatus(temperatureC, isReeferActive) + + MySQL.insert("INSERT INTO `nyx_refrigerated_logs` (`container_id`, `item_name`, `durability`, `is_reefer_active`, `temperature_c`, `inspection_status`) VALUES (?, ?, ?, ?, ?, ?)", { + containerId, itemName, durability, isReeferActive and 1 or 0, temperatureC, status + }) + + return status +end + +-- Server Event Registrations +RegisterNetEvent("nyx_services:server:feedRanch", function(ranchId) + local src = source + if not ranchId then return end + local success = Nyx.Services.FeedRanch(ranchId, 25.0) + if success then + TriggerClientEvent("nyx_ui:showNotification", src, "Feed delivered to ranch successfully.", "success") + end +end) + +RegisterNetEvent("nyx_services:server:slaughterCattle", function(facilityId) + local src = source + if not facilityId then return end + local success, msg, carcassCount = Nyx.Services.SlaughterCattle(facilityId, 1) + if success then + TriggerClientEvent("nyx_ui:showNotification", src, string.format("Slaughtered 1 cattle head into %d raw carcasses.", carcassCount), "success") + end +end) diff --git a/FiveM/nyx_services/shared/livestock.lua b/FiveM/nyx_services/shared/livestock.lua new file mode 100644 index 0000000..afc30be --- /dev/null +++ b/FiveM/nyx_services/shared/livestock.lua @@ -0,0 +1,215 @@ +--[[ + Nyxframework: nyx_services + File: shared/livestock.lua + Description: Pure Lua 5.4 deterministic calculations for agricultural livestock, + cattle growth/husbandry, perishable carcass decay, cold-chain temperature + preservation, butchering conversion yields, and reefer freight payouts. +]] + +Nyx = Nyx or {} +Nyx.Services = Nyx.Services or {} +Nyx.Services.Livestock = Nyx.Services.Livestock or {} + +-- Container Cold-Chain Decay Multipliers +Nyx.Services.Livestock.ContainerMultipliers = { + ['cold_storage'] = 0.10, -- Industrial walk-in freezer (90% decay reduction) + ['refrigerated_truck'] = 0.15, -- Active reefer trailer / thermo-van + ['stash'] = 0.50, -- Standard indoor storage + ['player'] = 1.00, -- Player inventory + ['drop'] = 2.50 -- Exposed outdoor ground drop +} + +--- Calculate cattle ranch growth, feed consumption, and health updates over elapsed time +--- @param cattleHead number Current count of cattle +--- @param maxCapacity number Maximum ranch capacity +--- @param feedLevel number Current feed level percentage (0.0 to 100.0) +--- @param healthIndex number Current health index percentage (0.0 to 100.0) +--- @param elapsedSeconds number Elapsed time in seconds +--- @return number newFeedLevel, number newHealthIndex, number cattleGained +function Nyx.Services.Livestock.CalculateCattleGrowth(cattleHead, maxCapacity, feedLevel, healthIndex, elapsedSeconds) + cattleHead = math.max(0, math.floor(tonumber(cattleHead) or 0)) + maxCapacity = math.max(1, math.floor(tonumber(maxCapacity) or 100)) + feedLevel = math.max(0.0, math.min(100.0, tonumber(feedLevel) or 100.0)) + healthIndex = math.max(0.0, math.min(100.0, tonumber(healthIndex) or 100.0)) + elapsedSeconds = math.max(0, math.floor(tonumber(elapsedSeconds) or 0)) + + if elapsedSeconds <= 0 or cattleHead <= 0 then + return feedLevel, healthIndex, 0 + end + + local hours = elapsedSeconds / 3600.0 + + -- Feed consumption rate: 0.05% feed per cattle head per hour + local feedConsumed = hours * (cattleHead * 0.05) + local newFeedLevel = math.max(0.0, math.floor((feedLevel - feedConsumed) * 100.0 + 0.5) / 100.0) + + -- Health dynamics: If fed (>10%), health regenerates by 5%/hr up to 100%. If starved, health degrades by 10%/hr. + local newHealthIndex = healthIndex + if newFeedLevel > 10.0 then + newHealthIndex = math.min(100.0, math.floor((healthIndex + (hours * 5.0)) * 100.0 + 0.5) / 100.0) + else + newHealthIndex = math.max(10.0, math.floor((healthIndex - (hours * 10.0)) * 100.0 + 0.5) / 100.0) + end + + -- Cattle growth rate: If health >= 75% and feed > 20%, gains 1 head per 50 head per 2 hours + local cattleGained = 0 + if newHealthIndex >= 75.0 and newFeedLevel >= 20.0 and cattleHead < maxCapacity then + local baseGrowth = (cattleHead / 50.0) * (hours / 2.0) + cattleGained = math.floor(baseGrowth) + if cattleHead + cattleGained > maxCapacity then + cattleGained = maxCapacity - cattleHead + end + end + + return newFeedLevel, newHealthIndex, cattleGained +end + +--- Calculate perishable carcass / meat durability decay and temperature protection +--- @param durability number Current item durability (0.0 to 100.0) +--- @param decayRate number Base decay rate (e.g. 0.12) +--- @param elapsedSeconds number Time elapsed in seconds +--- @param containerType string Container type archetype ('cold_storage', 'refrigerated_truck', 'stash', 'player', 'drop') +--- @param isReeferActive boolean Whether active refrigeration unit is turned on +--- @param ambientTempC number Ambient temperature in degrees Celsius +--- @return number newDurability, boolean isSpoiled, string tier, number loss +function Nyx.Services.Livestock.CalculateCarcassDecay(durability, decayRate, elapsedSeconds, containerType, isReeferActive, ambientTempC) + durability = math.max(0.0, math.min(100.0, tonumber(durability) or 100.0)) + decayRate = math.max(0.0, tonumber(decayRate) or 0.08) + elapsedSeconds = math.max(0, math.floor(tonumber(elapsedSeconds) or 0)) + ambientTempC = tonumber(ambientTempC) or 22.0 + + if elapsedSeconds <= 0 or durability <= 0.0 then + local spoiled = durability <= 0.0 + local tier = spoiled and "SPOILED" or (durability >= 75.0 and "PRISTINE" or (durability >= 50.0 and "FRESH" or "STALE")) + return durability, spoiled, tier, 0.0 + end + + local mult = Nyx.Services.Livestock.ContainerMultipliers[containerType] or 1.00 + + -- Refrigeration unit override + if isReeferActive or ambientTempC <= 4.0 then + mult = math.min(mult, 0.10) + end + + local rawLoss = (elapsedSeconds / 3600.0) * decayRate * 100.0 * mult + local loss = math.max(0.0, math.floor(rawLoss * 10.0 + 0.5) / 10.0) + local newDurability = math.max(0.0, math.floor((durability - loss) * 10.0 + 0.5) / 10.0) + + local isSpoiled = newDurability <= 0.0 + local tier = "SPOILED" + if not isSpoiled then + if newDurability >= 75.0 then + tier = "PRISTINE" + elseif newDurability >= 50.0 then + tier = "FRESH" + else + tier = "STALE" + end + end + + return newDurability, isSpoiled, tier, loss +end + +--- Evaluate cold chain operational status and temperature compliance +--- @param currentTempC number Actual cold storage temperature in C +--- @param isReeferActive boolean Whether refrigeration power unit is running +--- @return string status, number targetTemp, number currentTemp +function Nyx.Services.Livestock.EvaluateColdChainStatus(currentTempC, isReeferActive) + currentTempC = math.floor((tonumber(currentTempC) or 2.0) * 10.0 + 0.5) / 10.0 + local targetTemp = 2.0 + + if not isReeferActive then + return "CRITICAL_SPOILAGE", targetTemp, currentTempC + end + + if currentTempC <= 4.0 then + return "PRISTINE", targetTemp, currentTempC + elseif currentTempC <= 8.0 then + return "FRESH", targetTemp, currentTempC + elseif currentTempC <= 12.0 then + return "DEGRADED", targetTemp, currentTempC + else + return "CRITICAL_SPOILAGE", targetTemp, currentTempC + end +end + +--- Calculate slaughtering & butchering yield output based on carcass state and facility cleanliness +--- @param carcassDurability number Carcass durability percentage +--- @param cleanlinessIndex number Slaughterhouse cleanliness percentage +--- @return number cutsAmount, number palletAmount, string qualityGrade +function Nyx.Services.Livestock.CalculateButcheringYield(carcassDurability, cleanlinessIndex) + carcassDurability = math.max(0.0, math.min(100.0, tonumber(carcassDurability) or 100.0)) + cleanlinessIndex = math.max(0.0, math.min(100.0, tonumber(cleanlinessIndex) or 100.0)) + + if carcassDurability <= 0.0 then + return 0, 0, "SPOILED" + end + + local durabilityFactor = carcassDurability / 100.0 + local cleanFactor = cleanlinessIndex / 100.0 + + local baseCuts = 10 + local basePallets = 1 + + local cutsAmount = math.max(1, math.floor(baseCuts * durabilityFactor * cleanFactor + 0.5)) + local palletAmount = (carcassDurability >= 50.0 and cleanlinessIndex >= 50.0) and 1 or 0 + + local qualityGrade = "PRISTINE" + local combinedScore = (durabilityFactor * 0.6) + (cleanFactor * 0.4) + if combinedScore < 0.40 then + qualityGrade = "SPOILED" + elseif combinedScore < 0.65 then + qualityGrade = "STALE" + elseif combinedScore < 0.85 then + qualityGrade = "FRESH" + end + + return cutsAmount, palletAmount, qualityGrade +end + +--- Calculate reefer cold-chain freight haulage payout, bond returns, and spoilage penalties +--- @param basePayout number Contract base payment +--- @param averageDurability number Average durability of delivered perishable freight +--- @param transitSeconds number Elapsed time in transit +--- @param deadlineSeconds number Contract max allowed transit time +--- @return number finalPayout, number escrowReturn, number penaltyDeduction, number bonus +function Nyx.Services.Livestock.CalculateRefrigeratedFreightPayout(basePayout, averageDurability, transitSeconds, deadlineSeconds) + basePayout = math.max(0.0, tonumber(basePayout) or 1000.0) + averageDurability = math.max(0.0, math.min(100.0, tonumber(averageDurability) or 100.0)) + transitSeconds = math.max(0, math.floor(tonumber(transitSeconds) or 0)) + deadlineSeconds = math.max(1, math.floor(tonumber(deadlineSeconds) or 1800)) + + local escrowBond = math.floor(basePayout * 0.15 * 100.0 + 0.5) / 100.0 + + if averageDurability <= 0.0 then + -- Total spoilage: Payout is 0, bond is forfeited as penalty + return 0.0, 0.0, escrowBond, 0.0 + end + + local durabilityMult = 1.00 + if averageDurability >= 90.0 then + durabilityMult = 1.10 -- 10% pristine cold-chain bonus + elseif averageDurability >= 75.0 then + durabilityMult = 1.00 + elseif averageDurability >= 50.0 then + durabilityMult = 0.75 -- 25% stale penalty + else + durabilityMult = 0.40 -- 60% severe decay penalty + end + + local timeMult = 1.00 + if transitSeconds > deadlineSeconds then + timeMult = 0.70 -- 30% late delivery penalty + end + + local bonus = 0.0 + if averageDurability >= 90.0 and transitSeconds <= (deadlineSeconds * 0.75) then + bonus = math.floor(basePayout * 0.15 * 100.0 + 0.5) / 100.0 -- 15% express cold-chain bonus + end + + local rawPayout = (basePayout * durabilityMult * timeMult) + bonus + local finalPayout = math.floor(rawPayout * 100.0 + 0.5) / 100.0 + local penaltyDeduction = math.max(0.0, math.floor((basePayout - (basePayout * durabilityMult * timeMult)) * 100.0 + 0.5) / 100.0) + + return finalPayout, escrowBond, penaltyDeduction, bonus +end diff --git a/FiveM/nyx_services/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql b/FiveM/nyx_services/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql new file mode 100644 index 0000000..671c59f --- /dev/null +++ b/FiveM/nyx_services/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql @@ -0,0 +1,73 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_logistics, nyx_vehicles & nyx_inventory +-- Migration: 067_livestock_slaughterhouse_and_refrigerated_logistics.sql +-- Description: Agricultural livestock husbandry, slaughterhouse processing, +-- carcass decay, refrigerated cold-chain logistics, and reefer +-- freight haulage tracking. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create livestock ranches table +CREATE TABLE IF NOT EXISTS `nyx_livestock_ranches` ( + `ranch_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `ranch_name` VARCHAR(64) NOT NULL, + `zone_name` VARCHAR(64) NOT NULL, + `cattle_head` INT NOT NULL DEFAULT 50, + `max_capacity` INT NOT NULL DEFAULT 200, + `feed_level` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `health_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_ranch_zone` (`zone_name`), + INDEX `idx_ranch_health` (`health_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create slaughterhouse facilities table +CREATE TABLE IF NOT EXISTS `nyx_slaughterhouses` ( + `facility_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `facility_name` VARCHAR(64) NOT NULL, + `cold_chain_temp_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `cold_storage_capacity` INT NOT NULL DEFAULT 500, + `cleanliness_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_carcasses_processed` INT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_slaughter_temp` (`cold_chain_temp_c`), + INDEX `idx_slaughter_clean` (`cleanliness_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create refrigerated cold-chain telemetry logs table +CREATE TABLE IF NOT EXISTS `nyx_refrigerated_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `container_id` VARCHAR(64) NOT NULL, + `item_name` VARCHAR(64) NOT NULL, + `durability` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `is_reefer_active` TINYINT(1) NOT NULL DEFAULT 1, + `temperature_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `inspection_status` ENUM('PRISTINE', 'FRESH', 'STALE', 'SPOILED') NOT NULL DEFAULT 'PRISTINE', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_reefer_container` (`container_id`), + INDEX `idx_reefer_item` (`item_name`), + INDEX `idx_reefer_status` (`inspection_status`), + INDEX `idx_reefer_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Seed regional livestock ranches +INSERT INTO `nyx_livestock_ranches` + (`ranch_id`, `ranch_name`, `zone_name`, `cattle_head`, `max_capacity`, `feed_level`, `health_index`) +VALUES + ('ranch_grapeseed', 'O\'Neil Grapeseed Livestock Ranch', 'Grapeseed', 120, 200, 100.00, 100.00), + ('ranch_paleto', 'Paleto Bay Coastal Cattle Ranch', 'Paleto Bay', 80, 150, 95.00, 98.00) +ON DUPLICATE KEY UPDATE `ranch_name` = VALUES(`ranch_name`); + +-- 5. Seed regional slaughterhouse facilities +INSERT INTO `nyx_slaughterhouses` + (`facility_id`, `facility_name`, `cold_chain_temp_c`, `cold_storage_capacity`, `cleanliness_index`, `total_carcasses_processed`) +VALUES + ('slaughterhouse_cypress_flats', 'Cypress Flats Industrial Slaughterhouse', 2.0, 500, 100.00, 0), + ('slaughterhouse_paleto', 'Paleto Meat Processing Plant', 1.5, 300, 96.50, 0) +ON DUPLICATE KEY UPDATE `facility_name` = VALUES(`facility_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_vehicles/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql b/FiveM/nyx_vehicles/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql new file mode 100644 index 0000000..671c59f --- /dev/null +++ b/FiveM/nyx_vehicles/sql/migrations/067_livestock_slaughterhouse_and_refrigerated_logistics.sql @@ -0,0 +1,73 @@ +-- ============================================================================ +-- Nyxframework: nyx_services, nyx_logistics, nyx_vehicles & nyx_inventory +-- Migration: 067_livestock_slaughterhouse_and_refrigerated_logistics.sql +-- Description: Agricultural livestock husbandry, slaughterhouse processing, +-- carcass decay, refrigerated cold-chain logistics, and reefer +-- freight haulage tracking. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create livestock ranches table +CREATE TABLE IF NOT EXISTS `nyx_livestock_ranches` ( + `ranch_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `ranch_name` VARCHAR(64) NOT NULL, + `zone_name` VARCHAR(64) NOT NULL, + `cattle_head` INT NOT NULL DEFAULT 50, + `max_capacity` INT NOT NULL DEFAULT 200, + `feed_level` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `health_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_ranch_zone` (`zone_name`), + INDEX `idx_ranch_health` (`health_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create slaughterhouse facilities table +CREATE TABLE IF NOT EXISTS `nyx_slaughterhouses` ( + `facility_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `facility_name` VARCHAR(64) NOT NULL, + `cold_chain_temp_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `cold_storage_capacity` INT NOT NULL DEFAULT 500, + `cleanliness_index` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `total_carcasses_processed` INT NOT NULL DEFAULT 0, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_slaughter_temp` (`cold_chain_temp_c`), + INDEX `idx_slaughter_clean` (`cleanliness_index`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Create refrigerated cold-chain telemetry logs table +CREATE TABLE IF NOT EXISTS `nyx_refrigerated_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `container_id` VARCHAR(64) NOT NULL, + `item_name` VARCHAR(64) NOT NULL, + `durability` DECIMAL(5, 2) NOT NULL DEFAULT 100.00, + `is_reefer_active` TINYINT(1) NOT NULL DEFAULT 1, + `temperature_c` DECIMAL(4, 1) NOT NULL DEFAULT 2.0, + `inspection_status` ENUM('PRISTINE', 'FRESH', 'STALE', 'SPOILED') NOT NULL DEFAULT 'PRISTINE', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_reefer_container` (`container_id`), + INDEX `idx_reefer_item` (`item_name`), + INDEX `idx_reefer_status` (`inspection_status`), + INDEX `idx_reefer_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 4. Seed regional livestock ranches +INSERT INTO `nyx_livestock_ranches` + (`ranch_id`, `ranch_name`, `zone_name`, `cattle_head`, `max_capacity`, `feed_level`, `health_index`) +VALUES + ('ranch_grapeseed', 'O\'Neil Grapeseed Livestock Ranch', 'Grapeseed', 120, 200, 100.00, 100.00), + ('ranch_paleto', 'Paleto Bay Coastal Cattle Ranch', 'Paleto Bay', 80, 150, 95.00, 98.00) +ON DUPLICATE KEY UPDATE `ranch_name` = VALUES(`ranch_name`); + +-- 5. Seed regional slaughterhouse facilities +INSERT INTO `nyx_slaughterhouses` + (`facility_id`, `facility_name`, `cold_chain_temp_c`, `cold_storage_capacity`, `cleanliness_index`, `total_carcasses_processed`) +VALUES + ('slaughterhouse_cypress_flats', 'Cypress Flats Industrial Slaughterhouse', 2.0, 500, 100.00, 0), + ('slaughterhouse_paleto', 'Paleto Meat Processing Plant', 1.5, 300, 96.50, 0) +ON DUPLICATE KEY UPDATE `facility_name` = VALUES(`facility_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/tests/test_livestock_slaughterhouse_and_refrigerated_logistics.py b/tests/test_livestock_slaughterhouse_and_refrigerated_logistics.py new file mode 100644 index 0000000..5942839 --- /dev/null +++ b/tests/test_livestock_slaughterhouse_and_refrigerated_logistics.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +Unit and Integration Tests for Agricultural Livestock, Slaughterhouse Processing & Cold-Chain Logistics. +Covers: Migration 067, nyx_inventory item registrations (livestock_cattle, carcass_beef_raw, packaged_meat_cuts, etc.), + nyx_logistics recipes and extraction nodes, Pure Lua 5.4 Livestock Engine math, + Cold-chain decay, reefer temperature evaluation, butchering yields, freight payouts, and Test 62 harness. +""" + +import os +import sys +import json +import unittest + +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +FIVEM_DIR = os.path.join(ROOT_DIR, "FiveM") + + +class TestLivestockSlaughterhouseAndRefrigeratedLogistics(unittest.TestCase): + def test_migration_067_exists_and_registered(self): + """Verify migration 067 exists on disk, mirrored, and registered in index.json.""" + index_path = os.path.join(FIVEM_DIR, "nyx_core", "sql", "migrations", "index.json") + self.assertTrue(os.path.isfile(index_path), "Migration index.json missing") + with open(index_path, "r", encoding="utf-8") as f: + migrations = json.load(f) + + self.assertIn("067_livestock_slaughterhouse_and_refrigerated_logistics.sql", migrations) + + core_mig = os.path.join(FIVEM_DIR, "nyx_core", "sql", "migrations", "067_livestock_slaughterhouse_and_refrigerated_logistics.sql") + services_mig = os.path.join(FIVEM_DIR, "nyx_services", "sql", "migrations", "067_livestock_slaughterhouse_and_refrigerated_logistics.sql") + logistics_mig = os.path.join(FIVEM_DIR, "nyx_logistics", "sql", "migrations", "067_livestock_slaughterhouse_and_refrigerated_logistics.sql") + + self.assertTrue(os.path.isfile(core_mig), "Core migration 067 missing") + self.assertTrue(os.path.isfile(services_mig), "nyx_services migration 067 missing") + self.assertTrue(os.path.isfile(logistics_mig), "nyx_logistics migration 067 missing") + + with open(core_mig, "r", encoding="utf-8") as f: + sql = f.read() + + self.assertIn("nyx_livestock_ranches", sql) + self.assertIn("nyx_slaughterhouses", sql) + self.assertIn("nyx_refrigerated_logs", sql) + self.assertIn("ranch_grapeseed", sql) + self.assertIn("slaughterhouse_cypress_flats", sql) + + def test_fxmanifest_declarations(self): + """Verify fxmanifest.lua declares shared, server, and client livestock scripts.""" + manifest_path = os.path.join(FIVEM_DIR, "nyx_services", "fxmanifest.lua") + self.assertTrue(os.path.isfile(manifest_path)) + with open(manifest_path, "r", encoding="utf-8") as f: + manifest = f.read() + + self.assertIn("shared/livestock.lua", manifest) + self.assertIn("server/livestock.lua", manifest) + self.assertIn("client/livestock.lua", manifest) + + self.assertTrue(os.path.isfile(os.path.join(FIVEM_DIR, "nyx_services", "shared", "livestock.lua"))) + self.assertTrue(os.path.isfile(os.path.join(FIVEM_DIR, "nyx_services", "server", "livestock.lua"))) + self.assertTrue(os.path.isfile(os.path.join(FIVEM_DIR, "nyx_services", "client", "livestock.lua"))) + + def test_inventory_items_registered(self): + """Verify livestock, raw carcass, packaged meat, and reefer pallet items are registered.""" + items_path = os.path.join(FIVEM_DIR, "nyx_inventory", "shared", "items.lua") + self.assertTrue(os.path.isfile(items_path)) + with open(items_path, "r", encoding="utf-8") as f: + content = f.read() + + required_items = [ + "livestock_cattle", + "carcass_beef_raw", + "packaged_meat_cuts", + "pallet_refrigerated_beef", + "livestock_feed_sack" + ] + + for item in required_items: + self.assertIn(f"['{item}']", content, f"Item {item} missing from inventory registry") + + def test_logistics_recipes_and_nodes_registered(self): + """Verify nyx_logistics recipes and extraction nodes include cattle processing.""" + recipes_path = os.path.join(FIVEM_DIR, "nyx_logistics", "shared", "recipes.lua") + with open(recipes_path, "r", encoding="utf-8") as f: + recipes = f.read() + + self.assertIn("cattle_slaughtering", recipes) + self.assertIn("carcass_butchering", recipes) + self.assertIn("livestock_feed_mixing", recipes) + + nodes_path = os.path.join(FIVEM_DIR, "nyx_logistics", "shared", "nodes.lua") + with open(nodes_path, "r", encoding="utf-8") as f: + nodes = f.read() + + self.assertIn("ranch_grapeseed", nodes) + self.assertIn("ranch_paleto", nodes) + + contracts_path = os.path.join(FIVEM_DIR, "nyx_logistics", "shared", "contracts.lua") + with open(contracts_path, "r", encoding="utf-8") as f: + contracts = f.read() + + self.assertIn("terminal_cypress_slaughterhouse", contracts) + + def test_cattle_growth_and_husbandry_math(self): + """Verify cattle ranch growth and feed consumption formulas.""" + def calc_cattle_growth(cattle_head, max_capacity, feed_level, health_index, elapsed_seconds): + if elapsed_seconds <= 0 or cattle_head <= 0: + return feed_level, health_index, 0 + hours = elapsed_seconds / 3600.0 + feed_consumed = hours * (cattle_head * 0.05) + new_feed = max(0.0, round(feed_level - feed_consumed, 2)) + if new_feed > 10.0: + new_health = min(100.0, round(health_index + (hours * 5.0), 2)) + else: + new_health = max(10.0, round(health_index - (hours * 10.0), 2)) + + gained = 0 + if new_health >= 75.0 and new_feed >= 20.0 and cattle_head < max_capacity: + base_growth = (cattle_head / 50.0) * (hours / 2.0) + gained = int(base_growth) + if cattle_head + gained > max_capacity: + gained = max_capacity - cattle_head + return new_feed, new_health, gained + + f1, h1, g1 = calc_cattle_growth(100, 200, 100.0, 100.0, 3600) + self.assertEqual(f1, 95.0) + self.assertEqual(h1, 100.0) + self.assertEqual(g1, 1) + + def test_carcass_decay_and_cold_chain(self): + """Verify cold-chain perishable decay formulas.""" + multipliers = { + "cold_storage": 0.10, + "refrigerated_truck": 0.15, + "stash": 0.50, + "player": 1.00, + "drop": 2.50 + } + + def calc_carcass_decay(durability, decay_rate, seconds, container_type, is_reefer_active, temp_c): + mult = multipliers.get(container_type, 1.00) + if is_reefer_active or temp_c <= 4.0: + mult = min(mult, 0.10) + raw_loss = (seconds / 3600.0) * decay_rate * 100.0 * mult + loss = max(0.0, round(raw_loss, 1)) + new_dur = max(0.0, round(durability - loss, 1)) + is_spoiled = new_dur <= 0.0 + tier = "SPOILED" if is_spoiled else ("PRISTINE" if new_dur >= 75.0 else ("FRESH" if new_dur >= 50.0 else "STALE")) + return new_dur, is_spoiled, tier, loss + + # Cold storage 1hr -> 98.8% + d_cold, sp_cold, t_cold, _ = calc_carcass_decay(100.0, 0.12, 3600, "cold_storage", True, 2.0) + self.assertEqual(d_cold, 98.8) + self.assertEqual(t_cold, "PRISTINE") + self.assertFalse(sp_cold) + + # Drop 1hr -> 70.0% + d_drop, sp_drop, t_drop, _ = calc_carcass_decay(100.0, 0.12, 3600, "drop", False, 25.0) + self.assertEqual(d_drop, 70.0) + self.assertEqual(t_drop, "FRESH") + + def test_cold_chain_status_evaluation(self): + """Verify cold storage temperature compliance statuses.""" + def eval_cold_chain(temp_c, is_reefer_active): + if not is_reefer_active or temp_c > 12.0: + return "CRITICAL_SPOILAGE" + if temp_c <= 4.0: + return "PRISTINE" + elif temp_c <= 8.0: + return "FRESH" + else: + return "DEGRADED" + + self.assertEqual(eval_cold_chain(2.0, True), "PRISTINE") + self.assertEqual(eval_cold_chain(6.0, True), "FRESH") + self.assertEqual(eval_cold_chain(10.0, True), "DEGRADED") + self.assertEqual(eval_cold_chain(15.0, False), "CRITICAL_SPOILAGE") + + def test_refrigerated_freight_payout_math(self): + """Verify reefer freight payout, escrow return, bonus, and penalty calculations.""" + def calc_freight_payout(base_payout, avg_durability, transit_sec, deadline_sec): + escrow = round(base_payout * 0.15, 2) + if avg_durability <= 0.0: + return 0.0, 0.0, escrow, 0.0 + + dur_mult = 1.10 if avg_durability >= 90.0 else (1.00 if avg_durability >= 75.0 else (0.75 if avg_durability >= 50.0 else 0.40)) + time_mult = 0.70 if transit_sec > deadline_sec else 1.00 + bonus = round(base_payout * 0.15, 2) if (avg_durability >= 90.0 and transit_sec <= (deadline_sec * 0.75)) else 0.0 + + final_payout = round((base_payout * dur_mult * time_mult) + bonus, 2) + penalty = max(0.0, round(base_payout - (base_payout * dur_mult * time_mult), 2)) + return final_payout, escrow, penalty, bonus + + p1, b1, pen1, bon1 = calc_freight_payout(1000.0, 95.0, 1200, 1800) + self.assertEqual(p1, 1250.0) + self.assertEqual(b1, 150.0) + self.assertEqual(bon1, 150.0) + + p2, b2, pen2, bon2 = calc_freight_payout(1000.0, 0.0, 1200, 1800) + self.assertEqual(p2, 0.0) + self.assertEqual(b2, 0.0) + self.assertEqual(pen2, 150.0) + + def test_in_engine_test_62_registration(self): + """Verify Test 62 is registered in nyx_core tests.lua.""" + tests_lua_path = os.path.join(FIVEM_DIR, "nyx_core", "server", "tests.lua") + self.assertTrue(os.path.isfile(tests_lua_path)) + with open(tests_lua_path, "r", encoding="utf-8") as f: + tests_lua = f.read() + + self.assertIn("TestLivestockSlaughterhouseAndRefrigeratedLogistics", tests_lua) + self.assertIn("t62 = Nyx.Tests.TestLivestockSlaughterhouseAndRefrigeratedLogistics()", tests_lua) + + +if __name__ == "__main__": + unittest.main()