├── [Inventory-Images] ├── coke_baggy.png ├── meth_baggy.png └── weed_baggy.png ├── fxmanifest.lua ├── locales └── en.lua ├── client ├── phone.lua ├── nui.lua └── main.lua ├── html ├── index.html ├── css │ └── style.css └── js │ └── app.js ├── README.md ├── shared └── config.lua └── server └── main.lua /[Inventory-Images]/coke_baggy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaorNC/nc-trapphone/HEAD/[Inventory-Images]/coke_baggy.png -------------------------------------------------------------------------------- /[Inventory-Images]/meth_baggy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaorNC/nc-trapphone/HEAD/[Inventory-Images]/meth_baggy.png -------------------------------------------------------------------------------- /[Inventory-Images]/weed_baggy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NaorNC/nc-trapphone/HEAD/[Inventory-Images]/weed_baggy.png -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | game 'gta5' 3 | 4 | author 'NaorNC' 5 | description 'Trap Phone System for Drug Selling' 6 | version '1.0.0' 7 | 8 | shared_scripts { 9 | '@qb-core/shared/locale.lua', 10 | '@es_extended/imports.lua', 11 | 'shared/config.lua' 12 | } 13 | 14 | client_scripts { 15 | 'client/nui.lua', 16 | 'client/phone.lua', 17 | 'client/main.lua' 18 | } 19 | 20 | server_scripts { 21 | 'server/main.lua' 22 | } 23 | 24 | ui_page 'html/index.html' 25 | 26 | files { 27 | 'html/index.html', 28 | 'html/css/style.css', 29 | 'html/js/app.js' 30 | } 31 | 32 | lua54 'yes' 33 | -------------------------------------------------------------------------------- /locales/en.lua: -------------------------------------------------------------------------------- 1 | local Translations = { 2 | notify = { 3 | ['no_drugs'] = 'You don\'t have any drugs to sell', 4 | ['deal_success'] = 'Deal successful!', 5 | ['deal_failed'] = 'Deal failed', 6 | ['police_called'] = 'Someone is calling the police!', 7 | ['npc_hostile'] = 'The NPC became hostile!', 8 | ['drugs_added'] = 'Drugs added to inventory', 9 | ['not_admin'] = 'You are not an admin', 10 | }, 11 | info = { 12 | ['offer_drugs'] = 'Offer Drugs', 13 | ['place_product'] = 'Place product here', 14 | ['asking_price'] = 'Asking Price', 15 | ['fair_price'] = 'Fair price', 16 | ['success_chance'] = 'Chance of success', 17 | ['make_deal'] = 'DONE', 18 | ['relationship'] = 'Relationship', 19 | ['addiction'] = 'Addiction', 20 | ['standards'] = 'Standards', 21 | ['favourite_effects'] = 'Favourite Effects', 22 | }, 23 | commands = { 24 | ['toggle_dealing'] = 'Toggle drug dealing mode', 25 | ['get_drugs'] = 'Get drugs for testing (Admin Only)', 26 | }, 27 | effects = { 28 | ['energizing'] = 'Energizing', 29 | ['paranoia'] = 'Paranoia', 30 | ['sneaky'] = 'Sneaky', 31 | }, 32 | npc_types = { 33 | ['addict'] = 'Addict', 34 | ['party'] = 'Party Goer', 35 | ['casual'] = 'Casual User', 36 | ['straight'] = 'Straight Edge', 37 | }, 38 | } 39 | 40 | Lang = Lang or Locale:new({ 41 | phrases = Translations, 42 | warnOnMissing = true 43 | }) -------------------------------------------------------------------------------- /client/phone.lua: -------------------------------------------------------------------------------- 1 | function CalculateOfferSuccessChance(price, fairPrice, relationship) 2 | local priceDifference = (price - fairPrice) / fairPrice 3 | 4 | local baseChance = relationship or 50 5 | 6 | local finalChance = baseChance 7 | 8 | if priceDifference <= -0.2 then 9 | finalChance = finalChance * 1.5 10 | elseif priceDifference <= -0.1 then 11 | finalChance = finalChance * 1.3 12 | elseif priceDifference <= 0 then 13 | finalChance = finalChance * 1.1 14 | elseif priceDifference <= 0.1 then 15 | finalChance = finalChance * 0.9 16 | elseif priceDifference <= 0.2 then 17 | finalChance = finalChance * 0.7 18 | else 19 | finalChance = finalChance * 0.5 20 | end 21 | 22 | return math.min(95, math.max(5, finalChance)) 23 | end 24 | 25 | function UpdateRelationship(contactId, changeAmount) 26 | if not ActiveContact or ActiveContact.id ~= contactId then 27 | return false 28 | end 29 | 30 | local newRelationship = ActiveContact.relationship + changeAmount 31 | newRelationship = math.min(100, math.max(0, newRelationship)) 32 | 33 | ActiveContact.relationship = newRelationship 34 | 35 | return true 36 | end 37 | 38 | function GenerateRandomPrice(drugName, relationship) 39 | for _, drug in pairs(Config.TrapPhoneDrugs) do 40 | if drug.name == drugName then 41 | local minPrice = drug.priceRange[1] 42 | local maxPrice = drug.priceRange[2] 43 | 44 | local relationshipFactor = relationship / 100 45 | local adjustedMin = minPrice * (1 - (relationshipFactor * 0.1)) 46 | local adjustedMax = maxPrice * (1 - (relationshipFactor * 0.1)) 47 | 48 | return math.floor(adjustedMin + math.random() * (adjustedMax - adjustedMin)) 49 | end 50 | end 51 | 52 | return 100 53 | end 54 | 55 | function HasEnoughPolice() 56 | local minPolice = Config.PoliceSettings.minimumPolice 57 | 58 | if minPolice <= 0 then 59 | return true 60 | end 61 | 62 | local policeCount = 0 63 | 64 | if Config.Framework == 'qb' then 65 | local players = QBCore.Functions.GetQBPlayers() 66 | for _, player in pairs(players) do 67 | if player.PlayerData.job.name == "police" and player.PlayerData.job.onduty then 68 | policeCount = policeCount + 1 69 | end 70 | end 71 | elseif Config.Framework == 'esx' then 72 | local players = ESX.GetPlayers() 73 | for _, playerId in ipairs(players) do 74 | local xPlayer = ESX.GetPlayerFromId(playerId) 75 | if xPlayer and xPlayer.job.name == "police" and xPlayer.job.onduty then 76 | policeCount = policeCount + 1 77 | end 78 | end 79 | end 80 | 81 | return policeCount >= minPolice 82 | end 83 | 84 | RegisterNetEvent('trap_phone:client:setupDealZone') 85 | AddEventHandler('trap_phone:client:setupDealZone', function(data) 86 | TriggerEvent('drug_selling:client:attemptDeal', data.entity) 87 | end) 88 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Trap Phone 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
4:27 PM Friday
22 |
4G
23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
Messages
32 |
33 | 34 |
35 |
36 | 37 |
38 |
Journal
39 |
40 | 41 |
42 |
43 | 44 |
45 |
Products
46 |
47 | 48 |
49 |
50 | 51 |
52 |
Contacts
53 |
54 | 55 |
56 |
57 | 58 |
59 |
Map
60 |
61 | 62 |
63 |
64 | 65 |
66 |
Dealers
67 |
68 | 69 |
70 |
71 | 72 |
73 |
Deliveries
74 |
75 |
76 |
77 | 78 |
79 |
80 |
81 | 82 |
83 |

Messages

84 |
85 | 86 |
87 |
88 | 89 |
90 |
91 |
92 | 93 |
94 |
95 |
96 | 97 |
98 |
99 |
100 | 👨 101 |
102 |
Kevin Oakley
103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 |
111 |
112 | 113 |
114 |
115 |
116 | 117 |
118 |
119 | 123 | 154 |
155 | 156 |
157 | 161 | 166 |
167 | 168 |
169 |
170 |
171 |
172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nc-trapphone 2 | 3 | A comprehensive and immersive burner phone system for drug dealing in QBCore and ESX Frameworks with realistic messaging, dynamic pricing, and NPC deal locations. 4 | - For support and more resources, join our Discord - [Discord.gg/NCHub](https://discord.gg/NCHub) 5 | ## Preview & Support 6 | Youtube Video Showcase - [Click Me](https://www.youtube.com/watch?v=yX49Cc8MTsg) 7 | 8 | ![NCHub Trap Phone Drug Selling](https://github.com/user-attachments/assets/100b8e3b-7ead-4980-8c44-471ea41a3929) 9 | 10 | ## Features 11 | 12 | - **Interactive Phone Interface**: Fully functional trap phone UI with messaging system 13 | - **Dynamic Contact System**: Request new contacts for deals with cooldown timers 14 | - **Counter-Offer Mechanics**: Negotiate prices with contacts based on relationship 15 | - **Location-Based Deals**: Meet dealers at random locations throughout the map 16 | - **NPC Dealer Generation**: Realistic dealer NPCs spawn at meeting locations 17 | - **Inventory Integration**: Full item management with drug quantities and prices 18 | - **Police Alert System**: Configurable chance of police notification 19 | - **Relationship System**: Build rapport with recurring contacts for better deals 20 | - **Multiple Interaction Methods**: Support for both qb-target and traditional E-key interactions 21 | 22 | ## Dependencies 23 | 24 | - QBCore Framework OR ESX Framework 25 | - qb-target (For QBCore enhanced interactions) 26 | - ox_target (for ESX & QBCore enhanced interactions) 27 | 28 | ## Optional Integrations 29 | 30 | - Various police dispatch systems 31 | - Custom inventory systems 32 | 33 | ## Installation 34 | 35 | 1. Download or clone this repository 36 | 2. Place the resource in your server's `resources` folder 37 | 3. Add `ensure nc-trapphone` to your server.cfg 38 | 4. Add the trap phone item to your items.lua (see below for instructions) 39 | 5. Configure settings in `config.lua` to match your server's economy 40 | 6. Restart your server 41 | 42 | ## Framework Selection 43 | In the config.lua file, select your framework: 44 | ```lua 45 | Config.Framework = 'qb' -- 'qb' for QBCore, 'esx' for ESX 46 | ``` 47 | 48 | ## Item Installation 49 | 50 | ### Adding the Trap Phone Item 51 | 52 | #### For Older QBCore (items.lua): 53 | ```lua 54 | ['trapphone'] = { 55 | ['name'] = 'trapphone', 56 | ['label'] = 'Trap Phone', 57 | ['weight'] = 500, 58 | ['type'] = 'item', 59 | ['image'] = 'trap_phone.png', 60 | ['unique'] = true, 61 | ['useable'] = true, 62 | ['shouldClose'] = true, 63 | ['combinable'] = nil, 64 | ['description'] = 'A burner phone used for illegal business' 65 | }, 66 | ``` 67 | 68 | #### For Newer QBCore (shared/items.lua): 69 | ```lua 70 | ['trapphone'] = { 71 | name = 'trapphone', 72 | label = 'Trap Phone', 73 | weight = 500, 74 | type = 'item', 75 | image = 'trap_phone.png', 76 | unique = true, 77 | useable = true, 78 | shouldClose = true, 79 | combinable = nil, 80 | description = 'A burner phone used for illegal business' 81 | }, 82 | ``` 83 | 84 | ### Adding Drug Items (if needed) 85 | 86 | #### For Older QBCore (items.lua): 87 | ```lua 88 | ['weed_baggy'] = { 89 | ['name'] = 'weed_baggy', 90 | ['label'] = 'Weed Baggy', 91 | ['weight'] = 200, 92 | ['type'] = 'item', 93 | ['image'] = 'weed_baggy.png', 94 | ['unique'] = false, 95 | ['useable'] = true, 96 | ['shouldClose'] = true, 97 | ['combinable'] = nil, 98 | ['description'] = 'Small bag of weed' 99 | }, 100 | ['coke_baggy'] = { 101 | ['name'] = 'coke_baggy', 102 | ['label'] = 'Cocaine Baggy', 103 | ['weight'] = 200, 104 | ['type'] = 'item', 105 | ['image'] = 'coke_baggy.png', 106 | ['unique'] = false, 107 | ['useable'] = true, 108 | ['shouldClose'] = true, 109 | ['combinable'] = nil, 110 | ['description'] = 'Small bag of cocaine' 111 | }, 112 | ['meth_baggy'] = { 113 | ['name'] = 'meth_baggy', 114 | ['label'] = 'Meth Baggy', 115 | ['weight'] = 200, 116 | ['type'] = 'item', 117 | ['image'] = 'meth_baggy.png', 118 | ['unique'] = false, 119 | ['useable'] = true, 120 | ['shouldClose'] = true, 121 | ['combinable'] = nil, 122 | ['description'] = 'Small bag of meth' 123 | }, 124 | ['mega_death'] = { 125 | ['name'] = 'mega_death', 126 | ['label'] = 'Mega Death', 127 | ['weight'] = 200, 128 | ['type'] = 'item', 129 | ['image'] = 'mega_death.png', 130 | ['unique'] = false, 131 | ['useable'] = true, 132 | ['shouldClose'] = true, 133 | ['combinable'] = nil, 134 | ['description'] = 'High-value custom drug' 135 | }, 136 | ``` 137 | 138 | #### For Newer QBCore (shared/items.lua): 139 | ```lua 140 | ['weed_baggy'] = { 141 | name = 'weed_baggy', 142 | label = 'Weed Baggy', 143 | weight = 200, 144 | type = 'item', 145 | image = 'weed_baggy.png', 146 | unique = false, 147 | useable = true, 148 | shouldClose = true, 149 | combinable = nil, 150 | description = 'Small bag of weed' 151 | }, 152 | ['coke_baggy'] = { 153 | name = 'coke_baggy', 154 | label = 'Cocaine Baggy', 155 | weight = 200, 156 | type = 'item', 157 | image = 'coke_baggy.png', 158 | unique = false, 159 | useable = true, 160 | shouldClose = true, 161 | combinable = nil, 162 | description = 'Small bag of cocaine' 163 | }, 164 | ['meth_baggy'] = { 165 | name = 'meth_baggy', 166 | label = 'Meth Baggy', 167 | weight = 200, 168 | type = 'item', 169 | image = 'meth_baggy.png', 170 | unique = false, 171 | useable = true, 172 | shouldClose = true, 173 | combinable = nil, 174 | description = 'Small bag of meth' 175 | }, 176 | ['mega_death'] = { 177 | name = 'mega_death', 178 | label = 'Mega Death', 179 | weight = 200, 180 | type = 'item', 181 | image = 'mega_death.png', 182 | unique = false, 183 | useable = true, 184 | shouldClose = true, 185 | combinable = nil, 186 | description = 'High-value custom drug' 187 | }, 188 | ``` 189 | 190 | ### For ESX 191 | 192 | #### Using Database Method: 193 | ```sql 194 | INSERT INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES 195 | ('trapphone', 'Trap Phone', 1, 0, 1), 196 | ('weed_baggy', 'Weed Baggy', 1, 0, 1), 197 | ('coke_baggy', 'Cocaine Baggy', 1, 0, 1), 198 | ('meth_baggy', 'Meth Baggy', 1, 0, 1), 199 | ('mega_death', 'Mega Death', 1, 0, 1); 200 | 201 | #### Using ox_inventory (data/items.lua): 202 | ```lua 203 | ['trapphone'] = { 204 | label = 'Trap Phone', 205 | weight = 500, 206 | stack = false, 207 | close = true, 208 | description = 'A burner phone used for illegal business' 209 | }, 210 | ['weed_baggy'] = { 211 | label = 'Weed Baggy', 212 | weight = 200, 213 | stack = true, 214 | close = true, 215 | description = 'Small bag of weed' 216 | }, 217 | ['coke_baggy'] = { 218 | label = 'Cocaine Baggy', 219 | weight = 200, 220 | stack = true, 221 | close = true, 222 | description = 'Small bag of cocaine' 223 | }, 224 | ['meth_baggy'] = { 225 | label = 'Meth Baggy', 226 | weight = 200, 227 | stack = true, 228 | close = true, 229 | description = 'Small bag of meth' 230 | }, 231 | ['mega_death'] = { 232 | label = 'Mega Death', 233 | weight = 200, 234 | stack = true, 235 | close = true, 236 | description = 'High-value custom drug' 237 | }, 238 | ``` 239 | 240 | ### Adding Drug Items (if needed) 241 | 242 | See the sections above for adding the drug items in your respective framework. 243 | 244 | ## Configuration Options 245 | 246 | The `config.lua` file provides extensive customization options: 247 | 248 | ```lua 249 | Config = { 250 | TrapPhoneItem = 'trapphone', -- Item name for the trap phone 251 | UseQBTarget = true, -- Use QB-Target instead of E key 252 | ContactCooldown = 2, -- Minutes between requesting new contacts 253 | PoliceSettings = { 254 | minimumPolice = 0, -- Minimum police required for functionality 255 | alertChance = 10 -- Chance (0-100) that police will be alerted 256 | }, 257 | MeetLocations = { -- Configurable meeting locations 258 | { name = "Vinewood Hills", coords = vector3(-1530.32, 142.43, 55.65) }, 259 | { name = "Downtown", coords = vector3(219.72, -805.97, 30.39) }, 260 | -- And many more locations 261 | }, 262 | TrapPhoneDrugs = { -- Configurable drug types with pricing 263 | { 264 | name = "weed_baggy", 265 | label = "Weed Baggy", 266 | streetName = "Green", 267 | basePrice = 220, 268 | priceRange = {200, 240} 269 | }, 270 | -- Additional drugs can be configured 271 | } 272 | } 273 | ``` 274 | 275 | ### Image Paths 276 | - For QBCore: Add to `qb-inventory/html/images/` 277 | - For ESX: 278 | - Standard ESX inventory: Add to `esx_inventoryhud/html/img/items/` 279 | - ox_inventory: Add to `ox_inventory/web/images/` 280 | 281 | ## Image Installation 282 | 283 | Make sure to add the following images to your inventory resource: 284 | - `trap_phone.png` - For the trap phone item 285 | - `weed_baggy.png` - For weed baggies (if not already present) 286 | - `coke_baggy.png` - For cocaine baggies (if not already present) 287 | - `meth_baggy.png` - For meth baggies (if not already present) 288 | - `mega_death.png` - For the custom Mega Death drug 289 | 290 | ## Usage 291 | 292 | 1. Obtain a trap phone item through your server's economy 293 | 2. Use the trap phone from your inventory to open the interface 294 | 3. Request a new contact and wait for them to message you 295 | 4. Negotiate drug prices using the counter-offer system 296 | 5. Agree on a meeting location with your contact 297 | 6. Travel to the marked location on your map 298 | 7. Interact with the dealer NPC to complete the transaction 299 | 8. Be careful - deals may alert police based on your configuration settings! 300 | 301 | ## Admin Commands 302 | 303 | - `/givetrapphone [id]` - Admin command to give a trap phone to a specific player ID 304 | 305 | ## Customization 306 | 307 | - Modify contacts, prices, and locations in the config.lua file 308 | - Adjust the police notification settings based on your server's needs 309 | - Customize the UI appearance through the CSS files 310 | -------------------------------------------------------------------------------- /shared/config.lua: -------------------------------------------------------------------------------- 1 | Config = {} 2 | 3 | Config.Framework = 'qb' -- 'qb' for QBCore, 'esx' for ESX 4 | 5 | Config.TrapPhoneItem = 'trapphone' 6 | 7 | Config.UseQBTarget = true -- Use QB-Target (QBCore only) 8 | Config.UseOxTarget = false -- Use ox_target (Works with both QBCore and ESX) 9 | 10 | Config.Phone = { 11 | DefaultX = 70, -- % from right edge of screen 12 | DefaultY = 70, -- % from bottom of screen 13 | DefaultScale = 0.8 -- Scale of phone UI (0.8 = 80% of original size) 14 | } 15 | 16 | Config.ContactCooldown = 2 -- 2 minutes between requesting new contacts 17 | 18 | Config.Contacts = { 19 | { 20 | name = "Kevin Oakley", 21 | avatar = "👨", 22 | avatarColor = "#9C6E3C", 23 | verified = true, -- Has checkmark 24 | initialMessage = "Hey, I could use some Mega Death. Got any? I'll pay." 25 | }, 26 | { 27 | name = "Eugene Buckley", 28 | avatar = "👴", 29 | avatarColor = "#9E9E9E", 30 | verified = true, 31 | initialMessage = "Need something special for a party tonight. What you got?" 32 | }, 33 | { 34 | name = "Charles Rowland", 35 | avatar = "👨‍🦳", 36 | avatarColor = "#E0E0E0", 37 | verified = true, 38 | initialMessage = "My usual guy is out. Can you hook me up with something good?" 39 | }, 40 | { 41 | name = "Greg Thompson", 42 | avatar = "🧔", 43 | avatarColor = "#BDBDBD", 44 | verified = true, 45 | initialMessage = "I heard you might have some product. Looking to buy." 46 | }, 47 | { 48 | name = "Ken Walsh", 49 | avatar = "👨‍🦱", 50 | avatarColor = "#A1887F", 51 | verified = false, 52 | initialMessage = "Friend gave me your contact. Looking for some party favors." 53 | }, 54 | { 55 | name = "Peter Santiago", 56 | avatar = "👦", 57 | avatarColor = "#90CAF9", 58 | verified = false, 59 | initialMessage = "Yo, someone said you could help me out with some stuff?" 60 | }, 61 | { 62 | name = "Elizabeth Chen", 63 | avatar = "👩", 64 | avatarColor = "#81D4FA", 65 | verified = false, 66 | initialMessage = "Need some product for me and my friends this weekend." 67 | }, 68 | { 69 | name = "Michael Davis", 70 | avatar = "👨‍🦰", 71 | avatarColor = "#FF5722", 72 | verified = false, 73 | initialMessage = "Hey, heard you're the one to talk to about some recreational goods?" 74 | }, 75 | { 76 | name = "Sarah Johnson", 77 | avatar = "👩‍🦱", 78 | avatarColor = "#E91E63", 79 | verified = true, 80 | initialMessage = "I'm organizing a beach party. Need supplies. You available?" 81 | }, 82 | { 83 | name = "Tyrone Jackson", 84 | avatar = "🧔‍♂️", 85 | avatarColor = "#8BC34A", 86 | verified = true, 87 | initialMessage = "My connect got busted. Need a new supplier. Heard good things." 88 | }, 89 | { 90 | name = "Jennifer Lopez", 91 | avatar = "💃", 92 | avatarColor = "#9C27B0", 93 | verified = false, 94 | initialMessage = "Friend from downtown said you might have what I need for this weekend." 95 | }, 96 | { 97 | name = "David Kim", 98 | avatar = "🧑‍🎓", 99 | avatarColor = "#2196F3", 100 | verified = false, 101 | initialMessage = "College party coming up. Looking to make it memorable. You got goods?" 102 | }, 103 | { 104 | name = "Marcus Reed", 105 | avatar = "🕴️", 106 | avatarColor = "#607D8B", 107 | verified = true, 108 | initialMessage = "Business professional looking for something to help with stress. Discreet." 109 | }, 110 | { 111 | name = "Liam Rodriguez", 112 | avatar = "🧑", 113 | avatarColor = "#795548", 114 | verified = false, 115 | initialMessage = "Looking for something to make this weekend interesting. You selling?" 116 | }, 117 | { 118 | name = "Zoe Williams", 119 | avatar = "👱‍♀️", 120 | avatarColor = "#FFC107", 121 | verified = true, 122 | initialMessage = "VIP party at the club this weekend. Need premium stuff." 123 | } 124 | } 125 | 126 | Config.TrapPhoneDrugs = { 127 | { 128 | name = "weed_baggy", 129 | label = "Weed Baggy", 130 | streetName = "Green", 131 | basePrice = 220, 132 | priceRange = {200, 240} 133 | }, 134 | { 135 | name = "coke_baggy", 136 | label = "Cocaine Baggy", 137 | streetName = "Snow", 138 | basePrice = 550, 139 | priceRange = {500, 600} 140 | }, 141 | { 142 | name = "meth_baggy", 143 | label = "Meth Baggy", 144 | streetName = "Ice", 145 | basePrice = 380, 146 | priceRange = {350, 410} 147 | }, 148 | { 149 | name = "mega_death", 150 | label = "Mega Death", 151 | streetName = "Mega Death", 152 | basePrice = 450, 153 | priceRange = {400, 500} 154 | } 155 | } 156 | 157 | Config.MeetLocations = { 158 | { name = "Sandy Shores", coords = vector3(1777.64, 3799.93, 33.65) }, 159 | { name = "Paleto Bay", coords = vector3(-159.89, 6385.76, 31.47) }, 160 | { name = "Vinewood Hills", coords = vector3(-1530.32, 142.43, 55.65) }, 161 | { name = "Downtown", coords = vector3(219.72, -805.97, 30.39) }, 162 | { name = "East Los Santos", coords = vector3(1122.25, -644.54, 56.81) }, 163 | { name = "Mirror Park", coords = vector3(1124.05, -644.34, 56.81) }, 164 | { name = "Chumash", coords = vector3(-3175.41, 1087.95, 20.84) }, 165 | { name = "Harmony", coords = vector3(593.0, 2744.4, 41.9) }, 166 | { name = "La Mesa", coords = vector3(845.52, -1162.56, 25.28) }, 167 | { name = "Vespucci Beach", coords = vector3(-1348.63, -1236.19, 4.57) }, 168 | { name = "Rockford Hills", coords = vector3(-816.84, -191.68, 37.62) }, 169 | { name = "Grapeseed", coords = vector3(1701.24, 4921.91, 42.06) }, 170 | { name = "Del Perro Pier", coords = vector3(-1589.01, -1042.63, 13.02) }, 171 | { name = "Senora Desert", coords = vector3(2352.14, 3133.23, 48.21) }, 172 | { name = "Pacific Bluffs", coords = vector3(-2022.67, -465.03, 11.46) }, 173 | { name = "Davis", coords = vector3(88.33, -1924.33, 20.79) }, 174 | { name = "Legion Square", coords = vector3(195.17, -934.0, 30.69) }, 175 | { name = "Little Seoul", coords = vector3(-679.55, -878.31, 24.48) }, 176 | { name = "El Burro Heights", coords = vector3(1384.05, -2079.77, 52.23) }, 177 | { name = "Cypress Flats", coords = vector3(817.14, -2135.99, 29.36) }, 178 | { name = "Observatory", coords = vector3(-402.09, 1196.38, 325.64) }, 179 | { name = "Mount Chiliad", coords = vector3(450.04, 5566.86, 796.68) }, 180 | { name = "Tongva Hills", coords = vector3(-1866.3, 2062.58, 135.43) } 181 | } 182 | 183 | Config.ResponseOptions = { 184 | initial = { 185 | {text = "Deal", value = "deal", nextState = "deal_accepted"}, 186 | {text = "[Counter-offer]", value = "counter", nextState = "counter_offer"}, 187 | {text = "Not right now", value = "reject", nextState = "deal_rejected"} 188 | }, 189 | deal_accepted = { 190 | {text = "Send me the location", value = "location", nextState = "meet_location"}, 191 | {text = "I'll be in touch", value = "no_response", nextState = "no_response"} 192 | }, 193 | deal_rejected = { 194 | {text = "Maybe another time", value = "another_time", nextState = "closed"}, 195 | {text = "I don't sell that stuff", value = "dont_sell", nextState = "closed"} 196 | } 197 | } 198 | 199 | Config.NPCResponses = { 200 | deal_accepted = { 201 | "Great! I'll set everything up.", 202 | "Perfect! Let me know where to meet.", 203 | "Excellent. Let me know where and when.", 204 | "Nice! I'm ready when you are.", 205 | "That's what I'm talking about. Just need the meeting spot.", 206 | "Awesome, got the cash ready. Where should we meet?", 207 | "Sounds good to me. Just tell me when and where." 208 | }, 209 | counter_accepted = { 210 | "Deal. That works for me.", 211 | "Alright, I can do that price.", 212 | "Fine by me, we have a deal.", 213 | "That'll work. See you soon.", 214 | "I can live with that price. Let's do it.", 215 | "That's fair enough. I'll take it.", 216 | "You drive a hard bargain, but it's a deal." 217 | }, 218 | counter_rejected = { 219 | "Sorry, that's too expensive for me.", 220 | "I can't go that high. Maybe next time.", 221 | "That's way more than I can afford.", 222 | "No thanks, I'll find someone else.", 223 | "You're crazy with those prices. No way.", 224 | "That's a ripoff. I'll pass.", 225 | "Too rich for my blood. I'll look elsewhere." 226 | }, 227 | meet_location = { 228 | "Meet me at %s in 30 minutes. Don't be late.", 229 | "I'll be waiting at %s. Come alone.", 230 | "Let's meet at %s. Be discreet.", 231 | "I'll see you at %s. Make sure you're not followed.", 232 | "I'm heading to %s now. Don't bring any heat.", 233 | "Look for me at %s. I'll be smoking a cigarette.", 234 | "Come to %s. Park away from cameras.", 235 | "I'll be at %s. No phones, no cops, no problems.", 236 | "Meet me behind the building at %s. Be careful." 237 | }, 238 | no_response = { 239 | "Don't take too long or I'll find someone else.", 240 | "Alright, let me know soon.", 241 | "Don't keep me waiting.", 242 | "Fine, but don't ghost me.", 243 | "I've got other buyers waiting, so hurry up.", 244 | "I won't hold this deal forever.", 245 | "My schedule's tight. Don't waste my time." 246 | }, 247 | closed = { 248 | "Whatever man.", 249 | "Your loss.", 250 | "Fine by me.", 251 | "I'll find someone else.", 252 | "No skin off my back.", 253 | "Plenty of other buyers out there.", 254 | "Don't call this number again." 255 | } 256 | } 257 | 258 | Config.PoliceSettings = { 259 | minimumPolice = 0, 260 | alertChance = 10 261 | } 262 | -------------------------------------------------------------------------------- /server/main.lua: -------------------------------------------------------------------------------- 1 | local QBCore, ESX = nil, nil 2 | 3 | if Config.Framework == 'qb' then 4 | QBCore = exports['qb-core']:GetCoreObject() 5 | elseif Config.Framework == 'esx' then 6 | ESX = exports['es_extended']:getSharedObject() 7 | end 8 | 9 | local ActiveDeals = {} 10 | 11 | local function GetPlayer(src) 12 | if Config.Framework == 'qb' then 13 | return QBCore.Functions.GetPlayer(src) 14 | elseif Config.Framework == 'esx' then 15 | return ESX.GetPlayerFromId(src) 16 | end 17 | end 18 | 19 | local function HasDrug(player, drugName, quantity) 20 | if not player or not drugName then 21 | print("^1HasDrug called with invalid parameters^7") 22 | return false 23 | end 24 | 25 | quantity = tonumber(quantity) or 1 26 | 27 | if Config.Framework == 'qb' then 28 | local item = player.Functions.GetItemByName(drugName) 29 | if not item then 30 | print("^1Player does not have item: " .. drugName .. "^7") 31 | return false 32 | end 33 | 34 | if item.amount < quantity then 35 | print("^1Player has insufficient quantity - Has: " .. 36 | item.amount .. ", Needs: " .. quantity .. "^7") 37 | return false 38 | end 39 | elseif Config.Framework == 'esx' then 40 | local item = player.getInventoryItem(drugName) 41 | if not item then 42 | print("^1Player does not have item: " .. drugName .. "^7") 43 | return false 44 | end 45 | 46 | if item.count < quantity then 47 | print("^1Player has insufficient quantity - Has: " .. 48 | item.count .. ", Needs: " .. quantity .. "^7") 49 | return false 50 | end 51 | end 52 | 53 | return true 54 | end 55 | 56 | Citizen.CreateThread(function() 57 | if Config.Framework == 'qb' then 58 | if not QBCore.Shared.Items[Config.TrapPhoneItem] then 59 | QBCore.Functions.AddItem(Config.TrapPhoneItem, { 60 | name = Config.TrapPhoneItem, 61 | label = 'Trap Phone', 62 | weight = 500, 63 | type = 'item', 64 | image = 'trap_phone.png', 65 | unique = true, 66 | useable = true, 67 | shouldClose = true, 68 | combinable = nil, 69 | description = 'A burner phone used for illegal business' 70 | }) 71 | 72 | print("^2Trap Phone: Item registered with QBCore^7") 73 | end 74 | end 75 | end) 76 | 77 | if Config.Framework == 'qb' then 78 | QBCore.Functions.CreateCallback('QBCore:HasItem', function(source, cb, itemName, amount) 79 | local Player = QBCore.Functions.GetPlayer(source) 80 | if not Player then return cb(false) end 81 | 82 | amount = amount or 1 83 | local item = Player.Functions.GetItemByName(itemName) 84 | 85 | if item and item.amount >= amount then 86 | cb(true) 87 | else 88 | cb(false) 89 | end 90 | end) 91 | elseif Config.Framework == 'esx' then 92 | ESX.RegisterServerCallback('QBCore:HasItem', function(source, cb, itemName, amount) 93 | local xPlayer = ESX.GetPlayerFromId(source) 94 | if not xPlayer then return cb(false) end 95 | 96 | amount = amount or 1 97 | local item = xPlayer.getInventoryItem(itemName) 98 | 99 | if item and item.count >= amount then 100 | cb(true) 101 | else 102 | cb(false) 103 | end 104 | end) 105 | end 106 | 107 | if Config.Framework == 'qb' then 108 | QBCore.Functions.CreateUseableItem(Config.TrapPhoneItem, function(source, item) 109 | local src = source 110 | local Player = QBCore.Functions.GetPlayer(src) 111 | 112 | if Player.Functions.GetItemByName(Config.TrapPhoneItem) then 113 | TriggerClientEvent('trap_phone:usePhone', src) 114 | end 115 | end) 116 | elseif Config.Framework == 'esx' then 117 | ESX.RegisterUsableItem(Config.TrapPhoneItem, function(source) 118 | local src = source 119 | local xPlayer = ESX.GetPlayerFromId(src) 120 | 121 | if xPlayer.getInventoryItem(Config.TrapPhoneItem).count > 0 then 122 | TriggerClientEvent('trap_phone:usePhone', src) 123 | end 124 | end) 125 | end 126 | 127 | RegisterServerEvent('trap_phone:registerDeal') 128 | AddEventHandler('trap_phone:registerDeal', function(dealData) 129 | local src = source 130 | local Player = GetPlayer(src) 131 | 132 | if not Player then return end 133 | 134 | local citizenId = "" 135 | if Config.Framework == 'qb' then 136 | citizenId = Player.PlayerData.citizenid 137 | elseif Config.Framework == 'esx' then 138 | citizenId = Player.identifier 139 | end 140 | 141 | local dealId = dealData.dealId or ('deal_' .. citizenId .. '_' .. os.time()) 142 | 143 | local drugName = dealData.drugName or "weed_baggy" 144 | local quantity = tonumber(dealData.quantity) or 1 145 | local price = tonumber(dealData.price) or 200 146 | 147 | print("^4SERVER RECEIVED DEAL DATA: " .. drugName .. 148 | " x" .. quantity .. " for $" .. price .. "^7") 149 | 150 | ActiveDeals[dealId] = { 151 | playerId = src, 152 | citizenId = citizenId, 153 | contactName = dealData.contactName or "Unknown", 154 | drugName = drugName, 155 | quantity = quantity, 156 | price = price, 157 | location = dealData.location, 158 | timestamp = os.time(), 159 | status = 'pending' 160 | } 161 | 162 | print("^4SERVER STORED DEAL: " .. drugName .. 163 | " x" .. quantity .. " for $" .. price .. "^7") 164 | 165 | local playerName = "" 166 | if Config.Framework == 'qb' then 167 | playerName = Player.PlayerData.name 168 | elseif Config.Framework == 'esx' then 169 | playerName = Player.getName() 170 | end 171 | 172 | print("^2Trap Phone: Deal registered for " .. playerName .. 173 | " - " .. drugName .. " x" .. quantity .. 174 | " for $" .. price .. "^7") 175 | end) 176 | 177 | RegisterServerEvent('trap_phone:registerTransaction') 178 | AddEventHandler('trap_phone:registerTransaction', function(dealData) 179 | local src = source 180 | local Player = GetPlayer(src) 181 | 182 | if not Player then return end 183 | 184 | local drugName = dealData.drugName or "weed_baggy" 185 | local quantity = tonumber(dealData.quantity) or 1 186 | local price = tonumber(dealData.price) or 200 187 | 188 | print("^2Trap Phone: Transaction initiated - " .. 189 | quantity .. "x " .. drugName .. 190 | " for $" .. price .. "^7") 191 | 192 | local transactionId = 'trans_' .. src .. '_' .. os.time() 193 | 194 | local citizenId = "" 195 | if Config.Framework == 'qb' then 196 | citizenId = Player.PlayerData.citizenid 197 | elseif Config.Framework == 'esx' then 198 | citizenId = Player.identifier 199 | end 200 | 201 | ActiveDeals[transactionId] = { 202 | playerId = src, 203 | citizenId = citizenId, 204 | contactName = dealData.contactName or "Unknown", 205 | drugName = drugName, 206 | quantity = quantity, 207 | price = price, 208 | timestamp = os.time(), 209 | status = 'pending' 210 | } 211 | end) 212 | 213 | RegisterServerEvent('trap_phone:completeDeal') 214 | AddEventHandler('trap_phone:completeDeal', function(dealId, dealData) 215 | local src = source 216 | local Player = GetPlayer(src) 217 | 218 | if not Player then return end 219 | 220 | print("^1RECEIVED FROM CLIENT: dealId=" .. tostring(dealId) .. "^7") 221 | if dealData then 222 | print("^1DEAL DATA: drug=" .. tostring(dealData.drugName) .. 223 | ", quantity=" .. tostring(dealData.quantity) .. 224 | ", price=" .. tostring(dealData.price) .. "^7") 225 | else 226 | print("^1NO DEAL DATA RECEIVED^7") 227 | end 228 | 229 | local deal = nil 230 | 231 | if dealData and dealData.drugName and dealData.quantity and dealData.price then 232 | local citizenId = "" 233 | if Config.Framework == 'qb' then 234 | citizenId = Player.PlayerData.citizenid 235 | elseif Config.Framework == 'esx' then 236 | citizenId = Player.identifier 237 | end 238 | 239 | deal = { 240 | playerId = src, 241 | citizenId = citizenId, 242 | drugName = dealData.drugName, 243 | quantity = tonumber(dealData.quantity), 244 | price = tonumber(dealData.price), 245 | status = 'pending' 246 | } 247 | 248 | print("^2DIRECT DEAL DETAILS USED: " .. 249 | deal.drugName .. " x" .. deal.quantity .. 250 | " for $" .. deal.price .. "^7") 251 | elseif dealId and ActiveDeals[dealId] then 252 | deal = ActiveDeals[dealId] 253 | print("^3Using stored deal: " .. dealId .. "^7") 254 | else 255 | for id, dealInfo in pairs(ActiveDeals) do 256 | if dealInfo.playerId == src and dealInfo.status == 'pending' then 257 | deal = dealInfo 258 | dealId = id 259 | print("^3Using found deal by player ID^7") 260 | break 261 | end 262 | end 263 | end 264 | 265 | if not deal then 266 | print("^1NO DEAL FOUND FOR PLAYER^7") 267 | if Config.Framework == 'qb' then 268 | TriggerClientEvent('QBCore:Notify', src, 'No active deal found', 'error') 269 | elseif Config.Framework == 'esx' then 270 | TriggerClientEvent('esx:showNotification', src, 'No active deal found') 271 | end 272 | return 273 | end 274 | 275 | deal.quantity = tonumber(deal.quantity) or 1 276 | deal.price = tonumber(deal.price) or 200 277 | 278 | print("^2FINAL DEAL VALUES: Drug=" .. deal.drugName .. 279 | ", Quantity=" .. deal.quantity .. 280 | ", Price=$" .. deal.price .. "^7") 281 | 282 | if not HasDrug(Player, deal.drugName, deal.quantity) then 283 | print("^1Player missing drugs: " .. deal.drugName .. " x" .. deal.quantity .. "^7") 284 | if Config.Framework == 'qb' then 285 | TriggerClientEvent('QBCore:Notify', src, 'You don\'t have ' .. deal.quantity .. 'x ' .. deal.drugName, 'error') 286 | elseif Config.Framework == 'esx' then 287 | TriggerClientEvent('esx:showNotification', src, 'You don\'t have ' .. deal.quantity .. 'x ' .. deal.drugName) 288 | end 289 | return 290 | end 291 | 292 | deal.status = 'completed' 293 | 294 | if Config.Framework == 'qb' then 295 | Player.Functions.RemoveItem(deal.drugName, deal.quantity) 296 | TriggerClientEvent('inventory:client:ItemBox', src, QBCore.Shared.Items[deal.drugName], 'remove', deal.quantity) 297 | Player.Functions.AddMoney('cash', deal.price) 298 | TriggerClientEvent('QBCore:Notify', src, 'Deal completed: Received $' .. deal.price .. ' for ' .. deal.quantity .. 'x ' .. deal.drugName, 'success') 299 | elseif Config.Framework == 'esx' then 300 | Player.removeInventoryItem(deal.drugName, deal.quantity) 301 | Player.addMoney(deal.price) 302 | TriggerClientEvent('esx:showNotification', src, 'Deal completed: Received $' .. deal.price .. ' for ' .. deal.quantity .. 'x ' .. deal.drugName) 303 | end 304 | 305 | local playerName = "" 306 | if Config.Framework == 'qb' then 307 | playerName = Player.PlayerData.name 308 | elseif Config.Framework == 'esx' then 309 | playerName = Player.getName() 310 | end 311 | 312 | print("^2DEAL COMPLETED: " .. playerName .. 313 | " - " .. deal.drugName .. " x" .. deal.quantity .. 314 | " for $" .. deal.price .. "^7") 315 | 316 | if dealId and ActiveDeals[dealId] then 317 | ActiveDeals[dealId].status = 'completed' 318 | 319 | Citizen.SetTimeout(300000, function() 320 | if ActiveDeals[dealId] then 321 | ActiveDeals[dealId] = nil 322 | end 323 | end) 324 | end 325 | end) 326 | 327 | if Config.Framework == 'qb' then 328 | QBCore.Commands.Add('givetrapphone', 'Give trap phone to player (Admin only)', {{name='id', help='Player ID'}}, true, function(source, args) 329 | local src = source 330 | local Player = QBCore.Functions.GetPlayer(tonumber(args[1])) 331 | 332 | if not Player then 333 | TriggerClientEvent('QBCore:Notify', src, 'Player not found', 'error') 334 | return 335 | end 336 | 337 | Player.Functions.AddItem(Config.TrapPhoneItem, 1) 338 | TriggerClientEvent('inventory:client:ItemBox', tonumber(args[1]), QBCore.Shared.Items[Config.TrapPhoneItem], 'add', 1) 339 | TriggerClientEvent('QBCore:Notify', src, 'Trap phone given to ' .. Player.PlayerData.name, 'success') 340 | end, 'admin') 341 | elseif Config.Framework == 'esx' then 342 | RegisterCommand('givetrapphone', function(source, args) 343 | local src = source 344 | local xPlayer = ESX.GetPlayerFromId(src) 345 | 346 | if not xPlayer.getGroup() == 'admin' then 347 | TriggerClientEvent('esx:showNotification', src, 'You don\'t have permission to use this command') 348 | return 349 | end 350 | 351 | local targetPlayer = ESX.GetPlayerFromId(tonumber(args[1])) 352 | if not targetPlayer then 353 | TriggerClientEvent('esx:showNotification', src, 'Player not found') 354 | return 355 | end 356 | 357 | targetPlayer.addInventoryItem(Config.TrapPhoneItem, 1) 358 | TriggerClientEvent('esx:showNotification', src, 'Trap phone given to ' .. targetPlayer.getName()) 359 | end, false) 360 | end 361 | -------------------------------------------------------------------------------- /client/nui.lua: -------------------------------------------------------------------------------- 1 | local QBCore, ESX = nil, nil 2 | 3 | if Config.Framework == 'qb' then 4 | QBCore = exports['qb-core']:GetCoreObject() 5 | elseif Config.Framework == 'esx' then 6 | ESX = exports['es_extended']:getSharedObject() 7 | end 8 | 9 | function GetCurrentTimeFormatted() 10 | local hours = GetClockHours() 11 | local minutes = GetClockMinutes() 12 | 13 | if hours < 10 then hours = "0" .. hours end 14 | if minutes < 10 then minutes = "0" .. minutes end 15 | 16 | return hours .. ":" .. minutes 17 | end 18 | 19 | RegisterNUICallback('closePhone', function(data, cb) 20 | CloseTrapPhone() 21 | cb({status = "success"}) 22 | end) 23 | 24 | RegisterNUICallback('requestNewContact', function(data, cb) 25 | local newContact = CreateNewContact() 26 | 27 | if newContact then 28 | cb({ 29 | status = "success", 30 | contact = newContact 31 | }) 32 | else 33 | cb({ 34 | status = "error", 35 | message = "Failed to create new contact" 36 | }) 37 | end 38 | end) 39 | 40 | RegisterNUICallback('showNotification', function(data, cb) 41 | if Config.Framework == 'qb' then 42 | QBCore.Functions.Notify(data.message, data.type) 43 | elseif Config.Framework == 'esx' then 44 | ESX.ShowNotification(data.message) 45 | else 46 | TriggerEvent('trap_phone:showNotification', data.message, data.type) 47 | end 48 | cb({status = "success"}) 49 | end) 50 | 51 | RegisterNUICallback('setWaypoint', function(data, cb) 52 | if CurrentMeetLocation then 53 | print("^3A meeting location is already set. Updating deal info only.^7") 54 | 55 | local drugName = data.drugItemName or data.drugName 56 | local quantity = tonumber(data.quantity) or 1 57 | local price = tonumber(data.price) or 200 58 | 59 | if ActiveDeal then 60 | ActiveDeal.drugName = drugName 61 | ActiveDeal.quantity = quantity 62 | ActiveDeal.price = price 63 | 64 | _G.CurrentDealInfo = { 65 | drugName = drugName, 66 | quantity = quantity, 67 | price = price, 68 | timestamp = GetGameTimer() 69 | } 70 | 71 | print("^2Updated ActiveDeal without new location: " .. 72 | ActiveDeal.drugName .. " x" .. 73 | ActiveDeal.quantity .. " for $" .. 74 | ActiveDeal.price .. "^7") 75 | 76 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 77 | end 78 | 79 | cb({status = "success", message = "Deal details updated, using existing meeting location"}) 80 | return 81 | end 82 | 83 | local drugName = data.drugItemName or data.drugName 84 | local quantity = tonumber(data.quantity) or 1 85 | local price = tonumber(data.price) or 200 86 | 87 | print("^3SetWaypoint: Drug=" .. tostring(drugName) .. 88 | ", Quantity=" .. tostring(quantity) .. 89 | ", Price=" .. tostring(price) .. "^7") 90 | 91 | ActiveDeal = { 92 | drugName = drugName or "weed_baggy", 93 | quantity = quantity, 94 | price = price, 95 | contactName = ActiveContact and ActiveContact.name or "Unknown" 96 | } 97 | 98 | _G.CurrentDealInfo = { 99 | drugName = drugName or "weed_baggy", 100 | quantity = quantity, 101 | price = price, 102 | timestamp = GetGameTimer() 103 | } 104 | 105 | print("^2Created/Updated ActiveDeal with: " .. 106 | ActiveDeal.drugName .. " x" .. 107 | ActiveDeal.quantity .. " for $" .. 108 | ActiveDeal.price .. "^7") 109 | 110 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 111 | 112 | local success = ProcessDealLocation() 113 | 114 | cb({status = "success", message = "Waypoint set"}) 115 | end) 116 | 117 | RegisterNUICallback('sendMessage', function(data, cb) 118 | if not ActiveContact then 119 | cb({ 120 | status = "error", 121 | message = "No active contact" 122 | }) 123 | return 124 | end 125 | 126 | local originalMessageCount = #ActiveContact.messages 127 | 128 | print("^3Before adding player message - Message count: " .. originalMessageCount .. "^7") 129 | 130 | table.insert(ActiveContact.messages, { 131 | sender = "me", 132 | text = data.message, 133 | time = GetCurrentTimeFormatted() 134 | }) 135 | 136 | local responseState = data.nextState 137 | local responseOptions = Config.NPCResponses[responseState] 138 | 139 | local drugName = data.drugName 140 | local drugItemName = data.drugItemName 141 | local quantity = tonumber(data.quantity) or 1 142 | local price = tonumber(data.price) or 200 143 | 144 | local skipLocationRequest = data.skipLocationRequest 145 | 146 | if responseState == "deal_accepted" or responseState == "meet_location" or responseState == "ready_to_meet" then 147 | if not ActiveDeal then 148 | print("^2Creating new ActiveDeal for state: " .. responseState .. "^7") 149 | 150 | ActiveDeal = { 151 | drugName = drugItemName or drugName or "weed_baggy", 152 | quantity = quantity, 153 | price = price, 154 | contactName = ActiveContact.name 155 | } 156 | 157 | _G.CurrentDealInfo = { 158 | drugName = drugItemName or drugName or "weed_baggy", 159 | quantity = quantity, 160 | price = price, 161 | timestamp = GetGameTimer() 162 | } 163 | 164 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 165 | else 166 | print("^2Using existing ActiveDeal for state: " .. responseState .. "^7") 167 | if drugItemName then ActiveDeal.drugName = drugItemName 168 | elseif drugName then ActiveDeal.drugName = drugName end 169 | if quantity then ActiveDeal.quantity = quantity end 170 | if price then ActiveDeal.price = price end 171 | 172 | _G.CurrentDealInfo = { 173 | drugName = ActiveDeal.drugName, 174 | quantity = ActiveDeal.quantity, 175 | price = ActiveDeal.price, 176 | timestamp = GetGameTimer() 177 | } 178 | end 179 | end 180 | 181 | if responseState == "meet_location" then 182 | if not ActiveDeal then 183 | print("^1Warning: No ActiveDeal before meet_location - creating default^7") 184 | ActiveDeal = { 185 | drugName = drugItemName or drugName or "weed_baggy", 186 | quantity = quantity, 187 | price = price, 188 | contactName = ActiveContact.name 189 | } 190 | 191 | _G.CurrentDealInfo = { 192 | drugName = drugItemName or drugName or "weed_baggy", 193 | quantity = quantity, 194 | price = price, 195 | timestamp = GetGameTimer() 196 | } 197 | 198 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 199 | end 200 | 201 | if not CurrentMeetLocation and not skipLocationRequest then 202 | ProcessDealLocation() 203 | else 204 | print("^3Meeting location already exists or skip request received - not creating new location^7") 205 | _G.CurrentDealInfo = { 206 | drugName = ActiveDeal.drugName, 207 | quantity = ActiveDeal.quantity, 208 | price = ActiveDeal.price, 209 | timestamp = GetGameTimer() 210 | } 211 | 212 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 213 | end 214 | 215 | cb({ 216 | status = "success", 217 | messages = ActiveContact.messages, 218 | preserveChat = true 219 | }) 220 | return 221 | end 222 | 223 | if responseState == "ready_to_meet" then 224 | local readyText = "Great, I'm ready too. Let me send you the location." 225 | table.insert(ActiveContact.messages, { 226 | sender = "them", 227 | text = readyText, 228 | time = GetCurrentTimeFormatted() 229 | }) 230 | 231 | if not ActiveDeal then 232 | print("^1Warning: No ActiveDeal before ready_to_meet - creating default^7") 233 | ActiveDeal = { 234 | drugName = drugItemName or drugName or "weed_baggy", 235 | quantity = quantity, 236 | price = price, 237 | contactName = ActiveContact.name 238 | } 239 | 240 | _G.CurrentDealInfo = { 241 | drugName = drugItemName or drugName or "weed_baggy", 242 | quantity = quantity, 243 | price = price, 244 | timestamp = GetGameTimer() 245 | } 246 | 247 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 248 | end 249 | 250 | if not CurrentMeetLocation and not skipLocationRequest then 251 | Citizen.SetTimeout(800, function() 252 | ProcessDealLocation() 253 | 254 | SendNUIMessage({ 255 | action = "updateMessages", 256 | messages = ActiveContact.messages, 257 | preserveChat = true 258 | }) 259 | end) 260 | else 261 | print("^3Not setting new location - already exists or skip requested^7") 262 | _G.CurrentDealInfo = { 263 | drugName = ActiveDeal.drugName, 264 | quantity = ActiveDeal.quantity, 265 | price = ActiveDeal.price, 266 | timestamp = GetGameTimer() 267 | } 268 | end 269 | end 270 | 271 | print("^3After sendMessage processing - Message count: " .. #ActiveContact.messages .. "^7") 272 | 273 | if responseOptions and responseState ~= "meet_location" then 274 | local responseText = responseOptions[math.random(#responseOptions)] 275 | 276 | Citizen.SetTimeout(800, function() 277 | table.insert(ActiveContact.messages, { 278 | sender = "them", 279 | text = responseText, 280 | time = GetCurrentTimeFormatted() 281 | }) 282 | 283 | if responseState == "deal_accepted" then 284 | Citizen.SetTimeout(800, function() 285 | local followUpMessage = "Where and when should we meet?" 286 | 287 | table.insert(ActiveContact.messages, { 288 | sender = "them", 289 | text = followUpMessage, 290 | time = GetCurrentTimeFormatted() 291 | }) 292 | 293 | SendNUIMessage({ 294 | action = "updateMessages", 295 | messages = ActiveContact.messages, 296 | preserveChat = true 297 | }) 298 | 299 | print("^3After follow-up message - Message count: " .. #ActiveContact.messages .. "^7") 300 | end) 301 | else 302 | SendNUIMessage({ 303 | action = "updateMessages", 304 | messages = ActiveContact.messages, 305 | preserveChat = true 306 | }) 307 | 308 | print("^3After response message - Message count: " .. #ActiveContact.messages .. "^7") 309 | end 310 | end) 311 | end 312 | 313 | cb({ 314 | status = "success", 315 | messages = ActiveContact.messages, 316 | preserveChat = true 317 | }) 318 | end) 319 | 320 | RegisterNUICallback('sendCounterOffer', function(data, cb) 321 | if not ActiveContact then 322 | cb({ 323 | status = "error", 324 | message = "No active contact" 325 | }) 326 | return 327 | end 328 | 329 | print("^3Before counter offer - Messages count: " .. #ActiveContact.messages .. "^7") 330 | 331 | local drugName = data.drugName 332 | local drugItemName = data.drugItemName 333 | local quantity = tonumber(data.quantity) or 1 334 | local price = tonumber(data.price) or 200 335 | local fairPrice = data.fairPrice 336 | local successChance = data.successChance 337 | 338 | print("^4COUNTER OFFER DATA - Drug Label: " .. drugName .. 339 | ", Item Name: " .. (drugItemName or "unknown") .. 340 | ", Quantity: " .. quantity .. 341 | ", Price: " .. price .. "^7") 342 | 343 | local counterMessage = "I can give you " .. quantity .. "x " .. 344 | drugName .. " for $" .. price .. ". Deal?" 345 | 346 | table.insert(ActiveContact.messages, { 347 | sender = "me", 348 | text = counterMessage, 349 | time = GetCurrentTimeFormatted() 350 | }) 351 | 352 | local roll = math.random(100) 353 | local accepted = roll <= successChance 354 | 355 | local responseOptions = nil 356 | if accepted then 357 | responseOptions = Config.NPCResponses.counter_accepted 358 | else 359 | responseOptions = Config.NPCResponses.counter_rejected 360 | end 361 | 362 | local responseText = responseOptions[math.random(#responseOptions)] 363 | 364 | local actualDrugName = drugItemName 365 | 366 | if not actualDrugName then 367 | for _, drug in pairs(Config.TrapPhoneDrugs) do 368 | if drug.label == drugName then 369 | actualDrugName = drug.name 370 | break 371 | end 372 | end 373 | end 374 | 375 | if not actualDrugName then 376 | if #Config.TrapPhoneDrugs > 0 then 377 | actualDrugName = Config.TrapPhoneDrugs[1].name 378 | else 379 | actualDrugName = "weed_baggy" 380 | end 381 | end 382 | 383 | ActiveDeal = { 384 | drugName = actualDrugName, 385 | quantity = quantity, 386 | price = price, 387 | contactName = ActiveContact.name, 388 | accepted = accepted 389 | } 390 | 391 | _G.CurrentDealInfo = { 392 | drugName = actualDrugName, 393 | quantity = quantity, 394 | price = price, 395 | accepted = accepted, 396 | timestamp = GetGameTimer() 397 | } 398 | 399 | print("^4COUNTER OFFER - Global deal info updated [" .. GetGameTimer() .. "]: " .. actualDrugName .. 400 | " x" .. quantity .. " for $" .. price .. 401 | " (Accepted: " .. tostring(accepted) .. ")^7") 402 | 403 | cb({ 404 | status = "success", 405 | messages = ActiveContact.messages, 406 | preserveChat = true 407 | }) 408 | 409 | Citizen.SetTimeout(800, function() 410 | table.insert(ActiveContact.messages, { 411 | sender = "them", 412 | text = responseText, 413 | time = GetCurrentTimeFormatted() 414 | }) 415 | 416 | SendNUIMessage({ 417 | action = "updateMessages", 418 | messages = ActiveContact.messages, 419 | offerAccepted = accepted, 420 | preserveChat = true 421 | }) 422 | 423 | print("^3After counter response - Messages count: " .. #ActiveContact.messages .. "^7") 424 | 425 | if accepted then 426 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 427 | 428 | Citizen.SetTimeout(800, function() 429 | table.insert(ActiveContact.messages, { 430 | sender = "them", 431 | text = "So, where should we meet?", 432 | time = GetCurrentTimeFormatted() 433 | }) 434 | 435 | SendNUIMessage({ 436 | action = "updateMessages", 437 | messages = ActiveContact.messages, 438 | offerAccepted = true, 439 | preserveChat = true 440 | }) 441 | 442 | print("^3After follow-up message - Messages count: " .. #ActiveContact.messages .. "^7") 443 | end) 444 | end 445 | end) 446 | end) 447 | 448 | RegisterNUICallback('getPlayerDrugs', function(data, cb) 449 | local playerDrugs = GetPlayerDrugs() 450 | cb({ 451 | status = "success", 452 | drugs = playerDrugs 453 | }) 454 | end) 455 | 456 | RegisterNUICallback('deleteConversation', function(data, cb) 457 | ActiveContact = nil 458 | ActiveDeal = nil 459 | CurrentMeetLocation = nil 460 | _G.CurrentDealInfo = nil 461 | 462 | cb({ 463 | status = "success", 464 | message = "Conversation deleted" 465 | }) 466 | end) 467 | 468 | RegisterNUICallback('getActiveContact', function(data, cb) 469 | if ActiveContact then 470 | cb({ 471 | status = "success", 472 | contact = ActiveContact 473 | }) 474 | else 475 | cb({ 476 | status = "error", 477 | message = "No active contact" 478 | }) 479 | end 480 | end) 481 | -------------------------------------------------------------------------------- /html/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | font-family: 'Roboto', sans-serif; 8 | } 9 | 10 | body { 11 | display: flex; 12 | justify-content: flex-end; 13 | align-items: flex-end; 14 | min-height: 100vh; 15 | background-color: transparent; 16 | overflow: hidden; 17 | } 18 | 19 | .phone-container { 20 | position: fixed; 21 | right: 40px; 22 | bottom: 40px; 23 | transform-origin: bottom right; 24 | transform: scale(0.8); 25 | transition: transform 0.3s ease; 26 | display: none; 27 | } 28 | 29 | .phone { 30 | position: relative; 31 | width: 300px; 32 | height: 600px; 33 | background-color: #111; 34 | border-radius: 30px; 35 | overflow: hidden; 36 | border: 2px solid #333; 37 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.5); 38 | } 39 | 40 | .phone-inner { 41 | position: relative; 42 | width: 100%; 43 | height: 100%; 44 | display: flex; 45 | flex-direction: column; 46 | } 47 | 48 | .status-bar { 49 | display: flex; 50 | justify-content: space-between; 51 | align-items: center; 52 | padding: 5px 15px; 53 | color: white; 54 | font-size: 14px; 55 | height: 25px; 56 | z-index: 10; 57 | } 58 | 59 | .home-screen .status-bar { 60 | background-color: rgba(0, 0, 0, 0.253); 61 | position: absolute; 62 | top: 0; 63 | left: 0; 64 | right: 0; 65 | width: 100%; 66 | } 67 | 68 | .home-screen .time { 69 | text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8); 70 | } 71 | 72 | .notch { 73 | position: absolute; 74 | top: 0; 75 | left: 50%; 76 | transform: translateX(-50%); 77 | width: 60px; 78 | height: 20px; 79 | background-color: #111; 80 | border-bottom-left-radius: 10px; 81 | border-bottom-right-radius: 10px; 82 | display: flex; 83 | justify-content: center; 84 | align-items: center; 85 | z-index: 11; 86 | } 87 | 88 | .notch-circle { 89 | width: 8px; 90 | height: 8px; 91 | background-color: #444; 92 | border-radius: 50%; 93 | } 94 | 95 | .home-button { 96 | position: absolute; 97 | bottom: 10px; 98 | left: 50%; 99 | transform: translateX(-50%); 100 | width: 100px; 101 | height: 4px; 102 | background-color: #333; 103 | border-radius: 2px; 104 | z-index: 10; 105 | cursor: pointer; 106 | } 107 | 108 | .home-screen { 109 | position: absolute; 110 | top: 0; 111 | left: 0; 112 | width: 100%; 113 | height: 100%; 114 | display: flex; 115 | flex-direction: column; 116 | z-index: 1; 117 | transition: transform 0.3s ease; 118 | } 119 | 120 | .app-grid { 121 | display: grid; 122 | grid-template-columns: repeat(3, 1fr); 123 | gap: 20px; 124 | padding: 20px; 125 | margin-top: 50px; 126 | } 127 | 128 | .app { 129 | display: flex; 130 | flex-direction: column; 131 | align-items: center; 132 | justify-content: center; 133 | cursor: pointer; 134 | } 135 | 136 | .app-icon { 137 | position: relative; 138 | width: 60px; 139 | height: 60px; 140 | display: flex; 141 | justify-content: center; 142 | align-items: center; 143 | border-radius: 15px; 144 | color: white; 145 | font-size: 24px; 146 | margin-bottom: 5px; 147 | } 148 | 149 | .notification-badge { 150 | position: absolute; 151 | top: -5px; 152 | right: -5px; 153 | background-color: #ff3b30; 154 | color: white; 155 | font-size: 12px; 156 | min-width: 20px; 157 | height: 20px; 158 | display: flex; 159 | justify-content: center; 160 | align-items: center; 161 | border-radius: 10px; 162 | font-weight: bold; 163 | display: none; 164 | } 165 | 166 | .app-name { 167 | color: white; 168 | font-size: 12px; 169 | text-align: center; 170 | } 171 | 172 | .messages-app { background-color: #4CD964; } 173 | .journal-app { background-color: #FF9500; } 174 | .products-app { background-color: #1197F0; } 175 | .contacts-app { background-color: #007AFF; } 176 | .map-app { background-color: #34C759; } 177 | .dealers-app { background-color: #5856D6; } 178 | .deliveries-app { background-color: #AF52DE; } 179 | 180 | .background-pattern { 181 | position: absolute; 182 | top: 0; 183 | left: 0; 184 | width: 100%; 185 | height: 100%; 186 | background-image: 187 | url('https://i.ibb.co/cST4wNSH/DD.png'); 188 | background-size: cover; 189 | z-index: -1; 190 | } 191 | 192 | .messages-screen { 193 | position: absolute; 194 | top: 0; 195 | left: 0; 196 | width: 100%; 197 | height: 100%; 198 | display: flex; 199 | flex-direction: column; 200 | background-color: #fff; 201 | z-index: 2; 202 | transform: translateX(100%); 203 | transition: transform 0.3s ease; 204 | } 205 | 206 | .header { 207 | position: relative; 208 | width: 100%; 209 | padding: 15px; 210 | background-color: #fff; 211 | border-bottom: 1px solid #e0e0e0; 212 | display: flex; 213 | align-items: center; 214 | justify-content: center; 215 | z-index: 2; 216 | margin-top: 20px; 217 | } 218 | 219 | .header h1 { 220 | font-size: 20px; 221 | font-weight: 500; 222 | color: #000; 223 | } 224 | 225 | .back-btn { 226 | color: #007AFF; 227 | font-size: 20px; 228 | cursor: pointer; 229 | position: absolute; 230 | left: 15px; 231 | } 232 | 233 | .add-contact { 234 | position: absolute; 235 | right: 15px; 236 | top: 50%; 237 | transform: translateY(-50%); 238 | font-size: 20px; 239 | color: #007AFF; 240 | cursor: pointer; 241 | } 242 | 243 | .messages-container { 244 | flex: 1; 245 | overflow-y: auto; 246 | background-color: #fff; 247 | } 248 | 249 | .message-item { 250 | padding: 10px 15px; 251 | display: flex; 252 | align-items: center; 253 | border-bottom: 1px solid #f0f0f0; 254 | position: relative; 255 | cursor: pointer; 256 | } 257 | 258 | .message-item:active { 259 | background-color: #f5f5f5; 260 | } 261 | 262 | .avatar { 263 | width: 45px; 264 | height: 45px; 265 | border-radius: 50%; 266 | background-color: #f0f0f0; 267 | margin-right: 10px; 268 | display: flex; 269 | justify-content: center; 270 | align-items: center; 271 | overflow: hidden; 272 | position: relative; 273 | } 274 | 275 | .avatar img { 276 | width: 100%; 277 | height: 100%; 278 | object-fit: cover; 279 | } 280 | 281 | .online-indicator { 282 | position: absolute; 283 | bottom: 0; 284 | right: 0; 285 | width: 12px; 286 | height: 12px; 287 | background-color: #4CD964; 288 | border-radius: 50%; 289 | border: 2px solid #fff; 290 | transform: translate(3px, 3px); 291 | z-index: 2; 292 | } 293 | 294 | .contact-name { 295 | font-size: 15px; 296 | font-weight: 500; 297 | color: #000; 298 | margin-bottom: 3px; 299 | display: flex; 300 | align-items: center; 301 | } 302 | 303 | .contact-name-text { 304 | margin-right: 5px; 305 | } 306 | 307 | .online-dot { 308 | width: 8px; 309 | height: 8px; 310 | background-color: #4CD964; 311 | border-radius: 50%; 312 | display: inline-block; 313 | margin-left: 5px; 314 | } 315 | 316 | .message-content { 317 | flex: 1; 318 | } 319 | 320 | .message-preview { 321 | font-size: 13px; 322 | color: #666; 323 | white-space: nowrap; 324 | overflow: hidden; 325 | text-overflow: ellipsis; 326 | max-width: 180px; 327 | } 328 | 329 | .message-actions { 330 | display: flex; 331 | align-items: center; 332 | } 333 | 334 | .delete-btn { 335 | width: 20px; 336 | height: 20px; 337 | background-color: transparent; 338 | color: #999; 339 | border: none; 340 | cursor: pointer; 341 | display: flex; 342 | justify-content: center; 343 | align-items: center; 344 | font-size: 14px; 345 | } 346 | 347 | .chat-screen { 348 | position: absolute; 349 | top: 0; 350 | left: 0; 351 | width: 100%; 352 | height: 100%; 353 | display: flex; 354 | flex-direction: column; 355 | background-color: #e9e9e9; 356 | z-index: 3; 357 | transform: translateX(100%); 358 | transition: transform 0.3s ease; 359 | } 360 | 361 | .chat-header { 362 | padding: 10px 15px; 363 | background-color: #e0e0e0; 364 | color: #333; 365 | display: flex; 366 | align-items: center; 367 | margin-top: 20px; 368 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 369 | } 370 | 371 | .chat-contact { 372 | display: flex; 373 | align-items: center; 374 | flex: 1; 375 | margin-left: 15px; 376 | } 377 | 378 | .chat-avatar { 379 | width: 35px; 380 | height: 35px; 381 | border-radius: 50%; 382 | margin-right: 10px; 383 | display: flex; 384 | justify-content: center; 385 | align-items: center; 386 | background-color: #9C6E3C; 387 | overflow: hidden; 388 | } 389 | 390 | .chat-name { 391 | font-weight: 500; 392 | font-size: 16px; 393 | margin-left: 5px; 394 | color: #333; 395 | } 396 | 397 | .chat-status { 398 | width: 10px; 399 | height: 10px; 400 | background-color: #4CD964; 401 | border-radius: 50%; 402 | margin-left: 5px; 403 | } 404 | 405 | .rep-bar { 406 | width: 100px; 407 | height: 10px; 408 | background: linear-gradient(to right, #FF0000, #FF9900, #00A2FF); 409 | border-radius: 5px; 410 | margin-left: 10px; 411 | position: relative; 412 | } 413 | 414 | .rep-bar::after { 415 | content: ''; 416 | position: absolute; 417 | top: 0; 418 | left: 0; 419 | width: var(--relationship, 50%); 420 | height: 100%; 421 | background-color: rgba(255, 255, 255, 0.4); 422 | border-radius: 5px; 423 | } 424 | 425 | .chat-container { 426 | flex: 1; 427 | padding: 15px; 428 | overflow-y: auto; 429 | display: flex; 430 | flex-direction: column; 431 | margin-bottom: 155px; 432 | } 433 | 434 | .message-bubble { 435 | max-width: 70%; 436 | padding: 10px 15px; 437 | border-radius: 18px; 438 | margin-bottom: 10px; 439 | position: relative; 440 | word-break: break-word; 441 | } 442 | 443 | .message-them { 444 | background-color: #e5e5ea; 445 | color: #000; 446 | align-self: flex-start; 447 | border-top-left-radius: 5px; 448 | } 449 | 450 | .message-me { 451 | background-color: #429ed3; 452 | color: white; 453 | align-self: flex-end; 454 | border-top-right-radius: 5px; 455 | } 456 | 457 | .options-container { 458 | position: absolute; 459 | bottom: 50px; 460 | left: 0; 461 | width: 100%; 462 | padding: 0 15px; 463 | z-index: 5; 464 | } 465 | 466 | .option-btn { 467 | width: 100%; 468 | padding: 12px; 469 | margin-bottom: 8px; 470 | background-color: #429ed3; 471 | color: white; 472 | border: none; 473 | border-radius: 8px; 474 | font-size: 16px; 475 | font-weight: 500; 476 | cursor: pointer; 477 | text-align: center; 478 | overflow: hidden; 479 | text-overflow: ellipsis; 480 | white-space: nowrap; 481 | } 482 | 483 | .option-btn:disabled { 484 | opacity: 0.5; 485 | cursor: not-allowed; 486 | } 487 | 488 | .option-disabled { 489 | opacity: 0.5 !important; 490 | cursor: not-allowed !important; 491 | } 492 | 493 | .counter-offer-popup { 494 | position: absolute; 495 | top: 50%; 496 | left: 50%; 497 | transform: translate(-50%, -50%); 498 | width: 85%; 499 | background-color: white; 500 | border-radius: 10px; 501 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); 502 | z-index: 20; 503 | display: none; 504 | } 505 | 506 | .counter-offer-popup.show { 507 | display: block; 508 | } 509 | 510 | .popup-header { 511 | padding: 12px 15px; 512 | text-align: center; 513 | font-size: 16px; 514 | font-weight: 500; 515 | position: relative; 516 | border-bottom: 1px solid #f0f0f0; 517 | border-top-left-radius: 10px; 518 | border-top-right-radius: 10px; 519 | background-color: #f8f8f8; 520 | } 521 | 522 | .close-btn { 523 | position: absolute; 524 | top: 10px; 525 | right: 15px; 526 | font-size: 20px; 527 | cursor: pointer; 528 | color: #999; 529 | } 530 | 531 | .popup-content { 532 | padding: 20px; 533 | } 534 | 535 | .section-title { 536 | text-align: center; 537 | font-size: 14px; 538 | color: #666; 539 | margin-bottom: 15px; 540 | } 541 | 542 | .item-container { 543 | display: flex; 544 | justify-content: center; 545 | align-items: center; 546 | margin-bottom: 20px; 547 | } 548 | 549 | .minus-btn, .plus-btn { 550 | font-size: 18px; 551 | color: #666; 552 | margin: 0 15px; 553 | cursor: pointer; 554 | background-color: #f0f0f0; 555 | width: 30px; 556 | height: 30px; 557 | display: flex; 558 | align-items: center; 559 | justify-content: center; 560 | border-radius: 50%; 561 | } 562 | 563 | .item-display { 564 | display: flex; 565 | align-items: center; 566 | background-color: #f9f9f9; 567 | padding: 8px 15px; 568 | border-radius: 20px; 569 | border: 1px solid #eee; 570 | } 571 | 572 | .hand-icon { 573 | font-size: 18px; 574 | margin-right: 8px; 575 | } 576 | 577 | .item-text { 578 | font-size: 14px; 579 | font-weight: 500; 580 | } 581 | 582 | .drug-selector { 583 | margin-bottom: 20px; 584 | text-align: center; 585 | } 586 | 587 | .drug-selector select { 588 | width: 80%; 589 | padding: 10px; 590 | border-radius: 8px; 591 | border: 1px solid #ddd; 592 | background-color: #f9f9f9; 593 | font-size: 14px; 594 | appearance: none; 595 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'%3E%3Cpath fill='%23333' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); 596 | background-position: right 10px center; 597 | background-repeat: no-repeat; 598 | padding-right: 30px; 599 | } 600 | 601 | .receive-title { 602 | text-align: center; 603 | font-size: 14px; 604 | color: #666; 605 | margin-bottom: 15px; 606 | } 607 | 608 | .price { 609 | text-align: center; 610 | font-size: 24px; 611 | color: #4CD964; 612 | font-weight: bold; 613 | margin-bottom: 10px; 614 | } 615 | 616 | .price-increments { 617 | text-align: center; 618 | font-size: 12px; 619 | color: #888; 620 | margin-bottom: 10px; 621 | display: flex; 622 | justify-content: center; 623 | align-items: center; 624 | flex-wrap: wrap; 625 | gap: 8px; 626 | } 627 | 628 | .price-btn { 629 | cursor: pointer; 630 | color: #666; 631 | padding: 4px 8px; 632 | background-color: #f0f0f0; 633 | border-radius: 4px; 634 | transition: background-color 0.2s; 635 | } 636 | 637 | .price-btn:hover { 638 | background-color: #e0e0e0; 639 | color: #333; 640 | } 641 | 642 | .price-green { 643 | color: #4CD964; 644 | font-weight: bold; 645 | padding: 4px 8px; 646 | } 647 | 648 | .fair-price { 649 | text-align: center; 650 | font-size: 13px; 651 | color: #888; 652 | margin-bottom: 20px; 653 | } 654 | 655 | .send-btn { 656 | width: 100%; 657 | padding: 12px; 658 | background-color: #429ed3; 659 | color: white; 660 | border: none; 661 | border-radius: 8px; 662 | font-size: 16px; 663 | font-weight: 500; 664 | cursor: pointer; 665 | transition: background-color 0.2s; 666 | } 667 | 668 | .send-btn:hover { 669 | background-color: #3a8cbb; 670 | } 671 | 672 | .overlay { 673 | position: absolute; 674 | top: 0; 675 | left: 0; 676 | width: 100%; 677 | height: 100%; 678 | background-color: rgba(0, 0, 0, 0.5); 679 | z-index: 15; 680 | display: none; 681 | } 682 | 683 | .overlay.show { 684 | display: block; 685 | } 686 | 687 | .contact-popup { 688 | position: absolute; 689 | top: 50%; 690 | left: 50%; 691 | transform: translate(-50%, -50%); 692 | width: 85%; 693 | background-color: white; 694 | border-radius: 5px; 695 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); 696 | z-index: 20; 697 | display: none; 698 | } 699 | 700 | .contact-popup.show { 701 | display: block; 702 | } 703 | 704 | .contact-info { 705 | text-align: center; 706 | margin-bottom: 20px; 707 | font-size: 14px; 708 | color: #333; 709 | } 710 | 711 | .contact-btn { 712 | width: 100%; 713 | padding: 10px; 714 | background-color: #429ed3; 715 | color: white; 716 | border: none; 717 | border-radius: 5px; 718 | font-size: 15px; 719 | cursor: pointer; 720 | margin-bottom: 10px; 721 | } 722 | 723 | .cancel-btn { 724 | background-color: #e74c3c; 725 | } 726 | 727 | .avatar-icon { 728 | width: 100%; 729 | height: 100%; 730 | display: flex; 731 | justify-content: center; 732 | align-items: center; 733 | color: #fff; 734 | font-size: 18px; 735 | } 736 | 737 | .check-icon { 738 | width: 20px; 739 | height: 20px; 740 | border-radius: 50%; 741 | background-color: #4CD964; 742 | display: flex; 743 | justify-content: center; 744 | align-items: center; 745 | color: white; 746 | margin-left: 5px; 747 | } 748 | 749 | .char-img { 750 | width: 100%; 751 | height: 100%; 752 | display: flex; 753 | justify-content: center; 754 | align-items: center; 755 | } 756 | 757 | .char-img i { 758 | font-size: 24px; 759 | color: #666; 760 | } 761 | 762 | @keyframes phoneIn { 763 | 0% { transform: scale(0.5); opacity: 0; } 764 | 100% { transform: scale(0.8); opacity: 1; } 765 | } 766 | 767 | @keyframes phoneOut { 768 | 0% { transform: scale(0.8); opacity: 1; } 769 | 100% { transform: scale(0.5); opacity: 0; } 770 | } 771 | 772 | .phone-in { 773 | animation: phoneIn 0.3s forwards; 774 | } 775 | 776 | .phone-out { 777 | animation: phoneOut 0.3s forwards; 778 | } 779 | -------------------------------------------------------------------------------- /html/js/app.js: -------------------------------------------------------------------------------- 1 | let activeContact = null; 2 | let playerDrugs = []; 3 | let selectedDrug = null; 4 | let currentQuantity = 1; 5 | let currentPrice = 200; 6 | let fairPrice = 200; 7 | let successChance = 75; 8 | let messageHistory = []; 9 | let contactsList = []; 10 | let counterOfferSent = false; 11 | let lastContactTime = 0; 12 | let conversationStates = {}; 13 | let hasActiveContact = false; 14 | let CurrentMeetLocation = null; 15 | 16 | function closeContactPopup() { 17 | const contactPopup = document.getElementById('contactPopup'); 18 | const overlay = document.getElementById('overlay'); 19 | if (contactPopup) contactPopup.classList.remove('show'); 20 | if (overlay) overlay.classList.remove('show'); 21 | } 22 | 23 | function showContactPopup() { 24 | const contactPopup = document.getElementById('contactPopup'); 25 | const overlay = document.getElementById('overlay'); 26 | if (contactPopup) contactPopup.classList.add('show'); 27 | if (overlay) overlay.classList.add('show'); 28 | } 29 | 30 | function closeCounterOfferPopup() { 31 | const overlay = document.getElementById('overlay'); 32 | const counterOfferPopup = document.getElementById('counterOfferPopup'); 33 | if (overlay) overlay.classList.remove('show'); 34 | if (counterOfferPopup) counterOfferPopup.classList.remove('show'); 35 | } 36 | 37 | function openCounterOfferPopup() { 38 | sendPostMessage('getPlayerDrugs', {}, function(response) { 39 | if (response && response.status === 'success') { 40 | playerDrugs = response.drugs; 41 | 42 | populateDrugSelector(); 43 | 44 | resetCounterOffer(); 45 | 46 | const overlay = document.getElementById('overlay'); 47 | const counterOfferPopup = document.getElementById('counterOfferPopup'); 48 | if (overlay) overlay.classList.add('show'); 49 | if (counterOfferPopup) counterOfferPopup.classList.add('show'); 50 | } 51 | }); 52 | } 53 | 54 | function updateNotificationBadge() { 55 | const notificationBadge = document.querySelector('.notification-badge'); 56 | if (notificationBadge) { 57 | const count = contactsList.length; 58 | notificationBadge.textContent = count > 0 ? count : ''; 59 | notificationBadge.style.display = count > 0 ? 'flex' : 'none'; 60 | } 61 | } 62 | 63 | function saveConversationState(contactId) { 64 | if (!activeContact || !contactId) return; 65 | 66 | conversationStates[contactId] = { 67 | messages: JSON.parse(JSON.stringify(activeContact.messages)), 68 | responseState: activeContact.currentState || 'initial', 69 | counterOfferSent: counterOfferSent 70 | }; 71 | 72 | console.log(`Saved conversation state for ${contactId} with ${activeContact.messages.length} messages`); 73 | } 74 | 75 | function loadConversationState(contactId) { 76 | if (!contactId || !conversationStates[contactId]) return false; 77 | 78 | const state = conversationStates[contactId]; 79 | 80 | if (activeContact) { 81 | activeContact.messages = JSON.parse(JSON.stringify(state.messages)); 82 | activeContact.currentState = state.responseState; 83 | counterOfferSent = state.counterOfferSent; 84 | 85 | console.log(`Loaded conversation state for ${contactId} with ${activeContact.messages.length} messages`); 86 | return true; 87 | } 88 | 89 | return false; 90 | } 91 | 92 | document.addEventListener('DOMContentLoaded', function() { 93 | setupEventListeners(); 94 | updateTime(); 95 | setInterval(updateTime, 60000); 96 | 97 | contactsList = []; 98 | updateNotificationBadge(); 99 | 100 | counterOfferSent = false; 101 | lastContactTime = 0; 102 | 103 | conversationStates = {}; 104 | 105 | hasActiveContact = false; 106 | 107 | CurrentMeetLocation = null; 108 | }); 109 | 110 | function setupEventListeners() { 111 | const homeScreen = document.getElementById('homeScreen'); 112 | const messagesScreen = document.getElementById('messagesScreen'); 113 | const messagesApp = document.getElementById('messagesApp'); 114 | const homeButton = document.getElementById('homeButton'); 115 | const chatScreen = document.getElementById('chatScreen'); 116 | const backToMessages = document.getElementById('backToMessages'); 117 | const overlay = document.getElementById('overlay'); 118 | const addContactBtn = document.getElementById('addContactBtn'); 119 | const homeFromMessages = document.getElementById('homeFromMessages'); 120 | 121 | const counterOfferBtn = document.getElementById('counterOfferBtn'); 122 | const counterOfferPopup = document.getElementById('counterOfferPopup'); 123 | const sendOfferBtn = document.getElementById('sendOfferBtn'); 124 | const closeCounterOfferBtn = document.getElementById('closeCounterOffer'); 125 | 126 | const contactPopup = document.getElementById('contactPopup'); 127 | const confirmContactBtn = document.getElementById('confirmContact'); 128 | const cancelContactBtn = document.getElementById('cancelContact'); 129 | const closeContactPopupBtn = document.getElementById('closeContactPopup'); 130 | 131 | const priceMinus100 = document.getElementById('price-sub-100'); 132 | const priceMinus10 = document.getElementById('price-sub-10'); 133 | const priceMinus1 = document.getElementById('price-sub-1'); 134 | const pricePlus1 = document.getElementById('price-add-1'); 135 | const pricePlus10 = document.getElementById('price-add-10'); 136 | const pricePlus100 = document.getElementById('price-add-100'); 137 | 138 | const quantityMinus = document.getElementById('quantityMinus'); 139 | const quantityPlus = document.getElementById('quantityPlus'); 140 | 141 | const drugSelector = document.getElementById('drugSelector'); 142 | 143 | if (messagesApp) { 144 | messagesApp.addEventListener('click', function() { 145 | if (homeScreen) homeScreen.style.transform = 'translateX(-100%)'; 146 | if (messagesScreen) messagesScreen.style.transform = 'translateX(0)'; 147 | }); 148 | } 149 | 150 | if (homeFromMessages) { 151 | homeFromMessages.addEventListener('click', function() { 152 | if (homeScreen) homeScreen.style.transform = 'translateX(0)'; 153 | if (messagesScreen) messagesScreen.style.transform = 'translateX(100%)'; 154 | }); 155 | } 156 | 157 | if (homeButton) { 158 | homeButton.addEventListener('click', function(e) { 159 | e.preventDefault(); 160 | if (homeScreen) homeScreen.style.transform = 'translateX(0)'; 161 | if (messagesScreen) messagesScreen.style.transform = 'translateX(100%)'; 162 | if (chatScreen) chatScreen.style.transform = 'translateX(100%)'; 163 | 164 | closeCounterOfferPopup(); 165 | closeContactPopup(); 166 | }); 167 | } 168 | 169 | if (backToMessages) { 170 | backToMessages.addEventListener('click', function() { 171 | if (activeContact) { 172 | saveConversationState(activeContact.id); 173 | } 174 | 175 | if (messagesScreen) messagesScreen.style.transform = 'translateX(0)'; 176 | if (chatScreen) chatScreen.style.transform = 'translateX(100%)'; 177 | closeCounterOfferPopup(); 178 | }); 179 | } 180 | 181 | if (addContactBtn) { 182 | addContactBtn.addEventListener('click', function() { 183 | if (hasActiveContact) { 184 | sendPostMessage('showNotification', { 185 | message: "You already have an active contact. Finish your business first.", 186 | type: 'error' 187 | }); 188 | return; 189 | } 190 | 191 | const now = Date.now(); 192 | const cooldownTime = 120000; 193 | 194 | if (now - lastContactTime < cooldownTime) { 195 | const remainingTime = Math.ceil((cooldownTime - (now - lastContactTime)) / 1000 / 60); 196 | 197 | sendPostMessage('showNotification', { 198 | message: `Please wait ${remainingTime} minute(s) before requesting a new contact.`, 199 | type: 'error' 200 | }); 201 | return; 202 | } 203 | 204 | showContactPopup(); 205 | }); 206 | } 207 | 208 | if (confirmContactBtn) { 209 | confirmContactBtn.addEventListener('click', function() { 210 | closeContactPopup(); 211 | requestNewContact(); 212 | lastContactTime = Date.now(); 213 | 214 | CurrentMeetLocation = null; 215 | }); 216 | } 217 | 218 | if (cancelContactBtn) { 219 | cancelContactBtn.addEventListener('click', function() { 220 | closeContactPopup(); 221 | }); 222 | } 223 | 224 | if (closeContactPopupBtn) { 225 | closeContactPopupBtn.addEventListener('click', function() { 226 | closeContactPopup(); 227 | }); 228 | } 229 | 230 | if (counterOfferBtn) { 231 | counterOfferBtn.addEventListener('click', function() { 232 | openCounterOfferPopup(); 233 | }); 234 | } 235 | 236 | if (closeCounterOfferBtn) { 237 | closeCounterOfferBtn.addEventListener('click', function() { 238 | closeCounterOfferPopup(); 239 | }); 240 | } 241 | 242 | if (priceMinus100) priceMinus100.addEventListener('click', function() { adjustPrice(-100); }); 243 | if (priceMinus10) priceMinus10.addEventListener('click', function() { adjustPrice(-10); }); 244 | if (priceMinus1) priceMinus1.addEventListener('click', function() { adjustPrice(-1); }); 245 | if (pricePlus1) pricePlus1.addEventListener('click', function() { adjustPrice(1); }); 246 | if (pricePlus10) pricePlus10.addEventListener('click', function() { adjustPrice(10); }); 247 | if (pricePlus100) pricePlus100.addEventListener('click', function() { adjustPrice(100); }); 248 | 249 | if (quantityMinus) quantityMinus.addEventListener('click', decreaseQuantity); 250 | if (quantityPlus) quantityPlus.addEventListener('click', increaseQuantity); 251 | 252 | if (drugSelector) { 253 | drugSelector.addEventListener('change', function() { 254 | selectDrug(this.value); 255 | }); 256 | } 257 | 258 | if (sendOfferBtn) { 259 | sendOfferBtn.addEventListener('click', function() { 260 | sendCounterOffer(); 261 | }); 262 | } 263 | 264 | if (overlay) { 265 | overlay.addEventListener('click', function() { 266 | closeCounterOfferPopup(); 267 | closeContactPopup(); 268 | }); 269 | } 270 | 271 | document.querySelectorAll('.delete-btn').forEach(btn => { 272 | btn.addEventListener('click', function(e) { 273 | e.stopPropagation(); 274 | const messageItem = this.closest('.message-item'); 275 | if (messageItem) { 276 | const contactId = messageItem.getAttribute('data-id'); 277 | deleteConversation(contactId); 278 | messageItem.remove(); 279 | } 280 | }); 281 | }); 282 | } 283 | 284 | function updateTime() { 285 | const now = new Date(); 286 | let hours = now.getHours(); 287 | const minutes = now.getMinutes().toString().padStart(2, '0'); 288 | const ampm = hours >= 12 ? 'PM' : 'AM'; 289 | hours = hours % 12; 290 | hours = hours ? hours : 12; 291 | 292 | const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 293 | const dayName = days[now.getDay()]; 294 | 295 | const timeDisplay = document.getElementById('timeDisplay'); 296 | if (timeDisplay) { 297 | timeDisplay.textContent = `${hours}:${minutes} ${ampm} ${dayName}`; 298 | } 299 | } 300 | 301 | function requestNewContact() { 302 | hasActiveContact = false; 303 | activeContact = null; 304 | 305 | CurrentMeetLocation = null; 306 | 307 | sendPostMessage('requestNewContact', {}, function(response) { 308 | if (response && response.status === 'success' && response.contact) { 309 | hasActiveContact = true; 310 | 311 | contactsList = [response.contact]; 312 | 313 | const messagesContainer = document.getElementById('messagesContainer'); 314 | if (messagesContainer) { 315 | messagesContainer.innerHTML = ''; 316 | } 317 | 318 | addContactToMessageList(response.contact); 319 | 320 | updateNotificationBadge(); 321 | 322 | activeContact = response.contact; 323 | 324 | conversationStates[response.contact.id] = { 325 | messages: JSON.parse(JSON.stringify(response.contact.messages)), 326 | responseState: 'initial', 327 | counterOfferSent: false 328 | }; 329 | 330 | counterOfferSent = false; 331 | 332 | sendPostMessage('showNotification', { 333 | message: `New contact added: ${response.contact.name}`, 334 | type: 'success' 335 | }); 336 | } else { 337 | sendPostMessage('showNotification', { 338 | message: `Failed to find a new contact`, 339 | type: 'error' 340 | }); 341 | } 342 | }); 343 | } 344 | 345 | function addContactToMessageList(contact) { 346 | const messagesContainer = document.getElementById('messagesContainer'); 347 | if (!messagesContainer) return; 348 | 349 | const existingItem = document.querySelector(`.message-item[data-id="${contact.id}"]`); 350 | if (existingItem) { 351 | console.warn("Contact already exists in UI:", contact.id); 352 | return; 353 | } 354 | 355 | const messageItem = document.createElement('div'); 356 | messageItem.className = 'message-item'; 357 | messageItem.setAttribute('data-id', contact.id); 358 | 359 | const lastMessage = contact.messages[contact.messages.length - 1]; 360 | 361 | messageItem.innerHTML = ` 362 |
363 |
364 | ${contact.avatar} 365 |
366 |
367 |
368 |
369 |
370 | ${contact.name} 371 | 372 |
373 |
${lastMessage ? lastMessage.text : ''}
374 |
375 |
376 | 379 |
380 | `; 381 | 382 | messageItem.addEventListener('click', function() { 383 | openChat(contact); 384 | }); 385 | 386 | const deleteBtn = messageItem.querySelector('.delete-btn'); 387 | if (deleteBtn) { 388 | deleteBtn.addEventListener('click', function(e) { 389 | e.stopPropagation(); 390 | deleteConversation(contact.id); 391 | messageItem.remove(); 392 | 393 | const index = contactsList.findIndex(c => c.id === contact.id); 394 | if (index !== -1) { 395 | contactsList.splice(index, 1); 396 | updateNotificationBadge(); 397 | } 398 | 399 | hasActiveContact = false; 400 | 401 | CurrentMeetLocation = null; 402 | }); 403 | } 404 | 405 | messagesContainer.appendChild(messageItem); 406 | 407 | updateNotificationBadge(); 408 | } 409 | 410 | function openChat(contact) { 411 | const messagesScreen = document.getElementById('messagesScreen'); 412 | const chatScreen = document.getElementById('chatScreen'); 413 | if (!messagesScreen || !chatScreen) return; 414 | 415 | activeContact = JSON.parse(JSON.stringify(contact)); 416 | 417 | const stateLoaded = loadConversationState(contact.id); 418 | 419 | if (!stateLoaded) { 420 | contact.currentState = 'initial'; 421 | counterOfferSent = false; 422 | } 423 | 424 | updateChatHeader(activeContact); 425 | 426 | updateChatMessages(activeContact.messages); 427 | 428 | updateResponseOptions(activeContact.currentState || 'initial'); 429 | 430 | messagesScreen.style.transform = 'translateX(-100%)'; 431 | chatScreen.style.transform = 'translateX(0)'; 432 | } 433 | 434 | function updateChatHeader(contact) { 435 | const chatName = document.getElementById('chatName'); 436 | const chatAvatar = document.getElementById('chatAvatar'); 437 | const repBar = document.getElementById('repBar'); 438 | const checkIcon = document.getElementById('checkIcon'); 439 | 440 | if (chatName) chatName.textContent = contact.name; 441 | 442 | if (chatAvatar) { 443 | chatAvatar.style.backgroundColor = contact.avatarColor; 444 | chatAvatar.innerHTML = `${contact.avatar}`; 445 | } 446 | 447 | if (repBar) { 448 | repBar.style.setProperty('--relationship', `${contact.relationship}%`); 449 | } 450 | 451 | if (checkIcon) { 452 | checkIcon.style.display = contact.verified ? 'flex' : 'none'; 453 | } 454 | } 455 | 456 | function updateChatMessages(messages) { 457 | const chatContainer = document.getElementById('chatContainer'); 458 | if (!chatContainer) return; 459 | 460 | console.log(`Updating chat with ${messages.length} messages`); 461 | 462 | chatContainer.innerHTML = ''; 463 | 464 | messageHistory = JSON.parse(JSON.stringify(messages)); 465 | 466 | messages.forEach((message, index) => { 467 | const messageElement = document.createElement('div'); 468 | messageElement.className = `message-bubble message-${message.sender}`; 469 | messageElement.textContent = message.text; 470 | messageElement.setAttribute('data-index', index); 471 | chatContainer.appendChild(messageElement); 472 | }); 473 | 474 | chatContainer.scrollTop = chatContainer.scrollHeight; 475 | } 476 | 477 | function updateResponseOptions(state) { 478 | const optionsContainer = document.getElementById('optionsContainer'); 479 | if (!optionsContainer) return; 480 | 481 | if (activeContact) { 482 | activeContact.currentState = state; 483 | } 484 | 485 | optionsContainer.innerHTML = ''; 486 | 487 | if (state === 'initial') { 488 | const dealBtn = document.createElement('button'); 489 | dealBtn.className = 'option-btn option-disabled'; 490 | dealBtn.textContent = 'Deal'; 491 | dealBtn.style.opacity = '0.5'; 492 | dealBtn.style.cursor = 'not-allowed'; 493 | dealBtn.disabled = true; 494 | dealBtn.title = 'You must make a counter offer first'; 495 | optionsContainer.appendChild(dealBtn); 496 | 497 | const counterBtn = document.createElement('button'); 498 | counterBtn.className = 'option-btn'; 499 | counterBtn.textContent = '[Counter-offer]'; 500 | counterBtn.addEventListener('click', function() { 501 | openCounterOfferPopup(); 502 | }); 503 | optionsContainer.appendChild(counterBtn); 504 | 505 | const rejectBtn = document.createElement('button'); 506 | rejectBtn.className = 'option-btn'; 507 | rejectBtn.textContent = 'Not right now'; 508 | rejectBtn.addEventListener('click', function() { 509 | sendPlayerResponse('Not right now', 'deal_rejected'); 510 | }); 511 | optionsContainer.appendChild(rejectBtn); 512 | 513 | return; 514 | } 515 | 516 | const stateHandler = responseStateHandlers[state]; 517 | if (stateHandler) { 518 | stateHandler(optionsContainer); 519 | } else { 520 | defaultResponseOptions(optionsContainer); 521 | } 522 | 523 | if (activeContact) { 524 | saveConversationState(activeContact.id); 525 | } 526 | } 527 | 528 | const responseStateHandlers = { 529 | 'deal_accepted': function(container) { 530 | const locationBtn = document.createElement('button'); 531 | locationBtn.className = 'option-btn'; 532 | locationBtn.textContent = 'Send me the location'; 533 | locationBtn.addEventListener('click', function() { 534 | sendPlayerResponse('Send me the location', 'meet_location'); 535 | 536 | setTimeout(() => { 537 | sendLocationRequest(); 538 | }, 300); 539 | }); 540 | container.appendChild(locationBtn); 541 | 542 | const noResponseBtn = document.createElement('button'); 543 | noResponseBtn.className = 'option-btn'; 544 | noResponseBtn.textContent = 'I\'ll be in touch'; 545 | noResponseBtn.addEventListener('click', function() { 546 | sendPlayerResponse('I\'ll be in touch', 'no_response'); 547 | }); 548 | container.appendChild(noResponseBtn); 549 | }, 550 | 551 | 'deal_rejected': function(container) { 552 | const laterBtn = document.createElement('button'); 553 | laterBtn.className = 'option-btn'; 554 | laterBtn.textContent = 'Maybe another time'; 555 | laterBtn.addEventListener('click', function() { 556 | sendPlayerResponse('Maybe another time', 'closed'); 557 | }); 558 | container.appendChild(laterBtn); 559 | 560 | const dontSellBtn = document.createElement('button'); 561 | dontSellBtn.className = 'option-btn'; 562 | dontSellBtn.textContent = 'I don\'t sell that stuff'; 563 | dontSellBtn.addEventListener('click', function() { 564 | sendPlayerResponse('I don\'t sell that stuff', 'closed'); 565 | }); 566 | container.appendChild(dontSellBtn); 567 | }, 568 | 569 | 'counter_accepted': function(container) { 570 | responseStateHandlers['deal_accepted'](container); 571 | }, 572 | 573 | 'counter_rejected': function(container) { 574 | responseStateHandlers['deal_rejected'](container); 575 | }, 576 | 577 | 'meet_location': function(container) { 578 | container.innerHTML = '
Meeting location set
'; 579 | }, 580 | 581 | 'no_response': function(container) { 582 | const continueBtn = document.createElement('button'); 583 | continueBtn.className = 'option-btn'; 584 | continueBtn.textContent = 'I\'m ready to meet now'; 585 | continueBtn.addEventListener('click', function() { 586 | sendPlayerResponse('I\'m ready to meet now', 'ready_to_meet'); 587 | 588 | setTimeout(() => { 589 | sendLocationRequest(); 590 | }, 500); 591 | }); 592 | container.appendChild(continueBtn); 593 | 594 | const locationBtn = document.createElement('button'); 595 | locationBtn.className = 'option-btn'; 596 | locationBtn.textContent = 'Send me the location'; 597 | locationBtn.addEventListener('click', function() { 598 | sendPlayerResponse('Send me the location', 'meet_location'); 599 | 600 | setTimeout(() => { 601 | sendLocationRequest(); 602 | }, 300); 603 | }); 604 | container.appendChild(locationBtn); 605 | }, 606 | 607 | 'ready_to_meet': function(container) { 608 | const locationBtn = document.createElement('button'); 609 | locationBtn.className = 'option-btn'; 610 | locationBtn.textContent = 'Send me the location'; 611 | locationBtn.addEventListener('click', function() { 612 | sendPlayerResponse('Send me the location', 'meet_location'); 613 | 614 | setTimeout(() => { 615 | sendLocationRequest(); 616 | }, 300); 617 | }); 618 | container.appendChild(locationBtn); 619 | }, 620 | 621 | 'closed': function(container) { 622 | container.innerHTML = '
Conversation ended
'; 623 | } 624 | }; 625 | 626 | function defaultResponseOptions(container) { 627 | const infoText = document.createElement('div'); 628 | infoText.style.textAlign = 'center'; 629 | infoText.style.color = '#666'; 630 | infoText.style.padding = '10px'; 631 | infoText.textContent = 'No response options available'; 632 | container.appendChild(infoText); 633 | } 634 | 635 | function sendPlayerResponse(message, nextState) { 636 | if (activeContact) { 637 | console.log(`Before sending response - Messages count: ${activeContact.messages.length}`); 638 | 639 | const originalMessages = JSON.parse(JSON.stringify(activeContact.messages)); 640 | 641 | const messageObj = { 642 | sender: 'me', 643 | text: message, 644 | time: getCurrentTime() 645 | }; 646 | 647 | activeContact.messages.push(messageObj); 648 | 649 | updateChatMessages(activeContact.messages); 650 | 651 | updateMessagePreview(activeContact.id, message); 652 | 653 | const isLocationRelated = nextState === 'meet_location' || nextState === 'ready_to_meet'; 654 | 655 | const skipLocationRequest = isLocationRelated && CurrentMeetLocation !== null; 656 | 657 | let extraData = {}; 658 | if (selectedDrug) { 659 | extraData = { 660 | drugName: selectedDrug.label, 661 | drugItemName: selectedDrug.name, 662 | quantity: currentQuantity, 663 | price: currentPrice, 664 | skipLocationRequest: skipLocationRequest 665 | }; 666 | console.log(`Including drug details: ${selectedDrug.name} x${currentQuantity} for $${currentPrice}`); 667 | console.log(`Skip location request: ${skipLocationRequest}`); 668 | } 669 | 670 | sendPostMessage('sendMessage', { 671 | message: message, 672 | nextState: nextState, 673 | contactId: activeContact.id, 674 | preserveChat: true, 675 | originalMessageCount: activeContact.messages.length, 676 | skipLocationRequest: skipLocationRequest, 677 | ...extraData 678 | }, function(response) { 679 | if (response && response.status === 'success') { 680 | console.log(`Response successful - Messages received: ${response.messages ? response.messages.length : 'none'}`); 681 | 682 | if (!response.messages || response.messages.length < originalMessages.length) { 683 | console.log("Message loss detected, restoring from backup!"); 684 | 685 | let restoredMessages = JSON.parse(JSON.stringify(originalMessages)); 686 | 687 | const lastMessage = restoredMessages[restoredMessages.length - 1]; 688 | if (!lastMessage || lastMessage.sender !== 'me' || lastMessage.text !== message) { 689 | restoredMessages.push(messageObj); 690 | } 691 | 692 | if (isLocationRelated) { 693 | const locations = ["Vinewood Hills", "Downtown", "Mirror Park", "Sandy Shores"]; 694 | const randomLocation = locations[Math.floor(Math.random() * locations.length)]; 695 | 696 | const locationMessage = { 697 | sender: 'them', 698 | text: `I'll be waiting at ${randomLocation}. Make sure you're not followed.`, 699 | time: getCurrentTime() 700 | }; 701 | 702 | const hasLocationMessage = restoredMessages.some(msg => 703 | msg.sender === 'them' && msg.text.includes('waiting at') 704 | ); 705 | 706 | if (!hasLocationMessage) { 707 | restoredMessages.push(locationMessage); 708 | } 709 | 710 | CurrentMeetLocation = randomLocation; 711 | } 712 | 713 | activeContact.messages = restoredMessages; 714 | 715 | updateChatMessages(activeContact.messages); 716 | 717 | if (activeContact.messages.length > 0) { 718 | const lastMsg = activeContact.messages[activeContact.messages.length - 1]; 719 | updateMessagePreview(activeContact.id, lastMsg.text); 720 | } 721 | } else { 722 | activeContact.messages = response.messages; 723 | 724 | updateChatMessages(activeContact.messages); 725 | 726 | if (activeContact.messages.length > 0) { 727 | const lastMsg = activeContact.messages[activeContact.messages.length - 1]; 728 | updateMessagePreview(activeContact.id, lastMsg.text); 729 | } 730 | 731 | if (isLocationRelated && response.locationSet) { 732 | CurrentMeetLocation = response.locationName || "Unknown"; 733 | console.log(`Updated current meet location to: ${CurrentMeetLocation}`); 734 | } 735 | } 736 | 737 | updateResponseOptions(nextState); 738 | 739 | activeContact.currentState = nextState; 740 | 741 | saveConversationState(activeContact.id); 742 | 743 | console.log(`After server response - Messages count: ${activeContact.messages.length}`); 744 | } else { 745 | console.log("Server response failed, using local backup"); 746 | 747 | activeContact.messages = JSON.parse(JSON.stringify(originalMessages)); 748 | 749 | const lastMessage = activeContact.messages[activeContact.messages.length - 1]; 750 | if (!lastMessage || lastMessage.sender !== 'me' || lastMessage.text !== message) { 751 | activeContact.messages.push(messageObj); 752 | } 753 | 754 | setTimeout(() => { 755 | let responseText = "OK, got it."; 756 | 757 | switch(nextState) { 758 | case 'deal_accepted': 759 | responseText = "Great! I'll set everything up."; 760 | break; 761 | case 'deal_rejected': 762 | responseText = "Fine, whatever. Your loss."; 763 | break; 764 | case 'meet_location': 765 | responseText = "I'll be waiting at Vinewood Hills. Come alone."; 766 | CurrentMeetLocation = "Vinewood Hills"; 767 | break; 768 | case 'no_response': 769 | responseText = "Don't take too long or I'll find someone else."; 770 | break; 771 | case 'ready_to_meet': 772 | responseText = "Great, I'm ready too. Let me send you the location."; 773 | break; 774 | case 'closed': 775 | responseText = "Whatever man."; 776 | break; 777 | } 778 | 779 | const responseObj = { 780 | sender: 'them', 781 | text: responseText, 782 | time: getCurrentTime() 783 | }; 784 | 785 | const hasSimilarMessage = activeContact.messages.some(msg => 786 | msg.sender === 'them' && msg.text === responseText 787 | ); 788 | 789 | if (!hasSimilarMessage) { 790 | activeContact.messages.push(responseObj); 791 | } 792 | 793 | if (nextState === 'deal_accepted') { 794 | setTimeout(() => { 795 | const followUpMsg = "Where and when should we meet?"; 796 | 797 | const hasFollowUp = activeContact.messages.some(msg => 798 | msg.sender === 'them' && msg.text === followUpMsg 799 | ); 800 | 801 | if (!hasFollowUp) { 802 | const followUpObj = { 803 | sender: 'them', 804 | text: followUpMsg, 805 | time: getCurrentTime() 806 | }; 807 | 808 | activeContact.messages.push(followUpObj); 809 | } 810 | 811 | updateChatMessages(activeContact.messages); 812 | updateMessagePreview(activeContact.id, followUpMsg); 813 | }, 800); 814 | } 815 | 816 | if (nextState === 'ready_to_meet') { 817 | setTimeout(() => { 818 | const locationMsg = "I'll be waiting at Mirror Park. Come alone."; 819 | 820 | const hasLocation = activeContact.messages.some(msg => 821 | msg.sender === 'them' && msg.text.includes('waiting at') 822 | ); 823 | 824 | if (!hasLocation) { 825 | const locationObj = { 826 | sender: 'them', 827 | text: locationMsg, 828 | time: getCurrentTime() 829 | }; 830 | activeContact.messages.push(locationObj); 831 | } 832 | 833 | updateChatMessages(activeContact.messages); 834 | updateMessagePreview(activeContact.id, locationMsg); 835 | 836 | nextState = 'meet_location'; 837 | updateResponseOptions(nextState); 838 | activeContact.currentState = nextState; 839 | saveConversationState(activeContact.id); 840 | 841 | sendPostMessage('setWaypoint', { 842 | location: 'Mirror Park', 843 | drugName: selectedDrug ? selectedDrug.name : null, 844 | drugItemName: selectedDrug ? selectedDrug.name : null, 845 | quantity: currentQuantity, 846 | price: currentPrice, 847 | skipLocationRequest: skipLocationRequest 848 | }); 849 | 850 | CurrentMeetLocation = "Mirror Park"; 851 | }, 1000); 852 | } 853 | 854 | if (nextState === 'meet_location') { 855 | sendPostMessage('setWaypoint', { 856 | location: 'Vinewood Hills', 857 | drugName: selectedDrug ? selectedDrug.label : null, 858 | drugItemName: selectedDrug ? selectedDrug.name : null, 859 | quantity: currentQuantity, 860 | price: currentPrice, 861 | skipLocationRequest: skipLocationRequest 862 | }); 863 | 864 | CurrentMeetLocation = "Vinewood Hills"; 865 | } 866 | 867 | updateChatMessages(activeContact.messages); 868 | updateMessagePreview(activeContact.id, responseText); 869 | 870 | updateResponseOptions(nextState); 871 | activeContact.currentState = nextState; 872 | 873 | saveConversationState(activeContact.id); 874 | 875 | console.log(`After local backup - Messages count: ${activeContact.messages.length}`); 876 | }, 800); 877 | } 878 | }); 879 | } 880 | } 881 | 882 | function updateMessagePreview(contactId, text) { 883 | const messageItem = document.querySelector(`.message-item[data-id="${contactId}"]`); 884 | if (messageItem) { 885 | const messagePreview = messageItem.querySelector('.message-preview'); 886 | if (messagePreview) { 887 | messagePreview.textContent = text; 888 | } 889 | } 890 | } 891 | 892 | function getCurrentTime() { 893 | const now = new Date(); 894 | const hours = now.getHours().toString().padStart(2, '0'); 895 | const minutes = now.getMinutes().toString().padStart(2, '0'); 896 | return `${hours}:${minutes}`; 897 | } 898 | 899 | function deleteConversation(contactId) { 900 | sendPostMessage('deleteConversation', { 901 | contactId: contactId 902 | }); 903 | 904 | if (conversationStates[contactId]) { 905 | delete conversationStates[contactId]; 906 | } 907 | 908 | const index = contactsList.findIndex(c => c.id === contactId); 909 | if (index !== -1) { 910 | contactsList.splice(index, 1); 911 | updateNotificationBadge(); 912 | } 913 | 914 | if (activeContact && activeContact.id === contactId) { 915 | activeContact = null; 916 | } 917 | 918 | hasActiveContact = false; 919 | 920 | CurrentMeetLocation = null; 921 | } 922 | 923 | function populateDrugSelector() { 924 | const drugSelector = document.getElementById('drugSelector'); 925 | if (!drugSelector) return; 926 | 927 | drugSelector.innerHTML = ''; 928 | 929 | playerDrugs.forEach(drug => { 930 | const option = document.createElement('option'); 931 | option.value = drug.name; 932 | option.textContent = drug.label; 933 | drugSelector.appendChild(option); 934 | }); 935 | 936 | if (playerDrugs.length > 0) { 937 | selectDrug(playerDrugs[0].name); 938 | } 939 | } 940 | 941 | function selectDrug(drugName) { 942 | selectedDrug = playerDrugs.find(drug => drug.name === drugName); 943 | 944 | if (selectedDrug) { 945 | currentQuantity = 1; 946 | 947 | fairPrice = selectedDrug.basePrice; 948 | 949 | currentPrice = fairPrice; 950 | 951 | calculateSuccessChance(); 952 | 953 | updateCounterOfferUI(); 954 | } 955 | } 956 | 957 | function calculateSuccessChance() { 958 | const totalFairPrice = fairPrice * currentQuantity; 959 | const priceDifference = (currentPrice - totalFairPrice) / totalFairPrice; 960 | 961 | let baseChance = 70; 962 | 963 | if (priceDifference <= -0.3) { 964 | successChance = 95; 965 | } else if (priceDifference <= -0.2) { 966 | successChance = 90; 967 | } else if (priceDifference <= -0.1) { 968 | successChance = 80; 969 | } else if (priceDifference <= 0) { 970 | successChance = 70; 971 | } else if (priceDifference <= 0.1) { 972 | successChance = 60; 973 | } else if (priceDifference <= 0.1) { 974 | successChance = 60; 975 | } else if (priceDifference <= 0.2) { 976 | successChance = 40; 977 | } else if (priceDifference <= 0.3) { 978 | successChance = 20; 979 | } else { 980 | successChance = 10; 981 | } 982 | 983 | return successChance; 984 | } 985 | 986 | function resetCounterOffer() { 987 | currentQuantity = 1; 988 | 989 | if (playerDrugs.length > 0) { 990 | selectDrug(playerDrugs[0].name); 991 | } 992 | } 993 | 994 | function updateCounterOfferUI() { 995 | if (!selectedDrug) return; 996 | 997 | const itemQuantityDisplay = document.getElementById('itemQuantityDisplay'); 998 | if (itemQuantityDisplay) { 999 | itemQuantityDisplay.textContent = `${currentQuantity}x ${selectedDrug.label}`; 1000 | } 1001 | 1002 | const priceDisplay = document.getElementById('priceDisplay'); 1003 | const currentPriceDisplay = document.getElementById('currentPrice'); 1004 | if (priceDisplay) priceDisplay.textContent = `$${currentPrice}`; 1005 | if (currentPriceDisplay) currentPriceDisplay.textContent = `$${currentPrice}`; 1006 | 1007 | const calculatedFairPrice = fairPrice * currentQuantity; 1008 | const fairPriceDisplay = document.getElementById('fairPriceDisplay'); 1009 | if (fairPriceDisplay) { 1010 | fairPriceDisplay.textContent = `Fair price: $${calculatedFairPrice}`; 1011 | } 1012 | 1013 | calculateSuccessChance(); 1014 | 1015 | const sendOfferBtn = document.getElementById('sendOfferBtn'); 1016 | if (sendOfferBtn) { 1017 | sendOfferBtn.textContent = `Send (${successChance}%)`; 1018 | } 1019 | } 1020 | 1021 | function increaseQuantity() { 1022 | if (!selectedDrug) return; 1023 | 1024 | if (currentQuantity < selectedDrug.amount) { 1025 | currentQuantity++; 1026 | calculateSuccessChance(); 1027 | updateCounterOfferUI(); 1028 | } 1029 | } 1030 | 1031 | function decreaseQuantity() { 1032 | if (currentQuantity > 1) { 1033 | currentQuantity--; 1034 | calculateSuccessChance(); 1035 | updateCounterOfferUI(); 1036 | } 1037 | } 1038 | 1039 | function adjustPrice(amount) { 1040 | currentPrice += amount; 1041 | 1042 | if (currentPrice < 0) { 1043 | currentPrice = 0; 1044 | } 1045 | 1046 | calculateSuccessChance(); 1047 | 1048 | updateCounterOfferUI(); 1049 | } 1050 | 1051 | function sendCounterOffer() { 1052 | if (!selectedDrug || !activeContact) return; 1053 | 1054 | console.log(`Before sending counter offer - Messages count: ${activeContact.messages.length}`); 1055 | 1056 | const originalMessages = JSON.parse(JSON.stringify(activeContact.messages)); 1057 | 1058 | const offerQuantity = parseInt(currentQuantity); 1059 | const offerPrice = parseInt(currentPrice); 1060 | 1061 | const offerData = { 1062 | drugName: selectedDrug.label, 1063 | drugItemName: selectedDrug.name, 1064 | quantity: offerQuantity, 1065 | price: offerPrice, 1066 | fairPrice: fairPrice * offerQuantity, 1067 | successChance: successChance, 1068 | contactId: activeContact.id, 1069 | preserveChat: true 1070 | }; 1071 | 1072 | console.log(`Sending counter offer: ${selectedDrug.name} (${selectedDrug.label}) x${offerQuantity} (${typeof offerQuantity}) for $${offerPrice} (${typeof offerPrice})`); 1073 | 1074 | closeCounterOfferPopup(); 1075 | 1076 | const counterMessage = `I can give you ${offerQuantity}x ${selectedDrug.label} for $${offerPrice}. Deal?`; 1077 | const messageObj = { 1078 | sender: 'me', 1079 | text: counterMessage, 1080 | time: getCurrentTime() 1081 | }; 1082 | 1083 | activeContact.messages.push(messageObj); 1084 | 1085 | updateChatMessages(activeContact.messages); 1086 | 1087 | updateMessagePreview(activeContact.id, counterMessage); 1088 | 1089 | counterOfferSent = true; 1090 | 1091 | updateResponseOptions('initial'); 1092 | 1093 | saveConversationState(activeContact.id); 1094 | 1095 | sendPostMessage('sendCounterOffer', offerData, function(response) { 1096 | if (response && response.status === 'success') { 1097 | console.log('Counter offer sent successfully'); 1098 | 1099 | if (response.messages && response.messages.length >= originalMessages.length) { 1100 | console.log(`Received ${response.messages.length} messages with preserveChat flag`); 1101 | 1102 | activeContact.messages = response.messages; 1103 | 1104 | updateChatMessages(response.messages); 1105 | 1106 | if (response.messages.length > 0) { 1107 | const lastMsg = response.messages[response.messages.length - 1]; 1108 | updateMessagePreview(activeContact.id, lastMsg.text); 1109 | } 1110 | 1111 | if (response.offerAccepted !== undefined) { 1112 | updateResponseOptions(response.offerAccepted ? 'counter_accepted' : 'counter_rejected'); 1113 | 1114 | saveConversationState(activeContact.id); 1115 | } 1116 | } else { 1117 | console.log("Message loss detected in counter offer response, keeping local messages"); 1118 | } 1119 | 1120 | console.log(`After server counter offer response - Messages count: ${activeContact.messages.length}`); 1121 | } else { 1122 | console.log("Server response failed for counter offer, using local fallback"); 1123 | 1124 | setTimeout(() => { 1125 | const accepted = Math.random() > 0.3; 1126 | 1127 | const responseText = accepted ? 1128 | "Deal! That works for me." : 1129 | "Sorry, that's too expensive for me."; 1130 | 1131 | const hasResponse = activeContact.messages.some(msg => 1132 | msg.sender === 'them' && msg.text === responseText 1133 | ); 1134 | 1135 | if (!hasResponse) { 1136 | const responseMsg = { 1137 | sender: 'them', 1138 | text: responseText, 1139 | time: getCurrentTime() 1140 | }; 1141 | 1142 | activeContact.messages.push(responseMsg); 1143 | } 1144 | 1145 | if (accepted) { 1146 | const followUpText = "So, where should we meet?"; 1147 | 1148 | const hasFollowUp = activeContact.messages.some(msg => 1149 | msg.sender === 'them' && msg.text === followUpText 1150 | ); 1151 | 1152 | if (!hasFollowUp) { 1153 | setTimeout(() => { 1154 | const followUpMsg = { 1155 | sender: 'them', 1156 | text: followUpText, 1157 | time: getCurrentTime() 1158 | }; 1159 | 1160 | activeContact.messages.push(followUpMsg); 1161 | updateChatMessages(activeContact.messages); 1162 | updateMessagePreview(activeContact.id, followUpMsg.text); 1163 | }, 1000); 1164 | } 1165 | } 1166 | 1167 | updateChatMessages(activeContact.messages); 1168 | 1169 | updateResponseOptions(accepted ? 'counter_accepted' : 'counter_rejected'); 1170 | 1171 | updateMessagePreview(activeContact.id, responseText); 1172 | 1173 | saveConversationState(activeContact.id); 1174 | 1175 | console.log(`After local counter offer fallback - Messages count: ${activeContact.messages.length}`); 1176 | }, 1000); 1177 | } 1178 | }); 1179 | } 1180 | 1181 | function sendLocationRequest() { 1182 | if (!activeContact) return; 1183 | 1184 | const skipLocationRequest = !!CurrentMeetLocation; 1185 | 1186 | const drugInfo = {}; 1187 | if (selectedDrug) { 1188 | drugInfo.drugName = selectedDrug.label; 1189 | drugInfo.drugItemName = selectedDrug.name; 1190 | drugInfo.quantity = parseInt(currentQuantity); 1191 | drugInfo.price = parseInt(currentPrice); 1192 | } 1193 | 1194 | sendPostMessage('setWaypoint', { 1195 | location: 'requested', 1196 | skipLocationRequest: skipLocationRequest, 1197 | ...drugInfo, 1198 | preserveChat: true 1199 | }, function(response) { 1200 | console.log(`Location request sent with drug info: ${JSON.stringify(drugInfo)}`); 1201 | console.log(`Skip location flag: ${skipLocationRequest}`); 1202 | 1203 | if (response && response.status === 'success') { 1204 | console.log('Waypoint set successfully'); 1205 | 1206 | if (response.locationName) { 1207 | CurrentMeetLocation = response.locationName; 1208 | console.log(`Updated current meet location to: ${CurrentMeetLocation}`); 1209 | } 1210 | } 1211 | }); 1212 | } 1213 | 1214 | window.addEventListener('message', function(event) { 1215 | const data = event.data; 1216 | 1217 | if (data.action === 'openPhone') { 1218 | const phoneContainer = document.querySelector('.phone-container'); 1219 | if (phoneContainer) { 1220 | phoneContainer.style.display = 'block'; 1221 | phoneContainer.classList.add('phone-in'); 1222 | } 1223 | 1224 | if (data.drugs) { 1225 | playerDrugs = data.drugs; 1226 | } 1227 | 1228 | if (data.currentMeetLocation) { 1229 | CurrentMeetLocation = data.currentMeetLocation; 1230 | console.log(`Phone opened with active meeting location: ${CurrentMeetLocation}`); 1231 | } else { 1232 | CurrentMeetLocation = null; 1233 | } 1234 | 1235 | if (data.contacts) { 1236 | contactsList = data.contacts.map(contact => JSON.parse(JSON.stringify(contact))); 1237 | 1238 | updateNotificationBadge(); 1239 | 1240 | const messagesContainer = document.getElementById('messagesContainer'); 1241 | if (messagesContainer) { 1242 | messagesContainer.innerHTML = ''; 1243 | 1244 | contactsList.forEach(contact => { 1245 | addContactToMessageList(contact); 1246 | }); 1247 | } 1248 | 1249 | contactsList.forEach(contact => { 1250 | if (!conversationStates[contact.id]) { 1251 | conversationStates[contact.id] = { 1252 | messages: JSON.parse(JSON.stringify(contact.messages)), 1253 | responseState: 'initial', 1254 | counterOfferSent: false 1255 | }; 1256 | } 1257 | }); 1258 | 1259 | hasActiveContact = contactsList.length > 0; 1260 | } 1261 | } 1262 | else if (data.action === 'closePhone') { 1263 | if (activeContact) { 1264 | saveConversationState(activeContact.id); 1265 | } 1266 | 1267 | const phoneContainer = document.querySelector('.phone-container'); 1268 | if (phoneContainer) { 1269 | phoneContainer.classList.remove('phone-in'); 1270 | phoneContainer.classList.add('phone-out'); 1271 | 1272 | setTimeout(() => { 1273 | phoneContainer.style.display = 'none'; 1274 | phoneContainer.classList.remove('phone-out'); 1275 | }, 300); 1276 | } 1277 | } 1278 | else if (data.action === 'newContact') { 1279 | if (data.contact) { 1280 | hasActiveContact = true; 1281 | 1282 | contactsList = [JSON.parse(JSON.stringify(data.contact))]; 1283 | 1284 | const messagesContainer = document.getElementById('messagesContainer'); 1285 | if (messagesContainer) { 1286 | messagesContainer.innerHTML = ''; 1287 | } 1288 | 1289 | addContactToMessageList(data.contact); 1290 | 1291 | conversationStates[data.contact.id] = { 1292 | messages: JSON.parse(JSON.stringify(data.contact.messages)), 1293 | responseState: 'initial', 1294 | counterOfferSent: false 1295 | }; 1296 | 1297 | updateNotificationBadge(); 1298 | 1299 | CurrentMeetLocation = null; 1300 | } 1301 | } 1302 | else if (data.action === 'updateMessages') { 1303 | if (data.messages && activeContact) { 1304 | console.log(`updateMessages event: Received ${data.messages.length} messages, preserveChat=${data.preserveChat}`); 1305 | 1306 | if (data.preserveChat) { 1307 | if (data.messages.length >= activeContact.messages.length) { 1308 | activeContact.messages = JSON.parse(JSON.stringify(data.messages)); 1309 | } else { 1310 | console.log("Warning: Server sent fewer messages than we have locally, keeping local version"); 1311 | } 1312 | } else { 1313 | activeContact.messages = JSON.parse(JSON.stringify(data.messages)); 1314 | } 1315 | 1316 | updateChatMessages(activeContact.messages); 1317 | 1318 | if (activeContact.messages.length > 0) { 1319 | const lastMsg = activeContact.messages[activeContact.messages.length - 1]; 1320 | updateMessagePreview(activeContact.id, lastMsg.text); 1321 | } 1322 | 1323 | if (data.locationSet) { 1324 | CurrentMeetLocation = data.locationName || "Unknown"; 1325 | console.log(`Updated current meet location to: ${CurrentMeetLocation}`); 1326 | } 1327 | 1328 | if (activeContact.id) { 1329 | saveConversationState(activeContact.id); 1330 | } 1331 | } 1332 | 1333 | if (data.offerAccepted !== undefined) { 1334 | updateResponseOptions(data.offerAccepted ? 'counter_accepted' : 'counter_rejected'); 1335 | } 1336 | } 1337 | }); 1338 | 1339 | function sendPostMessage(action, data = {}, callback = null) { 1340 | const messageId = `msg_${Date.now()}_${Math.floor(Math.random() * 1000)}`; 1341 | 1342 | data.messageId = messageId; 1343 | 1344 | if (callback) { 1345 | window.callbacks = window.callbacks || {}; 1346 | window.callbacks[messageId] = callback; 1347 | } 1348 | 1349 | fetch(`https://${GetParentResourceName()}/${action}`, { 1350 | method: 'POST', 1351 | headers: { 1352 | 'Content-Type': 'application/json' 1353 | }, 1354 | body: JSON.stringify(data) 1355 | }) 1356 | .then(resp => resp.json()) 1357 | .then(resp => { 1358 | if (window.callbacks && window.callbacks[messageId]) { 1359 | window.callbacks[messageId](resp); 1360 | delete window.callbacks[messageId]; 1361 | } 1362 | }) 1363 | .catch(error => { 1364 | console.error('Error:', error); 1365 | if (window.callbacks && window.callbacks[messageId]) { 1366 | if (action === 'requestNewContact') { 1367 | callback({ 1368 | status: 'success', 1369 | contact: { 1370 | id: 'test_contact_' + Date.now(), 1371 | name: 'Kevin Oakley', 1372 | avatar: '👨', 1373 | avatarColor: '#9C6E3C', 1374 | verified: true, 1375 | relationship: 50, 1376 | messages: [ 1377 | { 1378 | sender: 'them', 1379 | text: 'Hey, I could use some Mega Death. Got any? I\'ll pay.', 1380 | time: getCurrentTime() 1381 | } 1382 | ] 1383 | } 1384 | }); 1385 | } else if (action === 'sendCounterOffer') { 1386 | let accepted = Math.random() > 0.2; 1387 | 1388 | let response = { 1389 | status: 'success', 1390 | offerAccepted: accepted, 1391 | messages: [ 1392 | ...activeContact.messages, 1393 | ], 1394 | preserveChat: true 1395 | }; 1396 | 1397 | response.messages.push({ 1398 | sender: 'them', 1399 | text: accepted ? 1400 | 'Deal! That works for me.' : 1401 | 'Sorry, that\'s too expensive for me.', 1402 | time: getCurrentTime() 1403 | }); 1404 | 1405 | if (accepted) { 1406 | response.messages.push({ 1407 | sender: 'them', 1408 | text: 'So, where should we meet?', 1409 | time: getCurrentTime() 1410 | }); 1411 | } 1412 | 1413 | callback(response); 1414 | } else if (action === 'sendMessage') { 1415 | const nextState = data.nextState; 1416 | 1417 | let response = { 1418 | status: 'success', 1419 | messages: [ 1420 | ...activeContact.messages 1421 | ], 1422 | preserveChat: true 1423 | }; 1424 | 1425 | if (nextState === 'deal_rejected') { 1426 | response.messages.push({ 1427 | sender: 'them', 1428 | text: 'Fine, whatever. Your loss.', 1429 | time: getCurrentTime() 1430 | }); 1431 | } else if (nextState === 'deal_accepted') { 1432 | response.messages.push({ 1433 | sender: 'them', 1434 | text: 'Great! I\'ll set everything up.', 1435 | time: getCurrentTime() 1436 | }); 1437 | response.messages.push({ 1438 | sender: 'them', 1439 | text: 'Where and when should we meet?', 1440 | time: getCurrentTime() 1441 | }); 1442 | } else if (nextState === 'meet_location') { 1443 | response.messages.push({ 1444 | sender: 'them', 1445 | text: 'I\'ll be waiting at Vinewood Hills. Come alone.', 1446 | time: getCurrentTime() 1447 | }); 1448 | 1449 | CurrentMeetLocation = "Vinewood Hills"; 1450 | response.locationSet = true; 1451 | response.locationName = "Vinewood Hills"; 1452 | } else if (nextState === 'ready_to_meet') { 1453 | response.messages.push({ 1454 | sender: 'them', 1455 | text: 'Great, I\'m ready too. Let me send you the location.', 1456 | time: getCurrentTime() 1457 | }); 1458 | setTimeout(() => { 1459 | response.messages.push({ 1460 | sender: 'them', 1461 | text: 'I\'ll be waiting at Mirror Park. Come alone.', 1462 | time: getCurrentTime() 1463 | }); 1464 | 1465 | CurrentMeetLocation = "Mirror Park"; 1466 | }, 800); 1467 | } 1468 | 1469 | callback(response); 1470 | } else if (action === 'setWaypoint') { 1471 | console.log('Setting waypoint to ' + data.location); 1472 | 1473 | if (data.location && data.location !== 'requested') { 1474 | CurrentMeetLocation = data.location; 1475 | } else { 1476 | CurrentMeetLocation = "Default Location"; 1477 | } 1478 | 1479 | callback({ 1480 | status: 'success', 1481 | message: 'Waypoint set', 1482 | locationSet: true, 1483 | locationName: CurrentMeetLocation 1484 | }); 1485 | } else { 1486 | callback({status: 'success', message: 'Test mode, no game client'}); 1487 | } 1488 | delete window.callbacks[messageId]; 1489 | } 1490 | }); 1491 | } 1492 | 1493 | function GetParentResourceName() { 1494 | try { 1495 | return window.GetParentResourceName(); 1496 | } catch(e) { 1497 | return 'nc-trapphone'; 1498 | } 1499 | } 1500 | 1501 | document.addEventListener('keydown', function(event) { 1502 | if (event.key === 'Escape') { 1503 | if (activeContact) { 1504 | saveConversationState(activeContact.id); 1505 | } 1506 | 1507 | sendPostMessage('closePhone'); 1508 | } 1509 | }); -------------------------------------------------------------------------------- /client/main.lua: -------------------------------------------------------------------------------- 1 | local QBCore, ESX = nil, nil 2 | local PlayerData = {} 3 | local TrapPhoneVisible = false 4 | local ActiveContact = nil 5 | local ActiveDeal = nil 6 | local CurrentMeetLocation = nil 7 | local LastUsedLocation = nil 8 | local DealerPeds = {} 9 | local TimeSeed = 0 10 | local DealerDeals = {} 11 | local PedDealMap = {} 12 | 13 | local phoneProp = 0 14 | local phoneAnimDict = "cellphone@" 15 | local phoneAnim = "cellphone_text_in" 16 | 17 | if Config.Framework == 'esx' then 18 | Citizen.CreateThread(function() 19 | while ESX == nil do 20 | Wait(0) 21 | end 22 | 23 | PlayerData = ESX.GetPlayerData() 24 | print("Initial ESX player data loaded") 25 | end) 26 | 27 | RegisterNetEvent('esx:playerLoaded') 28 | AddEventHandler('esx:playerLoaded', function(xPlayer) 29 | PlayerData = xPlayer 30 | print("ESX player data updated from playerLoaded event") 31 | end) 32 | 33 | RegisterNetEvent('esx:addInventoryItem') 34 | AddEventHandler('esx:addInventoryItem', function(itemName, count) 35 | print("Item added: " .. itemName .. " x" .. count) 36 | if not PlayerData.inventory then PlayerData.inventory = {} end 37 | 38 | local found = false 39 | for i=1, #PlayerData.inventory do 40 | if PlayerData.inventory[i].name == itemName then 41 | PlayerData.inventory[i].count = count 42 | found = true 43 | break 44 | end 45 | end 46 | 47 | if not found then 48 | table.insert(PlayerData.inventory, {name = itemName, count = count}) 49 | end 50 | end) 51 | 52 | RegisterNetEvent('esx:removeInventoryItem') 53 | AddEventHandler('esx:removeInventoryItem', function(itemName, count) 54 | print("Item removed: " .. itemName .. " x" .. count) 55 | if not PlayerData.inventory then return end 56 | 57 | for i=1, #PlayerData.inventory do 58 | if PlayerData.inventory[i].name == itemName then 59 | PlayerData.inventory[i].count = count 60 | break 61 | end 62 | end 63 | end) 64 | end 65 | 66 | if Config.Framework == 'qb' then 67 | QBCore = exports['qb-core']:GetCoreObject() 68 | elseif Config.Framework == 'esx' then 69 | ESX = exports['es_extended']:getSharedObject() 70 | end 71 | 72 | function StartPhoneAnimation() 73 | local player = PlayerPedId() 74 | local animDict = phoneAnimDict 75 | local animation = phoneAnim 76 | 77 | if IsPedInAnyVehicle(player, false) then 78 | animDict = "cellphone@in_car@ds" 79 | end 80 | 81 | StopAnimTask(player, animDict, animation, 1.0) 82 | 83 | RequestAnimDict(animDict) 84 | while not HasAnimDictLoaded(animDict) do 85 | Citizen.Wait(10) 86 | end 87 | 88 | DeletePhone() 89 | 90 | TaskPlayAnim(player, animDict, animation, 3.0, 3.0, -1, 50, 0, false, false, false) 91 | 92 | local x,y,z = table.unpack(GetEntityCoords(player)) 93 | local propName = `prop_npc_phone_02` 94 | RequestModel(propName) 95 | 96 | while not HasModelLoaded(propName) do 97 | Citizen.Wait(10) 98 | end 99 | 100 | phoneProp = CreateObject(propName, x, y, z+0.2, true, true, true) 101 | local boneIndex = GetPedBoneIndex(player, 28422) 102 | AttachEntityToEntity(phoneProp, player, boneIndex, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, true, true, false, true, 1, true) 103 | SetModelAsNoLongerNeeded(propName) 104 | end 105 | 106 | function DeletePhone() 107 | if phoneProp ~= 0 then 108 | DeleteObject(phoneProp) 109 | phoneProp = 0 110 | end 111 | 112 | local player = PlayerPedId() 113 | StopAnimTask(player, phoneAnimDict, phoneAnim, 1.0) 114 | StopAnimTask(player, "cellphone@in_car@ds", phoneAnim, 1.0) 115 | end 116 | 117 | _G.CurrentDealInfo = nil 118 | 119 | Citizen.CreateThread(function() 120 | while QBCore == nil and ESX == nil do 121 | Wait(200) 122 | end 123 | 124 | if Config.Framework == 'qb' then 125 | PlayerData = QBCore.Functions.GetPlayerData() 126 | elseif Config.Framework == 'esx' then 127 | PlayerData = ESX.GetPlayerData() 128 | end 129 | 130 | print("^2Trap Phone: Script initializing...^7") 131 | 132 | RegisterCommand('trapphone', function() 133 | ToggleTrapPhone() 134 | end, false) 135 | 136 | RegisterKeyMapping('trapphone', 'Toggle Trap Phone', 'keyboard', 'F8') 137 | 138 | RegisterNetEvent('trap_phone:usePhone') 139 | AddEventHandler('trap_phone:usePhone', function() 140 | ToggleTrapPhone() 141 | end) 142 | 143 | TimeSeed = GetGameTimer() 144 | math.randomseed(TimeSeed) 145 | 146 | CleanupAllDealerPeds() 147 | end) 148 | 149 | AddEventHandler('baseevents:onPlayerDied', function() 150 | if TrapPhoneVisible then 151 | CloseTrapPhone() 152 | end 153 | end) 154 | 155 | AddEventHandler('baseevents:onPlayerWasted', function() 156 | if TrapPhoneVisible then 157 | CloseTrapPhone() 158 | end 159 | end) 160 | 161 | Citizen.CreateThread(function() 162 | while true do 163 | TimeSeed = (TimeSeed + 10000) + GetGameTimer() 164 | math.randomseed(TimeSeed) 165 | Wait(60000) 166 | end 167 | end) 168 | 169 | if Config.Framework == 'qb' then 170 | RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() 171 | PlayerData = QBCore.Functions.GetPlayerData() 172 | end) 173 | 174 | RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() 175 | PlayerData = {} 176 | CloseTrapPhone() 177 | CleanupAllDealerPeds() 178 | end) 179 | 180 | RegisterNetEvent('QBCore:Player:SetPlayerData', function(data) 181 | PlayerData = data 182 | end) 183 | elseif Config.Framework == 'esx' then 184 | RegisterNetEvent('esx:playerLoaded') 185 | AddEventHandler('esx:playerLoaded', function(xPlayer) 186 | PlayerData = xPlayer 187 | end) 188 | 189 | RegisterNetEvent('esx:onPlayerLogout') 190 | AddEventHandler('esx:onPlayerLogout', function() 191 | PlayerData = {} 192 | CloseTrapPhone() 193 | CleanupAllDealerPeds() 194 | end) 195 | 196 | RegisterNetEvent('esx:setPlayerData') 197 | AddEventHandler('esx:setPlayerData', function(key, value) 198 | PlayerData[key] = value 199 | end) 200 | end 201 | 202 | RegisterNetEvent('trap_phone:showNotification') 203 | AddEventHandler('trap_phone:showNotification', function(message, type) 204 | if Config.Framework == 'qb' then 205 | QBCore.Functions.Notify(message, type) 206 | elseif Config.Framework == 'esx' then 207 | ESX.ShowNotification(message) 208 | end 209 | end) 210 | 211 | RegisterNUICallback('setWaypoint', function(data, cb) 212 | if CurrentMeetLocation then 213 | print("^3Meeting location already exists, updating deal info only^7") 214 | 215 | local drugName = data.drugItemName or data.drugName 216 | local quantity = tonumber(data.quantity) or 1 217 | local price = tonumber(data.price) or 200 218 | 219 | if ActiveDeal then 220 | ActiveDeal.drugName = drugName 221 | ActiveDeal.quantity = quantity 222 | ActiveDeal.price = price 223 | 224 | print("^2Updated ActiveDeal without changing location: " .. 225 | ActiveDeal.drugName .. " x" .. 226 | ActiveDeal.quantity .. " for $" .. 227 | ActiveDeal.price .. "^7") 228 | else 229 | ActiveDeal = { 230 | drugName = drugName, 231 | quantity = quantity, 232 | price = price, 233 | contactName = ActiveContact and ActiveContact.name or "Unknown" 234 | } 235 | 236 | print("^2Created new ActiveDeal with existing location: " .. 237 | ActiveDeal.drugName .. " x" .. 238 | ActiveDeal.quantity .. " for $" .. 239 | ActiveDeal.price .. "^7") 240 | end 241 | 242 | _G.CurrentDealInfo = { 243 | drugName = drugName, 244 | quantity = quantity, 245 | price = price, 246 | timestamp = GetGameTimer() 247 | } 248 | 249 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 250 | 251 | cb({status = "success", message = "Deal details updated"}) 252 | return 253 | end 254 | 255 | local drugName = data.drugItemName or data.drugName or "weed_baggy" 256 | local quantity = tonumber(data.quantity) or 1 257 | local price = tonumber(data.price) or 200 258 | 259 | print("^3Creating new ActiveDeal in setWaypoint: " .. drugName .. " x" .. quantity .. " for $" .. price .. "^7") 260 | 261 | ActiveDeal = { 262 | drugName = drugName, 263 | quantity = quantity, 264 | price = price, 265 | contactName = ActiveContact and ActiveContact.name or "Unknown" 266 | } 267 | 268 | _G.CurrentDealInfo = { 269 | drugName = drugName, 270 | quantity = quantity, 271 | price = price, 272 | timestamp = GetGameTimer() 273 | } 274 | 275 | print("^4Global deal info set: " .. _G.CurrentDealInfo.drugName .. 276 | " x" .. _G.CurrentDealInfo.quantity .. 277 | " for $" .. _G.CurrentDealInfo.price .. "^7") 278 | 279 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 280 | 281 | local success = ProcessDealLocation() 282 | 283 | cb({status = "success", message = "Waypoint set"}) 284 | end) 285 | 286 | function ToggleTrapPhone() 287 | if HasTrapPhone() then 288 | if TrapPhoneVisible then 289 | CloseTrapPhone() 290 | else 291 | OpenTrapPhone() 292 | end 293 | else 294 | if Config.Framework == 'qb' then 295 | QBCore.Functions.Notify('You need a trap phone to do this', 'error') 296 | elseif Config.Framework == 'esx' then 297 | ESX.ShowNotification('You need a trap phone to do this') 298 | end 299 | end 300 | end 301 | 302 | function HasTrapPhone() 303 | print("Checking for trap phone: " .. Config.TrapPhoneItem) 304 | 305 | if Config.Framework == 'qb' then 306 | local items = PlayerData.items 307 | if not items then return false end 308 | 309 | for _, item in pairs(items) do 310 | if item.name == Config.TrapPhoneItem and item.amount > 0 then 311 | return true 312 | end 313 | end 314 | elseif Config.Framework == 'esx' then 315 | if not PlayerData.inventory then 316 | print("ESX: PlayerData.inventory is nil") 317 | return false 318 | end 319 | 320 | print("ESX Inventory Contents:") 321 | for _, item in pairs(PlayerData.inventory) do 322 | print(item.name .. " x" .. (item.count or 0)) 323 | end 324 | 325 | for _, item in pairs(PlayerData.inventory) do 326 | if item.name == Config.TrapPhoneItem and item.count > 0 then 327 | print("Found trap phone in inventory: " .. item.count) 328 | return true 329 | end 330 | end 331 | end 332 | 333 | return false 334 | end 335 | 336 | function OpenTrapPhone() 337 | TrapPhoneVisible = true 338 | SetNuiFocus(true, true) 339 | 340 | StartPhoneAnimation() 341 | 342 | local availableDrugs = GetPlayerDrugs() 343 | 344 | local contacts = {} 345 | if ActiveContact then 346 | table.insert(contacts, ActiveContact) 347 | end 348 | 349 | local currentMeetLocationData = nil 350 | if CurrentMeetLocation then 351 | currentMeetLocationData = CurrentMeetLocation 352 | end 353 | 354 | local playerName = "" 355 | if Config.Framework == 'qb' then 356 | playerName = PlayerData.charinfo.firstname .. ' ' .. PlayerData.charinfo.lastname 357 | elseif Config.Framework == 'esx' then 358 | playerName = PlayerData.name 359 | end 360 | 361 | SendNUIMessage({ 362 | action = 'openPhone', 363 | drugs = availableDrugs, 364 | contacts = contacts, 365 | playerName = playerName, 366 | currentMeetLocation = currentMeetLocationData 367 | }) 368 | 369 | Citizen.CreateThread(function() 370 | while TrapPhoneVisible do 371 | DisableControlAction(0, 1, true) 372 | DisableControlAction(0, 2, true) 373 | DisableControlAction(0, 142, true) 374 | DisableControlAction(0, 18, true) 375 | DisableControlAction(0, 322, true) 376 | DisableControlAction(0, 106, true) 377 | Wait(0) 378 | end 379 | end) 380 | end 381 | 382 | function CloseTrapPhone() 383 | TrapPhoneVisible = false 384 | SetNuiFocus(false, false) 385 | SendNUIMessage({ 386 | action = 'closePhone' 387 | }) 388 | 389 | DeletePhone() 390 | end 391 | 392 | function GetPlayerDrugs() 393 | local playerDrugs = {} 394 | 395 | if Config.Framework == 'qb' then 396 | local items = PlayerData.items 397 | if not items then return playerDrugs end 398 | 399 | for _, item in pairs(items) do 400 | for _, drugConfig in pairs(Config.TrapPhoneDrugs) do 401 | if item.name == drugConfig.name and item.amount > 0 then 402 | table.insert(playerDrugs, { 403 | name = item.name, 404 | label = drugConfig.label, 405 | streetName = drugConfig.streetName, 406 | amount = item.amount, 407 | basePrice = drugConfig.basePrice, 408 | minPrice = drugConfig.priceRange[1], 409 | maxPrice = drugConfig.priceRange[2] 410 | }) 411 | break 412 | end 413 | end 414 | end 415 | elseif Config.Framework == 'esx' then 416 | local inventory = nil 417 | 418 | print("ESX PlayerData Structure: ") 419 | for k, v in pairs(PlayerData) do 420 | print(k, type(v)) 421 | end 422 | 423 | if PlayerData.inventory then 424 | inventory = PlayerData.inventory 425 | print("Using standard ESX inventory") 426 | elseif ESX.GetPlayerData().inventory then 427 | inventory = ESX.GetPlayerData().inventory 428 | print("Using ESX GetPlayerData inventory") 429 | else 430 | print("No inventory found in PlayerData, trying to get it directly") 431 | ESX.TriggerServerCallback('esx:getPlayerData', function(data) 432 | if data and data.inventory then 433 | inventory = data.inventory 434 | end 435 | end) 436 | 437 | local waited = 0 438 | while inventory == nil and waited < 10 do 439 | Citizen.Wait(100) 440 | waited = waited + 1 441 | end 442 | end 443 | 444 | if not inventory then 445 | print("Failed to get inventory from ESX") 446 | return playerDrugs 447 | end 448 | 449 | for k, item in pairs(inventory) do 450 | print("Checking item: " .. item.name .. " count: " .. tostring(item.count)) 451 | 452 | for _, drugConfig in pairs(Config.TrapPhoneDrugs) do 453 | if item.name == drugConfig.name and item.count and item.count > 0 then 454 | print("Found matching drug: " .. item.name .. " x" .. item.count) 455 | table.insert(playerDrugs, { 456 | name = item.name, 457 | label = drugConfig.label, 458 | streetName = drugConfig.streetName, 459 | amount = item.count, 460 | basePrice = drugConfig.basePrice, 461 | minPrice = drugConfig.priceRange[1], 462 | maxPrice = drugConfig.priceRange[2] 463 | }) 464 | break 465 | end 466 | end 467 | end 468 | end 469 | 470 | print("Found " .. #playerDrugs .. " drugs in inventory") 471 | return playerDrugs 472 | end 473 | 474 | function GetCurrentTimeFormatted() 475 | local hours = GetClockHours() 476 | local minutes = GetClockMinutes() 477 | 478 | if hours < 10 then hours = "0" .. hours end 479 | if minutes < 10 then minutes = "0" .. minutes end 480 | 481 | return hours .. ":" .. minutes 482 | end 483 | 484 | function GetRandomMeetLocation() 485 | TimeSeed = (TimeSeed + 12345) + GetGameTimer() 486 | math.randomseed(TimeSeed) 487 | 488 | local availableLocations = {} 489 | for i, location in ipairs(Config.MeetLocations) do 490 | table.insert(availableLocations, location) 491 | end 492 | 493 | if LastUsedLocation then 494 | for i, location in ipairs(availableLocations) do 495 | if location.name == LastUsedLocation.name then 496 | table.remove(availableLocations, i) 497 | break 498 | end 499 | end 500 | end 501 | 502 | if #availableLocations == 0 then 503 | for i, location in ipairs(Config.MeetLocations) do 504 | table.insert(availableLocations, location) 505 | end 506 | end 507 | 508 | local randomIndex = math.random(1, #availableLocations) 509 | local chosenLocation = availableLocations[randomIndex] 510 | 511 | LastUsedLocation = chosenLocation 512 | 513 | print("^3Selected meeting location: " .. chosenLocation.name .. "^7") 514 | 515 | return chosenLocation 516 | end 517 | 518 | function CreateNewContact() 519 | TimeSeed = (TimeSeed + 54321) + GetGameTimer() 520 | math.randomseed(TimeSeed) 521 | 522 | local availableContacts = {} 523 | for i, contact in ipairs(Config.Contacts) do 524 | table.insert(availableContacts, contact) 525 | end 526 | 527 | if ActiveContact then 528 | for i, contact in ipairs(availableContacts) do 529 | if contact.name == ActiveContact.name then 530 | table.remove(availableContacts, i) 531 | break 532 | end 533 | end 534 | end 535 | 536 | if #availableContacts == 0 then 537 | for i, contact in ipairs(Config.Contacts) do 538 | table.insert(availableContacts, contact) 539 | end 540 | end 541 | 542 | local randomIndex = math.random(1, #availableContacts) 543 | local contact = availableContacts[randomIndex] 544 | 545 | local contactId = 'contact_' .. GetGameTimer() .. '_' .. math.random(1000, 9999) 546 | 547 | local newContact = { 548 | id = contactId, 549 | name = contact.name, 550 | avatar = contact.avatar, 551 | avatarColor = contact.avatarColor, 552 | verified = contact.verified, 553 | relationship = math.random(30, 70), 554 | initialMessage = contact.initialMessage, 555 | messages = {} 556 | } 557 | 558 | table.insert(newContact.messages, { 559 | sender = 'them', 560 | text = contact.initialMessage, 561 | time = GetCurrentTimeFormatted() 562 | }) 563 | 564 | ActiveContact = newContact 565 | 566 | _G.CurrentDealInfo = nil 567 | ActiveDeal = nil 568 | CurrentMeetLocation = nil 569 | 570 | SendNUIMessage({ 571 | action = 'newContact', 572 | contact = newContact 573 | }) 574 | 575 | print("^2Trap Phone: New contact created: " .. newContact.name .. "^7") 576 | 577 | return newContact 578 | end 579 | 580 | function ProcessDealLocation() 581 | if CurrentMeetLocation then 582 | 583 | if not ActiveDeal and _G.CurrentDealInfo then 584 | ActiveDeal = { 585 | drugName = _G.CurrentDealInfo.drugName, 586 | quantity = _G.CurrentDealInfo.quantity, 587 | price = _G.CurrentDealInfo.price, 588 | contactName = ActiveContact and ActiveContact.name or "Unknown" 589 | } 590 | print("^2Recreated ActiveDeal from global data^7") 591 | end 592 | 593 | return false 594 | end 595 | 596 | if not ActiveDeal then 597 | print("^1Critical: ProcessDealLocation called without ActiveDeal - creating a default one^7") 598 | 599 | if _G.CurrentDealInfo then 600 | ActiveDeal = { 601 | drugName = _G.CurrentDealInfo.drugName, 602 | quantity = _G.CurrentDealInfo.quantity, 603 | price = _G.CurrentDealInfo.price, 604 | contactName = ActiveContact and ActiveContact.name or "Unknown" 605 | } 606 | print("^2Created ActiveDeal from global data: " .. 607 | ActiveDeal.drugName .. " x" .. 608 | ActiveDeal.quantity .. " for $" .. 609 | ActiveDeal.price .. "^7") 610 | else 611 | ActiveDeal = { 612 | drugName = "weed_baggy", 613 | quantity = 1, 614 | price = 200, 615 | contactName = ActiveContact and ActiveContact.name or "Unknown" 616 | } 617 | 618 | _G.CurrentDealInfo = { 619 | drugName = "weed_baggy", 620 | quantity = 1, 621 | price = 200, 622 | timestamp = GetGameTimer() 623 | } 624 | 625 | print("^3Created default ActiveDeal with no existing data^7") 626 | end 627 | 628 | TriggerServerEvent('trap_phone:registerTransaction', ActiveDeal) 629 | else 630 | _G.CurrentDealInfo = { 631 | drugName = ActiveDeal.drugName, 632 | quantity = ActiveDeal.quantity, 633 | price = ActiveDeal.price, 634 | timestamp = GetGameTimer() 635 | } 636 | end 637 | 638 | local location = GetRandomMeetLocation() 639 | if not location then 640 | print("^1Warning: Failed to get random location^7") 641 | return false 642 | end 643 | 644 | CurrentMeetLocation = location 645 | 646 | if ActiveContact then 647 | print("^3Before adding location message - Messages count: " .. #ActiveContact.messages .. "^7") 648 | end 649 | 650 | local responses = Config.NPCResponses.meet_location 651 | local responseIndex = math.random(1, #responses) 652 | local response = string.format(responses[responseIndex], location.name) 653 | 654 | if ActiveContact then 655 | local isDuplicate = false 656 | if #ActiveContact.messages > 0 then 657 | local lastMsg = ActiveContact.messages[#ActiveContact.messages] 658 | if lastMsg.sender == 'them' and lastMsg.text == response then 659 | isDuplicate = true 660 | print("^3Avoided adding duplicate location message^7") 661 | end 662 | end 663 | 664 | if not isDuplicate then 665 | table.insert(ActiveContact.messages, { 666 | sender = 'them', 667 | text = response, 668 | time = GetCurrentTimeFormatted() 669 | }) 670 | end 671 | 672 | SendNUIMessage({ 673 | action = 'updateMessages', 674 | messages = ActiveContact.messages, 675 | preserveChat = true, 676 | locationSet = true, 677 | locationName = location.name 678 | }) 679 | 680 | print("^3After adding location message - Messages count: " .. #ActiveContact.messages .. "^7") 681 | end 682 | 683 | SetNewWaypoint(location.coords.x, location.coords.y) 684 | 685 | SetupMeetingPoint(location) 686 | 687 | if Config.Framework == 'qb' then 688 | QBCore.Functions.Notify('Meeting location marked on your GPS', 'success') 689 | elseif Config.Framework == 'esx' then 690 | ESX.ShowNotification('Meeting location marked on your GPS') 691 | end 692 | 693 | if math.random(100) <= Config.PoliceSettings.alertChance then 694 | AlertPolice() 695 | end 696 | 697 | return true 698 | end 699 | 700 | function GetSafeGroundPosition(position) 701 | local x, y, z = position.x, position.y, position.z 702 | local groundFound, groundZ = GetGroundZFor_3dCoord(x, y, z, false) 703 | 704 | if groundFound then 705 | return vector3(x, y, groundZ) 706 | else 707 | local offsetX = math.random(-5, 5) 708 | local offsetY = math.random(-5, 5) 709 | groundFound, groundZ = GetGroundZFor_3dCoord(x + offsetX, y + offsetY, z, false) 710 | 711 | if groundFound then 712 | return vector3(x + offsetX, y + offsetY, groundZ) 713 | else 714 | return vector3(x, y, z + 0.5) 715 | end 716 | end 717 | end 718 | 719 | function GetNearestRoadPosition(coords) 720 | local safePos = GetSafeGroundPosition(coords) 721 | 722 | local success, roadPosition = GetNthClosestVehicleNode(safePos.x, safePos.y, safePos.z, 1, 0, 0, 0) 723 | 724 | if success then 725 | if type(roadPosition) == "vector3" then 726 | return roadPosition 727 | end 728 | end 729 | 730 | local success2, roadPos = GetClosestVehicleNodeWithHeading(safePos.x, safePos.y, safePos.z, 0, 3.0, 0) 731 | 732 | if success2 and type(roadPos) == "vector3" then 733 | return roadPos 734 | end 735 | 736 | return safePos 737 | end 738 | 739 | function SetupMeetingPoint(location) 740 | if not ActiveDeal then 741 | print("^1Error: No active deal when trying to setup meeting point^7") 742 | if _G.CurrentDealInfo then 743 | ActiveDeal = { 744 | drugName = _G.CurrentDealInfo.drugName, 745 | quantity = _G.CurrentDealInfo.quantity, 746 | price = _G.CurrentDealInfo.price, 747 | contactName = ActiveContact and ActiveContact.name or "Unknown" 748 | } 749 | print("^2Created ActiveDeal from global data in SetupMeetingPoint^7") 750 | else 751 | print("^1No global deal info available, cannot setup meeting^7") 752 | return 753 | end 754 | end 755 | 756 | ActiveDeal.quantity = tonumber(ActiveDeal.quantity) or 1 757 | ActiveDeal.price = tonumber(ActiveDeal.price) or 200 758 | 759 | print("^4BEFORE CREATING DEAL - Active Deal Values: " .. 760 | ActiveDeal.drugName .. " x" .. 761 | ActiveDeal.quantity .. " for $" .. 762 | ActiveDeal.price .. "^7") 763 | 764 | local dealId = 'deal_' .. GetGameTimer() .. '_' .. math.random(1000, 9999) 765 | 766 | local exactDrugName = ActiveDeal.drugName 767 | local exactQuantity = tonumber(ActiveDeal.quantity) 768 | local exactPrice = tonumber(ActiveDeal.price) 769 | 770 | print("^3Setting up deal - Drug: " .. exactDrugName .. 771 | ", Quantity: " .. exactQuantity .. 772 | ", Price: $" .. exactPrice .. "^7") 773 | 774 | local dealDetails = { 775 | dealId = dealId, 776 | contactName = ActiveContact and ActiveContact.name or "Unknown", 777 | drugName = exactDrugName, 778 | quantity = exactQuantity, 779 | price = exactPrice, 780 | location = location 781 | } 782 | 783 | print("^4CRITICAL - Deal details before storage: " .. 784 | dealDetails.drugName .. " x" .. 785 | dealDetails.quantity .. " for $" .. 786 | dealDetails.price .. "^7") 787 | 788 | DealerDeals[dealId] = { 789 | dealId = dealId, 790 | contactName = dealDetails.contactName, 791 | drugName = exactDrugName, 792 | quantity = exactQuantity, 793 | price = exactPrice, 794 | location = location 795 | } 796 | 797 | print("^4VERIFICATION - DealerDeals[" .. dealId .. "]: " .. 798 | DealerDeals[dealId].drugName .. " x" .. 799 | DealerDeals[dealId].quantity .. " for $" .. 800 | DealerDeals[dealId].price .. "^7") 801 | 802 | _G.CurrentDealInfo = { 803 | drugName = exactDrugName, 804 | quantity = exactQuantity, 805 | price = exactPrice, 806 | timestamp = GetGameTimer() 807 | } 808 | 809 | print("^4CRITICAL - Global deal info updated: " .. 810 | _G.CurrentDealInfo.drugName .. " x" .. 811 | _G.CurrentDealInfo.quantity .. " for $" .. 812 | _G.CurrentDealInfo.price .. "^7") 813 | 814 | TriggerServerEvent('trap_phone:registerDeal', DealerDeals[dealId]) 815 | 816 | CreateDealerNPCAtLocation(location, dealId) 817 | 818 | TriggerEvent('drug_selling:client:createDealerNPC', { 819 | dealId = dealId, 820 | drugName = exactDrugName, 821 | quantity = exactQuantity, 822 | price = exactPrice, 823 | coords = location.coords 824 | }) 825 | end 826 | 827 | function CreateDealerNPCAtLocation(location, dealId) 828 | if not DealerDeals[dealId] then 829 | print("^1ERROR: No DealerDeals found for dealId " .. dealId .. " before creating NPC^7") 830 | return 831 | end 832 | 833 | local exactDrugName = DealerDeals[dealId].drugName 834 | local exactQuantity = tonumber(DealerDeals[dealId].quantity) 835 | local exactPrice = tonumber(DealerDeals[dealId].price) 836 | 837 | print("^4CREATE DEALER - Using exact deal info: " .. 838 | exactDrugName .. " x" .. exactQuantity .. 839 | " for $" .. exactPrice .. "^7") 840 | 841 | Citizen.CreateThread(function() 842 | local isPlayerClose = false 843 | local proximityCheckInterval = 3000 844 | local timeoutCounter = 0 845 | local maxTimeout = 40 846 | 847 | while not isPlayerClose and timeoutCounter < maxTimeout do 848 | Wait(proximityCheckInterval) 849 | timeoutCounter = timeoutCounter + 1 850 | 851 | local playerCoords = GetEntityCoords(PlayerPedId()) 852 | local distanceToLocation = #(playerCoords - location.coords) 853 | 854 | if distanceToLocation < 200.0 then 855 | isPlayerClose = true 856 | end 857 | end 858 | 859 | if not DealerDeals[dealId] then 860 | print("^1Error: DealerDeals was lost during proximity wait for dealId: " .. dealId .. "^7") 861 | 862 | DealerDeals[dealId] = { 863 | dealId = dealId, 864 | contactName = ActiveContact and ActiveContact.name or "Unknown", 865 | drugName = exactDrugName, 866 | quantity = exactQuantity, 867 | price = exactPrice, 868 | location = location 869 | } 870 | 871 | print("^2Recreated DealerDeals with exact values: " .. 872 | exactDrugName .. " x" .. exactQuantity .. 873 | " for $" .. exactPrice .. "^7") 874 | else 875 | DealerDeals[dealId].drugName = exactDrugName 876 | DealerDeals[dealId].quantity = exactQuantity 877 | DealerDeals[dealId].price = exactPrice 878 | 879 | print("^2Verified and corrected DealerDeals data: " .. 880 | DealerDeals[dealId].drugName .. " x" .. 881 | DealerDeals[dealId].quantity .. " for $" .. 882 | DealerDeals[dealId].price .. "^7") 883 | end 884 | 885 | print("^2Creating dealer NPC at location: " .. location.name .. "^7") 886 | 887 | local pedModels = { 888 | "a_m_m_eastsa_01", 889 | "a_m_m_eastsa_02", 890 | "a_m_y_eastsa_01", 891 | "a_m_m_soucent_01", 892 | "a_m_y_soucent_01", 893 | "a_m_m_mexlabor_01", 894 | "g_m_y_mexgoon_01", 895 | "a_m_m_afriamer_01", 896 | "a_m_y_mexthug_01", 897 | "cs_orleans" 898 | } 899 | 900 | TimeSeed = (TimeSeed + 98765) + GetGameTimer() 901 | math.randomseed(TimeSeed) 902 | local pedModel = pedModels[math.random(#pedModels)] 903 | 904 | local pedHash = GetHashKey(pedModel) 905 | RequestModel(pedHash) 906 | 907 | local modelLoadTimeoutCounter = 0 908 | while not HasModelLoaded(pedHash) and modelLoadTimeoutCounter < 100 do 909 | Wait(100) 910 | modelLoadTimeoutCounter = modelLoadTimeoutCounter + 1 911 | end 912 | 913 | if HasModelLoaded(pedHash) then 914 | local offsetX = math.random(-3, 3) * 0.5 915 | local offsetY = math.random(-3, 3) * 0.5 916 | local dealerPos = vector3(location.coords.x + offsetX, location.coords.y + offsetY, location.coords.z) 917 | 918 | local groundFound, groundZ = GetGroundZFor_3dCoord(dealerPos.x, dealerPos.y, dealerPos.z, false) 919 | if groundFound then 920 | dealerPos = vector3(dealerPos.x, dealerPos.y, groundZ) 921 | end 922 | 923 | local dealerPed = CreatePed(4, pedHash, dealerPos.x, dealerPos.y, dealerPos.z, 0.0, false, true) 924 | 925 | table.insert(DealerPeds, dealerPed) 926 | 927 | SetEntityInvincible(dealerPed, true) 928 | SetBlockingOfNonTemporaryEvents(dealerPed, true) 929 | 930 | local heading = math.random(0, 359) + 0.0 931 | SetEntityHeading(dealerPed, heading) 932 | 933 | local blip = AddBlipForEntity(dealerPed) 934 | SetBlipSprite(blip, 500) 935 | SetBlipColour(blip, 1) 936 | SetBlipScale(blip, 0.8) 937 | SetBlipAsShortRange(blip, true) 938 | BeginTextCommandSetBlipName("STRING") 939 | AddTextComponentString("Drug Deal") 940 | EndTextCommandSetBlipName(blip) 941 | 942 | Wait(500) 943 | 944 | local animDict = "amb@world_human_smoking@male@male_a@base" 945 | RequestAnimDict(animDict) 946 | 947 | local animLoadTimeoutCounter = 0 948 | while not HasAnimDictLoaded(animDict) and animLoadTimeoutCounter < 100 do 949 | Wait(100) 950 | animLoadTimeoutCounter = animLoadTimeoutCounter + 1 951 | end 952 | 953 | local propName = "prop_cs_ciggy_01" 954 | RequestModel(GetHashKey(propName)) 955 | 956 | local propLoadTimeoutCounter = 0 957 | while not HasModelLoaded(GetHashKey(propName)) and propLoadTimeoutCounter < 100 do 958 | Wait(100) 959 | propLoadTimeoutCounter = propLoadTimeoutCounter + 1 960 | end 961 | 962 | if HasAnimDictLoaded(animDict) and HasModelLoaded(GetHashKey(propName)) then 963 | TaskPlayAnim(dealerPed, animDict, "base", 8.0, -8.0, -1, 1, 0, false, false, false) 964 | 965 | local boneIndex = GetPedBoneIndex(dealerPed, 28422) 966 | local cigaretteProp = CreateObject(GetHashKey(propName), dealerPos.x, dealerPos.y, dealerPos.z, true, true, true) 967 | 968 | if cigaretteProp ~= 0 then 969 | AttachEntityToEntity(cigaretteProp, dealerPed, boneIndex, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, true, true, false, true, 1, true) 970 | end 971 | 972 | SetModelAsNoLongerNeeded(GetHashKey(propName)) 973 | end 974 | 975 | PedDealMap[dealerPed] = dealId 976 | 977 | print("^3Dealer deal data - DealId: " .. dealId .. 978 | ", Drug: " .. exactDrugName .. 979 | ", Quantity: " .. exactQuantity .. 980 | ", Price: $" .. exactPrice .. "^7") 981 | 982 | PedDealMap[dealerPed] = dealId 983 | 984 | if Entity then 985 | local state = Entity(dealerPed).state 986 | if state then 987 | local dealDetailsCopy = { 988 | dealId = dealId, 989 | drugName = exactDrugName, 990 | quantity = exactQuantity, 991 | price = exactPrice, 992 | contactName = DealerDeals[dealId].contactName 993 | } 994 | state:set('dealDetails', dealDetailsCopy, true) 995 | print("^2Deal details saved to entity state successfully^7") 996 | end 997 | end 998 | 999 | _G.CurrentDealInfo = { 1000 | drugName = exactDrugName, 1001 | quantity = exactQuantity, 1002 | price = exactPrice, 1003 | timestamp = GetGameTimer() 1004 | } 1005 | 1006 | Citizen.InvokeNative(0x9CB1A1623062F402, dealerPed, "trap_dealer_" .. dealId) 1007 | 1008 | print("^4CRITICAL - Global deal info updated: " .. _G.CurrentDealInfo.drugName .. 1009 | " x" .. _G.CurrentDealInfo.quantity .. 1010 | " for $" .. _G.CurrentDealInfo.price .. "^7") 1011 | 1012 | if Config.Framework == 'qb' then 1013 | QBCore.Functions.Notify('The dealer is waiting for you at the marked location', 'info') 1014 | elseif Config.Framework == 'esx' then 1015 | ESX.ShowNotification('The dealer is waiting for you at the marked location') 1016 | end 1017 | 1018 | if Config.Framework == 'qb' and Config.UseQBTarget then 1019 | SetupQBTargetInteraction(dealerPed, dealId, blip) 1020 | elseif Config.UseOxTarget then 1021 | SetupOxTargetInteraction(dealerPed, dealId, blip) 1022 | else 1023 | SetupDealerInteraction(dealerPed, dealId, blip) 1024 | end 1025 | 1026 | SetModelAsNoLongerNeeded(pedHash) 1027 | else 1028 | print("^1Failed to load dealer ped model^7") 1029 | end 1030 | end) 1031 | end 1032 | 1033 | function SetupQBTargetInteraction(dealerPed, dealId, blip) 1034 | exports['qb-target']:AddTargetEntity(dealerPed, { 1035 | options = { 1036 | { 1037 | type = "client", 1038 | event = "", 1039 | icon = "fas fa-handshake", 1040 | label = "Trap Deal", 1041 | action = function() 1042 | if ActiveDeal and ActiveDeal.drugName and ActiveDeal.quantity and ActiveDeal.price then 1043 | print("^4CRITICAL - Using ActiveDeal as highest priority source^7") 1044 | print("^4ActiveDeal contains: " .. ActiveDeal.drugName .. 1045 | " x" .. ActiveDeal.quantity .. 1046 | " for $" .. ActiveDeal.price .. "^7") 1047 | 1048 | local exactDealDetails = { 1049 | dealId = dealId or "active_deal", 1050 | drugName = ActiveDeal.drugName, 1051 | quantity = tonumber(ActiveDeal.quantity), 1052 | price = tonumber(ActiveDeal.price) 1053 | } 1054 | 1055 | print("^4CRITICAL - Using exact ActiveDeal details: " .. 1056 | exactDealDetails.drugName .. " x" .. 1057 | exactDealDetails.quantity .. " for $" .. 1058 | exactDealDetails.price .. "^7") 1059 | 1060 | local dealCompleted = HandleDrugDeal(dealerPed, dealId, exactDealDetails) 1061 | 1062 | if dealCompleted then 1063 | if blip and DoesBlipExist(blip) then 1064 | RemoveBlip(blip) 1065 | end 1066 | end 1067 | 1068 | return 1069 | end 1070 | 1071 | local dealDetails = nil 1072 | 1073 | local pedDealId = PedDealMap[dealerPed] or dealId 1074 | 1075 | print("^4Looking for deal. Ped Deal ID: " .. tostring(pedDealId) .. "^7") 1076 | 1077 | if pedDealId and DealerDeals and DealerDeals[pedDealId] then 1078 | dealDetails = DealerDeals[pedDealId] 1079 | print("^2QB-Target: Found deal in DealerDeals with ID: " .. pedDealId .. "^7") 1080 | 1081 | elseif Entity then 1082 | local state = Entity(dealerPed).state 1083 | if state and state.dealDetails then 1084 | dealDetails = state.dealDetails 1085 | print("^2QB-Target: Found deal in entity state^7") 1086 | end 1087 | 1088 | elseif _G.CurrentDealInfo then 1089 | dealDetails = { 1090 | dealId = pedDealId or "global_deal", 1091 | drugName = _G.CurrentDealInfo.drugName, 1092 | quantity = _G.CurrentDealInfo.quantity, 1093 | price = _G.CurrentDealInfo.price 1094 | } 1095 | print("^2QB-Target: Using global CurrentDealInfo^7") 1096 | 1097 | elseif ActiveDeal then 1098 | dealDetails = { 1099 | dealId = pedDealId or "active_deal", 1100 | drugName = ActiveDeal.drugName, 1101 | quantity = ActiveDeal.quantity, 1102 | price = ActiveDeal.price 1103 | } 1104 | print("^2QB-Target: Using ActiveDeal as fallback^7") 1105 | end 1106 | 1107 | if dealDetails then 1108 | print("^3QB-Target interaction with details - Drug: " .. dealDetails.drugName .. 1109 | ", Quantity: " .. tostring(dealDetails.quantity) .. 1110 | ", Price: $" .. tostring(dealDetails.price) .. "^7") 1111 | else 1112 | print("^1QB-Target: No deal details found! Creating default deal.^7") 1113 | 1114 | dealDetails = { 1115 | dealId = pedDealId or "default_deal", 1116 | drugName = "weed_baggy", 1117 | quantity = 1, 1118 | price = 200 1119 | } 1120 | end 1121 | 1122 | local dealCompleted = HandleDrugDeal(dealerPed, pedDealId, dealDetails) 1123 | 1124 | if dealCompleted then 1125 | if blip and DoesBlipExist(blip) then 1126 | RemoveBlip(blip) 1127 | end 1128 | end 1129 | end, 1130 | canInteract = function() 1131 | return true 1132 | end, 1133 | } 1134 | }, 1135 | distance = 2.5, 1136 | }) 1137 | end 1138 | 1139 | function SetupOxTargetInteraction(dealerPed, dealId, blip) 1140 | exports.ox_target:addLocalEntity(dealerPed, { 1141 | { 1142 | name = 'drug_deal_' .. dealId, 1143 | icon = 'fas fa-handshake', 1144 | label = 'Trap Deal', 1145 | distance = 2.5, 1146 | onSelect = function() 1147 | if ActiveDeal and ActiveDeal.drugName and ActiveDeal.quantity and ActiveDeal.price then 1148 | print("^4CRITICAL - Using ActiveDeal as highest priority source^7") 1149 | print("^4ActiveDeal contains: " .. ActiveDeal.drugName .. 1150 | " x" .. ActiveDeal.quantity .. 1151 | " for $" .. ActiveDeal.price .. "^7") 1152 | 1153 | local exactDealDetails = { 1154 | dealId = dealId or "active_deal", 1155 | drugName = ActiveDeal.drugName, 1156 | quantity = tonumber(ActiveDeal.quantity), 1157 | price = tonumber(ActiveDeal.price) 1158 | } 1159 | 1160 | print("^4CRITICAL - Using exact ActiveDeal details: " .. 1161 | exactDealDetails.drugName .. " x" .. 1162 | exactDealDetails.quantity .. " for $" .. 1163 | exactDealDetails.price .. "^7") 1164 | 1165 | local dealCompleted = HandleDrugDeal(dealerPed, dealId, exactDealDetails) 1166 | 1167 | if dealCompleted then 1168 | if blip and DoesBlipExist(blip) then 1169 | RemoveBlip(blip) 1170 | end 1171 | end 1172 | 1173 | return 1174 | end 1175 | 1176 | local dealDetails = nil 1177 | 1178 | local pedDealId = PedDealMap[dealerPed] or dealId 1179 | 1180 | print("^4Looking for deal. Ped Deal ID: " .. tostring(pedDealId) .. "^7") 1181 | 1182 | if pedDealId and DealerDeals and DealerDeals[pedDealId] then 1183 | dealDetails = DealerDeals[pedDealId] 1184 | print("^2OX-Target: Found deal in DealerDeals with ID: " .. pedDealId .. "^7") 1185 | 1186 | elseif Entity then 1187 | local state = Entity(dealerPed).state 1188 | if state and state.dealDetails then 1189 | dealDetails = state.dealDetails 1190 | print("^2OX-Target: Found deal in entity state^7") 1191 | end 1192 | 1193 | elseif _G.CurrentDealInfo then 1194 | dealDetails = { 1195 | dealId = pedDealId or "global_deal", 1196 | drugName = _G.CurrentDealInfo.drugName, 1197 | quantity = _G.CurrentDealInfo.quantity, 1198 | price = _G.CurrentDealInfo.price 1199 | } 1200 | print("^2OX-Target: Using global CurrentDealInfo^7") 1201 | 1202 | elseif ActiveDeal then 1203 | dealDetails = { 1204 | dealId = pedDealId or "active_deal", 1205 | drugName = ActiveDeal.drugName, 1206 | quantity = ActiveDeal.quantity, 1207 | price = ActiveDeal.price 1208 | } 1209 | print("^2OX-Target: Using ActiveDeal as fallback^7") 1210 | end 1211 | 1212 | if dealDetails then 1213 | print("^3OX-Target interaction with details - Drug: " .. dealDetails.drugName .. 1214 | ", Quantity: " .. tostring(dealDetails.quantity) .. 1215 | ", Price: $" .. tostring(dealDetails.price) .. "^7") 1216 | else 1217 | print("^1OX-Target: No deal details found! Creating default deal.^7") 1218 | 1219 | dealDetails = { 1220 | dealId = pedDealId or "default_deal", 1221 | drugName = "weed_baggy", 1222 | quantity = 1, 1223 | price = 200 1224 | } 1225 | end 1226 | 1227 | local dealCompleted = HandleDrugDeal(dealerPed, pedDealId, dealDetails) 1228 | 1229 | if dealCompleted then 1230 | if blip and DoesBlipExist(blip) then 1231 | RemoveBlip(blip) 1232 | end 1233 | end 1234 | end 1235 | } 1236 | }) 1237 | end 1238 | 1239 | function SetupDealerInteraction(dealerPed, dealId, blip) 1240 | Citizen.CreateThread(function() 1241 | local interactionActive = true 1242 | local dealCompleted = false 1243 | 1244 | while interactionActive and DoesEntityExist(dealerPed) do 1245 | Wait(0) 1246 | 1247 | local playerCoords = GetEntityCoords(PlayerPedId()) 1248 | local pedCoords = GetEntityCoords(dealerPed) 1249 | local distance = #(playerCoords - pedCoords) 1250 | 1251 | if distance < 2.0 and not dealCompleted then 1252 | AddTextEntry('TRAPPHONE_DEAL', "Press ~INPUT_CONTEXT~ to deal with the dealer") 1253 | DisplayHelpTextThisFrame('TRAPPHONE_DEAL', false) 1254 | 1255 | if IsControlJustReleased(0, 38) then 1256 | if ActiveDeal and ActiveDeal.drugName and ActiveDeal.quantity and ActiveDeal.price then 1257 | print("^4CRITICAL - Using ActiveDeal as highest priority source^7") 1258 | print("^4ActiveDeal contains: " .. ActiveDeal.drugName .. 1259 | " x" .. ActiveDeal.quantity .. 1260 | " for $" .. ActiveDeal.price .. "^7") 1261 | 1262 | local exactDealDetails = { 1263 | dealId = dealId or "active_deal", 1264 | drugName = ActiveDeal.drugName, 1265 | quantity = tonumber(ActiveDeal.quantity), 1266 | price = tonumber(ActiveDeal.price) 1267 | } 1268 | 1269 | print("^4CRITICAL - Using exact ActiveDeal details: " .. 1270 | exactDealDetails.drugName .. " x" .. 1271 | exactDealDetails.quantity .. " for $" .. 1272 | exactDealDetails.price .. "^7") 1273 | 1274 | local dealCompleted = HandleDrugDeal(dealerPed, dealId, exactDealDetails) 1275 | 1276 | if dealCompleted then 1277 | if blip and DoesBlipExist(blip) then 1278 | RemoveBlip(blip) 1279 | end 1280 | end 1281 | 1282 | return 1283 | end 1284 | 1285 | local dealDetails = nil 1286 | 1287 | local pedDealId = PedDealMap[dealerPed] or dealId 1288 | 1289 | print("^4Looking for deal with E-Key. Ped Deal ID: " .. tostring(pedDealId) .. "^7") 1290 | 1291 | if pedDealId and DealerDeals and DealerDeals[pedDealId] then 1292 | dealDetails = DealerDeals[pedDealId] 1293 | print("^2E-Key: Found deal in DealerDeals with ID: " .. pedDealId .. "^7") 1294 | 1295 | elseif Entity then 1296 | local state = Entity(dealerPed).state 1297 | if state and state.dealDetails then 1298 | dealDetails = state.dealDetails 1299 | print("^2E-Key: Found deal in entity state^7") 1300 | end 1301 | 1302 | elseif _G.CurrentDealInfo then 1303 | dealDetails = { 1304 | dealId = pedDealId or "global_deal", 1305 | drugName = _G.CurrentDealInfo.drugName, 1306 | quantity = _G.CurrentDealInfo.quantity, 1307 | price = _G.CurrentDealInfo.price 1308 | } 1309 | print("^2E-Key: Using global CurrentDealInfo^7") 1310 | 1311 | elseif ActiveDeal then 1312 | dealDetails = { 1313 | dealId = pedDealId or "active_deal", 1314 | drugName = ActiveDeal.drugName, 1315 | quantity = ActiveDeal.quantity, 1316 | price = ActiveDeal.price 1317 | } 1318 | print("^2E-Key: Using ActiveDeal as fallback^7") 1319 | end 1320 | 1321 | if dealDetails then 1322 | print("^3E-Key interaction with details - Drug: " .. dealDetails.drugName .. 1323 | ", Quantity: " .. tostring(dealDetails.quantity) .. 1324 | ", Price: $" .. tostring(dealDetails.price) .. "^7") 1325 | else 1326 | print("^1E-Key: No deal details found! Creating default deal.^7") 1327 | 1328 | dealDetails = { 1329 | dealId = pedDealId or "default_deal", 1330 | drugName = "weed_baggy", 1331 | quantity = 1, 1332 | price = 200 1333 | } 1334 | end 1335 | 1336 | dealCompleted = HandleDrugDeal(dealerPed, pedDealId, dealDetails) 1337 | 1338 | if dealCompleted then 1339 | if blip and DoesBlipExist(blip) then 1340 | RemoveBlip(blip) 1341 | end 1342 | end 1343 | end 1344 | end 1345 | 1346 | if distance > 100.0 then 1347 | interactionActive = false 1348 | end 1349 | 1350 | if dealCompleted then 1351 | interactionActive = false 1352 | end 1353 | end 1354 | end) 1355 | end 1356 | 1357 | function HandleDrugDeal(dealerPed, dealId, dealDetails) 1358 | if ActiveDeal and ActiveDeal.drugName and ActiveDeal.quantity and ActiveDeal.price then 1359 | print("^4CRITICAL - Using ActiveDeal as highest priority source^7") 1360 | print("^4ActiveDeal contains: " .. ActiveDeal.drugName .. 1361 | " x" .. ActiveDeal.quantity .. 1362 | " for $" .. ActiveDeal.price .. "^7") 1363 | 1364 | local exactDealDetails = { 1365 | dealId = dealId or "active_deal", 1366 | drugName = ActiveDeal.drugName, 1367 | quantity = tonumber(ActiveDeal.quantity), 1368 | price = tonumber(ActiveDeal.price) 1369 | } 1370 | 1371 | print("^4CRITICAL - Using exact ActiveDeal details: " .. 1372 | exactDealDetails.drugName .. " x" .. 1373 | exactDealDetails.quantity .. " for $" .. 1374 | exactDealDetails.price .. "^7") 1375 | 1376 | local drugName = exactDealDetails.drugName 1377 | local quantity = exactDealDetails.quantity 1378 | local price = exactDealDetails.price 1379 | 1380 | print("^3FINAL DEAL VALUES BEFORE CALLBACK - Drug: " .. drugName .. 1381 | ", Quantity: " .. tostring(quantity) .. 1382 | ", Price: $" .. tostring(price) .. "^7") 1383 | 1384 | if Config.Framework == 'qb' then 1385 | QBCore.Functions.TriggerCallback('QBCore:HasItem', function(hasItem) 1386 | HandleDrugDealResponse(hasItem, dealerPed, dealId, drugName, quantity, price, blip) 1387 | end, drugName, quantity) 1388 | elseif Config.Framework == 'esx' then 1389 | ESX.TriggerServerCallback('QBCore:HasItem', function(hasItem) 1390 | HandleDrugDealResponse(hasItem, dealerPed, dealId, drugName, quantity, price, blip) 1391 | end, drugName, quantity) 1392 | end 1393 | 1394 | return true 1395 | end 1396 | 1397 | print("^2HandleDrugDeal called with dealId: " .. tostring(dealId) .. "^7") 1398 | if dealDetails then 1399 | print("^2Received dealDetails directly - drug: " .. tostring(dealDetails.drugName) .. 1400 | ", quantity: " .. tostring(dealDetails.quantity) .. 1401 | ", price: " .. tostring(dealDetails.price) .. "^7") 1402 | else 1403 | print("^1NO DEAL DETAILS RECEIVED DIRECTLY^7") 1404 | end 1405 | 1406 | local finalDealDetails = nil 1407 | 1408 | if dealDetails and dealDetails.drugName and dealDetails.quantity and dealDetails.price then 1409 | finalDealDetails = dealDetails 1410 | print("^2Using directly provided dealDetails^7") 1411 | 1412 | elseif dealId and DealerDeals and DealerDeals[dealId] then 1413 | finalDealDetails = DealerDeals[dealId] 1414 | print("^2Using DealerDeals[" .. dealId .. "]^7") 1415 | 1416 | elseif Entity and DoesEntityExist(dealerPed) then 1417 | local state = Entity(dealerPed).state 1418 | if state and state.dealDetails then 1419 | finalDealDetails = state.dealDetails 1420 | print("^2Using Entity state dealDetails^7") 1421 | end 1422 | 1423 | elseif _G.CurrentDealInfo then 1424 | finalDealDetails = { 1425 | drugName = _G.CurrentDealInfo.drugName, 1426 | quantity = _G.CurrentDealInfo.quantity, 1427 | price = _G.CurrentDealInfo.price 1428 | } 1429 | print("^2Using global CurrentDealInfo^7") 1430 | 1431 | elseif ActiveDeal then 1432 | finalDealDetails = { 1433 | drugName = ActiveDeal.drugName, 1434 | quantity = ActiveDeal.quantity, 1435 | price = ActiveDeal.price 1436 | } 1437 | print("^2Using ActiveDeal as fallback^7") 1438 | end 1439 | 1440 | if not finalDealDetails then 1441 | if Config.Framework == 'qb' then 1442 | QBCore.Functions.Notify('No active deal to complete', 'error') 1443 | elseif Config.Framework == 'esx' then 1444 | ESX.ShowNotification('No active deal to complete') 1445 | end 1446 | return false 1447 | end 1448 | 1449 | local drugName = finalDealDetails.drugName or "weed_baggy" 1450 | local quantity = tonumber(finalDealDetails.quantity) or 1 1451 | local price = tonumber(finalDealDetails.price) or 200 1452 | 1453 | print("^3FINAL DEAL VALUES BEFORE CALLBACK - Drug: " .. drugName .. 1454 | ", Quantity: " .. tostring(quantity) .. 1455 | ", Price: $" .. tostring(price) .. "^7") 1456 | 1457 | if Config.Framework == 'qb' then 1458 | QBCore.Functions.TriggerCallback('QBCore:HasItem', function(hasItem) 1459 | HandleDrugDealResponse(hasItem, dealerPed, dealId, drugName, quantity, price, blip) 1460 | end, drugName, quantity) 1461 | elseif Config.Framework == 'esx' then 1462 | ESX.TriggerServerCallback('QBCore:HasItem', function(hasItem) 1463 | HandleDrugDealResponse(hasItem, dealerPed, dealId, drugName, quantity, price, blip) 1464 | end, drugName, quantity) 1465 | end 1466 | 1467 | return true 1468 | end 1469 | 1470 | function HandleDrugDealResponse(hasItem, dealerPed, dealId, drugName, quantity, price, blip) 1471 | if hasItem then 1472 | local playerPed = PlayerPedId() 1473 | 1474 | RequestAnimDict("mp_common") 1475 | while not HasAnimDictLoaded("mp_common") do 1476 | Wait(10) 1477 | end 1478 | 1479 | TaskTurnPedToFaceEntity(dealerPed, playerPed, 1000) 1480 | Wait(1000) 1481 | 1482 | ClearPedTasks(dealerPed) 1483 | 1484 | TaskPlayAnim(playerPed, "mp_common", "givetake1_a", 8.0, -8.0, 2000, 0, 0, false, false, false) 1485 | 1486 | TaskPlayAnim(dealerPed, "mp_common", "givetake1_b", 8.0, -8.0, 2000, 0, 0, false, false, false) 1487 | 1488 | Wait(2000) 1489 | 1490 | local dealData = { 1491 | dealId = dealId, 1492 | drugName = drugName, 1493 | quantity = quantity, 1494 | price = price 1495 | } 1496 | 1497 | TriggerServerEvent('trap_phone:completeDeal', dealId, dealData) 1498 | 1499 | if Config.Framework == 'qb' then 1500 | QBCore.Functions.Notify('Deal completed successfully. You received $' .. price .. ' for ' .. quantity .. 'x ' .. drugName, 'success') 1501 | elseif Config.Framework == 'esx' then 1502 | ESX.ShowNotification('Deal completed successfully. You received $' .. price .. ' for ' .. quantity .. 'x ' .. drugName) 1503 | end 1504 | 1505 | ActiveDeal = nil 1506 | _G.CurrentDealInfo = nil 1507 | CurrentMeetLocation = nil 1508 | 1509 | ClearPedTasksImmediately(dealerPed) 1510 | TaskWanderStandard(dealerPed, 10.0, 10) 1511 | 1512 | Citizen.SetTimeout(15000, function() 1513 | if DoesEntityExist(dealerPed) then 1514 | PedDealMap[dealerPed] = nil 1515 | DeleteEntity(dealerPed) 1516 | end 1517 | end) 1518 | 1519 | if DealerDeals[dealId] then 1520 | DealerDeals[dealId] = nil 1521 | end 1522 | 1523 | return true 1524 | else 1525 | if Config.Framework == 'qb' then 1526 | QBCore.Functions.Notify('You don\'t have ' .. quantity .. 'x ' .. drugName, 'error') 1527 | elseif Config.Framework == 'esx' then 1528 | ESX.ShowNotification('You don\'t have ' .. quantity .. 'x ' .. drugName) 1529 | end 1530 | return false 1531 | end 1532 | end 1533 | 1534 | function CleanupAllDealerPeds() 1535 | for _, ped in ipairs(DealerPeds) do 1536 | if DoesEntityExist(ped) then 1537 | PedDealMap[ped] = nil 1538 | DeleteEntity(ped) 1539 | end 1540 | end 1541 | DealerPeds = {} 1542 | 1543 | DealerDeals = {} 1544 | 1545 | PedDealMap = {} 1546 | 1547 | _G.CurrentDealInfo = nil 1548 | CurrentMeetLocation = nil 1549 | end 1550 | 1551 | function AlertPolice() 1552 | local playerCoords = GetEntityCoords(PlayerPedId()) 1553 | 1554 | TriggerServerEvent('police:server:policeAlert', 'Suspicious Phone Activity', playerCoords) 1555 | 1556 | if Config.Framework == 'qb' then 1557 | QBCore.Functions.Notify('Someone might have reported suspicious activity', 'error') 1558 | elseif Config.Framework == 'esx' then 1559 | ESX.ShowNotification('Someone might have reported suspicious activity') 1560 | end 1561 | end 1562 | 1563 | RegisterNetEvent('drug_selling:client:createDealerNPC') 1564 | AddEventHandler('drug_selling:client:createDealerNPC', function(data) 1565 | print("^2Trap Phone: External createDealerNPC event triggered for " .. data.dealId .. "^7") 1566 | end) 1567 | --------------------------------------------------------------------------------