package fortnite import ( "math/rand" "regexp" "github.com/ectrc/snow/aid" "github.com/google/uuid" ) var ( priceLookup = map[string]map[string]int{ "EFortRarity::Legendary": { "AthenaCharacter": 2000, "AthenaBackpack": 1500, "AthenaPickaxe": 1500, "AthenaGlider": 1800, "AthenaDance": 500, "AthenaItemWrap": 800, }, "EFortRarity::Epic": { "AthenaCharacter": 1500, "AthenaBackpack": 1200, "AthenaPickaxe": 1200, "AthenaGlider": 1500, "AthenaDance": 800, "AthenaItemWrap": 800, }, "EFortRarity::Rare": { "AthenaCharacter": 1200, "AthenaBackpack": 800, "AthenaPickaxe": 800, "AthenaGlider": 800, "AthenaDance": 500, "AthenaItemWrap": 600, }, "EFortRarity::Uncommon": { "AthenaCharacter": 800, "AthenaBackpack": 200, "AthenaPickaxe": 500, "AthenaGlider": 500, "AthenaDance": 200, "AthenaItemWrap": 300, }, "EFortRarity::Common": { "AthenaCharacter": 500, "AthenaBackpack": 200, "AthenaPickaxe": 500, "AthenaGlider": 500, "AthenaDance": 200, "AthenaItemWrap": 300, }, } dailyItemLookup = map[int]int{ 2: 4, 4: 6, 13: 10, } weeklySetLookup = map[int]int{ 2: 2, 4: 3, 11: 4, 13: 3, } ) func price(rarity, type_ string) int { return priceLookup[rarity][type_] } func dailyItems(season int) int { var items int for s, i := range dailyItemLookup { if season >= s { items = i } } return items } func weeklySets(season int) int { var sets int for s, i := range weeklySetLookup { if season >= s { sets = i } } return sets } type FortniteCatalogSectionOffer struct { ID string Grants []*FortniteItem TotalPrice int Meta struct { DisplayAssetPath string NewDisplayAssetPath string SectionId string TileSize string Category string ProfileId string } Frontend struct { Title string Description string ShortDescription string } Giftable bool BundleInfo struct { IsBundle bool PricePercent float32 } } func NewFortniteCatalogSectionOffer() *FortniteCatalogSectionOffer { return &FortniteCatalogSectionOffer{} } func (f *FortniteCatalogSectionOffer) GenerateID() { for _, item := range f.Grants { f.ID += item.Type.BackendValue + ":" + item.ID + "," } f.ID = "v2:/" + aid.Hash([]byte(f.ID)) } func (f *FortniteCatalogSectionOffer) GenerateTotalPrice() { if !f.BundleInfo.IsBundle { f.TotalPrice = price(f.Grants[0].Rarity.BackendValue, f.Grants[0].Type.BackendValue) return } for _, item := range f.Grants { f.TotalPrice += price(item.Rarity.BackendValue, item.Rarity.BackendValue) } } func (f *FortniteCatalogSectionOffer) GenerateFortniteCatalogSectionOffer() aid.JSON { f.GenerateTotalPrice() itemGrantResponse := []aid.JSON{} purchaseRequirementsResponse := []aid.JSON{} for _, item := range f.Grants { itemGrantResponse = append(itemGrantResponse, aid.JSON{ "templateId": item.Type.BackendValue + ":" + item.ID, "quantity": 1, }) purchaseRequirementsResponse = append(purchaseRequirementsResponse, aid.JSON{ "requirementType": "DenyOnItemOwnership", "requiredId": item.Type.BackendValue + ":" + item.ID, "minQuantity": 1, }) } return aid.JSON{ "devName": uuid.New().String(), "offerId": f.ID, "offerType": "StaticPrice", "prices": []aid.JSON{{ "currencyType": "MtxCurrency", "currencySubType": "", "regularPrice": f.TotalPrice, "dynamicRegularPrice": f.TotalPrice, "finalPrice": f.TotalPrice, "basePrice": f.TotalPrice, "saleExpiration": "9999-12-31T23:59:59.999Z", }}, "itemGrants": itemGrantResponse, "meta": aid.JSON{ "TileSize": f.Meta.TileSize, "SectionId": f.Meta.SectionId, "NewDisplayAssetPath": f.Meta.NewDisplayAssetPath, "DisplayAssetPath": f.Meta.DisplayAssetPath, }, "metaInfo": []aid.JSON{ { "Key": "TileSize", "Value": f.Meta.TileSize, }, { "Key": "SectionId", "Value": f.Meta.SectionId, }, { "Key": "NewDisplayAssetPath", "Value": f.Meta.NewDisplayAssetPath, }, { "Key": "DisplayAssetPath", "Value": f.Meta.DisplayAssetPath, }, }, "giftInfo": aid.JSON{ "bIsEnabled": f.Giftable, "forcedGiftBoxTemplateId": "", "purchaseRequirements": purchaseRequirementsResponse, "giftRecordIds": []string{}, }, "purchaseRequirements": purchaseRequirementsResponse, "categories": []string{f.Meta.Category}, "title": f.Frontend.Title, "description": f.Frontend.Description, "shortDescription": f.Frontend.ShortDescription, "displayAssetPath": f.Meta.DisplayAssetPath, "appStoreId": []string{}, "fufillmentIds": []string{}, "dailyLimit": -1, "weeklyLimit": -1, "monthlyLimit": -1, "sortPriority": 0, "catalogGroupPriority": 0, "filterWeight": 0, "refundable": true, } } type FortniteCatalogSection struct { Name string Offers []*FortniteCatalogSectionOffer } func NewFortniteCatalogSection(name string) *FortniteCatalogSection { return &FortniteCatalogSection{ Name: name, } } func (f *FortniteCatalogSection) GenerateFortniteCatalogSection() aid.JSON { catalogEntiresResponse := []aid.JSON{} for _, offer := range f.Offers { catalogEntiresResponse = append(catalogEntiresResponse, offer.GenerateFortniteCatalogSectionOffer()) } return aid.JSON{ "name": f.Name, "catalogEntries": catalogEntiresResponse, } } func (f *FortniteCatalogSection) GetGroupedOffers() map[string][]*FortniteCatalogSectionOffer { groupedOffers := map[string][]*FortniteCatalogSectionOffer{} for _, offer := range f.Offers { if groupedOffers[offer.Meta.Category] == nil { groupedOffers[offer.Meta.Category] = []*FortniteCatalogSectionOffer{} } groupedOffers[offer.Meta.Category] = append(groupedOffers[offer.Meta.Category], offer) } return groupedOffers } type FortniteCatalog struct { Sections []*FortniteCatalogSection } func NewFortniteCatalog() *FortniteCatalog { return &FortniteCatalog{} } func (f *FortniteCatalog) GenerateFortniteCatalog() aid.JSON { catalogSectionsResponse := []aid.JSON{} for _, section := range f.Sections { catalogSectionsResponse = append(catalogSectionsResponse, section.GenerateFortniteCatalogSection()) } return aid.JSON{ "storefronts": catalogSectionsResponse, "refreshIntervalHrs": 24, "dailyPurchaseHrs": 24, "expiration": "9999-12-31T23:59:59.999Z", } } func NewRandomFortniteCatalog() *FortniteCatalog { aid.SetRandom(rand.New(rand.NewSource(int64(aid.Config.Fortnite.ShopSeed) + aid.CurrentDayUnix()))) catalog := NewFortniteCatalog() daily := NewFortniteCatalogSection("BRDailyStorefront") for len(daily.Offers) < dailyItems(aid.Config.Fortnite.Season) { entry := newEntryFromFortniteItem(GetRandomItemWithDisplayAssetOfNotType("AthenaCharacter"), false) entry.Meta.SectionId = "Daily" daily.Offers = append(daily.Offers, entry) } catalog.Sections = append(catalog.Sections, daily) weekly := NewFortniteCatalogSection("BRWeeklyStorefront") for len(weekly.GetGroupedOffers()) < weeklySets(aid.Config.Fortnite.Season) { set := GetRandomSet() for _, item := range set.Items { if item.DisplayAssetPath == "" || item.DisplayAssetPath2 == "" { continue } entry := newEntryFromFortniteItem(item, true) entry.Meta.Category = set.BackendName entry.Meta.SectionId = "Featured" weekly.Offers = append(weekly.Offers, entry) } } catalog.Sections = append(catalog.Sections, weekly) return catalog } func newEntryFromFortniteItem(fortniteItem *FortniteItem, addAssets bool) *FortniteCatalogSectionOffer { displayAsset := regexp.MustCompile(`[^/]+$`).FindString(fortniteItem.DisplayAssetPath) entry := NewFortniteCatalogSectionOffer() entry.Meta.TileSize = "Small" if fortniteItem.Type.BackendValue == "AthenaCharacter" { entry.Meta.TileSize = "Normal" } if addAssets { entry.Meta.NewDisplayAssetPath = "/Game/Catalog/NewDisplayAssets/" + fortniteItem.DisplayAssetPath2 + "." + fortniteItem.DisplayAssetPath2 if displayAsset != "" { entry.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/" + displayAsset + "." + displayAsset } } entry.Meta.ProfileId = "athena" entry.Giftable = true entry.Grants = append(entry.Grants, fortniteItem) entry.GenerateTotalPrice() entry.GenerateID() return entry } func GetOfferByOfferId(id string) *FortniteCatalogSectionOffer { catalog := NewRandomFortniteCatalog() for _, section := range catalog.Sections { for _, offer := range section.Offers { if offer.ID == id { return offer } } } return nil }