-
-
Notifications
You must be signed in to change notification settings - Fork 34
/
Copy pathRagfairOfferHelper.ts
884 lines (766 loc) · 34.8 KB
/
RagfairOfferHelper.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
import { BotHelper } from "@spt/helpers/BotHelper";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { PaymentHelper } from "@spt/helpers/PaymentHelper";
import { PresetHelper } from "@spt/helpers/PresetHelper";
import { ProfileHelper } from "@spt/helpers/ProfileHelper";
import { QuestHelper } from "@spt/helpers/QuestHelper";
import { RagfairHelper } from "@spt/helpers/RagfairHelper";
import { RagfairServerHelper } from "@spt/helpers/RagfairServerHelper";
import { RagfairSortHelper } from "@spt/helpers/RagfairSortHelper";
import { TraderHelper } from "@spt/helpers/TraderHelper";
import { IPmcData } from "@spt/models/eft/common/IPmcData";
import { IItem } from "@spt/models/eft/common/tables/IItem";
import { ITraderAssort } from "@spt/models/eft/common/tables/ITrader";
import { IItemEventRouterResponse } from "@spt/models/eft/itemEvent/IItemEventRouterResponse";
import { ISptProfile, ISystemData } from "@spt/models/eft/profile/ISptProfile";
import { IRagfairOffer } from "@spt/models/eft/ragfair/IRagfairOffer";
import { ISearchRequestData, OfferOwnerType } from "@spt/models/eft/ragfair/ISearchRequestData";
import { BaseClasses } from "@spt/models/enums/BaseClasses";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { MemberCategory } from "@spt/models/enums/MemberCategory";
import { MessageType } from "@spt/models/enums/MessageType";
import { RagfairSort } from "@spt/models/enums/RagfairSort";
import { Traders } from "@spt/models/enums/Traders";
import { IBotConfig } from "@spt/models/spt/config/IBotConfig";
import { IQuestConfig } from "@spt/models/spt/config/IQuestConfig";
import { IRagfairConfig, ITieredFlea } from "@spt/models/spt/config/IRagfairConfig";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { EventOutputHolder } from "@spt/routers/EventOutputHolder";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { SaveServer } from "@spt/servers/SaveServer";
import { DatabaseService } from "@spt/services/DatabaseService";
import { LocaleService } from "@spt/services/LocaleService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { MailSendService } from "@spt/services/MailSendService";
import { RagfairOfferService } from "@spt/services/RagfairOfferService";
import { RagfairRequiredItemsService } from "@spt/services/RagfairRequiredItemsService";
import { HashUtil } from "@spt/utils/HashUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
import { inject, injectable } from "tsyringe";
@injectable()
export class RagfairOfferHelper {
protected static goodSoldTemplate = "5bdabfb886f7743e152e867e 0"; // Your {soldItem} {itemCount} items were bought by {buyerNickname}.
protected ragfairConfig: IRagfairConfig;
protected questConfig: IQuestConfig;
protected botConfig: IBotConfig;
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("DatabaseService") protected databaseService: DatabaseService,
@inject("TraderHelper") protected traderHelper: TraderHelper,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("BotHelper") protected botHelper: BotHelper,
@inject("PaymentHelper") protected paymentHelper: PaymentHelper,
@inject("PresetHelper") protected presetHelper: PresetHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("QuestHelper") protected questHelper: QuestHelper,
@inject("RagfairServerHelper") protected ragfairServerHelper: RagfairServerHelper,
@inject("RagfairSortHelper") protected ragfairSortHelper: RagfairSortHelper,
@inject("RagfairHelper") protected ragfairHelper: RagfairHelper,
@inject("RagfairOfferService") protected ragfairOfferService: RagfairOfferService,
@inject("RagfairRequiredItemsService") protected ragfairRequiredItemsService: RagfairRequiredItemsService,
@inject("LocaleService") protected localeService: LocaleService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("MailSendService") protected mailSendService: MailSendService,
@inject("ConfigServer") protected configServer: ConfigServer,
) {
this.ragfairConfig = this.configServer.getConfig(ConfigTypes.RAGFAIR);
this.questConfig = this.configServer.getConfig(ConfigTypes.QUEST);
this.botConfig = this.configServer.getConfig(ConfigTypes.BOT);
}
/**
* Passthrough to ragfairOfferService.getOffers(), get flea offers a player should see
* @param searchRequest Data from client
* @param itemsToAdd ragfairHelper.filterCategories()
* @param traderAssorts Trader assorts
* @param pmcData Player profile
* @returns Offers the player should see
*/
public getValidOffers(
searchRequest: ISearchRequestData,
itemsToAdd: string[],
traderAssorts: Record<string, ITraderAssort>,
pmcData: IPmcData,
): IRagfairOffer[] {
const playerIsFleaBanned = this.profileHelper.playerIsFleaBanned(pmcData);
const tieredFlea = this.ragfairConfig.tieredFlea;
const tieredFleaLimitTypes = Object.keys(tieredFlea.unlocksType);
return this.ragfairOfferService.getOffers().filter((offer) => {
if (!this.passesSearchFilterCriteria(searchRequest, offer, pmcData)) {
return false;
}
const isDisplayable = this.isDisplayableOffer(
searchRequest,
itemsToAdd,
traderAssorts,
offer,
pmcData,
playerIsFleaBanned,
);
if (!isDisplayable) {
return false;
}
// Not trader offer + tiered flea enabled
if (tieredFlea.enabled && !this.offerIsFromTrader(offer)) {
this.checkAndLockOfferFromPlayerTieredFlea(tieredFlea, offer, tieredFleaLimitTypes, pmcData.Info.Level);
}
return true;
});
}
/**
* Disable offer if item is flagged by tiered flea config
* @param tieredFlea Tiered flea settings from ragfair config
* @param offer Ragfair offer to check
* @param tieredFleaLimitTypes Dict of item types with player level to be viewable
* @param playerLevel Level of player viewing offer
*/
protected checkAndLockOfferFromPlayerTieredFlea(
tieredFlea: ITieredFlea,
offer: IRagfairOffer,
tieredFleaLimitTypes: string[],
playerLevel: number,
): void {
const offerItemTpl = offer.items[0]._tpl;
if (tieredFlea.ammoTplUnlocks && this.itemHelper.isOfBaseclass(offerItemTpl, BaseClasses.AMMO)) {
const unlockLevel = tieredFlea.ammoTplUnlocks[offerItemTpl];
if (unlockLevel && playerLevel < unlockLevel) {
offer.locked = true;
return;
}
}
// Check for a direct level requirement for the offer item
const itemLevelRequirement = tieredFlea.unlocksTpl[offerItemTpl];
if (itemLevelRequirement) {
if (playerLevel < itemLevelRequirement) {
offer.locked = true;
return;
}
}
// Optimisation - Ensure the item has at least one of the limited base types
if (this.itemHelper.isOfBaseclasses(offerItemTpl, tieredFleaLimitTypes)) {
// Loop over all flea types to find the matching one
for (const tieredItemType of tieredFleaLimitTypes) {
if (this.itemHelper.isOfBaseclass(offerItemTpl, tieredItemType)) {
if (playerLevel < tieredFlea.unlocksType[tieredItemType]) {
offer.locked = true;
return;
}
break;
}
}
}
}
/**
* Get matching offers that require the desired item and filter out offers from non traders if player is below ragfair unlock level
* @param searchRequest Search request from client
* @param pmcDataPlayer profile
* @returns Matching IRagfairOffer objects
*/
public getOffersThatRequireItem(searchRequest: ISearchRequestData, pmcData: IPmcData): IRagfairOffer[] {
// Get all offers that requre the desired item and filter out offers from non traders if player below ragifar unlock
const requiredOffers = this.ragfairRequiredItemsService.getRequiredItemsById(searchRequest.neededSearchId);
const tieredFlea = this.ragfairConfig.tieredFlea;
const tieredFleaLimitTypes = Object.keys(tieredFlea.unlocksType);
return requiredOffers.filter((offer: IRagfairOffer) => {
if (!this.passesSearchFilterCriteria(searchRequest, offer, pmcData)) {
return false;
}
if (tieredFlea.enabled && !this.offerIsFromTrader(offer)) {
this.checkAndLockOfferFromPlayerTieredFlea(tieredFlea, offer, tieredFleaLimitTypes, pmcData.Info.Level);
}
return true;
});
}
/**
* Get offers from flea/traders specifically when building weapon preset
* @param searchRequest Search request data
* @param itemsToAdd string array of item tpls to search for
* @param traderAssorts All trader assorts player can access/buy
* @param pmcData Player profile
* @returns IRagfairOffer array
*/
public getOffersForBuild(
searchRequest: ISearchRequestData,
itemsToAdd: string[],
traderAssorts: Record<string, ITraderAssort>,
pmcData: IPmcData,
): IRagfairOffer[] {
const offersMap = new Map<string, IRagfairOffer[]>();
const offersToReturn: IRagfairOffer[] = [];
const playerIsFleaBanned = this.profileHelper.playerIsFleaBanned(pmcData);
const tieredFlea = this.ragfairConfig.tieredFlea;
const tieredFleaLimitTypes = Object.keys(tieredFlea.unlocksType);
for (const desiredItemTpl of Object.keys(searchRequest.buildItems)) {
const matchingOffers = this.ragfairOfferService.getOffersOfType(desiredItemTpl);
for (const offer of matchingOffers) {
// Dont show pack offers
if (offer.sellInOnePiece) {
continue;
}
if (!this.passesSearchFilterCriteria(searchRequest, offer, pmcData)) {
continue;
}
if (
!this.isDisplayableOffer(
searchRequest,
itemsToAdd,
traderAssorts,
offer,
pmcData,
playerIsFleaBanned,
)
) {
continue;
}
if (this.offerIsFromTrader(offer)) {
if (this.traderBuyRestrictionReached(offer)) {
continue;
}
if (this.traderOutOfStock(offer)) {
continue;
}
if (this.traderOfferItemQuestLocked(offer, traderAssorts)) {
continue;
}
if (this.traderOfferLockedBehindLoyaltyLevel(offer, pmcData)) {
continue;
}
}
// Tiered flea and not trader offer
if (tieredFlea.enabled && !this.offerIsFromTrader(offer)) {
this.checkAndLockOfferFromPlayerTieredFlea(
tieredFlea,
offer,
tieredFleaLimitTypes,
pmcData.Info.Level,
);
}
const key = offer.items[0]._tpl;
if (!offersMap.has(key)) {
offersMap.set(key, []);
}
offersMap.get(key).push(offer);
}
}
// Get best offer for each item to show on screen
for (let possibleOffers of offersMap.values()) {
// Remove offers with locked = true (quest locked) when > 1 possible offers
// single trader item = shows greyed out
// multiple offers for item = is greyed out
if (possibleOffers.length > 1) {
const lockedOffers = this.getLoyaltyLockedOffers(possibleOffers, pmcData);
// Exclude locked offers + above loyalty locked offers if at least 1 was found
possibleOffers = possibleOffers.filter((offer) => !(offer.locked || lockedOffers.includes(offer._id)));
// Exclude trader offers over their buy restriction limit
possibleOffers = this.getOffersInsideBuyRestrictionLimits(possibleOffers);
}
// Sort offers by price and pick the best
const offer = this.ragfairSortHelper.sortOffers(possibleOffers, RagfairSort.PRICE, 0)[0];
offersToReturn.push(offer);
}
return offersToReturn;
}
/**
* Get offers that have not exceeded buy limits
* @param possibleOffers offers to process
* @returns Offers
*/
protected getOffersInsideBuyRestrictionLimits(possibleOffers: IRagfairOffer[]) {
// Check offer has buy limit + is from trader + current buy count is at or over max
return possibleOffers.filter((offer) => {
if (
typeof offer.buyRestrictionMax !== "undefined" &&
this.offerIsFromTrader(offer) &&
offer.buyRestrictionCurrent >= offer.buyRestrictionMax
) {
if (offer.buyRestrictionCurrent >= offer.buyRestrictionMax) {
return false;
}
}
// Doesnt have buy limits, retrun offer
return true;
});
}
/**
* Check if offer is from trader standing the player does not have
* @param offer Offer to check
* @param pmcProfile Player profile
* @returns True if item is locked, false if item is purchaseable
*/
protected traderOfferLockedBehindLoyaltyLevel(offer: IRagfairOffer, pmcProfile: IPmcData): boolean {
const userTraderSettings = pmcProfile.TradersInfo[offer.user.id];
return userTraderSettings.loyaltyLevel < offer.loyaltyLevel;
}
/**
* Check if offer item is quest locked for current player by looking at sptQuestLocked property in traders barter_scheme
* @param offer Offer to check is quest locked
* @param traderAssorts all trader assorts for player
* @returns true if quest locked
*/
public traderOfferItemQuestLocked(offer: IRagfairOffer, traderAssorts: Record<string, ITraderAssort>): boolean {
return offer.items?.some((i) =>
traderAssorts[offer.user.id].barter_scheme[i._id]?.some((bs1) => bs1?.some((bs2) => bs2.sptQuestLocked)),
);
}
/**
* Has trader offer ran out of stock to sell to player
* @param offer Offer to check stock of
* @returns true if out of stock
*/
protected traderOutOfStock(offer: IRagfairOffer): boolean {
if (offer?.items?.length === 0) {
return true;
}
return offer.items[0]?.upd?.StackObjectsCount === 0;
}
/**
* Check if trader offers' BuyRestrictionMax value has been reached
* @param offer Offer to check restriction properties of
* @returns true if restriction reached, false if no restrictions/not reached
*/
protected traderBuyRestrictionReached(offer: IRagfairOffer): boolean {
const traderAssorts = this.traderHelper.getTraderAssortsByTraderId(offer.user.id).items;
// Find item being purchased from traders assorts
const assortData = traderAssorts.find((item) => item._id === offer.items[0]._id);
// No trader assort data
if (!assortData) {
this.logger.warning(
`Unable to find trader: ${offer.user.nickname} assort for item: ${this.itemHelper.getItemName(
offer.items[0]._tpl,
)} ${offer.items[0]._tpl}, cannot check if buy restriction reached`,
);
return false;
}
if (!assortData.upd) {
return false;
}
// No restriction values
// Can't use !assortData.upd.BuyRestrictionX as value could be 0
const assortUpd = assortData.upd;
if (assortUpd.BuyRestrictionMax === undefined || assortUpd.BuyRestrictionCurrent === undefined) {
return false;
}
// Current equals max, limit reached
if (assortUpd.BuyRestrictionCurrent >= assortUpd.BuyRestrictionMax) {
return true;
}
return false;
}
/**
* Get an array of flea offers that are inaccessible to player due to their inadequate loyalty level
* @param offers Offers to check
* @param pmcProfile Players profile with trader loyalty levels
* @returns Array of offer ids player cannot see
*/
protected getLoyaltyLockedOffers(offers: IRagfairOffer[], pmcProfile: IPmcData): string[] {
const loyaltyLockedOffers: string[] = [];
for (const offer of offers.filter((offer) => this.offerIsFromTrader(offer))) {
const traderDetails = pmcProfile.TradersInfo[offer.user.id];
if (traderDetails.loyaltyLevel < offer.loyaltyLevel) {
loyaltyLockedOffers.push(offer._id);
}
}
return loyaltyLockedOffers;
}
/**
* Process all player-listed flea offers for a desired profile
* @param sessionID Session id to process offers for
* @returns true = complete
*/
public processOffersOnProfile(sessionID: string): boolean {
const timestamp = this.timeUtil.getTimestamp();
const profileOffers = this.getProfileOffers(sessionID);
// No offers, don't do anything
if (!profileOffers?.length) {
return true;
}
for (const offer of profileOffers.values()) {
if (offer.sellResult?.length > 0 && timestamp >= offer.sellResult[0].sellTime) {
// Checks first item, first is spliced out of array after being processed
// Item sold
let totalItemsCount = 1;
let boughtAmount = 1;
if (!offer.sellInOnePiece) {
// offer.items.reduce((sum, item) => sum + item.upd?.StackObjectsCount ?? 0, 0);
totalItemsCount = this.getTotalStackCountSize([offer.items]);
boughtAmount = offer.sellResult[0].amount;
}
const ratingToAdd = (offer.summaryCost / totalItemsCount) * boughtAmount;
this.increaseProfileRagfairRating(this.saveServer.getProfile(sessionID), ratingToAdd);
this.completeOffer(sessionID, offer, boughtAmount);
offer.sellResult.splice(0, 1); // Remove the sell result object now its been processed
}
}
return true;
}
/**
* Count up all rootitem StackObjectsCount properties of an array of items
* @param itemsInInventoryToList items to sum up
* @returns Total stack count
*/
public getTotalStackCountSize(itemsInInventoryToList: IItem[][]): number {
let total = 0;
for (const itemAndChildren of itemsInInventoryToList) {
// Only count the root items stack count in total
const rootItem = itemAndChildren[0];
total += rootItem.upd?.StackObjectsCount ?? 1;
}
return total;
}
/**
* Add amount to players ragfair rating
* @param sessionId Profile to update
* @param amountToIncrementBy Raw amount to add to players ragfair rating (excluding the reputation gain multiplier)
*/
public increaseProfileRagfairRating(profile: ISptProfile, amountToIncrementBy: number): void {
const ragfairGlobalsConfig = this.databaseService.getGlobals().config.RagFair;
profile.characters.pmc.RagfairInfo.isRatingGrowing = true;
if (Number.isNaN(amountToIncrementBy)) {
this.logger.warning(`Unable to increment ragfair rating, value was not a number: ${amountToIncrementBy}`);
return;
}
profile.characters.pmc.RagfairInfo.rating +=
(ragfairGlobalsConfig.ratingIncreaseCount / ragfairGlobalsConfig.ratingSumForIncrease) *
amountToIncrementBy;
}
/**
* Return all offers a player has listed on a desired profile
* @param sessionID Session id
* @returns Array of ragfair offers
*/
protected getProfileOffers(sessionID: string): IRagfairOffer[] {
const profile = this.profileHelper.getPmcProfile(sessionID);
if (profile.RagfairInfo === undefined || profile.RagfairInfo.offers === undefined) {
return [];
}
return profile.RagfairInfo.offers;
}
/**
* Delete an offer from a desired profile and from ragfair offers
* @param sessionID Session id of profile to delete offer from
* @param offerId Id of offer to delete
*/
protected deleteOfferById(sessionID: string, offerId: string): void {
const profileRagfairInfo = this.saveServer.getProfile(sessionID).characters.pmc.RagfairInfo;
const index = profileRagfairInfo.offers.findIndex((o) => o._id === offerId);
profileRagfairInfo.offers.splice(index, 1);
// Also delete from ragfair
this.ragfairOfferService.removeOfferById(offerId);
}
/**
* Complete the selling of players' offer
* @param sessionID Session id
* @param offer Sold offer details
* @param boughtAmount Amount item was purchased for
* @returns IItemEventRouterResponse
*/
public completeOffer(sessionID: string, offer: IRagfairOffer, boughtAmount: number): IItemEventRouterResponse {
const itemTpl = offer.items[0]._tpl;
let paymentItemsToSendToPlayer: IItem[] = [];
const offerStackCount = offer.items[0].upd.StackObjectsCount;
// Pack or ALL items of a multi-offer were bought - remove entire ofer
if (offer.sellInOnePiece || boughtAmount === offerStackCount) {
this.deleteOfferById(sessionID, offer._id);
} else {
const offerRootItem = offer.items[0];
// Reduce offer root items stack count
offerRootItem.upd.StackObjectsCount -= boughtAmount;
}
// Assemble payment to send to seller now offer was purchased
for (const requirement of offer.requirements) {
// Create an item template item
const requestedItem: IItem = {
_id: this.hashUtil.generate(),
_tpl: requirement._tpl,
upd: { StackObjectsCount: requirement.count * boughtAmount },
};
const stacks = this.itemHelper.splitStack(requestedItem);
for (const item of stacks) {
const outItems = [item];
// TODO - is this code used?, may have been when adding barters to flea was still possible for player
if (requirement.onlyFunctional) {
const presetItems = this.ragfairServerHelper.getPresetItemsByTpl(item);
if (presetItems.length) {
outItems.push(presetItems[0]);
}
}
paymentItemsToSendToPlayer = [...paymentItemsToSendToPlayer, ...outItems];
}
}
const ragfairDetails = {
offerId: offer._id,
count: offer.sellInOnePiece ? offerStackCount : boughtAmount, // pack-offers NEED to the full item count otherwise it only removes 1 from the pack, leaving phantom offer on client ui
handbookId: itemTpl,
};
this.mailSendService.sendDirectNpcMessageToPlayer(
sessionID,
this.traderHelper.getTraderById(Traders.RAGMAN),
MessageType.FLEAMARKET_MESSAGE,
this.getLocalisedOfferSoldMessage(itemTpl, boughtAmount),
paymentItemsToSendToPlayer,
this.timeUtil.getHoursAsSeconds(
this.questHelper.getMailItemRedeemTimeHoursForProfile(this.profileHelper.getPmcProfile(sessionID)),
),
undefined,
ragfairDetails,
);
return this.eventOutputHolder.getOutput(sessionID);
}
/**
* Get a localised message for when players offer has sold on flea
* @param itemTpl Item sold
* @param boughtAmount How many were purchased
* @returns Localised message text
*/
protected getLocalisedOfferSoldMessage(itemTpl: string, boughtAmount: number): string {
// Generate a message to inform that item was sold
const globalLocales = this.localeService.getLocaleDb();
const soldMessageLocaleGuid = globalLocales[RagfairOfferHelper.goodSoldTemplate];
if (!soldMessageLocaleGuid) {
this.logger.error(
this.localisationService.getText(
"ragfair-unable_to_find_locale_by_key",
RagfairOfferHelper.goodSoldTemplate,
),
);
}
// Used to replace tokens in sold message sent to player
const tplVars: ISystemData = {
soldItem: globalLocales[`${itemTpl} Name`] || itemTpl,
buyerNickname: this.botHelper.getPmcNicknameOfMaxLength(this.botConfig.botNameLengthLimit),
itemCount: boughtAmount,
};
const offerSoldMessageText = soldMessageLocaleGuid.replace(/{\w+}/g, (matched) => {
return tplVars[matched.replace(/{|}/g, "")];
});
return offerSoldMessageText.replace(/"/g, "");
}
/**
* Check an offer passes the various search criteria the player requested
* @param searchRequest Client search request
* @param offer Offer to check
* @param pmcData Player profile
* @returns True if offer passes criteria
*/
protected passesSearchFilterCriteria(
searchRequest: ISearchRequestData,
offer: IRagfairOffer,
pmcData: IPmcData,
): boolean {
const isDefaultUserOffer = offer.user.memberType === MemberCategory.DEFAULT;
const offerRootItem = offer.items[0];
const moneyTypeTpl = offer.requirements[0]._tpl;
const isTraderOffer = this.offerIsFromTrader(offer);
if (pmcData.Info.Level < this.databaseService.getGlobals().config.RagFair.minUserLevel && isDefaultUserOffer) {
// Skip item if player is < global unlock level (default is 15) and item is from a dynamically generated source
return false;
}
if (searchRequest.offerOwnerType === OfferOwnerType.TRADEROWNERTYPE && !isTraderOffer) {
// don't include player offers
return false;
}
if (searchRequest.offerOwnerType === OfferOwnerType.PLAYEROWNERTYPE && isTraderOffer) {
// don't include trader offers
return false;
}
if (
searchRequest.oneHourExpiration &&
offer.endTime - this.timeUtil.getTimestamp() > TimeUtil.ONE_HOUR_AS_SECONDS
) {
// offer expires within an hour
return false;
}
if (searchRequest.quantityFrom > 0 && searchRequest.quantityFrom >= offerRootItem.upd.StackObjectsCount) {
// too little items to offer
return false;
}
if (searchRequest.quantityTo > 0 && searchRequest.quantityTo <= offerRootItem.upd.StackObjectsCount) {
// too many items to offer
return false;
}
if (searchRequest.onlyFunctional && !this.isItemFunctional(offerRootItem, offer)) {
// don't include non-functional items
return false;
}
if (offer.items.length === 1) {
// Single item
if (
this.isConditionItem(offerRootItem) &&
!this.itemQualityInRange(offerRootItem, searchRequest.conditionFrom, searchRequest.conditionTo)
) {
return false;
}
} else {
const itemQualityPercent = this.itemHelper.getItemQualityModifierForItems(offer.items) * 100;
if (itemQualityPercent < searchRequest.conditionFrom) {
return false;
}
if (itemQualityPercent > searchRequest.conditionTo) {
return false;
}
}
if (searchRequest.currency > 0 && this.paymentHelper.isMoneyTpl(moneyTypeTpl)) {
const currencies = ["all", "RUB", "USD", "EUR"];
if (this.ragfairHelper.getCurrencyTag(moneyTypeTpl) !== currencies[searchRequest.currency]) {
// don't include item paid in wrong currency
return false;
}
}
if (searchRequest.priceFrom > 0 && searchRequest.priceFrom >= offer.requirementsCost) {
// price is too low
return false;
}
if (searchRequest.priceTo > 0 && searchRequest.priceTo <= offer.requirementsCost) {
// price is too high
return false;
}
// Passes above checks, search criteria filters have not filtered offer out
return true;
}
/**
* Check that the passed in offer item is functional
* @param offerRootItem The root item of the offer
* @param offer Flea offer to check
* @returns True if the given item is functional
*/
public isItemFunctional(offerRootItem: IItem, offer: IRagfairOffer): boolean {
// Non-preset weapons/armor are always functional
if (!this.presetHelper.hasPreset(offerRootItem._tpl)) {
return true;
}
// For armor items that can hold mods, make sure the item count is atleast the amount of required plates
if (this.itemHelper.armorItemCanHoldMods(offerRootItem._tpl)) {
const offerRootTemplate = this.itemHelper.getItem(offerRootItem._tpl)[1];
const requiredPlateCount = offerRootTemplate._props.Slots?.filter((item) => item._required)?.length;
return offer.items.length > requiredPlateCount;
}
// For other presets, make sure the offer has more than 1 item
return offer.items.length > 1;
}
/**
* Should a ragfair offer be visible to the player
* @param searchRequest Search request
* @param itemsToAdd ?
* @param traderAssorts Trader assort items - used for filtering out locked trader items
* @param offer The flea offer
* @param pmcProfile Player profile
* @returns True = should be shown to player
*/
public isDisplayableOffer(
searchRequest: ISearchRequestData,
itemsToAdd: string[],
traderAssorts: Record<string, ITraderAssort>,
offer: IRagfairOffer,
pmcProfile: IPmcData,
playerIsFleaBanned?: boolean,
): boolean {
const offerRootItem = offer.items[0];
/** Currency offer is sold for */
const moneyTypeTpl = offer.requirements[0]._tpl;
const isTraderOffer = offer.user.id in this.databaseService.getTraders();
if (!isTraderOffer && playerIsFleaBanned) {
return false;
}
// Offer root items tpl not in searched for array
if (!itemsToAdd?.includes(offerRootItem._tpl)) {
// skip items we shouldn't include
return false;
}
// Performing a required search and offer doesn't have requirement for item
if (
searchRequest.neededSearchId &&
!offer.requirements.some((requirement) => requirement._tpl === searchRequest.neededSearchId)
) {
return false;
}
// Weapon/equipment search + offer is preset
if (
Object.keys(searchRequest.buildItems).length === 0 && // Prevent equipment loadout searches filtering out presets
searchRequest.buildCount &&
this.presetHelper.hasPreset(offerRootItem._tpl)
) {
return false;
}
// commented out as required search "which is for checking offers that are barters"
// has info.removeBartering as true, this if statement removed barter items.
if (searchRequest.removeBartering && !this.paymentHelper.isMoneyTpl(moneyTypeTpl)) {
// Don't include barter offers
return false;
}
if (Number.isNaN(offer.requirementsCost)) {
// Don't include offers with undefined or NaN in it
return false;
}
// Handle trader items to remove items that are not available to the user right now
// e.g. required search for "lamp" shows 4 items, 3 of which are not available to a new player
// filter those out
if (isTraderOffer) {
if (!(offer.user.id in traderAssorts)) {
// trader not visible on flea market
return false;
}
if (
!traderAssorts[offer.user.id].items.some((item) => {
return item._id === offer.root;
})
) {
// skip (quest) locked items
return false;
}
}
return true;
}
public isDisplayableOfferThatNeedsItem(searchRequest: ISearchRequestData, offer: IRagfairOffer): boolean {
if (offer.requirements.some((requirement) => requirement._tpl === searchRequest.neededSearchId)) {
return true;
}
return false;
}
/**
* Does the passed in item have a condition property
* @param item Item to check
* @returns True if has condition
*/
protected isConditionItem(item: IItem): boolean {
// thanks typescript, undefined assertion is not returnable since it
// tries to return a multitype object
return !!(
item.upd.MedKit ||
item.upd.Repairable ||
item.upd.Resource ||
item.upd.FoodDrink ||
item.upd.Key ||
item.upd.RepairKit
);
}
/**
* Is items quality value within desired range
* @param item Item to check quality of
* @param min Desired minimum quality
* @param max Desired maximum quality
* @returns True if in range
*/
protected itemQualityInRange(item: IItem, min: number, max: number): boolean {
const itemQualityPercentage = 100 * this.itemHelper.getItemQualityModifier(item);
if (min > 0 && min > itemQualityPercentage) {
// Item condition too low
return false;
}
if (max < 100 && max <= itemQualityPercentage) {
// Item condition too high
return false;
}
return true;
}
/**
* Does this offer come from a trader
* @param offer Offer to check
* @returns True = from trader
*/
public offerIsFromTrader(offer: IRagfairOffer) {
return offer.user.memberType === MemberCategory.TRADER;
}
}