> For the complete documentation index, see [llms.txt](https://docs.kuzquality.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kuzquality.com/resources/premium-resources/drug-dealing/config.md).

# Config

```lua
Config = {}

-- Enabling this will add additional prints and debug visuals
Config.debug = false

-- Available locales: de, en, es, fr, nl, no, pl, pt-br
-- Edit and add more in `/locales`
Config.locale = 'en'

-- The money accounts used for payouts. Use the names your framework uses
-- (kq_link will try to automatically translate them, only change these if you encounter issues)
Config.accounts = {
    cash = 'money',
    bank = 'bank',
}

Config.editor = {
    -- Command that opens the admin panel
    -- Access is controlled through the ACE permission 'command.drugdeals:admin'
    command = 'drugdeals:admin',
}

-- Props held during the hand-over animation
Config.dealProps = {
    enabled = true,

    -- Whether the props should be visible to other players
    networked = true,

    -- Spawn the props on the server instead of the client. (Useful when using lockdown mode relaxed/strict)
    serverSpawned = true,

    -- The prop the buyer hands back
    cash = {
        model = 'prop_cash_pile_02',
        bone = 28422,
        offset = vec3(0.1, 0.04, 0.0),
        rotation = vec3(25.0, 0.0, 10.0),
    },

    -- Selectable per drug in the admin panel. The first entry is the default for drugs with no prop selected
    drugs = {
        {
            model = 'prop_drug_package_02',
            label = 'Drug package',
            bone = 28422,
            offset = vec3(0.04, 0.06, -0.01),
            rotation = vec3(135.0, -135.0, 40.0),
        },
        {
            model = 'hei_prop_hei_drug_pack_02',
            label = 'Drug package 2',
            bone = 28422,
            offset = vec3(0.04, 0.06, -0.01),
            rotation = vec3(135.0, -135.0, 40.0),
        },
        {
            model = 'prop_meth_bag_01',
            label = 'Meth bag',
            bone = 28422,
            offset = vec3(0.1, 0.0, -0.01),
            rotation = vec3(135.0, -100.0, 40.0),
        },
        {
            model = 'prop_paper_bag_small',
            label = 'Paper bag',
            bone = 28422,
            offset = vec3(0.0991, -0.0464, -0.0843),
            rotation = vec3(-48.9, -7.65, 16.91),
        },
        {
            model = 'xm3_prop_xm3_bag_weed_01a',
            label = 'Large weed bag',
            bone = 28422,
            offset = vec3(0.1184, 0.0537, -0.0732),
            rotation = vec3(28.57, 46.51, 43.07)
        },
        {
            model = 'p_cs_joint_02',
            label = 'Joint',
            bone = 28422,
            offset = vec3(0.06, 0.02, 0.0),
            rotation = vec3(0.0, 0.0, 0.0),
        },
        {
            model = 'p_cs_script_bottle_s',
            label = 'Pills Bottle',
            bone = 28422,
            offset = vec3(0.0530, -0.0489, -0.0653),
            rotation = vec3(-66.01, 11.50, -8.29)
        },
        {
            model = 'hei_prop_pill_bag_01',
            label = 'Larger pills bag',
            bone = 28422,
            offset = vec3(0.0916, 0.0592, -0.1003),
            rotation = vec3(-72.96, 80.06, 28.76)
        },
        {
            model = 'prop_coke_block_01',
            label = 'Coke brick',
            bone = 28422,
            offset = vec3(0.0122, -0.0025, -0.0847),
            rotation = vec3(-132.65, 101.65, 11.77)
        },
        {
            model = 'prop_weed_block_01',
            label = 'Weed brick',
            bone = 28422,
            offset = vec3(0.0515, 0.0185, -0.0753),
            rotation = vec3(-116.39, -168.34, -43.35)
        },
        {
            model = 'h4_prop_h4_cash_bag_01a',
            label = 'Money bag',
            bone = 28422,
            offset = vec3(0.0928, -0.0087, -0.0955),
            rotation = vec3(-51.70, -178.76, -25.52)
        },
        {
            model = 'prop_box_guncase_02a',
            label = 'Weapon case',
            bone = 28422,
            offset = vec3(0.0953, -0.0158, -0.1318),
            rotation = vec3(141.67, 174.68, -33.37)
        },
    },
}

-- Global market price drift. Selling pushes a drug's price down, prices recover back toward base over time. Server-wide, saved in the database
Config.market = {
    -- How far a price can stray from its base in either direction (0-100)
    -- 20 = prices stay between 80% and 120% of the base price
    maxFluctuation = 20,

    -- Minutes between market recalculations. Sales are tallied in-between and repriced all at once
    recalcInterval = 20,

    -- The maximum price change in % per single recalculation (0-100)
    maxStep = 3,
}

-- Street deals
Config.street = {
    enabled = true,

    -- Whether to hide the "~$120" market hint in the offer popup
    hideMarketPrice = false,

    npc = {
        -- Ped models that can not be dealt with
        blacklistedModels = {
            's_m_m_fibsec_01',
            's_m_y_fireman_01',
            's_m_y_mime',
            's_m_y_hwaycop_01',
            's_m_y_marine_01',
            's_m_y_marine_02',
            's_m_y_marine_03',
            's_m_m_marine_01',
            's_m_y_ranger_01',
            's_m_y_sheriff_01',
            's_m_m_strperf_01',
            's_m_m_security_01',
            's_m_m_armoured_01',
            's_m_m_armoured_02',
            'csb_trafficwarden',
            's_f_y_cop_01',
            's_f_y_ranger_01',
            's_m_y_cop_01',
        },
    },

    -- NPCs refuse to deal when too many other people stand around them
    crowd = {
        enabled = true,
        radius = 15.0,

        -- The buyer refuses once more than this many peds are nearby
        maxPeds = 4,
    },

    -- A street deal consists of two separate rolls:
    -- - Approach, whether the NPC is willing to talk at all
    -- - Offer, whether they accept the offered price for the drug
    accept = {
        approach = {
            -- Base chance an NPC is willing to talk (0-100) (chance = base * timeOfDay)
            base = 75,

            -- Time of day windows (24h format)
            timeOfDay = {
                -- More willing at night
                { from = 21, to = 5, multiplier = 1.2 },

                -- Less willing in the morning/afternoon
                { from = 7, to = 14, multiplier = 0.7 },
            },
        },

        offer = {
            -- Accept chance based on the asking price vs the market value, per unit
            greedCurve = {
                { ratio = 0.1, multiplier = 3.0 }, -- npcs are 3x more likely to buy the drug is player sells it for 10% of its value
                { ratio = 0.5, multiplier = 1.5 },
                { ratio = 1.0, multiplier = 1.0 },
                { ratio = 1.2, multiplier = 0.6 },
                { ratio = 1.5, multiplier = 0.25 },
                { ratio = 2.0, multiplier = 0.1 }, -- npcs have 0.1x chance to buy drug that's priced at double of the market price
            },
        },
    },

    cooldowns = {
        -- Seconds between street deals for each player
        global = 2,
    },
}

-- Corner selling
-- Admin-placed corner zones where the player posts up and buyers come to them
Config.corner = {
    enabled = true,

    -- Whether buyers may be SPAWNED when no ambient NPC qualifies nearby
    -- Disabled = corners only ever use naturally existing ambient NPCs
    spawnBuyers = true,

    -- Blip on a corner, only shown while carrying one of its drugs and within
    -- viewDistance (meters) of the corner
    blip = {
        enabled = true,
        sprite = 465,
        color = 66,
        scale = 0.7,
        viewDistance = 100.0,
        areaAlpha = 80,
    },

    -- Idle animations played while posted up, one rolled per post-up
    -- Walking breaks the pose; an animation the ped rejects falls to the next roll
    idleAnims = {
        { dict = 'anim@heists@heist_corona@single_team', anim = 'single_team_loop_boss' },
        { dict = 'missbigscore2aig_3', anim = 'wait_for_van_c' },
        { dict = 'rcmnigel1a', anim = 'base' },
    },
}

-- Jobs that count as police
-- Used by the witness dispatch and the minimum police requirements
Config.policeJobs = {
    'police',
    'sheriff',
}

-- NPCs witnessing a completed street sale may call the police
Config.dispatch = {
    enabled = true,
    witnesses = {
        -- Chance per nearby NPC (0-100), the first successful roll becomes the witness
        chancePerPed = 5,
        -- Radius at which npcs will have a chance to call the dispatch
        radius = 25.0,

        -- Seconds between two dispatch calls (per player)
        cooldown = 180,
    },

    -- Only used for non-custom dispatch system (configured in kq_link)
    blip = {
        sprite = 51,
        color = 1,
        scale = 1.0,
        flash = true,
    },
}

Config.app = {
    -- Usable items that open the dealer app. The plug phone is a modern smartphone the trap phone is an old-school burner phone
    -- On standalone servers, with no items, the item names get registered as commands instead (/kq_plugphone, /kq_trapphone)
    phoneItem = {
        enabled = true,
        item = 'kq_plugphone',
    },
    classicPhoneItem = {
        enabled = true,
        item = 'kq_trapphone',
    },

    -- The custom app registered into the server's phone resource.
    -- Phones are wired up in shared/editable/phoneIntegrations.lua
    -- Supported phone resources:
    -- lb-phone
    -- lb-tablet
    -- yseries
    -- qs-smartphone-pro
    -- gksphone
    -- high-phone
    -- sd-phone
    -- codem-phone
    -- 17mov_Phone
    -- roadphone (manual setup on the phone's side, see ROADPHONE_APP.md)
    customApp = {
        -- Whether to register the app into supported phone resources at all
        enabled = true,

        -- Whether to push notifications through the phone
        notifications = true,
    },

    -- Whether the trap phone screen should react to mouse clicks
    -- When disabled the phone is usable by its keypad and keyboard inputs
    classicTouchScreen = false,

    -- Whether to remove the Prices tab from the app
    hideMarketPrices = false,

    -- How many buyer conversations can run at the same time
    maxActiveThreads = 4,

    -- Seconds before an unanswered buyer walks away
    threadExpiry = 600,

    -- Star rating tiers. Stars go from 1.0 to 5.0 and get floored (2.7 counts as tier 2)
    -- priceTolerance multiplies the market price into the maximum buyers will accept
    starTiers = {
        [1] = { requestsPerHour = 3, priceTolerance = 0.85 },
        [2] = { requestsPerHour = 3, priceTolerance = 0.9 },
        [3] = { requestsPerHour = 4, priceTolerance = 1.0 },
        [4] = { requestsPerHour = 5, priceTolerance = 1.05 },
        [5] = { requestsPerHour = 6, priceTolerance = 1.15 },
    },

    -- Rating changes after every finished deal
    rating = {
        -- Delivered within this percentage of the buyer's deadline (0-100)
        fast = { within = 40, change = 0.05 },

        -- Delivered comfortably within it
        steady = { within = 70, change = 0.04 },

        -- Delivered close to the deadline
        slow = { change = 0.025 },

        -- The buyer gave up waiting, or the package was stolen before they arrived
        failed = { change = -0.08 },

        -- The player declined the buyer, or haggled until they walked off
        declined = { change = -0.02 },

        -- The buyer was ignored or left on read
        expired = { change = -0.005 },
    },

    -- (For Drugs using Metadata pricing only!)
    -- App buyers never ask for a specific grade/quality of a drug
    -- When delivering they expect at least "Neutral" satisfaction of the drug.
    -- Anything above will give extra standing. Anything below will reduce the standing gain
    quality = {
        -- Star change per quality class above/below the reference, applied to the delivered average
        -- For example, a drug with meta satisfaction of "Great" will receive + (2 * starsPerLevel) on the rating
        -- in this case +0.04 total rating on top of the deal rating gain
        starsPerLevel = 0.02,

        -- Contact standing change per quality class (used for drugs with metadata pricing + satisfaction levels)
        -- Same as "starsPerLevel", but on the contact standing instead when dealing with saved contacts
        standingPerLevel = 4,

        -- Added to the post-deal contact offer chance (0-100) per class above the reference
        -- The chance of the buyer to add the player to contacts based on the satisfaction of the drug
        -- When "addChancePerLevel = 8", when delivering "Great" satisfaction of a drug, the chances of the buyer requesting to be saved as contact are +16%
        addChancePerLevel = 8,
    },

    -- Reply delay in seconds
    replyDelay = {
        min = 6,
        max = 15,
    },

    -- The buyer sends a message once this many seconds are left for their delivery
    lowTimeWarning = 240,

    -- Buyer personas
    -- chance = how often this persona shows up, relative to the others
    -- budget = the buyer's hidden per-unit price ceiling as a multiple of the market value
    -- patience = how many counter offers they sit through before walking away
    personas = {
        { persona = 'broke',   chance = 12, budget = { min = 0.45, max = 0.75 }, patience = 4 },
        { persona = 'cheap',   chance = 22, budget = { min = 0.8, max = 1.02 }, patience = 3 },
        { persona = 'rushed',  chance = 16, budget = { min = 0.95, max = 1.2 }, patience = 1 },
        { persona = 'regular', chance = 36, budget = { min = 0.95, max = 1.10 }, patience = 3 },
        { persona = 'rich',   chance = 14, budget = { min = 1.15, max = 1.60 }, patience = 2 },
    },
}

-- Saved contacts
-- Repeat customers. A happy app buyer may ask to be saved after a completed deal
-- Saved contacts keep a persistent personality and drugs they buy, message the player on their own and can be offered deals directly
Config.contacts = {
    enabled = true,

    -- Chance (0-100) a buyer asks to be saved after a completed deal
    addChance = 15,

    -- Guarantees the first contact. Once the player has completed this many app deals and still has no active contacts
    -- 0 = disabled
    firstContactAfterDeals = 3,

    -- Contact slots by star rating
    -- Players will not get new contacts if they have the max amount for their rating
    maxByRating = {
        { stars = 4.8, max = 16 },
        { stars = 4.0, max = 13 },
        { stars = 3.5, max = 10 },
        { stars = 3.0, max = 8 },
        { stars = 2.0, max = 6 },
        { stars = 0.0, max = 5 },
    },

    -- How many (real) days a fresh contact stays for. After this time the contact will send a goodbye message and be unavailable
    lifetimeDays = { min = 4, max = 30 },

    -- Minutes between contact buy request (per contact)
    messageInterval = { min = 40, max = 160 },

    -- Minutes before the player can offer product to the same contact again
    -- When offered again within this time from the last deal/offer, the contacts standing will drop
    offerCooldownMinutes = 50,

    -- Minutes a contact stays quiet after the player declines their buy request
    declineSnoozeMinutes = 120,

    -- Chance (0-100) a contact never buys from player started offers, they only buy when they reach out by themselves
    neverBuysChance = 25,

    -- Per-contact standing, 0-100
    -- At 0 the contact blocks the player
    standing = {
        -- Default standing when gaining a new contact
        default = 40,

        -- Gained on a completed deal
        deal = 7,

        -- Lost when a delivery times out or the package gets stolen
        failed = 15,

        -- Lost when haggling makes them walk off
        walked = 9,

        -- Lost when the player declines or ignores their request
        declined = 4,

        -- Lost per offer sent within the offerCooldownMinutes window
        nag = 8,
    },

    -- Price tolerance per standing level (Bad..Great). Replaces the star tier's priceTolerance for contacts
    priceTolerance = {
        0.9, -- Bad standing
        0.95, -- Poor standing
        1.0, -- Neutral standing
        1.1, -- Good standing
        1.2,  -- Great standing
    },

    -- Chance (0-100) a contact accepts a player started offer, per standing level
    acceptChance = {
        30, -- Bad standing
        45, -- Poor standing
        65, -- Neutral standing
        75, -- Good standing
        85, -- Great standing
    },

    -- Referrals: a contact with good or great standing sends a friend over
    referral = {
        enabled = true,

        -- Chance (0-100) per hour, per contact at or above minStandingLevel to send a referral deal
        chancePerHour = 5,

        -- Standing level required before a contact vouches for the player (1 = Bad, 2 = Poor, 3 = Neutral, 4 = Good, 5 = Great)
        minStandingLevel = 4,
    },
}

-- Deliveries (phone app deals)
Config.delivery = {
    deaddrop = {
        -- Models of the deaddrop packages, randomized per deaddrop
        models = {
            'prop_mp_drug_package',
            'xm3_prop_xm3_backpack_01a',
            'v_ind_meatboxsml_02',
            'v_ind_meatboxsml',
        },

        -- Seconds before the buyer will collect the package
        collectAfter = 60,

        -- Whether to send an NPC to physically collect the package (only happens if the player stays near the package)
        collector = true,

        -- Npc models that will go out and collect the deaddrop package
        collectorModels = {
            'g_m_y_mexgoon_03',
            'a_m_m_soucent_01',
            'a_m_o_soucent_01',
            'g_m_y_ballasout_01',
            'a_m_y_downtown_01',
            'a_m_m_salton_03',
            'a_f_y_soucent_02',
            'g_m_y_armgoon_02',
            's_m_o_busker_01',
            'a_f_m_trampbeac_01',
        },
    },

    -- Minutes the player will have to deliver the package, randomized per deal and rounded to the nearest 5
    waitMinutes = { min = 15, max = 45 },
}

-- Organizations
Config.org = {
    enabled = true,

    -- Blip on an organizations active sell location, only shown once the player has unlocked the org
    blip = {
        enabled = true,
        sprite = 84,
        color = 5,
        scale = 0.7,
    },

    -- Per-character standing with each organization (stored as 0-100)
    -- 5 display levels (Bad, Poor, Neutral, Good, Great)
    standing = {
        -- Standing of a fresh organization relation (40-59 = Neutral)
        default = 40,

        -- Points gained with an org per batch sold to them
        pointsPerBatch = 2,

        -- Points lost per batch with every org that lists the buyer as an enemy
        enemyPointsPerBatch = 3,

        -- Hours of real time without organization activity before an org drops one level (20 points)
        -- 0 = no decay
        decayHours = 72,
    },

    -- Favours, allows players to perform favours for organizations to up their standing
    favour = {
        enabled = true,

        -- Blip on the favour target while one is active (hit and vehicle favours)
        blip = {
            enabled = true,
            sprite = 303,
            color = 6,
            scale = 1.0,
        },

        -- Blip on the drop-off location of a package delivery favour
        packageBlip = {
            enabled = true,
            sprite = 478,
            color = 6,
            scale = 1.0,
        },

        -- Minutes to finish the favour
        timeLimitMinutes = 30,

        -- Minutes before the same org offers another favour
        cooldownMinutes = 45,

        -- The item handed over for package favours
        packageItem = 'kq_drugpackage',

        -- Vehicle models for destruction favours
        vehicles = {
            'cavalcade2',
            'kuruma',
            'toros',
            'jubilee',
            'tulip',
            'dubsta2',
            'burrito3',
            'speedo',
            'reaper',
            'xls',
            'deviant',
            'youga',
        },

        -- Chance (0-100) the enemy org finds out and punishes the player
        enemyNoticeChance = 75,

        standing = {
            -- Standing gain after finishing a favour
            complete = 15,

            -- Standing lost after failing to finish a favour (on time)
            fail = 10,

            -- Standing lost when an enemy organization finds out about doing favours for others
            enemy = 25,
        },
    },
}

-- Minimum police requirements
Config.minimumPolice = {
    block = {
        -- Disallows selling entirely when not enough officers are online
        enabled = false,
    },

    -- Allows selling but lowers drug prices when not enough officers are online
    priceCut = {
        enabled = false,

        -- Percentage of the drug prices paid while not enough police online (0-100)
        factor = 30,
    },

    -- Officers needed per sale type
    street = 0,
    app = 0,
    org = 0,
    corner = 0,
}

-- Police sting operations
-- Officers run undercover buys through the sting phone item
Config.police = {
    enabled = true,

    -- Jobs with sting phone access
    -- view = minimum grade to open the app and watch the shared operation
    -- control = minimum grade to run it (pick sites, targets, terminate)
    jobs = {
        ['police'] = { view = 0, control = 1 },
        ['sheriff'] = { view = 0, control = 1 },
    },

    -- Usable item that opens the sting phone. Only works for the jobs above
    phoneItem = {
        enabled = true,
        item = 'kq_stingphone',
    },

    -- Option to open the sting phone using a command instead of an item
    command = {
        enabled = false,
        command = 'stingphone',
    },

    -- Minutes before any officer can run another sting
    globalCooldown = 30,

    -- Blip on the agreed drop location, visible to officers only
    blip = {
        sprite = 478,
        color = 29,
        scale = 0.65,
    },

    -- The Hotspots app, a police-only heat map of where street deals happen
    heatmap = {
        enabled = true,
    },
}

-- Position of the deal popups
Config.uiPopup = {
    position = { -- CSS styling
        left = '50%',
        right = nil,
        top = nil,
        bottom = '2.5rem',
        transform = 'translateX(-50%)',
    },
}

-- Full UI customization via RGB color values
Config.uiStyling = {
    ['color-background'] = '18, 20, 19',
    ['color-background-light'] = '30, 32, 31',
    ['color-primary-dark'] = '120, 170, 0',
    ['color-primary'] = '178, 243, 0',
    ['color-primary-light'] = '211, 252, 98',
    ['color-secondary'] = '193, 207, 176',
    ['color-secondary-light'] = '226, 245, 203',
    ['color-secondary-dark'] = '111, 117, 103',
    ['color-white'] = '251, 251, 251',
    ['color-black'] = '10, 10, 10',
    ['border-radius-sm'] = '0.25rem',
    ['border-radius-md'] = '0.5rem',
    ['border-radius-lg'] = '0.75rem',
    ['border-radius-xl'] = '1.5rem',
    ['backdrop-blur'] = '16px',
}

```
