├── [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 |
17 |
18 |
19 |
20 |
21 |
4:27 PM Friday
22 |
4G
23 |
24 |
25 |
26 |
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 |
92 |
93 |
94 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
155 |
156 |
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 | 
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 |
--------------------------------------------------------------------------------