diff --git a/README.md b/README.md index 0cdb693d..917880b4 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Unless explicitly stated otherwise, the repository license applies to the source - Agent collections - Graffiti boxes - Patch packs and patch collections +- Charm collections - Item glossary hub with dedicated screens for skins and collectibles - Trade-Up simulator @@ -40,13 +41,13 @@ Unless explicitly stated otherwise, the repository license applies to the source - Flutter - Dart - Local JSON assets for all generated content -- Dart-based importer for containers, skins, stickers, pins, music kits, agents, graffiti, patches, and collection metadata +- Dart-based importer for containers, skins, stickers, pins, music kits, agents, graffiti, patches, charms, and unified source metadata ## Project Structure - [lib/](lib) application code - [assets/data/](assets/data) generated JSON data -- [assets/cases/](assets/cases) container images +- [assets/containers/](assets/containers) images for containers and collection-type sources - [assets/skins/](assets/skins) skin images - [assets/stickers/](assets/stickers) sticker images - [assets/pins/](assets/pins) pin images @@ -54,6 +55,7 @@ Unless explicitly stated otherwise, the repository license applies to the source - [assets/agents/](assets/agents) agent images - [assets/graffiti/](assets/graffiti) graffiti images - [assets/patches/](assets/patches) patch images +- [assets/charms/](assets/charms) charm images - [tool/import_cs_data.dart](tool/import_cs_data.dart) main importer entrypoint - [tool/prune_generated_assets.dart](tool/prune_generated_assets.dart) cleanup tool for orphaned generated assets @@ -103,6 +105,9 @@ dart run tool/import_cs_data.dart --compression=max-compress - `fast` is the default mode and is intended for normal development work - `max-compress` is intended for rare clean release rebuilds +The main generated source registry is `assets/data/containers.json`. +It includes regular containers as well as collection-type sources such as reward collections, legacy operation collections, agent collections, sticker collections, patch collections, and charm collections. + After a large migration or a clean import, you can remove orphaned generated assets: ```bash @@ -113,6 +118,7 @@ dart run tool/prune_generated_assets.dart - Existing generated assets are not overwritten during normal imports - The importer stores the actual generated extension, including `.webp` where applicable +- Collection-type source images are stored alongside regular container images in `assets/containers/` - Container dates are resolved locally instead of trusting API sale dates - Supported container types fail the import if a hardcoded release date is missing - Generated assets can be rebuilt in `fast` or `max-compress` mode depending on whether you are doing normal development or a release rebuild @@ -143,17 +149,25 @@ The project is actively evolving, with current work focused on: ## Roadmap -### v0.10 +### v0.11 + +- Major tournament section covering CS:GO and CS2 eras +- Tournament pages with dates, organizers, winners, and placements +- Better linking between Majors and their souvenir packages, sticker capsules, and autograph capsules + +### v0.12 + +- Skin pattern and finish seed support, including knife phases, gem variants, fade-style finishes, and other pattern-driven outcomes + +### v0.13 -- Trade-Up rewrite and UI cleanup -- Charm support -- Unified handling of regular and StatTrakā„¢ Music Kits as one grouped item -- Broader simulator accuracy pass across more container types -- Better browsing and glossary coverage for non-skin content +- Inventory or item ownership tracking in some form +- Opening and Trade-Up history with per-container and per-item stats +- Better collection browsing around owned items, seen drops, and completion progress ### Future -- Skin pattern and finish seed support for items where patterns matter - Cleaner navigation across containers, collections, and collectibles +- Better automated test coverage beyond basic smoke checks - Music Kit preview playback if a reliable audio source is available - Optional China / Perfect World visual mode if a reliable alternate asset source is available diff --git a/assets/app_icon/latest_case.png b/assets/app_icon/latest_container.png similarity index 100% rename from assets/app_icon/latest_case.png rename to assets/app_icon/latest_container.png diff --git a/assets/app_icon/latest_case_foreground.png b/assets/app_icon/latest_container_foreground.png similarity index 100% rename from assets/app_icon/latest_case_foreground.png rename to assets/app_icon/latest_container_foreground.png diff --git a/assets/app_icon/latest_case_ios_dark.png b/assets/app_icon/latest_container_ios_dark.png similarity index 100% rename from assets/app_icon/latest_case_ios_dark.png rename to assets/app_icon/latest_container_ios_dark.png diff --git a/assets/app_icon/latest_case_ios_tinted.png b/assets/app_icon/latest_container_ios_tinted.png similarity index 100% rename from assets/app_icon/latest_case_ios_tinted.png rename to assets/app_icon/latest_container_ios_tinted.png diff --git a/assets/app_icon/latest_case_monochrome.png b/assets/app_icon/latest_container_monochrome.png similarity index 100% rename from assets/app_icon/latest_case_monochrome.png rename to assets/app_icon/latest_container_monochrome.png diff --git a/assets/charms/1.webp b/assets/charms/1.webp new file mode 100644 index 00000000..a45965f7 Binary files /dev/null and b/assets/charms/1.webp differ diff --git a/assets/charms/10.webp b/assets/charms/10.webp new file mode 100644 index 00000000..dd3feff2 Binary files /dev/null and b/assets/charms/10.webp differ diff --git a/assets/charms/11.webp b/assets/charms/11.webp new file mode 100644 index 00000000..2bd7f9e0 Binary files /dev/null and b/assets/charms/11.webp differ diff --git a/assets/charms/12.webp b/assets/charms/12.webp new file mode 100644 index 00000000..e05c4d75 Binary files /dev/null and b/assets/charms/12.webp differ diff --git a/assets/charms/13.webp b/assets/charms/13.webp new file mode 100644 index 00000000..7f7a5f62 Binary files /dev/null and b/assets/charms/13.webp differ diff --git a/assets/charms/14.webp b/assets/charms/14.webp new file mode 100644 index 00000000..67636158 Binary files /dev/null and b/assets/charms/14.webp differ diff --git a/assets/charms/15.webp b/assets/charms/15.webp new file mode 100644 index 00000000..13c3307f Binary files /dev/null and b/assets/charms/15.webp differ diff --git a/assets/charms/16.webp b/assets/charms/16.webp new file mode 100644 index 00000000..eaa08c1c Binary files /dev/null and b/assets/charms/16.webp differ diff --git a/assets/charms/17.webp b/assets/charms/17.webp new file mode 100644 index 00000000..e24d9583 Binary files /dev/null and b/assets/charms/17.webp differ diff --git a/assets/charms/18.webp b/assets/charms/18.webp new file mode 100644 index 00000000..6c1db54c Binary files /dev/null and b/assets/charms/18.webp differ diff --git a/assets/charms/19.webp b/assets/charms/19.webp new file mode 100644 index 00000000..e1b3a810 Binary files /dev/null and b/assets/charms/19.webp differ diff --git a/assets/charms/2.webp b/assets/charms/2.webp new file mode 100644 index 00000000..1f8902b2 Binary files /dev/null and b/assets/charms/2.webp differ diff --git a/assets/charms/20.webp b/assets/charms/20.webp new file mode 100644 index 00000000..468d7f9a Binary files /dev/null and b/assets/charms/20.webp differ diff --git a/assets/charms/21.webp b/assets/charms/21.webp new file mode 100644 index 00000000..def653bd Binary files /dev/null and b/assets/charms/21.webp differ diff --git a/assets/charms/22.webp b/assets/charms/22.webp new file mode 100644 index 00000000..c352c6ee Binary files /dev/null and b/assets/charms/22.webp differ diff --git a/assets/charms/23.webp b/assets/charms/23.webp new file mode 100644 index 00000000..dc70d5be Binary files /dev/null and b/assets/charms/23.webp differ diff --git a/assets/charms/24.webp b/assets/charms/24.webp new file mode 100644 index 00000000..d7a2fb0b Binary files /dev/null and b/assets/charms/24.webp differ diff --git a/assets/charms/25.webp b/assets/charms/25.webp new file mode 100644 index 00000000..cc89b369 Binary files /dev/null and b/assets/charms/25.webp differ diff --git a/assets/charms/26.webp b/assets/charms/26.webp new file mode 100644 index 00000000..09a04da6 Binary files /dev/null and b/assets/charms/26.webp differ diff --git a/assets/charms/27.webp b/assets/charms/27.webp new file mode 100644 index 00000000..e2c636f3 Binary files /dev/null and b/assets/charms/27.webp differ diff --git a/assets/charms/28.webp b/assets/charms/28.webp new file mode 100644 index 00000000..026f12cb Binary files /dev/null and b/assets/charms/28.webp differ diff --git a/assets/charms/29.webp b/assets/charms/29.webp new file mode 100644 index 00000000..b0a798ef Binary files /dev/null and b/assets/charms/29.webp differ diff --git a/assets/charms/3.webp b/assets/charms/3.webp new file mode 100644 index 00000000..27e3d29a Binary files /dev/null and b/assets/charms/3.webp differ diff --git a/assets/charms/30.webp b/assets/charms/30.webp new file mode 100644 index 00000000..78553ca3 Binary files /dev/null and b/assets/charms/30.webp differ diff --git a/assets/charms/31.webp b/assets/charms/31.webp new file mode 100644 index 00000000..d14d672f Binary files /dev/null and b/assets/charms/31.webp differ diff --git a/assets/charms/32.webp b/assets/charms/32.webp new file mode 100644 index 00000000..e807f019 Binary files /dev/null and b/assets/charms/32.webp differ diff --git a/assets/charms/33.webp b/assets/charms/33.webp new file mode 100644 index 00000000..63b72da0 Binary files /dev/null and b/assets/charms/33.webp differ diff --git a/assets/charms/38.webp b/assets/charms/38.webp new file mode 100644 index 00000000..53d6558b Binary files /dev/null and b/assets/charms/38.webp differ diff --git a/assets/charms/39.webp b/assets/charms/39.webp new file mode 100644 index 00000000..1cc16e88 Binary files /dev/null and b/assets/charms/39.webp differ diff --git a/assets/charms/4.webp b/assets/charms/4.webp new file mode 100644 index 00000000..33b9da4c Binary files /dev/null and b/assets/charms/4.webp differ diff --git a/assets/charms/40.webp b/assets/charms/40.webp new file mode 100644 index 00000000..a6c3c0c1 Binary files /dev/null and b/assets/charms/40.webp differ diff --git a/assets/charms/41.webp b/assets/charms/41.webp new file mode 100644 index 00000000..28a35802 Binary files /dev/null and b/assets/charms/41.webp differ diff --git a/assets/charms/42.webp b/assets/charms/42.webp new file mode 100644 index 00000000..67f44923 Binary files /dev/null and b/assets/charms/42.webp differ diff --git a/assets/charms/43.webp b/assets/charms/43.webp new file mode 100644 index 00000000..95ee9268 Binary files /dev/null and b/assets/charms/43.webp differ diff --git a/assets/charms/44.webp b/assets/charms/44.webp new file mode 100644 index 00000000..1fa460fc Binary files /dev/null and b/assets/charms/44.webp differ diff --git a/assets/charms/45.webp b/assets/charms/45.webp new file mode 100644 index 00000000..7b4ad998 Binary files /dev/null and b/assets/charms/45.webp differ diff --git a/assets/charms/46.webp b/assets/charms/46.webp new file mode 100644 index 00000000..86b883ee Binary files /dev/null and b/assets/charms/46.webp differ diff --git a/assets/charms/47.webp b/assets/charms/47.webp new file mode 100644 index 00000000..2f3ab1b9 Binary files /dev/null and b/assets/charms/47.webp differ diff --git a/assets/charms/48.webp b/assets/charms/48.webp new file mode 100644 index 00000000..faf79df3 Binary files /dev/null and b/assets/charms/48.webp differ diff --git a/assets/charms/49.webp b/assets/charms/49.webp new file mode 100644 index 00000000..865d4029 Binary files /dev/null and b/assets/charms/49.webp differ diff --git a/assets/charms/5.webp b/assets/charms/5.webp new file mode 100644 index 00000000..4452ada8 Binary files /dev/null and b/assets/charms/5.webp differ diff --git a/assets/charms/50.webp b/assets/charms/50.webp new file mode 100644 index 00000000..99b9a04a Binary files /dev/null and b/assets/charms/50.webp differ diff --git a/assets/charms/51.webp b/assets/charms/51.webp new file mode 100644 index 00000000..4439aeba Binary files /dev/null and b/assets/charms/51.webp differ diff --git a/assets/charms/52.webp b/assets/charms/52.webp new file mode 100644 index 00000000..fc802fef Binary files /dev/null and b/assets/charms/52.webp differ diff --git a/assets/charms/53.webp b/assets/charms/53.webp new file mode 100644 index 00000000..c26d4a16 Binary files /dev/null and b/assets/charms/53.webp differ diff --git a/assets/charms/54.webp b/assets/charms/54.webp new file mode 100644 index 00000000..e3ca4254 Binary files /dev/null and b/assets/charms/54.webp differ diff --git a/assets/charms/55.webp b/assets/charms/55.webp new file mode 100644 index 00000000..6ac23e59 Binary files /dev/null and b/assets/charms/55.webp differ diff --git a/assets/charms/56.webp b/assets/charms/56.webp new file mode 100644 index 00000000..45d57fdb Binary files /dev/null and b/assets/charms/56.webp differ diff --git a/assets/charms/57.webp b/assets/charms/57.webp new file mode 100644 index 00000000..403f39cd Binary files /dev/null and b/assets/charms/57.webp differ diff --git a/assets/charms/58.webp b/assets/charms/58.webp new file mode 100644 index 00000000..72a673cb Binary files /dev/null and b/assets/charms/58.webp differ diff --git a/assets/charms/59.webp b/assets/charms/59.webp new file mode 100644 index 00000000..5e3bbdb1 Binary files /dev/null and b/assets/charms/59.webp differ diff --git a/assets/charms/6.webp b/assets/charms/6.webp new file mode 100644 index 00000000..4c091205 Binary files /dev/null and b/assets/charms/6.webp differ diff --git a/assets/charms/60.webp b/assets/charms/60.webp new file mode 100644 index 00000000..346c85a8 Binary files /dev/null and b/assets/charms/60.webp differ diff --git a/assets/charms/61.webp b/assets/charms/61.webp new file mode 100644 index 00000000..de96ffab Binary files /dev/null and b/assets/charms/61.webp differ diff --git a/assets/charms/62.webp b/assets/charms/62.webp new file mode 100644 index 00000000..b7caf6f2 Binary files /dev/null and b/assets/charms/62.webp differ diff --git a/assets/charms/63.webp b/assets/charms/63.webp new file mode 100644 index 00000000..34ae4d9d Binary files /dev/null and b/assets/charms/63.webp differ diff --git a/assets/charms/64.webp b/assets/charms/64.webp new file mode 100644 index 00000000..7fd93f0c Binary files /dev/null and b/assets/charms/64.webp differ diff --git a/assets/charms/65.webp b/assets/charms/65.webp new file mode 100644 index 00000000..3d314e62 Binary files /dev/null and b/assets/charms/65.webp differ diff --git a/assets/charms/66.webp b/assets/charms/66.webp new file mode 100644 index 00000000..e4690b7d Binary files /dev/null and b/assets/charms/66.webp differ diff --git a/assets/charms/67.webp b/assets/charms/67.webp new file mode 100644 index 00000000..83c30b85 Binary files /dev/null and b/assets/charms/67.webp differ diff --git a/assets/charms/68.webp b/assets/charms/68.webp new file mode 100644 index 00000000..22f6291c Binary files /dev/null and b/assets/charms/68.webp differ diff --git a/assets/charms/69.webp b/assets/charms/69.webp new file mode 100644 index 00000000..742751a4 Binary files /dev/null and b/assets/charms/69.webp differ diff --git a/assets/charms/7.webp b/assets/charms/7.webp new file mode 100644 index 00000000..c58621b3 Binary files /dev/null and b/assets/charms/7.webp differ diff --git a/assets/charms/70.webp b/assets/charms/70.webp new file mode 100644 index 00000000..d76ae71d Binary files /dev/null and b/assets/charms/70.webp differ diff --git a/assets/charms/71.webp b/assets/charms/71.webp new file mode 100644 index 00000000..7fa03d5f Binary files /dev/null and b/assets/charms/71.webp differ diff --git a/assets/charms/72.webp b/assets/charms/72.webp new file mode 100644 index 00000000..9c89b64b Binary files /dev/null and b/assets/charms/72.webp differ diff --git a/assets/charms/73.webp b/assets/charms/73.webp new file mode 100644 index 00000000..e7d67e6c Binary files /dev/null and b/assets/charms/73.webp differ diff --git a/assets/charms/74.webp b/assets/charms/74.webp new file mode 100644 index 00000000..3edbee88 Binary files /dev/null and b/assets/charms/74.webp differ diff --git a/assets/charms/75.webp b/assets/charms/75.webp new file mode 100644 index 00000000..0135e84d Binary files /dev/null and b/assets/charms/75.webp differ diff --git a/assets/charms/76.webp b/assets/charms/76.webp new file mode 100644 index 00000000..8616b73d Binary files /dev/null and b/assets/charms/76.webp differ diff --git a/assets/charms/77.webp b/assets/charms/77.webp new file mode 100644 index 00000000..355c58e8 Binary files /dev/null and b/assets/charms/77.webp differ diff --git a/assets/charms/78.webp b/assets/charms/78.webp new file mode 100644 index 00000000..0a416d2f Binary files /dev/null and b/assets/charms/78.webp differ diff --git a/assets/charms/79.webp b/assets/charms/79.webp new file mode 100644 index 00000000..eada7f68 Binary files /dev/null and b/assets/charms/79.webp differ diff --git a/assets/charms/8.webp b/assets/charms/8.webp new file mode 100644 index 00000000..a120493d Binary files /dev/null and b/assets/charms/8.webp differ diff --git a/assets/charms/80.webp b/assets/charms/80.webp new file mode 100644 index 00000000..ab02399e Binary files /dev/null and b/assets/charms/80.webp differ diff --git a/assets/charms/81.webp b/assets/charms/81.webp new file mode 100644 index 00000000..3a352146 Binary files /dev/null and b/assets/charms/81.webp differ diff --git a/assets/charms/82.webp b/assets/charms/82.webp new file mode 100644 index 00000000..0ddc0b01 Binary files /dev/null and b/assets/charms/82.webp differ diff --git a/assets/charms/9.webp b/assets/charms/9.webp new file mode 100644 index 00000000..fc0a77b1 Binary files /dev/null and b/assets/charms/9.webp differ diff --git a/assets/cases/10.webp b/assets/containers/10.webp similarity index 100% rename from assets/cases/10.webp rename to assets/containers/10.webp diff --git a/assets/reward_collections/10001.webp b/assets/containers/10001.webp similarity index 100% rename from assets/reward_collections/10001.webp rename to assets/containers/10001.webp diff --git a/assets/reward_collections/10002.webp b/assets/containers/10002.webp similarity index 100% rename from assets/reward_collections/10002.webp rename to assets/containers/10002.webp diff --git a/assets/reward_collections/10003.svg b/assets/containers/10003.svg similarity index 100% rename from assets/reward_collections/10003.svg rename to assets/containers/10003.svg diff --git a/assets/reward_collections/10004.svg b/assets/containers/10004.svg similarity index 100% rename from assets/reward_collections/10004.svg rename to assets/containers/10004.svg diff --git a/assets/reward_collections/10005.webp b/assets/containers/10005.webp similarity index 100% rename from assets/reward_collections/10005.webp rename to assets/containers/10005.webp diff --git a/assets/reward_collections/10006.svg b/assets/containers/10006.svg similarity index 100% rename from assets/reward_collections/10006.svg rename to assets/containers/10006.svg diff --git a/assets/reward_collections/10007.webp b/assets/containers/10007.webp similarity index 100% rename from assets/reward_collections/10007.webp rename to assets/containers/10007.webp diff --git a/assets/reward_collections/10008.webp b/assets/containers/10008.webp similarity index 100% rename from assets/reward_collections/10008.webp rename to assets/containers/10008.webp diff --git a/assets/reward_collections/10009.webp b/assets/containers/10009.webp similarity index 100% rename from assets/reward_collections/10009.webp rename to assets/containers/10009.webp diff --git a/assets/reward_collections/10010.svg b/assets/containers/10010.svg similarity index 100% rename from assets/reward_collections/10010.svg rename to assets/containers/10010.svg diff --git a/assets/reward_collections/10011.webp b/assets/containers/10011.webp similarity index 100% rename from assets/reward_collections/10011.webp rename to assets/containers/10011.webp diff --git a/assets/cases/11.webp b/assets/containers/11.webp similarity index 100% rename from assets/cases/11.webp rename to assets/containers/11.webp diff --git a/assets/cases/133.webp b/assets/containers/133.webp similarity index 100% rename from assets/cases/133.webp rename to assets/containers/133.webp diff --git a/assets/cases/134.webp b/assets/containers/134.webp similarity index 100% rename from assets/cases/134.webp rename to assets/containers/134.webp diff --git a/assets/cases/137.webp b/assets/containers/137.webp similarity index 100% rename from assets/cases/137.webp rename to assets/containers/137.webp diff --git a/assets/cases/138.webp b/assets/containers/138.webp similarity index 100% rename from assets/cases/138.webp rename to assets/containers/138.webp diff --git a/assets/cases/139.webp b/assets/containers/139.webp similarity index 100% rename from assets/cases/139.webp rename to assets/containers/139.webp diff --git a/assets/cases/14.webp b/assets/containers/14.webp similarity index 100% rename from assets/cases/14.webp rename to assets/containers/14.webp diff --git a/assets/cases/141.webp b/assets/containers/141.webp similarity index 100% rename from assets/cases/141.webp rename to assets/containers/141.webp diff --git a/assets/cases/142.webp b/assets/containers/142.webp similarity index 100% rename from assets/cases/142.webp rename to assets/containers/142.webp diff --git a/assets/cases/145.webp b/assets/containers/145.webp similarity index 100% rename from assets/cases/145.webp rename to assets/containers/145.webp diff --git a/assets/cases/146.webp b/assets/containers/146.webp similarity index 100% rename from assets/cases/146.webp rename to assets/containers/146.webp diff --git a/assets/cases/147.webp b/assets/containers/147.webp similarity index 100% rename from assets/cases/147.webp rename to assets/containers/147.webp diff --git a/assets/cases/15.webp b/assets/containers/15.webp similarity index 100% rename from assets/cases/15.webp rename to assets/containers/15.webp diff --git a/assets/cases/151.webp b/assets/containers/151.webp similarity index 100% rename from assets/cases/151.webp rename to assets/containers/151.webp diff --git a/assets/cases/152.webp b/assets/containers/152.webp similarity index 100% rename from assets/cases/152.webp rename to assets/containers/152.webp diff --git a/assets/cases/153.webp b/assets/containers/153.webp similarity index 100% rename from assets/cases/153.webp rename to assets/containers/153.webp diff --git a/assets/cases/155.webp b/assets/containers/155.webp similarity index 100% rename from assets/cases/155.webp rename to assets/containers/155.webp diff --git a/assets/cases/156.webp b/assets/containers/156.webp similarity index 100% rename from assets/cases/156.webp rename to assets/containers/156.webp diff --git a/assets/cases/16.webp b/assets/containers/16.webp similarity index 100% rename from assets/cases/16.webp rename to assets/containers/16.webp diff --git a/assets/cases/162.webp b/assets/containers/162.webp similarity index 100% rename from assets/cases/162.webp rename to assets/containers/162.webp diff --git a/assets/cases/163.webp b/assets/containers/163.webp similarity index 100% rename from assets/cases/163.webp rename to assets/containers/163.webp diff --git a/assets/cases/166.webp b/assets/containers/166.webp similarity index 100% rename from assets/cases/166.webp rename to assets/containers/166.webp diff --git a/assets/cases/167.webp b/assets/containers/167.webp similarity index 100% rename from assets/cases/167.webp rename to assets/containers/167.webp diff --git a/assets/cases/168.webp b/assets/containers/168.webp similarity index 100% rename from assets/cases/168.webp rename to assets/containers/168.webp diff --git a/assets/cases/169.webp b/assets/containers/169.webp similarity index 100% rename from assets/cases/169.webp rename to assets/containers/169.webp diff --git a/assets/cases/17.webp b/assets/containers/17.webp similarity index 100% rename from assets/cases/17.webp rename to assets/containers/17.webp diff --git a/assets/cases/170.webp b/assets/containers/170.webp similarity index 100% rename from assets/cases/170.webp rename to assets/containers/170.webp diff --git a/assets/cases/172.webp b/assets/containers/172.webp similarity index 100% rename from assets/cases/172.webp rename to assets/containers/172.webp diff --git a/assets/cases/173.webp b/assets/containers/173.webp similarity index 100% rename from assets/cases/173.webp rename to assets/containers/173.webp diff --git a/assets/cases/174.webp b/assets/containers/174.webp similarity index 100% rename from assets/cases/174.webp rename to assets/containers/174.webp diff --git a/assets/cases/176.webp b/assets/containers/176.webp similarity index 100% rename from assets/cases/176.webp rename to assets/containers/176.webp diff --git a/assets/cases/177.webp b/assets/containers/177.webp similarity index 100% rename from assets/cases/177.webp rename to assets/containers/177.webp diff --git a/assets/cases/178.webp b/assets/containers/178.webp similarity index 100% rename from assets/cases/178.webp rename to assets/containers/178.webp diff --git a/assets/cases/179.webp b/assets/containers/179.webp similarity index 100% rename from assets/cases/179.webp rename to assets/containers/179.webp diff --git a/assets/cases/18.webp b/assets/containers/18.webp similarity index 100% rename from assets/cases/18.webp rename to assets/containers/18.webp diff --git a/assets/cases/183.webp b/assets/containers/183.webp similarity index 100% rename from assets/cases/183.webp rename to assets/containers/183.webp diff --git a/assets/cases/184.webp b/assets/containers/184.webp similarity index 100% rename from assets/cases/184.webp rename to assets/containers/184.webp diff --git a/assets/cases/185.webp b/assets/containers/185.webp similarity index 100% rename from assets/cases/185.webp rename to assets/containers/185.webp diff --git a/assets/cases/186.webp b/assets/containers/186.webp similarity index 100% rename from assets/cases/186.webp rename to assets/containers/186.webp diff --git a/assets/cases/187.webp b/assets/containers/187.webp similarity index 100% rename from assets/cases/187.webp rename to assets/containers/187.webp diff --git a/assets/cases/188.webp b/assets/containers/188.webp similarity index 100% rename from assets/cases/188.webp rename to assets/containers/188.webp diff --git a/assets/cases/189.webp b/assets/containers/189.webp similarity index 100% rename from assets/cases/189.webp rename to assets/containers/189.webp diff --git a/assets/cases/19.webp b/assets/containers/19.webp similarity index 100% rename from assets/cases/19.webp rename to assets/containers/19.webp diff --git a/assets/cases/192.webp b/assets/containers/192.webp similarity index 100% rename from assets/cases/192.webp rename to assets/containers/192.webp diff --git a/assets/cases/193.webp b/assets/containers/193.webp similarity index 100% rename from assets/cases/193.webp rename to assets/containers/193.webp diff --git a/assets/cases/1977.webp b/assets/containers/1977.webp similarity index 100% rename from assets/cases/1977.webp rename to assets/containers/1977.webp diff --git a/assets/cases/1978.webp b/assets/containers/1978.webp similarity index 100% rename from assets/cases/1978.webp rename to assets/containers/1978.webp diff --git a/assets/cases/1979.webp b/assets/containers/1979.webp similarity index 100% rename from assets/cases/1979.webp rename to assets/containers/1979.webp diff --git a/assets/cases/1980.webp b/assets/containers/1980.webp similarity index 100% rename from assets/cases/1980.webp rename to assets/containers/1980.webp diff --git a/assets/cases/1981.webp b/assets/containers/1981.webp similarity index 100% rename from assets/cases/1981.webp rename to assets/containers/1981.webp diff --git a/assets/cases/1982.webp b/assets/containers/1982.webp similarity index 100% rename from assets/cases/1982.webp rename to assets/containers/1982.webp diff --git a/assets/cases/1983.webp b/assets/containers/1983.webp similarity index 100% rename from assets/cases/1983.webp rename to assets/containers/1983.webp diff --git a/assets/cases/1984.webp b/assets/containers/1984.webp similarity index 100% rename from assets/cases/1984.webp rename to assets/containers/1984.webp diff --git a/assets/cases/1985.webp b/assets/containers/1985.webp similarity index 100% rename from assets/cases/1985.webp rename to assets/containers/1985.webp diff --git a/assets/cases/1986.webp b/assets/containers/1986.webp similarity index 100% rename from assets/cases/1986.webp rename to assets/containers/1986.webp diff --git a/assets/cases/1987.webp b/assets/containers/1987.webp similarity index 100% rename from assets/cases/1987.webp rename to assets/containers/1987.webp diff --git a/assets/cases/1988.webp b/assets/containers/1988.webp similarity index 100% rename from assets/cases/1988.webp rename to assets/containers/1988.webp diff --git a/assets/cases/1989.webp b/assets/containers/1989.webp similarity index 100% rename from assets/cases/1989.webp rename to assets/containers/1989.webp diff --git a/assets/cases/199.webp b/assets/containers/199.webp similarity index 100% rename from assets/cases/199.webp rename to assets/containers/199.webp diff --git a/assets/cases/1990.webp b/assets/containers/1990.webp similarity index 100% rename from assets/cases/1990.webp rename to assets/containers/1990.webp diff --git a/assets/cases/1991.webp b/assets/containers/1991.webp similarity index 100% rename from assets/cases/1991.webp rename to assets/containers/1991.webp diff --git a/assets/cases/1992.webp b/assets/containers/1992.webp similarity index 100% rename from assets/cases/1992.webp rename to assets/containers/1992.webp diff --git a/assets/cases/1993.webp b/assets/containers/1993.webp similarity index 100% rename from assets/cases/1993.webp rename to assets/containers/1993.webp diff --git a/assets/cases/1994.webp b/assets/containers/1994.webp similarity index 100% rename from assets/cases/1994.webp rename to assets/containers/1994.webp diff --git a/assets/cases/1995.webp b/assets/containers/1995.webp similarity index 100% rename from assets/cases/1995.webp rename to assets/containers/1995.webp diff --git a/assets/cases/1996.webp b/assets/containers/1996.webp similarity index 100% rename from assets/cases/1996.webp rename to assets/containers/1996.webp diff --git a/assets/cases/1997.webp b/assets/containers/1997.webp similarity index 100% rename from assets/cases/1997.webp rename to assets/containers/1997.webp diff --git a/assets/cases/1998.webp b/assets/containers/1998.webp similarity index 100% rename from assets/cases/1998.webp rename to assets/containers/1998.webp diff --git a/assets/cases/1999.webp b/assets/containers/1999.webp similarity index 100% rename from assets/cases/1999.webp rename to assets/containers/1999.webp diff --git a/assets/cases/20.webp b/assets/containers/20.webp similarity index 100% rename from assets/cases/20.webp rename to assets/containers/20.webp diff --git a/assets/cases/2000.webp b/assets/containers/2000.webp similarity index 100% rename from assets/cases/2000.webp rename to assets/containers/2000.webp diff --git a/assets/operation_collections/20001.webp b/assets/containers/20001.webp similarity index 100% rename from assets/operation_collections/20001.webp rename to assets/containers/20001.webp diff --git a/assets/operation_collections/20002.webp b/assets/containers/20002.webp similarity index 100% rename from assets/operation_collections/20002.webp rename to assets/containers/20002.webp diff --git a/assets/operation_collections/20003.webp b/assets/containers/20003.webp similarity index 100% rename from assets/operation_collections/20003.webp rename to assets/containers/20003.webp diff --git a/assets/operation_collections/20004.webp b/assets/containers/20004.webp similarity index 100% rename from assets/operation_collections/20004.webp rename to assets/containers/20004.webp diff --git a/assets/operation_collections/20005.webp b/assets/containers/20005.webp similarity index 100% rename from assets/operation_collections/20005.webp rename to assets/containers/20005.webp diff --git a/assets/operation_collections/20006.webp b/assets/containers/20006.webp similarity index 100% rename from assets/operation_collections/20006.webp rename to assets/containers/20006.webp diff --git a/assets/operation_collections/20007.webp b/assets/containers/20007.webp similarity index 100% rename from assets/operation_collections/20007.webp rename to assets/containers/20007.webp diff --git a/assets/operation_collections/20008.webp b/assets/containers/20008.webp similarity index 100% rename from assets/operation_collections/20008.webp rename to assets/containers/20008.webp diff --git a/assets/operation_collections/20009.webp b/assets/containers/20009.webp similarity index 100% rename from assets/operation_collections/20009.webp rename to assets/containers/20009.webp diff --git a/assets/cases/2001.webp b/assets/containers/2001.webp similarity index 100% rename from assets/cases/2001.webp rename to assets/containers/2001.webp diff --git a/assets/operation_collections/20010.webp b/assets/containers/20010.webp similarity index 100% rename from assets/operation_collections/20010.webp rename to assets/containers/20010.webp diff --git a/assets/operation_collections/20011.webp b/assets/containers/20011.webp similarity index 100% rename from assets/operation_collections/20011.webp rename to assets/containers/20011.webp diff --git a/assets/operation_collections/20012.webp b/assets/containers/20012.webp similarity index 100% rename from assets/operation_collections/20012.webp rename to assets/containers/20012.webp diff --git a/assets/operation_collections/20013.webp b/assets/containers/20013.webp similarity index 100% rename from assets/operation_collections/20013.webp rename to assets/containers/20013.webp diff --git a/assets/operation_collections/20014.webp b/assets/containers/20014.webp similarity index 100% rename from assets/operation_collections/20014.webp rename to assets/containers/20014.webp diff --git a/assets/operation_collections/20015.webp b/assets/containers/20015.webp similarity index 100% rename from assets/operation_collections/20015.webp rename to assets/containers/20015.webp diff --git a/assets/operation_collections/20016.webp b/assets/containers/20016.webp similarity index 100% rename from assets/operation_collections/20016.webp rename to assets/containers/20016.webp diff --git a/assets/operation_collections/20017.webp b/assets/containers/20017.webp similarity index 100% rename from assets/operation_collections/20017.webp rename to assets/containers/20017.webp diff --git a/assets/operation_collections/20018.webp b/assets/containers/20018.webp similarity index 100% rename from assets/operation_collections/20018.webp rename to assets/containers/20018.webp diff --git a/assets/operation_collections/20019.webp b/assets/containers/20019.webp similarity index 100% rename from assets/operation_collections/20019.webp rename to assets/containers/20019.webp diff --git a/assets/cases/2002.webp b/assets/containers/2002.webp similarity index 100% rename from assets/cases/2002.webp rename to assets/containers/2002.webp diff --git a/assets/operation_collections/20020.webp b/assets/containers/20020.webp similarity index 100% rename from assets/operation_collections/20020.webp rename to assets/containers/20020.webp diff --git a/assets/operation_collections/20021.webp b/assets/containers/20021.webp similarity index 100% rename from assets/operation_collections/20021.webp rename to assets/containers/20021.webp diff --git a/assets/operation_collections/20022.webp b/assets/containers/20022.webp similarity index 100% rename from assets/operation_collections/20022.webp rename to assets/containers/20022.webp diff --git a/assets/operation_collections/20023.webp b/assets/containers/20023.webp similarity index 100% rename from assets/operation_collections/20023.webp rename to assets/containers/20023.webp diff --git a/assets/operation_collections/20024.webp b/assets/containers/20024.webp similarity index 100% rename from assets/operation_collections/20024.webp rename to assets/containers/20024.webp diff --git a/assets/operation_collections/20025.webp b/assets/containers/20025.webp similarity index 100% rename from assets/operation_collections/20025.webp rename to assets/containers/20025.webp diff --git a/assets/cases/2003.webp b/assets/containers/2003.webp similarity index 100% rename from assets/cases/2003.webp rename to assets/containers/2003.webp diff --git a/assets/cases/2004.webp b/assets/containers/2004.webp similarity index 100% rename from assets/cases/2004.webp rename to assets/containers/2004.webp diff --git a/assets/cases/2005.webp b/assets/containers/2005.webp similarity index 100% rename from assets/cases/2005.webp rename to assets/containers/2005.webp diff --git a/assets/cases/2006.webp b/assets/containers/2006.webp similarity index 100% rename from assets/cases/2006.webp rename to assets/containers/2006.webp diff --git a/assets/cases/2007.webp b/assets/containers/2007.webp similarity index 100% rename from assets/cases/2007.webp rename to assets/containers/2007.webp diff --git a/assets/cases/2008.webp b/assets/containers/2008.webp similarity index 100% rename from assets/cases/2008.webp rename to assets/containers/2008.webp diff --git a/assets/cases/2009.webp b/assets/containers/2009.webp similarity index 100% rename from assets/cases/2009.webp rename to assets/containers/2009.webp diff --git a/assets/cases/2010.webp b/assets/containers/2010.webp similarity index 100% rename from assets/cases/2010.webp rename to assets/containers/2010.webp diff --git a/assets/cases/2011.webp b/assets/containers/2011.webp similarity index 100% rename from assets/cases/2011.webp rename to assets/containers/2011.webp diff --git a/assets/cases/2012.webp b/assets/containers/2012.webp similarity index 100% rename from assets/cases/2012.webp rename to assets/containers/2012.webp diff --git a/assets/cases/2013.webp b/assets/containers/2013.webp similarity index 100% rename from assets/cases/2013.webp rename to assets/containers/2013.webp diff --git a/assets/cases/2014.webp b/assets/containers/2014.webp similarity index 100% rename from assets/cases/2014.webp rename to assets/containers/2014.webp diff --git a/assets/cases/2015.webp b/assets/containers/2015.webp similarity index 100% rename from assets/cases/2015.webp rename to assets/containers/2015.webp diff --git a/assets/cases/2016.webp b/assets/containers/2016.webp similarity index 100% rename from assets/cases/2016.webp rename to assets/containers/2016.webp diff --git a/assets/cases/2017.webp b/assets/containers/2017.webp similarity index 100% rename from assets/cases/2017.webp rename to assets/containers/2017.webp diff --git a/assets/cases/2018.webp b/assets/containers/2018.webp similarity index 100% rename from assets/cases/2018.webp rename to assets/containers/2018.webp diff --git a/assets/cases/2019.webp b/assets/containers/2019.webp similarity index 100% rename from assets/cases/2019.webp rename to assets/containers/2019.webp diff --git a/assets/cases/202.webp b/assets/containers/202.webp similarity index 100% rename from assets/cases/202.webp rename to assets/containers/202.webp diff --git a/assets/cases/2020.webp b/assets/containers/2020.webp similarity index 100% rename from assets/cases/2020.webp rename to assets/containers/2020.webp diff --git a/assets/cases/2021.webp b/assets/containers/2021.webp similarity index 100% rename from assets/cases/2021.webp rename to assets/containers/2021.webp diff --git a/assets/cases/2022.webp b/assets/containers/2022.webp similarity index 100% rename from assets/cases/2022.webp rename to assets/containers/2022.webp diff --git a/assets/cases/2023.webp b/assets/containers/2023.webp similarity index 100% rename from assets/cases/2023.webp rename to assets/containers/2023.webp diff --git a/assets/cases/2024.webp b/assets/containers/2024.webp similarity index 100% rename from assets/cases/2024.webp rename to assets/containers/2024.webp diff --git a/assets/cases/2025.webp b/assets/containers/2025.webp similarity index 100% rename from assets/cases/2025.webp rename to assets/containers/2025.webp diff --git a/assets/cases/2026.webp b/assets/containers/2026.webp similarity index 100% rename from assets/cases/2026.webp rename to assets/containers/2026.webp diff --git a/assets/cases/2027.webp b/assets/containers/2027.webp similarity index 100% rename from assets/cases/2027.webp rename to assets/containers/2027.webp diff --git a/assets/cases/2028.webp b/assets/containers/2028.webp similarity index 100% rename from assets/cases/2028.webp rename to assets/containers/2028.webp diff --git a/assets/cases/2029.webp b/assets/containers/2029.webp similarity index 100% rename from assets/cases/2029.webp rename to assets/containers/2029.webp diff --git a/assets/cases/203.webp b/assets/containers/203.webp similarity index 100% rename from assets/cases/203.webp rename to assets/containers/203.webp diff --git a/assets/cases/2030.webp b/assets/containers/2030.webp similarity index 100% rename from assets/cases/2030.webp rename to assets/containers/2030.webp diff --git a/assets/cases/2031.webp b/assets/containers/2031.webp similarity index 100% rename from assets/cases/2031.webp rename to assets/containers/2031.webp diff --git a/assets/cases/2032.webp b/assets/containers/2032.webp similarity index 100% rename from assets/cases/2032.webp rename to assets/containers/2032.webp diff --git a/assets/cases/2033.webp b/assets/containers/2033.webp similarity index 100% rename from assets/cases/2033.webp rename to assets/containers/2033.webp diff --git a/assets/cases/2034.webp b/assets/containers/2034.webp similarity index 100% rename from assets/cases/2034.webp rename to assets/containers/2034.webp diff --git a/assets/cases/2035.webp b/assets/containers/2035.webp similarity index 100% rename from assets/cases/2035.webp rename to assets/containers/2035.webp diff --git a/assets/cases/2036.webp b/assets/containers/2036.webp similarity index 100% rename from assets/cases/2036.webp rename to assets/containers/2036.webp diff --git a/assets/cases/2037.webp b/assets/containers/2037.webp similarity index 100% rename from assets/cases/2037.webp rename to assets/containers/2037.webp diff --git a/assets/cases/2038.webp b/assets/containers/2038.webp similarity index 100% rename from assets/cases/2038.webp rename to assets/containers/2038.webp diff --git a/assets/cases/2039.webp b/assets/containers/2039.webp similarity index 100% rename from assets/cases/2039.webp rename to assets/containers/2039.webp diff --git a/assets/cases/204.webp b/assets/containers/204.webp similarity index 100% rename from assets/cases/204.webp rename to assets/containers/204.webp diff --git a/assets/cases/2040.webp b/assets/containers/2040.webp similarity index 100% rename from assets/cases/2040.webp rename to assets/containers/2040.webp diff --git a/assets/cases/2041.webp b/assets/containers/2041.webp similarity index 100% rename from assets/cases/2041.webp rename to assets/containers/2041.webp diff --git a/assets/cases/2042.webp b/assets/containers/2042.webp similarity index 100% rename from assets/cases/2042.webp rename to assets/containers/2042.webp diff --git a/assets/cases/2043.webp b/assets/containers/2043.webp similarity index 100% rename from assets/cases/2043.webp rename to assets/containers/2043.webp diff --git a/assets/cases/2044.webp b/assets/containers/2044.webp similarity index 100% rename from assets/cases/2044.webp rename to assets/containers/2044.webp diff --git a/assets/cases/2045.webp b/assets/containers/2045.webp similarity index 100% rename from assets/cases/2045.webp rename to assets/containers/2045.webp diff --git a/assets/cases/2046.webp b/assets/containers/2046.webp similarity index 100% rename from assets/cases/2046.webp rename to assets/containers/2046.webp diff --git a/assets/cases/2047.webp b/assets/containers/2047.webp similarity index 100% rename from assets/cases/2047.webp rename to assets/containers/2047.webp diff --git a/assets/cases/2048.webp b/assets/containers/2048.webp similarity index 100% rename from assets/cases/2048.webp rename to assets/containers/2048.webp diff --git a/assets/cases/2049.webp b/assets/containers/2049.webp similarity index 100% rename from assets/cases/2049.webp rename to assets/containers/2049.webp diff --git a/assets/cases/205.webp b/assets/containers/205.webp similarity index 100% rename from assets/cases/205.webp rename to assets/containers/205.webp diff --git a/assets/cases/2050.webp b/assets/containers/2050.webp similarity index 100% rename from assets/cases/2050.webp rename to assets/containers/2050.webp diff --git a/assets/cases/2051.webp b/assets/containers/2051.webp similarity index 100% rename from assets/cases/2051.webp rename to assets/containers/2051.webp diff --git a/assets/cases/2052.webp b/assets/containers/2052.webp similarity index 100% rename from assets/cases/2052.webp rename to assets/containers/2052.webp diff --git a/assets/cases/2053.webp b/assets/containers/2053.webp similarity index 100% rename from assets/cases/2053.webp rename to assets/containers/2053.webp diff --git a/assets/cases/2054.webp b/assets/containers/2054.webp similarity index 100% rename from assets/cases/2054.webp rename to assets/containers/2054.webp diff --git a/assets/cases/2055.webp b/assets/containers/2055.webp similarity index 100% rename from assets/cases/2055.webp rename to assets/containers/2055.webp diff --git a/assets/cases/2056.webp b/assets/containers/2056.webp similarity index 100% rename from assets/cases/2056.webp rename to assets/containers/2056.webp diff --git a/assets/cases/2057.webp b/assets/containers/2057.webp similarity index 100% rename from assets/cases/2057.webp rename to assets/containers/2057.webp diff --git a/assets/cases/2058.webp b/assets/containers/2058.webp similarity index 100% rename from assets/cases/2058.webp rename to assets/containers/2058.webp diff --git a/assets/cases/2059.webp b/assets/containers/2059.webp similarity index 100% rename from assets/cases/2059.webp rename to assets/containers/2059.webp diff --git a/assets/cases/206.webp b/assets/containers/206.webp similarity index 100% rename from assets/cases/206.webp rename to assets/containers/206.webp diff --git a/assets/cases/2060.webp b/assets/containers/2060.webp similarity index 100% rename from assets/cases/2060.webp rename to assets/containers/2060.webp diff --git a/assets/cases/2061.webp b/assets/containers/2061.webp similarity index 100% rename from assets/cases/2061.webp rename to assets/containers/2061.webp diff --git a/assets/cases/2062.webp b/assets/containers/2062.webp similarity index 100% rename from assets/cases/2062.webp rename to assets/containers/2062.webp diff --git a/assets/cases/2063.webp b/assets/containers/2063.webp similarity index 100% rename from assets/cases/2063.webp rename to assets/containers/2063.webp diff --git a/assets/cases/2064.webp b/assets/containers/2064.webp similarity index 100% rename from assets/cases/2064.webp rename to assets/containers/2064.webp diff --git a/assets/cases/2065.webp b/assets/containers/2065.webp similarity index 100% rename from assets/cases/2065.webp rename to assets/containers/2065.webp diff --git a/assets/cases/2066.webp b/assets/containers/2066.webp similarity index 100% rename from assets/cases/2066.webp rename to assets/containers/2066.webp diff --git a/assets/cases/2067.webp b/assets/containers/2067.webp similarity index 100% rename from assets/cases/2067.webp rename to assets/containers/2067.webp diff --git a/assets/cases/2068.webp b/assets/containers/2068.webp similarity index 100% rename from assets/cases/2068.webp rename to assets/containers/2068.webp diff --git a/assets/cases/2069.webp b/assets/containers/2069.webp similarity index 100% rename from assets/cases/2069.webp rename to assets/containers/2069.webp diff --git a/assets/cases/2070.webp b/assets/containers/2070.webp similarity index 100% rename from assets/cases/2070.webp rename to assets/containers/2070.webp diff --git a/assets/cases/2071.webp b/assets/containers/2071.webp similarity index 100% rename from assets/cases/2071.webp rename to assets/containers/2071.webp diff --git a/assets/cases/2072.webp b/assets/containers/2072.webp similarity index 100% rename from assets/cases/2072.webp rename to assets/containers/2072.webp diff --git a/assets/cases/2073.webp b/assets/containers/2073.webp similarity index 100% rename from assets/cases/2073.webp rename to assets/containers/2073.webp diff --git a/assets/cases/2074.webp b/assets/containers/2074.webp similarity index 100% rename from assets/cases/2074.webp rename to assets/containers/2074.webp diff --git a/assets/cases/2075.webp b/assets/containers/2075.webp similarity index 100% rename from assets/cases/2075.webp rename to assets/containers/2075.webp diff --git a/assets/cases/2076.webp b/assets/containers/2076.webp similarity index 100% rename from assets/cases/2076.webp rename to assets/containers/2076.webp diff --git a/assets/cases/2077.webp b/assets/containers/2077.webp similarity index 100% rename from assets/cases/2077.webp rename to assets/containers/2077.webp diff --git a/assets/cases/2078.webp b/assets/containers/2078.webp similarity index 100% rename from assets/cases/2078.webp rename to assets/containers/2078.webp diff --git a/assets/cases/2079.webp b/assets/containers/2079.webp similarity index 100% rename from assets/cases/2079.webp rename to assets/containers/2079.webp diff --git a/assets/cases/208.webp b/assets/containers/208.webp similarity index 100% rename from assets/cases/208.webp rename to assets/containers/208.webp diff --git a/assets/cases/2080.webp b/assets/containers/2080.webp similarity index 100% rename from assets/cases/2080.webp rename to assets/containers/2080.webp diff --git a/assets/cases/2081.webp b/assets/containers/2081.webp similarity index 100% rename from assets/cases/2081.webp rename to assets/containers/2081.webp diff --git a/assets/cases/2082.webp b/assets/containers/2082.webp similarity index 100% rename from assets/cases/2082.webp rename to assets/containers/2082.webp diff --git a/assets/cases/2083.webp b/assets/containers/2083.webp similarity index 100% rename from assets/cases/2083.webp rename to assets/containers/2083.webp diff --git a/assets/cases/2084.webp b/assets/containers/2084.webp similarity index 100% rename from assets/cases/2084.webp rename to assets/containers/2084.webp diff --git a/assets/cases/2085.webp b/assets/containers/2085.webp similarity index 100% rename from assets/cases/2085.webp rename to assets/containers/2085.webp diff --git a/assets/cases/2086.webp b/assets/containers/2086.webp similarity index 100% rename from assets/cases/2086.webp rename to assets/containers/2086.webp diff --git a/assets/cases/2087.webp b/assets/containers/2087.webp similarity index 100% rename from assets/cases/2087.webp rename to assets/containers/2087.webp diff --git a/assets/cases/2088.webp b/assets/containers/2088.webp similarity index 100% rename from assets/cases/2088.webp rename to assets/containers/2088.webp diff --git a/assets/cases/2089.webp b/assets/containers/2089.webp similarity index 100% rename from assets/cases/2089.webp rename to assets/containers/2089.webp diff --git a/assets/cases/209.webp b/assets/containers/209.webp similarity index 100% rename from assets/cases/209.webp rename to assets/containers/209.webp diff --git a/assets/cases/2090.webp b/assets/containers/2090.webp similarity index 100% rename from assets/cases/2090.webp rename to assets/containers/2090.webp diff --git a/assets/cases/2091.webp b/assets/containers/2091.webp similarity index 100% rename from assets/cases/2091.webp rename to assets/containers/2091.webp diff --git a/assets/cases/2092.webp b/assets/containers/2092.webp similarity index 100% rename from assets/cases/2092.webp rename to assets/containers/2092.webp diff --git a/assets/cases/2093.webp b/assets/containers/2093.webp similarity index 100% rename from assets/cases/2093.webp rename to assets/containers/2093.webp diff --git a/assets/cases/2094.webp b/assets/containers/2094.webp similarity index 100% rename from assets/cases/2094.webp rename to assets/containers/2094.webp diff --git a/assets/cases/2095.webp b/assets/containers/2095.webp similarity index 100% rename from assets/cases/2095.webp rename to assets/containers/2095.webp diff --git a/assets/cases/2096.webp b/assets/containers/2096.webp similarity index 100% rename from assets/cases/2096.webp rename to assets/containers/2096.webp diff --git a/assets/cases/2097.webp b/assets/containers/2097.webp similarity index 100% rename from assets/cases/2097.webp rename to assets/containers/2097.webp diff --git a/assets/cases/2098.webp b/assets/containers/2098.webp similarity index 100% rename from assets/cases/2098.webp rename to assets/containers/2098.webp diff --git a/assets/cases/2099.webp b/assets/containers/2099.webp similarity index 100% rename from assets/cases/2099.webp rename to assets/containers/2099.webp diff --git a/assets/cases/21.webp b/assets/containers/21.webp similarity index 100% rename from assets/cases/21.webp rename to assets/containers/21.webp diff --git a/assets/cases/210.webp b/assets/containers/210.webp similarity index 100% rename from assets/cases/210.webp rename to assets/containers/210.webp diff --git a/assets/cases/2100.webp b/assets/containers/2100.webp similarity index 100% rename from assets/cases/2100.webp rename to assets/containers/2100.webp diff --git a/assets/cases/2101.webp b/assets/containers/2101.webp similarity index 100% rename from assets/cases/2101.webp rename to assets/containers/2101.webp diff --git a/assets/cases/2102.webp b/assets/containers/2102.webp similarity index 100% rename from assets/cases/2102.webp rename to assets/containers/2102.webp diff --git a/assets/cases/2103.webp b/assets/containers/2103.webp similarity index 100% rename from assets/cases/2103.webp rename to assets/containers/2103.webp diff --git a/assets/cases/2104.webp b/assets/containers/2104.webp similarity index 100% rename from assets/cases/2104.webp rename to assets/containers/2104.webp diff --git a/assets/cases/2105.webp b/assets/containers/2105.webp similarity index 100% rename from assets/cases/2105.webp rename to assets/containers/2105.webp diff --git a/assets/cases/2106.webp b/assets/containers/2106.webp similarity index 100% rename from assets/cases/2106.webp rename to assets/containers/2106.webp diff --git a/assets/cases/2107.webp b/assets/containers/2107.webp similarity index 100% rename from assets/cases/2107.webp rename to assets/containers/2107.webp diff --git a/assets/cases/2108.webp b/assets/containers/2108.webp similarity index 100% rename from assets/cases/2108.webp rename to assets/containers/2108.webp diff --git a/assets/cases/2109.webp b/assets/containers/2109.webp similarity index 100% rename from assets/cases/2109.webp rename to assets/containers/2109.webp diff --git a/assets/cases/211.webp b/assets/containers/211.webp similarity index 100% rename from assets/cases/211.webp rename to assets/containers/211.webp diff --git a/assets/cases/2110.webp b/assets/containers/2110.webp similarity index 100% rename from assets/cases/2110.webp rename to assets/containers/2110.webp diff --git a/assets/cases/2111.webp b/assets/containers/2111.webp similarity index 100% rename from assets/cases/2111.webp rename to assets/containers/2111.webp diff --git a/assets/cases/2112.webp b/assets/containers/2112.webp similarity index 100% rename from assets/cases/2112.webp rename to assets/containers/2112.webp diff --git a/assets/cases/2113.webp b/assets/containers/2113.webp similarity index 100% rename from assets/cases/2113.webp rename to assets/containers/2113.webp diff --git a/assets/cases/2114.webp b/assets/containers/2114.webp similarity index 100% rename from assets/cases/2114.webp rename to assets/containers/2114.webp diff --git a/assets/cases/2115.webp b/assets/containers/2115.webp similarity index 100% rename from assets/cases/2115.webp rename to assets/containers/2115.webp diff --git a/assets/cases/2116.webp b/assets/containers/2116.webp similarity index 100% rename from assets/cases/2116.webp rename to assets/containers/2116.webp diff --git a/assets/cases/2117.webp b/assets/containers/2117.webp similarity index 100% rename from assets/cases/2117.webp rename to assets/containers/2117.webp diff --git a/assets/cases/2118.webp b/assets/containers/2118.webp similarity index 100% rename from assets/cases/2118.webp rename to assets/containers/2118.webp diff --git a/assets/cases/2119.webp b/assets/containers/2119.webp similarity index 100% rename from assets/cases/2119.webp rename to assets/containers/2119.webp diff --git a/assets/cases/212.webp b/assets/containers/212.webp similarity index 100% rename from assets/cases/212.webp rename to assets/containers/212.webp diff --git a/assets/cases/2120.webp b/assets/containers/2120.webp similarity index 100% rename from assets/cases/2120.webp rename to assets/containers/2120.webp diff --git a/assets/cases/2121.webp b/assets/containers/2121.webp similarity index 100% rename from assets/cases/2121.webp rename to assets/containers/2121.webp diff --git a/assets/cases/2122.webp b/assets/containers/2122.webp similarity index 100% rename from assets/cases/2122.webp rename to assets/containers/2122.webp diff --git a/assets/cases/2123.webp b/assets/containers/2123.webp similarity index 100% rename from assets/cases/2123.webp rename to assets/containers/2123.webp diff --git a/assets/cases/2124.webp b/assets/containers/2124.webp similarity index 100% rename from assets/cases/2124.webp rename to assets/containers/2124.webp diff --git a/assets/cases/2125.webp b/assets/containers/2125.webp similarity index 100% rename from assets/cases/2125.webp rename to assets/containers/2125.webp diff --git a/assets/cases/2126.webp b/assets/containers/2126.webp similarity index 100% rename from assets/cases/2126.webp rename to assets/containers/2126.webp diff --git a/assets/cases/2127.webp b/assets/containers/2127.webp similarity index 100% rename from assets/cases/2127.webp rename to assets/containers/2127.webp diff --git a/assets/cases/2128.webp b/assets/containers/2128.webp similarity index 100% rename from assets/cases/2128.webp rename to assets/containers/2128.webp diff --git a/assets/cases/2129.webp b/assets/containers/2129.webp similarity index 100% rename from assets/cases/2129.webp rename to assets/containers/2129.webp diff --git a/assets/cases/213.webp b/assets/containers/213.webp similarity index 100% rename from assets/cases/213.webp rename to assets/containers/213.webp diff --git a/assets/cases/2130.webp b/assets/containers/2130.webp similarity index 100% rename from assets/cases/2130.webp rename to assets/containers/2130.webp diff --git a/assets/cases/2131.webp b/assets/containers/2131.webp similarity index 100% rename from assets/cases/2131.webp rename to assets/containers/2131.webp diff --git a/assets/cases/2132.webp b/assets/containers/2132.webp similarity index 100% rename from assets/cases/2132.webp rename to assets/containers/2132.webp diff --git a/assets/cases/2133.webp b/assets/containers/2133.webp similarity index 100% rename from assets/cases/2133.webp rename to assets/containers/2133.webp diff --git a/assets/cases/2134.webp b/assets/containers/2134.webp similarity index 100% rename from assets/cases/2134.webp rename to assets/containers/2134.webp diff --git a/assets/cases/2135.webp b/assets/containers/2135.webp similarity index 100% rename from assets/cases/2135.webp rename to assets/containers/2135.webp diff --git a/assets/cases/2136.webp b/assets/containers/2136.webp similarity index 100% rename from assets/cases/2136.webp rename to assets/containers/2136.webp diff --git a/assets/cases/2137.webp b/assets/containers/2137.webp similarity index 100% rename from assets/cases/2137.webp rename to assets/containers/2137.webp diff --git a/assets/cases/2138.webp b/assets/containers/2138.webp similarity index 100% rename from assets/cases/2138.webp rename to assets/containers/2138.webp diff --git a/assets/cases/2139.webp b/assets/containers/2139.webp similarity index 100% rename from assets/cases/2139.webp rename to assets/containers/2139.webp diff --git a/assets/cases/214.webp b/assets/containers/214.webp similarity index 100% rename from assets/cases/214.webp rename to assets/containers/214.webp diff --git a/assets/cases/2140.webp b/assets/containers/2140.webp similarity index 100% rename from assets/cases/2140.webp rename to assets/containers/2140.webp diff --git a/assets/cases/2141.webp b/assets/containers/2141.webp similarity index 100% rename from assets/cases/2141.webp rename to assets/containers/2141.webp diff --git a/assets/cases/2142.webp b/assets/containers/2142.webp similarity index 100% rename from assets/cases/2142.webp rename to assets/containers/2142.webp diff --git a/assets/cases/2143.webp b/assets/containers/2143.webp similarity index 100% rename from assets/cases/2143.webp rename to assets/containers/2143.webp diff --git a/assets/cases/2144.webp b/assets/containers/2144.webp similarity index 100% rename from assets/cases/2144.webp rename to assets/containers/2144.webp diff --git a/assets/cases/2145.webp b/assets/containers/2145.webp similarity index 100% rename from assets/cases/2145.webp rename to assets/containers/2145.webp diff --git a/assets/cases/2146.webp b/assets/containers/2146.webp similarity index 100% rename from assets/cases/2146.webp rename to assets/containers/2146.webp diff --git a/assets/cases/2147.webp b/assets/containers/2147.webp similarity index 100% rename from assets/cases/2147.webp rename to assets/containers/2147.webp diff --git a/assets/cases/2148.webp b/assets/containers/2148.webp similarity index 100% rename from assets/cases/2148.webp rename to assets/containers/2148.webp diff --git a/assets/cases/2149.webp b/assets/containers/2149.webp similarity index 100% rename from assets/cases/2149.webp rename to assets/containers/2149.webp diff --git a/assets/cases/215.webp b/assets/containers/215.webp similarity index 100% rename from assets/cases/215.webp rename to assets/containers/215.webp diff --git a/assets/cases/2150.webp b/assets/containers/2150.webp similarity index 100% rename from assets/cases/2150.webp rename to assets/containers/2150.webp diff --git a/assets/cases/2151.webp b/assets/containers/2151.webp similarity index 100% rename from assets/cases/2151.webp rename to assets/containers/2151.webp diff --git a/assets/cases/2152.webp b/assets/containers/2152.webp similarity index 100% rename from assets/cases/2152.webp rename to assets/containers/2152.webp diff --git a/assets/cases/2153.webp b/assets/containers/2153.webp similarity index 100% rename from assets/cases/2153.webp rename to assets/containers/2153.webp diff --git a/assets/cases/2154.webp b/assets/containers/2154.webp similarity index 100% rename from assets/cases/2154.webp rename to assets/containers/2154.webp diff --git a/assets/cases/2155.webp b/assets/containers/2155.webp similarity index 100% rename from assets/cases/2155.webp rename to assets/containers/2155.webp diff --git a/assets/cases/2156.webp b/assets/containers/2156.webp similarity index 100% rename from assets/cases/2156.webp rename to assets/containers/2156.webp diff --git a/assets/cases/2157.webp b/assets/containers/2157.webp similarity index 100% rename from assets/cases/2157.webp rename to assets/containers/2157.webp diff --git a/assets/cases/2158.webp b/assets/containers/2158.webp similarity index 100% rename from assets/cases/2158.webp rename to assets/containers/2158.webp diff --git a/assets/cases/2159.webp b/assets/containers/2159.webp similarity index 100% rename from assets/cases/2159.webp rename to assets/containers/2159.webp diff --git a/assets/cases/216.webp b/assets/containers/216.webp similarity index 100% rename from assets/cases/216.webp rename to assets/containers/216.webp diff --git a/assets/cases/2160.webp b/assets/containers/2160.webp similarity index 100% rename from assets/cases/2160.webp rename to assets/containers/2160.webp diff --git a/assets/cases/2161.webp b/assets/containers/2161.webp similarity index 100% rename from assets/cases/2161.webp rename to assets/containers/2161.webp diff --git a/assets/cases/2162.webp b/assets/containers/2162.webp similarity index 100% rename from assets/cases/2162.webp rename to assets/containers/2162.webp diff --git a/assets/cases/2163.webp b/assets/containers/2163.webp similarity index 100% rename from assets/cases/2163.webp rename to assets/containers/2163.webp diff --git a/assets/cases/2164.webp b/assets/containers/2164.webp similarity index 100% rename from assets/cases/2164.webp rename to assets/containers/2164.webp diff --git a/assets/cases/2165.webp b/assets/containers/2165.webp similarity index 100% rename from assets/cases/2165.webp rename to assets/containers/2165.webp diff --git a/assets/cases/2166.webp b/assets/containers/2166.webp similarity index 100% rename from assets/cases/2166.webp rename to assets/containers/2166.webp diff --git a/assets/cases/2167.webp b/assets/containers/2167.webp similarity index 100% rename from assets/cases/2167.webp rename to assets/containers/2167.webp diff --git a/assets/cases/2168.webp b/assets/containers/2168.webp similarity index 100% rename from assets/cases/2168.webp rename to assets/containers/2168.webp diff --git a/assets/cases/2169.webp b/assets/containers/2169.webp similarity index 100% rename from assets/cases/2169.webp rename to assets/containers/2169.webp diff --git a/assets/cases/217.webp b/assets/containers/217.webp similarity index 100% rename from assets/cases/217.webp rename to assets/containers/217.webp diff --git a/assets/cases/2170.webp b/assets/containers/2170.webp similarity index 100% rename from assets/cases/2170.webp rename to assets/containers/2170.webp diff --git a/assets/cases/2171.webp b/assets/containers/2171.webp similarity index 100% rename from assets/cases/2171.webp rename to assets/containers/2171.webp diff --git a/assets/cases/2172.webp b/assets/containers/2172.webp similarity index 100% rename from assets/cases/2172.webp rename to assets/containers/2172.webp diff --git a/assets/cases/2173.webp b/assets/containers/2173.webp similarity index 100% rename from assets/cases/2173.webp rename to assets/containers/2173.webp diff --git a/assets/cases/2174.webp b/assets/containers/2174.webp similarity index 100% rename from assets/cases/2174.webp rename to assets/containers/2174.webp diff --git a/assets/cases/2175.webp b/assets/containers/2175.webp similarity index 100% rename from assets/cases/2175.webp rename to assets/containers/2175.webp diff --git a/assets/cases/2176.webp b/assets/containers/2176.webp similarity index 100% rename from assets/cases/2176.webp rename to assets/containers/2176.webp diff --git a/assets/cases/2177.webp b/assets/containers/2177.webp similarity index 100% rename from assets/cases/2177.webp rename to assets/containers/2177.webp diff --git a/assets/cases/2178.webp b/assets/containers/2178.webp similarity index 100% rename from assets/cases/2178.webp rename to assets/containers/2178.webp diff --git a/assets/cases/2179.webp b/assets/containers/2179.webp similarity index 100% rename from assets/cases/2179.webp rename to assets/containers/2179.webp diff --git a/assets/cases/218.webp b/assets/containers/218.webp similarity index 100% rename from assets/cases/218.webp rename to assets/containers/218.webp diff --git a/assets/cases/2180.webp b/assets/containers/2180.webp similarity index 100% rename from assets/cases/2180.webp rename to assets/containers/2180.webp diff --git a/assets/cases/2181.webp b/assets/containers/2181.webp similarity index 100% rename from assets/cases/2181.webp rename to assets/containers/2181.webp diff --git a/assets/cases/2182.webp b/assets/containers/2182.webp similarity index 100% rename from assets/cases/2182.webp rename to assets/containers/2182.webp diff --git a/assets/cases/2183.webp b/assets/containers/2183.webp similarity index 100% rename from assets/cases/2183.webp rename to assets/containers/2183.webp diff --git a/assets/cases/2184.webp b/assets/containers/2184.webp similarity index 100% rename from assets/cases/2184.webp rename to assets/containers/2184.webp diff --git a/assets/cases/2185.webp b/assets/containers/2185.webp similarity index 100% rename from assets/cases/2185.webp rename to assets/containers/2185.webp diff --git a/assets/cases/2186.webp b/assets/containers/2186.webp similarity index 100% rename from assets/cases/2186.webp rename to assets/containers/2186.webp diff --git a/assets/cases/2187.webp b/assets/containers/2187.webp similarity index 100% rename from assets/cases/2187.webp rename to assets/containers/2187.webp diff --git a/assets/cases/2188.webp b/assets/containers/2188.webp similarity index 100% rename from assets/cases/2188.webp rename to assets/containers/2188.webp diff --git a/assets/cases/2189.webp b/assets/containers/2189.webp similarity index 100% rename from assets/cases/2189.webp rename to assets/containers/2189.webp diff --git a/assets/cases/219.webp b/assets/containers/219.webp similarity index 100% rename from assets/cases/219.webp rename to assets/containers/219.webp diff --git a/assets/cases/2190.webp b/assets/containers/2190.webp similarity index 100% rename from assets/cases/2190.webp rename to assets/containers/2190.webp diff --git a/assets/cases/2191.webp b/assets/containers/2191.webp similarity index 100% rename from assets/cases/2191.webp rename to assets/containers/2191.webp diff --git a/assets/cases/2192.webp b/assets/containers/2192.webp similarity index 100% rename from assets/cases/2192.webp rename to assets/containers/2192.webp diff --git a/assets/cases/2193.webp b/assets/containers/2193.webp similarity index 100% rename from assets/cases/2193.webp rename to assets/containers/2193.webp diff --git a/assets/cases/2194.webp b/assets/containers/2194.webp similarity index 100% rename from assets/cases/2194.webp rename to assets/containers/2194.webp diff --git a/assets/cases/2195.webp b/assets/containers/2195.webp similarity index 100% rename from assets/cases/2195.webp rename to assets/containers/2195.webp diff --git a/assets/cases/2196.webp b/assets/containers/2196.webp similarity index 100% rename from assets/cases/2196.webp rename to assets/containers/2196.webp diff --git a/assets/cases/2197.webp b/assets/containers/2197.webp similarity index 100% rename from assets/cases/2197.webp rename to assets/containers/2197.webp diff --git a/assets/cases/2198.webp b/assets/containers/2198.webp similarity index 100% rename from assets/cases/2198.webp rename to assets/containers/2198.webp diff --git a/assets/cases/2199.webp b/assets/containers/2199.webp similarity index 100% rename from assets/cases/2199.webp rename to assets/containers/2199.webp diff --git a/assets/cases/22.webp b/assets/containers/22.webp similarity index 100% rename from assets/cases/22.webp rename to assets/containers/22.webp diff --git a/assets/cases/220.webp b/assets/containers/220.webp similarity index 100% rename from assets/cases/220.webp rename to assets/containers/220.webp diff --git a/assets/cases/2200.webp b/assets/containers/2200.webp similarity index 100% rename from assets/cases/2200.webp rename to assets/containers/2200.webp diff --git a/assets/cases/2201.webp b/assets/containers/2201.webp similarity index 100% rename from assets/cases/2201.webp rename to assets/containers/2201.webp diff --git a/assets/cases/2202.webp b/assets/containers/2202.webp similarity index 100% rename from assets/cases/2202.webp rename to assets/containers/2202.webp diff --git a/assets/cases/2203.webp b/assets/containers/2203.webp similarity index 100% rename from assets/cases/2203.webp rename to assets/containers/2203.webp diff --git a/assets/cases/2204.webp b/assets/containers/2204.webp similarity index 100% rename from assets/cases/2204.webp rename to assets/containers/2204.webp diff --git a/assets/cases/2205.webp b/assets/containers/2205.webp similarity index 100% rename from assets/cases/2205.webp rename to assets/containers/2205.webp diff --git a/assets/cases/2206.webp b/assets/containers/2206.webp similarity index 100% rename from assets/cases/2206.webp rename to assets/containers/2206.webp diff --git a/assets/cases/2207.webp b/assets/containers/2207.webp similarity index 100% rename from assets/cases/2207.webp rename to assets/containers/2207.webp diff --git a/assets/cases/2208.webp b/assets/containers/2208.webp similarity index 100% rename from assets/cases/2208.webp rename to assets/containers/2208.webp diff --git a/assets/cases/2209.webp b/assets/containers/2209.webp similarity index 100% rename from assets/cases/2209.webp rename to assets/containers/2209.webp diff --git a/assets/cases/221.webp b/assets/containers/221.webp similarity index 100% rename from assets/cases/221.webp rename to assets/containers/221.webp diff --git a/assets/cases/2210.webp b/assets/containers/2210.webp similarity index 100% rename from assets/cases/2210.webp rename to assets/containers/2210.webp diff --git a/assets/cases/2211.webp b/assets/containers/2211.webp similarity index 100% rename from assets/cases/2211.webp rename to assets/containers/2211.webp diff --git a/assets/cases/2212.webp b/assets/containers/2212.webp similarity index 100% rename from assets/cases/2212.webp rename to assets/containers/2212.webp diff --git a/assets/cases/2213.webp b/assets/containers/2213.webp similarity index 100% rename from assets/cases/2213.webp rename to assets/containers/2213.webp diff --git a/assets/cases/2214.webp b/assets/containers/2214.webp similarity index 100% rename from assets/cases/2214.webp rename to assets/containers/2214.webp diff --git a/assets/cases/2215.webp b/assets/containers/2215.webp similarity index 100% rename from assets/cases/2215.webp rename to assets/containers/2215.webp diff --git a/assets/cases/2216.webp b/assets/containers/2216.webp similarity index 100% rename from assets/cases/2216.webp rename to assets/containers/2216.webp diff --git a/assets/cases/2217.webp b/assets/containers/2217.webp similarity index 100% rename from assets/cases/2217.webp rename to assets/containers/2217.webp diff --git a/assets/cases/2218.webp b/assets/containers/2218.webp similarity index 100% rename from assets/cases/2218.webp rename to assets/containers/2218.webp diff --git a/assets/cases/2219.webp b/assets/containers/2219.webp similarity index 100% rename from assets/cases/2219.webp rename to assets/containers/2219.webp diff --git a/assets/cases/222.webp b/assets/containers/222.webp similarity index 100% rename from assets/cases/222.webp rename to assets/containers/222.webp diff --git a/assets/cases/2220.webp b/assets/containers/2220.webp similarity index 100% rename from assets/cases/2220.webp rename to assets/containers/2220.webp diff --git a/assets/cases/2221.webp b/assets/containers/2221.webp similarity index 100% rename from assets/cases/2221.webp rename to assets/containers/2221.webp diff --git a/assets/cases/2222.webp b/assets/containers/2222.webp similarity index 100% rename from assets/cases/2222.webp rename to assets/containers/2222.webp diff --git a/assets/cases/2223.webp b/assets/containers/2223.webp similarity index 100% rename from assets/cases/2223.webp rename to assets/containers/2223.webp diff --git a/assets/cases/2224.webp b/assets/containers/2224.webp similarity index 100% rename from assets/cases/2224.webp rename to assets/containers/2224.webp diff --git a/assets/cases/2225.webp b/assets/containers/2225.webp similarity index 100% rename from assets/cases/2225.webp rename to assets/containers/2225.webp diff --git a/assets/cases/2226.webp b/assets/containers/2226.webp similarity index 100% rename from assets/cases/2226.webp rename to assets/containers/2226.webp diff --git a/assets/cases/2227.webp b/assets/containers/2227.webp similarity index 100% rename from assets/cases/2227.webp rename to assets/containers/2227.webp diff --git a/assets/cases/2228.webp b/assets/containers/2228.webp similarity index 100% rename from assets/cases/2228.webp rename to assets/containers/2228.webp diff --git a/assets/cases/2229.webp b/assets/containers/2229.webp similarity index 100% rename from assets/cases/2229.webp rename to assets/containers/2229.webp diff --git a/assets/cases/223.webp b/assets/containers/223.webp similarity index 100% rename from assets/cases/223.webp rename to assets/containers/223.webp diff --git a/assets/cases/2230.webp b/assets/containers/2230.webp similarity index 100% rename from assets/cases/2230.webp rename to assets/containers/2230.webp diff --git a/assets/cases/2231.webp b/assets/containers/2231.webp similarity index 100% rename from assets/cases/2231.webp rename to assets/containers/2231.webp diff --git a/assets/cases/2232.webp b/assets/containers/2232.webp similarity index 100% rename from assets/cases/2232.webp rename to assets/containers/2232.webp diff --git a/assets/cases/2233.webp b/assets/containers/2233.webp similarity index 100% rename from assets/cases/2233.webp rename to assets/containers/2233.webp diff --git a/assets/cases/2234.webp b/assets/containers/2234.webp similarity index 100% rename from assets/cases/2234.webp rename to assets/containers/2234.webp diff --git a/assets/cases/2235.webp b/assets/containers/2235.webp similarity index 100% rename from assets/cases/2235.webp rename to assets/containers/2235.webp diff --git a/assets/cases/2236.webp b/assets/containers/2236.webp similarity index 100% rename from assets/cases/2236.webp rename to assets/containers/2236.webp diff --git a/assets/cases/2237.webp b/assets/containers/2237.webp similarity index 100% rename from assets/cases/2237.webp rename to assets/containers/2237.webp diff --git a/assets/cases/2238.webp b/assets/containers/2238.webp similarity index 100% rename from assets/cases/2238.webp rename to assets/containers/2238.webp diff --git a/assets/cases/2239.webp b/assets/containers/2239.webp similarity index 100% rename from assets/cases/2239.webp rename to assets/containers/2239.webp diff --git a/assets/cases/224.webp b/assets/containers/224.webp similarity index 100% rename from assets/cases/224.webp rename to assets/containers/224.webp diff --git a/assets/cases/2240.webp b/assets/containers/2240.webp similarity index 100% rename from assets/cases/2240.webp rename to assets/containers/2240.webp diff --git a/assets/containers/2241.webp b/assets/containers/2241.webp new file mode 100644 index 00000000..c7d878ad Binary files /dev/null and b/assets/containers/2241.webp differ diff --git a/assets/containers/2242.webp b/assets/containers/2242.webp new file mode 100644 index 00000000..263533f7 Binary files /dev/null and b/assets/containers/2242.webp differ diff --git a/assets/containers/2243.webp b/assets/containers/2243.webp new file mode 100644 index 00000000..26570727 Binary files /dev/null and b/assets/containers/2243.webp differ diff --git a/assets/containers/2244.webp b/assets/containers/2244.webp new file mode 100644 index 00000000..c252db5f Binary files /dev/null and b/assets/containers/2244.webp differ diff --git a/assets/cases/225.webp b/assets/containers/225.webp similarity index 100% rename from assets/cases/225.webp rename to assets/containers/225.webp diff --git a/assets/cases/226.webp b/assets/containers/226.webp similarity index 100% rename from assets/cases/226.webp rename to assets/containers/226.webp diff --git a/assets/cases/227.webp b/assets/containers/227.webp similarity index 100% rename from assets/cases/227.webp rename to assets/containers/227.webp diff --git a/assets/cases/228.webp b/assets/containers/228.webp similarity index 100% rename from assets/cases/228.webp rename to assets/containers/228.webp diff --git a/assets/cases/229.webp b/assets/containers/229.webp similarity index 100% rename from assets/cases/229.webp rename to assets/containers/229.webp diff --git a/assets/cases/23.webp b/assets/containers/23.webp similarity index 100% rename from assets/cases/23.webp rename to assets/containers/23.webp diff --git a/assets/cases/230.webp b/assets/containers/230.webp similarity index 100% rename from assets/cases/230.webp rename to assets/containers/230.webp diff --git a/assets/cases/231.webp b/assets/containers/231.webp similarity index 100% rename from assets/cases/231.webp rename to assets/containers/231.webp diff --git a/assets/cases/232.webp b/assets/containers/232.webp similarity index 100% rename from assets/cases/232.webp rename to assets/containers/232.webp diff --git a/assets/cases/233.webp b/assets/containers/233.webp similarity index 100% rename from assets/cases/233.webp rename to assets/containers/233.webp diff --git a/assets/cases/234.webp b/assets/containers/234.webp similarity index 100% rename from assets/cases/234.webp rename to assets/containers/234.webp diff --git a/assets/cases/235.webp b/assets/containers/235.webp similarity index 100% rename from assets/cases/235.webp rename to assets/containers/235.webp diff --git a/assets/cases/236.webp b/assets/containers/236.webp similarity index 100% rename from assets/cases/236.webp rename to assets/containers/236.webp diff --git a/assets/cases/237.webp b/assets/containers/237.webp similarity index 100% rename from assets/cases/237.webp rename to assets/containers/237.webp diff --git a/assets/cases/238.webp b/assets/containers/238.webp similarity index 100% rename from assets/cases/238.webp rename to assets/containers/238.webp diff --git a/assets/cases/239.webp b/assets/containers/239.webp similarity index 100% rename from assets/cases/239.webp rename to assets/containers/239.webp diff --git a/assets/cases/24.webp b/assets/containers/24.webp similarity index 100% rename from assets/cases/24.webp rename to assets/containers/24.webp diff --git a/assets/cases/240.webp b/assets/containers/240.webp similarity index 100% rename from assets/cases/240.webp rename to assets/containers/240.webp diff --git a/assets/cases/241.webp b/assets/containers/241.webp similarity index 100% rename from assets/cases/241.webp rename to assets/containers/241.webp diff --git a/assets/cases/242.webp b/assets/containers/242.webp similarity index 100% rename from assets/cases/242.webp rename to assets/containers/242.webp diff --git a/assets/cases/243.webp b/assets/containers/243.webp similarity index 100% rename from assets/cases/243.webp rename to assets/containers/243.webp diff --git a/assets/cases/244.webp b/assets/containers/244.webp similarity index 100% rename from assets/cases/244.webp rename to assets/containers/244.webp diff --git a/assets/cases/245.webp b/assets/containers/245.webp similarity index 100% rename from assets/cases/245.webp rename to assets/containers/245.webp diff --git a/assets/cases/248.webp b/assets/containers/248.webp similarity index 100% rename from assets/cases/248.webp rename to assets/containers/248.webp diff --git a/assets/cases/25.webp b/assets/containers/25.webp similarity index 100% rename from assets/cases/25.webp rename to assets/containers/25.webp diff --git a/assets/cases/250.webp b/assets/containers/250.webp similarity index 100% rename from assets/cases/250.webp rename to assets/containers/250.webp diff --git a/assets/cases/251.webp b/assets/containers/251.webp similarity index 100% rename from assets/cases/251.webp rename to assets/containers/251.webp diff --git a/assets/cases/252.webp b/assets/containers/252.webp similarity index 100% rename from assets/cases/252.webp rename to assets/containers/252.webp diff --git a/assets/cases/253.webp b/assets/containers/253.webp similarity index 100% rename from assets/cases/253.webp rename to assets/containers/253.webp diff --git a/assets/cases/254.webp b/assets/containers/254.webp similarity index 100% rename from assets/cases/254.webp rename to assets/containers/254.webp diff --git a/assets/cases/255.webp b/assets/containers/255.webp similarity index 100% rename from assets/cases/255.webp rename to assets/containers/255.webp diff --git a/assets/cases/259.webp b/assets/containers/259.webp similarity index 100% rename from assets/cases/259.webp rename to assets/containers/259.webp diff --git a/assets/cases/26.webp b/assets/containers/26.webp similarity index 100% rename from assets/cases/26.webp rename to assets/containers/26.webp diff --git a/assets/cases/260.webp b/assets/containers/260.webp similarity index 100% rename from assets/cases/260.webp rename to assets/containers/260.webp diff --git a/assets/cases/262.webp b/assets/containers/262.webp similarity index 100% rename from assets/cases/262.webp rename to assets/containers/262.webp diff --git a/assets/cases/263.webp b/assets/containers/263.webp similarity index 100% rename from assets/cases/263.webp rename to assets/containers/263.webp diff --git a/assets/cases/264.webp b/assets/containers/264.webp similarity index 100% rename from assets/cases/264.webp rename to assets/containers/264.webp diff --git a/assets/cases/267.webp b/assets/containers/267.webp similarity index 100% rename from assets/cases/267.webp rename to assets/containers/267.webp diff --git a/assets/cases/268.webp b/assets/containers/268.webp similarity index 100% rename from assets/cases/268.webp rename to assets/containers/268.webp diff --git a/assets/cases/269.webp b/assets/containers/269.webp similarity index 100% rename from assets/cases/269.webp rename to assets/containers/269.webp diff --git a/assets/cases/27.webp b/assets/containers/27.webp similarity index 100% rename from assets/cases/27.webp rename to assets/containers/27.webp diff --git a/assets/cases/271.webp b/assets/containers/271.webp similarity index 100% rename from assets/cases/271.webp rename to assets/containers/271.webp diff --git a/assets/cases/272.webp b/assets/containers/272.webp similarity index 100% rename from assets/cases/272.webp rename to assets/containers/272.webp diff --git a/assets/cases/273.webp b/assets/containers/273.webp similarity index 100% rename from assets/cases/273.webp rename to assets/containers/273.webp diff --git a/assets/cases/275.webp b/assets/containers/275.webp similarity index 100% rename from assets/cases/275.webp rename to assets/containers/275.webp diff --git a/assets/cases/276.webp b/assets/containers/276.webp similarity index 100% rename from assets/cases/276.webp rename to assets/containers/276.webp diff --git a/assets/cases/278.webp b/assets/containers/278.webp similarity index 100% rename from assets/cases/278.webp rename to assets/containers/278.webp diff --git a/assets/cases/279.webp b/assets/containers/279.webp similarity index 100% rename from assets/cases/279.webp rename to assets/containers/279.webp diff --git a/assets/cases/280.webp b/assets/containers/280.webp similarity index 100% rename from assets/cases/280.webp rename to assets/containers/280.webp diff --git a/assets/cases/281.webp b/assets/containers/281.webp similarity index 100% rename from assets/cases/281.webp rename to assets/containers/281.webp diff --git a/assets/cases/282.webp b/assets/containers/282.webp similarity index 100% rename from assets/cases/282.webp rename to assets/containers/282.webp diff --git a/assets/cases/283.webp b/assets/containers/283.webp similarity index 100% rename from assets/cases/283.webp rename to assets/containers/283.webp diff --git a/assets/cases/284.webp b/assets/containers/284.webp similarity index 100% rename from assets/cases/284.webp rename to assets/containers/284.webp diff --git a/assets/cases/287.webp b/assets/containers/287.webp similarity index 100% rename from assets/cases/287.webp rename to assets/containers/287.webp diff --git a/assets/cases/288.webp b/assets/containers/288.webp similarity index 100% rename from assets/cases/288.webp rename to assets/containers/288.webp diff --git a/assets/cases/289.webp b/assets/containers/289.webp similarity index 100% rename from assets/cases/289.webp rename to assets/containers/289.webp diff --git a/assets/cases/291.webp b/assets/containers/291.webp similarity index 100% rename from assets/cases/291.webp rename to assets/containers/291.webp diff --git a/assets/cases/292.webp b/assets/containers/292.webp similarity index 100% rename from assets/cases/292.webp rename to assets/containers/292.webp diff --git a/assets/cases/293.webp b/assets/containers/293.webp similarity index 100% rename from assets/cases/293.webp rename to assets/containers/293.webp diff --git a/assets/cases/294.webp b/assets/containers/294.webp similarity index 100% rename from assets/cases/294.webp rename to assets/containers/294.webp diff --git a/assets/cases/295.webp b/assets/containers/295.webp similarity index 100% rename from assets/cases/295.webp rename to assets/containers/295.webp diff --git a/assets/cases/296.webp b/assets/containers/296.webp similarity index 100% rename from assets/cases/296.webp rename to assets/containers/296.webp diff --git a/assets/cases/297.webp b/assets/containers/297.webp similarity index 100% rename from assets/cases/297.webp rename to assets/containers/297.webp diff --git a/assets/cases/298.webp b/assets/containers/298.webp similarity index 100% rename from assets/cases/298.webp rename to assets/containers/298.webp diff --git a/assets/cases/299.webp b/assets/containers/299.webp similarity index 100% rename from assets/cases/299.webp rename to assets/containers/299.webp diff --git a/assets/agent_collections/30001.webp b/assets/containers/30001.webp similarity index 100% rename from assets/agent_collections/30001.webp rename to assets/containers/30001.webp diff --git a/assets/agent_collections/30002.webp b/assets/containers/30002.webp similarity index 100% rename from assets/agent_collections/30002.webp rename to assets/containers/30002.webp diff --git a/assets/agent_collections/30003.webp b/assets/containers/30003.webp similarity index 100% rename from assets/agent_collections/30003.webp rename to assets/containers/30003.webp diff --git a/assets/cases/303.webp b/assets/containers/303.webp similarity index 100% rename from assets/cases/303.webp rename to assets/containers/303.webp diff --git a/assets/cases/304.webp b/assets/containers/304.webp similarity index 100% rename from assets/cases/304.webp rename to assets/containers/304.webp diff --git a/assets/cases/305.webp b/assets/containers/305.webp similarity index 100% rename from assets/cases/305.webp rename to assets/containers/305.webp diff --git a/assets/cases/306.webp b/assets/containers/306.webp similarity index 100% rename from assets/cases/306.webp rename to assets/containers/306.webp diff --git a/assets/cases/307.webp b/assets/containers/307.webp similarity index 100% rename from assets/cases/307.webp rename to assets/containers/307.webp diff --git a/assets/cases/308.webp b/assets/containers/308.webp similarity index 100% rename from assets/cases/308.webp rename to assets/containers/308.webp diff --git a/assets/cases/309.webp b/assets/containers/309.webp similarity index 100% rename from assets/cases/309.webp rename to assets/containers/309.webp diff --git a/assets/cases/310.webp b/assets/containers/310.webp similarity index 100% rename from assets/cases/310.webp rename to assets/containers/310.webp diff --git a/assets/cases/311.webp b/assets/containers/311.webp similarity index 100% rename from assets/cases/311.webp rename to assets/containers/311.webp diff --git a/assets/cases/312.webp b/assets/containers/312.webp similarity index 100% rename from assets/cases/312.webp rename to assets/containers/312.webp diff --git a/assets/cases/318.webp b/assets/containers/318.webp similarity index 100% rename from assets/cases/318.webp rename to assets/containers/318.webp diff --git a/assets/cases/321.webp b/assets/containers/321.webp similarity index 100% rename from assets/cases/321.webp rename to assets/containers/321.webp diff --git a/assets/cases/322.webp b/assets/containers/322.webp similarity index 100% rename from assets/cases/322.webp rename to assets/containers/322.webp diff --git a/assets/cases/323.webp b/assets/containers/323.webp similarity index 100% rename from assets/cases/323.webp rename to assets/containers/323.webp diff --git a/assets/cases/324.webp b/assets/containers/324.webp similarity index 100% rename from assets/cases/324.webp rename to assets/containers/324.webp diff --git a/assets/cases/329.webp b/assets/containers/329.webp similarity index 100% rename from assets/cases/329.webp rename to assets/containers/329.webp diff --git a/assets/cases/33.webp b/assets/containers/33.webp similarity index 100% rename from assets/cases/33.webp rename to assets/containers/33.webp diff --git a/assets/cases/330.webp b/assets/containers/330.webp similarity index 100% rename from assets/cases/330.webp rename to assets/containers/330.webp diff --git a/assets/cases/331.webp b/assets/containers/331.webp similarity index 100% rename from assets/cases/331.webp rename to assets/containers/331.webp diff --git a/assets/cases/332.webp b/assets/containers/332.webp similarity index 100% rename from assets/cases/332.webp rename to assets/containers/332.webp diff --git a/assets/cases/333.webp b/assets/containers/333.webp similarity index 100% rename from assets/cases/333.webp rename to assets/containers/333.webp diff --git a/assets/cases/334.webp b/assets/containers/334.webp similarity index 100% rename from assets/cases/334.webp rename to assets/containers/334.webp diff --git a/assets/cases/34.webp b/assets/containers/34.webp similarity index 100% rename from assets/cases/34.webp rename to assets/containers/34.webp diff --git a/assets/cases/340.webp b/assets/containers/340.webp similarity index 100% rename from assets/cases/340.webp rename to assets/containers/340.webp diff --git a/assets/cases/341.webp b/assets/containers/341.webp similarity index 100% rename from assets/cases/341.webp rename to assets/containers/341.webp diff --git a/assets/cases/344.webp b/assets/containers/344.webp similarity index 100% rename from assets/cases/344.webp rename to assets/containers/344.webp diff --git a/assets/cases/345.webp b/assets/containers/345.webp similarity index 100% rename from assets/cases/345.webp rename to assets/containers/345.webp diff --git a/assets/cases/346.webp b/assets/containers/346.webp similarity index 100% rename from assets/cases/346.webp rename to assets/containers/346.webp diff --git a/assets/cases/347.webp b/assets/containers/347.webp similarity index 100% rename from assets/cases/347.webp rename to assets/containers/347.webp diff --git a/assets/cases/348.webp b/assets/containers/348.webp similarity index 100% rename from assets/cases/348.webp rename to assets/containers/348.webp diff --git a/assets/cases/349.webp b/assets/containers/349.webp similarity index 100% rename from assets/cases/349.webp rename to assets/containers/349.webp diff --git a/assets/cases/350.webp b/assets/containers/350.webp similarity index 100% rename from assets/cases/350.webp rename to assets/containers/350.webp diff --git a/assets/cases/351.webp b/assets/containers/351.webp similarity index 100% rename from assets/cases/351.webp rename to assets/containers/351.webp diff --git a/assets/cases/352.webp b/assets/containers/352.webp similarity index 100% rename from assets/cases/352.webp rename to assets/containers/352.webp diff --git a/assets/cases/358.webp b/assets/containers/358.webp similarity index 100% rename from assets/cases/358.webp rename to assets/containers/358.webp diff --git a/assets/cases/359.webp b/assets/containers/359.webp similarity index 100% rename from assets/cases/359.webp rename to assets/containers/359.webp diff --git a/assets/cases/362.webp b/assets/containers/362.webp similarity index 100% rename from assets/cases/362.webp rename to assets/containers/362.webp diff --git a/assets/cases/363.webp b/assets/containers/363.webp similarity index 100% rename from assets/cases/363.webp rename to assets/containers/363.webp diff --git a/assets/cases/364.webp b/assets/containers/364.webp similarity index 100% rename from assets/cases/364.webp rename to assets/containers/364.webp diff --git a/assets/cases/365.webp b/assets/containers/365.webp similarity index 100% rename from assets/cases/365.webp rename to assets/containers/365.webp diff --git a/assets/cases/368.webp b/assets/containers/368.webp similarity index 100% rename from assets/cases/368.webp rename to assets/containers/368.webp diff --git a/assets/cases/369.webp b/assets/containers/369.webp similarity index 100% rename from assets/cases/369.webp rename to assets/containers/369.webp diff --git a/assets/cases/37.webp b/assets/containers/37.webp similarity index 100% rename from assets/cases/37.webp rename to assets/containers/37.webp diff --git a/assets/cases/370.webp b/assets/containers/370.webp similarity index 100% rename from assets/cases/370.webp rename to assets/containers/370.webp diff --git a/assets/cases/38.webp b/assets/containers/38.webp similarity index 100% rename from assets/cases/38.webp rename to assets/containers/38.webp diff --git a/assets/cases/380.webp b/assets/containers/380.webp similarity index 100% rename from assets/cases/380.webp rename to assets/containers/380.webp diff --git a/assets/cases/384.webp b/assets/containers/384.webp similarity index 100% rename from assets/cases/384.webp rename to assets/containers/384.webp diff --git a/assets/cases/386.webp b/assets/containers/386.webp similarity index 100% rename from assets/cases/386.webp rename to assets/containers/386.webp diff --git a/assets/cases/388.webp b/assets/containers/388.webp similarity index 100% rename from assets/cases/388.webp rename to assets/containers/388.webp diff --git a/assets/cases/389.webp b/assets/containers/389.webp similarity index 100% rename from assets/cases/389.webp rename to assets/containers/389.webp diff --git a/assets/cases/39.webp b/assets/containers/39.webp similarity index 100% rename from assets/cases/39.webp rename to assets/containers/39.webp diff --git a/assets/cases/390.webp b/assets/containers/390.webp similarity index 100% rename from assets/cases/390.webp rename to assets/containers/390.webp diff --git a/assets/cases/391.webp b/assets/containers/391.webp similarity index 100% rename from assets/cases/391.webp rename to assets/containers/391.webp diff --git a/assets/cases/4.webp b/assets/containers/4.webp similarity index 100% rename from assets/cases/4.webp rename to assets/containers/4.webp diff --git a/assets/cases/401.webp b/assets/containers/401.webp similarity index 100% rename from assets/cases/401.webp rename to assets/containers/401.webp diff --git a/assets/cases/402.webp b/assets/containers/402.webp similarity index 100% rename from assets/cases/402.webp rename to assets/containers/402.webp diff --git a/assets/cases/403.webp b/assets/containers/403.webp similarity index 100% rename from assets/cases/403.webp rename to assets/containers/403.webp diff --git a/assets/cases/404.webp b/assets/containers/404.webp similarity index 100% rename from assets/cases/404.webp rename to assets/containers/404.webp diff --git a/assets/cases/405.webp b/assets/containers/405.webp similarity index 100% rename from assets/cases/405.webp rename to assets/containers/405.webp diff --git a/assets/cases/406.png b/assets/containers/406.png similarity index 100% rename from assets/cases/406.png rename to assets/containers/406.png diff --git a/assets/cases/452.png b/assets/containers/452.png similarity index 100% rename from assets/cases/452.png rename to assets/containers/452.png diff --git a/assets/cases/452.webp b/assets/containers/452.webp similarity index 100% rename from assets/cases/452.webp rename to assets/containers/452.webp diff --git a/assets/data/agent_collections.json b/assets/data/agent_collections.json deleted file mode 100644 index 77ce4d2d..00000000 --- a/assets/data/agent_collections.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "id": "30003", - "name": "Shattered Web Agents", - "image": "assets/agent_collections/30003.webp", - "operationId": "SHATTERED_WEB", - "operationName": "Operation Shattered Web", - "releaseDate": "2019-11-18" - }, - { - "id": "30002", - "name": "Broken Fang Agents", - "image": "assets/agent_collections/30002.webp", - "operationId": "BROKEN_FANG", - "operationName": "Operation Broken Fang", - "releaseDate": "2020-12-03" - }, - { - "id": "30001", - "name": "Operation Riptide Agents", - "image": "assets/agent_collections/30001.webp", - "operationId": "RIPTIDE", - "operationName": "Operation Riptide", - "releaseDate": "2021-09-21" - } -] diff --git a/assets/data/charm_contents.json b/assets/data/charm_contents.json new file mode 100644 index 00000000..4872796f --- /dev/null +++ b/assets/data/charm_contents.json @@ -0,0 +1,100 @@ +[ + { + "containerId": "2241", + "charmIds": [ + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "80", + "81", + "82" + ] + }, + { + "containerId": "2242", + "charmIds": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "33" + ] + }, + { + "containerId": "2243", + "charmIds": [ + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "60" + ] + }, + { + "containerId": "2244", + "charmIds": [ + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32" + ] + } +] diff --git a/assets/data/charms.json b/assets/data/charms.json new file mode 100644 index 00000000..24e0dc99 --- /dev/null +++ b/assets/data/charms.json @@ -0,0 +1,548 @@ +[ + { + "id": "1", + "name": "Lil' Ava", + "charmImage": "assets/charms/1.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "2", + "name": "That's Bananas", + "charmImage": "assets/charms/2.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "3", + "name": "Lil' Whiskers", + "charmImage": "assets/charms/3.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "4", + "name": "Lil' Sandy", + "charmImage": "assets/charms/4.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "5", + "name": "Chicken Lil'", + "charmImage": "assets/charms/5.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "6", + "name": "Lil' Crass", + "charmImage": "assets/charms/6.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "7", + "name": "Hot Howl", + "charmImage": "assets/charms/7.webp", + "rarity": "EXTRAORDINARY", + "collection": "Missing Link Charm Collection" + }, + { + "id": "8", + "name": "Big Kev", + "charmImage": "assets/charms/8.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "9", + "name": "Lil' Monster", + "charmImage": "assets/charms/9.webp", + "rarity": "EXOTIC", + "collection": "Missing Link Charm Collection" + }, + { + "id": "10", + "name": "Hot Sauce", + "charmImage": "assets/charms/10.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "11", + "name": "Diamond Dog", + "charmImage": "assets/charms/11.webp", + "rarity": "EXOTIC", + "collection": "Missing Link Charm Collection" + }, + { + "id": "12", + "name": "Pinch O' Salt", + "charmImage": "assets/charms/12.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "13", + "name": "Diner Dog", + "charmImage": "assets/charms/13.webp", + "rarity": "EXOTIC", + "collection": "Missing Link Charm Collection" + }, + { + "id": "14", + "name": "Lil' Teacup", + "charmImage": "assets/charms/14.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "15", + "name": "Lil' SAS", + "charmImage": "assets/charms/15.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "16", + "name": "Hot Wurst", + "charmImage": "assets/charms/16.webp", + "rarity": "EXTRAORDINARY", + "collection": "Missing Link Charm Collection" + }, + { + "id": "17", + "name": "Baby's AK", + "charmImage": "assets/charms/17.webp", + "rarity": "HIGH_GRADE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "18", + "name": "Die-cast AK", + "charmImage": "assets/charms/18.webp", + "rarity": "REMARKABLE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "19", + "name": "Pocket AWP", + "charmImage": "assets/charms/19.webp", + "rarity": "HIGH_GRADE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "20", + "name": "Titeenium AWP", + "charmImage": "assets/charms/20.webp", + "rarity": "EXOTIC", + "collection": "Small Arms Charm Collection" + }, + { + "id": "21", + "name": "Baby Karat CT", + "charmImage": "assets/charms/21.webp", + "rarity": "EXTRAORDINARY", + "collection": "Small Arms Charm Collection" + }, + { + "id": "22", + "name": "Whittle Knife", + "charmImage": "assets/charms/22.webp", + "rarity": "HIGH_GRADE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "23", + "name": "POP Art", + "charmImage": "assets/charms/23.webp", + "rarity": "REMARKABLE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "24", + "name": "Lil' Squirt", + "charmImage": "assets/charms/24.webp", + "rarity": "EXOTIC", + "collection": "Small Arms Charm Collection" + }, + { + "id": "25", + "name": "Disco MAC", + "charmImage": "assets/charms/25.webp", + "rarity": "REMARKABLE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "26", + "name": "Backsplash", + "charmImage": "assets/charms/26.webp", + "rarity": "HIGH_GRADE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "27", + "name": "Lil' Cap Gun", + "charmImage": "assets/charms/27.webp", + "rarity": "HIGH_GRADE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "28", + "name": "Hot Hands", + "charmImage": "assets/charms/28.webp", + "rarity": "REMARKABLE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "29", + "name": "Semi-Precious", + "charmImage": "assets/charms/29.webp", + "rarity": "EXOTIC", + "collection": "Small Arms Charm Collection" + }, + { + "id": "30", + "name": "Baby Karat T", + "charmImage": "assets/charms/30.webp", + "rarity": "EXTRAORDINARY", + "collection": "Small Arms Charm Collection" + }, + { + "id": "31", + "name": "Glamour Shot", + "charmImage": "assets/charms/31.webp", + "rarity": "REMARKABLE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "32", + "name": "Stitch-Loaded", + "charmImage": "assets/charms/32.webp", + "rarity": "HIGH_GRADE", + "collection": "Small Arms Charm Collection" + }, + { + "id": "33", + "name": "Lil' Squatch", + "charmImage": "assets/charms/33.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Charm Collection" + }, + { + "id": "38", + "name": "Lil' Curse", + "charmImage": "assets/charms/38.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "39", + "name": "Lil' Serpent", + "charmImage": "assets/charms/39.webp", + "rarity": "EXTRAORDINARY", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "40", + "name": "Lil' Baller", + "charmImage": "assets/charms/40.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "41", + "name": "Dead Weight", + "charmImage": "assets/charms/41.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "42", + "name": "Lil' Prick", + "charmImage": "assets/charms/42.webp", + "rarity": "EXOTIC", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "43", + "name": "Lil' Buns", + "charmImage": "assets/charms/43.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "44", + "name": "Lil' Smokey", + "charmImage": "assets/charms/44.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "45", + "name": "Lil' Eldritch", + "charmImage": "assets/charms/45.webp", + "rarity": "EXTRAORDINARY", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "46", + "name": "Lil' Happy", + "charmImage": "assets/charms/46.webp", + "rarity": "EXOTIC", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "47", + "name": "Magmatude", + "charmImage": "assets/charms/47.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "48", + "name": "Lil' Cackle", + "charmImage": "assets/charms/48.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "49", + "name": "Quick Silver", + "charmImage": "assets/charms/49.webp", + "rarity": "EXTRAORDINARY", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "50", + "name": "Lil' No. 2", + "charmImage": "assets/charms/50.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "51", + "name": "PiƱatita", + "charmImage": "assets/charms/51.webp", + "rarity": "EXOTIC", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "52", + "name": "Pocket Pop", + "charmImage": "assets/charms/52.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "53", + "name": "Lil' Hero", + "charmImage": "assets/charms/53.webp", + "rarity": "EXOTIC", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "54", + "name": "Lil' Tusk", + "charmImage": "assets/charms/54.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "55", + "name": "Lil' Goop", + "charmImage": "assets/charms/55.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "56", + "name": "Hang Loose", + "charmImage": "assets/charms/56.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "57", + "name": "Lil' Vino", + "charmImage": "assets/charms/57.webp", + "rarity": "HIGH_GRADE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "58", + "name": "Lil' Moments", + "charmImage": "assets/charms/58.webp", + "rarity": "REMARKABLE", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "59", + "name": "Lil' Boo", + "charmImage": "assets/charms/59.webp", + "rarity": "EXTRAORDINARY", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "60", + "name": "Lil' Chirp", + "charmImage": "assets/charms/60.webp", + "rarity": "EXOTIC", + "collection": "Missing Link Community Charm Collection" + }, + { + "id": "61", + "name": "Big Brain", + "charmImage": "assets/charms/61.webp", + "rarity": "REMARKABLE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "62", + "name": "Biomech", + "charmImage": "assets/charms/62.webp", + "rarity": "HIGH_GRADE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "63", + "name": "Splatter Cat", + "charmImage": "assets/charms/63.webp", + "rarity": "HIGH_GRADE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "64", + "name": "Gritty", + "charmImage": "assets/charms/64.webp", + "rarity": "HIGH_GRADE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "65", + "name": "Fluffy", + "charmImage": "assets/charms/65.webp", + "rarity": "HIGH_GRADE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "66", + "name": "Whittle Guy", + "charmImage": "assets/charms/66.webp", + "rarity": "HIGH_GRADE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "67", + "name": "Lil' Zen", + "charmImage": "assets/charms/67.webp", + "rarity": "HIGH_GRADE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "68", + "name": "Lil' Facelift", + "charmImage": "assets/charms/68.webp", + "rarity": "REMARKABLE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "69", + "name": "Lil' Chomper", + "charmImage": "assets/charms/69.webp", + "rarity": "REMARKABLE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "70", + "name": "Lil' Bloody", + "charmImage": "assets/charms/70.webp", + "rarity": "REMARKABLE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "71", + "name": "Lil' Dumplin'", + "charmImage": "assets/charms/71.webp", + "rarity": "REMARKABLE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "72", + "name": "Bomb Tag", + "charmImage": "assets/charms/72.webp", + "rarity": "REMARKABLE", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "73", + "name": "Glitter Bomb", + "charmImage": "assets/charms/73.webp", + "rarity": "EXTRAORDINARY", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "74", + "name": "Lil' Eco", + "charmImage": "assets/charms/74.webp", + "rarity": "EXOTIC", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "75", + "name": "Hungry Eyes", + "charmImage": "assets/charms/75.webp", + "rarity": "EXOTIC", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "76", + "name": "Eye of Ball", + "charmImage": "assets/charms/76.webp", + "rarity": "EXOTIC", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "77", + "name": "Lil' Yeti", + "charmImage": "assets/charms/77.webp", + "rarity": "EXOTIC", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "78", + "name": "8 Ball IGL", + "charmImage": "assets/charms/78.webp", + "rarity": "EXTRAORDINARY", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "79", + "name": "Flash Bomb", + "charmImage": "assets/charms/79.webp", + "rarity": "EXOTIC", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "80", + "name": "Lil' Ferno", + "charmImage": "assets/charms/80.webp", + "rarity": "EXTRAORDINARY", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "81", + "name": "Butane Buddy", + "charmImage": "assets/charms/81.webp", + "rarity": "EXTRAORDINARY", + "collection": "Dr Boom Charm Collection" + }, + { + "id": "82", + "name": "Dr. Brian", + "charmImage": "assets/charms/82.webp", + "rarity": "REMARKABLE", + "collection": "Dr Boom Charm Collection" + } +] diff --git a/assets/data/case_contents.json b/assets/data/container_contents.json similarity index 94% rename from assets/data/case_contents.json rename to assets/data/container_contents.json index e2b845e6..f7225757 100644 --- a/assets/data/case_contents.json +++ b/assets/data/container_contents.json @@ -1,6 +1,6 @@ [ { - "caseId": "4", + "containerId": "4", "skinIds": [ "14827", "214965", @@ -24,7 +24,7 @@ ] }, { - "caseId": "10", + "containerId": "10", "skinIds": [ "64266", "294349", @@ -48,7 +48,7 @@ ] }, { - "caseId": "11", + "containerId": "11", "skinIds": [ "12557", "27365", @@ -71,7 +71,7 @@ ] }, { - "caseId": "14", + "containerId": "14", "skinIds": [ "943", "68840", @@ -95,7 +95,7 @@ ] }, { - "caseId": "15", + "containerId": "15", "skinIds": [ "46800", "50561", @@ -118,7 +118,7 @@ ] }, { - "caseId": "16", + "containerId": "16", "skinIds": [ "65787", "119597", @@ -138,7 +138,7 @@ ] }, { - "caseId": "17", + "containerId": "17", "skinIds": [ "5092", "121609", @@ -162,7 +162,7 @@ ] }, { - "caseId": "18", + "containerId": "18", "skinIds": [ "98", "9349", @@ -186,7 +186,7 @@ ] }, { - "caseId": "19", + "containerId": "19", "skinIds": [ "93845", "908333", @@ -204,7 +204,7 @@ ] }, { - "caseId": "20", + "containerId": "20", "skinIds": [ "4307", "34719", @@ -224,7 +224,7 @@ ] }, { - "caseId": "21", + "containerId": "21", "skinIds": [ "3433", "17176", @@ -245,7 +245,7 @@ ] }, { - "caseId": "22", + "containerId": "22", "skinIds": [ "12367", "292235", @@ -265,7 +265,7 @@ ] }, { - "caseId": "23", + "containerId": "23", "skinIds": [ "703071", "2369670", @@ -279,7 +279,7 @@ ] }, { - "caseId": "24", + "containerId": "24", "skinIds": [ "65787", "119597", @@ -299,7 +299,7 @@ ] }, { - "caseId": "25", + "containerId": "25", "skinIds": [ "5578", "44994", @@ -319,7 +319,7 @@ ] }, { - "caseId": "26", + "containerId": "26", "skinIds": [ "14827", "214965", @@ -343,7 +343,7 @@ ] }, { - "caseId": "27", + "containerId": "27", "skinIds": [ "98", "9349", @@ -367,7 +367,7 @@ ] }, { - "caseId": "33", + "containerId": "33", "skinIds": [ "64266", "294349", @@ -391,7 +391,7 @@ ] }, { - "caseId": "34", + "containerId": "34", "skinIds": [ "12557", "27365", @@ -414,7 +414,7 @@ ] }, { - "caseId": "37", + "containerId": "37", "skinIds": [ "943", "68840", @@ -438,7 +438,7 @@ ] }, { - "caseId": "38", + "containerId": "38", "skinIds": [ "46800", "50561", @@ -461,7 +461,7 @@ ] }, { - "caseId": "39", + "containerId": "39", "skinIds": [ "1623", "96925", @@ -482,7 +482,7 @@ ] }, { - "caseId": "133", + "containerId": "133", "skinIds": [ "3433", "17176", @@ -503,7 +503,7 @@ ] }, { - "caseId": "134", + "containerId": "134", "skinIds": [ "12557", "27365", @@ -526,7 +526,7 @@ ] }, { - "caseId": "137", + "containerId": "137", "skinIds": [ "12367", "292235", @@ -546,7 +546,7 @@ ] }, { - "caseId": "138", + "containerId": "138", "skinIds": [ "46800", "50561", @@ -569,7 +569,7 @@ ] }, { - "caseId": "139", + "containerId": "139", "skinIds": [ "65787", "119597", @@ -589,7 +589,7 @@ ] }, { - "caseId": "141", + "containerId": "141", "skinIds": [ "5578", "44994", @@ -609,7 +609,7 @@ ] }, { - "caseId": "142", + "containerId": "142", "skinIds": [ "487239", "1933708", @@ -620,7 +620,7 @@ ] }, { - "caseId": "145", + "containerId": "145", "skinIds": [ "93845", "908333", @@ -638,7 +638,7 @@ ] }, { - "caseId": "146", + "containerId": "146", "skinIds": [ "4307", "34719", @@ -658,7 +658,7 @@ ] }, { - "caseId": "147", + "containerId": "147", "skinIds": [ "427", "62457", @@ -669,7 +669,7 @@ ] }, { - "caseId": "151", + "containerId": "151", "skinIds": [ "12367", "292235", @@ -689,7 +689,7 @@ ] }, { - "caseId": "152", + "containerId": "152", "skinIds": [ "703071", "2369670", @@ -703,7 +703,7 @@ ] }, { - "caseId": "153", + "containerId": "153", "skinIds": [ "65787", "119597", @@ -723,7 +723,7 @@ ] }, { - "caseId": "155", + "containerId": "155", "skinIds": [ "5578", "44994", @@ -743,7 +743,7 @@ ] }, { - "caseId": "156", + "containerId": "156", "skinIds": [ "14827", "214965", @@ -767,7 +767,7 @@ ] }, { - "caseId": "162", + "containerId": "162", "skinIds": [ "64266", "294349", @@ -791,7 +791,7 @@ ] }, { - "caseId": "163", + "containerId": "163", "skinIds": [ "12557", "27365", @@ -814,7 +814,7 @@ ] }, { - "caseId": "166", + "containerId": "166", "skinIds": [ "943", "68840", @@ -838,7 +838,7 @@ ] }, { - "caseId": "167", + "containerId": "167", "skinIds": [ "46800", "50561", @@ -861,7 +861,7 @@ ] }, { - "caseId": "168", + "containerId": "168", "skinIds": [ "49500", "76663", @@ -882,7 +882,7 @@ ] }, { - "caseId": "169", + "containerId": "169", "skinIds": [ "1623", "96925", @@ -903,7 +903,7 @@ ] }, { - "caseId": "170", + "containerId": "170", "skinIds": [ "37954", "39692", @@ -937,7 +937,7 @@ ] }, { - "caseId": "172", + "containerId": "172", "skinIds": [ "66494", "73365", @@ -1011,7 +1011,7 @@ ] }, { - "caseId": "173", + "containerId": "173", "skinIds": [ "33350", "51490", @@ -1088,7 +1088,7 @@ ] }, { - "caseId": "174", + "containerId": "174", "skinIds": [ "66494", "73365", @@ -1165,7 +1165,7 @@ ] }, { - "caseId": "176", + "containerId": "176", "skinIds": [ "2607", "7787", @@ -1245,7 +1245,7 @@ ] }, { - "caseId": "177", + "containerId": "177", "skinIds": [ "2607", "7055", @@ -1327,7 +1327,7 @@ ] }, { - "caseId": "178", + "containerId": "178", "skinIds": [ "2607", "7787", @@ -1406,7 +1406,7 @@ ] }, { - "caseId": "179", + "containerId": "179", "skinIds": [ "73", "892", @@ -1452,7 +1452,7 @@ ] }, { - "caseId": "183", + "containerId": "183", "skinIds": [ "93845", "908333", @@ -1470,7 +1470,7 @@ ] }, { - "caseId": "184", + "containerId": "184", "skinIds": [ "4307", "34719", @@ -1490,7 +1490,7 @@ ] }, { - "caseId": "185", + "containerId": "185", "skinIds": [ "3433", "17176", @@ -1511,7 +1511,7 @@ ] }, { - "caseId": "186", + "containerId": "186", "skinIds": [ "12367", "292235", @@ -1531,7 +1531,7 @@ ] }, { - "caseId": "187", + "containerId": "187", "skinIds": [ "703071", "2369670", @@ -1545,7 +1545,7 @@ ] }, { - "caseId": "188", + "containerId": "188", "skinIds": [ "65787", "119597", @@ -1565,7 +1565,7 @@ ] }, { - "caseId": "189", + "containerId": "189", "skinIds": [ "5578", "44994", @@ -1585,7 +1585,7 @@ ] }, { - "caseId": "192", + "containerId": "192", "skinIds": [ "14827", "214965", @@ -1609,7 +1609,7 @@ ] }, { - "caseId": "193", + "containerId": "193", "skinIds": [ "98", "9349", @@ -1633,7 +1633,7 @@ ] }, { - "caseId": "199", + "containerId": "199", "skinIds": [ "12557", "27365", @@ -1656,7 +1656,7 @@ ] }, { - "caseId": "202", + "containerId": "202", "skinIds": [ "943", "68840", @@ -1680,7 +1680,7 @@ ] }, { - "caseId": "203", + "containerId": "203", "skinIds": [ "46800", "50561", @@ -1703,7 +1703,7 @@ ] }, { - "caseId": "204", + "containerId": "204", "skinIds": [ "65787", "119597", @@ -1723,7 +1723,7 @@ ] }, { - "caseId": "205", + "containerId": "205", "skinIds": [ "5092", "121609", @@ -1747,7 +1747,7 @@ ] }, { - "caseId": "206", + "containerId": "206", "skinIds": [ "419", "741", @@ -1817,7 +1817,7 @@ ] }, { - "caseId": "208", + "containerId": "208", "skinIds": [ "1132", "3433", @@ -1914,7 +1914,7 @@ ] }, { - "caseId": "209", + "containerId": "209", "skinIds": [ "93845", "908333", @@ -1932,7 +1932,7 @@ ] }, { - "caseId": "210", + "containerId": "210", "skinIds": [ "4307", "34719", @@ -1952,7 +1952,7 @@ ] }, { - "caseId": "211", + "containerId": "211", "skinIds": [ "3433", "17176", @@ -1973,7 +1973,7 @@ ] }, { - "caseId": "212", + "containerId": "212", "skinIds": [ "427", "62457", @@ -1984,7 +1984,7 @@ ] }, { - "caseId": "213", + "containerId": "213", "skinIds": [ "12367", "292235", @@ -2004,7 +2004,7 @@ ] }, { - "caseId": "214", + "containerId": "214", "skinIds": [ "703071", "2369670", @@ -2018,7 +2018,7 @@ ] }, { - "caseId": "215", + "containerId": "215", "skinIds": [ "65787", "119597", @@ -2038,7 +2038,7 @@ ] }, { - "caseId": "216", + "containerId": "216", "skinIds": [ "93845", "908333", @@ -2056,7 +2056,7 @@ ] }, { - "caseId": "217", + "containerId": "217", "skinIds": [ "4307", "34719", @@ -2076,7 +2076,7 @@ ] }, { - "caseId": "218", + "containerId": "218", "skinIds": [ "3433", "17176", @@ -2097,7 +2097,7 @@ ] }, { - "caseId": "219", + "containerId": "219", "skinIds": [ "427", "62457", @@ -2108,7 +2108,7 @@ ] }, { - "caseId": "220", + "containerId": "220", "skinIds": [ "12367", "292235", @@ -2128,7 +2128,7 @@ ] }, { - "caseId": "221", + "containerId": "221", "skinIds": [ "65787", "119597", @@ -2148,7 +2148,7 @@ ] }, { - "caseId": "222", + "containerId": "222", "skinIds": [ "5578", "44994", @@ -2168,7 +2168,7 @@ ] }, { - "caseId": "223", + "containerId": "223", "skinIds": [ "79", "515", @@ -2240,7 +2240,7 @@ ] }, { - "caseId": "224", + "containerId": "224", "skinIds": [ "1132", "3433", @@ -2336,7 +2336,7 @@ ] }, { - "caseId": "225", + "containerId": "225", "skinIds": [ "93845", "908333", @@ -2354,7 +2354,7 @@ ] }, { - "caseId": "226", + "containerId": "226", "skinIds": [ "4307", "34719", @@ -2374,7 +2374,7 @@ ] }, { - "caseId": "227", + "containerId": "227", "skinIds": [ "3433", "17176", @@ -2395,7 +2395,7 @@ ] }, { - "caseId": "228", + "containerId": "228", "skinIds": [ "427", "62457", @@ -2406,7 +2406,7 @@ ] }, { - "caseId": "229", + "containerId": "229", "skinIds": [ "12367", "292235", @@ -2426,7 +2426,7 @@ ] }, { - "caseId": "230", + "containerId": "230", "skinIds": [ "703071", "2369670", @@ -2440,7 +2440,7 @@ ] }, { - "caseId": "231", + "containerId": "231", "skinIds": [ "65787", "119597", @@ -2460,7 +2460,7 @@ ] }, { - "caseId": "232", + "containerId": "232", "skinIds": [ "93845", "908333", @@ -2478,7 +2478,7 @@ ] }, { - "caseId": "233", + "containerId": "233", "skinIds": [ "4307", "34719", @@ -2498,7 +2498,7 @@ ] }, { - "caseId": "234", + "containerId": "234", "skinIds": [ "3433", "17176", @@ -2519,7 +2519,7 @@ ] }, { - "caseId": "235", + "containerId": "235", "skinIds": [ "427", "62457", @@ -2530,7 +2530,7 @@ ] }, { - "caseId": "236", + "containerId": "236", "skinIds": [ "12367", "292235", @@ -2550,7 +2550,7 @@ ] }, { - "caseId": "237", + "containerId": "237", "skinIds": [ "65787", "119597", @@ -2570,7 +2570,7 @@ ] }, { - "caseId": "238", + "containerId": "238", "skinIds": [ "5578", "44994", @@ -2590,7 +2590,7 @@ ] }, { - "caseId": "239", + "containerId": "239", "skinIds": [ "93845", "908333", @@ -2608,7 +2608,7 @@ ] }, { - "caseId": "240", + "containerId": "240", "skinIds": [ "4307", "34719", @@ -2628,7 +2628,7 @@ ] }, { - "caseId": "241", + "containerId": "241", "skinIds": [ "3433", "17176", @@ -2649,7 +2649,7 @@ ] }, { - "caseId": "242", + "containerId": "242", "skinIds": [ "427", "62457", @@ -2660,7 +2660,7 @@ ] }, { - "caseId": "243", + "containerId": "243", "skinIds": [ "12367", "292235", @@ -2680,7 +2680,7 @@ ] }, { - "caseId": "244", + "containerId": "244", "skinIds": [ "703071", "2369670", @@ -2694,7 +2694,7 @@ ] }, { - "caseId": "245", + "containerId": "245", "skinIds": [ "65787", "119597", @@ -2714,7 +2714,7 @@ ] }, { - "caseId": "248", + "containerId": "248", "skinIds": [ "8654", "17996", @@ -2747,7 +2747,7 @@ ] }, { - "caseId": "250", + "containerId": "250", "skinIds": [ "87", "88", @@ -2817,7 +2817,7 @@ ] }, { - "caseId": "251", + "containerId": "251", "skinIds": [ "2920", "4086", @@ -2887,7 +2887,7 @@ ] }, { - "caseId": "252", + "containerId": "252", "skinIds": [ "7124", "24690", @@ -2921,7 +2921,7 @@ ] }, { - "caseId": "253", + "containerId": "253", "skinIds": [ "292", "579", @@ -2993,7 +2993,7 @@ ] }, { - "caseId": "254", + "containerId": "254", "skinIds": [ "292", "673", @@ -3065,7 +3065,7 @@ ] }, { - "caseId": "255", + "containerId": "255", "skinIds": [ "1760", "5240", @@ -3111,7 +3111,7 @@ ] }, { - "caseId": "259", + "containerId": "259", "skinIds": [ "419", "741", @@ -3181,7 +3181,7 @@ ] }, { - "caseId": "260", + "containerId": "260", "skinIds": [ "510", "76917", @@ -3213,7 +3213,7 @@ ] }, { - "caseId": "262", + "containerId": "262", "skinIds": [ "93845", "908333", @@ -3231,7 +3231,7 @@ ] }, { - "caseId": "263", + "containerId": "263", "skinIds": [ "3433", "17176", @@ -3252,7 +3252,7 @@ ] }, { - "caseId": "264", + "containerId": "264", "skinIds": [ "12557", "27365", @@ -3275,7 +3275,7 @@ ] }, { - "caseId": "267", + "containerId": "267", "skinIds": [ "12367", "292235", @@ -3295,7 +3295,7 @@ ] }, { - "caseId": "268", + "containerId": "268", "skinIds": [ "46800", "50561", @@ -3318,7 +3318,7 @@ ] }, { - "caseId": "269", + "containerId": "269", "skinIds": [ "65787", "119597", @@ -3338,7 +3338,7 @@ ] }, { - "caseId": "271", + "containerId": "271", "skinIds": [ "5578", "44994", @@ -3358,7 +3358,7 @@ ] }, { - "caseId": "272", + "containerId": "272", "skinIds": [ "13189", "24690", @@ -3392,7 +3392,7 @@ ] }, { - "caseId": "273", + "containerId": "273", "skinIds": [ "93845", "908333", @@ -3410,7 +3410,7 @@ ] }, { - "caseId": "275", + "containerId": "275", "skinIds": [ "4307", "34719", @@ -3430,7 +3430,7 @@ ] }, { - "caseId": "276", + "containerId": "276", "skinIds": [ "427", "62457", @@ -3441,7 +3441,7 @@ ] }, { - "caseId": "278", + "containerId": "278", "skinIds": [ "12367", "292235", @@ -3461,7 +3461,7 @@ ] }, { - "caseId": "279", + "containerId": "279", "skinIds": [ "703071", "2369670", @@ -3475,7 +3475,7 @@ ] }, { - "caseId": "280", + "containerId": "280", "skinIds": [ "65787", "119597", @@ -3495,7 +3495,7 @@ ] }, { - "caseId": "281", + "containerId": "281", "skinIds": [ "5578", "44994", @@ -3515,7 +3515,7 @@ ] }, { - "caseId": "282", + "containerId": "282", "skinIds": [ "93845", "908333", @@ -3533,7 +3533,7 @@ ] }, { - "caseId": "283", + "containerId": "283", "skinIds": [ "3433", "17176", @@ -3554,7 +3554,7 @@ ] }, { - "caseId": "284", + "containerId": "284", "skinIds": [ "12557", "27365", @@ -3577,7 +3577,7 @@ ] }, { - "caseId": "287", + "containerId": "287", "skinIds": [ "12367", "292235", @@ -3597,7 +3597,7 @@ ] }, { - "caseId": "288", + "containerId": "288", "skinIds": [ "46800", "50561", @@ -3620,7 +3620,7 @@ ] }, { - "caseId": "289", + "containerId": "289", "skinIds": [ "65787", "119597", @@ -3640,7 +3640,7 @@ ] }, { - "caseId": "291", + "containerId": "291", "skinIds": [ "5578", "44994", @@ -3660,7 +3660,7 @@ ] }, { - "caseId": "292", + "containerId": "292", "skinIds": [ "93845", "908333", @@ -3678,7 +3678,7 @@ ] }, { - "caseId": "293", + "containerId": "293", "skinIds": [ "4307", "34719", @@ -3698,7 +3698,7 @@ ] }, { - "caseId": "294", + "containerId": "294", "skinIds": [ "3433", "17176", @@ -3719,7 +3719,7 @@ ] }, { - "caseId": "295", + "containerId": "295", "skinIds": [ "427", "62457", @@ -3730,7 +3730,7 @@ ] }, { - "caseId": "296", + "containerId": "296", "skinIds": [ "12367", "292235", @@ -3750,7 +3750,7 @@ ] }, { - "caseId": "297", + "containerId": "297", "skinIds": [ "703071", "2369670", @@ -3764,7 +3764,7 @@ ] }, { - "caseId": "298", + "containerId": "298", "skinIds": [ "65787", "119597", @@ -3784,7 +3784,7 @@ ] }, { - "caseId": "299", + "containerId": "299", "skinIds": [ "5578", "44994", @@ -3804,7 +3804,7 @@ ] }, { - "caseId": "303", + "containerId": "303", "skinIds": [ "40122", "44519", @@ -3884,7 +3884,7 @@ ] }, { - "caseId": "304", + "containerId": "304", "skinIds": [ "1924", "4675", @@ -3915,7 +3915,7 @@ ] }, { - "caseId": "305", + "containerId": "305", "skinIds": [ "22157", "22597", @@ -3961,7 +3961,7 @@ ] }, { - "caseId": "306", + "containerId": "306", "skinIds": [ "2447", "3289", @@ -4007,7 +4007,7 @@ ] }, { - "caseId": "307", + "containerId": "307", "skinIds": [ "18", "40148", @@ -4085,7 +4085,7 @@ ] }, { - "caseId": "308", + "containerId": "308", "skinIds": [ "79", "836", @@ -4157,7 +4157,7 @@ ] }, { - "caseId": "309", + "containerId": "309", "skinIds": [ "26634", "66494", @@ -4236,7 +4236,7 @@ ] }, { - "caseId": "310", + "containerId": "310", "skinIds": [ "28557", "33946", @@ -4269,7 +4269,7 @@ ] }, { - "caseId": "311", + "containerId": "311", "skinIds": [ "14827", "214965", @@ -4293,7 +4293,7 @@ ] }, { - "caseId": "312", + "containerId": "312", "skinIds": [ "98", "9349", @@ -4317,7 +4317,7 @@ ] }, { - "caseId": "318", + "containerId": "318", "skinIds": [ "12557", "27365", @@ -4340,7 +4340,7 @@ ] }, { - "caseId": "321", + "containerId": "321", "skinIds": [ "943", "68840", @@ -4364,7 +4364,7 @@ ] }, { - "caseId": "322", + "containerId": "322", "skinIds": [ "46800", "50561", @@ -4387,7 +4387,7 @@ ] }, { - "caseId": "323", + "containerId": "323", "skinIds": [ "65787", "119597", @@ -4407,7 +4407,7 @@ ] }, { - "caseId": "324", + "containerId": "324", "skinIds": [ "5092", "121609", @@ -4431,7 +4431,7 @@ ] }, { - "caseId": "329", + "containerId": "329", "skinIds": [ "452", "2484", @@ -4501,7 +4501,7 @@ ] }, { - "caseId": "330", + "containerId": "330", "skinIds": [ "2484", "6661", @@ -4571,7 +4571,7 @@ ] }, { - "caseId": "331", + "containerId": "331", "skinIds": [ "3724", "4959", @@ -4617,7 +4617,7 @@ ] }, { - "caseId": "332", + "containerId": "332", "skinIds": [ "892", "1395", @@ -4663,7 +4663,7 @@ ] }, { - "caseId": "333", + "containerId": "333", "skinIds": [ "344", "2340", @@ -4745,7 +4745,7 @@ ] }, { - "caseId": "334", + "containerId": "334", "skinIds": [ "14827", "214965", @@ -4769,7 +4769,7 @@ ] }, { - "caseId": "340", + "containerId": "340", "skinIds": [ "64266", "294349", @@ -4793,7 +4793,7 @@ ] }, { - "caseId": "341", + "containerId": "341", "skinIds": [ "12557", "27365", @@ -4816,7 +4816,7 @@ ] }, { - "caseId": "344", + "containerId": "344", "skinIds": [ "943", "68840", @@ -4840,7 +4840,7 @@ ] }, { - "caseId": "345", + "containerId": "345", "skinIds": [ "46800", "50561", @@ -4863,7 +4863,7 @@ ] }, { - "caseId": "346", + "containerId": "346", "skinIds": [ "65787", "119597", @@ -4883,7 +4883,7 @@ ] }, { - "caseId": "347", + "containerId": "347", "skinIds": [ "5092", "121609", @@ -4907,7 +4907,7 @@ ] }, { - "caseId": "348", + "containerId": "348", "skinIds": [ "595", "9934", @@ -4951,7 +4951,7 @@ ] }, { - "caseId": "349", + "containerId": "349", "skinIds": [ "155", "511975", @@ -4973,7 +4973,7 @@ ] }, { - "caseId": "350", + "containerId": "350", "skinIds": [ "3733", "26921", @@ -5006,7 +5006,7 @@ ] }, { - "caseId": "351", + "containerId": "351", "skinIds": [ "14827", "214965", @@ -5030,7 +5030,7 @@ ] }, { - "caseId": "352", + "containerId": "352", "skinIds": [ "98", "9349", @@ -5054,7 +5054,7 @@ ] }, { - "caseId": "358", + "containerId": "358", "skinIds": [ "64266", "294349", @@ -5078,7 +5078,7 @@ ] }, { - "caseId": "359", + "containerId": "359", "skinIds": [ "12557", "27365", @@ -5101,7 +5101,7 @@ ] }, { - "caseId": "362", + "containerId": "362", "skinIds": [ "943", "68840", @@ -5125,7 +5125,7 @@ ] }, { - "caseId": "363", + "containerId": "363", "skinIds": [ "46800", "50561", @@ -5148,7 +5148,7 @@ ] }, { - "caseId": "364", + "containerId": "364", "skinIds": [ "5092", "121609", @@ -5172,7 +5172,7 @@ ] }, { - "caseId": "365", + "containerId": "365", "skinIds": [ "2920", "6102", @@ -5242,7 +5242,7 @@ ] }, { - "caseId": "368", + "containerId": "368", "skinIds": [ "287", "22157", @@ -5288,7 +5288,7 @@ ] }, { - "caseId": "369", + "containerId": "369", "skinIds": [ "792", "2133", @@ -5370,7 +5370,7 @@ ] }, { - "caseId": "370", + "containerId": "370", "skinIds": [ "358", "792", @@ -5452,7 +5452,7 @@ ] }, { - "caseId": "380", + "containerId": "380", "skinIds": [ "14827", "214965", @@ -5476,7 +5476,7 @@ ] }, { - "caseId": "384", + "containerId": "384", "skinIds": [ "64266", "294349", @@ -5500,7 +5500,7 @@ ] }, { - "caseId": "386", + "containerId": "386", "skinIds": [ "12557", "27365", @@ -5523,7 +5523,7 @@ ] }, { - "caseId": "388", + "containerId": "388", "skinIds": [ "943", "68840", @@ -5547,7 +5547,7 @@ ] }, { - "caseId": "389", + "containerId": "389", "skinIds": [ "46800", "50561", @@ -5570,7 +5570,7 @@ ] }, { - "caseId": "390", + "containerId": "390", "skinIds": [ "65787", "119597", @@ -5590,7 +5590,7 @@ ] }, { - "caseId": "391", + "containerId": "391", "skinIds": [ "5092", "121609", @@ -5614,7 +5614,7 @@ ] }, { - "caseId": "401", + "containerId": "401", "skinIds": [ "66494", "72939", @@ -5692,7 +5692,7 @@ ] }, { - "caseId": "403", + "containerId": "403", "skinIds": [ "540", "11508", @@ -5766,7 +5766,7 @@ ] }, { - "caseId": "404", + "containerId": "404", "skinIds": [ "66494", "73365", @@ -5843,7 +5843,7 @@ ] }, { - "caseId": "405", + "containerId": "405", "skinIds": [ "27042", "66494", @@ -5925,13 +5925,13 @@ ] }, { - "caseId": "406", + "containerId": "406", "skinIds": [ "7591" ] }, { - "caseId": "452", + "containerId": "452", "skinIds": [ "510", "76917", diff --git a/assets/data/cases.json b/assets/data/containers.json similarity index 76% rename from assets/data/cases.json rename to assets/data/containers.json index 4ad3cb31..cb6bc762 100644 --- a/assets/data/cases.json +++ b/assets/data/containers.json @@ -1,8 +1,106 @@ [ + { + "id": "20011", + "name": "The Assault Collection", + "containerImage": "assets/containers/20011.webp", + "releaseDate": "2013-04-25", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "PAYBACK", + "sourceName": "Operation Payback", + "currency": null, + "cost": null + }, + { + "id": "20008", + "name": "The Aztec Collection", + "containerImage": "assets/containers/20008.webp", + "releaseDate": "2013-04-25", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "PAYBACK", + "sourceName": "Operation Payback", + "currency": null, + "cost": null + }, + { + "id": "20024", + "name": "The Inferno Collection", + "containerImage": "assets/containers/20024.webp", + "releaseDate": "2013-04-25", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "PAYBACK", + "sourceName": "Operation Payback", + "currency": null, + "cost": null + }, + { + "id": "20020", + "name": "The Militia Collection", + "containerImage": "assets/containers/20020.webp", + "releaseDate": "2013-04-25", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "PAYBACK", + "sourceName": "Operation Payback", + "currency": null, + "cost": null + }, + { + "id": "20021", + "name": "The Nuke Collection", + "containerImage": "assets/containers/20021.webp", + "releaseDate": "2013-04-25", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "PAYBACK", + "sourceName": "Operation Payback", + "currency": null, + "cost": null + }, + { + "id": "20025", + "name": "The Office Collection", + "containerImage": "assets/containers/20025.webp", + "releaseDate": "2013-04-25", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "PAYBACK", + "sourceName": "Operation Payback", + "currency": null, + "cost": null + }, + { + "id": "20005", + "name": "The Vertigo Collection", + "containerImage": "assets/containers/20005.webp", + "releaseDate": "2013-04-25", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "PAYBACK", + "sourceName": "Operation Payback", + "currency": null, + "cost": null + }, { "id": "172", "name": "CS:GO Weapon Case", - "caseImage": "assets/cases/172.webp", + "containerImage": "assets/containers/172.webp", "releaseDate": "2013-08-14", "type": "CASE", "tournamentName": null, @@ -14,7 +112,7 @@ { "id": "403", "name": "eSports 2013 Case", - "caseImage": "assets/cases/403.webp", + "containerImage": "assets/containers/403.webp", "releaseDate": "2013-08-14", "type": "CASE", "tournamentName": null, @@ -26,7 +124,7 @@ { "id": "303", "name": "Operation Bravo Case", - "caseImage": "assets/cases/303.webp", + "containerImage": "assets/containers/303.webp", "releaseDate": "2013-09-19", "type": "CASE", "tournamentName": null, @@ -35,10 +133,108 @@ "sourceId": null, "sourceName": null }, + { + "id": "20007", + "name": "The Dust 2 Collection", + "containerImage": "assets/containers/20007.webp", + "releaseDate": "2013-09-19", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BRAVO", + "sourceName": "Operation Bravo", + "currency": null, + "cost": null + }, + { + "id": "20009", + "name": "The Dust Collection", + "containerImage": "assets/containers/20009.webp", + "releaseDate": "2013-09-19", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BRAVO", + "sourceName": "Operation Bravo", + "currency": null, + "cost": null + }, + { + "id": "20012", + "name": "The Italy Collection", + "containerImage": "assets/containers/20012.webp", + "releaseDate": "2013-09-19", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BRAVO", + "sourceName": "Operation Bravo", + "currency": null, + "cost": null + }, + { + "id": "20014", + "name": "The Lake Collection", + "containerImage": "assets/containers/20014.webp", + "releaseDate": "2013-09-19", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BRAVO", + "sourceName": "Operation Bravo", + "currency": null, + "cost": null + }, + { + "id": "20013", + "name": "The Mirage Collection", + "containerImage": "assets/containers/20013.webp", + "releaseDate": "2013-09-19", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BRAVO", + "sourceName": "Operation Bravo", + "currency": null, + "cost": null + }, + { + "id": "20016", + "name": "The Safehouse Collection", + "containerImage": "assets/containers/20016.webp", + "releaseDate": "2013-09-19", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BRAVO", + "sourceName": "Operation Bravo", + "currency": null, + "cost": null + }, + { + "id": "20022", + "name": "The Train Collection", + "containerImage": "assets/containers/20022.webp", + "releaseDate": "2013-09-19", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BRAVO", + "sourceName": "Operation Bravo", + "currency": null, + "cost": null + }, { "id": "173", "name": "CS:GO Weapon Case 2", - "caseImage": "assets/cases/173.webp", + "containerImage": "assets/containers/173.webp", "releaseDate": "2013-11-08", "type": "CASE", "tournamentName": null, @@ -50,7 +246,7 @@ { "id": "208", "name": "DreamHack 2013 Souvenir Package", - "caseImage": "assets/cases/208.webp", + "containerImage": "assets/containers/208.webp", "releaseDate": "2013-11-28", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Winter 2013", @@ -62,7 +258,7 @@ { "id": "401", "name": "Winter Offensive Weapon Case", - "caseImage": "assets/cases/401.webp", + "containerImage": "assets/containers/401.webp", "releaseDate": "2013-12-18", "type": "CASE", "tournamentName": null, @@ -74,7 +270,7 @@ { "id": "404", "name": "eSports 2013 Winter Case", - "caseImage": "assets/cases/404.webp", + "containerImage": "assets/containers/404.webp", "releaseDate": "2013-12-18", "type": "CASE", "tournamentName": null, @@ -86,7 +282,7 @@ { "id": "2170", "name": "Sticker Capsule", - "caseImage": "assets/cases/2170.webp", + "containerImage": "assets/containers/2170.webp", "releaseDate": "2014-01-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -98,7 +294,7 @@ { "id": "2171", "name": "Sticker Capsule 2", - "caseImage": "assets/cases/2171.webp", + "containerImage": "assets/containers/2171.webp", "releaseDate": "2014-01-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -110,7 +306,7 @@ { "id": "174", "name": "CS:GO Weapon Case 3", - "caseImage": "assets/cases/174.webp", + "containerImage": "assets/containers/174.webp", "releaseDate": "2014-02-12", "type": "CASE", "tournamentName": null, @@ -122,7 +318,7 @@ { "id": "307", "name": "Operation Phoenix Weapon Case", - "caseImage": "assets/cases/307.webp", + "containerImage": "assets/containers/307.webp", "releaseDate": "2014-02-20", "type": "CASE", "tournamentName": null, @@ -131,10 +327,24 @@ "sourceId": null, "sourceName": null }, + { + "id": "20004", + "name": "The Bank Collection", + "containerImage": "assets/containers/20004.webp", + "releaseDate": "2014-02-20", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "PHOENIX", + "sourceName": "Operation Phoenix", + "currency": null, + "cost": null + }, { "id": "2186", "name": "EMS Katowice 2014 Challengers", - "caseImage": "assets/cases/2186.webp", + "containerImage": "assets/containers/2186.webp", "releaseDate": "2014-03-13", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -146,7 +356,7 @@ { "id": "2187", "name": "EMS Katowice 2014 Legends", - "caseImage": "assets/cases/2187.webp", + "containerImage": "assets/containers/2187.webp", "releaseDate": "2014-03-13", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -158,7 +368,7 @@ { "id": "224", "name": "EMS One 2014 Souvenir Package", - "caseImage": "assets/cases/224.webp", + "containerImage": "assets/containers/224.webp", "releaseDate": "2014-03-13", "type": "SOUVENIR_PACKAGE", "tournamentName": "EMS One Katowice 2014", @@ -170,7 +380,7 @@ { "id": "260", "name": "Huntsman Weapon Case", - "caseImage": "assets/cases/260.webp", + "containerImage": "assets/containers/260.webp", "releaseDate": "2014-05-01", "type": "CASE", "tournamentName": null, @@ -182,7 +392,7 @@ { "id": "452", "name": "Huntsman Weapon Case (Legacy)", - "caseImage": "assets/cases/452.webp", + "containerImage": "assets/containers/260.webp", "releaseDate": "2014-05-01", "type": "CASE", "tournamentName": null, @@ -194,7 +404,7 @@ { "id": "2109", "name": "Community Sticker Capsule 1", - "caseImage": "assets/cases/2109.webp", + "containerImage": "assets/containers/2109.webp", "releaseDate": "2014-06-11", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -206,7 +416,7 @@ { "id": "304", "name": "Operation Breakout Weapon Case", - "caseImage": "assets/cases/304.webp", + "containerImage": "assets/containers/304.webp", "releaseDate": "2014-07-01", "type": "CASE", "tournamentName": null, @@ -215,10 +425,66 @@ "sourceId": null, "sourceName": null }, + { + "id": "20003", + "name": "The Baggage Collection", + "containerImage": "assets/containers/20003.webp", + "releaseDate": "2014-07-01", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BREAKOUT", + "sourceName": "Operation Breakout", + "currency": null, + "cost": null + }, + { + "id": "20017", + "name": "The Cache Collection", + "containerImage": "assets/containers/20017.webp", + "releaseDate": "2014-07-01", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BREAKOUT", + "sourceName": "Operation Breakout", + "currency": null, + "cost": null + }, + { + "id": "20018", + "name": "The Cobblestone Collection", + "containerImage": "assets/containers/20018.webp", + "releaseDate": "2014-07-01", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BREAKOUT", + "sourceName": "Operation Breakout", + "currency": null, + "cost": null + }, + { + "id": "20019", + "name": "The Overpass Collection", + "containerImage": "assets/containers/20019.webp", + "releaseDate": "2014-07-01", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BREAKOUT", + "sourceName": "Operation Breakout", + "currency": null, + "cost": null + }, { "id": "405", "name": "eSports 2014 Summer Case", - "caseImage": "assets/cases/405.webp", + "containerImage": "assets/containers/405.webp", "releaseDate": "2014-07-10", "type": "CASE", "tournamentName": null, @@ -230,7 +496,7 @@ { "id": "225", "name": "ESL One Cologne 2014 Cache Souvenir Package", - "caseImage": "assets/cases/225.webp", + "containerImage": "assets/containers/225.webp", "releaseDate": "2014-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2014", @@ -242,7 +508,7 @@ { "id": "2189", "name": "ESL One Cologne 2014 Challengers", - "caseImage": "assets/cases/2189.webp", + "containerImage": "assets/containers/2189.webp", "releaseDate": "2014-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -254,7 +520,7 @@ { "id": "226", "name": "ESL One Cologne 2014 Cobblestone Souvenir Package", - "caseImage": "assets/cases/226.webp", + "containerImage": "assets/containers/226.webp", "releaseDate": "2014-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2014", @@ -266,7 +532,7 @@ { "id": "227", "name": "ESL One Cologne 2014 Dust II Souvenir Package", - "caseImage": "assets/cases/227.webp", + "containerImage": "assets/containers/227.webp", "releaseDate": "2014-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2014", @@ -278,7 +544,7 @@ { "id": "228", "name": "ESL One Cologne 2014 Inferno Souvenir Package", - "caseImage": "assets/cases/228.webp", + "containerImage": "assets/containers/228.webp", "releaseDate": "2014-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2014", @@ -290,7 +556,7 @@ { "id": "2188", "name": "ESL One Cologne 2014 Legends", - "caseImage": "assets/cases/2188.webp", + "containerImage": "assets/containers/2188.webp", "releaseDate": "2014-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -302,7 +568,7 @@ { "id": "229", "name": "ESL One Cologne 2014 Mirage Souvenir Package", - "caseImage": "assets/cases/229.webp", + "containerImage": "assets/containers/229.webp", "releaseDate": "2014-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2014", @@ -314,7 +580,7 @@ { "id": "230", "name": "ESL One Cologne 2014 Nuke Souvenir Package", - "caseImage": "assets/cases/230.webp", + "containerImage": "assets/containers/230.webp", "releaseDate": "2014-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2014", @@ -326,7 +592,7 @@ { "id": "231", "name": "ESL One Cologne 2014 Overpass Souvenir Package", - "caseImage": "assets/cases/231.webp", + "containerImage": "assets/containers/231.webp", "releaseDate": "2014-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2014", @@ -338,7 +604,7 @@ { "id": "309", "name": "Operation Vanguard Weapon Case", - "caseImage": "assets/cases/309.webp", + "containerImage": "assets/containers/309.webp", "releaseDate": "2014-11-11", "type": "CASE", "tournamentName": null, @@ -350,7 +616,7 @@ { "id": "209", "name": "DreamHack 2014 Cache Souvenir Package", - "caseImage": "assets/cases/209.webp", + "containerImage": "assets/containers/209.webp", "releaseDate": "2014-11-27", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Winter 2014", @@ -362,7 +628,7 @@ { "id": "210", "name": "DreamHack 2014 Cobblestone Souvenir Package", - "caseImage": "assets/cases/210.webp", + "containerImage": "assets/containers/210.webp", "releaseDate": "2014-11-27", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Winter 2014", @@ -374,7 +640,7 @@ { "id": "211", "name": "DreamHack 2014 Dust II Souvenir Package", - "caseImage": "assets/cases/211.webp", + "containerImage": "assets/containers/211.webp", "releaseDate": "2014-11-27", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Winter 2014", @@ -386,7 +652,7 @@ { "id": "212", "name": "DreamHack 2014 Inferno Souvenir Package", - "caseImage": "assets/cases/212.webp", + "containerImage": "assets/containers/212.webp", "releaseDate": "2014-11-27", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Winter 2014", @@ -398,7 +664,7 @@ { "id": "2190", "name": "DreamHack 2014 Legends (Holo/Foil)", - "caseImage": "assets/cases/2190.webp", + "containerImage": "assets/containers/2190.webp", "releaseDate": "2014-11-27", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -410,7 +676,7 @@ { "id": "213", "name": "DreamHack 2014 Mirage Souvenir Package", - "caseImage": "assets/cases/213.webp", + "containerImage": "assets/containers/213.webp", "releaseDate": "2014-11-27", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Winter 2014", @@ -422,7 +688,7 @@ { "id": "214", "name": "DreamHack 2014 Nuke Souvenir Package", - "caseImage": "assets/cases/214.webp", + "containerImage": "assets/containers/214.webp", "releaseDate": "2014-11-27", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Winter 2014", @@ -434,7 +700,7 @@ { "id": "215", "name": "DreamHack 2014 Overpass Souvenir Package", - "caseImage": "assets/cases/215.webp", + "containerImage": "assets/containers/215.webp", "releaseDate": "2014-11-27", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Winter 2014", @@ -446,7 +712,7 @@ { "id": "178", "name": "Chroma Case", - "caseImage": "assets/cases/178.webp", + "containerImage": "assets/containers/178.webp", "releaseDate": "2015-01-08", "type": "CASE", "tournamentName": null, @@ -458,7 +724,7 @@ { "id": "239", "name": "ESL One Katowice 2015 Cache Souvenir Package", - "caseImage": "assets/cases/239.webp", + "containerImage": "assets/containers/239.webp", "releaseDate": "2015-03-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Katowice 2015", @@ -470,7 +736,7 @@ { "id": "2191", "name": "ESL One Katowice 2015 Challengers (Holo/Foil)", - "caseImage": "assets/cases/2191.webp", + "containerImage": "assets/containers/2191.webp", "releaseDate": "2015-03-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -482,7 +748,7 @@ { "id": "240", "name": "ESL One Katowice 2015 Cobblestone Souvenir Package", - "caseImage": "assets/cases/240.webp", + "containerImage": "assets/containers/240.webp", "releaseDate": "2015-03-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Katowice 2015", @@ -494,7 +760,7 @@ { "id": "241", "name": "ESL One Katowice 2015 Dust II Souvenir Package", - "caseImage": "assets/cases/241.webp", + "containerImage": "assets/containers/241.webp", "releaseDate": "2015-03-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Katowice 2015", @@ -506,7 +772,7 @@ { "id": "242", "name": "ESL One Katowice 2015 Inferno Souvenir Package", - "caseImage": "assets/cases/242.webp", + "containerImage": "assets/containers/242.webp", "releaseDate": "2015-03-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Katowice 2015", @@ -518,7 +784,7 @@ { "id": "2192", "name": "ESL One Katowice 2015 Legends (Holo/Foil)", - "caseImage": "assets/cases/2192.webp", + "containerImage": "assets/containers/2192.webp", "releaseDate": "2015-03-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -530,7 +796,7 @@ { "id": "243", "name": "ESL One Katowice 2015 Mirage Souvenir Package", - "caseImage": "assets/cases/243.webp", + "containerImage": "assets/containers/243.webp", "releaseDate": "2015-03-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Katowice 2015", @@ -542,7 +808,7 @@ { "id": "244", "name": "ESL One Katowice 2015 Nuke Souvenir Package", - "caseImage": "assets/cases/244.webp", + "containerImage": "assets/containers/244.webp", "releaseDate": "2015-03-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Katowice 2015", @@ -554,7 +820,7 @@ { "id": "245", "name": "ESL One Katowice 2015 Overpass Souvenir Package", - "caseImage": "assets/cases/245.webp", + "containerImage": "assets/containers/245.webp", "releaseDate": "2015-03-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Katowice 2015", @@ -566,7 +832,7 @@ { "id": "176", "name": "Chroma 2 Case", - "caseImage": "assets/cases/176.webp", + "containerImage": "assets/containers/176.webp", "releaseDate": "2015-04-15", "type": "CASE", "tournamentName": null, @@ -578,7 +844,7 @@ { "id": "2118", "name": "Enfu Sticker Capsule", - "caseImage": "assets/cases/2118.webp", + "containerImage": "assets/containers/2118.webp", "releaseDate": "2015-04-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -590,7 +856,7 @@ { "id": "248", "name": "Falchion Case", - "caseImage": "assets/cases/248.webp", + "containerImage": "assets/containers/248.webp", "releaseDate": "2015-05-26", "type": "CASE", "tournamentName": null, @@ -599,10 +865,52 @@ "sourceId": null, "sourceName": null }, + { + "id": "20023", + "name": "The Chop Shop Collection", + "containerImage": "assets/containers/20023.webp", + "releaseDate": "2015-05-26", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BLOODHOUND", + "sourceName": "Operation Bloodhound", + "currency": null, + "cost": null + }, + { + "id": "20015", + "name": "The Gods and Monsters Collection", + "containerImage": "assets/containers/20015.webp", + "releaseDate": "2015-05-26", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BLOODHOUND", + "sourceName": "Operation Bloodhound", + "currency": null, + "cost": null + }, + { + "id": "20001", + "name": "The Rising Sun Collection", + "containerImage": "assets/containers/20001.webp", + "releaseDate": "2015-05-26", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BLOODHOUND", + "sourceName": "Operation Bloodhound", + "currency": null, + "cost": null + }, { "id": "2001", "name": "Autograph Capsule | Cloud9 G2A | Cologne 2015", - "caseImage": "assets/cases/2001.webp", + "containerImage": "assets/containers/2001.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -614,7 +922,7 @@ { "id": "2005", "name": "Autograph Capsule | Counter Logic Gaming | Cologne 2015", - "caseImage": "assets/cases/2005.webp", + "containerImage": "assets/containers/2005.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -626,7 +934,7 @@ { "id": "2013", "name": "Autograph Capsule | Flipsid3 Tactics | Cologne 2015", - "caseImage": "assets/cases/2013.webp", + "containerImage": "assets/containers/2013.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -638,7 +946,7 @@ { "id": "2018", "name": "Autograph Capsule | Fnatic | Cologne 2015", - "caseImage": "assets/cases/2018.webp", + "containerImage": "assets/containers/2018.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -650,7 +958,7 @@ { "id": "2029", "name": "Autograph Capsule | Group A (Foil) | Cologne 2015", - "caseImage": "assets/cases/2029.webp", + "containerImage": "assets/containers/2029.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -662,7 +970,7 @@ { "id": "2030", "name": "Autograph Capsule | Group B (Foil) | Cologne 2015", - "caseImage": "assets/cases/2030.webp", + "containerImage": "assets/containers/2030.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -674,7 +982,7 @@ { "id": "2031", "name": "Autograph Capsule | Group C (Foil) | Cologne 2015", - "caseImage": "assets/cases/2031.webp", + "containerImage": "assets/containers/2031.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -686,7 +994,7 @@ { "id": "2032", "name": "Autograph Capsule | Group D (Foil) | Cologne 2015", - "caseImage": "assets/cases/2032.webp", + "containerImage": "assets/containers/2032.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -698,7 +1006,7 @@ { "id": "2039", "name": "Autograph Capsule | Luminosity Gaming | Cologne 2015", - "caseImage": "assets/cases/2039.webp", + "containerImage": "assets/containers/2039.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -710,7 +1018,7 @@ { "id": "2043", "name": "Autograph Capsule | Natus Vincere | Cologne 2015", - "caseImage": "assets/cases/2043.webp", + "containerImage": "assets/containers/2043.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -722,7 +1030,7 @@ { "id": "2047", "name": "Autograph Capsule | Ninjas in Pyjamas | Cologne 2015", - "caseImage": "assets/cases/2047.webp", + "containerImage": "assets/containers/2047.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -734,7 +1042,7 @@ { "id": "2053", "name": "Autograph Capsule | Renegades | Cologne 2015", - "caseImage": "assets/cases/2053.webp", + "containerImage": "assets/containers/2053.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -746,7 +1054,7 @@ { "id": "2061", "name": "Autograph Capsule | Team EnVyUs | Cologne 2015", - "caseImage": "assets/cases/2061.webp", + "containerImage": "assets/containers/2061.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -758,7 +1066,7 @@ { "id": "2064", "name": "Autograph Capsule | Team Immunity | Cologne 2015", - "caseImage": "assets/cases/2064.webp", + "containerImage": "assets/containers/2064.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -770,7 +1078,7 @@ { "id": "2065", "name": "Autograph Capsule | Team Kinguin | Cologne 2015", - "caseImage": "assets/cases/2065.webp", + "containerImage": "assets/containers/2065.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -782,7 +1090,7 @@ { "id": "2071", "name": "Autograph Capsule | Team SoloMid | Cologne 2015", - "caseImage": "assets/cases/2071.webp", + "containerImage": "assets/containers/2071.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -794,7 +1102,7 @@ { "id": "2072", "name": "Autograph Capsule | Team eBettle | Cologne 2015", - "caseImage": "assets/cases/2072.webp", + "containerImage": "assets/containers/2072.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -806,7 +1114,7 @@ { "id": "2074", "name": "Autograph Capsule | Titan | Cologne 2015", - "caseImage": "assets/cases/2074.webp", + "containerImage": "assets/containers/2074.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -818,7 +1126,7 @@ { "id": "2078", "name": "Autograph Capsule | Virtus.Pro | Cologne 2015", - "caseImage": "assets/cases/2078.webp", + "containerImage": "assets/containers/2078.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -830,7 +1138,7 @@ { "id": "2083", "name": "Autograph Capsule | mousesports | Cologne 2015", - "caseImage": "assets/cases/2083.webp", + "containerImage": "assets/containers/2083.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -842,7 +1150,7 @@ { "id": "232", "name": "ESL One Cologne 2015 Cache Souvenir Package", - "caseImage": "assets/cases/232.webp", + "containerImage": "assets/containers/232.webp", "releaseDate": "2015-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2015", @@ -854,7 +1162,7 @@ { "id": "2194", "name": "ESL One Cologne 2015 Challengers (Foil)", - "caseImage": "assets/cases/2194.webp", + "containerImage": "assets/containers/2194.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -866,7 +1174,7 @@ { "id": "233", "name": "ESL One Cologne 2015 Cobblestone Souvenir Package", - "caseImage": "assets/cases/233.webp", + "containerImage": "assets/containers/233.webp", "releaseDate": "2015-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2015", @@ -878,7 +1186,7 @@ { "id": "234", "name": "ESL One Cologne 2015 Dust II Souvenir Package", - "caseImage": "assets/cases/234.webp", + "containerImage": "assets/containers/234.webp", "releaseDate": "2015-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2015", @@ -890,7 +1198,7 @@ { "id": "235", "name": "ESL One Cologne 2015 Inferno Souvenir Package", - "caseImage": "assets/cases/235.webp", + "containerImage": "assets/containers/235.webp", "releaseDate": "2015-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2015", @@ -902,7 +1210,7 @@ { "id": "2193", "name": "ESL One Cologne 2015 Legends (Foil)", - "caseImage": "assets/cases/2193.webp", + "containerImage": "assets/containers/2193.webp", "releaseDate": "2015-08-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -914,7 +1222,7 @@ { "id": "236", "name": "ESL One Cologne 2015 Mirage Souvenir Package", - "caseImage": "assets/cases/236.webp", + "containerImage": "assets/containers/236.webp", "releaseDate": "2015-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2015", @@ -926,7 +1234,7 @@ { "id": "237", "name": "ESL One Cologne 2015 Overpass Souvenir Package", - "caseImage": "assets/cases/237.webp", + "containerImage": "assets/containers/237.webp", "releaseDate": "2015-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2015", @@ -938,7 +1246,7 @@ { "id": "238", "name": "ESL One Cologne 2015 Train Souvenir Package", - "caseImage": "assets/cases/238.webp", + "containerImage": "assets/containers/238.webp", "releaseDate": "2015-08-14", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2015", @@ -950,7 +1258,7 @@ { "id": "350", "name": "Shadow Case", - "caseImage": "assets/cases/350.webp", + "containerImage": "assets/containers/350.webp", "releaseDate": "2015-09-17", "type": "CASE", "tournamentName": null, @@ -962,7 +1270,7 @@ { "id": "1998", "name": "Autograph Capsule | Challengers (Foil) | Cluj-Napoca 2015", - "caseImage": "assets/cases/1998.webp", + "containerImage": "assets/containers/1998.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -974,7 +1282,7 @@ { "id": "2002", "name": "Autograph Capsule | Cloud9 | Cluj-Napoca 2015", - "caseImage": "assets/cases/2002.webp", + "containerImage": "assets/containers/2002.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -986,7 +1294,7 @@ { "id": "2004", "name": "Autograph Capsule | Counter Logic Gaming | Cluj-Napoca 2015", - "caseImage": "assets/cases/2004.webp", + "containerImage": "assets/containers/2004.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -998,7 +1306,7 @@ { "id": "2012", "name": "Autograph Capsule | Flipsid3 Tactics | Cluj-Napoca 2015", - "caseImage": "assets/cases/2012.webp", + "containerImage": "assets/containers/2012.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1010,7 +1318,7 @@ { "id": "2017", "name": "Autograph Capsule | Fnatic | Cluj-Napoca 2015", - "caseImage": "assets/cases/2017.webp", + "containerImage": "assets/containers/2017.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1022,7 +1330,7 @@ { "id": "2022", "name": "Autograph Capsule | G2 Esports | Cluj-Napoca 2015", - "caseImage": "assets/cases/2022.webp", + "containerImage": "assets/containers/2022.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1034,7 +1342,7 @@ { "id": "2035", "name": "Autograph Capsule | Legends (Foil) | Cluj-Napoca 2015", - "caseImage": "assets/cases/2035.webp", + "containerImage": "assets/containers/2035.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1046,7 +1354,7 @@ { "id": "2038", "name": "Autograph Capsule | Luminosity Gaming | Cluj-Napoca 2015", - "caseImage": "assets/cases/2038.webp", + "containerImage": "assets/containers/2038.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1058,7 +1366,7 @@ { "id": "2042", "name": "Autograph Capsule | Natus Vincere | Cluj-Napoca 2015", - "caseImage": "assets/cases/2042.webp", + "containerImage": "assets/containers/2042.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1070,7 +1378,7 @@ { "id": "2046", "name": "Autograph Capsule | Ninjas in Pyjamas | Cluj-Napoca 2015", - "caseImage": "assets/cases/2046.webp", + "containerImage": "assets/containers/2046.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1082,7 +1390,7 @@ { "id": "2057", "name": "Autograph Capsule | Team Dignitas | Cluj-Napoca 2015", - "caseImage": "assets/cases/2057.webp", + "containerImage": "assets/containers/2057.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1094,7 +1402,7 @@ { "id": "2060", "name": "Autograph Capsule | Team EnVyUs | Cluj-Napoca 2015", - "caseImage": "assets/cases/2060.webp", + "containerImage": "assets/containers/2060.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1106,7 +1414,7 @@ { "id": "2067", "name": "Autograph Capsule | Team Liquid | Cluj-Napoca 2015", - "caseImage": "assets/cases/2067.webp", + "containerImage": "assets/containers/2067.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1118,7 +1426,7 @@ { "id": "2070", "name": "Autograph Capsule | Team SoloMid | Cluj-Napoca 2015", - "caseImage": "assets/cases/2070.webp", + "containerImage": "assets/containers/2070.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1130,7 +1438,7 @@ { "id": "2073", "name": "Autograph Capsule | Titan | Cluj-Napoca 2015", - "caseImage": "assets/cases/2073.webp", + "containerImage": "assets/containers/2073.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1142,7 +1450,7 @@ { "id": "2075", "name": "Autograph Capsule | Vexed Gaming | Cluj-Napoca 2015", - "caseImage": "assets/cases/2075.webp", + "containerImage": "assets/containers/2075.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1154,7 +1462,7 @@ { "id": "2077", "name": "Autograph Capsule | Virtus.Pro | Cluj-Napoca 2015", - "caseImage": "assets/cases/2077.webp", + "containerImage": "assets/containers/2077.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1166,7 +1474,7 @@ { "id": "2082", "name": "Autograph Capsule | mousesports | Cluj-Napoca 2015", - "caseImage": "assets/cases/2082.webp", + "containerImage": "assets/containers/2082.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1178,7 +1486,7 @@ { "id": "216", "name": "DreamHack Cluj-Napoca 2015 Cache Souvenir Package", - "caseImage": "assets/cases/216.webp", + "containerImage": "assets/containers/216.webp", "releaseDate": "2015-10-28", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Cluj-Napoca 2015", @@ -1190,7 +1498,7 @@ { "id": "2196", "name": "DreamHack Cluj-Napoca 2015 Challengers (Foil)", - "caseImage": "assets/cases/2196.webp", + "containerImage": "assets/containers/2196.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1202,7 +1510,7 @@ { "id": "217", "name": "DreamHack Cluj-Napoca 2015 Cobblestone Souvenir Package", - "caseImage": "assets/cases/217.webp", + "containerImage": "assets/containers/217.webp", "releaseDate": "2015-10-28", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Cluj-Napoca 2015", @@ -1214,7 +1522,7 @@ { "id": "218", "name": "DreamHack Cluj-Napoca 2015 Dust II Souvenir Package", - "caseImage": "assets/cases/218.webp", + "containerImage": "assets/containers/218.webp", "releaseDate": "2015-10-28", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Cluj-Napoca 2015", @@ -1226,7 +1534,7 @@ { "id": "219", "name": "DreamHack Cluj-Napoca 2015 Inferno Souvenir Package", - "caseImage": "assets/cases/219.webp", + "containerImage": "assets/containers/219.webp", "releaseDate": "2015-10-28", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Cluj-Napoca 2015", @@ -1238,7 +1546,7 @@ { "id": "2195", "name": "DreamHack Cluj-Napoca 2015 Legends (Foil)", - "caseImage": "assets/cases/2195.webp", + "containerImage": "assets/containers/2195.webp", "releaseDate": "2015-10-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1250,7 +1558,7 @@ { "id": "220", "name": "DreamHack Cluj-Napoca 2015 Mirage Souvenir Package", - "caseImage": "assets/cases/220.webp", + "containerImage": "assets/containers/220.webp", "releaseDate": "2015-10-28", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Cluj-Napoca 2015", @@ -1262,7 +1570,7 @@ { "id": "221", "name": "DreamHack Cluj-Napoca 2015 Overpass Souvenir Package", - "caseImage": "assets/cases/221.webp", + "containerImage": "assets/containers/221.webp", "releaseDate": "2015-10-28", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Cluj-Napoca 2015", @@ -1274,7 +1582,7 @@ { "id": "222", "name": "DreamHack Cluj-Napoca 2015 Train Souvenir Package", - "caseImage": "assets/cases/222.webp", + "containerImage": "assets/containers/222.webp", "releaseDate": "2015-10-28", "type": "SOUVENIR_PACKAGE", "tournamentName": "DreamHack Cluj-Napoca 2015", @@ -1286,7 +1594,7 @@ { "id": "2145", "name": "Pinups Capsule", - "caseImage": "assets/cases/2145.webp", + "containerImage": "assets/containers/2145.webp", "releaseDate": "2015-12-01", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1298,7 +1606,7 @@ { "id": "2162", "name": "Slid3 Capsule", - "caseImage": "assets/cases/2162.webp", + "containerImage": "assets/containers/2162.webp", "releaseDate": "2015-12-01", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1310,7 +1618,7 @@ { "id": "2179", "name": "Team Roles Capsule", - "caseImage": "assets/cases/2179.webp", + "containerImage": "assets/containers/2179.webp", "releaseDate": "2015-12-01", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1322,7 +1630,7 @@ { "id": "333", "name": "Revolver Case", - "caseImage": "assets/cases/333.webp", + "containerImage": "assets/containers/333.webp", "releaseDate": "2015-12-08", "type": "CASE", "tournamentName": null, @@ -1334,7 +1642,7 @@ { "id": "310", "name": "Operation Wildfire Case", - "caseImage": "assets/cases/310.webp", + "containerImage": "assets/containers/310.webp", "releaseDate": "2016-02-17", "type": "CASE", "tournamentName": null, @@ -1346,7 +1654,7 @@ { "id": "1996", "name": "Autograph Capsule | Astralis | MLG Columbus 2016", - "caseImage": "assets/cases/1996.webp", + "containerImage": "assets/containers/1996.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1358,7 +1666,7 @@ { "id": "2000", "name": "Autograph Capsule | Challengers (Foil) | MLG Columbus 2016", - "caseImage": "assets/cases/2000.webp", + "containerImage": "assets/containers/2000.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1370,7 +1678,7 @@ { "id": "2003", "name": "Autograph Capsule | Cloud9 | MLG Columbus 2016", - "caseImage": "assets/cases/2003.webp", + "containerImage": "assets/containers/2003.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1382,7 +1690,7 @@ { "id": "2007", "name": "Autograph Capsule | Counter Logic Gaming | MLG Columbus 2016", - "caseImage": "assets/cases/2007.webp", + "containerImage": "assets/containers/2007.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1394,7 +1702,7 @@ { "id": "2010", "name": "Autograph Capsule | FaZe Clan | MLG Columbus 2016", - "caseImage": "assets/cases/2010.webp", + "containerImage": "assets/containers/2010.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1406,7 +1714,7 @@ { "id": "2015", "name": "Autograph Capsule | Flipsid3 Tactics | MLG Columbus 2016", - "caseImage": "assets/cases/2015.webp", + "containerImage": "assets/containers/2015.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1418,7 +1726,7 @@ { "id": "2020", "name": "Autograph Capsule | Fnatic | MLG Columbus 2016", - "caseImage": "assets/cases/2020.webp", + "containerImage": "assets/containers/2020.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1430,7 +1738,7 @@ { "id": "2024", "name": "Autograph Capsule | G2 Esports | MLG Columbus 2016", - "caseImage": "assets/cases/2024.webp", + "containerImage": "assets/containers/2024.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1442,7 +1750,7 @@ { "id": "2028", "name": "Autograph Capsule | Gambit Gaming | MLG Columbus 2016", - "caseImage": "assets/cases/2028.webp", + "containerImage": "assets/containers/2028.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1454,7 +1762,7 @@ { "id": "2037", "name": "Autograph Capsule | Legends (Foil) | MLG Columbus 2016", - "caseImage": "assets/cases/2037.webp", + "containerImage": "assets/containers/2037.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1466,7 +1774,7 @@ { "id": "2040", "name": "Autograph Capsule | Luminosity Gaming | MLG Columbus 2016", - "caseImage": "assets/cases/2040.webp", + "containerImage": "assets/containers/2040.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1478,7 +1786,7 @@ { "id": "2045", "name": "Autograph Capsule | Natus Vincere | MLG Columbus 2016", - "caseImage": "assets/cases/2045.webp", + "containerImage": "assets/containers/2045.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1490,7 +1798,7 @@ { "id": "2049", "name": "Autograph Capsule | Ninjas in Pyjamas | MLG Columbus 2016", - "caseImage": "assets/cases/2049.webp", + "containerImage": "assets/containers/2049.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1502,7 +1810,7 @@ { "id": "2056", "name": "Autograph Capsule | Splyce | MLG Columbus 2016", - "caseImage": "assets/cases/2056.webp", + "containerImage": "assets/containers/2056.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1514,7 +1822,7 @@ { "id": "2063", "name": "Autograph Capsule | Team EnVyUs | MLG Columbus 2016", - "caseImage": "assets/cases/2063.webp", + "containerImage": "assets/containers/2063.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1526,7 +1834,7 @@ { "id": "2069", "name": "Autograph Capsule | Team Liquid | MLG Columbus 2016", - "caseImage": "assets/cases/2069.webp", + "containerImage": "assets/containers/2069.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1538,7 +1846,7 @@ { "id": "2080", "name": "Autograph Capsule | Virtus.Pro | MLG Columbus 2016", - "caseImage": "assets/cases/2080.webp", + "containerImage": "assets/containers/2080.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1550,7 +1858,7 @@ { "id": "2085", "name": "Autograph Capsule | mousesports | MLG Columbus 2016", - "caseImage": "assets/cases/2085.webp", + "containerImage": "assets/containers/2085.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1562,7 +1870,7 @@ { "id": "292", "name": "MLG Columbus 2016 Cache Souvenir Package", - "caseImage": "assets/cases/292.webp", + "containerImage": "assets/containers/292.webp", "releaseDate": "2016-03-29", "type": "SOUVENIR_PACKAGE", "tournamentName": "MLG Columbus 2016", @@ -1574,7 +1882,7 @@ { "id": "2198", "name": "MLG Columbus 2016 Challengers (Holo/Foil)", - "caseImage": "assets/cases/2198.webp", + "containerImage": "assets/containers/2198.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1586,7 +1894,7 @@ { "id": "293", "name": "MLG Columbus 2016 Cobblestone Souvenir Package", - "caseImage": "assets/cases/293.webp", + "containerImage": "assets/containers/293.webp", "releaseDate": "2016-03-29", "type": "SOUVENIR_PACKAGE", "tournamentName": "MLG Columbus 2016", @@ -1598,7 +1906,7 @@ { "id": "294", "name": "MLG Columbus 2016 Dust II Souvenir Package", - "caseImage": "assets/cases/294.webp", + "containerImage": "assets/containers/294.webp", "releaseDate": "2016-03-29", "type": "SOUVENIR_PACKAGE", "tournamentName": "MLG Columbus 2016", @@ -1610,7 +1918,7 @@ { "id": "295", "name": "MLG Columbus 2016 Inferno Souvenir Package", - "caseImage": "assets/cases/295.webp", + "containerImage": "assets/containers/295.webp", "releaseDate": "2016-03-29", "type": "SOUVENIR_PACKAGE", "tournamentName": "MLG Columbus 2016", @@ -1622,7 +1930,7 @@ { "id": "2197", "name": "MLG Columbus 2016 Legends (Holo/Foil)", - "caseImage": "assets/cases/2197.webp", + "containerImage": "assets/containers/2197.webp", "releaseDate": "2016-03-29", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1634,7 +1942,7 @@ { "id": "296", "name": "MLG Columbus 2016 Mirage Souvenir Package", - "caseImage": "assets/cases/296.webp", + "containerImage": "assets/containers/296.webp", "releaseDate": "2016-03-29", "type": "SOUVENIR_PACKAGE", "tournamentName": "MLG Columbus 2016", @@ -1646,7 +1954,7 @@ { "id": "297", "name": "MLG Columbus 2016 Nuke Souvenir Package", - "caseImage": "assets/cases/297.webp", + "containerImage": "assets/containers/297.webp", "releaseDate": "2016-03-29", "type": "SOUVENIR_PACKAGE", "tournamentName": "MLG Columbus 2016", @@ -1658,7 +1966,7 @@ { "id": "298", "name": "MLG Columbus 2016 Overpass Souvenir Package", - "caseImage": "assets/cases/298.webp", + "containerImage": "assets/containers/298.webp", "releaseDate": "2016-03-29", "type": "SOUVENIR_PACKAGE", "tournamentName": "MLG Columbus 2016", @@ -1670,7 +1978,7 @@ { "id": "299", "name": "MLG Columbus 2016 Train Souvenir Package", - "caseImage": "assets/cases/299.webp", + "containerImage": "assets/containers/299.webp", "releaseDate": "2016-03-29", "type": "SOUVENIR_PACKAGE", "tournamentName": "MLG Columbus 2016", @@ -1682,7 +1990,7 @@ { "id": "177", "name": "Chroma 3 Case", - "caseImage": "assets/cases/177.webp", + "containerImage": "assets/containers/177.webp", "releaseDate": "2016-04-27", "type": "CASE", "tournamentName": null, @@ -1694,7 +2002,7 @@ { "id": "2105", "name": "Collectible Pins Capsule Series 1", - "caseImage": "assets/cases/2105.webp", + "containerImage": "assets/containers/2105.webp", "releaseDate": "2016-06-01", "type": "PIN_CAPSULE", "tournamentName": null, @@ -1706,7 +2014,7 @@ { "id": "254", "name": "Gamma Case", - "caseImage": "assets/cases/254.webp", + "containerImage": "assets/containers/254.webp", "releaseDate": "2016-06-15", "type": "CASE", "tournamentName": null, @@ -1718,7 +2026,7 @@ { "id": "1995", "name": "Autograph Capsule | Astralis | Cologne 2016", - "caseImage": "assets/cases/1995.webp", + "containerImage": "assets/containers/1995.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1730,7 +2038,7 @@ { "id": "1999", "name": "Autograph Capsule | Challengers (Foil) | Cologne 2016", - "caseImage": "assets/cases/1999.webp", + "containerImage": "assets/containers/1999.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1742,7 +2050,7 @@ { "id": "2006", "name": "Autograph Capsule | Counter Logic Gaming | Cologne 2016", - "caseImage": "assets/cases/2006.webp", + "containerImage": "assets/containers/2006.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1754,7 +2062,7 @@ { "id": "2009", "name": "Autograph Capsule | FaZe Clan | Cologne 2016", - "caseImage": "assets/cases/2009.webp", + "containerImage": "assets/containers/2009.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1766,7 +2074,7 @@ { "id": "2014", "name": "Autograph Capsule | Flipsid3 Tactics | Cologne 2016", - "caseImage": "assets/cases/2014.webp", + "containerImage": "assets/containers/2014.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1778,7 +2086,7 @@ { "id": "2019", "name": "Autograph Capsule | Fnatic | Cologne 2016", - "caseImage": "assets/cases/2019.webp", + "containerImage": "assets/containers/2019.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1790,7 +2098,7 @@ { "id": "2023", "name": "Autograph Capsule | G2 Esports | Cologne 2016", - "caseImage": "assets/cases/2023.webp", + "containerImage": "assets/containers/2023.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1802,7 +2110,7 @@ { "id": "2027", "name": "Autograph Capsule | Gambit Gaming | Cologne 2016", - "caseImage": "assets/cases/2027.webp", + "containerImage": "assets/containers/2027.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1814,7 +2122,7 @@ { "id": "2036", "name": "Autograph Capsule | Legends (Foil) | Cologne 2016", - "caseImage": "assets/cases/2036.webp", + "containerImage": "assets/containers/2036.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1826,7 +2134,7 @@ { "id": "2044", "name": "Autograph Capsule | Natus Vincere | Cologne 2016", - "caseImage": "assets/cases/2044.webp", + "containerImage": "assets/containers/2044.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1838,7 +2146,7 @@ { "id": "2048", "name": "Autograph Capsule | Ninjas in Pyjamas | Cologne 2016", - "caseImage": "assets/cases/2048.webp", + "containerImage": "assets/containers/2048.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1850,7 +2158,7 @@ { "id": "2052", "name": "Autograph Capsule | OpTic Gaming | Cologne 2016", - "caseImage": "assets/cases/2052.webp", + "containerImage": "assets/containers/2052.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1862,7 +2170,7 @@ { "id": "2055", "name": "Autograph Capsule | SK Gaming | Cologne 2016", - "caseImage": "assets/cases/2055.webp", + "containerImage": "assets/containers/2055.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1874,7 +2182,7 @@ { "id": "2058", "name": "Autograph Capsule | Team Dignitas | Cologne 2016", - "caseImage": "assets/cases/2058.webp", + "containerImage": "assets/containers/2058.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1886,7 +2194,7 @@ { "id": "2062", "name": "Autograph Capsule | Team EnVyUs | Cologne 2016", - "caseImage": "assets/cases/2062.webp", + "containerImage": "assets/containers/2062.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1898,7 +2206,7 @@ { "id": "2068", "name": "Autograph Capsule | Team Liquid | Cologne 2016", - "caseImage": "assets/cases/2068.webp", + "containerImage": "assets/containers/2068.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1910,7 +2218,7 @@ { "id": "2079", "name": "Autograph Capsule | Virtus.Pro | Cologne 2016", - "caseImage": "assets/cases/2079.webp", + "containerImage": "assets/containers/2079.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1922,7 +2230,7 @@ { "id": "2084", "name": "Autograph Capsule | mousesports | Cologne 2016", - "caseImage": "assets/cases/2084.webp", + "containerImage": "assets/containers/2084.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1934,7 +2242,7 @@ { "id": "183", "name": "Cologne 2016 Cache Souvenir Package", - "caseImage": "assets/cases/183.webp", + "containerImage": "assets/containers/183.webp", "releaseDate": "2016-07-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2016", @@ -1946,7 +2254,7 @@ { "id": "2200", "name": "Cologne 2016 Challengers (Holo/Foil)", - "caseImage": "assets/cases/2200.webp", + "containerImage": "assets/containers/2200.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1958,7 +2266,7 @@ { "id": "184", "name": "Cologne 2016 Cobblestone Souvenir Package", - "caseImage": "assets/cases/184.webp", + "containerImage": "assets/containers/184.webp", "releaseDate": "2016-07-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2016", @@ -1970,7 +2278,7 @@ { "id": "185", "name": "Cologne 2016 Dust II Souvenir Package", - "caseImage": "assets/cases/185.webp", + "containerImage": "assets/containers/185.webp", "releaseDate": "2016-07-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2016", @@ -1982,7 +2290,7 @@ { "id": "2199", "name": "Cologne 2016 Legends (Holo/Foil)", - "caseImage": "assets/cases/2199.webp", + "containerImage": "assets/containers/2199.webp", "releaseDate": "2016-07-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -1994,7 +2302,7 @@ { "id": "186", "name": "Cologne 2016 Mirage Souvenir Package", - "caseImage": "assets/cases/186.webp", + "containerImage": "assets/containers/186.webp", "releaseDate": "2016-07-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2016", @@ -2006,7 +2314,7 @@ { "id": "187", "name": "Cologne 2016 Nuke Souvenir Package", - "caseImage": "assets/cases/187.webp", + "containerImage": "assets/containers/187.webp", "releaseDate": "2016-07-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2016", @@ -2018,7 +2326,7 @@ { "id": "188", "name": "Cologne 2016 Overpass Souvenir Package", - "caseImage": "assets/cases/188.webp", + "containerImage": "assets/containers/188.webp", "releaseDate": "2016-07-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2016", @@ -2030,7 +2338,7 @@ { "id": "189", "name": "Cologne 2016 Train Souvenir Package", - "caseImage": "assets/cases/189.webp", + "containerImage": "assets/containers/189.webp", "releaseDate": "2016-07-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "ESL One Cologne 2016", @@ -2042,7 +2350,7 @@ { "id": "2090", "name": "Bestiary Capsule", - "caseImage": "assets/cases/2090.webp", + "containerImage": "assets/containers/2090.webp", "releaseDate": "2016-08-16", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2054,7 +2362,7 @@ { "id": "2168", "name": "StatTrakā„¢ Radicals Box", - "caseImage": "assets/cases/2168.webp", + "containerImage": "assets/containers/2168.webp", "releaseDate": "2016-08-16", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -2066,7 +2374,7 @@ { "id": "2177", "name": "Sugarface Capsule", - "caseImage": "assets/cases/2177.webp", + "containerImage": "assets/containers/2177.webp", "releaseDate": "2016-08-16", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2078,7 +2386,7 @@ { "id": "253", "name": "Gamma 2 Case", - "caseImage": "assets/cases/253.webp", + "containerImage": "assets/containers/253.webp", "releaseDate": "2016-08-18", "type": "CASE", "tournamentName": null, @@ -2090,7 +2398,7 @@ { "id": "2106", "name": "Collectible Pins Capsule Series 2", - "caseImage": "assets/cases/2106.webp", + "containerImage": "assets/containers/2106.webp", "releaseDate": "2016-09-28", "type": "PIN_CAPSULE", "tournamentName": null, @@ -2102,7 +2410,7 @@ { "id": "2231", "name": "CS:GO Graffiti Box", - "caseImage": "assets/cases/2231.webp", + "containerImage": "assets/containers/2231.webp", "releaseDate": "2016-10-06", "type": "GRAFFITI_BOX", "tournamentName": null, @@ -2114,7 +2422,7 @@ { "id": "2232", "name": "Community Graffiti Box 1", - "caseImage": "assets/cases/2232.webp", + "containerImage": "assets/containers/2232.webp", "releaseDate": "2016-10-06", "type": "GRAFFITI_BOX", "tournamentName": null, @@ -2126,7 +2434,7 @@ { "id": "255", "name": "Glove Case", - "caseImage": "assets/cases/255.webp", + "containerImage": "assets/containers/255.webp", "releaseDate": "2016-11-28", "type": "CASE", "tournamentName": null, @@ -2138,7 +2446,7 @@ { "id": "19", "name": "Atlanta 2017 Cache Souvenir Package", - "caseImage": "assets/cases/19.webp", + "containerImage": "assets/containers/19.webp", "releaseDate": "2017-01-22", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Atlanta 2017", @@ -2150,7 +2458,7 @@ { "id": "2202", "name": "Atlanta 2017 Challengers (Holo/Foil)", - "caseImage": "assets/cases/2202.webp", + "containerImage": "assets/containers/2202.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2162,7 +2470,7 @@ { "id": "20", "name": "Atlanta 2017 Cobblestone Souvenir Package", - "caseImage": "assets/cases/20.webp", + "containerImage": "assets/containers/20.webp", "releaseDate": "2017-01-22", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Atlanta 2017", @@ -2174,7 +2482,7 @@ { "id": "21", "name": "Atlanta 2017 Dust II Souvenir Package", - "caseImage": "assets/cases/21.webp", + "containerImage": "assets/containers/21.webp", "releaseDate": "2017-01-22", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Atlanta 2017", @@ -2186,7 +2494,7 @@ { "id": "2201", "name": "Atlanta 2017 Legends (Holo/Foil)", - "caseImage": "assets/cases/2201.webp", + "containerImage": "assets/containers/2201.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2198,7 +2506,7 @@ { "id": "22", "name": "Atlanta 2017 Mirage Souvenir Package", - "caseImage": "assets/cases/22.webp", + "containerImage": "assets/containers/22.webp", "releaseDate": "2017-01-22", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Atlanta 2017", @@ -2210,7 +2518,7 @@ { "id": "23", "name": "Atlanta 2017 Nuke Souvenir Package", - "caseImage": "assets/cases/23.webp", + "containerImage": "assets/containers/23.webp", "releaseDate": "2017-01-22", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Atlanta 2017", @@ -2222,7 +2530,7 @@ { "id": "24", "name": "Atlanta 2017 Overpass Souvenir Package", - "caseImage": "assets/cases/24.webp", + "containerImage": "assets/containers/24.webp", "releaseDate": "2017-01-22", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Atlanta 2017", @@ -2234,7 +2542,7 @@ { "id": "25", "name": "Atlanta 2017 Train Souvenir Package", - "caseImage": "assets/cases/25.webp", + "containerImage": "assets/containers/25.webp", "releaseDate": "2017-01-22", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Atlanta 2017", @@ -2246,7 +2554,7 @@ { "id": "1994", "name": "Autograph Capsule | Astralis | Atlanta 2017", - "caseImage": "assets/cases/1994.webp", + "containerImage": "assets/containers/1994.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2258,7 +2566,7 @@ { "id": "1997", "name": "Autograph Capsule | Challengers (Foil) | Atlanta 2017", - "caseImage": "assets/cases/1997.webp", + "containerImage": "assets/containers/1997.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2270,7 +2578,7 @@ { "id": "2008", "name": "Autograph Capsule | FaZe Clan | Atlanta 2017", - "caseImage": "assets/cases/2008.webp", + "containerImage": "assets/containers/2008.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2282,7 +2590,7 @@ { "id": "2011", "name": "Autograph Capsule | Flipsid3 Tactics | Atlanta 2017", - "caseImage": "assets/cases/2011.webp", + "containerImage": "assets/containers/2011.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2294,7 +2602,7 @@ { "id": "2016", "name": "Autograph Capsule | Fnatic | Atlanta 2017", - "caseImage": "assets/cases/2016.webp", + "containerImage": "assets/containers/2016.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2306,7 +2614,7 @@ { "id": "2021", "name": "Autograph Capsule | G2 Esports | Atlanta 2017", - "caseImage": "assets/cases/2021.webp", + "containerImage": "assets/containers/2021.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2318,7 +2626,7 @@ { "id": "2025", "name": "Autograph Capsule | GODSENT | Atlanta 2017", - "caseImage": "assets/cases/2025.webp", + "containerImage": "assets/containers/2025.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2330,7 +2638,7 @@ { "id": "2026", "name": "Autograph Capsule | Gambit Gaming | Atlanta 2017", - "caseImage": "assets/cases/2026.webp", + "containerImage": "assets/containers/2026.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2342,7 +2650,7 @@ { "id": "2033", "name": "Autograph Capsule | HellRaisers | Atlanta 2017", - "caseImage": "assets/cases/2033.webp", + "containerImage": "assets/containers/2033.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2354,7 +2662,7 @@ { "id": "2034", "name": "Autograph Capsule | Legends (Foil) | Atlanta 2017", - "caseImage": "assets/cases/2034.webp", + "containerImage": "assets/containers/2034.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2366,7 +2674,7 @@ { "id": "2041", "name": "Autograph Capsule | Natus Vincere | Atlanta 2017", - "caseImage": "assets/cases/2041.webp", + "containerImage": "assets/containers/2041.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2378,7 +2686,7 @@ { "id": "2050", "name": "Autograph Capsule | North | Atlanta 2017", - "caseImage": "assets/cases/2050.webp", + "containerImage": "assets/containers/2050.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2390,7 +2698,7 @@ { "id": "2051", "name": "Autograph Capsule | OpTic Gaming | Atlanta 2017", - "caseImage": "assets/cases/2051.webp", + "containerImage": "assets/containers/2051.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2402,7 +2710,7 @@ { "id": "2054", "name": "Autograph Capsule | SK Gaming | Atlanta 2017", - "caseImage": "assets/cases/2054.webp", + "containerImage": "assets/containers/2054.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2414,7 +2722,7 @@ { "id": "2059", "name": "Autograph Capsule | Team EnVyUs | Atlanta 2017", - "caseImage": "assets/cases/2059.webp", + "containerImage": "assets/containers/2059.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2426,7 +2734,7 @@ { "id": "2066", "name": "Autograph Capsule | Team Liquid | Atlanta 2017", - "caseImage": "assets/cases/2066.webp", + "containerImage": "assets/containers/2066.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2438,7 +2746,7 @@ { "id": "2076", "name": "Autograph Capsule | Virtus.Pro | Atlanta 2017", - "caseImage": "assets/cases/2076.webp", + "containerImage": "assets/containers/2076.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2450,7 +2758,7 @@ { "id": "2081", "name": "Autograph Capsule | mousesports | Atlanta 2017", - "caseImage": "assets/cases/2081.webp", + "containerImage": "assets/containers/2081.webp", "releaseDate": "2017-01-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2462,7 +2770,7 @@ { "id": "370", "name": "Spectrum Case", - "caseImage": "assets/cases/370.webp", + "containerImage": "assets/containers/370.webp", "releaseDate": "2017-03-15", "type": "CASE", "tournamentName": null, @@ -2474,7 +2782,7 @@ { "id": "306", "name": "Operation Hydra Case", - "caseImage": "assets/cases/306.webp", + "containerImage": "assets/containers/306.webp", "releaseDate": "2017-05-23", "type": "CASE", "tournamentName": null, @@ -2486,7 +2794,7 @@ { "id": "273", "name": "Krakow 2017 Cache Souvenir Package", - "caseImage": "assets/cases/273.webp", + "containerImage": "assets/containers/273.webp", "releaseDate": "2017-07-16", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Kraków 2017", @@ -2498,7 +2806,7 @@ { "id": "2204", "name": "Krakow 2017 Challengers (Holo/Foil)", - "caseImage": "assets/cases/2204.webp", + "containerImage": "assets/containers/2204.webp", "releaseDate": "2017-07-16", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2510,7 +2818,7 @@ { "id": "2128", "name": "Krakow 2017 Challengers Autograph Capsule", - "caseImage": "assets/cases/2128.webp", + "containerImage": "assets/containers/2128.webp", "releaseDate": "2017-07-16", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2522,7 +2830,7 @@ { "id": "275", "name": "Krakow 2017 Cobblestone Souvenir Package", - "caseImage": "assets/cases/275.webp", + "containerImage": "assets/containers/275.webp", "releaseDate": "2017-07-16", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Kraków 2017", @@ -2534,7 +2842,7 @@ { "id": "276", "name": "Krakow 2017 Inferno Souvenir Package", - "caseImage": "assets/cases/276.webp", + "containerImage": "assets/containers/276.webp", "releaseDate": "2017-07-16", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Kraków 2017", @@ -2546,7 +2854,7 @@ { "id": "2203", "name": "Krakow 2017 Legends (Holo/Foil)", - "caseImage": "assets/cases/2203.webp", + "containerImage": "assets/containers/2203.webp", "releaseDate": "2017-07-16", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2558,7 +2866,7 @@ { "id": "2129", "name": "Krakow 2017 Legends Autograph Capsule", - "caseImage": "assets/cases/2129.webp", + "containerImage": "assets/containers/2129.webp", "releaseDate": "2017-07-16", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2570,7 +2878,7 @@ { "id": "278", "name": "Krakow 2017 Mirage Souvenir Package", - "caseImage": "assets/cases/278.webp", + "containerImage": "assets/containers/278.webp", "releaseDate": "2017-07-16", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Kraków 2017", @@ -2582,7 +2890,7 @@ { "id": "279", "name": "Krakow 2017 Nuke Souvenir Package", - "caseImage": "assets/cases/279.webp", + "containerImage": "assets/containers/279.webp", "releaseDate": "2017-07-16", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Kraków 2017", @@ -2594,7 +2902,7 @@ { "id": "280", "name": "Krakow 2017 Overpass Souvenir Package", - "caseImage": "assets/cases/280.webp", + "containerImage": "assets/containers/280.webp", "releaseDate": "2017-07-16", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Kraków 2017", @@ -2606,7 +2914,7 @@ { "id": "281", "name": "Krakow 2017 Train Souvenir Package", - "caseImage": "assets/cases/281.webp", + "containerImage": "assets/containers/281.webp", "releaseDate": "2017-07-16", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Kraków 2017", @@ -2618,7 +2926,7 @@ { "id": "2233", "name": "Perfect World Graffiti Box", - "caseImage": "assets/cases/2233.webp", + "containerImage": "assets/containers/2233.webp", "releaseDate": "2017-09-14", "type": "GRAFFITI_BOX", "tournamentName": null, @@ -2630,7 +2938,7 @@ { "id": "369", "name": "Spectrum 2 Case", - "caseImage": "assets/cases/369.webp", + "containerImage": "assets/containers/369.webp", "releaseDate": "2017-09-14", "type": "CASE", "tournamentName": null, @@ -2642,7 +2950,7 @@ { "id": "2143", "name": "Perfect World Sticker Capsule 1", - "caseImage": "assets/cases/2143.webp", + "containerImage": "assets/containers/2143.webp", "releaseDate": "2017-09-15", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2654,7 +2962,7 @@ { "id": "2144", "name": "Perfect World Sticker Capsule 2", - "caseImage": "assets/cases/2144.webp", + "containerImage": "assets/containers/2144.webp", "releaseDate": "2017-09-15", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2666,7 +2974,7 @@ { "id": "2108", "name": "Community Capsule 2018", - "caseImage": "assets/cases/2108.webp", + "containerImage": "assets/containers/2108.webp", "releaseDate": "2017-12-11", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2678,7 +2986,7 @@ { "id": "2206", "name": "Boston 2018 Attending Legends (Holo/Foil)", - "caseImage": "assets/cases/2206.webp", + "containerImage": "assets/containers/2206.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2690,7 +2998,7 @@ { "id": "2091", "name": "Boston 2018 Attending Legends Autograph Capsule", - "caseImage": "assets/cases/2091.webp", + "containerImage": "assets/containers/2091.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2702,7 +3010,7 @@ { "id": "145", "name": "Boston 2018 Cache Souvenir Package", - "caseImage": "assets/cases/145.webp", + "containerImage": "assets/containers/145.webp", "releaseDate": "2018-01-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Boston 2018", @@ -2714,7 +3022,7 @@ { "id": "146", "name": "Boston 2018 Cobblestone Souvenir Package", - "caseImage": "assets/cases/146.webp", + "containerImage": "assets/containers/146.webp", "releaseDate": "2018-01-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Boston 2018", @@ -2726,7 +3034,7 @@ { "id": "147", "name": "Boston 2018 Inferno Souvenir Package", - "caseImage": "assets/cases/147.webp", + "containerImage": "assets/containers/147.webp", "releaseDate": "2018-01-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Boston 2018", @@ -2738,7 +3046,7 @@ { "id": "2205", "name": "Boston 2018 Legends (Holo/Foil)", - "caseImage": "assets/cases/2205.webp", + "containerImage": "assets/containers/2205.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2750,7 +3058,7 @@ { "id": "2092", "name": "Boston 2018 Legends Autograph Capsule", - "caseImage": "assets/cases/2092.webp", + "containerImage": "assets/containers/2092.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2762,7 +3070,7 @@ { "id": "2208", "name": "Boston 2018 Minor Challengers (Holo/Foil)", - "caseImage": "assets/cases/2208.webp", + "containerImage": "assets/containers/2208.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2774,7 +3082,7 @@ { "id": "2093", "name": "Boston 2018 Minor Challengers Autograph Capsule", - "caseImage": "assets/cases/2093.webp", + "containerImage": "assets/containers/2093.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2786,7 +3094,7 @@ { "id": "2209", "name": "Boston 2018 Minor Challengers with Flash Gaming (Holo/Foil)", - "caseImage": "assets/cases/2209.webp", + "containerImage": "assets/containers/2209.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2798,7 +3106,7 @@ { "id": "2094", "name": "Boston 2018 Minor Challengers with Flash Gaming Autograph Capsule", - "caseImage": "assets/cases/2094.webp", + "containerImage": "assets/containers/2094.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2810,7 +3118,7 @@ { "id": "151", "name": "Boston 2018 Mirage Souvenir Package", - "caseImage": "assets/cases/151.webp", + "containerImage": "assets/containers/151.webp", "releaseDate": "2018-01-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Boston 2018", @@ -2822,7 +3130,7 @@ { "id": "152", "name": "Boston 2018 Nuke Souvenir Package", - "caseImage": "assets/cases/152.webp", + "containerImage": "assets/containers/152.webp", "releaseDate": "2018-01-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Boston 2018", @@ -2834,7 +3142,7 @@ { "id": "153", "name": "Boston 2018 Overpass Souvenir Package", - "caseImage": "assets/cases/153.webp", + "containerImage": "assets/containers/153.webp", "releaseDate": "2018-01-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Boston 2018", @@ -2846,7 +3154,7 @@ { "id": "2207", "name": "Boston 2018 Returning Challengers (Holo/Foil)", - "caseImage": "assets/cases/2207.webp", + "containerImage": "assets/containers/2207.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2858,7 +3166,7 @@ { "id": "2095", "name": "Boston 2018 Returning Challengers Autograph Capsule", - "caseImage": "assets/cases/2095.webp", + "containerImage": "assets/containers/2095.webp", "releaseDate": "2018-01-12", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2870,7 +3178,7 @@ { "id": "155", "name": "Boston 2018 Train Souvenir Package", - "caseImage": "assets/cases/155.webp", + "containerImage": "assets/containers/155.webp", "releaseDate": "2018-01-12", "type": "SOUVENIR_PACKAGE", "tournamentName": "ELEAGUE Boston 2018", @@ -2882,7 +3190,7 @@ { "id": "179", "name": "Clutch Case", - "caseImage": "assets/cases/179.webp", + "containerImage": "assets/containers/179.webp", "releaseDate": "2018-02-14", "type": "CASE", "tournamentName": null, @@ -2894,7 +3202,7 @@ { "id": "2107", "name": "Collectible Pins Capsule Series 3", - "caseImage": "assets/cases/2107.webp", + "containerImage": "assets/containers/2107.webp", "releaseDate": "2018-03-01", "type": "PIN_CAPSULE", "tournamentName": null, @@ -2906,7 +3214,7 @@ { "id": "259", "name": "Horizon Case", - "caseImage": "assets/cases/259.webp", + "containerImage": "assets/containers/259.webp", "releaseDate": "2018-08-02", "type": "CASE", "tournamentName": null, @@ -2918,7 +3226,7 @@ { "id": "282", "name": "London 2018 Cache Souvenir Package", - "caseImage": "assets/cases/282.webp", + "containerImage": "assets/containers/282.webp", "releaseDate": "2018-09-05", "type": "SOUVENIR_PACKAGE", "tournamentName": "FACEIT London 2018", @@ -2930,7 +3238,7 @@ { "id": "283", "name": "London 2018 Dust II Souvenir Package", - "caseImage": "assets/cases/283.webp", + "containerImage": "assets/containers/283.webp", "releaseDate": "2018-09-05", "type": "SOUVENIR_PACKAGE", "tournamentName": "FACEIT London 2018", @@ -2942,7 +3250,7 @@ { "id": "284", "name": "London 2018 Inferno Souvenir Package", - "caseImage": "assets/cases/284.webp", + "containerImage": "assets/containers/284.webp", "releaseDate": "2018-09-05", "type": "SOUVENIR_PACKAGE", "tournamentName": "FACEIT London 2018", @@ -2954,7 +3262,7 @@ { "id": "2210", "name": "London 2018 Legends (Holo/Foil)", - "caseImage": "assets/cases/2210.webp", + "containerImage": "assets/containers/2210.webp", "releaseDate": "2018-09-05", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2966,7 +3274,7 @@ { "id": "2130", "name": "London 2018 Legends Autograph Capsule", - "caseImage": "assets/cases/2130.webp", + "containerImage": "assets/containers/2130.webp", "releaseDate": "2018-09-05", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2978,7 +3286,7 @@ { "id": "2212", "name": "London 2018 Minor Challengers (Holo/Foil)", - "caseImage": "assets/cases/2212.webp", + "containerImage": "assets/containers/2212.webp", "releaseDate": "2018-09-05", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -2990,7 +3298,7 @@ { "id": "2131", "name": "London 2018 Minor Challengers Autograph Capsule", - "caseImage": "assets/cases/2131.webp", + "containerImage": "assets/containers/2131.webp", "releaseDate": "2018-09-05", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3002,7 +3310,7 @@ { "id": "287", "name": "London 2018 Mirage Souvenir Package", - "caseImage": "assets/cases/287.webp", + "containerImage": "assets/containers/287.webp", "releaseDate": "2018-09-05", "type": "SOUVENIR_PACKAGE", "tournamentName": "FACEIT London 2018", @@ -3014,7 +3322,7 @@ { "id": "288", "name": "London 2018 Nuke Souvenir Package", - "caseImage": "assets/cases/288.webp", + "containerImage": "assets/containers/288.webp", "releaseDate": "2018-09-05", "type": "SOUVENIR_PACKAGE", "tournamentName": "FACEIT London 2018", @@ -3026,7 +3334,7 @@ { "id": "289", "name": "London 2018 Overpass Souvenir Package", - "caseImage": "assets/cases/289.webp", + "containerImage": "assets/containers/289.webp", "releaseDate": "2018-09-05", "type": "SOUVENIR_PACKAGE", "tournamentName": "FACEIT London 2018", @@ -3038,7 +3346,7 @@ { "id": "2211", "name": "London 2018 Returning Challengers (Holo/Foil)", - "caseImage": "assets/cases/2211.webp", + "containerImage": "assets/containers/2211.webp", "releaseDate": "2018-09-05", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3050,7 +3358,7 @@ { "id": "2132", "name": "London 2018 Returning Challengers Autograph Capsule", - "caseImage": "assets/cases/2132.webp", + "containerImage": "assets/containers/2132.webp", "releaseDate": "2018-09-05", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3062,7 +3370,7 @@ { "id": "291", "name": "London 2018 Train Souvenir Package", - "caseImage": "assets/cases/291.webp", + "containerImage": "assets/containers/291.webp", "releaseDate": "2018-09-05", "type": "SOUVENIR_PACKAGE", "tournamentName": "FACEIT London 2018", @@ -3074,7 +3382,7 @@ { "id": "2161", "name": "Skill Groups Capsule", - "caseImage": "assets/cases/2161.webp", + "containerImage": "assets/containers/2161.webp", "releaseDate": "2018-11-15", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3086,7 +3394,7 @@ { "id": "206", "name": "Danger Zone Case", - "caseImage": "assets/cases/206.webp", + "containerImage": "assets/containers/206.webp", "releaseDate": "2018-12-06", "type": "CASE", "tournamentName": null, @@ -3098,7 +3406,7 @@ { "id": "262", "name": "Katowice 2019 Cache Souvenir Package", - "caseImage": "assets/cases/262.webp", + "containerImage": "assets/containers/262.webp", "releaseDate": "2019-02-13", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Katowice 2019", @@ -3110,7 +3418,7 @@ { "id": "263", "name": "Katowice 2019 Dust II Souvenir Package", - "caseImage": "assets/cases/263.webp", + "containerImage": "assets/containers/263.webp", "releaseDate": "2019-02-13", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Katowice 2019", @@ -3122,7 +3430,7 @@ { "id": "264", "name": "Katowice 2019 Inferno Souvenir Package", - "caseImage": "assets/cases/264.webp", + "containerImage": "assets/containers/264.webp", "releaseDate": "2019-02-13", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Katowice 2019", @@ -3134,7 +3442,7 @@ { "id": "2213", "name": "Katowice 2019 Legends (Holo/Foil)", - "caseImage": "assets/cases/2213.webp", + "containerImage": "assets/containers/2213.webp", "releaseDate": "2019-02-13", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3146,7 +3454,7 @@ { "id": "2125", "name": "Katowice 2019 Legends Autograph Capsule", - "caseImage": "assets/cases/2125.webp", + "containerImage": "assets/containers/2125.webp", "releaseDate": "2019-02-13", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3158,7 +3466,7 @@ { "id": "2214", "name": "Katowice 2019 Minor Challengers (Holo/Foil)", - "caseImage": "assets/cases/2214.webp", + "containerImage": "assets/containers/2214.webp", "releaseDate": "2019-02-13", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3170,7 +3478,7 @@ { "id": "2126", "name": "Katowice 2019 Minor Challengers Autograph Capsule", - "caseImage": "assets/cases/2126.webp", + "containerImage": "assets/containers/2126.webp", "releaseDate": "2019-02-13", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3182,7 +3490,7 @@ { "id": "267", "name": "Katowice 2019 Mirage Souvenir Package", - "caseImage": "assets/cases/267.webp", + "containerImage": "assets/containers/267.webp", "releaseDate": "2019-02-13", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Katowice 2019", @@ -3194,7 +3502,7 @@ { "id": "268", "name": "Katowice 2019 Nuke Souvenir Package", - "caseImage": "assets/cases/268.webp", + "containerImage": "assets/containers/268.webp", "releaseDate": "2019-02-13", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Katowice 2019", @@ -3206,7 +3514,7 @@ { "id": "269", "name": "Katowice 2019 Overpass Souvenir Package", - "caseImage": "assets/cases/269.webp", + "containerImage": "assets/containers/269.webp", "releaseDate": "2019-02-13", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Katowice 2019", @@ -3218,7 +3526,7 @@ { "id": "2215", "name": "Katowice 2019 Returning Challengers (Holo/Foil)", - "caseImage": "assets/cases/2215.webp", + "containerImage": "assets/containers/2215.webp", "releaseDate": "2019-02-13", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3230,7 +3538,7 @@ { "id": "2127", "name": "Katowice 2019 Returning Challengers Autograph Capsule", - "caseImage": "assets/cases/2127.webp", + "containerImage": "assets/containers/2127.webp", "releaseDate": "2019-02-13", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3242,7 +3550,7 @@ { "id": "271", "name": "Katowice 2019 Train Souvenir Package", - "caseImage": "assets/cases/271.webp", + "containerImage": "assets/containers/271.webp", "releaseDate": "2019-02-13", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Katowice 2019", @@ -3254,7 +3562,7 @@ { "id": "330", "name": "Prisma Case", - "caseImage": "assets/cases/330.webp", + "containerImage": "assets/containers/330.webp", "releaseDate": "2019-03-13", "type": "CASE", "tournamentName": null, @@ -3266,7 +3574,7 @@ { "id": "2120", "name": "Feral Predators Capsule", - "caseImage": "assets/cases/2120.webp", + "containerImage": "assets/containers/2120.webp", "releaseDate": "2019-04-15", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3278,7 +3586,7 @@ { "id": "2104", "name": "Chicken Capsule", - "caseImage": "assets/cases/2104.webp", + "containerImage": "assets/containers/2104.webp", "releaseDate": "2019-06-10", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3290,7 +3598,7 @@ { "id": "133", "name": "Berlin 2019 Dust II Souvenir Package", - "caseImage": "assets/cases/133.webp", + "containerImage": "assets/containers/133.webp", "releaseDate": "2019-08-23", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Berlin 2019", @@ -3302,7 +3610,7 @@ { "id": "134", "name": "Berlin 2019 Inferno Souvenir Package", - "caseImage": "assets/cases/134.webp", + "containerImage": "assets/containers/134.webp", "releaseDate": "2019-08-23", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Berlin 2019", @@ -3314,7 +3622,7 @@ { "id": "2216", "name": "Berlin 2019 Legends (Holo/Foil)", - "caseImage": "assets/cases/2216.webp", + "containerImage": "assets/containers/2216.webp", "releaseDate": "2019-08-23", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3326,7 +3634,7 @@ { "id": "2087", "name": "Berlin 2019 Legends Autograph Capsule", - "caseImage": "assets/cases/2087.webp", + "containerImage": "assets/containers/2087.webp", "releaseDate": "2019-08-23", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3338,7 +3646,7 @@ { "id": "2218", "name": "Berlin 2019 Minor Challengers (Holo/Foil)", - "caseImage": "assets/cases/2218.webp", + "containerImage": "assets/containers/2218.webp", "releaseDate": "2019-08-23", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3350,7 +3658,7 @@ { "id": "2088", "name": "Berlin 2019 Minor Challengers Autograph Capsule", - "caseImage": "assets/cases/2088.webp", + "containerImage": "assets/containers/2088.webp", "releaseDate": "2019-08-23", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3362,7 +3670,7 @@ { "id": "137", "name": "Berlin 2019 Mirage Souvenir Package", - "caseImage": "assets/cases/137.webp", + "containerImage": "assets/containers/137.webp", "releaseDate": "2019-08-23", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Berlin 2019", @@ -3374,7 +3682,7 @@ { "id": "138", "name": "Berlin 2019 Nuke Souvenir Package", - "caseImage": "assets/cases/138.webp", + "containerImage": "assets/containers/138.webp", "releaseDate": "2019-08-23", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Berlin 2019", @@ -3386,7 +3694,7 @@ { "id": "139", "name": "Berlin 2019 Overpass Souvenir Package", - "caseImage": "assets/cases/139.webp", + "containerImage": "assets/containers/139.webp", "releaseDate": "2019-08-23", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Berlin 2019", @@ -3398,7 +3706,7 @@ { "id": "2217", "name": "Berlin 2019 Returning Challengers (Holo/Foil)", - "caseImage": "assets/cases/2217.webp", + "containerImage": "assets/containers/2217.webp", "releaseDate": "2019-08-23", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3410,7 +3718,7 @@ { "id": "2089", "name": "Berlin 2019 Returning Challengers Autograph Capsule", - "caseImage": "assets/cases/2089.webp", + "containerImage": "assets/containers/2089.webp", "releaseDate": "2019-08-23", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3422,7 +3730,7 @@ { "id": "141", "name": "Berlin 2019 Train Souvenir Package", - "caseImage": "assets/cases/141.webp", + "containerImage": "assets/containers/141.webp", "releaseDate": "2019-08-23", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Berlin 2019", @@ -3434,7 +3742,7 @@ { "id": "142", "name": "Berlin 2019 Vertigo Souvenir Package", - "caseImage": "assets/cases/142.webp", + "containerImage": "assets/containers/142.webp", "releaseDate": "2019-08-23", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Berlin 2019", @@ -3446,19 +3754,19 @@ { "id": "406", "name": "The X-Ray Collection", - "caseImage": "assets/cases/406.png", "releaseDate": "2019-09-30", "type": "XRAY_PACKAGE", "tournamentName": null, "tournamentLogo": null, "sourceType": null, "sourceId": null, - "sourceName": null + "sourceName": null, + "containerImage": "assets/containers/406.png" }, { "id": "402", "name": "X-Ray P250 Package", - "caseImage": "assets/cases/402.webp", + "containerImage": "assets/containers/402.webp", "releaseDate": "2019-09-30", "type": "XRAY_PACKAGE", "tournamentName": null, @@ -3470,7 +3778,7 @@ { "id": "2103", "name": "CS20 Sticker Capsule", - "caseImage": "assets/cases/2103.webp", + "containerImage": "assets/containers/2103.webp", "releaseDate": "2019-10-16", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3482,7 +3790,7 @@ { "id": "170", "name": "CS20 Case", - "caseImage": "assets/cases/170.webp", + "containerImage": "assets/containers/170.webp", "releaseDate": "2019-10-18", "type": "CASE", "tournamentName": null, @@ -3491,10 +3799,24 @@ "sourceId": null, "sourceName": null }, + { + "id": "30003", + "name": "Shattered Web Agents", + "containerImage": "assets/containers/30003.webp", + "releaseDate": "2019-11-18", + "type": "AGENT_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "SHATTERED_WEB", + "sourceName": "Operation Shattered Web", + "currency": null, + "cost": null + }, { "id": "365", "name": "Shattered Web Case", - "caseImage": "assets/cases/365.webp", + "containerImage": "assets/containers/365.webp", "releaseDate": "2019-11-18", "type": "CASE", "tournamentName": null, @@ -3506,7 +3828,7 @@ { "id": "2219", "name": "Shattered Web Sticker Collection", - "caseImage": "assets/cases/2219.webp", + "containerImage": "assets/containers/2219.webp", "releaseDate": "2019-11-18", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -3515,10 +3837,52 @@ "sourceId": "SHATTERED_WEB", "sourceName": "Operation Shattered Web" }, + { + "id": "20006", + "name": "The Canals Collection", + "containerImage": "assets/containers/20006.webp", + "releaseDate": "2019-11-18", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "SHATTERED_WEB", + "sourceName": "Operation Shattered Web", + "currency": null, + "cost": null + }, + { + "id": "20010", + "name": "The Norse Collection", + "containerImage": "assets/containers/20010.webp", + "releaseDate": "2019-11-18", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "SHATTERED_WEB", + "sourceName": "Operation Shattered Web", + "currency": null, + "cost": null + }, + { + "id": "20002", + "name": "The St. Marc Collection", + "containerImage": "assets/containers/20002.webp", + "releaseDate": "2019-11-18", + "type": "OPERATION_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "SHATTERED_WEB", + "sourceName": "Operation Shattered Web", + "currency": null, + "cost": null + }, { "id": "2123", "name": "Halo Capsule", - "caseImage": "assets/cases/2123.webp", + "containerImage": "assets/containers/2123.webp", "releaseDate": "2019-11-25", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3530,7 +3894,7 @@ { "id": "2234", "name": "CS:GO Patch Pack", - "caseImage": "assets/cases/2234.webp", + "containerImage": "assets/containers/2234.webp", "releaseDate": "2020-02-24", "type": "PATCH_PACK", "tournamentName": null, @@ -3542,7 +3906,7 @@ { "id": "2236", "name": "Metal Skill Group Patch Collection", - "caseImage": "assets/cases/2236.webp", + "containerImage": "assets/containers/2236.webp", "releaseDate": "2020-02-24", "type": "PATCH_COLLECTION", "tournamentName": null, @@ -3554,7 +3918,7 @@ { "id": "2121", "name": "Half-Life: Alyx Collectible Pins Capsule", - "caseImage": "assets/cases/2121.webp", + "containerImage": "assets/containers/2121.webp", "releaseDate": "2020-03-23", "type": "PIN_CAPSULE", "tournamentName": null, @@ -3566,7 +3930,7 @@ { "id": "2235", "name": "Half-Life: Alyx Patch Pack", - "caseImage": "assets/cases/2235.webp", + "containerImage": "assets/containers/2235.webp", "releaseDate": "2020-03-23", "type": "PATCH_PACK", "tournamentName": null, @@ -3578,7 +3942,7 @@ { "id": "2122", "name": "Half-Life: Alyx Sticker Capsule", - "caseImage": "assets/cases/2122.webp", + "containerImage": "assets/containers/2122.webp", "releaseDate": "2020-03-23", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3590,7 +3954,7 @@ { "id": "329", "name": "Prisma 2 Case", - "caseImage": "assets/cases/329.webp", + "containerImage": "assets/containers/329.webp", "releaseDate": "2020-03-31", "type": "CASE", "tournamentName": null, @@ -3602,7 +3966,7 @@ { "id": "2134", "name": "Masterminds Music Kit Box", - "caseImage": "assets/cases/2134.webp", + "containerImage": "assets/containers/2134.webp", "releaseDate": "2020-04-22", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -3614,7 +3978,7 @@ { "id": "2166", "name": "StatTrakā„¢ Masterminds Music Kit Box", - "caseImage": "assets/cases/2166.webp", + "containerImage": "assets/containers/2166.webp", "releaseDate": "2020-04-22", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -3626,7 +3990,7 @@ { "id": "2183", "name": "Warhammer 40,000 Sticker Capsule", - "caseImage": "assets/cases/2183.webp", + "containerImage": "assets/containers/2183.webp", "releaseDate": "2020-05-28", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3638,7 +4002,7 @@ { "id": "251", "name": "Fracture Case", - "caseImage": "assets/cases/251.webp", + "containerImage": "assets/containers/251.webp", "releaseDate": "2020-08-06", "type": "CASE", "tournamentName": null, @@ -3647,10 +4011,24 @@ "sourceId": null, "sourceName": null }, + { + "id": "30002", + "name": "Broken Fang Agents", + "containerImage": "assets/containers/30002.webp", + "releaseDate": "2020-12-03", + "type": "AGENT_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "BROKEN_FANG", + "sourceName": "Operation Broken Fang", + "currency": null, + "cost": null + }, { "id": "2222", "name": "Broken Fang Sticker Collection", - "caseImage": "assets/cases/2222.webp", + "containerImage": "assets/containers/2222.webp", "releaseDate": "2020-12-03", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -3662,7 +4040,7 @@ { "id": "305", "name": "Operation Broken Fang Case", - "caseImage": "assets/cases/305.webp", + "containerImage": "assets/containers/305.webp", "releaseDate": "2020-12-03", "type": "CASE", "tournamentName": null, @@ -3674,7 +4052,7 @@ { "id": "2221", "name": "Recoil Sticker Collection", - "caseImage": "assets/cases/2221.webp", + "containerImage": "assets/containers/2221.webp", "releaseDate": "2020-12-03", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -3683,10 +4061,52 @@ "sourceId": "BROKEN_FANG", "sourceName": "Operation Broken Fang" }, + { + "id": "10001", + "name": "The Ancient Collection", + "containerImage": "assets/containers/10001.webp", + "releaseDate": "2020-12-03", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "OPERATION_REWARD", + "sourceId": "BROKEN_FANG", + "sourceName": "Operation Broken Fang", + "currency": "STARS", + "cost": 4 + }, + { + "id": "10009", + "name": "The Control Collection", + "containerImage": "assets/containers/10009.webp", + "releaseDate": "2020-12-03", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "OPERATION_REWARD", + "sourceId": "BROKEN_FANG", + "sourceName": "Operation Broken Fang", + "currency": "STARS", + "cost": 4 + }, + { + "id": "10002", + "name": "The Havoc Collection", + "containerImage": "assets/containers/10002.webp", + "releaseDate": "2020-12-03", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "OPERATION_REWARD", + "sourceId": "BROKEN_FANG", + "sourceName": "Operation Broken Fang", + "currency": "STARS", + "cost": 4 + }, { "id": "2224", "name": "2020 RMR Challengers", - "caseImage": "assets/cases/2224.webp", + "containerImage": "assets/containers/2224.webp", "releaseDate": "2021-01-27", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3698,7 +4118,7 @@ { "id": "2225", "name": "2020 RMR Contenders", - "caseImage": "assets/cases/2225.webp", + "containerImage": "assets/containers/2225.webp", "releaseDate": "2021-01-27", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3710,7 +4130,7 @@ { "id": "2223", "name": "2020 RMR Legends", - "caseImage": "assets/cases/2223.webp", + "containerImage": "assets/containers/2223.webp", "releaseDate": "2021-01-27", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3722,7 +4142,7 @@ { "id": "2146", "name": "Poorly Drawn Capsule", - "caseImage": "assets/cases/2146.webp", + "containerImage": "assets/containers/2146.webp", "releaseDate": "2021-02-14", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3734,7 +4154,7 @@ { "id": "368", "name": "Snakebite Case", - "caseImage": "assets/cases/368.webp", + "containerImage": "assets/containers/368.webp", "releaseDate": "2021-05-03", "type": "CASE", "tournamentName": null, @@ -3746,7 +4166,7 @@ { "id": "2169", "name": "StatTrakā„¢ Tacticians Music Kit Box", - "caseImage": "assets/cases/2169.webp", + "containerImage": "assets/containers/2169.webp", "releaseDate": "2021-07-20", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -3758,7 +4178,7 @@ { "id": "2178", "name": "Tacticians Music Kit Box", - "caseImage": "assets/cases/2178.webp", + "containerImage": "assets/containers/2178.webp", "releaseDate": "2021-07-20", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -3770,7 +4190,7 @@ { "id": "1978", "name": "2021 Community Sticker Capsule", - "caseImage": "assets/cases/1978.webp", + "containerImage": "assets/containers/1978.webp", "releaseDate": "2021-09-02", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3779,10 +4199,24 @@ "sourceId": null, "sourceName": null }, + { + "id": "30001", + "name": "Operation Riptide Agents", + "containerImage": "assets/containers/30001.webp", + "releaseDate": "2021-09-21", + "type": "AGENT_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "LEGACY_OPERATION", + "sourceId": "RIPTIDE", + "sourceName": "Operation Riptide", + "currency": null, + "cost": null + }, { "id": "2237", "name": "Operation Riptide Patch Collection", - "caseImage": "assets/cases/2237.webp", + "containerImage": "assets/containers/2237.webp", "releaseDate": "2021-09-21", "type": "PATCH_COLLECTION", "tournamentName": null, @@ -3794,7 +4228,7 @@ { "id": "2227", "name": "Operation Riptide Sticker Collection", - "caseImage": "assets/cases/2227.webp", + "containerImage": "assets/containers/2227.webp", "releaseDate": "2021-09-21", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -3806,7 +4240,7 @@ { "id": "2226", "name": "Riptide Surf Shop Sticker Collection", - "caseImage": "assets/cases/2226.webp", + "containerImage": "assets/containers/2226.webp", "releaseDate": "2021-09-21", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -3815,10 +4249,66 @@ "sourceId": "RIPTIDE", "sourceName": "Operation Riptide" }, + { + "id": "10005", + "name": "The 2021 Dust 2 Collection", + "containerImage": "assets/containers/10005.webp", + "releaseDate": "2021-09-21", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "OPERATION_REWARD", + "sourceId": "RIPTIDE", + "sourceName": "Operation Riptide", + "currency": "STARS", + "cost": 4 + }, + { + "id": "10011", + "name": "The 2021 Mirage Collection", + "containerImage": "assets/containers/10011.webp", + "releaseDate": "2021-09-21", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "OPERATION_REWARD", + "sourceId": "RIPTIDE", + "sourceName": "Operation Riptide", + "currency": "STARS", + "cost": 4 + }, + { + "id": "10008", + "name": "The 2021 Train Collection", + "containerImage": "assets/containers/10008.webp", + "releaseDate": "2021-09-21", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "OPERATION_REWARD", + "sourceId": "RIPTIDE", + "sourceName": "Operation Riptide", + "currency": "STARS", + "cost": 4 + }, + { + "id": "10007", + "name": "The 2021 Vertigo Collection", + "containerImage": "assets/containers/10007.webp", + "releaseDate": "2021-09-21", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "OPERATION_REWARD", + "sourceId": "RIPTIDE", + "sourceName": "Operation Riptide", + "currency": "STARS", + "cost": 4 + }, { "id": "308", "name": "Operation Riptide Case", - "caseImage": "assets/cases/308.webp", + "containerImage": "assets/containers/308.webp", "releaseDate": "2021-09-22", "type": "CASE", "tournamentName": null, @@ -3830,7 +4320,7 @@ { "id": "2086", "name": "Battlefield 2042 Sticker Capsule", - "caseImage": "assets/cases/2086.webp", + "containerImage": "assets/containers/2086.webp", "releaseDate": "2021-10-07", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3842,7 +4332,7 @@ { "id": "380", "name": "Stockholm 2021 Ancient Souvenir Package", - "caseImage": "assets/cases/380.webp", + "containerImage": "assets/containers/380.webp", "releaseDate": "2021-10-26", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Stockholm 2021", @@ -3854,7 +4344,7 @@ { "id": "2238", "name": "Stockholm 2021 Challengers Patch Pack", - "caseImage": "assets/cases/2238.webp", + "containerImage": "assets/containers/2238.webp", "releaseDate": "2021-10-26", "type": "PATCH_PACK", "tournamentName": null, @@ -3866,7 +4356,7 @@ { "id": "2172", "name": "Stockholm 2021 Challengers Sticker Capsule", - "caseImage": "assets/cases/2172.webp", + "containerImage": "assets/containers/2172.webp", "releaseDate": "2021-10-26", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3878,7 +4368,7 @@ { "id": "2173", "name": "Stockholm 2021 Champions Autograph Capsule", - "caseImage": "assets/cases/2173.webp", + "containerImage": "assets/containers/2173.webp", "releaseDate": "2021-10-26", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3890,7 +4380,7 @@ { "id": "2239", "name": "Stockholm 2021 Contenders Patch Pack", - "caseImage": "assets/cases/2239.webp", + "containerImage": "assets/containers/2239.webp", "releaseDate": "2021-10-26", "type": "PATCH_PACK", "tournamentName": null, @@ -3902,7 +4392,7 @@ { "id": "2174", "name": "Stockholm 2021 Contenders Sticker Capsule", - "caseImage": "assets/cases/2174.webp", + "containerImage": "assets/containers/2174.webp", "releaseDate": "2021-10-26", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3914,7 +4404,7 @@ { "id": "384", "name": "Stockholm 2021 Dust II Souvenir Package", - "caseImage": "assets/cases/384.webp", + "containerImage": "assets/containers/384.webp", "releaseDate": "2021-10-26", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Stockholm 2021", @@ -3926,7 +4416,7 @@ { "id": "2175", "name": "Stockholm 2021 Finalists Autograph Capsule", - "caseImage": "assets/cases/2175.webp", + "containerImage": "assets/containers/2175.webp", "releaseDate": "2021-10-26", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3938,7 +4428,7 @@ { "id": "386", "name": "Stockholm 2021 Inferno Souvenir Package", - "caseImage": "assets/cases/386.webp", + "containerImage": "assets/containers/386.webp", "releaseDate": "2021-10-26", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Stockholm 2021", @@ -3950,7 +4440,7 @@ { "id": "2240", "name": "Stockholm 2021 Legends Patch Pack", - "caseImage": "assets/cases/2240.webp", + "containerImage": "assets/containers/2240.webp", "releaseDate": "2021-10-26", "type": "PATCH_PACK", "tournamentName": null, @@ -3962,7 +4452,7 @@ { "id": "2176", "name": "Stockholm 2021 Legends Sticker Capsule", - "caseImage": "assets/cases/2176.webp", + "containerImage": "assets/containers/2176.webp", "releaseDate": "2021-10-26", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -3974,7 +4464,7 @@ { "id": "388", "name": "Stockholm 2021 Mirage Souvenir Package", - "caseImage": "assets/cases/388.webp", + "containerImage": "assets/containers/388.webp", "releaseDate": "2021-10-26", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Stockholm 2021", @@ -3986,7 +4476,7 @@ { "id": "389", "name": "Stockholm 2021 Nuke Souvenir Package", - "caseImage": "assets/cases/389.webp", + "containerImage": "assets/containers/389.webp", "releaseDate": "2021-10-26", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Stockholm 2021", @@ -3998,7 +4488,7 @@ { "id": "390", "name": "Stockholm 2021 Overpass Souvenir Package", - "caseImage": "assets/cases/390.webp", + "containerImage": "assets/containers/390.webp", "releaseDate": "2021-10-26", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Stockholm 2021", @@ -4010,7 +4500,7 @@ { "id": "391", "name": "Stockholm 2021 Vertigo Souvenir Package", - "caseImage": "assets/cases/391.webp", + "containerImage": "assets/containers/391.webp", "releaseDate": "2021-10-26", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Stockholm 2021", @@ -4022,7 +4512,7 @@ { "id": "223", "name": "Dreams & Nightmares Case", - "caseImage": "assets/cases/223.webp", + "containerImage": "assets/containers/223.webp", "releaseDate": "2022-01-20", "type": "CASE", "tournamentName": null, @@ -4034,7 +4524,7 @@ { "id": "2180", "name": "The Boardroom Sticker Capsule", - "caseImage": "assets/cases/2180.webp", + "containerImage": "assets/containers/2180.webp", "releaseDate": "2022-02-20", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4046,7 +4536,7 @@ { "id": "4", "name": "Antwerp 2022 Ancient Souvenir Package", - "caseImage": "assets/cases/4.webp", + "containerImage": "assets/containers/4.webp", "releaseDate": "2022-05-09", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Antwerp 2022", @@ -4058,7 +4548,7 @@ { "id": "1980", "name": "Antwerp 2022 Challengers Autograph Capsule", - "caseImage": "assets/cases/1980.webp", + "containerImage": "assets/containers/1980.webp", "releaseDate": "2022-05-09", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4070,7 +4560,7 @@ { "id": "1981", "name": "Antwerp 2022 Challengers Sticker Capsule", - "caseImage": "assets/cases/1981.webp", + "containerImage": "assets/containers/1981.webp", "releaseDate": "2022-05-09", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4082,7 +4572,7 @@ { "id": "1982", "name": "Antwerp 2022 Champions Autograph Capsule", - "caseImage": "assets/cases/1982.webp", + "containerImage": "assets/containers/1982.webp", "releaseDate": "2022-05-09", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4094,7 +4584,7 @@ { "id": "1983", "name": "Antwerp 2022 Contenders Autograph Capsule", - "caseImage": "assets/cases/1983.webp", + "containerImage": "assets/containers/1983.webp", "releaseDate": "2022-05-09", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4106,7 +4596,7 @@ { "id": "1984", "name": "Antwerp 2022 Contenders Sticker Capsule", - "caseImage": "assets/cases/1984.webp", + "containerImage": "assets/containers/1984.webp", "releaseDate": "2022-05-09", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4118,7 +4608,7 @@ { "id": "10", "name": "Antwerp 2022 Dust II Souvenir Package", - "caseImage": "assets/cases/10.webp", + "containerImage": "assets/containers/10.webp", "releaseDate": "2022-05-09", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Antwerp 2022", @@ -4130,7 +4620,7 @@ { "id": "11", "name": "Antwerp 2022 Inferno Souvenir Package", - "caseImage": "assets/cases/11.webp", + "containerImage": "assets/containers/11.webp", "releaseDate": "2022-05-09", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Antwerp 2022", @@ -4142,7 +4632,7 @@ { "id": "1985", "name": "Antwerp 2022 Legends Autograph Capsule", - "caseImage": "assets/cases/1985.webp", + "containerImage": "assets/containers/1985.webp", "releaseDate": "2022-05-09", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4154,7 +4644,7 @@ { "id": "1986", "name": "Antwerp 2022 Legends Sticker Capsule", - "caseImage": "assets/cases/1986.webp", + "containerImage": "assets/containers/1986.webp", "releaseDate": "2022-05-09", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4166,7 +4656,7 @@ { "id": "14", "name": "Antwerp 2022 Mirage Souvenir Package", - "caseImage": "assets/cases/14.webp", + "containerImage": "assets/containers/14.webp", "releaseDate": "2022-05-09", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Antwerp 2022", @@ -4178,7 +4668,7 @@ { "id": "15", "name": "Antwerp 2022 Nuke Souvenir Package", - "caseImage": "assets/cases/15.webp", + "containerImage": "assets/containers/15.webp", "releaseDate": "2022-05-09", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Antwerp 2022", @@ -4190,7 +4680,7 @@ { "id": "16", "name": "Antwerp 2022 Overpass Souvenir Package", - "caseImage": "assets/cases/16.webp", + "containerImage": "assets/containers/16.webp", "releaseDate": "2022-05-09", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Antwerp 2022", @@ -4202,7 +4692,7 @@ { "id": "17", "name": "Antwerp 2022 Vertigo Souvenir Package", - "caseImage": "assets/cases/17.webp", + "containerImage": "assets/containers/17.webp", "releaseDate": "2022-05-09", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Antwerp 2022", @@ -4214,7 +4704,7 @@ { "id": "1977", "name": "10 Year Birthday Sticker Capsule", - "caseImage": "assets/cases/1977.webp", + "containerImage": "assets/containers/1977.webp", "releaseDate": "2022-06-15", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4226,7 +4716,7 @@ { "id": "331", "name": "Recoil Case", - "caseImage": "assets/cases/331.webp", + "containerImage": "assets/containers/331.webp", "releaseDate": "2022-07-01", "type": "CASE", "tournamentName": null, @@ -4238,7 +4728,7 @@ { "id": "2124", "name": "Initiators Music Kit Box", - "caseImage": "assets/cases/2124.webp", + "containerImage": "assets/containers/2124.webp", "releaseDate": "2022-08-15", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -4250,7 +4740,7 @@ { "id": "2164", "name": "StatTrakā„¢ Initiators Music Kit Box", - "caseImage": "assets/cases/2164.webp", + "containerImage": "assets/containers/2164.webp", "releaseDate": "2022-08-15", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -4262,7 +4752,7 @@ { "id": "334", "name": "Rio 2022 Ancient Souvenir Package", - "caseImage": "assets/cases/334.webp", + "containerImage": "assets/containers/334.webp", "releaseDate": "2022-10-31", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Rio 2022", @@ -4274,7 +4764,7 @@ { "id": "2147", "name": "Rio 2022 Challengers Autograph Capsule", - "caseImage": "assets/cases/2147.webp", + "containerImage": "assets/containers/2147.webp", "releaseDate": "2022-10-31", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4286,7 +4776,7 @@ { "id": "2148", "name": "Rio 2022 Challengers Sticker Capsule", - "caseImage": "assets/cases/2148.webp", + "containerImage": "assets/containers/2148.webp", "releaseDate": "2022-10-31", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4298,7 +4788,7 @@ { "id": "2149", "name": "Rio 2022 Champions Autograph Capsule", - "caseImage": "assets/cases/2149.webp", + "containerImage": "assets/containers/2149.webp", "releaseDate": "2022-10-31", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4310,7 +4800,7 @@ { "id": "2150", "name": "Rio 2022 Contenders Autograph Capsule", - "caseImage": "assets/cases/2150.webp", + "containerImage": "assets/containers/2150.webp", "releaseDate": "2022-10-31", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4322,7 +4812,7 @@ { "id": "2151", "name": "Rio 2022 Contenders Sticker Capsule", - "caseImage": "assets/cases/2151.webp", + "containerImage": "assets/containers/2151.webp", "releaseDate": "2022-10-31", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4334,7 +4824,7 @@ { "id": "340", "name": "Rio 2022 Dust II Souvenir Package", - "caseImage": "assets/cases/340.webp", + "containerImage": "assets/containers/340.webp", "releaseDate": "2022-10-31", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Rio 2022", @@ -4346,7 +4836,7 @@ { "id": "341", "name": "Rio 2022 Inferno Souvenir Package", - "caseImage": "assets/cases/341.webp", + "containerImage": "assets/containers/341.webp", "releaseDate": "2022-10-31", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Rio 2022", @@ -4358,7 +4848,7 @@ { "id": "2152", "name": "Rio 2022 Legends Autograph Capsule", - "caseImage": "assets/cases/2152.webp", + "containerImage": "assets/containers/2152.webp", "releaseDate": "2022-10-31", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4370,7 +4860,7 @@ { "id": "2153", "name": "Rio 2022 Legends Sticker Capsule", - "caseImage": "assets/cases/2153.webp", + "containerImage": "assets/containers/2153.webp", "releaseDate": "2022-10-31", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4382,7 +4872,7 @@ { "id": "344", "name": "Rio 2022 Mirage Souvenir Package", - "caseImage": "assets/cases/344.webp", + "containerImage": "assets/containers/344.webp", "releaseDate": "2022-10-31", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Rio 2022", @@ -4394,7 +4884,7 @@ { "id": "345", "name": "Rio 2022 Nuke Souvenir Package", - "caseImage": "assets/cases/345.webp", + "containerImage": "assets/containers/345.webp", "releaseDate": "2022-10-31", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Rio 2022", @@ -4406,7 +4896,7 @@ { "id": "346", "name": "Rio 2022 Overpass Souvenir Package", - "caseImage": "assets/cases/346.webp", + "containerImage": "assets/containers/346.webp", "releaseDate": "2022-10-31", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Rio 2022", @@ -4418,7 +4908,7 @@ { "id": "347", "name": "Rio 2022 Vertigo Souvenir Package", - "caseImage": "assets/cases/347.webp", + "containerImage": "assets/containers/347.webp", "releaseDate": "2022-10-31", "type": "SOUVENIR_PACKAGE", "tournamentName": "IEM Rio 2022", @@ -4430,7 +4920,7 @@ { "id": "2119", "name": "Espionage Sticker Capsule", - "caseImage": "assets/cases/2119.webp", + "containerImage": "assets/containers/2119.webp", "releaseDate": "2023-01-05", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4442,7 +4932,7 @@ { "id": "332", "name": "Revolution Case", - "caseImage": "assets/cases/332.webp", + "containerImage": "assets/containers/332.webp", "releaseDate": "2023-02-09", "type": "CASE", "tournamentName": null, @@ -4454,7 +4944,7 @@ { "id": "18", "name": "Anubis Collection Package", - "caseImage": "assets/cases/18.webp", + "containerImage": "assets/containers/18.webp", "releaseDate": "2023-03-22", "type": "COLLECTION_PACKAGE", "tournamentName": null, @@ -4466,7 +4956,7 @@ { "id": "311", "name": "Paris 2023 Ancient Souvenir Package", - "caseImage": "assets/cases/311.webp", + "containerImage": "assets/containers/311.webp", "releaseDate": "2023-05-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Paris 2023", @@ -4478,7 +4968,7 @@ { "id": "312", "name": "Paris 2023 Anubis Souvenir Package", - "caseImage": "assets/cases/312.webp", + "containerImage": "assets/containers/312.webp", "releaseDate": "2023-05-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Paris 2023", @@ -4490,7 +4980,7 @@ { "id": "2136", "name": "Paris 2023 Challengers Autograph Capsule", - "caseImage": "assets/cases/2136.webp", + "containerImage": "assets/containers/2136.webp", "releaseDate": "2023-05-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4502,7 +4992,7 @@ { "id": "2137", "name": "Paris 2023 Challengers Sticker Capsule", - "caseImage": "assets/cases/2137.webp", + "containerImage": "assets/containers/2137.webp", "releaseDate": "2023-05-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4514,7 +5004,7 @@ { "id": "2138", "name": "Paris 2023 Champions Autograph Capsule", - "caseImage": "assets/cases/2138.webp", + "containerImage": "assets/containers/2138.webp", "releaseDate": "2023-05-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4526,7 +5016,7 @@ { "id": "2139", "name": "Paris 2023 Contenders Autograph Capsule", - "caseImage": "assets/cases/2139.webp", + "containerImage": "assets/containers/2139.webp", "releaseDate": "2023-05-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4538,7 +5028,7 @@ { "id": "2140", "name": "Paris 2023 Contenders Sticker Capsule", - "caseImage": "assets/cases/2140.webp", + "containerImage": "assets/containers/2140.webp", "releaseDate": "2023-05-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4550,7 +5040,7 @@ { "id": "318", "name": "Paris 2023 Inferno Souvenir Package", - "caseImage": "assets/cases/318.webp", + "containerImage": "assets/containers/318.webp", "releaseDate": "2023-05-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Paris 2023", @@ -4562,7 +5052,7 @@ { "id": "2141", "name": "Paris 2023 Legends Autograph Capsule", - "caseImage": "assets/cases/2141.webp", + "containerImage": "assets/containers/2141.webp", "releaseDate": "2023-05-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4574,7 +5064,7 @@ { "id": "2142", "name": "Paris 2023 Legends Sticker Capsule", - "caseImage": "assets/cases/2142.webp", + "containerImage": "assets/containers/2142.webp", "releaseDate": "2023-05-08", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4586,7 +5076,7 @@ { "id": "321", "name": "Paris 2023 Mirage Souvenir Package", - "caseImage": "assets/cases/321.webp", + "containerImage": "assets/containers/321.webp", "releaseDate": "2023-05-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Paris 2023", @@ -4598,7 +5088,7 @@ { "id": "322", "name": "Paris 2023 Nuke Souvenir Package", - "caseImage": "assets/cases/322.webp", + "containerImage": "assets/containers/322.webp", "releaseDate": "2023-05-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Paris 2023", @@ -4610,7 +5100,7 @@ { "id": "323", "name": "Paris 2023 Overpass Souvenir Package", - "caseImage": "assets/cases/323.webp", + "containerImage": "assets/containers/323.webp", "releaseDate": "2023-05-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Paris 2023", @@ -4622,7 +5112,7 @@ { "id": "324", "name": "Paris 2023 Vertigo Souvenir Package", - "caseImage": "assets/cases/324.webp", + "containerImage": "assets/containers/324.webp", "releaseDate": "2023-05-08", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Paris 2023", @@ -4634,7 +5124,7 @@ { "id": "2135", "name": "NIGHTMODE Music Kit Box", - "caseImage": "assets/cases/2135.webp", + "containerImage": "assets/containers/2135.webp", "releaseDate": "2024-01-24", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -4646,7 +5136,7 @@ { "id": "2167", "name": "StatTrakā„¢ NIGHTMODE Music Kit Box", - "caseImage": "assets/cases/2167.webp", + "containerImage": "assets/containers/2167.webp", "releaseDate": "2024-01-24", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -4658,7 +5148,7 @@ { "id": "1979", "name": "Ambush Sticker Capsule", - "caseImage": "assets/cases/1979.webp", + "containerImage": "assets/containers/1979.webp", "releaseDate": "2024-01-25", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4670,7 +5160,7 @@ { "id": "272", "name": "Kilowatt Case", - "caseImage": "assets/cases/272.webp", + "containerImage": "assets/containers/272.webp", "releaseDate": "2024-02-06", "type": "CASE", "tournamentName": null, @@ -4682,7 +5172,7 @@ { "id": "192", "name": "Copenhagen 2024 Ancient Souvenir Package", - "caseImage": "assets/cases/192.webp", + "containerImage": "assets/containers/192.webp", "releaseDate": "2024-03-17", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Copenhagen 2024", @@ -4694,7 +5184,7 @@ { "id": "193", "name": "Copenhagen 2024 Anubis Souvenir Package", - "caseImage": "assets/cases/193.webp", + "containerImage": "assets/containers/193.webp", "releaseDate": "2024-03-17", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Copenhagen 2024", @@ -4706,7 +5196,7 @@ { "id": "2110", "name": "Copenhagen 2024 Challengers Autograph Capsule", - "caseImage": "assets/cases/2110.webp", + "containerImage": "assets/containers/2110.webp", "releaseDate": "2024-03-17", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4718,7 +5208,7 @@ { "id": "2111", "name": "Copenhagen 2024 Challengers Sticker Capsule", - "caseImage": "assets/cases/2111.webp", + "containerImage": "assets/containers/2111.webp", "releaseDate": "2024-03-17", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4730,7 +5220,7 @@ { "id": "2112", "name": "Copenhagen 2024 Champions Autograph Capsule", - "caseImage": "assets/cases/2112.webp", + "containerImage": "assets/containers/2112.webp", "releaseDate": "2024-03-17", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4742,7 +5232,7 @@ { "id": "2113", "name": "Copenhagen 2024 Contenders Autograph Capsule", - "caseImage": "assets/cases/2113.webp", + "containerImage": "assets/containers/2113.webp", "releaseDate": "2024-03-17", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4754,7 +5244,7 @@ { "id": "2114", "name": "Copenhagen 2024 Contenders Sticker Capsule", - "caseImage": "assets/cases/2114.webp", + "containerImage": "assets/containers/2114.webp", "releaseDate": "2024-03-17", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4766,7 +5256,7 @@ { "id": "199", "name": "Copenhagen 2024 Inferno Souvenir Package", - "caseImage": "assets/cases/199.webp", + "containerImage": "assets/containers/199.webp", "releaseDate": "2024-03-17", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Copenhagen 2024", @@ -4778,7 +5268,7 @@ { "id": "2115", "name": "Copenhagen 2024 Legends Autograph Capsule", - "caseImage": "assets/cases/2115.webp", + "containerImage": "assets/containers/2115.webp", "releaseDate": "2024-03-17", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4790,7 +5280,7 @@ { "id": "2116", "name": "Copenhagen 2024 Legends Sticker Capsule", - "caseImage": "assets/cases/2116.webp", + "containerImage": "assets/containers/2116.webp", "releaseDate": "2024-03-17", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4802,7 +5292,7 @@ { "id": "202", "name": "Copenhagen 2024 Mirage Souvenir Package", - "caseImage": "assets/cases/202.webp", + "containerImage": "assets/containers/202.webp", "releaseDate": "2024-03-17", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Copenhagen 2024", @@ -4814,7 +5304,7 @@ { "id": "203", "name": "Copenhagen 2024 Nuke Souvenir Package", - "caseImage": "assets/cases/203.webp", + "containerImage": "assets/containers/203.webp", "releaseDate": "2024-03-17", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Copenhagen 2024", @@ -4826,7 +5316,7 @@ { "id": "204", "name": "Copenhagen 2024 Overpass Souvenir Package", - "caseImage": "assets/cases/204.webp", + "containerImage": "assets/containers/204.webp", "releaseDate": "2024-03-17", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Copenhagen 2024", @@ -4838,7 +5328,7 @@ { "id": "205", "name": "Copenhagen 2024 Vertigo Souvenir Package", - "caseImage": "assets/cases/205.webp", + "containerImage": "assets/containers/205.webp", "releaseDate": "2024-03-17", "type": "SOUVENIR_PACKAGE", "tournamentName": "PGL Copenhagen 2024", @@ -4850,7 +5340,7 @@ { "id": "2133", "name": "Masterminds 2 Music Kit Box", - "caseImage": "assets/cases/2133.webp", + "containerImage": "assets/containers/2133.webp", "releaseDate": "2024-08-15", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -4862,7 +5352,7 @@ { "id": "2165", "name": "StatTrakā„¢ Masterminds 2 Music Kit Box", - "caseImage": "assets/cases/2165.webp", + "containerImage": "assets/containers/2165.webp", "releaseDate": "2024-08-15", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -4874,7 +5364,7 @@ { "id": "2220", "name": "Character Craft Sticker Pack", - "caseImage": "assets/cases/2220.webp", + "containerImage": "assets/containers/2220.webp", "releaseDate": "2024-10-02", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -4886,7 +5376,7 @@ { "id": "2228", "name": "Elemental Craft Sticker Pack", - "caseImage": "assets/cases/2228.webp", + "containerImage": "assets/containers/2228.webp", "releaseDate": "2024-10-02", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -4898,7 +5388,7 @@ { "id": "252", "name": "Gallery Case", - "caseImage": "assets/cases/252.webp", + "containerImage": "assets/containers/252.webp", "releaseDate": "2024-10-02", "type": "CASE", "tournamentName": null, @@ -4907,10 +5397,76 @@ "sourceId": null, "sourceName": null }, + { + "id": "2242", + "name": "Missing Link Charm Collection", + "containerImage": "assets/containers/2242.webp", + "releaseDate": "2024-10-02", + "type": "CHARM_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "ARMORY_REWARD", + "sourceId": "ARMORY", + "sourceName": "The Armory" + }, + { + "id": "2244", + "name": "Small Arms Charm Collection", + "containerImage": "assets/containers/2244.webp", + "releaseDate": "2024-10-02", + "type": "CHARM_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "ARMORY_REWARD", + "sourceId": "ARMORY", + "sourceName": "The Armory" + }, + { + "id": "10003", + "name": "The Graphic Design Collection", + "containerImage": "assets/containers/10003.svg", + "releaseDate": "2024-10-02", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "ARMORY_REWARD", + "sourceId": "ARMORY", + "sourceName": "The Armory", + "currency": "CREDITS", + "cost": 4 + }, + { + "id": "10004", + "name": "The Overpass 2024 Collection", + "containerImage": "assets/containers/10004.svg", + "releaseDate": "2024-10-02", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "ARMORY_REWARD", + "sourceId": "ARMORY", + "sourceName": "The Armory", + "currency": "CREDITS", + "cost": 4 + }, + { + "id": "10006", + "name": "The Sport & Field Collection", + "containerImage": "assets/containers/10006.svg", + "releaseDate": "2024-10-02", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "ARMORY_REWARD", + "sourceId": "ARMORY", + "sourceName": "The Armory", + "currency": "CREDITS", + "cost": 4 + }, { "id": "351", "name": "Shanghai 2024 Ancient Souvenir Package", - "caseImage": "assets/cases/351.webp", + "containerImage": "assets/containers/351.webp", "releaseDate": "2024-11-30", "type": "SOUVENIR_PACKAGE", "tournamentName": "Perfect World Shanghai 2024", @@ -4922,7 +5478,7 @@ { "id": "352", "name": "Shanghai 2024 Anubis Souvenir Package", - "caseImage": "assets/cases/352.webp", + "containerImage": "assets/containers/352.webp", "releaseDate": "2024-11-30", "type": "SOUVENIR_PACKAGE", "tournamentName": "Perfect World Shanghai 2024", @@ -4934,7 +5490,7 @@ { "id": "2154", "name": "Shanghai 2024 Challengers Autograph Capsule", - "caseImage": "assets/cases/2154.webp", + "containerImage": "assets/containers/2154.webp", "releaseDate": "2024-11-30", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4946,7 +5502,7 @@ { "id": "2155", "name": "Shanghai 2024 Challengers Sticker Capsule", - "caseImage": "assets/cases/2155.webp", + "containerImage": "assets/containers/2155.webp", "releaseDate": "2024-11-30", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4958,7 +5514,7 @@ { "id": "2156", "name": "Shanghai 2024 Champions Autograph Capsule", - "caseImage": "assets/cases/2156.webp", + "containerImage": "assets/containers/2156.webp", "releaseDate": "2024-11-30", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4970,7 +5526,7 @@ { "id": "2157", "name": "Shanghai 2024 Contenders Autograph Capsule", - "caseImage": "assets/cases/2157.webp", + "containerImage": "assets/containers/2157.webp", "releaseDate": "2024-11-30", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4982,7 +5538,7 @@ { "id": "2158", "name": "Shanghai 2024 Contenders Sticker Capsule", - "caseImage": "assets/cases/2158.webp", + "containerImage": "assets/containers/2158.webp", "releaseDate": "2024-11-30", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -4994,7 +5550,7 @@ { "id": "358", "name": "Shanghai 2024 Dust II Souvenir Package", - "caseImage": "assets/cases/358.webp", + "containerImage": "assets/containers/358.webp", "releaseDate": "2024-11-30", "type": "SOUVENIR_PACKAGE", "tournamentName": "Perfect World Shanghai 2024", @@ -5006,7 +5562,7 @@ { "id": "359", "name": "Shanghai 2024 Inferno Souvenir Package", - "caseImage": "assets/cases/359.webp", + "containerImage": "assets/containers/359.webp", "releaseDate": "2024-11-30", "type": "SOUVENIR_PACKAGE", "tournamentName": "Perfect World Shanghai 2024", @@ -5018,7 +5574,7 @@ { "id": "2159", "name": "Shanghai 2024 Legends Autograph Capsule", - "caseImage": "assets/cases/2159.webp", + "containerImage": "assets/containers/2159.webp", "releaseDate": "2024-11-30", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5030,7 +5586,7 @@ { "id": "2160", "name": "Shanghai 2024 Legends Sticker Capsule", - "caseImage": "assets/cases/2160.webp", + "containerImage": "assets/containers/2160.webp", "releaseDate": "2024-11-30", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5042,7 +5598,7 @@ { "id": "362", "name": "Shanghai 2024 Mirage Souvenir Package", - "caseImage": "assets/cases/362.webp", + "containerImage": "assets/containers/362.webp", "releaseDate": "2024-11-30", "type": "SOUVENIR_PACKAGE", "tournamentName": "Perfect World Shanghai 2024", @@ -5054,7 +5610,7 @@ { "id": "363", "name": "Shanghai 2024 Nuke Souvenir Package", - "caseImage": "assets/cases/363.webp", + "containerImage": "assets/containers/363.webp", "releaseDate": "2024-11-30", "type": "SOUVENIR_PACKAGE", "tournamentName": "Perfect World Shanghai 2024", @@ -5066,7 +5622,7 @@ { "id": "364", "name": "Shanghai 2024 Vertigo Souvenir Package", - "caseImage": "assets/cases/364.webp", + "containerImage": "assets/containers/364.webp", "releaseDate": "2024-11-30", "type": "SOUVENIR_PACKAGE", "tournamentName": "Perfect World Shanghai 2024", @@ -5078,7 +5634,7 @@ { "id": "250", "name": "Fever Case", - "caseImage": "assets/cases/250.webp", + "containerImage": "assets/containers/250.webp", "releaseDate": "2025-03-31", "type": "CASE", "tournamentName": null, @@ -5087,10 +5643,24 @@ "sourceId": null, "sourceName": null }, + { + "id": "10010", + "name": "The Train 2025 Collection", + "containerImage": "assets/containers/10010.svg", + "releaseDate": "2025-03-31", + "type": "REWARD_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "ARMORY_REWARD", + "sourceId": "ARMORY", + "sourceName": "The Armory", + "currency": "CREDITS", + "cost": 4 + }, { "id": "2181", "name": "Warhammer 40,000 Adeptus Astartes Sticker Capsule", - "caseImage": "assets/cases/2181.webp", + "containerImage": "assets/containers/2181.webp", "releaseDate": "2025-05-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5102,7 +5672,7 @@ { "id": "2182", "name": "Warhammer 40,000 Imperium Sticker Capsule", - "caseImage": "assets/cases/2182.webp", + "containerImage": "assets/containers/2182.webp", "releaseDate": "2025-05-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5114,7 +5684,7 @@ { "id": "2184", "name": "Warhammer 40,000 Traitor Astartes Sticker Capsule", - "caseImage": "assets/cases/2184.webp", + "containerImage": "assets/containers/2184.webp", "releaseDate": "2025-05-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5126,7 +5696,7 @@ { "id": "2185", "name": "Warhammer 40,000 Xenos Sticker Capsule", - "caseImage": "assets/cases/2185.webp", + "containerImage": "assets/containers/2185.webp", "releaseDate": "2025-05-22", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5138,7 +5708,7 @@ { "id": "26", "name": "Austin 2025 Ancient Souvenir Package", - "caseImage": "assets/cases/26.webp", + "containerImage": "assets/containers/26.webp", "releaseDate": "2025-06-03", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Austin 2025", @@ -5150,7 +5720,7 @@ { "id": "27", "name": "Austin 2025 Anubis Souvenir Package", - "caseImage": "assets/cases/27.webp", + "containerImage": "assets/containers/27.webp", "releaseDate": "2025-06-03", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Austin 2025", @@ -5162,7 +5732,7 @@ { "id": "1987", "name": "Austin 2025 Challengers Autograph Capsule", - "caseImage": "assets/cases/1987.webp", + "containerImage": "assets/containers/1987.webp", "releaseDate": "2025-06-03", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5174,7 +5744,7 @@ { "id": "1988", "name": "Austin 2025 Challengers Sticker Capsule", - "caseImage": "assets/cases/1988.webp", + "containerImage": "assets/containers/1988.webp", "releaseDate": "2025-06-03", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5186,7 +5756,7 @@ { "id": "1989", "name": "Austin 2025 Champions Autograph Capsule", - "caseImage": "assets/cases/1989.webp", + "containerImage": "assets/containers/1989.webp", "releaseDate": "2025-06-03", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5198,7 +5768,7 @@ { "id": "1990", "name": "Austin 2025 Contenders Autograph Capsule", - "caseImage": "assets/cases/1990.webp", + "containerImage": "assets/containers/1990.webp", "releaseDate": "2025-06-03", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5210,7 +5780,7 @@ { "id": "1991", "name": "Austin 2025 Contenders Sticker Capsule", - "caseImage": "assets/cases/1991.webp", + "containerImage": "assets/containers/1991.webp", "releaseDate": "2025-06-03", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5222,7 +5792,7 @@ { "id": "33", "name": "Austin 2025 Dust II Souvenir Package", - "caseImage": "assets/cases/33.webp", + "containerImage": "assets/containers/33.webp", "releaseDate": "2025-06-03", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Austin 2025", @@ -5234,7 +5804,7 @@ { "id": "34", "name": "Austin 2025 Inferno Souvenir Package", - "caseImage": "assets/cases/34.webp", + "containerImage": "assets/containers/34.webp", "releaseDate": "2025-06-03", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Austin 2025", @@ -5246,7 +5816,7 @@ { "id": "1992", "name": "Austin 2025 Legends Autograph Capsule", - "caseImage": "assets/cases/1992.webp", + "containerImage": "assets/containers/1992.webp", "releaseDate": "2025-06-03", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5258,7 +5828,7 @@ { "id": "1993", "name": "Austin 2025 Legends Sticker Capsule", - "caseImage": "assets/cases/1993.webp", + "containerImage": "assets/containers/1993.webp", "releaseDate": "2025-06-03", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5270,7 +5840,7 @@ { "id": "37", "name": "Austin 2025 Mirage Souvenir Package", - "caseImage": "assets/cases/37.webp", + "containerImage": "assets/containers/37.webp", "releaseDate": "2025-06-03", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Austin 2025", @@ -5282,7 +5852,7 @@ { "id": "38", "name": "Austin 2025 Nuke Souvenir Package", - "caseImage": "assets/cases/38.webp", + "containerImage": "assets/containers/38.webp", "releaseDate": "2025-06-03", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Austin 2025", @@ -5294,7 +5864,7 @@ { "id": "39", "name": "Austin 2025 Train Souvenir Package", - "caseImage": "assets/cases/39.webp", + "containerImage": "assets/containers/39.webp", "releaseDate": "2025-06-03", "type": "SOUVENIR_PACKAGE", "tournamentName": "BLAST.tv Austin 2025", @@ -5306,7 +5876,7 @@ { "id": "2117", "name": "Deluge Music Kit Box", - "caseImage": "assets/cases/2117.webp", + "containerImage": "assets/containers/2117.webp", "releaseDate": "2025-06-27", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -5318,7 +5888,7 @@ { "id": "2163", "name": "StatTrakā„¢ Deluge Music Kit Box", - "caseImage": "assets/cases/2163.webp", + "containerImage": "assets/containers/2163.webp", "releaseDate": "2025-06-27", "type": "MUSIC_KIT_BOX", "tournamentName": null, @@ -5330,7 +5900,7 @@ { "id": "349", "name": "Sealed Genesis Terminal", - "caseImage": "assets/cases/349.webp", + "containerImage": "assets/containers/349.webp", "releaseDate": "2025-09-16", "type": "TERMINAL", "tournamentName": null, @@ -5342,7 +5912,7 @@ { "id": "2229", "name": "2025 Community Sticker Collection", - "caseImage": "assets/cases/2229.webp", + "containerImage": "assets/containers/2229.webp", "releaseDate": "2025-10-02", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -5351,10 +5921,34 @@ "sourceId": "ARMORY", "sourceName": "The Armory" }, + { + "id": "2241", + "name": "Dr Boom Charm Collection", + "containerImage": "assets/containers/2241.webp", + "releaseDate": "2025-10-02", + "type": "CHARM_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "ARMORY_REWARD", + "sourceId": "ARMORY", + "sourceName": "The Armory" + }, + { + "id": "2243", + "name": "Missing Link Community Charm Collection", + "containerImage": "assets/containers/2243.webp", + "releaseDate": "2025-10-02", + "type": "CHARM_COLLECTION", + "tournamentName": null, + "tournamentLogo": null, + "sourceType": "ARMORY_REWARD", + "sourceId": "ARMORY", + "sourceName": "The Armory" + }, { "id": "2230", "name": "Sugarface 2 Sticker Collection", - "caseImage": "assets/cases/2230.webp", + "containerImage": "assets/containers/2230.webp", "releaseDate": "2025-10-02", "type": "STICKER_COLLECTION", "tournamentName": null, @@ -5366,7 +5960,7 @@ { "id": "156", "name": "Budapest 2025 Ancient Souvenir Package", - "caseImage": "assets/cases/156.webp", + "containerImage": "assets/containers/156.webp", "releaseDate": "2025-11-24", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Budapest 2025", @@ -5378,7 +5972,7 @@ { "id": "2096", "name": "Budapest 2025 Challengers Autograph Capsule", - "caseImage": "assets/cases/2096.webp", + "containerImage": "assets/containers/2096.webp", "releaseDate": "2025-11-24", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5390,7 +5984,7 @@ { "id": "2097", "name": "Budapest 2025 Challengers Sticker Capsule", - "caseImage": "assets/cases/2097.webp", + "containerImage": "assets/containers/2097.webp", "releaseDate": "2025-11-24", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5402,7 +5996,7 @@ { "id": "2098", "name": "Budapest 2025 Champions Autograph Capsule", - "caseImage": "assets/cases/2098.webp", + "containerImage": "assets/containers/2098.webp", "releaseDate": "2025-11-24", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5414,7 +6008,7 @@ { "id": "2099", "name": "Budapest 2025 Contenders Autograph Capsule", - "caseImage": "assets/cases/2099.webp", + "containerImage": "assets/containers/2099.webp", "releaseDate": "2025-11-24", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5426,7 +6020,7 @@ { "id": "2100", "name": "Budapest 2025 Contenders Sticker Capsule", - "caseImage": "assets/cases/2100.webp", + "containerImage": "assets/containers/2100.webp", "releaseDate": "2025-11-24", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5438,7 +6032,7 @@ { "id": "162", "name": "Budapest 2025 Dust II Souvenir Package", - "caseImage": "assets/cases/162.webp", + "containerImage": "assets/containers/162.webp", "releaseDate": "2025-11-24", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Budapest 2025", @@ -5450,7 +6044,7 @@ { "id": "163", "name": "Budapest 2025 Inferno Souvenir Package", - "caseImage": "assets/cases/163.webp", + "containerImage": "assets/containers/163.webp", "releaseDate": "2025-11-24", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Budapest 2025", @@ -5462,7 +6056,7 @@ { "id": "2101", "name": "Budapest 2025 Legends Autograph Capsule", - "caseImage": "assets/cases/2101.webp", + "containerImage": "assets/containers/2101.webp", "releaseDate": "2025-11-24", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5474,7 +6068,7 @@ { "id": "2102", "name": "Budapest 2025 Legends Sticker Capsule", - "caseImage": "assets/cases/2102.webp", + "containerImage": "assets/containers/2102.webp", "releaseDate": "2025-11-24", "type": "STICKER_CAPSULE", "tournamentName": null, @@ -5486,7 +6080,7 @@ { "id": "166", "name": "Budapest 2025 Mirage Souvenir Package", - "caseImage": "assets/cases/166.webp", + "containerImage": "assets/containers/166.webp", "releaseDate": "2025-11-24", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Budapest 2025", @@ -5498,7 +6092,7 @@ { "id": "167", "name": "Budapest 2025 Nuke Souvenir Package", - "caseImage": "assets/cases/167.webp", + "containerImage": "assets/containers/167.webp", "releaseDate": "2025-11-24", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Budapest 2025", @@ -5510,7 +6104,7 @@ { "id": "168", "name": "Budapest 2025 Overpass Souvenir Package", - "caseImage": "assets/cases/168.webp", + "containerImage": "assets/containers/168.webp", "releaseDate": "2025-11-24", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Budapest 2025", @@ -5522,7 +6116,7 @@ { "id": "169", "name": "Budapest 2025 Train Souvenir Package", - "caseImage": "assets/cases/169.webp", + "containerImage": "assets/containers/169.webp", "releaseDate": "2025-11-24", "type": "SOUVENIR_PACKAGE", "tournamentName": "StarLadder Budapest 2025", @@ -5534,7 +6128,7 @@ { "id": "348", "name": "Sealed Dead Hand Terminal", - "caseImage": "assets/cases/348.webp", + "containerImage": "assets/containers/348.webp", "releaseDate": "2026-03-11", "type": "TERMINAL", "tournamentName": null, diff --git a/assets/data/graffiti_contents.json b/assets/data/graffiti_contents.json index 44e22167..7ade4763 100644 --- a/assets/data/graffiti_contents.json +++ b/assets/data/graffiti_contents.json @@ -1,6 +1,6 @@ [ { - "caseId": "2231", + "containerId": "2231", "graffitiIds": [ "1671", "1672", @@ -23,7 +23,7 @@ ] }, { - "caseId": "2232", + "containerId": "2232", "graffitiIds": [ "1653", "1654", @@ -46,7 +46,7 @@ ] }, { - "caseId": "2233", + "containerId": "2233", "graffitiIds": [ "2418", "2419", diff --git a/assets/data/music_kit_contents.json b/assets/data/music_kit_contents.json index 2439028b..9c8ab031 100644 --- a/assets/data/music_kit_contents.json +++ b/assets/data/music_kit_contents.json @@ -1,160 +1,532 @@ [ { - "caseId": "2117", - "musicKitIds": [ - "973258375", - "973796104", - "984479732", - "1000709437", - "1020124659", - "1025275940", - "1032490082", - "1050160811", - "1052161809", - "1063058078" + "containerId": "2117", + "items": [ + { + "musicKitId": "973258375", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "973796104", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "984479732", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1000709437", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1020124659", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1025275940", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1032490082", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1050160811", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1052161809", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1063058078", + "hasRegular": true, + "hasStatTrak": false + } ] }, { - "caseId": "2124", - "musicKitIds": [ - "976329060", - "996251847", - "999193725", - "1017750097", - "1050389227", - "1061750258" + "containerId": "2124", + "items": [ + { + "musicKitId": "976329060", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "996251847", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "999193725", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1017750097", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1050389227", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1061750258", + "hasRegular": true, + "hasStatTrak": false + } ] }, { - "caseId": "2133", - "musicKitIds": [ - "970676397", - "976850143", - "990519625", - "998457578", - "1001522256", - "1019800188", - "1052316358", - "1054885947" + "containerId": "2133", + "items": [ + { + "musicKitId": "970676397", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "976850143", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "990519625", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "998457578", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1001522256", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1019800188", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1052316358", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1054885947", + "hasRegular": true, + "hasStatTrak": false + } ] }, { - "caseId": "2134", - "musicKitIds": [ - "977121784", - "985214830", - "988045417", - "993368645", - "1007353574", - "1018567891", - "1059406591" + "containerId": "2134", + "items": [ + { + "musicKitId": "977121784", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "985214830", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "988045417", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "993368645", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1007353574", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1018567891", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1059406591", + "hasRegular": true, + "hasStatTrak": false + } ] }, { - "caseId": "2135", - "musicKitIds": [ - "971454122", - "999027984", - "1010421769", - "1047586438", - "1048343219", - "1059220517" + "containerId": "2135", + "items": [ + { + "musicKitId": "971454122", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "999027984", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1010421769", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1047586438", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1048343219", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1059220517", + "hasRegular": true, + "hasStatTrak": false + } ] }, { - "caseId": "2163", - "musicKitIds": [ - "975826813", - "986159988", - "987263905", - "1000538183", - "1005312915", - "1008303658", - "1012417359", - "1020735512", - "1024340982", - "1040151842" + "containerId": "2163", + "items": [ + { + "musicKitId": "973258375", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "973796104", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "984479732", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1000709437", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1020124659", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1025275940", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1032490082", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1050160811", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1052161809", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1063058078", + "hasRegular": false, + "hasStatTrak": true + } ] }, { - "caseId": "2164", - "musicKitIds": [ - "986102338", - "994554107", - "1001606814", - "1002198311", - "1011503984", - "1017749013" + "containerId": "2164", + "items": [ + { + "musicKitId": "976329060", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "996251847", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "999193725", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1017750097", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1050389227", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1061750258", + "hasRegular": false, + "hasStatTrak": true + } ] }, { - "caseId": "2165", - "musicKitIds": [ - "1003252223", - "1009155363", - "1011442758", - "1020755149", - "1024770198", - "1026524570", - "1057256691", - "1066759080" + "containerId": "2165", + "items": [ + { + "musicKitId": "970676397", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "976850143", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "990519625", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "998457578", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1001522256", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1019800188", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1052316358", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1054885947", + "hasRegular": false, + "hasStatTrak": true + } ] }, { - "caseId": "2166", - "musicKitIds": [ - "971231302", - "1010678901", - "1024737315", - "1029802512", - "1032701438", - "1040470171", - "1051886114" + "containerId": "2166", + "items": [ + { + "musicKitId": "977121784", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "985214830", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "988045417", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "993368645", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1007353574", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1018567891", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1059406591", + "hasRegular": false, + "hasStatTrak": true + } ] }, { - "caseId": "2167", - "musicKitIds": [ - "983861625", - "984052782", - "997687959", - "998333682", - "1004387147", - "1026639040" + "containerId": "2167", + "items": [ + { + "musicKitId": "971454122", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "999027984", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1010421769", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1047586438", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1048343219", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1059220517", + "hasRegular": false, + "hasStatTrak": true + } ] }, { - "caseId": "2168", - "musicKitIds": [ - "992902795", - "1009981235", - "1013122391", - "1036144690", - "1036314880", - "1036996197", - "1047455470" + "containerId": "2168", + "items": [ + { + "musicKitId": "992902795", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1009981235", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1013122391", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1036144690", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1036314880", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1036996197", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1047455470", + "hasRegular": false, + "hasStatTrak": true + } ] }, { - "caseId": "2169", - "musicKitIds": [ - "971420563", - "1011148448", - "1016818422", - "1027126827", - "1038380485", - "1055101006" + "containerId": "2169", + "items": [ + { + "musicKitId": "971420563", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1011148448", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1016818422", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1027126827", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1038380485", + "hasRegular": false, + "hasStatTrak": true + }, + { + "musicKitId": "1055101006", + "hasRegular": false, + "hasStatTrak": true + } ] }, { - "caseId": "2178", - "musicKitIds": [ - "997276857", - "1005980932", - "1034373778", - "1034543151", - "1044942782", - "1069896488" + "containerId": "2178", + "items": [ + { + "musicKitId": "971420563", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1011148448", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1016818422", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1027126827", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1038380485", + "hasRegular": true, + "hasStatTrak": false + }, + { + "musicKitId": "1055101006", + "hasRegular": true, + "hasStatTrak": false + } ] } ] diff --git a/assets/data/music_kits.json b/assets/data/music_kits.json index 876b00ff..e68ee606 100644 --- a/assets/data/music_kits.json +++ b/assets/data/music_kits.json @@ -5,15 +5,8 @@ "musicKitImage": "assets/music_kits/970676397.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds 2", - "isStatTrak": false - }, - { - "id": "971231302", - "name": "Matt Levine, Drifter", - "musicKitImage": "assets/music_kits/971231302.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "971420563", @@ -21,7 +14,8 @@ "musicKitImage": "assets/music_kits/971420563.webp", "rarity": "HIGH_GRADE", "collection": "Tacticians", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "971454122", @@ -29,7 +23,8 @@ "musicKitImage": "assets/music_kits/971454122.webp", "rarity": "HIGH_GRADE", "collection": "NIGHTMODE", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "972592427", @@ -37,7 +32,8 @@ "musicKitImage": "assets/music_kits/972592427.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "973258375", @@ -45,7 +41,8 @@ "musicKitImage": "assets/music_kits/973258375.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "973657692", @@ -53,7 +50,8 @@ "musicKitImage": "assets/music_kits/973657692.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": false }, { "id": "973796104", @@ -61,7 +59,8 @@ "musicKitImage": "assets/music_kits/973796104.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "973977466", @@ -69,7 +68,8 @@ "musicKitImage": "assets/music_kits/973977466.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "974613487", @@ -77,15 +77,8 @@ "musicKitImage": "assets/music_kits/974613487.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "975826813", - "name": "James and the Cold Gun, Chewing Glass", - "musicKitImage": "assets/music_kits/975826813.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "976329060", @@ -93,7 +86,8 @@ "musicKitImage": "assets/music_kits/976329060.webp", "rarity": "HIGH_GRADE", "collection": "Initiators", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "976850143", @@ -101,7 +95,8 @@ "musicKitImage": "assets/music_kits/976850143.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds 2", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "977121784", @@ -109,7 +104,8 @@ "musicKitImage": "assets/music_kits/977121784.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "981765877", @@ -117,15 +113,8 @@ "musicKitImage": "assets/music_kits/981765877.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "981815769", - "name": "Skog, Metal", - "musicKitImage": "assets/music_kits/981815769.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "983344523", @@ -133,7 +122,8 @@ "musicKitImage": "assets/music_kits/983344523.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": false }, { "id": "983830847", @@ -141,23 +131,8 @@ "musicKitImage": "assets/music_kits/983830847.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "983861625", - "name": "DRYDEN, Feel The Power", - "musicKitImage": "assets/music_kits/983861625.webp", - "rarity": "HIGH_GRADE", - "collection": "NIGHTMODE", - "isStatTrak": true - }, - { - "id": "984052782", - "name": "Rad Cat, Reason", - "musicKitImage": "assets/music_kits/984052782.webp", - "rarity": "HIGH_GRADE", - "collection": "NIGHTMODE", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "984428081", @@ -165,7 +140,8 @@ "musicKitImage": "assets/music_kits/984428081.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "984479732", @@ -173,7 +149,8 @@ "musicKitImage": "assets/music_kits/984479732.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "985214830", @@ -181,7 +158,8 @@ "musicKitImage": "assets/music_kits/985214830.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "985610938", @@ -189,7 +167,8 @@ "musicKitImage": "assets/music_kits/985610938.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "985798752", @@ -197,31 +176,8 @@ "musicKitImage": "assets/music_kits/985798752.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "986081934", - "name": "Various Artists, Hotline Miami", - "musicKitImage": "assets/music_kits/986081934.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "986102338", - "name": "Humanity's Last Breath, Void", - "musicKitImage": "assets/music_kits/986102338.webp", - "rarity": "HIGH_GRADE", - "collection": "Initiators", - "isStatTrak": true - }, - { - "id": "986159988", - "name": "Adam Beyer, Red Room", - "musicKitImage": "assets/music_kits/986159988.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "986295788", @@ -229,23 +185,8 @@ "musicKitImage": "assets/music_kits/986295788.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "986500260", - "name": "Daniel Sadowski, Crimson Assault", - "musicKitImage": "assets/music_kits/986500260.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "987263905", - "name": "Tigercub, The Perfume of Decay", - "musicKitImage": "assets/music_kits/987263905.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "988045417", @@ -253,7 +194,8 @@ "musicKitImage": "assets/music_kits/988045417.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "989574806", @@ -261,7 +203,8 @@ "musicKitImage": "assets/music_kits/989574806.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "990519625", @@ -269,23 +212,8 @@ "musicKitImage": "assets/music_kits/990519625.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds 2", - "isStatTrak": false - }, - { - "id": "991673260", - "name": "bbno$, u mad!", - "musicKitImage": "assets/music_kits/991673260.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "992680274", - "name": "Matt Lange, IsoRhythm", - "musicKitImage": "assets/music_kits/992680274.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "992902795", @@ -293,7 +221,8 @@ "musicKitImage": "assets/music_kits/992902795.webp", "rarity": "HIGH_GRADE", "collection": "Radicals", - "isStatTrak": true + "hasRegular": false, + "hasStatTrak": true }, { "id": "993368645", @@ -301,15 +230,8 @@ "musicKitImage": "assets/music_kits/993368645.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds", - "isStatTrak": false - }, - { - "id": "994554107", - "name": "Juelz, Shooters", - "musicKitImage": "assets/music_kits/994554107.webp", - "rarity": "HIGH_GRADE", - "collection": "Initiators", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "994671944", @@ -317,23 +239,8 @@ "musicKitImage": "assets/music_kits/994671944.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "994893180", - "name": "Amon Tobin, All for Dust", - "musicKitImage": "assets/music_kits/994893180.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "995696992", - "name": "Daniel Sadowski, Total Domination", - "musicKitImage": "assets/music_kits/995696992.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "996251847", @@ -341,7 +248,8 @@ "musicKitImage": "assets/music_kits/996251847.webp", "rarity": "HIGH_GRADE", "collection": "Initiators", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "996799574", @@ -349,31 +257,8 @@ "musicKitImage": "assets/music_kits/996799574.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "997068518", - "name": "Beartooth, Disgusting", - "musicKitImage": "assets/music_kits/997068518.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "997276857", - "name": "Sarah Schachner, KOLIBRI", - "musicKitImage": "assets/music_kits/997276857.webp", - "rarity": "HIGH_GRADE", - "collection": "Tacticians", - "isStatTrak": false - }, - { - "id": "997687959", - "name": "ISOxo, inhuman", - "musicKitImage": "assets/music_kits/997687959.webp", - "rarity": "HIGH_GRADE", - "collection": "NIGHTMODE", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "997721676", @@ -381,15 +266,8 @@ "musicKitImage": "assets/music_kits/997721676.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "998333682", - "name": "KILL SCRIPT, All Night", - "musicKitImage": "assets/music_kits/998333682.webp", - "rarity": "HIGH_GRADE", - "collection": "NIGHTMODE", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "998457578", @@ -397,7 +275,8 @@ "musicKitImage": "assets/music_kits/998457578.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds 2", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "998492121", @@ -405,7 +284,8 @@ "musicKitImage": "assets/music_kits/998492121.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": false }, { "id": "999027984", @@ -413,7 +293,8 @@ "musicKitImage": "assets/music_kits/999027984.webp", "rarity": "HIGH_GRADE", "collection": "NIGHTMODE", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "999193725", @@ -421,23 +302,8 @@ "musicKitImage": "assets/music_kits/999193725.webp", "rarity": "HIGH_GRADE", "collection": "Initiators", - "isStatTrak": false - }, - { - "id": "1000538183", - "name": "HEALTH, RAT WARS", - "musicKitImage": "assets/music_kits/1000538183.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true - }, - { - "id": "1000587013", - "name": "Damjan Mravunac, The Talos Principle", - "musicKitImage": "assets/music_kits/1000587013.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1000709437", @@ -445,7 +311,8 @@ "musicKitImage": "assets/music_kits/1000709437.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1001522256", @@ -453,15 +320,8 @@ "musicKitImage": "assets/music_kits/1001522256.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds 2", - "isStatTrak": false - }, - { - "id": "1001606814", - "name": "Knock2, dashstar*", - "musicKitImage": "assets/music_kits/1001606814.webp", - "rarity": "HIGH_GRADE", - "collection": "Initiators", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1001873278", @@ -469,15 +329,8 @@ "musicKitImage": "assets/music_kits/1001873278.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1002198311", - "name": "3kliksphilip, Heading for the Source", - "musicKitImage": "assets/music_kits/1002198311.webp", - "rarity": "HIGH_GRADE", - "collection": "Initiators", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1002316011", @@ -485,47 +338,8 @@ "musicKitImage": "assets/music_kits/1002316011.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1002527582", - "name": "Mord Fustang, Diamonds", - "musicKitImage": "assets/music_kits/1002527582.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1003252223", - "name": "Tree Adams, Seventh Moon", - "musicKitImage": "assets/music_kits/1003252223.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds 2", - "isStatTrak": true - }, - { - "id": "1004387147", - "name": "Knock2, Make U SWEAT!", - "musicKitImage": "assets/music_kits/1004387147.webp", - "rarity": "HIGH_GRADE", - "collection": "NIGHTMODE", - "isStatTrak": true - }, - { - "id": "1004485801", - "name": "Scarlxrd: King, Scar", - "musicKitImage": "assets/music_kits/1004485801.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1005312915", - "name": "Killer Mike, MICHAEL", - "musicKitImage": "assets/music_kits/1005312915.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1005794782", @@ -533,7 +347,8 @@ "musicKitImage": "assets/music_kits/1005794782.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1005821347", @@ -541,23 +356,8 @@ "musicKitImage": "assets/music_kits/1005821347.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1005980932", - "name": "Austin Wintory, Mocha Petal", - "musicKitImage": "assets/music_kits/1005980932.webp", - "rarity": "HIGH_GRADE", - "collection": "Tacticians", - "isStatTrak": false - }, - { - "id": "1006361788", - "name": "Sasha, LNOE", - "musicKitImage": "assets/music_kits/1006361788.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1006918543", @@ -565,7 +365,8 @@ "musicKitImage": "assets/music_kits/1006918543.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1007353574", @@ -573,15 +374,8 @@ "musicKitImage": "assets/music_kits/1007353574.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds", - "isStatTrak": false - }, - { - "id": "1007491651", - "name": "Perfect World, čŠ±č„ø Hua Lian (Painted Face)", - "musicKitImage": "assets/music_kits/1007491651.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1007691386", @@ -589,15 +383,8 @@ "musicKitImage": "assets/music_kits/1007691386.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1008303658", - "name": "PVRIS, Evergreen", - "musicKitImage": "assets/music_kits/1008303658.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1008602048", @@ -605,7 +392,8 @@ "musicKitImage": "assets/music_kits/1008602048.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1009017397", @@ -613,15 +401,8 @@ "musicKitImage": "assets/music_kits/1009017397.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1009155363", - "name": "Sam Marshall, Clutch", - "musicKitImage": "assets/music_kits/1009155363.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds 2", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1009632613", @@ -629,15 +410,8 @@ "musicKitImage": "assets/music_kits/1009632613.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1009748723", - "name": "Feed Me, High Noon", - "musicKitImage": "assets/music_kits/1009748723.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1009981235", @@ -645,7 +419,8 @@ "musicKitImage": "assets/music_kits/1009981235.webp", "rarity": "HIGH_GRADE", "collection": "Radicals", - "isStatTrak": true + "hasRegular": false, + "hasStatTrak": true }, { "id": "1010421769", @@ -653,31 +428,8 @@ "musicKitImage": "assets/music_kits/1010421769.webp", "rarity": "HIGH_GRADE", "collection": "NIGHTMODE", - "isStatTrak": false - }, - { - "id": "1010678901", - "name": "Tim Huling, Neo Noir", - "musicKitImage": "assets/music_kits/1010678901.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds", - "isStatTrak": true - }, - { - "id": "1010787345", - "name": "Perfect World, Ay Hey", - "musicKitImage": "assets/music_kits/1010787345.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1010972054", - "name": "Sean Murray, A*D*8", - "musicKitImage": "assets/music_kits/1010972054.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1011148448", @@ -685,31 +437,8 @@ "musicKitImage": "assets/music_kits/1011148448.webp", "rarity": "HIGH_GRADE", "collection": "Tacticians", - "isStatTrak": true - }, - { - "id": "1011442758", - "name": "Matt Levine, Agency", - "musicKitImage": "assets/music_kits/1011442758.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds 2", - "isStatTrak": true - }, - { - "id": "1011503984", - "name": "Meechy Darko, Gothic Luxury", - "musicKitImage": "assets/music_kits/1011503984.webp", - "rarity": "HIGH_GRADE", - "collection": "Initiators", - "isStatTrak": true - }, - { - "id": "1012417359", - "name": "Selective Response, No Love Only Pleasure", - "musicKitImage": "assets/music_kits/1012417359.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1012423437", @@ -717,15 +446,8 @@ "musicKitImage": "assets/music_kits/1012423437.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1012620674", - "name": "Daniel Sadowski, The 8-Bit Kit", - "musicKitImage": "assets/music_kits/1012620674.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1013122391", @@ -733,7 +455,8 @@ "musicKitImage": "assets/music_kits/1013122391.webp", "rarity": "HIGH_GRADE", "collection": "Radicals", - "isStatTrak": true + "hasRegular": false, + "hasStatTrak": true }, { "id": "1016279770", @@ -741,7 +464,8 @@ "musicKitImage": "assets/music_kits/1016279770.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1016818422", @@ -749,23 +473,8 @@ "musicKitImage": "assets/music_kits/1016818422.webp", "rarity": "HIGH_GRADE", "collection": "Tacticians", - "isStatTrak": true - }, - { - "id": "1017012278", - "name": "Ki:Theory, MOLOTOV", - "musicKitImage": "assets/music_kits/1017012278.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1017749013", - "name": "Sullivan King, Lock Me Up", - "musicKitImage": "assets/music_kits/1017749013.webp", - "rarity": "HIGH_GRADE", - "collection": "Initiators", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1017750097", @@ -773,7 +482,8 @@ "musicKitImage": "assets/music_kits/1017750097.webp", "rarity": "HIGH_GRADE", "collection": "Initiators", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1018567891", @@ -781,15 +491,8 @@ "musicKitImage": "assets/music_kits/1018567891.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds", - "isStatTrak": false - }, - { - "id": "1018800726", - "name": "The Verkkars, EZ4ENCE", - "musicKitImage": "assets/music_kits/1018800726.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1019800188", @@ -797,7 +500,8 @@ "musicKitImage": "assets/music_kits/1019800188.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds 2", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1020124659", @@ -805,47 +509,8 @@ "musicKitImage": "assets/music_kits/1020124659.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false - }, - { - "id": "1020735512", - "name": "Jonathan Young, Starship Velociraptor", - "musicKitImage": "assets/music_kits/1020735512.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true - }, - { - "id": "1020755149", - "name": "Daniel Sadowski, Dead Shot", - "musicKitImage": "assets/music_kits/1020755149.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds 2", - "isStatTrak": true - }, - { - "id": "1021464349", - "name": "Noisia, Sharpened", - "musicKitImage": "assets/music_kits/1021464349.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1023014483", - "name": "Proxy, Battlepack", - "musicKitImage": "assets/music_kits/1023014483.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1023991939", - "name": "New Beat Fund, Sponge Fingerz", - "musicKitImage": "assets/music_kits/1023991939.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1024081196", @@ -853,31 +518,8 @@ "musicKitImage": "assets/music_kits/1024081196.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1024340982", - "name": "Juelz, Floorspace", - "musicKitImage": "assets/music_kits/1024340982.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true - }, - { - "id": "1024737315", - "name": "Sam Marshall, Bodacious", - "musicKitImage": "assets/music_kits/1024737315.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds", - "isStatTrak": true - }, - { - "id": "1024770198", - "name": "Ben Bromfield, Rabbit Hole", - "musicKitImage": "assets/music_kits/1024770198.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds 2", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1025264551", @@ -885,7 +527,8 @@ "musicKitImage": "assets/music_kits/1025264551.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1025275940", @@ -893,7 +536,8 @@ "musicKitImage": "assets/music_kits/1025275940.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1025324012", @@ -901,23 +545,8 @@ "musicKitImage": "assets/music_kits/1025324012.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1026524570", - "name": "Tim Huling, Devil's Paintbrush", - "musicKitImage": "assets/music_kits/1026524570.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds 2", - "isStatTrak": true - }, - { - "id": "1026639040", - "name": "TWERL, Ekko & Sidetrack, Under Bright Lights", - "musicKitImage": "assets/music_kits/1026639040.webp", - "rarity": "HIGH_GRADE", - "collection": "NIGHTMODE", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1027126827", @@ -925,55 +554,8 @@ "musicKitImage": "assets/music_kits/1027126827.webp", "rarity": "HIGH_GRADE", "collection": "Tacticians", - "isStatTrak": true - }, - { - "id": "1027598311", - "name": "Skog, II-Headshot", - "musicKitImage": "assets/music_kits/1027598311.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1028581175", - "name": "Midnight Riders, All I Want for Christmas", - "musicKitImage": "assets/music_kits/1028581175.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1029067740", - "name": "Lennie Moore, Java Havana Funkaloo", - "musicKitImage": "assets/music_kits/1029067740.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1029448649", - "name": "The Verkkars & n0thing, Flashbang Dance", - "musicKitImage": "assets/music_kits/1029448649.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1029802512", - "name": "Tree Adams and Ben Bromfield, M.U.D.D. FORCE", - "musicKitImage": "assets/music_kits/1029802512.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds", - "isStatTrak": true - }, - { - "id": "1032471984", - "name": "Michael Bross, Invasion!", - "musicKitImage": "assets/music_kits/1032471984.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1032490082", @@ -981,15 +563,8 @@ "musicKitImage": "assets/music_kits/1032490082.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false - }, - { - "id": "1032701438", - "name": "Daniel Sadowski, Eye of the Dragon", - "musicKitImage": "assets/music_kits/1032701438.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1033066698", @@ -997,7 +572,8 @@ "musicKitImage": "assets/music_kits/1033066698.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1033436697", @@ -1005,7 +581,8 @@ "musicKitImage": "assets/music_kits/1033436697.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1034191515", @@ -1013,39 +590,8 @@ "musicKitImage": "assets/music_kits/1034191515.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1034373778", - "name": "Chipzel, ~Yellow Magic~", - "musicKitImage": "assets/music_kits/1034373778.webp", - "rarity": "HIGH_GRADE", - "collection": "Tacticians", - "isStatTrak": false - }, - { - "id": "1034543151", - "name": "Laura Shigihara: Work Hard, Play Hard", - "musicKitImage": "assets/music_kits/1034543151.webp", - "rarity": "HIGH_GRADE", - "collection": "Tacticians", - "isStatTrak": false - }, - { - "id": "1034676233", - "name": "Kelly Bailey, Hazardous Environments", - "musicKitImage": "assets/music_kits/1034676233.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1034776466", - "name": "Scarlxrd, CHAIN$AW.LXADXUT.", - "musicKitImage": "assets/music_kits/1034776466.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1036144690", @@ -1053,7 +599,8 @@ "musicKitImage": "assets/music_kits/1036144690.webp", "rarity": "HIGH_GRADE", "collection": "Radicals", - "isStatTrak": true + "hasRegular": false, + "hasStatTrak": true }, { "id": "1036314880", @@ -1061,15 +608,8 @@ "musicKitImage": "assets/music_kits/1036314880.webp", "rarity": "HIGH_GRADE", "collection": "Radicals", - "isStatTrak": true - }, - { - "id": "1036612800", - "name": "Austin Wintory, Desert Fire", - "musicKitImage": "assets/music_kits/1036612800.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": false, + "hasStatTrak": true }, { "id": "1036996197", @@ -1077,7 +617,8 @@ "musicKitImage": "assets/music_kits/1036996197.webp", "rarity": "HIGH_GRADE", "collection": "Radicals", - "isStatTrak": true + "hasRegular": false, + "hasStatTrak": true }, { "id": "1037048944", @@ -1085,23 +626,8 @@ "musicKitImage": "assets/music_kits/1037048944.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1038233452", - "name": "Troels Folmann, Uber Blasto Phone", - "musicKitImage": "assets/music_kits/1038233452.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1038272247", - "name": "Neck Deep, The Lowlife Pack", - "musicKitImage": "assets/music_kits/1038272247.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": false }, { "id": "1038380485", @@ -1109,23 +635,8 @@ "musicKitImage": "assets/music_kits/1038380485.webp", "rarity": "HIGH_GRADE", "collection": "Tacticians", - "isStatTrak": true - }, - { - "id": "1040151842", - "name": "Ghost, SkeletĆ”", - "musicKitImage": "assets/music_kits/1040151842.webp", - "rarity": "HIGH_GRADE", - "collection": "Deluge", - "isStatTrak": true - }, - { - "id": "1040470171", - "name": "Dren, Gunman Taco Truck", - "musicKitImage": "assets/music_kits/1040470171.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1040958037", @@ -1133,39 +644,8 @@ "musicKitImage": "assets/music_kits/1040958037.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1043031531", - "name": "Mateo Messina, For No Mankind", - "musicKitImage": "assets/music_kits/1043031531.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1044890021", - "name": "Dren, Death's Head Demolition", - "musicKitImage": "assets/music_kits/1044890021.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1044942782", - "name": "Jesse Harlin, Astro Bellum", - "musicKitImage": "assets/music_kits/1044942782.webp", - "rarity": "HIGH_GRADE", - "collection": "Tacticians", - "isStatTrak": false - }, - { - "id": "1046878779", - "name": "AWOLNATION, I Am", - "musicKitImage": "assets/music_kits/1046878779.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1047455470", @@ -1173,7 +653,8 @@ "musicKitImage": "assets/music_kits/1047455470.webp", "rarity": "HIGH_GRADE", "collection": "Radicals", - "isStatTrak": true + "hasRegular": false, + "hasStatTrak": true }, { "id": "1047586438", @@ -1181,7 +662,8 @@ "musicKitImage": "assets/music_kits/1047586438.webp", "rarity": "HIGH_GRADE", "collection": "NIGHTMODE", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1047665408", @@ -1189,7 +671,8 @@ "musicKitImage": "assets/music_kits/1047665408.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1047794109", @@ -1197,7 +680,8 @@ "musicKitImage": "assets/music_kits/1047794109.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1048343219", @@ -1205,15 +689,8 @@ "musicKitImage": "assets/music_kits/1048343219.webp", "rarity": "HIGH_GRADE", "collection": "NIGHTMODE", - "isStatTrak": false - }, - { - "id": "1049894869", - "name": "Ian Hultquist, Lion's Mouth", - "musicKitImage": "assets/music_kits/1049894869.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1050160811", @@ -1221,7 +698,8 @@ "musicKitImage": "assets/music_kits/1050160811.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1050389227", @@ -1229,15 +707,8 @@ "musicKitImage": "assets/music_kits/1050389227.webp", "rarity": "HIGH_GRADE", "collection": "Initiators", - "isStatTrak": false - }, - { - "id": "1051886114", - "name": "Austin Wintory, Bachram", - "musicKitImage": "assets/music_kits/1051886114.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1052161809", @@ -1245,7 +716,8 @@ "musicKitImage": "assets/music_kits/1052161809.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1052316358", @@ -1253,15 +725,8 @@ "musicKitImage": "assets/music_kits/1052316358.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds 2", - "isStatTrak": false - }, - { - "id": "1052897721", - "name": "Darude, Moments CS:GO", - "musicKitImage": "assets/music_kits/1052897721.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1053191400", @@ -1269,7 +734,8 @@ "musicKitImage": "assets/music_kits/1053191400.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1053895197", @@ -1277,7 +743,8 @@ "musicKitImage": "assets/music_kits/1053895197.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1054885947", @@ -1285,7 +752,8 @@ "musicKitImage": "assets/music_kits/1054885947.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds 2", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1055101006", @@ -1293,15 +761,8 @@ "musicKitImage": "assets/music_kits/1055101006.webp", "rarity": "HIGH_GRADE", "collection": "Tacticians", - "isStatTrak": true - }, - { - "id": "1057256691", - "name": "Austin Wintory, The Devil Went Clubbing In Georgia", - "musicKitImage": "assets/music_kits/1057256691.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds 2", - "isStatTrak": true + "hasRegular": true, + "hasStatTrak": true }, { "id": "1057745533", @@ -1309,7 +770,8 @@ "musicKitImage": "assets/music_kits/1057745533.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1059220517", @@ -1317,7 +779,8 @@ "musicKitImage": "assets/music_kits/1059220517.webp", "rarity": "HIGH_GRADE", "collection": "NIGHTMODE", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1059406591", @@ -1325,7 +788,8 @@ "musicKitImage": "assets/music_kits/1059406591.webp", "rarity": "HIGH_GRADE", "collection": "Masterminds", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1059508163", @@ -1333,7 +797,8 @@ "musicKitImage": "assets/music_kits/1059508163.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1061750258", @@ -1341,7 +806,8 @@ "musicKitImage": "assets/music_kits/1061750258.webp", "rarity": "HIGH_GRADE", "collection": "Initiators", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1062814563", @@ -1349,7 +815,8 @@ "musicKitImage": "assets/music_kits/1062814563.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": false }, { "id": "1063058078", @@ -1357,7 +824,8 @@ "musicKitImage": "assets/music_kits/1063058078.webp", "rarity": "HIGH_GRADE", "collection": "Deluge", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1063466361", @@ -1365,7 +833,8 @@ "musicKitImage": "assets/music_kits/1063466361.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true }, { "id": "1063784694", @@ -1373,38 +842,7 @@ "musicKitImage": "assets/music_kits/1063784694.webp", "rarity": "HIGH_GRADE", "collection": null, - "isStatTrak": false - }, - { - "id": "1065487654", - "name": "Denzel Curry, ULTIMATE", - "musicKitImage": "assets/music_kits/1065487654.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1066435192", - "name": "Robert Allaire, Insurgency", - "musicKitImage": "assets/music_kits/1066435192.webp", - "rarity": "HIGH_GRADE", - "collection": null, - "isStatTrak": true - }, - { - "id": "1066759080", - "name": "Dren McDonald, Coffee! Kofe! Kahveh!", - "musicKitImage": "assets/music_kits/1066759080.webp", - "rarity": "HIGH_GRADE", - "collection": "Masterminds 2", - "isStatTrak": true - }, - { - "id": "1069896488", - "name": "Freaky DNA, Vici", - "musicKitImage": "assets/music_kits/1069896488.webp", - "rarity": "HIGH_GRADE", - "collection": "Tacticians", - "isStatTrak": false + "hasRegular": true, + "hasStatTrak": true } ] diff --git a/assets/data/operation_collections.json b/assets/data/operation_collections.json deleted file mode 100644 index 14b2eda6..00000000 --- a/assets/data/operation_collections.json +++ /dev/null @@ -1,202 +0,0 @@ -[ - { - "id": "20023", - "name": "The Chop Shop Collection", - "image": "assets/operation_collections/20023.webp", - "operationId": "BLOODHOUND", - "operationName": "Operation Bloodhound", - "releaseDate": "2015-05-26" - }, - { - "id": "20015", - "name": "The Gods and Monsters Collection", - "image": "assets/operation_collections/20015.webp", - "operationId": "BLOODHOUND", - "operationName": "Operation Bloodhound", - "releaseDate": "2015-05-26" - }, - { - "id": "20001", - "name": "The Rising Sun Collection", - "image": "assets/operation_collections/20001.webp", - "operationId": "BLOODHOUND", - "operationName": "Operation Bloodhound", - "releaseDate": "2015-05-26" - }, - { - "id": "20007", - "name": "The Dust 2 Collection", - "image": "assets/operation_collections/20007.webp", - "operationId": "BRAVO", - "operationName": "Operation Bravo", - "releaseDate": "2013-09-19" - }, - { - "id": "20009", - "name": "The Dust Collection", - "image": "assets/operation_collections/20009.webp", - "operationId": "BRAVO", - "operationName": "Operation Bravo", - "releaseDate": "2013-09-19" - }, - { - "id": "20012", - "name": "The Italy Collection", - "image": "assets/operation_collections/20012.webp", - "operationId": "BRAVO", - "operationName": "Operation Bravo", - "releaseDate": "2013-09-19" - }, - { - "id": "20014", - "name": "The Lake Collection", - "image": "assets/operation_collections/20014.webp", - "operationId": "BRAVO", - "operationName": "Operation Bravo", - "releaseDate": "2013-09-19" - }, - { - "id": "20013", - "name": "The Mirage Collection", - "image": "assets/operation_collections/20013.webp", - "operationId": "BRAVO", - "operationName": "Operation Bravo", - "releaseDate": "2013-09-19" - }, - { - "id": "20016", - "name": "The Safehouse Collection", - "image": "assets/operation_collections/20016.webp", - "operationId": "BRAVO", - "operationName": "Operation Bravo", - "releaseDate": "2013-09-19" - }, - { - "id": "20022", - "name": "The Train Collection", - "image": "assets/operation_collections/20022.webp", - "operationId": "BRAVO", - "operationName": "Operation Bravo", - "releaseDate": "2013-09-19" - }, - { - "id": "20003", - "name": "The Baggage Collection", - "image": "assets/operation_collections/20003.webp", - "operationId": "BREAKOUT", - "operationName": "Operation Breakout", - "releaseDate": "2014-07-01" - }, - { - "id": "20017", - "name": "The Cache Collection", - "image": "assets/operation_collections/20017.webp", - "operationId": "BREAKOUT", - "operationName": "Operation Breakout", - "releaseDate": "2014-07-01" - }, - { - "id": "20018", - "name": "The Cobblestone Collection", - "image": "assets/operation_collections/20018.webp", - "operationId": "BREAKOUT", - "operationName": "Operation Breakout", - "releaseDate": "2014-07-01" - }, - { - "id": "20019", - "name": "The Overpass Collection", - "image": "assets/operation_collections/20019.webp", - "operationId": "BREAKOUT", - "operationName": "Operation Breakout", - "releaseDate": "2014-07-01" - }, - { - "id": "20011", - "name": "The Assault Collection", - "image": "assets/operation_collections/20011.webp", - "operationId": "PAYBACK", - "operationName": "Operation Payback", - "releaseDate": "2013-04-25" - }, - { - "id": "20008", - "name": "The Aztec Collection", - "image": "assets/operation_collections/20008.webp", - "operationId": "PAYBACK", - "operationName": "Operation Payback", - "releaseDate": "2013-04-25" - }, - { - "id": "20024", - "name": "The Inferno Collection", - "image": "assets/operation_collections/20024.webp", - "operationId": "PAYBACK", - "operationName": "Operation Payback", - "releaseDate": "2013-04-25" - }, - { - "id": "20020", - "name": "The Militia Collection", - "image": "assets/operation_collections/20020.webp", - "operationId": "PAYBACK", - "operationName": "Operation Payback", - "releaseDate": "2013-04-25" - }, - { - "id": "20021", - "name": "The Nuke Collection", - "image": "assets/operation_collections/20021.webp", - "operationId": "PAYBACK", - "operationName": "Operation Payback", - "releaseDate": "2013-04-25" - }, - { - "id": "20025", - "name": "The Office Collection", - "image": "assets/operation_collections/20025.webp", - "operationId": "PAYBACK", - "operationName": "Operation Payback", - "releaseDate": "2013-04-25" - }, - { - "id": "20005", - "name": "The Vertigo Collection", - "image": "assets/operation_collections/20005.webp", - "operationId": "PAYBACK", - "operationName": "Operation Payback", - "releaseDate": "2013-04-25" - }, - { - "id": "20004", - "name": "The Bank Collection", - "image": "assets/operation_collections/20004.webp", - "operationId": "PHOENIX", - "operationName": "Operation Phoenix", - "releaseDate": "2014-02-20" - }, - { - "id": "20006", - "name": "The Canals Collection", - "image": "assets/operation_collections/20006.webp", - "operationId": "SHATTERED_WEB", - "operationName": "Operation Shattered Web", - "releaseDate": "2019-11-18" - }, - { - "id": "20010", - "name": "The Norse Collection", - "image": "assets/operation_collections/20010.webp", - "operationId": "SHATTERED_WEB", - "operationName": "Operation Shattered Web", - "releaseDate": "2019-11-18" - }, - { - "id": "20002", - "name": "The St. Marc Collection", - "image": "assets/operation_collections/20002.webp", - "operationId": "SHATTERED_WEB", - "operationName": "Operation Shattered Web", - "releaseDate": "2019-11-18" - } -] diff --git a/assets/data/patch_contents.json b/assets/data/patch_contents.json index fcc9238d..6b782382 100644 --- a/assets/data/patch_contents.json +++ b/assets/data/patch_contents.json @@ -1,6 +1,6 @@ [ { - "caseId": "2234", + "containerId": "2234", "patchIds": [ "4550", "4551", @@ -26,7 +26,7 @@ ] }, { - "caseId": "2235", + "containerId": "2235", "patchIds": [ "4589", "4591", @@ -42,7 +42,7 @@ ] }, { - "caseId": "2236", + "containerId": "2236", "patchIds": [ "4571", "4572", @@ -64,7 +64,7 @@ ] }, { - "caseId": "2237", + "containerId": "2237", "patchIds": [ "4937", "4938", @@ -82,7 +82,7 @@ ] }, { - "caseId": "2238", + "containerId": "2238", "patchIds": [ "5095", "5096", @@ -105,7 +105,7 @@ ] }, { - "caseId": "2239", + "containerId": "2239", "patchIds": [ "5111", "5112", @@ -128,7 +128,7 @@ ] }, { - "caseId": "2240", + "containerId": "2240", "patchIds": [ "5079", "5080", diff --git a/assets/data/pin_contents.json b/assets/data/pin_contents.json index 225eb2b5..80294718 100644 --- a/assets/data/pin_contents.json +++ b/assets/data/pin_contents.json @@ -1,6 +1,6 @@ [ { - "caseId": "2105", + "containerId": "2105", "pinIds": [ "6101", "6102", @@ -16,7 +16,7 @@ ] }, { - "caseId": "2106", + "containerId": "2106", "pinIds": [ "6112", "6113", @@ -32,7 +32,7 @@ ] }, { - "caseId": "2107", + "containerId": "2107", "pinIds": [ "6123", "6124", @@ -48,7 +48,7 @@ ] }, { - "caseId": "2121", + "containerId": "2121", "pinIds": [ "4682", "4683", diff --git a/assets/data/reward_collections.json b/assets/data/reward_collections.json deleted file mode 100644 index e8c876d9..00000000 --- a/assets/data/reward_collections.json +++ /dev/null @@ -1,112 +0,0 @@ -[ - { - "id": "10003", - "name": "The Graphic Design Collection", - "image": "assets/reward_collections/10003.svg", - "sourceType": "ARMORY", - "sourceId": "ARMORY", - "currency": "CREDITS", - "cost": 4, - "releaseDate": "2024-10-02" - }, - { - "id": "10004", - "name": "The Overpass 2024 Collection", - "image": "assets/reward_collections/10004.svg", - "sourceType": "ARMORY", - "sourceId": "ARMORY", - "currency": "CREDITS", - "cost": 4, - "releaseDate": "2024-10-02" - }, - { - "id": "10006", - "name": "The Sport & Field Collection", - "image": "assets/reward_collections/10006.svg", - "sourceType": "ARMORY", - "sourceId": "ARMORY", - "currency": "CREDITS", - "cost": 4, - "releaseDate": "2024-10-02" - }, - { - "id": "10010", - "name": "The Train 2025 Collection", - "image": "assets/reward_collections/10010.svg", - "sourceType": "ARMORY", - "sourceId": "ARMORY", - "currency": "CREDITS", - "cost": 4, - "releaseDate": "2025-03-31" - }, - { - "id": "10001", - "name": "The Ancient Collection", - "image": "assets/reward_collections/10001.webp", - "sourceType": "OPERATION", - "sourceId": "BROKEN_FANG", - "currency": "STARS", - "cost": 4, - "releaseDate": "2020-12-03" - }, - { - "id": "10009", - "name": "The Control Collection", - "image": "assets/reward_collections/10009.webp", - "sourceType": "OPERATION", - "sourceId": "BROKEN_FANG", - "currency": "STARS", - "cost": 4, - "releaseDate": "2020-12-03" - }, - { - "id": "10002", - "name": "The Havoc Collection", - "image": "assets/reward_collections/10002.webp", - "sourceType": "OPERATION", - "sourceId": "BROKEN_FANG", - "currency": "STARS", - "cost": 4, - "releaseDate": "2020-12-03" - }, - { - "id": "10005", - "name": "The 2021 Dust 2 Collection", - "image": "assets/reward_collections/10005.webp", - "sourceType": "OPERATION", - "sourceId": "RIPTIDE", - "currency": "STARS", - "cost": 4, - "releaseDate": "2021-09-21" - }, - { - "id": "10011", - "name": "The 2021 Mirage Collection", - "image": "assets/reward_collections/10011.webp", - "sourceType": "OPERATION", - "sourceId": "RIPTIDE", - "currency": "STARS", - "cost": 4, - "releaseDate": "2021-09-21" - }, - { - "id": "10008", - "name": "The 2021 Train Collection", - "image": "assets/reward_collections/10008.webp", - "sourceType": "OPERATION", - "sourceId": "RIPTIDE", - "currency": "STARS", - "cost": 4, - "releaseDate": "2021-09-21" - }, - { - "id": "10007", - "name": "The 2021 Vertigo Collection", - "image": "assets/reward_collections/10007.webp", - "sourceType": "OPERATION", - "sourceId": "RIPTIDE", - "currency": "STARS", - "cost": 4, - "releaseDate": "2021-09-21" - } -] diff --git a/assets/data/sticker_contents.json b/assets/data/sticker_contents.json index 9edaa8ff..c0198eaf 100644 --- a/assets/data/sticker_contents.json +++ b/assets/data/sticker_contents.json @@ -1,6 +1,6 @@ [ { - "caseId": "1977", + "containerId": "1977", "stickerIds": [ "5896", "5897", @@ -70,7 +70,7 @@ ] }, { - "caseId": "1978", + "containerId": "1978", "stickerIds": [ "4882", "4883", @@ -96,7 +96,7 @@ ] }, { - "caseId": "1979", + "containerId": "1979", "stickerIds": [ "7233", "7234", @@ -122,7 +122,7 @@ ] }, { - "caseId": "1980", + "containerId": "1980", "stickerIds": [ "5556", "5557", @@ -287,7 +287,7 @@ ] }, { - "caseId": "1981", + "containerId": "1981", "stickerIds": [ "5303", "5304", @@ -328,7 +328,7 @@ ] }, { - "caseId": "1982", + "containerId": "1982", "stickerIds": [ "5876", "5877", @@ -353,7 +353,7 @@ ] }, { - "caseId": "1983", + "containerId": "1983", "stickerIds": [ "5716", "5717", @@ -518,7 +518,7 @@ ] }, { - "caseId": "1984", + "containerId": "1984", "stickerIds": [ "5335", "5336", @@ -559,7 +559,7 @@ ] }, { - "caseId": "1985", + "containerId": "1985", "stickerIds": [ "5396", "5397", @@ -724,7 +724,7 @@ ] }, { - "caseId": "1986", + "containerId": "1986", "stickerIds": [ "5271", "5272", @@ -765,7 +765,7 @@ ] }, { - "caseId": "1987", + "containerId": "1987", "stickerIds": [ "8879", "8880", @@ -930,7 +930,7 @@ ] }, { - "caseId": "1988", + "containerId": "1988", "stickerIds": [ "8586", "8587", @@ -971,7 +971,7 @@ ] }, { - "caseId": "1989", + "containerId": "1989", "stickerIds": [ "9431", "9432", @@ -996,7 +996,7 @@ ] }, { - "caseId": "1990", + "containerId": "1990", "stickerIds": [ "9039", "9040", @@ -1321,7 +1321,7 @@ ] }, { - "caseId": "1991", + "containerId": "1991", "stickerIds": [ "8618", "8619", @@ -1394,7 +1394,7 @@ ] }, { - "caseId": "1992", + "containerId": "1992", "stickerIds": [ "8719", "8720", @@ -1559,7 +1559,7 @@ ] }, { - "caseId": "1993", + "containerId": "1993", "stickerIds": [ "8554", "8555", @@ -1600,7 +1600,7 @@ ] }, { - "caseId": "1994", + "containerId": "1994", "stickerIds": [ "2018", "2021", @@ -1610,7 +1610,7 @@ ] }, { - "caseId": "1995", + "containerId": "1995", "stickerIds": [ "1580", "1583", @@ -1620,7 +1620,7 @@ ] }, { - "caseId": "1996", + "containerId": "1996", "stickerIds": [ "1270", "1273", @@ -1630,7 +1630,7 @@ ] }, { - "caseId": "1997", + "containerId": "1997", "stickerIds": [ "1824", "1827", @@ -1675,7 +1675,7 @@ ] }, { - "caseId": "1998", + "containerId": "1998", "stickerIds": [ "672", "675", @@ -1720,7 +1720,7 @@ ] }, { - "caseId": "1999", + "containerId": "1999", "stickerIds": [ "1401", "1404", @@ -1765,7 +1765,7 @@ ] }, { - "caseId": "2000", + "containerId": "2000", "stickerIds": [ "1076", "1079", @@ -1810,7 +1810,7 @@ ] }, { - "caseId": "2001", + "containerId": "2001", "stickerIds": [ "605", "608", @@ -1820,7 +1820,7 @@ ] }, { - "caseId": "2002", + "containerId": "2002", "stickerIds": [ "686", "689", @@ -1830,7 +1830,7 @@ ] }, { - "caseId": "2003", + "containerId": "2003", "stickerIds": [ "1090", "1093", @@ -1840,7 +1840,7 @@ ] }, { - "caseId": "2004", + "containerId": "2004", "stickerIds": [ "671", "674", @@ -1850,7 +1850,7 @@ ] }, { - "caseId": "2005", + "containerId": "2005", "stickerIds": [ "590", "593", @@ -1860,7 +1860,7 @@ ] }, { - "caseId": "2006", + "containerId": "2006", "stickerIds": [ "1385", "1388", @@ -1870,7 +1870,7 @@ ] }, { - "caseId": "2007", + "containerId": "2007", "stickerIds": [ "1075", "1078", @@ -1880,7 +1880,7 @@ ] }, { - "caseId": "2008", + "containerId": "2008", "stickerIds": [ "1898", "1901", @@ -1890,7 +1890,7 @@ ] }, { - "caseId": "2009", + "containerId": "2009", "stickerIds": [ "1460", "1463", @@ -1900,7 +1900,7 @@ ] }, { - "caseId": "2010", + "containerId": "2010", "stickerIds": [ "1150", "1153", @@ -1910,7 +1910,7 @@ ] }, { - "caseId": "2011", + "containerId": "2011", "stickerIds": [ "1868", "1871", @@ -1920,7 +1920,7 @@ ] }, { - "caseId": "2012", + "containerId": "2012", "stickerIds": [ "716", "719", @@ -1930,7 +1930,7 @@ ] }, { - "caseId": "2013", + "containerId": "2013", "stickerIds": [ "530", "533", @@ -1940,7 +1940,7 @@ ] }, { - "caseId": "2014", + "containerId": "2014", "stickerIds": [ "1430", "1433", @@ -1950,7 +1950,7 @@ ] }, { - "caseId": "2015", + "containerId": "2015", "stickerIds": [ "1120", "1123", @@ -1960,7 +1960,7 @@ ] }, { - "caseId": "2016", + "containerId": "2016", "stickerIds": [ "1883", "1886", @@ -1970,7 +1970,7 @@ ] }, { - "caseId": "2017", + "containerId": "2017", "stickerIds": [ "731", "734", @@ -1980,7 +1980,7 @@ ] }, { - "caseId": "2018", + "containerId": "2018", "stickerIds": [ "380", "383", @@ -1990,7 +1990,7 @@ ] }, { - "caseId": "2019", + "containerId": "2019", "stickerIds": [ "1445", "1448", @@ -2000,7 +2000,7 @@ ] }, { - "caseId": "2020", + "containerId": "2020", "stickerIds": [ "1135", "1138", @@ -2010,7 +2010,7 @@ ] }, { - "caseId": "2021", + "containerId": "2021", "stickerIds": [ "2003", "2006", @@ -2020,7 +2020,7 @@ ] }, { - "caseId": "2022", + "containerId": "2022", "stickerIds": [ "746", "749", @@ -2030,7 +2030,7 @@ ] }, { - "caseId": "2023", + "containerId": "2023", "stickerIds": [ "1565", "1568", @@ -2040,7 +2040,7 @@ ] }, { - "caseId": "2024", + "containerId": "2024", "stickerIds": [ "1255", "1258", @@ -2050,7 +2050,7 @@ ] }, { - "caseId": "2025", + "containerId": "2025", "stickerIds": [ "1958", "1961", @@ -2060,7 +2060,7 @@ ] }, { - "caseId": "2026", + "containerId": "2026", "stickerIds": [ "2033", "2036", @@ -2070,7 +2070,7 @@ ] }, { - "caseId": "2027", + "containerId": "2027", "stickerIds": [ "1595", "1598", @@ -2080,7 +2080,7 @@ ] }, { - "caseId": "2028", + "containerId": "2028", "stickerIds": [ "1285", "1288", @@ -2090,7 +2090,7 @@ ] }, { - "caseId": "2029", + "containerId": "2029", "stickerIds": [ "426", "429", @@ -2115,7 +2115,7 @@ ] }, { - "caseId": "2030", + "containerId": "2030", "stickerIds": [ "396", "399", @@ -2140,7 +2140,7 @@ ] }, { - "caseId": "2031", + "containerId": "2031", "stickerIds": [ "381", "384", @@ -2165,7 +2165,7 @@ ] }, { - "caseId": "2032", + "containerId": "2032", "stickerIds": [ "471", "474", @@ -2190,7 +2190,7 @@ ] }, { - "caseId": "2033", + "containerId": "2033", "stickerIds": [ "1823", "1826", @@ -2200,7 +2200,7 @@ ] }, { - "caseId": "2034", + "containerId": "2034", "stickerIds": [ "1869", "1872", @@ -2245,7 +2245,7 @@ ] }, { - "caseId": "2035", + "containerId": "2035", "stickerIds": [ "702", "705", @@ -2290,7 +2290,7 @@ ] }, { - "caseId": "2036", + "containerId": "2036", "stickerIds": [ "1386", "1389", @@ -2335,7 +2335,7 @@ ] }, { - "caseId": "2037", + "containerId": "2037", "stickerIds": [ "1106", "1109", @@ -2380,7 +2380,7 @@ ] }, { - "caseId": "2038", + "containerId": "2038", "stickerIds": [ "761", "764", @@ -2390,7 +2390,7 @@ ] }, { - "caseId": "2039", + "containerId": "2039", "stickerIds": [ "395", "398", @@ -2400,7 +2400,7 @@ ] }, { - "caseId": "2040", + "containerId": "2040", "stickerIds": [ "1165", "1168", @@ -2410,7 +2410,7 @@ ] }, { - "caseId": "2041", + "containerId": "2041", "stickerIds": [ "1943", "1946", @@ -2420,7 +2420,7 @@ ] }, { - "caseId": "2042", + "containerId": "2042", "stickerIds": [ "791", "794", @@ -2430,7 +2430,7 @@ ] }, { - "caseId": "2043", + "containerId": "2043", "stickerIds": [ "410", "413", @@ -2440,7 +2440,7 @@ ] }, { - "caseId": "2044", + "containerId": "2044", "stickerIds": [ "1505", "1508", @@ -2450,7 +2450,7 @@ ] }, { - "caseId": "2045", + "containerId": "2045", "stickerIds": [ "1195", "1198", @@ -2460,7 +2460,7 @@ ] }, { - "caseId": "2046", + "containerId": "2046", "stickerIds": [ "806", "809", @@ -2470,7 +2470,7 @@ ] }, { - "caseId": "2047", + "containerId": "2047", "stickerIds": [ "425", "428", @@ -2480,7 +2480,7 @@ ] }, { - "caseId": "2048", + "containerId": "2048", "stickerIds": [ "1520", "1523", @@ -2490,7 +2490,7 @@ ] }, { - "caseId": "2049", + "containerId": "2049", "stickerIds": [ "1210", "1213", @@ -2500,7 +2500,7 @@ ] }, { - "caseId": "2050", + "containerId": "2050", "stickerIds": [ "1973", "1976", @@ -2510,7 +2510,7 @@ ] }, { - "caseId": "2051", + "containerId": "2051", "stickerIds": [ "1838", "1841", @@ -2520,7 +2520,7 @@ ] }, { - "caseId": "2052", + "containerId": "2052", "stickerIds": [ "1400", "1403", @@ -2530,7 +2530,7 @@ ] }, { - "caseId": "2053", + "containerId": "2053", "stickerIds": [ "500", "503", @@ -2540,7 +2540,7 @@ ] }, { - "caseId": "2054", + "containerId": "2054", "stickerIds": [ "1913", "1916", @@ -2550,7 +2550,7 @@ ] }, { - "caseId": "2055", + "containerId": "2055", "stickerIds": [ "1475", "1478", @@ -2560,7 +2560,7 @@ ] }, { - "caseId": "2056", + "containerId": "2056", "stickerIds": [ "1225", "1228", @@ -2570,7 +2570,7 @@ ] }, { - "caseId": "2057", + "containerId": "2057", "stickerIds": [ "821", "824", @@ -2580,7 +2580,7 @@ ] }, { - "caseId": "2058", + "containerId": "2058", "stickerIds": [ "1535", "1538", @@ -2590,7 +2590,7 @@ ] }, { - "caseId": "2059", + "containerId": "2059", "stickerIds": [ "1853", "1856", @@ -2600,7 +2600,7 @@ ] }, { - "caseId": "2060", + "containerId": "2060", "stickerIds": [ "701", "704", @@ -2610,7 +2610,7 @@ ] }, { - "caseId": "2061", + "containerId": "2061", "stickerIds": [ "440", "443", @@ -2620,7 +2620,7 @@ ] }, { - "caseId": "2062", + "containerId": "2062", "stickerIds": [ "1415", "1418", @@ -2630,7 +2630,7 @@ ] }, { - "caseId": "2063", + "containerId": "2063", "stickerIds": [ "1105", "1108", @@ -2640,7 +2640,7 @@ ] }, { - "caseId": "2064", + "containerId": "2064", "stickerIds": [ "515", "518", @@ -2650,7 +2650,7 @@ ] }, { - "caseId": "2065", + "containerId": "2065", "stickerIds": [ "545", "548", @@ -2660,7 +2660,7 @@ ] }, { - "caseId": "2066", + "containerId": "2066", "stickerIds": [ "1988", "1991", @@ -2670,7 +2670,7 @@ ] }, { - "caseId": "2067", + "containerId": "2067", "stickerIds": [ "836", "839", @@ -2680,7 +2680,7 @@ ] }, { - "caseId": "2068", + "containerId": "2068", "stickerIds": [ "1550", "1553", @@ -2690,7 +2690,7 @@ ] }, { - "caseId": "2069", + "containerId": "2069", "stickerIds": [ "1240", "1243", @@ -2700,7 +2700,7 @@ ] }, { - "caseId": "2070", + "containerId": "2070", "stickerIds": [ "866", "869", @@ -2710,7 +2710,7 @@ ] }, { - "caseId": "2071", + "containerId": "2071", "stickerIds": [ "455", "458", @@ -2720,7 +2720,7 @@ ] }, { - "caseId": "2072", + "containerId": "2072", "stickerIds": [ "560", "563", @@ -2730,7 +2730,7 @@ ] }, { - "caseId": "2073", + "containerId": "2073", "stickerIds": [ "851", "854", @@ -2740,7 +2740,7 @@ ] }, { - "caseId": "2074", + "containerId": "2074", "stickerIds": [ "575", "578", @@ -2750,7 +2750,7 @@ ] }, { - "caseId": "2075", + "containerId": "2075", "stickerIds": [ "881", "884", @@ -2760,7 +2760,7 @@ ] }, { - "caseId": "2076", + "containerId": "2076", "stickerIds": [ "2048", "2051", @@ -2770,7 +2770,7 @@ ] }, { - "caseId": "2077", + "containerId": "2077", "stickerIds": [ "896", "899", @@ -2780,7 +2780,7 @@ ] }, { - "caseId": "2078", + "containerId": "2078", "stickerIds": [ "470", "473", @@ -2790,7 +2790,7 @@ ] }, { - "caseId": "2079", + "containerId": "2079", "stickerIds": [ "1610", "1613", @@ -2800,7 +2800,7 @@ ] }, { - "caseId": "2080", + "containerId": "2080", "stickerIds": [ "1300", "1303", @@ -2810,7 +2810,7 @@ ] }, { - "caseId": "2081", + "containerId": "2081", "stickerIds": [ "1928", "1931", @@ -2820,7 +2820,7 @@ ] }, { - "caseId": "2082", + "containerId": "2082", "stickerIds": [ "776", "779", @@ -2830,7 +2830,7 @@ ] }, { - "caseId": "2083", + "containerId": "2083", "stickerIds": [ "485", "488", @@ -2840,7 +2840,7 @@ ] }, { - "caseId": "2084", + "containerId": "2084", "stickerIds": [ "1490", "1493", @@ -2850,7 +2850,7 @@ ] }, { - "caseId": "2085", + "containerId": "2085", "stickerIds": [ "1180", "1183", @@ -2860,7 +2860,7 @@ ] }, { - "caseId": "2086", + "containerId": "2086", "stickerIds": [ "4903", "4904", @@ -2882,7 +2882,7 @@ ] }, { - "caseId": "2087", + "containerId": "2087", "stickerIds": [ "4144", "4145", @@ -3007,7 +3007,7 @@ ] }, { - "caseId": "2088", + "containerId": "2088", "stickerIds": [ "4339", "4340", @@ -3177,7 +3177,7 @@ ] }, { - "caseId": "2089", + "containerId": "2089", "stickerIds": [ "4264", "4265", @@ -3257,7 +3257,7 @@ ] }, { - "caseId": "2090", + "containerId": "2090", "stickerIds": [ "1639", "1640", @@ -3276,7 +3276,7 @@ ] }, { - "caseId": "2091", + "containerId": "2091", "stickerIds": [ "2561", "2562", @@ -3386,7 +3386,7 @@ ] }, { - "caseId": "2092", + "containerId": "2092", "stickerIds": [ "2561", "2562", @@ -3511,7 +3511,7 @@ ] }, { - "caseId": "2093", + "containerId": "2093", "stickerIds": [ "2801", "2802", @@ -3636,7 +3636,7 @@ ] }, { - "caseId": "2094", + "containerId": "2094", "stickerIds": [ "2801", "2802", @@ -3761,7 +3761,7 @@ ] }, { - "caseId": "2095", + "containerId": "2095", "stickerIds": [ "2681", "2682", @@ -3886,7 +3886,7 @@ ] }, { - "caseId": "2096", + "containerId": "2096", "stickerIds": [ "9814", "9815", @@ -4051,7 +4051,7 @@ ] }, { - "caseId": "2097", + "containerId": "2097", "stickerIds": [ "9521", "9522", @@ -4092,7 +4092,7 @@ ] }, { - "caseId": "2098", + "containerId": "2098", "stickerIds": [ "10294", "10295", @@ -4117,7 +4117,7 @@ ] }, { - "caseId": "2099", + "containerId": "2099", "stickerIds": [ "9974", "9975", @@ -4442,7 +4442,7 @@ ] }, { - "caseId": "2100", + "containerId": "2100", "stickerIds": [ "9553", "9554", @@ -4515,7 +4515,7 @@ ] }, { - "caseId": "2101", + "containerId": "2101", "stickerIds": [ "9654", "9655", @@ -4680,7 +4680,7 @@ ] }, { - "caseId": "2102", + "containerId": "2102", "stickerIds": [ "9489", "9490", @@ -4721,7 +4721,7 @@ ] }, { - "caseId": "2103", + "containerId": "2103", "stickerIds": [ "4513", "4514", @@ -4746,7 +4746,7 @@ ] }, { - "caseId": "2104", + "containerId": "2104", "stickerIds": [ "3966", "3967", @@ -4768,7 +4768,7 @@ ] }, { - "caseId": "2108", + "containerId": "2108", "stickerIds": [ "2921", "2922", @@ -4787,7 +4787,7 @@ ] }, { - "caseId": "2109", + "containerId": "2109", "stickerIds": [ "101", "104", @@ -4809,7 +4809,7 @@ ] }, { - "caseId": "2110", + "containerId": "2110", "stickerIds": [ "7539", "7540", @@ -4974,7 +4974,7 @@ ] }, { - "caseId": "2111", + "containerId": "2111", "stickerIds": [ "7286", "7287", @@ -5015,7 +5015,7 @@ ] }, { - "caseId": "2112", + "containerId": "2112", "stickerIds": [ "7859", "7860", @@ -5040,7 +5040,7 @@ ] }, { - "caseId": "2113", + "containerId": "2113", "stickerIds": [ "7699", "7700", @@ -5205,7 +5205,7 @@ ] }, { - "caseId": "2114", + "containerId": "2114", "stickerIds": [ "7318", "7319", @@ -5246,7 +5246,7 @@ ] }, { - "caseId": "2115", + "containerId": "2115", "stickerIds": [ "7379", "7380", @@ -5411,7 +5411,7 @@ ] }, { - "caseId": "2116", + "containerId": "2116", "stickerIds": [ "7254", "7255", @@ -5452,7 +5452,7 @@ ] }, { - "caseId": "2118", + "containerId": "2118", "stickerIds": [ "355", "356", @@ -5472,7 +5472,7 @@ ] }, { - "caseId": "2119", + "containerId": "2119", "stickerIds": [ "6586", "6587", @@ -5498,7 +5498,7 @@ ] }, { - "caseId": "2120", + "containerId": "2120", "stickerIds": [ "3945", "3946", @@ -5521,7 +5521,7 @@ ] }, { - "caseId": "2122", + "containerId": "2122", "stickerIds": [ "4601", "4602", @@ -5539,7 +5539,7 @@ ] }, { - "caseId": "2123", + "containerId": "2123", "stickerIds": [ "4533", "4534", @@ -5561,7 +5561,7 @@ ] }, { - "caseId": "2125", + "containerId": "2125", "stickerIds": [ "3585", "3586", @@ -5686,7 +5686,7 @@ ] }, { - "caseId": "2126", + "containerId": "2126", "stickerIds": [ "3600", "3601", @@ -5841,7 +5841,7 @@ ] }, { - "caseId": "2127", + "containerId": "2127", "stickerIds": [ "3630", "3631", @@ -5936,7 +5936,7 @@ ] }, { - "caseId": "2128", + "containerId": "2128", "stickerIds": [ "2268", "2269", @@ -6061,7 +6061,7 @@ ] }, { - "caseId": "2129", + "containerId": "2129", "stickerIds": [ "2148", "2149", @@ -6186,7 +6186,7 @@ ] }, { - "caseId": "2130", + "containerId": "2130", "stickerIds": [ "3080", "3081", @@ -6311,7 +6311,7 @@ ] }, { - "caseId": "2131", + "containerId": "2131", "stickerIds": [ "3320", "3321", @@ -6436,7 +6436,7 @@ ] }, { - "caseId": "2132", + "containerId": "2132", "stickerIds": [ "3200", "3201", @@ -6561,7 +6561,7 @@ ] }, { - "caseId": "2136", + "containerId": "2136", "stickerIds": [ "6893", "6894", @@ -6726,7 +6726,7 @@ ] }, { - "caseId": "2137", + "containerId": "2137", "stickerIds": [ "6640", "6641", @@ -6767,7 +6767,7 @@ ] }, { - "caseId": "2138", + "containerId": "2138", "stickerIds": [ "7213", "7214", @@ -6792,7 +6792,7 @@ ] }, { - "caseId": "2139", + "containerId": "2139", "stickerIds": [ "7053", "7054", @@ -6957,7 +6957,7 @@ ] }, { - "caseId": "2140", + "containerId": "2140", "stickerIds": [ "6672", "6673", @@ -6998,7 +6998,7 @@ ] }, { - "caseId": "2141", + "containerId": "2141", "stickerIds": [ "6733", "6734", @@ -7163,7 +7163,7 @@ ] }, { - "caseId": "2142", + "containerId": "2142", "stickerIds": [ "6608", "6609", @@ -7204,7 +7204,7 @@ ] }, { - "caseId": "2143", + "containerId": "2143", "stickerIds": [ "2388", "2389", @@ -7224,7 +7224,7 @@ ] }, { - "caseId": "2144", + "containerId": "2144", "stickerIds": [ "2403", "2404", @@ -7244,7 +7244,7 @@ ] }, { - "caseId": "2145", + "containerId": "2145", "stickerIds": [ "962", "963", @@ -7261,7 +7261,7 @@ ] }, { - "caseId": "2146", + "containerId": "2146", "stickerIds": [ "4797", "4798", @@ -7286,7 +7286,7 @@ ] }, { - "caseId": "2147", + "containerId": "2147", "stickerIds": [ "6246", "6247", @@ -7451,7 +7451,7 @@ ] }, { - "caseId": "2148", + "containerId": "2148", "stickerIds": [ "5993", "5994", @@ -7492,7 +7492,7 @@ ] }, { - "caseId": "2149", + "containerId": "2149", "stickerIds": [ "6566", "6567", @@ -7517,7 +7517,7 @@ ] }, { - "caseId": "2150", + "containerId": "2150", "stickerIds": [ "6406", "6407", @@ -7682,7 +7682,7 @@ ] }, { - "caseId": "2151", + "containerId": "2151", "stickerIds": [ "6025", "6026", @@ -7723,7 +7723,7 @@ ] }, { - "caseId": "2152", + "containerId": "2152", "stickerIds": [ "6086", "6087", @@ -7888,7 +7888,7 @@ ] }, { - "caseId": "2153", + "containerId": "2153", "stickerIds": [ "5961", "5962", @@ -7929,7 +7929,7 @@ ] }, { - "caseId": "2154", + "containerId": "2154", "stickerIds": [ "8213", "8214", @@ -8094,7 +8094,7 @@ ] }, { - "caseId": "2155", + "containerId": "2155", "stickerIds": [ "7960", "7961", @@ -8135,7 +8135,7 @@ ] }, { - "caseId": "2156", + "containerId": "2156", "stickerIds": [ "8533", "8534", @@ -8160,7 +8160,7 @@ ] }, { - "caseId": "2157", + "containerId": "2157", "stickerIds": [ "8373", "8374", @@ -8325,7 +8325,7 @@ ] }, { - "caseId": "2158", + "containerId": "2158", "stickerIds": [ "7992", "7993", @@ -8366,7 +8366,7 @@ ] }, { - "caseId": "2159", + "containerId": "2159", "stickerIds": [ "8053", "8054", @@ -8531,7 +8531,7 @@ ] }, { - "caseId": "2160", + "containerId": "2160", "stickerIds": [ "7928", "7929", @@ -8572,7 +8572,7 @@ ] }, { - "caseId": "2161", + "containerId": "2161", "stickerIds": [ "3440", "3441", @@ -8595,7 +8595,7 @@ ] }, { - "caseId": "2162", + "containerId": "2162", "stickerIds": [ "974", "975", @@ -8615,7 +8615,7 @@ ] }, { - "caseId": "2170", + "containerId": "2170", "stickerIds": [ "13", "14", @@ -8635,7 +8635,7 @@ ] }, { - "caseId": "2171", + "containerId": "2171", "stickerIds": [ "31", "32", @@ -8656,7 +8656,7 @@ ] }, { - "caseId": "2172", + "containerId": "2172", "stickerIds": [ "4986", "4987", @@ -8697,7 +8697,7 @@ ] }, { - "caseId": "2173", + "containerId": "2173", "stickerIds": [ "5129", "5130", @@ -8717,7 +8717,7 @@ ] }, { - "caseId": "2174", + "containerId": "2174", "stickerIds": [ "5018", "5019", @@ -8758,7 +8758,7 @@ ] }, { - "caseId": "2175", + "containerId": "2175", "stickerIds": [ "5144", "5145", @@ -8868,7 +8868,7 @@ ] }, { - "caseId": "2176", + "containerId": "2176", "stickerIds": [ "4954", "4955", @@ -8909,7 +8909,7 @@ ] }, { - "caseId": "2177", + "containerId": "2177", "stickerIds": [ "1625", "1626", @@ -8928,7 +8928,7 @@ ] }, { - "caseId": "2179", + "containerId": "2179", "stickerIds": [ "989", "990", @@ -8944,7 +8944,7 @@ ] }, { - "caseId": "2180", + "containerId": "2180", "stickerIds": [ "5249", "5250", @@ -8971,7 +8971,7 @@ ] }, { - "caseId": "2181", + "containerId": "2181", "stickerIds": [ "9359", "9360", @@ -8994,7 +8994,7 @@ ] }, { - "caseId": "2182", + "containerId": "2182", "stickerIds": [ "9395", "9396", @@ -9017,7 +9017,7 @@ ] }, { - "caseId": "2183", + "containerId": "2183", "stickerIds": [ "4614", "4615", @@ -9039,7 +9039,7 @@ ] }, { - "caseId": "2184", + "containerId": "2184", "stickerIds": [ "9377", "9378", @@ -9062,7 +9062,7 @@ ] }, { - "caseId": "2185", + "containerId": "2185", "stickerIds": [ "9413", "9414", @@ -9085,7 +9085,7 @@ ] }, { - "caseId": "2186", + "containerId": "2186", "stickerIds": [ "48", "49", @@ -9107,7 +9107,7 @@ ] }, { - "caseId": "2187", + "containerId": "2187", "stickerIds": [ "50", "51", @@ -9129,7 +9129,7 @@ ] }, { - "caseId": "2188", + "containerId": "2188", "stickerIds": [ "122", "123", @@ -9149,7 +9149,7 @@ ] }, { - "caseId": "2189", + "containerId": "2189", "stickerIds": [ "136", "137", @@ -9173,7 +9173,7 @@ ] }, { - "caseId": "2190", + "containerId": "2190", "stickerIds": [ "199", "200", @@ -9191,7 +9191,7 @@ ] }, { - "caseId": "2191", + "containerId": "2191", "stickerIds": [ "287", "288", @@ -9213,7 +9213,7 @@ ] }, { - "caseId": "2192", + "containerId": "2192", "stickerIds": [ "300", "307", @@ -9235,7 +9235,7 @@ ] }, { - "caseId": "2193", + "containerId": "2193", "stickerIds": [ "621", "624", @@ -9249,7 +9249,7 @@ ] }, { - "caseId": "2194", + "containerId": "2194", "stickerIds": [ "633", "636", @@ -9263,7 +9263,7 @@ ] }, { - "caseId": "2195", + "containerId": "2195", "stickerIds": [ "912", "933", @@ -9277,7 +9277,7 @@ ] }, { - "caseId": "2196", + "containerId": "2196", "stickerIds": [ "915", "918", @@ -9291,7 +9291,7 @@ ] }, { - "caseId": "2197", + "containerId": "2197", "stickerIds": [ "1008", "1009", @@ -9314,7 +9314,7 @@ ] }, { - "caseId": "2198", + "containerId": "2198", "stickerIds": [ "1012", "1013", @@ -9337,7 +9337,7 @@ ] }, { - "caseId": "2199", + "containerId": "2199", "stickerIds": [ "1318", "1319", @@ -9360,7 +9360,7 @@ ] }, { - "caseId": "2200", + "containerId": "2200", "stickerIds": [ "1322", "1323", @@ -9383,7 +9383,7 @@ ] }, { - "caseId": "2201", + "containerId": "2201", "stickerIds": [ "1739", "1740", @@ -9406,7 +9406,7 @@ ] }, { - "caseId": "2202", + "containerId": "2202", "stickerIds": [ "1743", "1744", @@ -9429,7 +9429,7 @@ ] }, { - "caseId": "2203", + "containerId": "2203", "stickerIds": [ "2064", "2065", @@ -9452,7 +9452,7 @@ ] }, { - "caseId": "2204", + "containerId": "2204", "stickerIds": [ "2096", "2097", @@ -9475,7 +9475,7 @@ ] }, { - "caseId": "2205", + "containerId": "2205", "stickerIds": [ "2437", "2438", @@ -9498,7 +9498,7 @@ ] }, { - "caseId": "2206", + "containerId": "2206", "stickerIds": [ "2437", "2438", @@ -9519,7 +9519,7 @@ ] }, { - "caseId": "2207", + "containerId": "2207", "stickerIds": [ "2469", "2470", @@ -9542,7 +9542,7 @@ ] }, { - "caseId": "2208", + "containerId": "2208", "stickerIds": [ "2501", "2502", @@ -9565,7 +9565,7 @@ ] }, { - "caseId": "2209", + "containerId": "2209", "stickerIds": [ "2501", "2502", @@ -9588,7 +9588,7 @@ ] }, { - "caseId": "2210", + "containerId": "2210", "stickerIds": [ "2956", "2957", @@ -9611,7 +9611,7 @@ ] }, { - "caseId": "2211", + "containerId": "2211", "stickerIds": [ "2988", "2989", @@ -9634,7 +9634,7 @@ ] }, { - "caseId": "2212", + "containerId": "2212", "stickerIds": [ "3020", "3021", @@ -9657,7 +9657,7 @@ ] }, { - "caseId": "2213", + "containerId": "2213", "stickerIds": [ "3461", "3462", @@ -9680,7 +9680,7 @@ ] }, { - "caseId": "2214", + "containerId": "2214", "stickerIds": [ "3465", "3466", @@ -9707,7 +9707,7 @@ ] }, { - "caseId": "2215", + "containerId": "2215", "stickerIds": [ "3473", "3474", @@ -9726,7 +9726,7 @@ ] }, { - "caseId": "2216", + "containerId": "2216", "stickerIds": [ "4020", "4021", @@ -9749,7 +9749,7 @@ ] }, { - "caseId": "2217", + "containerId": "2217", "stickerIds": [ "4052", "4053", @@ -9766,7 +9766,7 @@ ] }, { - "caseId": "2218", + "containerId": "2218", "stickerIds": [ "4072", "4073", @@ -9795,7 +9795,7 @@ ] }, { - "caseId": "2219", + "containerId": "2219", "stickerIds": [ "4504", "4505", @@ -9809,7 +9809,7 @@ ] }, { - "caseId": "2220", + "containerId": "2220", "stickerIds": [ "4577", "4580", @@ -9839,7 +9839,7 @@ ] }, { - "caseId": "2221", + "containerId": "2221", "stickerIds": [ "4649", "4650", @@ -9876,7 +9876,7 @@ ] }, { - "caseId": "2222", + "containerId": "2222", "stickerIds": [ "4681", "4682", @@ -9897,7 +9897,7 @@ ] }, { - "caseId": "2223", + "containerId": "2223", "stickerIds": [ "4701", "4702", @@ -9934,7 +9934,7 @@ ] }, { - "caseId": "2224", + "containerId": "2224", "stickerIds": [ "4713", "4714", @@ -9971,7 +9971,7 @@ ] }, { - "caseId": "2225", + "containerId": "2225", "stickerIds": [ "4737", "4738", @@ -10008,7 +10008,7 @@ ] }, { - "caseId": "2226", + "containerId": "2226", "stickerIds": [ "4817", "4818", @@ -10085,7 +10085,7 @@ ] }, { - "caseId": "2227", + "containerId": "2227", "stickerIds": [ "4923", "4924", @@ -10104,7 +10104,7 @@ ] }, { - "caseId": "2228", + "containerId": "2228", "stickerIds": [ "7882", "7883", @@ -10135,7 +10135,7 @@ ] }, { - "caseId": "2229", + "containerId": "2229", "stickerIds": [ "9451", "9452", @@ -10169,7 +10169,7 @@ ] }, { - "caseId": "2230", + "containerId": "2230", "stickerIds": [ "9480", "9481", diff --git a/assets/music_kits/1000538183.webp b/assets/music_kits/1000538183.webp deleted file mode 100644 index 597a7c1f..00000000 Binary files a/assets/music_kits/1000538183.webp and /dev/null differ diff --git a/assets/music_kits/1000587013.webp b/assets/music_kits/1000587013.webp deleted file mode 100644 index 927a59ec..00000000 Binary files a/assets/music_kits/1000587013.webp and /dev/null differ diff --git a/assets/music_kits/1001606814.webp b/assets/music_kits/1001606814.webp deleted file mode 100644 index 23db6e23..00000000 Binary files a/assets/music_kits/1001606814.webp and /dev/null differ diff --git a/assets/music_kits/1002198311.webp b/assets/music_kits/1002198311.webp deleted file mode 100644 index 2c21f5c5..00000000 Binary files a/assets/music_kits/1002198311.webp and /dev/null differ diff --git a/assets/music_kits/1002527582.webp b/assets/music_kits/1002527582.webp deleted file mode 100644 index b2cd2c64..00000000 Binary files a/assets/music_kits/1002527582.webp and /dev/null differ diff --git a/assets/music_kits/1003252223.webp b/assets/music_kits/1003252223.webp deleted file mode 100644 index 80d1b790..00000000 Binary files a/assets/music_kits/1003252223.webp and /dev/null differ diff --git a/assets/music_kits/1004387147.webp b/assets/music_kits/1004387147.webp deleted file mode 100644 index bc449556..00000000 Binary files a/assets/music_kits/1004387147.webp and /dev/null differ diff --git a/assets/music_kits/1004485801.webp b/assets/music_kits/1004485801.webp deleted file mode 100644 index 1f7a354b..00000000 Binary files a/assets/music_kits/1004485801.webp and /dev/null differ diff --git a/assets/music_kits/1005312915.webp b/assets/music_kits/1005312915.webp deleted file mode 100644 index 25772228..00000000 Binary files a/assets/music_kits/1005312915.webp and /dev/null differ diff --git a/assets/music_kits/1005980932.webp b/assets/music_kits/1005980932.webp deleted file mode 100644 index eeb95218..00000000 Binary files a/assets/music_kits/1005980932.webp and /dev/null differ diff --git a/assets/music_kits/1006361788.webp b/assets/music_kits/1006361788.webp deleted file mode 100644 index b84009e8..00000000 Binary files a/assets/music_kits/1006361788.webp and /dev/null differ diff --git a/assets/music_kits/1007491651.webp b/assets/music_kits/1007491651.webp deleted file mode 100644 index 07c4db1a..00000000 Binary files a/assets/music_kits/1007491651.webp and /dev/null differ diff --git a/assets/music_kits/1008303658.webp b/assets/music_kits/1008303658.webp deleted file mode 100644 index 30dd4d80..00000000 Binary files a/assets/music_kits/1008303658.webp and /dev/null differ diff --git a/assets/music_kits/1009155363.webp b/assets/music_kits/1009155363.webp deleted file mode 100644 index 85004165..00000000 Binary files a/assets/music_kits/1009155363.webp and /dev/null differ diff --git a/assets/music_kits/1009748723.webp b/assets/music_kits/1009748723.webp deleted file mode 100644 index 91e64d60..00000000 Binary files a/assets/music_kits/1009748723.webp and /dev/null differ diff --git a/assets/music_kits/1010678901.webp b/assets/music_kits/1010678901.webp deleted file mode 100644 index 198506d2..00000000 Binary files a/assets/music_kits/1010678901.webp and /dev/null differ diff --git a/assets/music_kits/1010787345.webp b/assets/music_kits/1010787345.webp deleted file mode 100644 index 5d1af9d8..00000000 Binary files a/assets/music_kits/1010787345.webp and /dev/null differ diff --git a/assets/music_kits/1010972054.webp b/assets/music_kits/1010972054.webp deleted file mode 100644 index 74ac9908..00000000 Binary files a/assets/music_kits/1010972054.webp and /dev/null differ diff --git a/assets/music_kits/1011442758.webp b/assets/music_kits/1011442758.webp deleted file mode 100644 index af9b1b7f..00000000 Binary files a/assets/music_kits/1011442758.webp and /dev/null differ diff --git a/assets/music_kits/1011503984.webp b/assets/music_kits/1011503984.webp deleted file mode 100644 index 2118d760..00000000 Binary files a/assets/music_kits/1011503984.webp and /dev/null differ diff --git a/assets/music_kits/1012417359.webp b/assets/music_kits/1012417359.webp deleted file mode 100644 index dd4f19ca..00000000 Binary files a/assets/music_kits/1012417359.webp and /dev/null differ diff --git a/assets/music_kits/1012620674.webp b/assets/music_kits/1012620674.webp deleted file mode 100644 index bb7197a0..00000000 Binary files a/assets/music_kits/1012620674.webp and /dev/null differ diff --git a/assets/music_kits/1017012278.webp b/assets/music_kits/1017012278.webp deleted file mode 100644 index 8b16befa..00000000 Binary files a/assets/music_kits/1017012278.webp and /dev/null differ diff --git a/assets/music_kits/1017749013.webp b/assets/music_kits/1017749013.webp deleted file mode 100644 index 339ec928..00000000 Binary files a/assets/music_kits/1017749013.webp and /dev/null differ diff --git a/assets/music_kits/1018800726.webp b/assets/music_kits/1018800726.webp deleted file mode 100644 index 913e9663..00000000 Binary files a/assets/music_kits/1018800726.webp and /dev/null differ diff --git a/assets/music_kits/1020735512.webp b/assets/music_kits/1020735512.webp deleted file mode 100644 index 1b643a22..00000000 Binary files a/assets/music_kits/1020735512.webp and /dev/null differ diff --git a/assets/music_kits/1020755149.webp b/assets/music_kits/1020755149.webp deleted file mode 100644 index 619c2a95..00000000 Binary files a/assets/music_kits/1020755149.webp and /dev/null differ diff --git a/assets/music_kits/1021464349.webp b/assets/music_kits/1021464349.webp deleted file mode 100644 index f036f89b..00000000 Binary files a/assets/music_kits/1021464349.webp and /dev/null differ diff --git a/assets/music_kits/1023014483.webp b/assets/music_kits/1023014483.webp deleted file mode 100644 index 0dbc6986..00000000 Binary files a/assets/music_kits/1023014483.webp and /dev/null differ diff --git a/assets/music_kits/1023991939.webp b/assets/music_kits/1023991939.webp deleted file mode 100644 index a28a2a50..00000000 Binary files a/assets/music_kits/1023991939.webp and /dev/null differ diff --git a/assets/music_kits/1024340982.webp b/assets/music_kits/1024340982.webp deleted file mode 100644 index 999fe6a9..00000000 Binary files a/assets/music_kits/1024340982.webp and /dev/null differ diff --git a/assets/music_kits/1024737315.webp b/assets/music_kits/1024737315.webp deleted file mode 100644 index b0337f64..00000000 Binary files a/assets/music_kits/1024737315.webp and /dev/null differ diff --git a/assets/music_kits/1024770198.webp b/assets/music_kits/1024770198.webp deleted file mode 100644 index 696dd709..00000000 Binary files a/assets/music_kits/1024770198.webp and /dev/null differ diff --git a/assets/music_kits/1026524570.webp b/assets/music_kits/1026524570.webp deleted file mode 100644 index 2e5b8543..00000000 Binary files a/assets/music_kits/1026524570.webp and /dev/null differ diff --git a/assets/music_kits/1026639040.webp b/assets/music_kits/1026639040.webp deleted file mode 100644 index 3f0d6a11..00000000 Binary files a/assets/music_kits/1026639040.webp and /dev/null differ diff --git a/assets/music_kits/1027598311.webp b/assets/music_kits/1027598311.webp deleted file mode 100644 index 27a7fd7e..00000000 Binary files a/assets/music_kits/1027598311.webp and /dev/null differ diff --git a/assets/music_kits/1028581175.webp b/assets/music_kits/1028581175.webp deleted file mode 100644 index 517686f5..00000000 Binary files a/assets/music_kits/1028581175.webp and /dev/null differ diff --git a/assets/music_kits/1029067740.webp b/assets/music_kits/1029067740.webp deleted file mode 100644 index fe22d2c5..00000000 Binary files a/assets/music_kits/1029067740.webp and /dev/null differ diff --git a/assets/music_kits/1029448649.webp b/assets/music_kits/1029448649.webp deleted file mode 100644 index 78cc9b8f..00000000 Binary files a/assets/music_kits/1029448649.webp and /dev/null differ diff --git a/assets/music_kits/1029802512.webp b/assets/music_kits/1029802512.webp deleted file mode 100644 index c2bad369..00000000 Binary files a/assets/music_kits/1029802512.webp and /dev/null differ diff --git a/assets/music_kits/1032471984.webp b/assets/music_kits/1032471984.webp deleted file mode 100644 index 1f3411d6..00000000 Binary files a/assets/music_kits/1032471984.webp and /dev/null differ diff --git a/assets/music_kits/1032701438.webp b/assets/music_kits/1032701438.webp deleted file mode 100644 index bda8adbc..00000000 Binary files a/assets/music_kits/1032701438.webp and /dev/null differ diff --git a/assets/music_kits/1034373778.webp b/assets/music_kits/1034373778.webp deleted file mode 100644 index 019f0f8e..00000000 Binary files a/assets/music_kits/1034373778.webp and /dev/null differ diff --git a/assets/music_kits/1034543151.webp b/assets/music_kits/1034543151.webp deleted file mode 100644 index a744cb87..00000000 Binary files a/assets/music_kits/1034543151.webp and /dev/null differ diff --git a/assets/music_kits/1034676233.webp b/assets/music_kits/1034676233.webp deleted file mode 100644 index 89a90e18..00000000 Binary files a/assets/music_kits/1034676233.webp and /dev/null differ diff --git a/assets/music_kits/1034776466.webp b/assets/music_kits/1034776466.webp deleted file mode 100644 index ea6fe8bb..00000000 Binary files a/assets/music_kits/1034776466.webp and /dev/null differ diff --git a/assets/music_kits/1036612800.webp b/assets/music_kits/1036612800.webp deleted file mode 100644 index 3646b0b4..00000000 Binary files a/assets/music_kits/1036612800.webp and /dev/null differ diff --git a/assets/music_kits/1038233452.webp b/assets/music_kits/1038233452.webp deleted file mode 100644 index a17367a5..00000000 Binary files a/assets/music_kits/1038233452.webp and /dev/null differ diff --git a/assets/music_kits/1038272247.webp b/assets/music_kits/1038272247.webp deleted file mode 100644 index e4da1be2..00000000 Binary files a/assets/music_kits/1038272247.webp and /dev/null differ diff --git a/assets/music_kits/1040151842.webp b/assets/music_kits/1040151842.webp deleted file mode 100644 index 4f57e836..00000000 Binary files a/assets/music_kits/1040151842.webp and /dev/null differ diff --git a/assets/music_kits/1040470171.webp b/assets/music_kits/1040470171.webp deleted file mode 100644 index 90270ae1..00000000 Binary files a/assets/music_kits/1040470171.webp and /dev/null differ diff --git a/assets/music_kits/1043031531.webp b/assets/music_kits/1043031531.webp deleted file mode 100644 index 580ef0d1..00000000 Binary files a/assets/music_kits/1043031531.webp and /dev/null differ diff --git a/assets/music_kits/1044890021.webp b/assets/music_kits/1044890021.webp deleted file mode 100644 index c9c3a018..00000000 Binary files a/assets/music_kits/1044890021.webp and /dev/null differ diff --git a/assets/music_kits/1044942782.webp b/assets/music_kits/1044942782.webp deleted file mode 100644 index 9eef629d..00000000 Binary files a/assets/music_kits/1044942782.webp and /dev/null differ diff --git a/assets/music_kits/1046878779.webp b/assets/music_kits/1046878779.webp deleted file mode 100644 index 3c98d1bf..00000000 Binary files a/assets/music_kits/1046878779.webp and /dev/null differ diff --git a/assets/music_kits/1049894869.webp b/assets/music_kits/1049894869.webp deleted file mode 100644 index 64dc32e5..00000000 Binary files a/assets/music_kits/1049894869.webp and /dev/null differ diff --git a/assets/music_kits/1051886114.webp b/assets/music_kits/1051886114.webp deleted file mode 100644 index 7b33041b..00000000 Binary files a/assets/music_kits/1051886114.webp and /dev/null differ diff --git a/assets/music_kits/1052897721.webp b/assets/music_kits/1052897721.webp deleted file mode 100644 index 3551db8c..00000000 Binary files a/assets/music_kits/1052897721.webp and /dev/null differ diff --git a/assets/music_kits/1057256691.webp b/assets/music_kits/1057256691.webp deleted file mode 100644 index 6fcb7dd4..00000000 Binary files a/assets/music_kits/1057256691.webp and /dev/null differ diff --git a/assets/music_kits/1065487654.webp b/assets/music_kits/1065487654.webp deleted file mode 100644 index d27cecfe..00000000 Binary files a/assets/music_kits/1065487654.webp and /dev/null differ diff --git a/assets/music_kits/1066435192.webp b/assets/music_kits/1066435192.webp deleted file mode 100644 index b84e1f71..00000000 Binary files a/assets/music_kits/1066435192.webp and /dev/null differ diff --git a/assets/music_kits/1066759080.webp b/assets/music_kits/1066759080.webp deleted file mode 100644 index 564c9a38..00000000 Binary files a/assets/music_kits/1066759080.webp and /dev/null differ diff --git a/assets/music_kits/1069896488.webp b/assets/music_kits/1069896488.webp deleted file mode 100644 index e76968ff..00000000 Binary files a/assets/music_kits/1069896488.webp and /dev/null differ diff --git a/assets/music_kits/971231302.webp b/assets/music_kits/971231302.webp deleted file mode 100644 index 46b56bef..00000000 Binary files a/assets/music_kits/971231302.webp and /dev/null differ diff --git a/assets/music_kits/975826813.webp b/assets/music_kits/975826813.webp deleted file mode 100644 index 589deaf3..00000000 Binary files a/assets/music_kits/975826813.webp and /dev/null differ diff --git a/assets/music_kits/981815769.webp b/assets/music_kits/981815769.webp deleted file mode 100644 index bef54e8c..00000000 Binary files a/assets/music_kits/981815769.webp and /dev/null differ diff --git a/assets/music_kits/983861625.webp b/assets/music_kits/983861625.webp deleted file mode 100644 index d2c11710..00000000 Binary files a/assets/music_kits/983861625.webp and /dev/null differ diff --git a/assets/music_kits/984052782.webp b/assets/music_kits/984052782.webp deleted file mode 100644 index c40d6e4d..00000000 Binary files a/assets/music_kits/984052782.webp and /dev/null differ diff --git a/assets/music_kits/986081934.webp b/assets/music_kits/986081934.webp deleted file mode 100644 index d1f6d1df..00000000 Binary files a/assets/music_kits/986081934.webp and /dev/null differ diff --git a/assets/music_kits/986102338.webp b/assets/music_kits/986102338.webp deleted file mode 100644 index a8875cdd..00000000 Binary files a/assets/music_kits/986102338.webp and /dev/null differ diff --git a/assets/music_kits/986159988.webp b/assets/music_kits/986159988.webp deleted file mode 100644 index 8546929a..00000000 Binary files a/assets/music_kits/986159988.webp and /dev/null differ diff --git a/assets/music_kits/986500260.webp b/assets/music_kits/986500260.webp deleted file mode 100644 index 01466d4b..00000000 Binary files a/assets/music_kits/986500260.webp and /dev/null differ diff --git a/assets/music_kits/987263905.webp b/assets/music_kits/987263905.webp deleted file mode 100644 index 03b3e8d0..00000000 Binary files a/assets/music_kits/987263905.webp and /dev/null differ diff --git a/assets/music_kits/991673260.webp b/assets/music_kits/991673260.webp deleted file mode 100644 index adb8167e..00000000 Binary files a/assets/music_kits/991673260.webp and /dev/null differ diff --git a/assets/music_kits/992680274.webp b/assets/music_kits/992680274.webp deleted file mode 100644 index c07b2a67..00000000 Binary files a/assets/music_kits/992680274.webp and /dev/null differ diff --git a/assets/music_kits/994554107.webp b/assets/music_kits/994554107.webp deleted file mode 100644 index f2e0cff0..00000000 Binary files a/assets/music_kits/994554107.webp and /dev/null differ diff --git a/assets/music_kits/994893180.webp b/assets/music_kits/994893180.webp deleted file mode 100644 index 8e0ad26a..00000000 Binary files a/assets/music_kits/994893180.webp and /dev/null differ diff --git a/assets/music_kits/995696992.webp b/assets/music_kits/995696992.webp deleted file mode 100644 index 1e6219d8..00000000 Binary files a/assets/music_kits/995696992.webp and /dev/null differ diff --git a/assets/music_kits/997068518.webp b/assets/music_kits/997068518.webp deleted file mode 100644 index e7ac03b9..00000000 Binary files a/assets/music_kits/997068518.webp and /dev/null differ diff --git a/assets/music_kits/997276857.webp b/assets/music_kits/997276857.webp deleted file mode 100644 index f51f7508..00000000 Binary files a/assets/music_kits/997276857.webp and /dev/null differ diff --git a/assets/music_kits/997687959.webp b/assets/music_kits/997687959.webp deleted file mode 100644 index 92f1f72e..00000000 Binary files a/assets/music_kits/997687959.webp and /dev/null differ diff --git a/assets/music_kits/998333682.webp b/assets/music_kits/998333682.webp deleted file mode 100644 index da1cd8fb..00000000 Binary files a/assets/music_kits/998333682.webp and /dev/null differ diff --git a/lib/core/app/app_info.dart b/lib/core/app/app_info.dart index 8cbea537..324a8f1c 100644 --- a/lib/core/app/app_info.dart +++ b/lib/core/app/app_info.dart @@ -1,7 +1,4 @@ const appDisplayName = 'CS2 Simulator'; -const appVersion = String.fromEnvironment( - 'APP_VERSION', - defaultValue: 'dev', -); +const appVersion = String.fromEnvironment('APP_VERSION', defaultValue: 'dev'); const appLegalese = 'Unofficial fan-made Counter-Strike 2 simulator.'; const appRepositoryUrl = 'https://github.com/Rarmash/CS2-Simulator'; diff --git a/lib/core/settings/app_settings.dart b/lib/core/settings/app_settings.dart index 6e87e16f..04fe3503 100644 --- a/lib/core/settings/app_settings.dart +++ b/lib/core/settings/app_settings.dart @@ -1,15 +1,11 @@ class AppSettings { final bool xrayOpeningEnabled; - const AppSettings({ - this.xrayOpeningEnabled = false, - }); + const AppSettings({this.xrayOpeningEnabled = false}); - AppSettings copyWith({ - bool? xrayOpeningEnabled, - }) { + AppSettings copyWith({bool? xrayOpeningEnabled}) { return AppSettings( xrayOpeningEnabled: xrayOpeningEnabled ?? this.xrayOpeningEnabled, ); } -} \ No newline at end of file +} diff --git a/lib/core/settings/settings_controller.dart b/lib/core/settings/settings_controller.dart index 837ad22a..bf1a4df3 100644 --- a/lib/core/settings/settings_controller.dart +++ b/lib/core/settings/settings_controller.dart @@ -32,4 +32,4 @@ class SettingsController extends ChangeNotifier { Future toggleXrayOpening() async { await setXrayOpeningEnabled(!xrayOpeningEnabled); } -} \ No newline at end of file +} diff --git a/lib/core/utils/date_format_helper.dart b/lib/core/utils/date_format_helper.dart index 3ef4b397..d0f6125f 100644 --- a/lib/core/utils/date_format_helper.dart +++ b/lib/core/utils/date_format_helper.dart @@ -26,4 +26,4 @@ class DateFormatHelper { return '$day $month $year'; } -} \ No newline at end of file +} diff --git a/lib/data/models/case_content_dto.dart b/lib/data/models/case_content_dto.dart deleted file mode 100644 index a541fd57..00000000 --- a/lib/data/models/case_content_dto.dart +++ /dev/null @@ -1,16 +0,0 @@ -class CaseContentDto { - final String caseId; - final List skinIds; - - CaseContentDto({ - required this.caseId, - required this.skinIds, - }); - - factory CaseContentDto.fromJson(Map json) { - return CaseContentDto( - caseId: json['caseId'] as String, - skinIds: List.from(json['skinIds'] as List), - ); - } -} \ No newline at end of file diff --git a/lib/data/models/charm_content_dto.dart b/lib/data/models/charm_content_dto.dart new file mode 100644 index 00000000..0b5b472e --- /dev/null +++ b/lib/data/models/charm_content_dto.dart @@ -0,0 +1,13 @@ +class CharmContentDto { + final String containerId; + final List charmIds; + + const CharmContentDto({required this.containerId, required this.charmIds}); + + factory CharmContentDto.fromJson(Map json) { + return CharmContentDto( + containerId: json['containerId'] as String, + charmIds: List.from(json['charmIds'] as List), + ); + } +} diff --git a/lib/data/models/charm_dto.dart b/lib/data/models/charm_dto.dart new file mode 100644 index 00000000..c42d800e --- /dev/null +++ b/lib/data/models/charm_dto.dart @@ -0,0 +1,25 @@ +class CharmDto { + final String id; + final String name; + final String charmImage; + final String rarity; + final String? collection; + + const CharmDto({ + required this.id, + required this.name, + required this.charmImage, + required this.rarity, + required this.collection, + }); + + factory CharmDto.fromJson(Map json) { + return CharmDto( + id: json['id'] as String, + name: json['name'] as String, + charmImage: json['charmImage'] as String, + rarity: json['rarity'] as String, + collection: json['collection'] as String?, + ); + } +} diff --git a/lib/data/models/container_content_dto.dart b/lib/data/models/container_content_dto.dart new file mode 100644 index 00000000..1c3237f8 --- /dev/null +++ b/lib/data/models/container_content_dto.dart @@ -0,0 +1,13 @@ +class ContainerContentDto { + final String containerId; + final List skinIds; + + ContainerContentDto({required this.containerId, required this.skinIds}); + + factory ContainerContentDto.fromJson(Map json) { + return ContainerContentDto( + containerId: json['containerId'] as String, + skinIds: List.from(json['skinIds'] as List), + ); + } +} diff --git a/lib/data/models/case_dto.dart b/lib/data/models/container_dto.dart similarity index 55% rename from lib/data/models/case_dto.dart rename to lib/data/models/container_dto.dart index 8527efbe..637b9147 100644 --- a/lib/data/models/case_dto.dart +++ b/lib/data/models/container_dto.dart @@ -1,34 +1,40 @@ -class CaseDto { +class ContainerDto { final String id; final String name; - final String caseImage; + final String containerImage; final String? releaseDate; final String type; final String? sourceType; final String? sourceId; final String? sourceName; + final String? currency; + final int? cost; - CaseDto({ + ContainerDto({ required this.id, required this.name, - required this.caseImage, + required this.containerImage, required this.releaseDate, required this.type, required this.sourceType, required this.sourceId, required this.sourceName, + required this.currency, + required this.cost, }); - factory CaseDto.fromJson(Map json) { - return CaseDto( + factory ContainerDto.fromJson(Map json) { + return ContainerDto( id: json['id'] as String, name: json['name'] as String, - caseImage: json['caseImage'] as String, + containerImage: json['containerImage'] as String, releaseDate: json['releaseDate'] as String?, type: (json['type'] as String?) ?? 'CASE', sourceType: json['sourceType'] as String?, sourceId: json['sourceId'] as String?, sourceName: json['sourceName'] as String?, + currency: json['currency'] as String?, + cost: (json['cost'] as num?)?.toInt(), ); } @@ -42,6 +48,10 @@ class CaseDto { bool get isGraffitiBox => type == 'GRAFFITI_BOX'; bool get isPatchPack => type == 'PATCH_PACK'; bool get isPatchCollection => type == 'PATCH_COLLECTION'; + bool get isCharmCollection => type == 'CHARM_COLLECTION'; + bool get isAgentCollection => type == 'AGENT_COLLECTION'; + bool get isRewardCollection => type == 'REWARD_COLLECTION'; + bool get isOperationCollection => type == 'OPERATION_COLLECTION'; bool get isXrayPackage => type == 'XRAY_PACKAGE'; bool get isTerminal => type == 'TERMINAL'; @@ -67,6 +77,14 @@ class CaseDto { return 'Patch Pack'; case 'PATCH_COLLECTION': return 'Patch Collection'; + case 'CHARM_COLLECTION': + return 'Charm Collection'; + case 'AGENT_COLLECTION': + return 'Agent Collection'; + case 'REWARD_COLLECTION': + return 'Reward Collection'; + case 'OPERATION_COLLECTION': + return 'Operation Collection'; case 'XRAY_PACKAGE': return 'X-Ray Package'; case 'TERMINAL': @@ -90,4 +108,51 @@ class CaseDto { return null; } } + + bool get isArmoryRewardCollection => + isRewardCollection && sourceType == 'ARMORY_REWARD'; + + bool get isOperationRewardCollection => + isRewardCollection && sourceType == 'OPERATION_REWARD'; + + String get sourceLabel { + final source = (sourceName ?? '').trim(); + if (source.isNotEmpty) { + return source; + } + + final id = (sourceId ?? '').trim(); + if (id.isEmpty) { + return 'Unknown Source'; + } + + return id + .split('_') + .map((part) { + if (part.isEmpty) return part; + final lower = part.toLowerCase(); + return lower[0].toUpperCase() + lower.substring(1); + }) + .join(' '); + } + + String get currencyLabel { + switch (currency) { + case 'STARS': + return 'Stars'; + case 'CREDITS': + return 'Credits'; + default: + return currency ?? ''; + } + } + + String get actionLabel { + if (cost == null || currency == null) { + return 'Open Collection'; + } + return 'Spend $cost $currencyLabel'; + } + + String get operationLabel => sourceLabel; } diff --git a/lib/data/models/graffiti_content_dto.dart b/lib/data/models/graffiti_content_dto.dart index e512f3ca..c2fe7435 100644 --- a/lib/data/models/graffiti_content_dto.dart +++ b/lib/data/models/graffiti_content_dto.dart @@ -1,15 +1,15 @@ class GraffitiContentDto { - final String caseId; + final String containerId; final List graffitiIds; const GraffitiContentDto({ - required this.caseId, + required this.containerId, required this.graffitiIds, }); factory GraffitiContentDto.fromJson(Map json) { return GraffitiContentDto( - caseId: json['caseId'] as String, + containerId: json['containerId'] as String, graffitiIds: List.from(json['graffitiIds'] as List), ); } diff --git a/lib/data/models/music_kit_content_dto.dart b/lib/data/models/music_kit_content_dto.dart index cf596191..8ff93d4d 100644 --- a/lib/data/models/music_kit_content_dto.dart +++ b/lib/data/models/music_kit_content_dto.dart @@ -1,13 +1,58 @@ +class MusicKitContentEntryDto { + final String musicKitId; + final bool hasRegular; + final bool hasStatTrak; + + const MusicKitContentEntryDto({ + required this.musicKitId, + required this.hasRegular, + required this.hasStatTrak, + }); + + factory MusicKitContentEntryDto.fromJson(Map json) { + return MusicKitContentEntryDto( + musicKitId: json['musicKitId'] as String, + hasRegular: json['hasRegular'] as bool? ?? false, + hasStatTrak: json['hasStatTrak'] as bool? ?? false, + ); + } +} + class MusicKitContentDto { - final String caseId; - final List musicKitIds; + final String containerId; + final List items; - const MusicKitContentDto({required this.caseId, required this.musicKitIds}); + const MusicKitContentDto({required this.containerId, required this.items}); factory MusicKitContentDto.fromJson(Map json) { + final rawItems = json['items']; + + if (rawItems is List) { + return MusicKitContentDto( + containerId: json['containerId'] as String, + items: rawItems + .whereType() + .map( + (item) => MusicKitContentEntryDto.fromJson( + item.map((k, v) => MapEntry(k.toString(), v)), + ), + ) + .toList(), + ); + } + + final legacyIds = List.from(json['musicKitIds'] as List); return MusicKitContentDto( - caseId: json['caseId'] as String, - musicKitIds: List.from(json['musicKitIds'] as List), + containerId: json['containerId'] as String, + items: legacyIds + .map( + (id) => MusicKitContentEntryDto( + musicKitId: id, + hasRegular: true, + hasStatTrak: false, + ), + ) + .toList(), ); } } diff --git a/lib/data/models/music_kit_dto.dart b/lib/data/models/music_kit_dto.dart index c06d97f5..c5c91230 100644 --- a/lib/data/models/music_kit_dto.dart +++ b/lib/data/models/music_kit_dto.dart @@ -4,7 +4,8 @@ class MusicKitDto { final String musicKitImage; final String rarity; final String? collection; - final bool isStatTrak; + final bool hasRegular; + final bool hasStatTrak; const MusicKitDto({ required this.id, @@ -12,17 +13,40 @@ class MusicKitDto { required this.musicKitImage, required this.rarity, required this.collection, - required this.isStatTrak, + required this.hasRegular, + required this.hasStatTrak, }); factory MusicKitDto.fromJson(Map json) { + final legacyIsStatTrak = json['isStatTrak'] as bool? ?? false; return MusicKitDto( id: json['id'] as String, name: json['name'] as String, musicKitImage: json['musicKitImage'] as String, rarity: json['rarity'] as String, collection: json['collection'] as String?, - isStatTrak: json['isStatTrak'] as bool? ?? false, + hasRegular: json['hasRegular'] as bool? ?? !legacyIsStatTrak, + hasStatTrak: json['hasStatTrak'] as bool? ?? legacyIsStatTrak, + ); + } + + MusicKitDto copyWith({ + String? id, + String? name, + String? musicKitImage, + String? rarity, + String? collection, + bool? hasRegular, + bool? hasStatTrak, + }) { + return MusicKitDto( + id: id ?? this.id, + name: name ?? this.name, + musicKitImage: musicKitImage ?? this.musicKitImage, + rarity: rarity ?? this.rarity, + collection: collection ?? this.collection, + hasRegular: hasRegular ?? this.hasRegular, + hasStatTrak: hasStatTrak ?? this.hasStatTrak, ); } @@ -39,6 +63,9 @@ class MusicKitDto { } String get displayName { - return isStatTrak ? 'StatTrakā„¢ $name' : name; + if (hasStatTrak && !hasRegular) { + return 'StatTrakā„¢ $name'; + } + return name; } } diff --git a/lib/data/models/music_kit_group_dto.dart b/lib/data/models/music_kit_group_dto.dart new file mode 100644 index 00000000..cbac87ef --- /dev/null +++ b/lib/data/models/music_kit_group_dto.dart @@ -0,0 +1,57 @@ +import 'music_kit_dto.dart'; + +class MusicKitGroupDto { + final String name; + final String trackName; + final String? artist; + final String? collection; + final String rarity; + final bool hasRegular; + final bool hasStatTrak; + final String imagePath; + final List variants; + + const MusicKitGroupDto({ + required this.name, + required this.trackName, + required this.artist, + required this.collection, + required this.rarity, + required this.hasRegular, + required this.hasStatTrak, + required this.imagePath, + required this.variants, + }); + + factory MusicKitGroupDto.fromVariants(List variants) { + final sorted = [...variants]..sort(_compareMusicKitVariants); + final primary = sorted.first; + + return MusicKitGroupDto( + name: primary.name, + trackName: primary.trackName, + artist: primary.artist, + collection: primary.collection, + rarity: primary.rarity, + hasRegular: sorted.any((item) => item.hasRegular), + hasStatTrak: sorted.any((item) => item.hasStatTrak), + imagePath: primary.musicKitImage, + variants: sorted, + ); + } + + MusicKitDto get primary => variants.first; + + static int _compareMusicKitVariants(MusicKitDto a, MusicKitDto b) { + final aOrder = a.hasRegular && !a.hasStatTrak + ? 0 + : (a.hasRegular && a.hasStatTrak ? 1 : 2); + final bOrder = b.hasRegular && !b.hasStatTrak + ? 0 + : (b.hasRegular && b.hasStatTrak ? 1 : 2); + if (aOrder == bOrder) { + return a.id.compareTo(b.id); + } + return aOrder.compareTo(bOrder); + } +} diff --git a/lib/data/models/operation_collection_content_dto.dart b/lib/data/models/operation_collection_content_dto.dart index b89f4de0..acd8e6f7 100644 --- a/lib/data/models/operation_collection_content_dto.dart +++ b/lib/data/models/operation_collection_content_dto.dart @@ -13,4 +13,4 @@ class OperationCollectionContentDto { skinIds: List.from(json['skinIds'] as List), ); } -} \ No newline at end of file +} diff --git a/lib/data/models/operation_collection_dto.dart b/lib/data/models/operation_collection_dto.dart index 623cb78c..0d282fda 100644 --- a/lib/data/models/operation_collection_dto.dart +++ b/lib/data/models/operation_collection_dto.dart @@ -27,4 +27,4 @@ class OperationCollectionDto { } String get operationLabel => operationName; -} \ No newline at end of file +} diff --git a/lib/data/models/patch_content_dto.dart b/lib/data/models/patch_content_dto.dart index e215cbec..613e090c 100644 --- a/lib/data/models/patch_content_dto.dart +++ b/lib/data/models/patch_content_dto.dart @@ -1,15 +1,12 @@ class PatchContentDto { - final String caseId; + final String containerId; final List patchIds; - const PatchContentDto({ - required this.caseId, - required this.patchIds, - }); + const PatchContentDto({required this.containerId, required this.patchIds}); factory PatchContentDto.fromJson(Map json) { return PatchContentDto( - caseId: json['caseId'] as String, + containerId: json['containerId'] as String, patchIds: List.from(json['patchIds'] as List), ); } diff --git a/lib/data/models/pin_content_dto.dart b/lib/data/models/pin_content_dto.dart index 2c0d0d64..c2247c4b 100644 --- a/lib/data/models/pin_content_dto.dart +++ b/lib/data/models/pin_content_dto.dart @@ -1,12 +1,12 @@ class PinContentDto { - final String caseId; + final String containerId; final List pinIds; - const PinContentDto({required this.caseId, required this.pinIds}); + const PinContentDto({required this.containerId, required this.pinIds}); factory PinContentDto.fromJson(Map json) { return PinContentDto( - caseId: json['caseId'] as String, + containerId: json['containerId'] as String, pinIds: List.from(json['pinIds'] as List), ); } diff --git a/lib/data/models/reward_collection_content_dto.dart b/lib/data/models/reward_collection_content_dto.dart index 7ed1d7e0..9bb57ca7 100644 --- a/lib/data/models/reward_collection_content_dto.dart +++ b/lib/data/models/reward_collection_content_dto.dart @@ -13,4 +13,4 @@ class RewardCollectionContentDto { skinIds: List.from(json['skinIds'] as List), ); } -} \ No newline at end of file +} diff --git a/lib/data/models/reward_collection_dto.dart b/lib/data/models/reward_collection_dto.dart index e791bcb7..9ef418ab 100644 --- a/lib/data/models/reward_collection_dto.dart +++ b/lib/data/models/reward_collection_dto.dart @@ -47,10 +47,10 @@ class RewardCollectionDto { return sourceId .split('_') .map((part) { - if (part.isEmpty) return part; - final lower = part.toLowerCase(); - return lower[0].toUpperCase() + lower.substring(1); - }) + if (part.isEmpty) return part; + final lower = part.toLowerCase(); + return lower[0].toUpperCase() + lower.substring(1); + }) .join(' '); } } @@ -67,4 +67,4 @@ class RewardCollectionDto { } String get actionLabel => 'Spend $cost $currencyLabel'; -} \ No newline at end of file +} diff --git a/lib/data/models/skin_dto.dart b/lib/data/models/skin_dto.dart index 7a7969a8..a9690ef8 100644 --- a/lib/data/models/skin_dto.dart +++ b/lib/data/models/skin_dto.dart @@ -63,9 +63,9 @@ class SkinDto { collectionSourceId: json['collectionSourceId'] as String?, isRewardCollection: (json['isRewardCollection'] as bool?) ?? false, operationCollectionIds: - (json['operationCollectionIds'] as List?) - ?.map((e) => e as String) - .toList() ?? + (json['operationCollectionIds'] as List?) + ?.map((e) => e as String) + .toList() ?? const [], isOperationCollection: (json['isOperationCollection'] as bool?) ?? false, ); @@ -183,4 +183,4 @@ class SkinDto { }; return map[id] ?? id; } -} \ No newline at end of file +} diff --git a/lib/data/models/sticker_content_dto.dart b/lib/data/models/sticker_content_dto.dart index 95adb548..dd96c273 100644 --- a/lib/data/models/sticker_content_dto.dart +++ b/lib/data/models/sticker_content_dto.dart @@ -1,12 +1,15 @@ class StickerContentDto { - final String caseId; + final String containerId; final List stickerIds; - const StickerContentDto({required this.caseId, required this.stickerIds}); + const StickerContentDto({ + required this.containerId, + required this.stickerIds, + }); factory StickerContentDto.fromJson(Map json) { return StickerContentDto( - caseId: json['caseId'] as String, + containerId: json['containerId'] as String, stickerIds: (json['stickerIds'] as List) .map((e) => e as String) .toList(), diff --git a/lib/data/repositories/local_data_repository.dart b/lib/data/repositories/local_data_repository.dart index 47f92294..4e1c962c 100644 --- a/lib/data/repositories/local_data_repository.dart +++ b/lib/data/repositories/local_data_repository.dart @@ -3,22 +3,22 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import '../models/agent_collection_content_dto.dart'; -import '../models/agent_collection_dto.dart'; import '../models/agent_dto.dart'; -import '../models/case_content_dto.dart'; -import '../models/case_dto.dart'; +import '../models/container_content_dto.dart'; +import '../models/container_dto.dart'; +import '../models/charm_content_dto.dart'; +import '../models/charm_dto.dart'; import '../models/graffiti_content_dto.dart'; import '../models/graffiti_dto.dart'; import '../models/music_kit_content_dto.dart'; import '../models/music_kit_dto.dart'; +import '../models/music_kit_group_dto.dart'; import '../models/operation_collection_content_dto.dart'; -import '../models/operation_collection_dto.dart'; import '../models/patch_content_dto.dart'; import '../models/patch_dto.dart'; import '../models/pin_content_dto.dart'; import '../models/pin_dto.dart'; import '../models/reward_collection_content_dto.dart'; -import '../models/reward_collection_dto.dart'; import '../models/skin_dto.dart'; import '../models/sticker_content_dto.dart'; import '../models/sticker_dto.dart'; @@ -27,4 +27,5 @@ part 'local_data_repository_loaders.dart'; part 'local_data_repository_queries.dart'; part 'local_data_repository_sorting.dart'; -class LocalDataRepository with _LocalDataRepositoryLoaders, _LocalDataRepositoryQueries {} +class LocalDataRepository + with _LocalDataRepositoryLoaders, _LocalDataRepositoryQueries {} diff --git a/lib/data/repositories/local_data_repository_loaders.dart b/lib/data/repositories/local_data_repository_loaders.dart index 91f3071b..9f7667c1 100644 --- a/lib/data/repositories/local_data_repository_loaders.dart +++ b/lib/data/repositories/local_data_repository_loaders.dart @@ -1,9 +1,12 @@ part of 'local_data_repository.dart'; mixin _LocalDataRepositoryLoaders { - Future> loadCases() async { - final cases = await _loadDtoList('assets/data/cases.json', CaseDto.fromJson); - cases.sort(_compareCaseByReleaseDateAsc); + Future> loadContainers() async { + final cases = await _loadDtoList( + 'assets/data/containers.json', + ContainerDto.fromJson, + ); + cases.sort(_compareContainerByReleaseDateAsc); return cases; } @@ -35,8 +38,15 @@ mixin _LocalDataRepositoryLoaders { return _loadDtoList('assets/data/patches.json', PatchDto.fromJson); } - Future> loadCaseContents() async { - return _loadDtoList('assets/data/case_contents.json', CaseContentDto.fromJson); + Future> loadCharms() async { + return _loadDtoList('assets/data/charms.json', CharmDto.fromJson); + } + + Future> loadContainerContents() async { + return _loadDtoList( + 'assets/data/container_contents.json', + ContainerContentDto.fromJson, + ); } Future> loadStickerContents() async { @@ -47,7 +57,10 @@ mixin _LocalDataRepositoryLoaders { } Future> loadPinContents() async { - return _loadDtoList('assets/data/pin_contents.json', PinContentDto.fromJson); + return _loadDtoList( + 'assets/data/pin_contents.json', + PinContentDto.fromJson, + ); } Future> loadMusicKitContents() async { @@ -57,13 +70,11 @@ mixin _LocalDataRepositoryLoaders { ); } - Future> loadAgentCollections() async { - final items = await _loadDtoList( - 'assets/data/agent_collections.json', - AgentCollectionDto.fromJson, - ); - items.sort(_compareNamedReleaseDateAsc); - return items; + Future> loadAgentCollections() async { + final items = await loadContainers(); + final result = items.where((item) => item.isAgentCollection).toList(); + result.sort(_compareCollectibleCollectionAsc); + return result; } Future> loadAgentCollectionContents() async { @@ -87,29 +98,33 @@ mixin _LocalDataRepositoryLoaders { ); } - Future> loadRewardCollections() async { - final items = await _loadDtoList( - 'assets/data/reward_collections.json', - RewardCollectionDto.fromJson, + Future> loadCharmContents() async { + return _loadDtoList( + 'assets/data/charm_contents.json', + CharmContentDto.fromJson, ); - items.sort(_compareNamedReleaseDateAsc); - return items; } - Future> loadRewardCollectionContents() async { + Future> loadRewardCollections() async { + final items = await loadContainers(); + final result = items.where((item) => item.isRewardCollection).toList(); + result.sort(_compareCollectibleCollectionAsc); + return result; + } + + Future> + loadRewardCollectionContents() async { return _loadDtoList( 'assets/data/reward_collection_contents.json', RewardCollectionContentDto.fromJson, ); } - Future> loadOperationCollections() async { - final items = await _loadDtoList( - 'assets/data/operation_collections.json', - OperationCollectionDto.fromJson, - ); - items.sort(_compareOperationCollectionAsc); - return items; + Future> loadOperationCollections() async { + final items = await loadContainers(); + final result = items.where((item) => item.isOperationCollection).toList(); + result.sort(_compareCollectibleCollectionAsc); + return result; } Future> @@ -120,11 +135,11 @@ mixin _LocalDataRepositoryLoaders { ); } - Future>> loadCaseToSkinIds() async { - final caseContents = await loadCaseContents(); + Future>> loadContainerToSkinIds() async { + final caseContents = await loadContainerContents(); return { for (final entry in caseContents) - entry.caseId: List.from(entry.skinIds), + entry.containerId: List.from(entry.skinIds), }; } diff --git a/lib/data/repositories/local_data_repository_queries.dart b/lib/data/repositories/local_data_repository_queries.dart index b49b53fb..07c79113 100644 --- a/lib/data/repositories/local_data_repository_queries.dart +++ b/lib/data/repositories/local_data_repository_queries.dart @@ -1,10 +1,10 @@ part of 'local_data_repository.dart'; mixin _LocalDataRepositoryQueries on _LocalDataRepositoryLoaders { - Future> loadSkinsForCase(String caseId) async { + Future> loadSkinsForContainer(String containerId) async { final skins = await loadSkins(); - final contents = await loadCaseContents(); - final content = contents.firstWhere((c) => c.caseId == caseId); + final contents = await loadContainerContents(); + final content = contents.firstWhere((c) => c.containerId == containerId); final ids = content.skinIds.toSet(); final result = skins.where((s) => ids.contains(s.id)).toList(); @@ -16,27 +16,27 @@ mixin _LocalDataRepositoryQueries on _LocalDataRepositoryLoaders { return result; } - Future> loadStickersForCase(String caseId) async { + Future> loadStickersForContainer(String containerId) async { final stickers = await loadStickers(); final contents = await loadStickerContents(); - final content = contents.firstWhere((c) => c.caseId == caseId); + final content = contents.firstWhere((c) => c.containerId == containerId); final ids = content.stickerIds.toSet(); final result = stickers.where((s) => ids.contains(s.id)).toList(); result.sort((a, b) { - final rarityCompare = _stickerRarityOrder(a).compareTo( - _stickerRarityOrder(b), - ); + final rarityCompare = _stickerRarityOrder( + a, + ).compareTo(_stickerRarityOrder(b)); if (rarityCompare != 0) return rarityCompare; return int.parse(a.id).compareTo(int.parse(b.id)); }); return result; } - Future> loadPinsForCase(String caseId) async { + Future> loadPinsForContainer(String containerId) async { final pins = await loadPins(); final contents = await loadPinContents(); - final content = contents.firstWhere((c) => c.caseId == caseId); + final content = contents.firstWhere((c) => c.containerId == containerId); final ids = content.pinIds.toSet(); final result = pins.where((p) => ids.contains(p.id)).toList(); @@ -48,60 +48,138 @@ mixin _LocalDataRepositoryQueries on _LocalDataRepositoryLoaders { return result; } - Future> loadMusicKitsForCase(String caseId) async { + Future> loadMusicKitsForContainer( + String containerId, + ) async { final musicKits = await loadMusicKits(); final contents = await loadMusicKitContents(); - final content = contents.firstWhere((c) => c.caseId == caseId); - final ids = content.musicKitIds.toSet(); + final content = contents.firstWhere((c) => c.containerId == containerId); + final entriesById = { + for (final entry in content.items) entry.musicKitId: entry, + }; + + final result = musicKits.where((m) => entriesById.containsKey(m.id)).map(( + musicKit, + ) { + final entry = entriesById[musicKit.id]!; + return musicKit.copyWith( + hasRegular: entry.hasRegular, + hasStatTrak: entry.hasStatTrak, + ); + }).toList(); + result.sort((a, b) { + final rarityCompare = _musicKitRarityOrder( + a, + ).compareTo(_musicKitRarityOrder(b)); + if (rarityCompare != 0) return rarityCompare; + final variantCompare = _musicKitVariantOrder( + a, + ).compareTo(_musicKitVariantOrder(b)); + if (variantCompare != 0) return variantCompare; + return int.parse(a.id).compareTo(int.parse(b.id)); + }); + return result; + } + + Future> loadGroupedMusicKits() async { + final musicKits = await loadMusicKits(); + final grouped = >{}; + + for (final musicKit in musicKits) { + final key = + '${musicKit.name.trim().toLowerCase()}|${(musicKit.collection ?? '').trim().toLowerCase()}'; + grouped.putIfAbsent(key, () => []).add(musicKit); + } + + final result = grouped.values.map(MusicKitGroupDto.fromVariants).toList(); - final result = musicKits.where((m) => ids.contains(m.id)).toList(); result.sort((a, b) { - final rarityCompare = _musicKitRarityOrder(a).compareTo( - _musicKitRarityOrder(b), - ); + final rarityCompare = _musicKitRarityOrder( + a.primary, + ).compareTo(_musicKitRarityOrder(b.primary)); if (rarityCompare != 0) return rarityCompare; - final statTrakCompare = a.isStatTrak == b.isStatTrak + final statTrakCompare = a.hasStatTrak == b.hasStatTrak ? 0 - : (a.isStatTrak ? 1 : -1); + : (a.hasStatTrak ? 1 : -1); if (statTrakCompare != 0) return statTrakCompare; - return int.parse(a.id).compareTo(int.parse(b.id)); + return a.trackName.compareTo(b.trackName); }); + return result; } - Future> loadGraffitiForCase(String caseId) async { + Future loadMusicKitGroup( + String musicKitName, + String? collection, + ) async { + final groups = await loadGroupedMusicKits(); + final normalizedCollection = (collection ?? '').trim().toLowerCase(); + + for (final group in groups) { + if (group.name == musicKitName && + (group.collection ?? '').trim().toLowerCase() == + normalizedCollection) { + return group; + } + } + + return null; + } + + Future> loadGraffitiForContainer(String containerId) async { final graffiti = await loadGraffiti(); final contents = await loadGraffitiContents(); - final content = contents.firstWhere((c) => c.caseId == caseId); + final content = contents.firstWhere((c) => c.containerId == containerId); final ids = content.graffitiIds.toSet(); final result = graffiti.where((g) => ids.contains(g.id)).toList(); result.sort((a, b) { - final rarityCompare = _graffitiRarityOrder(a).compareTo( - _graffitiRarityOrder(b), - ); + final rarityCompare = _graffitiRarityOrder( + a, + ).compareTo(_graffitiRarityOrder(b)); if (rarityCompare != 0) return rarityCompare; return int.parse(a.id).compareTo(int.parse(b.id)); }); return result; } - Future> loadPatchesForCase(String caseId) async { + Future> loadPatchesForContainer(String containerId) async { final patches = await loadPatches(); final contents = await loadPatchContents(); - final content = contents.firstWhere((c) => c.caseId == caseId); + final content = contents.firstWhere((c) => c.containerId == containerId); final ids = content.patchIds.toSet(); final result = patches.where((p) => ids.contains(p.id)).toList(); result.sort((a, b) { - final rarityCompare = _patchRarityOrder(a).compareTo(_patchRarityOrder(b)); + final rarityCompare = _patchRarityOrder( + a, + ).compareTo(_patchRarityOrder(b)); + if (rarityCompare != 0) return rarityCompare; + return int.parse(a.id).compareTo(int.parse(b.id)); + }); + return result; + } + + Future> loadCharmsForContainer(String containerId) async { + final charms = await loadCharms(); + final contents = await loadCharmContents(); + final content = contents.firstWhere((c) => c.containerId == containerId); + final ids = content.charmIds.toSet(); + + final result = charms.where((c) => ids.contains(c.id)).toList(); + result.sort((a, b) { + final rarityCompare = _charmRarityOrder( + a, + ).compareTo(_charmRarityOrder(b)); if (rarityCompare != 0) return rarityCompare; return int.parse(a.id).compareTo(int.parse(b.id)); }); return result; } - Future> loadAgentsForCollection(String agentCollectionId) async { + Future> loadAgentsForCollection( + String agentCollectionId, + ) async { final agents = await loadAgents(); final contents = await loadAgentCollectionContents(); final content = contents.firstWhere( @@ -111,7 +189,9 @@ mixin _LocalDataRepositoryQueries on _LocalDataRepositoryLoaders { final result = agents.where((a) => ids.contains(a.id)).toList(); result.sort((a, b) { - final rarityCompare = _agentRarityOrder(a).compareTo(_agentRarityOrder(b)); + final rarityCompare = _agentRarityOrder( + a, + ).compareTo(_agentRarityOrder(b)); if (rarityCompare != 0) return rarityCompare; return int.parse(a.id).compareTo(int.parse(b.id)); }); @@ -160,91 +240,148 @@ mixin _LocalDataRepositoryQueries on _LocalDataRepositoryLoaders { return result; } - Future> loadCasesForSkin(String skinId) async { - final cases = await loadCases(); - final contents = await loadCaseContents(); + Future> loadContainersForSkin(String skinId) async { + final containers = await loadContainers(); + final contents = await loadContainerContents(); - final caseIds = contents + final containerIds = contents .where((entry) => entry.skinIds.contains(skinId)) - .map((entry) => entry.caseId) + .map((entry) => entry.containerId) .toSet(); - final result = cases.where((c) => caseIds.contains(c.id)).toList(); - result.sort(_compareCaseByReleaseDateAsc); + final result = containers + .where((c) => containerIds.contains(c.id)) + .toList(); + result.sort(_compareContainerByReleaseDateAsc); return result; } - Future> loadCasesForSticker(String stickerId) async { - final cases = await loadCases(); + Future> loadContainersForSticker(String stickerId) async { + final containers = await loadContainers(); final contents = await loadStickerContents(); - final caseIds = contents + final containerIds = contents .where((entry) => entry.stickerIds.contains(stickerId)) - .map((entry) => entry.caseId) + .map((entry) => entry.containerId) .toSet(); - final result = cases.where((c) => caseIds.contains(c.id)).toList(); - result.sort(_compareCaseByReleaseDateAsc); + final result = containers + .where((c) => containerIds.contains(c.id)) + .toList(); + result.sort(_compareContainerByReleaseDateAsc); return result; } - Future> loadCasesForPin(String pinId) async { - final cases = await loadCases(); + Future> loadContainersForPin(String pinId) async { + final containers = await loadContainers(); final contents = await loadPinContents(); - final caseIds = contents + final containerIds = contents .where((entry) => entry.pinIds.contains(pinId)) - .map((entry) => entry.caseId) + .map((entry) => entry.containerId) .toSet(); - final result = cases.where((c) => caseIds.contains(c.id)).toList(); - result.sort(_compareCaseByReleaseDateAsc); + final result = containers + .where((c) => containerIds.contains(c.id)) + .toList(); + result.sort(_compareContainerByReleaseDateAsc); return result; } - Future> loadCasesForMusicKit(String musicKitId) async { - final cases = await loadCases(); + Future> loadContainersForMusicKit( + String musicKitId, + ) async { + final containers = await loadContainers(); final contents = await loadMusicKitContents(); - final caseIds = contents - .where((entry) => entry.musicKitIds.contains(musicKitId)) - .map((entry) => entry.caseId) + final containerIds = contents + .where( + (entry) => entry.items.any((item) => item.musicKitId == musicKitId), + ) + .map((entry) => entry.containerId) .toSet(); - final result = cases.where((c) => caseIds.contains(c.id)).toList(); - result.sort(_compareCaseByReleaseDateAsc); + final result = containers + .where((c) => containerIds.contains(c.id)) + .toList(); + result.sort(_compareContainerByReleaseDateAsc); return result; } - Future> loadCasesForGraffiti(String graffitiId) async { - final cases = await loadCases(); + Future> loadContainersForMusicKitGroup( + String musicKitName, + String? collection, + ) async { + final group = await loadMusicKitGroup(musicKitName, collection); + if (group == null) return const []; + + final seenContainerIds = {}; + final containers = []; + + for (final variant in group.variants) { + final sources = await loadContainersForMusicKit(variant.id); + for (final container in sources) { + if (seenContainerIds.add(container.id)) { + containers.add(container); + } + } + } + + containers.sort(_compareContainerByReleaseDateAsc); + return containers; + } + + Future> loadContainersForGraffiti( + String graffitiId, + ) async { + final containers = await loadContainers(); final contents = await loadGraffitiContents(); - final caseIds = contents + final containerIds = contents .where((entry) => entry.graffitiIds.contains(graffitiId)) - .map((entry) => entry.caseId) + .map((entry) => entry.containerId) .toSet(); - final result = cases.where((c) => caseIds.contains(c.id)).toList(); - result.sort(_compareCaseByReleaseDateAsc); + final result = containers + .where((c) => containerIds.contains(c.id)) + .toList(); + result.sort(_compareContainerByReleaseDateAsc); return result; } - Future> loadCasesForPatch(String patchId) async { - final cases = await loadCases(); + Future> loadContainersForPatch(String patchId) async { + final containers = await loadContainers(); final contents = await loadPatchContents(); - final caseIds = contents + final containerIds = contents .where((entry) => entry.patchIds.contains(patchId)) - .map((entry) => entry.caseId) + .map((entry) => entry.containerId) + .toSet(); + + final result = containers + .where((c) => containerIds.contains(c.id)) + .toList(); + result.sort(_compareContainerByReleaseDateAsc); + return result; + } + + Future> loadContainersForCharm(String charmId) async { + final containers = await loadContainers(); + final contents = await loadCharmContents(); + + final containerIds = contents + .where((entry) => entry.charmIds.contains(charmId)) + .map((entry) => entry.containerId) .toSet(); - final result = cases.where((c) => caseIds.contains(c.id)).toList(); - result.sort(_compareCaseByReleaseDateAsc); + final result = containers + .where((c) => containerIds.contains(c.id)) + .toList(); + result.sort(_compareContainerByReleaseDateAsc); return result; } - Future> loadAgentCollectionsForAgent( + Future> loadAgentCollectionsForAgent( String agentId, ) async { final collections = await loadAgentCollections(); @@ -256,27 +393,32 @@ mixin _LocalDataRepositoryQueries on _LocalDataRepositoryLoaders { .toSet(); final result = collections.where((c) => ids.contains(c.id)).toList(); - result.sort(_compareNamedReleaseDateAsc); + result.sort(_compareCollectibleCollectionAsc); return result; } - Future> loadStickerCollections() async { - final cases = await loadCases(); - final result = cases.where((c) => c.isStickerCollection).toList(); + Future> loadStickerCollections() async { + final containers = await loadContainers(); + final result = containers.where((c) => c.isStickerCollection).toList(); result.sort(_compareCollectibleCollectionAsc); return result; } - Future> loadPatchCollections() async { - final cases = await loadCases(); - final result = cases.where((c) => c.isPatchCollection).toList(); + Future> loadPatchCollections() async { + final containers = await loadContainers(); + final result = containers.where((c) => c.isPatchCollection).toList(); result.sort(_compareCollectibleCollectionAsc); return result; } - Future> loadRewardCollectionsForSkin( - String skinId, - ) async { + Future> loadCharmCollections() async { + final containers = await loadContainers(); + final result = containers.where((c) => c.isCharmCollection).toList(); + result.sort(_compareCollectibleCollectionAsc); + return result; + } + + Future> loadRewardCollectionsForSkin(String skinId) async { final collections = await loadRewardCollections(); final contents = await loadRewardCollectionContents(); @@ -290,7 +432,7 @@ mixin _LocalDataRepositoryQueries on _LocalDataRepositoryLoaders { return result; } - Future> loadOperationCollectionsForSkin( + Future> loadOperationCollectionsForSkin( String skinId, ) async { final collections = await loadOperationCollections(); @@ -302,7 +444,7 @@ mixin _LocalDataRepositoryQueries on _LocalDataRepositoryLoaders { .toSet(); final result = collections.where((c) => ids.contains(c.id)).toList(); - result.sort(_compareOperationCollectionAsc); + result.sort(_compareCollectibleCollectionAsc); return result; } } diff --git a/lib/data/repositories/local_data_repository_sorting.dart b/lib/data/repositories/local_data_repository_sorting.dart index d15697dc..7a1cf6ca 100644 --- a/lib/data/repositories/local_data_repository_sorting.dart +++ b/lib/data/repositories/local_data_repository_sorting.dart @@ -6,7 +6,7 @@ int _compareByReleaseDateAsc(String? a, String? b) { return left.compareTo(right); } -int _compareCaseByReleaseDateAsc(CaseDto a, CaseDto b) { +int _compareContainerByReleaseDateAsc(ContainerDto a, ContainerDto b) { final byDate = _compareByReleaseDateAsc(a.releaseDate, b.releaseDate); if (byDate != 0) return byDate; return a.name.compareTo(b.name); @@ -21,20 +21,7 @@ int _compareNamedReleaseDateAsc(dynamic a, dynamic b) { return (a.name as String).compareTo(b.name as String); } -int _compareOperationCollectionAsc( - OperationCollectionDto a, - OperationCollectionDto b, -) { - final byOperation = a.operationName.compareTo(b.operationName); - if (byOperation != 0) return byOperation; - - final byDate = _compareByReleaseDateAsc(a.releaseDate, b.releaseDate); - if (byDate != 0) return byDate; - - return a.name.compareTo(b.name); -} - -int _compareCollectibleCollectionAsc(CaseDto a, CaseDto b) { +int _compareCollectibleCollectionAsc(ContainerDto a, ContainerDto b) { final sourceA = a.sourceType ?? ''; final sourceB = b.sourceType ?? ''; final bySource = sourceA.compareTo(sourceB); @@ -112,6 +99,19 @@ int _musicKitRarityOrder(MusicKitDto musicKit) { } } +int _musicKitVariantOrder(MusicKitDto musicKit) { + if (musicKit.hasRegular && !musicKit.hasStatTrak) { + return 0; + } + if (musicKit.hasRegular && musicKit.hasStatTrak) { + return 1; + } + if (musicKit.hasStatTrak) { + return 2; + } + return 999; +} + int _agentRarityOrder(AgentDto agent) { switch (agent.rarity) { case 'DISTINGUISHED': @@ -154,3 +154,18 @@ int _patchRarityOrder(PatchDto patch) { return 99; } } + +int _charmRarityOrder(CharmDto charm) { + switch (charm.rarity) { + case 'HIGH_GRADE': + return 0; + case 'REMARKABLE': + return 1; + case 'EXOTIC': + return 2; + case 'EXTRAORDINARY': + return 3; + default: + return 99; + } +} diff --git a/lib/domain/agent_collection_simulator_service.dart b/lib/domain/agent_collection_simulator_service.dart index d8796634..c9e4cef5 100644 --- a/lib/domain/agent_collection_simulator_service.dart +++ b/lib/domain/agent_collection_simulator_service.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import '../data/models/agent_collection_dto.dart'; import '../data/models/agent_dto.dart'; +import '../data/models/container_dto.dart'; import 'dropped_agent.dart'; class AgentCollectionSimulatorService { @@ -9,7 +9,7 @@ class AgentCollectionSimulatorService { DroppedAgent openCollection({ required List agents, - required AgentCollectionDto collection, + required ContainerDto collection, }) { if (agents.isEmpty) { throw Exception('No agents found for agent collection'); diff --git a/lib/domain/case_odds.dart b/lib/domain/case_odds.dart index d5309e2e..b83a7129 100644 --- a/lib/domain/case_odds.dart +++ b/lib/domain/case_odds.dart @@ -7,4 +7,4 @@ enum CaseOdds { final double chance; const CaseOdds(this.chance); -} \ No newline at end of file +} diff --git a/lib/domain/charm_collection_simulator_service.dart b/lib/domain/charm_collection_simulator_service.dart new file mode 100644 index 00000000..2d9cd49e --- /dev/null +++ b/lib/domain/charm_collection_simulator_service.dart @@ -0,0 +1,30 @@ +import 'dart:math'; + +import '../data/models/charm_dto.dart'; +import 'dropped_charm.dart'; + +class CharmCollectionSimulatorService { + final Random _random = Random(); + + DroppedCharm openCollection({required List charms}) { + if (charms.isEmpty) { + throw Exception('No charms found for charm collection'); + } + + return DroppedCharm(charm: _selectCharm(charms)); + } + + CharmDto _selectCharm(List charms) { + final roll = _random.nextDouble(); + final rarity = switch (roll) { + <= 0.7992327 => 'HIGH_GRADE', + <= 0.9590792 => 'REMARKABLE', + <= 0.9910475 => 'EXTRAORDINARY', + _ => 'EXOTIC', + }; + + final filtered = charms.where((charm) => charm.rarity == rarity).toList(); + final pool = filtered.isEmpty ? charms : filtered; + return pool[_random.nextInt(pool.length)]; + } +} diff --git a/lib/domain/case_simulator_service.dart b/lib/domain/container_simulator_service.dart similarity index 79% rename from lib/domain/case_simulator_service.dart rename to lib/domain/container_simulator_service.dart index 80fc67f1..b45da633 100644 --- a/lib/domain/case_simulator_service.dart +++ b/lib/domain/container_simulator_service.dart @@ -1,64 +1,73 @@ import 'dart:math'; -import '../data/models/case_dto.dart'; +import '../data/models/container_dto.dart'; import '../data/models/skin_dto.dart'; import 'case_odds.dart'; import 'dropped_skin.dart'; import 'package_odds.dart'; +import 'skin_float_helper.dart'; import 'terminal_offer.dart'; -class CaseSimulatorService { +class ContainerSimulatorService { final Random _random = Random(); DroppedSkin openCase({ required List skins, - required CaseDto caseDto, + required ContainerDto containerDto, }) { if (skins.isEmpty) { throw Exception('No skins found for container'); } - if (caseDto.isTerminal) { + if (containerDto.isTerminal) { throw Exception('Use buildTerminalOffers() for terminal containers'); } - if (caseDto.isXrayPackage) { + if (containerDto.isXrayPackage) { final guaranteedSkin = skins.first; - final floatValue = _generateFloat( - guaranteedSkin.floatTop, - guaranteedSkin.floatBottom, + final wear = SkinFloatHelper.generateWear( + random: _random, + minFloat: guaranteedSkin.floatTop, + maxFloat: guaranteedSkin.floatBottom, ); return DroppedSkin( skin: guaranteedSkin, isStatTrak: false, isSouvenir: false, - skinFloat: floatValue, - exterior: _getExterior(floatValue), + skinFloat: wear.floatValue, + exterior: wear.exterior, ); } final SkinDto selectedSkin; - if (caseDto.isSouvenirPackage || caseDto.isCollectionPackage) { + if (containerDto.isSouvenirPackage || containerDto.isCollectionPackage) { selectedSkin = _selectPackageSkin(skins); } else { selectedSkin = _selectCaseSkin(skins); } - final isSouvenir = caseDto.isSouvenirPackage; - final isStatTrak = caseDto.isRegularCase && + final isSouvenir = containerDto.isSouvenirPackage; + final isStatTrak = + containerDto.isRegularCase && !selectedSkin.isGloves && !selectedSkin.isKnife && _generateStatTrak(); - final isVanillaKnife = selectedSkin.isKnife && selectedSkin.name == 'Vanilla'; + final isVanillaKnife = + selectedSkin.isKnife && selectedSkin.name == 'Vanilla'; double? value; String? exterior; if (!isVanillaKnife) { - value = _generateFloat(selectedSkin.floatTop, selectedSkin.floatBottom); - exterior = _getExterior(value); + final wear = SkinFloatHelper.generateWear( + random: _random, + minFloat: selectedSkin.floatTop, + maxFloat: selectedSkin.floatBottom, + ); + value = wear.floatValue; + exterior = wear.exterior; } return DroppedSkin( @@ -90,8 +99,13 @@ class CaseSimulatorService { String? exterior; if (!isVanillaKnife) { - value = _generateFloat(skin.floatTop, skin.floatBottom); - exterior = _getExterior(value); + final wear = SkinFloatHelper.generateWear( + random: _random, + minFloat: skin.floatTop, + maxFloat: skin.floatBottom, + ); + value = wear.floatValue; + exterior = wear.exterior; } return TerminalOffer( @@ -191,7 +205,8 @@ class CaseSimulatorService { }).toList(); case CaseOdds.specialItem: return skins.where((s) { - final specialLike = s.rarity == 'COVERT' || s.rarity == 'EXTRAORDINARY'; + final specialLike = + s.rarity == 'COVERT' || s.rarity == 'EXTRAORDINARY'; return specialLike && s.isSpecialItem; }).toList(); } @@ -230,24 +245,16 @@ class CaseSimulatorService { case PackageOdds.classified: return skins.where((s) => s.rarity == 'CLASSIFIED').toList(); case PackageOdds.covert: - return skins.where((s) => - s.rarity == 'COVERT' || - s.rarity == 'CONTRABAND' || - s.isSpecialItem).toList(); + return skins + .where( + (s) => + s.rarity == 'COVERT' || + s.rarity == 'CONTRABAND' || + s.isSpecialItem, + ) + .toList(); } } bool _generateStatTrak() => _random.nextInt(10) == 0; - - double _generateFloat(double min, double max) { - return min + _random.nextDouble() * (max - min); - } - - String _getExterior(double value) { - if (value >= 0.00 && value <= 0.07) return 'Factory New'; - if (value > 0.07 && value <= 0.15) return 'Minimal Wear'; - if (value > 0.15 && value <= 0.37) return 'Field-Tested'; - if (value > 0.37 && value <= 0.44) return 'Well-Worn'; - return 'Battle-Scarred'; - } -} \ No newline at end of file +} diff --git a/lib/domain/dropped_charm.dart b/lib/domain/dropped_charm.dart new file mode 100644 index 00000000..8c3d9b1a --- /dev/null +++ b/lib/domain/dropped_charm.dart @@ -0,0 +1,7 @@ +import '../data/models/charm_dto.dart'; + +class DroppedCharm { + final CharmDto charm; + + const DroppedCharm({required this.charm}); +} diff --git a/lib/domain/dropped_skin.dart b/lib/domain/dropped_skin.dart index d71d8589..44a9e36c 100644 --- a/lib/domain/dropped_skin.dart +++ b/lib/domain/dropped_skin.dart @@ -23,4 +23,4 @@ class DroppedSkin { final statTrakPrefix = isStatTrak ? 'StatTrakā„¢ ' : ''; return '$star$souvenirPrefix$statTrakPrefix${skin.itemDisplayName} | ${skin.name}'; } -} \ No newline at end of file +} diff --git a/lib/domain/operation_collection_simulator_service.dart b/lib/domain/operation_collection_simulator_service.dart index f13de083..e3fc962f 100644 --- a/lib/domain/operation_collection_simulator_service.dart +++ b/lib/domain/operation_collection_simulator_service.dart @@ -1,33 +1,35 @@ import 'dart:math'; -import '../data/models/operation_collection_dto.dart'; +import '../data/models/container_dto.dart'; import '../data/models/skin_dto.dart'; import 'dropped_skin.dart'; import 'package_odds.dart'; +import 'skin_float_helper.dart'; class OperationCollectionSimulatorService { final Random _random = Random(); DroppedSkin openCollection({ required List skins, - required OperationCollectionDto collection, + required ContainerDto collection, }) { if (skins.isEmpty) { throw Exception('No skins found for operation collection'); } final selectedSkin = _selectSkin(skins); - final floatValue = _generateFloat( - selectedSkin.floatTop, - selectedSkin.floatBottom, + final wear = SkinFloatHelper.generateWear( + random: _random, + minFloat: selectedSkin.floatTop, + maxFloat: selectedSkin.floatBottom, ); return DroppedSkin( skin: selectedSkin, isStatTrak: false, isSouvenir: false, - skinFloat: floatValue, - exterior: _getExterior(floatValue), + skinFloat: wear.floatValue, + exterior: wear.exterior, ); } @@ -80,16 +82,4 @@ class OperationCollectionSimulatorService { }).toList(); } } - - double _generateFloat(double min, double max) { - return min + _random.nextDouble() * (max - min); - } - - String _getExterior(double value) { - if (value <= 0.07) return 'Factory New'; - if (value <= 0.15) return 'Minimal Wear'; - if (value <= 0.37) return 'Field-Tested'; - if (value <= 0.44) return 'Well-Worn'; - return 'Battle-Scarred'; - } -} \ No newline at end of file +} diff --git a/lib/domain/package_odds.dart b/lib/domain/package_odds.dart index 624341a0..c82aba3d 100644 --- a/lib/domain/package_odds.dart +++ b/lib/domain/package_odds.dart @@ -8,4 +8,4 @@ enum PackageOdds { final double chance; const PackageOdds(this.chance); -} \ No newline at end of file +} diff --git a/lib/domain/reward_collection_simulator_service.dart b/lib/domain/reward_collection_simulator_service.dart index 053111b1..3ba55aac 100644 --- a/lib/domain/reward_collection_simulator_service.dart +++ b/lib/domain/reward_collection_simulator_service.dart @@ -1,33 +1,35 @@ import 'dart:math'; -import '../data/models/reward_collection_dto.dart'; +import '../data/models/container_dto.dart'; import '../data/models/skin_dto.dart'; import 'dropped_skin.dart'; import 'package_odds.dart'; +import 'skin_float_helper.dart'; class RewardCollectionSimulatorService { final Random _random = Random(); DroppedSkin openRewardCollection({ required List skins, - required RewardCollectionDto collection, + required ContainerDto collection, }) { if (skins.isEmpty) { throw Exception('No skins found for reward collection'); } final selectedSkin = _selectRewardSkin(skins); - final floatValue = _generateFloat( - selectedSkin.floatTop, - selectedSkin.floatBottom, + final wear = SkinFloatHelper.generateWear( + random: _random, + minFloat: selectedSkin.floatTop, + maxFloat: selectedSkin.floatBottom, ); return DroppedSkin( skin: selectedSkin, isStatTrak: false, isSouvenir: false, - skinFloat: floatValue, - exterior: _getExterior(floatValue), + skinFloat: wear.floatValue, + exterior: wear.exterior, ); } @@ -80,16 +82,4 @@ class RewardCollectionSimulatorService { }).toList(); } } - - double _generateFloat(double min, double max) { - return min + _random.nextDouble() * (max - min); - } - - String _getExterior(double value) { - if (value <= 0.07) return 'Factory New'; - if (value <= 0.15) return 'Minimal Wear'; - if (value <= 0.37) return 'Field-Tested'; - if (value <= 0.44) return 'Well-Worn'; - return 'Battle-Scarred'; - } -} \ No newline at end of file +} diff --git a/lib/domain/skin_float_helper.dart b/lib/domain/skin_float_helper.dart new file mode 100644 index 00000000..a73570a2 --- /dev/null +++ b/lib/domain/skin_float_helper.dart @@ -0,0 +1,110 @@ +import 'dart:math'; + +class WearFloatResult { + final double? floatValue; + final String? exterior; + + const WearFloatResult({required this.floatValue, required this.exterior}); +} + +class _WearTier { + final String label; + final double min; + final double max; + final double weight; + + const _WearTier({ + required this.label, + required this.min, + required this.max, + required this.weight, + }); +} + +class SkinFloatHelper { + static const List<_WearTier> _wearTiers = [ + _WearTier(label: 'Factory New', min: 0.00, max: 0.07, weight: 0.03), + _WearTier(label: 'Minimal Wear', min: 0.07, max: 0.15, weight: 0.24), + _WearTier(label: 'Field-Tested', min: 0.15, max: 0.38, weight: 0.33), + _WearTier(label: 'Well-Worn', min: 0.38, max: 0.45, weight: 0.24), + _WearTier(label: 'Battle-Scarred', min: 0.45, max: 1.00, weight: 0.16), + ]; + + static WearFloatResult generateWear({ + required Random random, + required double minFloat, + required double maxFloat, + }) { + final clampedMin = minFloat.clamp(0.0, 1.0); + final clampedMax = maxFloat.clamp(0.0, 1.0); + + if (clampedMax <= clampedMin) { + return WearFloatResult( + floatValue: clampedMin.toDouble(), + exterior: exteriorFromFloat(clampedMin.toDouble()), + ); + } + + final availableTiers = _wearTiers + .map((tier) { + final intersectionMin = max(clampedMin.toDouble(), tier.min); + final intersectionMax = min(clampedMax.toDouble(), tier.max); + final available = intersectionMax - intersectionMin; + if (available <= 0) return null; + final tierSpan = tier.max - tier.min; + final effectiveWeight = tierSpan <= 0 + ? 0.0 + : tier.weight * (available / tierSpan); + return ( + effectiveWeight: effectiveWeight, + tier: tier, + min: intersectionMin, + max: intersectionMax, + ); + }) + .whereType< + ({double effectiveWeight, double max, double min, _WearTier tier}) + >() + .where((entry) => entry.effectiveWeight > 0) + .toList(); + + if (availableTiers.isEmpty) { + final fallback = clampedMin.toDouble(); + return WearFloatResult( + floatValue: fallback, + exterior: exteriorFromFloat(fallback), + ); + } + + final totalWeight = availableTiers + .map((entry) => entry.effectiveWeight) + .reduce((a, b) => a + b); + + var roll = random.nextDouble() * totalWeight; + var selected = availableTiers.first; + + for (final entry in availableTiers) { + roll -= entry.effectiveWeight; + if (roll <= 0) { + selected = entry; + break; + } + } + + final floatValue = + selected.min + random.nextDouble() * (selected.max - selected.min); + + return WearFloatResult( + floatValue: floatValue, + exterior: selected.tier.label, + ); + } + + static String exteriorFromFloat(double value) { + if (value <= 0.07) return 'Factory New'; + if (value <= 0.15) return 'Minimal Wear'; + if (value <= 0.38) return 'Field-Tested'; + if (value <= 0.45) return 'Well-Worn'; + return 'Battle-Scarred'; + } +} diff --git a/lib/domain/sticker_simulator_service.dart b/lib/domain/sticker_simulator_service.dart index 56fdf449..4a9cdf03 100644 --- a/lib/domain/sticker_simulator_service.dart +++ b/lib/domain/sticker_simulator_service.dart @@ -27,11 +27,11 @@ class StickerSimulatorService { } } - addBucket('HIGH_GRADE', 0.7992327); - addBucket('REMARKABLE', 0.1598465); - addBucket('EXOTIC', 0.0319693); - addBucket('EXTRAORDINARY', 0.0063939); - addBucket('CONTRABAND', 0.0025576); + addBucket('HIGH_GRADE', 0.80); + addBucket('REMARKABLE', 0.16); + addBucket('EXOTIC', 0.032); + addBucket('EXTRAORDINARY', 0.00641); + addBucket('CONTRABAND', 0.00159); if (availableBuckets.isEmpty) { return stickers[_random.nextInt(stickers.length)]; diff --git a/lib/domain/terminal_offer.dart b/lib/domain/terminal_offer.dart index 46025020..66b29b8c 100644 --- a/lib/domain/terminal_offer.dart +++ b/lib/domain/terminal_offer.dart @@ -14,4 +14,4 @@ class TerminalOffer { required this.exterior, required this.offerIndex, }); -} \ No newline at end of file +} diff --git a/lib/domain/terminal_session.dart b/lib/domain/terminal_session.dart index 647e0fe7..448751cc 100644 --- a/lib/domain/terminal_session.dart +++ b/lib/domain/terminal_session.dart @@ -18,4 +18,4 @@ class TerminalSession { bool get isFinished => acceptedOffer != null || currentIndex >= offers.length; int get offersRemaining => offers.length - currentIndex; -} \ No newline at end of file +} diff --git a/lib/domain/tradeup_service.dart b/lib/domain/tradeup_service.dart index 89c1e7c1..60fd40d1 100644 --- a/lib/domain/tradeup_service.dart +++ b/lib/domain/tradeup_service.dart @@ -1,26 +1,68 @@ import 'dart:math'; import '../data/models/skin_dto.dart'; +import 'skin_float_helper.dart'; + +enum TradeUpInputQuality { regular, statTrak, souvenir } + +class TradeUpInputItem { + final SkinDto skin; + final double floatValue; + final TradeUpInputQuality quality; + + const TradeUpInputItem({ + required this.skin, + required this.floatValue, + this.quality = TradeUpInputQuality.regular, + }); + + bool get isStatTrak => quality == TradeUpInputQuality.statTrak; + bool get isSouvenir => quality == TradeUpInputQuality.souvenir; + + TradeUpInputItem copyWith({ + SkinDto? skin, + double? floatValue, + TradeUpInputQuality? quality, + }) { + return TradeUpInputItem( + skin: skin ?? this.skin, + floatValue: floatValue ?? this.floatValue, + quality: quality ?? this.quality, + ); + } +} class TradeUpResult { final SkinDto skin; final double floatValue; final String exterior; + final bool isStatTrak; + final bool isSouvenir; const TradeUpResult({ required this.skin, required this.floatValue, required this.exterior, + required this.isStatTrak, + required this.isSouvenir, }); } class TradeUpChance { final SkinDto skin; final double probability; // 0..1 + final double floatValue; + final String exterior; + final bool isStatTrak; + final bool isSouvenir; const TradeUpChance({ required this.skin, required this.probability, + required this.floatValue, + required this.exterior, + required this.isStatTrak, + required this.isSouvenir, }); } @@ -28,7 +70,7 @@ class TradeUpService { final Random _random = Random(); TradeUpResult tradeUp({ - required List input, + required List input, required List allSkins, required Map> skinIdToRegularCaseIds, required Map> regularCaseIdToSkinIds, @@ -37,16 +79,24 @@ class TradeUpService { throw Exception('No skins selected'); } - final rarity = input.first.rarity; + final rarity = input.first.skin.rarity; - if (input.any((s) => s.rarity != rarity)) { + if (input.any((s) => s.skin.rarity != rarity)) { throw Exception('All skins must have same rarity'); } - if (input.any((s) => s.isSpecialItem)) { + if (input.any((s) => s.skin.isSpecialItem)) { throw Exception('Knives and gloves are not allowed as input'); } + final issue = validationIssue( + input: input, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + ); + if (issue != null) { + throw Exception(issue); + } + final isSpecialTrade = rarity == 'COVERT'; if (isSpecialTrade) { @@ -65,42 +115,38 @@ class TradeUpService { throw Exception('Trade-up requires exactly 10 skins'); } - final nextRarity = _nextRarity(rarity); - - final selectedCollection = _weightedCollectionChoice(input: input); - - final possibleSkins = allSkins.where((s) { - return _sameCollection(s.collection, selectedCollection) && - s.rarity == nextRarity && - !s.isSpecialItem; - }).toList(); + final chances = getTradeUpChances( + input: input, + allSkins: allSkins, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + regularCaseIdToSkinIds: regularCaseIdToSkinIds, + ); - if (possibleSkins.isEmpty) { - throw Exception('No skins found for next rarity in selected collection'); + if (chances.isEmpty) { + throw Exception('No valid trade-up outcomes found'); } - final resultSkin = possibleSkins[_random.nextInt(possibleSkins.length)]; - final floatValue = _calculateOutputFloat(input, resultSkin); - - return TradeUpResult( - skin: resultSkin, - floatValue: floatValue, - exterior: _getExterior(floatValue), - ); + return _rollFromChances(chances); } List getTradeUpChances({ - required List input, + required List input, required List allSkins, required Map> skinIdToRegularCaseIds, required Map> regularCaseIdToSkinIds, }) { if (input.isEmpty) return const []; - final rarity = input.first.rarity; + final rarity = input.first.skin.rarity; - if (input.any((s) => s.rarity != rarity)) return const []; - if (input.any((s) => s.isSpecialItem)) return const []; + if (input.any((s) => s.skin.rarity != rarity)) return const []; + if (input.any((s) => s.skin.isSpecialItem)) return const []; + + final issue = validationIssue( + input: input, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + ); + if (issue != null) return const []; final isSpecialTrade = rarity == 'COVERT'; if (isSpecialTrade && input.length != 5) return const []; @@ -117,11 +163,11 @@ class TradeUpService { final Map skinProbabilityById = {}; for (final entry in caseWeights.entries) { - final caseId = entry.key; + final containerId = entry.key; final caseProbability = entry.value; final possibleSpecialSkins = _getSpecialSkinsForRegularCase( - caseId: caseId, + containerId: containerId, allSkins: allSkins, regularCaseIdToSkinIds: regularCaseIdToSkinIds, ); @@ -137,12 +183,18 @@ class TradeUpService { } } - final chances = skinProbabilityById.entries - .map((e) => TradeUpChance( - skin: skinById[e.key]!, - probability: e.value, - )) - .toList(); + final chances = skinProbabilityById.entries.map((e) { + final skin = skinById[e.key]!; + final floatValue = _calculateOutputFloat(input, skin); + return TradeUpChance( + skin: skin, + probability: e.value, + floatValue: floatValue, + exterior: _getExterior(floatValue), + isStatTrak: _isStatTrakContract(input), + isSouvenir: false, + ); + }).toList(); chances.sort((a, b) => b.probability.compareTo(a.probability)); return chances; @@ -173,54 +225,76 @@ class TradeUpService { } } - final chances = skinProbabilityById.entries - .map((e) => TradeUpChance( - skin: skinById[e.key]!, - probability: e.value, - )) - .toList(); + final chances = skinProbabilityById.entries.map((e) { + final skin = skinById[e.key]!; + final floatValue = _calculateOutputFloat(input, skin); + return TradeUpChance( + skin: skin, + probability: e.value, + floatValue: floatValue, + exterior: _getExterior(floatValue), + isStatTrak: _isStatTrakContract(input), + isSouvenir: false, + ); + }).toList(); chances.sort((a, b) => b.probability.compareTo(a.probability)); return chances; } TradeUpResult _covertToSpecial({ - required List input, + required List input, required List allSkins, required Map> skinIdToRegularCaseIds, required Map> regularCaseIdToSkinIds, }) { - final selectedCaseId = _weightedRegularCaseChoice( + final chances = getTradeUpChances( input: input, - skinIdToRegularCaseIds: skinIdToRegularCaseIds, - ); - - final possibleSkins = _getSpecialSkinsForRegularCase( - caseId: selectedCaseId, allSkins: allSkins, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, regularCaseIdToSkinIds: regularCaseIdToSkinIds, ); - if (possibleSkins.isEmpty) { + if (chances.isEmpty) { throw Exception('No knives/gloves found in related regular cases'); } - final resultSkin = possibleSkins[_random.nextInt(possibleSkins.length)]; - final floatValue = _calculateOutputFloat(input, resultSkin); + return _rollFromChances(chances); + } - return TradeUpResult( - skin: resultSkin, - floatValue: floatValue, - exterior: _getExterior(floatValue), - ); + String? validationIssue({ + required List input, + required Map> skinIdToRegularCaseIds, + }) { + if (input.isEmpty) return null; + + if (input.any((item) => item.isSouvenir)) { + return 'Souvenir items cannot be used in trade-up contracts'; + } + + final firstQuality = input.first.quality; + if (input.any((item) => item.quality != firstQuality)) { + return 'All selected skins must use the same quality mode'; + } + + if (input.first.skin.rarity == 'COVERT' && + _buildRegularCaseWeightsOrNull( + input: input, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + ) == + null) { + return 'These covert skins are not linked to a valid special-item pool'; + } + + return null; } List _getSpecialSkinsForRegularCase({ - required String caseId, + required String containerId, required List allSkins, required Map> regularCaseIdToSkinIds, }) { - final ids = regularCaseIdToSkinIds[caseId] ?? const []; + final ids = regularCaseIdToSkinIds[containerId] ?? const []; final idSet = ids.toSet(); return allSkins.where((s) { @@ -228,14 +302,28 @@ class TradeUpService { }).toList(); } - double _calculateOutputFloat(List input, SkinDto resultSkin) { - final avgFloat = input - .map((s) => (s.floatTop + s.floatBottom) / 2) - .reduce((a, b) => a + b) / - input.length; + double _calculateOutputFloat( + List input, + SkinDto resultSkin, + ) { + final avgNormalizedFloat = + input.map(_normalizedInputFloat).reduce((a, b) => a + b) / input.length; return resultSkin.floatTop + - avgFloat * (resultSkin.floatBottom - resultSkin.floatTop); + avgNormalizedFloat * (resultSkin.floatBottom - resultSkin.floatTop); + } + + double _normalizedInputFloat(TradeUpInputItem input) { + final min = input.skin.floatTop; + final max = input.skin.floatBottom; + final range = max - min; + + if (range <= 0) { + return 0; + } + + final normalized = (input.floatValue - min) / range; + return normalized.clamp(0.0, 1.0); } String _nextRarity(String rarity) { @@ -266,12 +354,12 @@ class TradeUpService { } Map _buildCollectionWeights({ - required List input, + required List input, }) { final counts = {}; - for (final skin in input) { - final collection = _normalizedCollection(skin.collection); + for (final item in input) { + final collection = _normalizedCollection(item.skin.collection); if (collection == null) continue; counts[collection] = (counts[collection] ?? 0) + 1; } @@ -281,82 +369,84 @@ class TradeUpService { throw Exception('Could not determine collections for selected skins'); } - return { - for (final entry in counts.entries) entry.key: entry.value / total, - }; + return {for (final entry in counts.entries) entry.key: entry.value / total}; } - String _weightedCollectionChoice({ - required List input, + Map _buildRegularCaseWeights({ + required List input, + required Map> skinIdToRegularCaseIds, }) { - final weights = _buildCollectionWeights(input: input); - - final roll = _random.nextDouble(); - double cumulative = 0.0; - - for (final entry in weights.entries) { - cumulative += entry.value; - if (roll <= cumulative) { - return entry.key; - } + final result = _buildRegularCaseWeightsOrNull( + input: input, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + ); + if (result == null) { + throw Exception( + 'Could not determine related regular cases for selected skins', + ); } - - return weights.keys.first; + return result; } - Map _buildRegularCaseWeights({ - required List input, + Map? _buildRegularCaseWeightsOrNull({ + required List input, required Map> skinIdToRegularCaseIds, }) { final counts = {}; - for (final skin in input) { - final caseIds = (skinIdToRegularCaseIds[skin.id] ?? const []).toSet().toList(); - if (caseIds.isEmpty) continue; + for (final item in input) { + final containerIds = (skinIdToRegularCaseIds[item.skin.id] ?? const []) + .toSet() + .toList(); + if (containerIds.isEmpty) continue; - final contribution = 1.0 / caseIds.length; - for (final caseId in caseIds) { - counts[caseId] = (counts[caseId] ?? 0) + contribution; + final contribution = 1.0 / containerIds.length; + for (final containerId in containerIds) { + counts[containerId] = (counts[containerId] ?? 0) + contribution; } } final total = counts.values.fold(0, (a, b) => a + b); if (total == 0) { - throw Exception('Could not determine related regular cases for selected skins'); + return null; } - return { - for (final entry in counts.entries) entry.key: entry.value / total, - }; + return {for (final entry in counts.entries) entry.key: entry.value / total}; } - String _weightedRegularCaseChoice({ - required List input, - required Map> skinIdToRegularCaseIds, - }) { - final weights = _buildRegularCaseWeights( - input: input, - skinIdToRegularCaseIds: skinIdToRegularCaseIds, - ); - + TradeUpResult _rollFromChances(List chances) { final roll = _random.nextDouble(); double cumulative = 0.0; - for (final entry in weights.entries) { - cumulative += entry.value; + for (final chance in chances) { + cumulative += chance.probability; if (roll <= cumulative) { - return entry.key; + return TradeUpResult( + skin: chance.skin, + floatValue: chance.floatValue, + exterior: chance.exterior, + isStatTrak: chance.isStatTrak, + isSouvenir: chance.isSouvenir, + ); } } - return weights.keys.first; + final fallback = chances.first; + return TradeUpResult( + skin: fallback.skin, + floatValue: fallback.floatValue, + exterior: fallback.exterior, + isStatTrak: fallback.isStatTrak, + isSouvenir: fallback.isSouvenir, + ); + } + + bool _isStatTrakContract(List input) { + return input.isNotEmpty && + input.every((item) => item.quality == TradeUpInputQuality.statTrak); } String _getExterior(double value) { - if (value <= 0.07) return 'Factory New'; - if (value <= 0.15) return 'Minimal Wear'; - if (value <= 0.37) return 'Field-Tested'; - if (value <= 0.44) return 'Well-Worn'; - return 'Battle-Scarred'; + return SkinFloatHelper.exteriorFromFloat(value); } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 889e1c6e..02605561 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,9 +38,7 @@ class _Cs2SimulatorAppState extends State { debugShowCheckedModeBanner: false, theme: ThemeData.dark(), home: const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), + body: Center(child: CircularProgressIndicator()), ), ); } @@ -57,4 +55,4 @@ class _Cs2SimulatorAppState extends State { }, ); } -} \ No newline at end of file +} diff --git a/lib/presentation/helpers/app_navigation_helper.dart b/lib/presentation/helpers/app_navigation_helper.dart index 0e1c640b..310f4d77 100644 --- a/lib/presentation/helpers/app_navigation_helper.dart +++ b/lib/presentation/helpers/app_navigation_helper.dart @@ -1,13 +1,17 @@ import 'package:flutter/material.dart'; import '../../core/settings/settings_controller.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; -import '../screens/case_open_screen.dart'; +import '../screens/agent_collection_open_screen.dart'; +import '../screens/container_open_screen.dart'; +import '../screens/charm_collection_open_screen.dart'; import '../screens/graffiti_box_open_screen.dart'; import '../screens/music_kit_box_open_screen.dart'; +import '../screens/operation_collection_open_screen.dart'; import '../screens/patch_container_open_screen.dart'; import '../screens/pin_container_open_screen.dart'; +import '../screens/reward_collection_open_screen.dart'; import '../screens/sticker_container_open_screen.dart'; import '../screens/terminal_open_screen.dart'; @@ -22,54 +26,78 @@ class AppNavigationHelper { } static Widget buildContainerOpenScreen({ - required CaseDto caseDto, + required ContainerDto containerDto, required LocalDataRepository repository, SettingsController? settingsController, }) { - if (caseDto.isStickerCapsule || caseDto.isStickerCollection) { + if (containerDto.isStickerCapsule || containerDto.isStickerCollection) { return StickerContainerOpenScreen( - caseDto: caseDto, + containerDto: containerDto, repository: repository, ); } - if (caseDto.isPinCapsule) { + if (containerDto.isPinCapsule) { return PinContainerOpenScreen( - caseDto: caseDto, + containerDto: containerDto, repository: repository, ); } - if (caseDto.isMusicKitBox) { + if (containerDto.isMusicKitBox) { return MusicKitBoxOpenScreen( - caseDto: caseDto, + containerDto: containerDto, repository: repository, ); } - if (caseDto.isGraffitiBox) { + if (containerDto.isGraffitiBox) { return GraffitiBoxOpenScreen( - caseDto: caseDto, + containerDto: containerDto, repository: repository, ); } - if (caseDto.isPatchPack) { + if (containerDto.isPatchPack) { return PatchContainerOpenScreen( - caseDto: caseDto, + containerDto: containerDto, repository: repository, ); } - if (caseDto.isTerminal) { + if (containerDto.isCharmCollection) { + return CharmCollectionOpenScreen( + collection: containerDto, + repository: repository, + ); + } + if (containerDto.isAgentCollection) { + return AgentCollectionOpenScreen( + collection: containerDto, + repository: repository, + ); + } + if (containerDto.isRewardCollection) { + return RewardCollectionOpenScreen( + collection: containerDto, + repository: repository, + ); + } + if (containerDto.isOperationCollection) { + return OperationCollectionOpenScreen( + collection: containerDto, + repository: repository, + ); + } + if (containerDto.isTerminal) { return TerminalOpenScreen( - caseDto: caseDto, + containerDto: containerDto, repository: repository, ); } assert( - settingsController != null || !caseDto.isRegularCase, + settingsController != null || !containerDto.isRegularCase, 'settingsController is required for regular case screens.', ); - return CaseOpenScreen( - caseDto: caseDto, + return ContainerOpenScreen( + containerDto: containerDto, repository: repository, settingsController: settingsController!, ); diff --git a/lib/presentation/helpers/charm_ui_helper.dart b/lib/presentation/helpers/charm_ui_helper.dart new file mode 100644 index 00000000..159ce515 --- /dev/null +++ b/lib/presentation/helpers/charm_ui_helper.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import '../../data/models/charm_dto.dart'; + +class CharmUiHelper { + static const Color _blue = Color(0xFF4B69FF); + static const Color _purple = Color(0xFF8847FF); + static const Color _red = Color(0xFFEB4B4B); + static const Color _pink = Color(0xFFD32CE6); + + static Color rarityColor(CharmDto charm) { + switch (charm.rarity) { + case 'REMARKABLE': + return _purple; + case 'EXTRAORDINARY': + return _red; + case 'EXOTIC': + return _pink; + case 'HIGH_GRADE': + default: + return _blue; + } + } + + static String rarityLabel(CharmDto charm) { + switch (charm.rarity) { + case 'HIGH_GRADE': + return 'High Grade'; + case 'REMARKABLE': + return 'Remarkable'; + case 'EXTRAORDINARY': + return 'Extraordinary'; + case 'EXOTIC': + return 'Exotic'; + default: + return charm.rarity; + } + } + + static String secondaryText(CharmDto charm) { + return charm.collection ?? 'Charm'; + } +} diff --git a/lib/presentation/helpers/music_kit_ui_helper.dart b/lib/presentation/helpers/music_kit_ui_helper.dart index 904605f1..5182868e 100644 --- a/lib/presentation/helpers/music_kit_ui_helper.dart +++ b/lib/presentation/helpers/music_kit_ui_helper.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../data/models/music_kit_dto.dart'; +import '../../data/models/music_kit_group_dto.dart'; class MusicKitUiHelper { static Color rarityColor(MusicKitDto musicKit) { @@ -22,15 +23,44 @@ class MusicKitUiHelper { } static String typeLabel(MusicKitDto musicKit) { - return musicKit.isStatTrak ? 'StatTrakā„¢ Music Kit' : 'Music Kit'; + if (musicKit.hasRegular && musicKit.hasStatTrak) { + return 'Music Kit / StatTrakā„¢'; + } + if (musicKit.hasStatTrak) { + return 'StatTrakā„¢ Music Kit'; + } + return 'Music Kit'; } static String secondaryText(MusicKitDto musicKit) { - final parts = [typeLabel(musicKit)]; + final parts = [ + typeLabel(musicKit), + if (musicKit.hasRegular && musicKit.hasStatTrak) 'Both variants', + ]; final collection = (musicKit.collection ?? '').trim(); if (collection.isNotEmpty) { parts.add(collection); } return parts.join(' | '); } + + static String groupedTypeLabel(MusicKitGroupDto group) { + if (group.hasRegular && group.hasStatTrak) { + return 'Music Kit / StatTrakā„¢'; + } + if (group.hasStatTrak) { + return 'StatTrakā„¢ Music Kit'; + } + return 'Music Kit'; + } + + static String groupedSecondaryText(MusicKitGroupDto group) { + final parts = [ + if ((group.artist ?? '').isNotEmpty) group.artist!, + groupedTypeLabel(group), + if (group.hasRegular && group.hasStatTrak) 'Both variants', + if ((group.collection ?? '').isNotEmpty) group.collection!, + ]; + return parts.join(' | '); + } } diff --git a/lib/presentation/helpers/pin_ui_helper.dart b/lib/presentation/helpers/pin_ui_helper.dart index 5bcc2488..4124e72e 100644 --- a/lib/presentation/helpers/pin_ui_helper.dart +++ b/lib/presentation/helpers/pin_ui_helper.dart @@ -14,7 +14,7 @@ class PinUiHelper { case 'EXOTIC': return Colors.pink; case 'EXTRAORDINARY': - return Colors.amber; + return const Color(0xFFEB4B4B); default: return Colors.white24; } diff --git a/lib/presentation/helpers/responsive_grid_helper.dart b/lib/presentation/helpers/responsive_grid_helper.dart index 29b6eaeb..14ea69bb 100644 --- a/lib/presentation/helpers/responsive_grid_helper.dart +++ b/lib/presentation/helpers/responsive_grid_helper.dart @@ -34,4 +34,4 @@ class ResponsiveGridHelper { if (width > 600) return 4; return 3; } -} \ No newline at end of file +} diff --git a/lib/presentation/helpers/skin_ui_helper.dart b/lib/presentation/helpers/skin_ui_helper.dart index 26b3c93c..2f7770b3 100644 --- a/lib/presentation/helpers/skin_ui_helper.dart +++ b/lib/presentation/helpers/skin_ui_helper.dart @@ -96,4 +96,4 @@ class SkinUiHelper { final statTrakPrefix = isStatTrak ? 'StatTrakā„¢ ' : ''; return '$star$souvenirPrefix$statTrakPrefix${skin.itemDisplayName} | ${skin.name}'; } -} \ No newline at end of file +} diff --git a/lib/presentation/helpers/source_color_helper.dart b/lib/presentation/helpers/source_color_helper.dart index 51aa0306..c28a8a18 100644 --- a/lib/presentation/helpers/source_color_helper.dart +++ b/lib/presentation/helpers/source_color_helper.dart @@ -44,6 +44,14 @@ class SourceColorHelper { return Colors.pinkAccent; case 'PATCH_COLLECTION': return Colors.pinkAccent; + case 'CHARM_COLLECTION': + return Colors.deepOrangeAccent; + case 'AGENT_COLLECTION': + return Colors.blueGrey; + case 'REWARD_COLLECTION': + return Colors.amberAccent; + case 'OPERATION_COLLECTION': + return Colors.amberAccent; case 'TERMINAL': return Colors.deepPurpleAccent; case 'XRAY_PACKAGE': diff --git a/lib/presentation/helpers/tradeup_controller.dart b/lib/presentation/helpers/tradeup_controller.dart new file mode 100644 index 00000000..f84beadf --- /dev/null +++ b/lib/presentation/helpers/tradeup_controller.dart @@ -0,0 +1,144 @@ +import 'package:flutter/foundation.dart'; + +import '../../data/models/skin_dto.dart'; +import '../../domain/tradeup_service.dart'; + +class TradeUpController extends ChangeNotifier { + final TradeUpService service; + final List selected = []; + + TradeUpResult? result; + List chances = []; + String? tradeIssue; + + TradeUpController({TradeUpService? service}) + : service = service ?? TradeUpService(); + + int maxSelectable() { + if (selected.isEmpty) return 10; + return selected.first.skin.rarity == 'COVERT' ? 5 : 10; + } + + bool get canAddMore => selected.length < maxSelectable(); + + bool get tradeReady { + if (selected.isEmpty) return false; + final rarity = selected.first.skin.rarity; + if (rarity == 'COVERT') { + return selected.length == 5; + } + return selected.length == 10; + } + + bool get canExecuteTrade => tradeReady && tradeIssue == null; + + void add( + SkinDto skin, { + required List allSkins, + required Map> skinIdToRegularCaseIds, + required Map> regularCaseIdToSkinIds, + }) { + if (!canAddMore) return; + if (selected.isNotEmpty && selected.first.skin.rarity != skin.rarity) { + throw Exception('All selected skins must have the same rarity'); + } + + selected.add( + TradeUpInputItem( + skin: skin, + floatValue: (skin.floatTop + skin.floatBottom) / 2, + ), + ); + result = null; + _recalculate( + allSkins: allSkins, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + regularCaseIdToSkinIds: regularCaseIdToSkinIds, + ); + notifyListeners(); + } + + void updateItem( + int index, { + double? floatValue, + TradeUpInputQuality? quality, + required List allSkins, + required Map> skinIdToRegularCaseIds, + required Map> regularCaseIdToSkinIds, + }) { + selected[index] = selected[index].copyWith( + floatValue: floatValue, + quality: quality, + ); + result = null; + _recalculate( + allSkins: allSkins, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + regularCaseIdToSkinIds: regularCaseIdToSkinIds, + ); + notifyListeners(); + } + + void removeAt( + int index, { + required List allSkins, + required Map> skinIdToRegularCaseIds, + required Map> regularCaseIdToSkinIds, + }) { + selected.removeAt(index); + result = null; + _recalculate( + allSkins: allSkins, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + regularCaseIdToSkinIds: regularCaseIdToSkinIds, + ); + notifyListeners(); + } + + void clear() { + selected.clear(); + result = null; + chances = []; + tradeIssue = null; + notifyListeners(); + } + + void executeTrade({ + required List allSkins, + required Map> skinIdToRegularCaseIds, + required Map> regularCaseIdToSkinIds, + }) { + result = service.tradeUp( + input: selected, + allSkins: allSkins, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + regularCaseIdToSkinIds: regularCaseIdToSkinIds, + ); + notifyListeners(); + } + + void _recalculate({ + required List allSkins, + required Map> skinIdToRegularCaseIds, + required Map> regularCaseIdToSkinIds, + }) { + tradeIssue = service.validationIssue( + input: selected, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + ); + + if (tradeReady && tradeIssue == null) { + chances = service.getTradeUpChances( + input: selected, + allSkins: allSkins, + skinIdToRegularCaseIds: skinIdToRegularCaseIds, + regularCaseIdToSkinIds: regularCaseIdToSkinIds, + ); + if (chances.isEmpty) { + tradeIssue = 'This selection cannot produce a valid trade-up result'; + } + } else { + chances = []; + } + } +} diff --git a/lib/presentation/screens/agent_collection_list_screen.dart b/lib/presentation/screens/agent_collection_list_screen.dart index 6d177b30..57bd5820 100644 --- a/lib/presentation/screens/agent_collection_list_screen.dart +++ b/lib/presentation/screens/agent_collection_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../data/models/agent_collection_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import '../helpers/source_color_helper.dart'; @@ -21,7 +21,7 @@ class AgentCollectionListScreen extends StatefulWidget { } class _AgentCollectionListScreenState extends State { - late Future> _future; + late Future> _future; @override void initState() { @@ -29,18 +29,18 @@ class _AgentCollectionListScreenState extends State { _future = widget.repository.loadAgentCollections(); } - Widget _buildCard(BuildContext context, AgentCollectionDto collection) { - final color = SourceColorHelper.operationColor(collection.operationId); + Widget _buildCard(BuildContext context, ContainerDto collection) { + final color = SourceColorHelper.operationColor(collection.sourceId ?? ''); return CollectionListCard( - imagePath: collection.image, + imagePath: collection.containerImage, title: collection.name, releaseDate: collection.releaseDate, chips: [ChipBadge(label: 'Agent Collection', color: color)], metadata: [ const SizedBox(height: 8), Text( - collection.operationName, + collection.sourceLabel, style: TextStyle( color: color, fontSize: 13, @@ -64,10 +64,10 @@ class _AgentCollectionListScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Agent Collections')), - body: AsyncCollectionLoader( + body: AsyncCollectionLoader( future: _future, builder: (context, items) { - return ResponsiveCollectionGrid( + return ResponsiveCollectionGrid( items: items, emptyMessage: 'No agent collections found.', itemBuilder: _buildCard, diff --git a/lib/presentation/screens/agent_collection_open_screen.dart b/lib/presentation/screens/agent_collection_open_screen.dart index c2758153..42ad2f20 100644 --- a/lib/presentation/screens/agent_collection_open_screen.dart +++ b/lib/presentation/screens/agent_collection_open_screen.dart @@ -3,8 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/agent_collection_dto.dart'; import '../../data/models/agent_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/agent_collection_simulator_service.dart'; import '../../domain/dropped_agent.dart'; @@ -20,7 +20,7 @@ import '../widgets/opening_loading_card.dart'; import '../widgets/source_badge.dart'; class AgentCollectionOpenScreen extends StatefulWidget { - final AgentCollectionDto collection; + final ContainerDto collection; final LocalDataRepository repository; const AgentCollectionOpenScreen({ @@ -78,7 +78,9 @@ class _AgentCollectionOpenScreenState extends State { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( widget.collection.releaseDate, ); - final color = SourceColorHelper.operationColor(widget.collection.operationId); + final color = SourceColorHelper.operationColor( + widget.collection.sourceId ?? '', + ); return Scaffold( appBar: AppBar(title: Text(widget.collection.name)), @@ -88,18 +90,20 @@ class _AgentCollectionOpenScreenState extends State { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.collection.image, + assetPath: widget.collection.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ SourceBadge( - label: widget.collection.operationName, + label: widget.collection.sourceLabel, color: color, ), ], releaseDateText: formattedReleaseDate, description: 'Agent collections open like operation rewards: no roulette, just the final reveal.', - buttonLabel: _isOpening ? 'OPENING...' : 'OPEN AGENT COLLECTION', + buttonLabel: _isOpening + ? 'OPENING...' + : 'OPEN AGENT COLLECTION', onPressed: (_isOpening || agents.isEmpty) ? null : () => _openCollection(agents), diff --git a/lib/presentation/screens/agent_details_screen.dart b/lib/presentation/screens/agent_details_screen.dart index 0da53bc4..1f640742 100644 --- a/lib/presentation/screens/agent_details_screen.dart +++ b/lib/presentation/screens/agent_details_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/agent_collection_dto.dart'; import '../../data/models/agent_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/agent_ui_helper.dart'; import '../helpers/app_navigation_helper.dart'; @@ -29,7 +29,7 @@ class AgentDetailsScreen extends StatelessWidget { return Scaffold( appBar: AppBar(title: Text(agent.name)), - body: FutureBuilder>( + body: FutureBuilder>( future: repository.loadAgentCollectionsForAgent(agent.id), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { @@ -48,7 +48,7 @@ class AgentDetailsScreen extends StatelessWidget { ); } - final collections = snapshot.data ?? const []; + final collections = snapshot.data ?? const []; return ListView( padding: const EdgeInsets.all(12), @@ -63,7 +63,9 @@ class AgentDetailsScreen extends StatelessWidget { color: rarityColor, ), DetailTag( - text: agent.team == 'COUNTER-TERRORIST' ? 'CT Side' : 'T Side', + text: agent.team == 'COUNTER-TERRORIST' + ? 'CT Side' + : 'T Side', ), if ((agent.collection ?? '').isNotEmpty) DetailTag(text: agent.collection!), @@ -80,20 +82,24 @@ class AgentDetailsScreen extends StatelessWidget { : 'Terrorist', ), if ((agent.collection ?? '').isNotEmpty) - DetailInfoRow(title: 'Collection', value: agent.collection!), + DetailInfoRow( + title: 'Collection', + value: agent.collection!, + ), ], ), const SizedBox(height: 12), - DetailSourceSection( + DetailSourceSection( title: 'Agent Collections', items: collections, emptyText: 'No agent collection sources found.', itemBuilder: (item) => DetailSourceTile( - imagePath: item.image, + imagePath: item.containerImage, title: item.name, - subtitle: item.operationName, + subtitle: item.sourceLabel, trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, diff --git a/lib/presentation/screens/agent_glossary_screen.dart b/lib/presentation/screens/agent_glossary_screen.dart index 7357c648..a1ca4c75 100644 --- a/lib/presentation/screens/agent_glossary_screen.dart +++ b/lib/presentation/screens/agent_glossary_screen.dart @@ -22,6 +22,7 @@ class AgentGlossaryScreen extends StatefulWidget { class _AgentGlossaryScreenState extends State { String _rarityFilter = 'ALL'; String _sideFilter = 'ALL'; + String _collectionFilter = 'ALL'; static const List _rarityOptions = [ GlossaryFilterOption('ALL', 'All rarities'), @@ -37,6 +38,21 @@ class _AgentGlossaryScreenState extends State { GlossaryFilterOption('TERRORIST', 'T Side'), ]; + List _collectionOptions(List items) { + final values = + items + .map((item) => (item.collection ?? '').trim()) + .where((value) => value.isNotEmpty) + .toSet() + .toList() + ..sort(); + + return [ + const GlossaryFilterOption('ALL', 'All collections'), + ...values.map((value) => GlossaryFilterOption(value, value)), + ]; + } + List _filterAndSort(List items, String query) { final filtered = items.where((agent) { if (_rarityFilter != 'ALL' && agent.rarity != _rarityFilter) { @@ -45,6 +61,10 @@ class _AgentGlossaryScreenState extends State { if (_sideFilter != 'ALL' && agent.team != _sideFilter) { return false; } + if (_collectionFilter != 'ALL' && + (agent.collection ?? '') != _collectionFilter) { + return false; + } if (query.isEmpty) return true; final haystack = [ agent.name, @@ -89,7 +109,7 @@ class _AgentGlossaryScreenState extends State { countLabelBuilder: (count) => '$count agents', emptyMessage: 'No agents found.', errorPrefix: 'Failed to load agents.', - headerControlsBuilder: (_) => [ + headerControlsBuilder: (_, items) => [ Row( children: [ Expanded( @@ -119,6 +139,17 @@ class _AgentGlossaryScreenState extends State { ), ], ), + const SizedBox(height: 10), + GlossaryFilterDropdown( + label: 'Collection', + value: _collectionFilter, + options: _collectionOptions(items), + onChanged: (value) { + setState(() { + _collectionFilter = value ?? 'ALL'; + }); + }, + ), ], itemBuilder: (context, agent) { final color = AgentUiHelper.rarityColor(agent); diff --git a/lib/presentation/screens/charm_collection_list_screen.dart b/lib/presentation/screens/charm_collection_list_screen.dart new file mode 100644 index 00000000..d3a173ee --- /dev/null +++ b/lib/presentation/screens/charm_collection_list_screen.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +import '../../data/models/container_dto.dart'; +import '../../data/repositories/local_data_repository.dart'; +import '../helpers/app_navigation_helper.dart'; +import '../helpers/source_color_helper.dart'; +import '../widgets/async_collection_loader.dart'; +import '../widgets/chip_badge.dart'; +import '../widgets/collection_list_card.dart'; +import '../widgets/responsive_collection_grid.dart'; +import 'charm_collection_open_screen.dart'; + +class CharmCollectionListScreen extends StatefulWidget { + final LocalDataRepository repository; + + const CharmCollectionListScreen({super.key, required this.repository}); + + @override + State createState() => + _CharmCollectionListScreenState(); +} + +class _CharmCollectionListScreenState extends State { + late Future> _future; + + @override + void initState() { + super.initState(); + _future = widget.repository.loadCharmCollections(); + } + + Widget _buildCard(BuildContext context, ContainerDto collection) { + final typeColor = SourceColorHelper.containerTypeColor(collection.type); + final sourceColor = SourceColorHelper.collectibleSourceColor( + collection.sourceType, + collection.sourceId, + ); + + return CollectionListCard( + imagePath: collection.containerImage, + title: collection.name, + releaseDate: collection.releaseDate, + chips: [ + ChipBadge(label: collection.typeLabel, color: typeColor), + if (collection.sourceTypeLabel != null) + ChipBadge(label: collection.sourceTypeLabel!, color: sourceColor), + ], + metadata: [ + if ((collection.sourceName ?? '').isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + collection.sourceName!, + style: TextStyle( + color: sourceColor, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + onTap: () { + AppNavigationHelper.pushScreen( + context, + CharmCollectionOpenScreen( + collection: collection, + repository: widget.repository, + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Charm Collections')), + body: AsyncCollectionLoader( + future: _future, + builder: (context, items) { + return ResponsiveCollectionGrid( + items: items, + emptyMessage: 'No charm collections found.', + itemBuilder: _buildCard, + ); + }, + ), + ); + } +} diff --git a/lib/presentation/screens/charm_collection_open_screen.dart b/lib/presentation/screens/charm_collection_open_screen.dart new file mode 100644 index 00000000..03af37d6 --- /dev/null +++ b/lib/presentation/screens/charm_collection_open_screen.dart @@ -0,0 +1,158 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../../core/utils/date_format_helper.dart'; +import '../../data/models/container_dto.dart'; +import '../../data/models/charm_dto.dart'; +import '../../data/repositories/local_data_repository.dart'; +import '../../domain/charm_collection_simulator_service.dart'; +import '../../domain/dropped_charm.dart'; +import '../helpers/collectible_open_flow_helper.dart'; +import '../helpers/source_color_helper.dart'; +import '../widgets/charm_drop_card.dart'; +import '../widgets/charm_grid_tile.dart'; +import '../widgets/chip_badge.dart'; +import '../widgets/collectible_contents_title.dart'; +import '../widgets/collectible_grid_sliver.dart'; +import '../widgets/collectible_open_body.dart'; +import '../widgets/collectible_open_header.dart'; +import '../widgets/opening_loading_card.dart'; + +class CharmCollectionOpenScreen extends StatefulWidget { + final ContainerDto collection; + final LocalDataRepository repository; + + const CharmCollectionOpenScreen({ + super.key, + required this.collection, + required this.repository, + }); + + @override + State createState() => + _CharmCollectionOpenScreenState(); +} + +class _CharmCollectionOpenScreenState extends State { + late Future> _charmsFuture; + final CharmCollectionSimulatorService _simulator = + CharmCollectionSimulatorService(); + final Random _random = Random(); + + DroppedCharm? _dropped; + bool _isOpening = false; + + @override + void initState() { + super.initState(); + _charmsFuture = widget.repository.loadCharmsForContainer( + widget.collection.id, + ); + } + + Future _openCollection(List charms) async { + await CollectibleOpenFlowHelper.runReveal( + setState: setState, + isMounted: () => mounted, + isOpening: _isOpening, + hasItems: charms.isNotEmpty, + random: _random, + onStart: () { + _isOpening = true; + _dropped = null; + }, + resolveDrop: () => _simulator.openCollection(charms: charms), + onComplete: (drop) { + _dropped = drop; + _isOpening = false; + }, + ); + } + + @override + Widget build(BuildContext context) { + final formattedReleaseDate = DateFormatHelper.formatReleaseDate( + widget.collection.releaseDate, + ); + final typeColor = SourceColorHelper.containerTypeColor( + widget.collection.type, + ); + final sourceColor = SourceColorHelper.collectibleSourceColor( + widget.collection.sourceType, + widget.collection.sourceId, + ); + + return Scaffold( + appBar: AppBar(title: Text(widget.collection.name)), + body: CollectibleOpenBody( + future: _charmsFuture, + sliverBuilder: (context, constraints, charms, gridCount, aspectRatio) { + return [ + SliverToBoxAdapter( + child: CollectibleOpenHeader( + assetPath: widget.collection.containerImage, + imageHeight: constraints.maxWidth < 700 ? 90 : 120, + badges: [ + ChipBadge( + label: widget.collection.typeLabel, + color: typeColor, + ), + if (widget.collection.sourceTypeLabel != null) + ChipBadge( + label: widget.collection.sourceTypeLabel!, + color: sourceColor, + ), + ], + metadata: [ + if ((widget.collection.sourceName ?? '').isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + widget.collection.sourceName!, + style: TextStyle( + color: sourceColor, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + releaseDateText: formattedReleaseDate, + description: + 'Charm collections open like Armory rewards: no roulette, just the final reveal.', + buttonLabel: _isOpening + ? 'OPENING...' + : 'OPEN CHARM COLLECTION', + onPressed: (_isOpening || charms.isEmpty) + ? null + : () => _openCollection(charms), + ), + ), + if (_isOpening) + const SliverToBoxAdapter( + child: OpeningLoadingCard(title: 'Opening charm collection...'), + ), + if (_dropped != null) + SliverToBoxAdapter(child: CharmDropCard(drop: _dropped!)), + const SliverToBoxAdapter( + child: CollectibleContentsTitle(title: 'Collection contents'), + ), + CollectibleGridSliver( + items: charms, + crossAxisCount: gridCount, + childAspectRatio: aspectRatio, + itemBuilder: (charm) { + final isDropped = _dropped?.charm.id == charm.id; + return CharmGridTile( + charm: charm, + highlighted: isDropped, + crossAxisCount: gridCount, + ); + }, + ), + ]; + }, + ), + ); + } +} diff --git a/lib/presentation/screens/charm_details_screen.dart b/lib/presentation/screens/charm_details_screen.dart new file mode 100644 index 00000000..64dc6888 --- /dev/null +++ b/lib/presentation/screens/charm_details_screen.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; + +import '../../core/utils/date_format_helper.dart'; +import '../../data/models/container_dto.dart'; +import '../../data/models/charm_dto.dart'; +import '../../data/repositories/local_data_repository.dart'; +import '../helpers/app_navigation_helper.dart'; +import '../helpers/charm_ui_helper.dart'; +import '../widgets/collectible_details_card.dart'; +import '../widgets/detail_info_row.dart'; +import '../widgets/detail_source_section.dart'; +import '../widgets/detail_source_tile.dart'; +import '../widgets/detail_tag.dart'; + +class CharmDetailsScreen extends StatelessWidget { + final LocalDataRepository repository; + final CharmDto charm; + + const CharmDetailsScreen({ + super.key, + required this.repository, + required this.charm, + }); + + @override + Widget build(BuildContext context) { + final rarityColor = CharmUiHelper.rarityColor(charm); + + return Scaffold( + appBar: AppBar(title: Text(charm.name)), + body: FutureBuilder>( + future: repository.loadContainersForCharm(charm.id), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + 'Failed to load charm details.\n${snapshot.error}', + textAlign: TextAlign.center, + ), + ), + ); + } + + final cases = snapshot.data ?? const []; + + return ListView( + padding: const EdgeInsets.all(12), + children: [ + CollectibleDetailsCard( + imagePath: charm.charmImage, + title: charm.name, + subtitle: CharmUiHelper.secondaryText(charm), + tags: [ + DetailTag( + text: CharmUiHelper.rarityLabel(charm), + color: rarityColor, + ), + if ((charm.collection ?? '').isNotEmpty) + DetailTag(text: charm.collection!), + ], + infoRows: [ + DetailInfoRow( + title: 'Rarity', + value: CharmUiHelper.rarityLabel(charm), + ), + if ((charm.collection ?? '').isNotEmpty) + DetailInfoRow( + title: 'Collection', + value: charm.collection!, + ), + ], + ), + const SizedBox(height: 12), + DetailSourceSection( + title: 'Collections', + items: cases, + emptyText: 'No charm collection sources found.', + itemBuilder: (item) => DetailSourceTile( + imagePath: item.containerImage, + title: item.name, + subtitle: item.typeLabel, + trailing: + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', + onTap: () { + AppNavigationHelper.pushScreen( + context, + AppNavigationHelper.buildContainerOpenScreen( + containerDto: item, + repository: repository, + ), + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/presentation/screens/charm_glossary_screen.dart b/lib/presentation/screens/charm_glossary_screen.dart new file mode 100644 index 00000000..6096d79a --- /dev/null +++ b/lib/presentation/screens/charm_glossary_screen.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; + +import '../../data/models/charm_dto.dart'; +import '../../data/repositories/local_data_repository.dart'; +import '../helpers/app_navigation_helper.dart'; +import '../helpers/charm_ui_helper.dart'; +import '../widgets/detail_tag.dart'; +import '../widgets/generic_glossary_screen.dart'; +import '../widgets/glossary_filter_dropdown.dart'; +import '../widgets/glossary_list_item.dart'; +import 'charm_details_screen.dart'; + +class CharmGlossaryScreen extends StatefulWidget { + final LocalDataRepository repository; + + const CharmGlossaryScreen({super.key, required this.repository}); + + @override + State createState() => _CharmGlossaryScreenState(); +} + +class _CharmGlossaryScreenState extends State { + String _rarityFilter = 'ALL'; + String _collectionFilter = 'ALL'; + + static const List _rarityOptions = [ + GlossaryFilterOption('ALL', 'All rarities'), + GlossaryFilterOption('HIGH_GRADE', 'High Grade'), + GlossaryFilterOption('REMARKABLE', 'Remarkable'), + GlossaryFilterOption('EXTRAORDINARY', 'Extraordinary'), + GlossaryFilterOption('EXOTIC', 'Exotic'), + ]; + + List _collectionOptions(List items) { + final values = + items + .map((item) => (item.collection ?? '').trim()) + .where((value) => value.isNotEmpty) + .toSet() + .toList() + ..sort(); + + return [ + const GlossaryFilterOption('ALL', 'All collections'), + ...values.map((value) => GlossaryFilterOption(value, value)), + ]; + } + + List _filterAndSort(List items, String query) { + final filtered = items.where((charm) { + if (_rarityFilter != 'ALL' && charm.rarity != _rarityFilter) { + return false; + } + if (_collectionFilter != 'ALL' && + (charm.collection ?? '') != _collectionFilter) { + return false; + } + if (query.isEmpty) return true; + final haystack = [ + charm.name, + charm.collection ?? '', + charm.rarity, + ].join(' ').toLowerCase(); + return haystack.contains(query); + }).toList(); + + filtered.sort((a, b) { + final rarityCompare = _rarityOrder(a).compareTo(_rarityOrder(b)); + if (rarityCompare != 0) return rarityCompare; + return a.name.compareTo(b.name); + }); + + return filtered; + } + + int _rarityOrder(CharmDto charm) { + switch (charm.rarity) { + case 'HIGH_GRADE': + return 0; + case 'REMARKABLE': + return 1; + case 'EXOTIC': + return 2; + case 'EXTRAORDINARY': + return 3; + default: + return 999; + } + } + + @override + Widget build(BuildContext context) { + return GenericGlossaryScreen( + title: 'Charm Glossary', + searchHint: 'Search by charm or collection...', + future: widget.repository.loadCharms(), + filterAndSort: _filterAndSort, + countLabelBuilder: (count) => '$count charms', + emptyMessage: 'No charms found.', + errorPrefix: 'Failed to load charms.', + headerControlsBuilder: (_, items) => [ + Row( + children: [ + Expanded( + child: GlossaryFilterDropdown( + label: 'Rarity', + value: _rarityFilter, + options: _rarityOptions, + onChanged: (value) { + setState(() { + _rarityFilter = value ?? 'ALL'; + }); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GlossaryFilterDropdown( + label: 'Collection', + value: _collectionFilter, + options: _collectionOptions(items), + onChanged: (value) { + setState(() { + _collectionFilter = value ?? 'ALL'; + }); + }, + ), + ), + ], + ), + ], + itemBuilder: (context, charm) { + final color = CharmUiHelper.rarityColor(charm); + return GlossaryListItem( + accentColor: color, + imagePath: charm.charmImage, + title: charm.name, + subtitle: CharmUiHelper.secondaryText(charm), + tags: [ + DetailTag(text: CharmUiHelper.rarityLabel(charm), color: color), + if ((charm.collection ?? '').isNotEmpty) + DetailTag(text: charm.collection!), + ], + onTap: () { + AppNavigationHelper.pushScreen( + context, + CharmDetailsScreen(repository: widget.repository, charm: charm), + ); + }, + ); + }, + ); + } +} diff --git a/lib/presentation/screens/case_list_screen.dart b/lib/presentation/screens/container_list_screen.dart similarity index 59% rename from lib/presentation/screens/case_list_screen.dart rename to lib/presentation/screens/container_list_screen.dart index 9f8006cb..e0c7d0e8 100644 --- a/lib/presentation/screens/case_list_screen.dart +++ b/lib/presentation/screens/container_list_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/settings/settings_controller.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import '../helpers/source_color_helper.dart'; @@ -11,22 +11,22 @@ import '../widgets/collection_filter_bar.dart'; import '../widgets/collection_list_card.dart'; import '../widgets/responsive_collection_grid.dart'; -class CaseListScreen extends StatefulWidget { +class ContainerListScreen extends StatefulWidget { final LocalDataRepository repository; final SettingsController settingsController; - const CaseListScreen({ + const ContainerListScreen({ super.key, required this.repository, required this.settingsController, }); @override - State createState() => _CaseListScreenState(); + State createState() => _ContainerListScreenState(); } -class _CaseListScreenState extends State { - late Future> _casesFuture; +class _ContainerListScreenState extends State { + late Future> _containersFuture; static const String _filterAll = 'ALL'; String _selectedFilter = _filterAll; @@ -34,19 +34,23 @@ class _CaseListScreenState extends State { @override void initState() { super.initState(); - _casesFuture = widget.repository.loadCases(); + _containersFuture = widget.repository.loadContainers(); } - List _availableFilters(List cases) { + List _availableFilters(List containers) { final types = {_filterAll}; - for (final caseDto in cases) { - if (caseDto.isXrayPackage || - caseDto.isStickerCollection || - caseDto.isPatchCollection) { + for (final containerDto in containers) { + if (containerDto.isXrayPackage || + containerDto.isStickerCollection || + containerDto.isPatchCollection || + containerDto.isCharmCollection || + containerDto.isAgentCollection || + containerDto.isRewardCollection || + containerDto.isOperationCollection) { continue; } - types.add(caseDto.type); + types.add(containerDto.type); } final ordered = [_filterAll]; @@ -104,13 +108,19 @@ class _CaseListScreenState extends State { } } - List _applyFilters(List cases) { - var filtered = List.from(cases); + List _applyFilters(List containers) { + var filtered = List.from(containers); filtered = filtered .where( (c) => - !c.isXrayPackage && !c.isStickerCollection && !c.isPatchCollection, + !c.isXrayPackage && + !c.isStickerCollection && + !c.isPatchCollection && + !c.isCharmCollection && + !c.isAgentCollection && + !c.isRewardCollection && + !c.isOperationCollection, ) .toList(); @@ -129,8 +139,8 @@ class _CaseListScreenState extends State { return filtered; } - Widget _buildFilterBar(List allCases) { - final filters = _availableFilters(allCases); + Widget _buildFilterBar(List allContainers) { + final filters = _availableFilters(allContainers); if (_selectedFilter != _filterAll && !filters.contains(_selectedFilter)) { _selectedFilter = _filterAll; @@ -148,35 +158,40 @@ class _CaseListScreenState extends State { ); } - Widget _buildCaseCard(BuildContext context, CaseDto caseDto) { - final typeColor = SourceColorHelper.containerTypeColor(caseDto.type); + Widget _buildCaseCard(BuildContext context, ContainerDto containerDto) { + final typeColor = SourceColorHelper.containerTypeColor(containerDto.type); final chips = [ - ChipBadge(label: caseDto.typeLabel, color: typeColor), + ChipBadge(label: containerDto.typeLabel, color: typeColor), ]; - if (caseDto.isStickerCollection && caseDto.sourceTypeLabel != null) { + if (containerDto.isStickerCollection && + containerDto.sourceTypeLabel != null) { final sourceColor = SourceColorHelper.collectibleSourceColor( - caseDto.sourceType, - caseDto.sourceId, + containerDto.sourceType, + containerDto.sourceId, + ); + chips.add( + ChipBadge(label: containerDto.sourceTypeLabel!, color: sourceColor), ); - chips.add(ChipBadge(label: caseDto.sourceTypeLabel!, color: sourceColor)); - if ((caseDto.sourceName ?? '').isNotEmpty) { - chips.add(ChipBadge(label: caseDto.sourceName!, color: sourceColor)); + if ((containerDto.sourceName ?? '').isNotEmpty) { + chips.add( + ChipBadge(label: containerDto.sourceName!, color: sourceColor), + ); } } return CollectionListCard( - imagePath: caseDto.caseImage, - title: caseDto.name, - releaseDate: caseDto.releaseDate, + imagePath: containerDto.containerImage, + title: containerDto.name, + releaseDate: containerDto.releaseDate, chips: chips, metadata: const [], onTap: () { AppNavigationHelper.pushScreen( context, AppNavigationHelper.buildContainerOpenScreen( - caseDto: caseDto, + containerDto: containerDto, repository: widget.repository, settingsController: widget.settingsController, ), @@ -189,15 +204,15 @@ class _CaseListScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Choose Container')), - body: AsyncCollectionLoader( - future: _casesFuture, - builder: (context, allCases) { - final visibleCases = _applyFilters(allCases); + body: AsyncCollectionLoader( + future: _containersFuture, + builder: (context, allContainers) { + final visibleContainers = _applyFilters(allContainers); - return ResponsiveCollectionGrid( - items: visibleCases, + return ResponsiveCollectionGrid( + items: visibleContainers, emptyMessage: 'No containers match the selected filters.', - header: _buildFilterBar(allCases), + header: _buildFilterBar(allContainers), itemBuilder: _buildCaseCard, ); }, diff --git a/lib/presentation/screens/case_open_screen.dart b/lib/presentation/screens/container_open_screen.dart similarity index 90% rename from lib/presentation/screens/case_open_screen.dart rename to lib/presentation/screens/container_open_screen.dart index 53afbda6..7ff37ab5 100644 --- a/lib/presentation/screens/case_open_screen.dart +++ b/lib/presentation/screens/container_open_screen.dart @@ -4,10 +4,10 @@ import 'package:flutter/material.dart'; import '../../core/settings/settings_controller.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/skin_dto.dart'; import '../../data/repositories/local_data_repository.dart'; -import '../../domain/case_simulator_service.dart'; +import '../../domain/container_simulator_service.dart'; import '../../domain/dropped_skin.dart'; import '../helpers/opening_roll_sequence_builder.dart'; import '../helpers/skin_ui_helper.dart'; @@ -24,25 +24,25 @@ import '../widgets/skin_drop_card.dart'; import '../widgets/skin_grid_tile.dart'; import '../widgets/xray_reveal_card.dart'; -class CaseOpenScreen extends StatefulWidget { - final CaseDto caseDto; +class ContainerOpenScreen extends StatefulWidget { + final ContainerDto containerDto; final LocalDataRepository repository; final SettingsController settingsController; - const CaseOpenScreen({ + const ContainerOpenScreen({ super.key, - required this.caseDto, + required this.containerDto, required this.repository, required this.settingsController, }); @override - State createState() => _CaseOpenScreenState(); + State createState() => _ContainerOpenScreenState(); } -class _CaseOpenScreenState extends State { +class _ContainerOpenScreenState extends State { late Future> _skinsFuture; - final CaseSimulatorService _simulator = CaseSimulatorService(); + final ContainerSimulatorService _simulator = ContainerSimulatorService(); final Random _random = Random(); final ScrollController _rollController = ScrollController(); @@ -53,10 +53,10 @@ class _CaseOpenScreenState extends State { DroppedSkin? _pendingXrayDrop; bool _xrayRevealActive = false; - bool get _isRegularCase => widget.caseDto.isRegularCase; - bool get _isSouvenirPackage => widget.caseDto.isSouvenirPackage; - bool get _isCollectionPackage => widget.caseDto.isCollectionPackage; - bool get _isXrayPackage => widget.caseDto.isXrayPackage; + bool get _isRegularCase => widget.containerDto.isRegularCase; + bool get _isSouvenirPackage => widget.containerDto.isSouvenirPackage; + bool get _isCollectionPackage => widget.containerDto.isCollectionPackage; + bool get _isXrayPackage => widget.containerDto.isXrayPackage; bool get _xrayModeEnabled => widget.settingsController.xrayOpeningEnabled; bool get _shouldUseSettingsXrayMode => @@ -69,7 +69,9 @@ class _CaseOpenScreenState extends State { @override void initState() { super.initState(); - _skinsFuture = widget.repository.loadSkinsForCase(widget.caseDto.id); + _skinsFuture = widget.repository.loadSkinsForContainer( + widget.containerDto.id, + ); } @override @@ -88,7 +90,10 @@ class _CaseOpenScreenState extends State { return; } - final drop = _simulator.openCase(skins: skins, caseDto: widget.caseDto); + final drop = _simulator.openCase( + skins: skins, + containerDto: widget.containerDto, + ); if (_isXrayPackage) { setState(() { @@ -344,22 +349,27 @@ class _CaseOpenScreenState extends State { @override Widget build(BuildContext context) { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( - widget.caseDto.releaseDate, + widget.containerDto.releaseDate, + ); + final typeColor = SourceColorHelper.containerTypeColor( + widget.containerDto.type, ); - final typeColor = SourceColorHelper.containerTypeColor(widget.caseDto.type); return Scaffold( - appBar: AppBar(title: Text(widget.caseDto.name)), + appBar: AppBar(title: Text(widget.containerDto.name)), body: CollectibleOpenBody( future: _skinsFuture, sliverBuilder: (context, constraints, skins, gridCount, aspectRatio) { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.caseDto.caseImage, + assetPath: widget.containerDto.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ - ChipBadge(label: widget.caseDto.typeLabel, color: typeColor), + ChipBadge( + label: widget.containerDto.typeLabel, + color: typeColor, + ), ], releaseDateText: formattedReleaseDate, description: _headerDescription(skins), diff --git a/lib/presentation/screens/glossary_hub_screen.dart b/lib/presentation/screens/glossary_hub_screen.dart index afcd46d5..6a8f8bc3 100644 --- a/lib/presentation/screens/glossary_hub_screen.dart +++ b/lib/presentation/screens/glossary_hub_screen.dart @@ -4,6 +4,7 @@ import '../../core/settings/settings_controller.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import 'agent_glossary_screen.dart'; +import 'charm_glossary_screen.dart'; import 'graffiti_glossary_screen.dart'; import 'music_kit_glossary_screen.dart'; import 'patch_glossary_screen.dart'; @@ -62,6 +63,11 @@ class GlossaryHubScreen extends StatelessWidget { title: 'Patches', buildScreen: () => PatchGlossaryScreen(repository: repository), ), + _GlossaryHubItem( + icon: Icons.key, + title: 'Charms', + buildScreen: () => CharmGlossaryScreen(repository: repository), + ), ]; return Scaffold( @@ -93,7 +99,10 @@ class GlossaryHubScreen extends StatelessWidget { children: [ Icon(item.icon), const SizedBox(width: 10), - Text(item.title, style: const TextStyle(fontSize: 18)), + Text( + item.title, + style: const TextStyle(fontSize: 18), + ), ], ), ), diff --git a/lib/presentation/screens/graffiti_box_open_screen.dart b/lib/presentation/screens/graffiti_box_open_screen.dart index ca4eec2d..60932ffa 100644 --- a/lib/presentation/screens/graffiti_box_open_screen.dart +++ b/lib/presentation/screens/graffiti_box_open_screen.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/graffiti_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/dropped_graffiti.dart'; @@ -22,12 +22,12 @@ import '../widgets/graffiti_grid_tile.dart'; import '../widgets/opening_roll_item_card.dart'; class GraffitiBoxOpenScreen extends StatefulWidget { - final CaseDto caseDto; + final ContainerDto containerDto; final LocalDataRepository repository; const GraffitiBoxOpenScreen({ super.key, - required this.caseDto, + required this.containerDto, required this.repository, }); @@ -49,7 +49,9 @@ class _GraffitiBoxOpenScreenState extends State { @override void initState() { super.initState(); - _graffitiFuture = widget.repository.loadGraffitiForCase(widget.caseDto.id); + _graffitiFuture = widget.repository.loadGraffitiForContainer( + widget.containerDto.id, + ); } @override @@ -88,7 +90,9 @@ class _GraffitiBoxOpenScreenState extends State { ) { final base = allGraffiti.where((g) => g.rarity == 'BASE_GRADE').toList(); final high = allGraffiti.where((g) => g.rarity == 'HIGH_GRADE').toList(); - final remarkable = allGraffiti.where((g) => g.rarity == 'REMARKABLE').toList(); + final remarkable = allGraffiti + .where((g) => g.rarity == 'REMARKABLE') + .toList(); final exotic = allGraffiti.where((g) => g.rarity == 'EXOTIC').toList(); return OpeningRollSequenceBuilder.build( @@ -99,7 +103,8 @@ class _GraffitiBoxOpenScreenState extends State { if (high.isNotEmpty) WeightedRollBucket(items: high, weight: 0.1598465), if (remarkable.isNotEmpty) WeightedRollBucket(items: remarkable, weight: 0.0319693), - if (exotic.isNotEmpty) WeightedRollBucket(items: exotic, weight: 0.0089515), + if (exotic.isNotEmpty) + WeightedRollBucket(items: exotic, weight: 0.0089515), ], nearWinnerBuckets: [ if (base.isNotEmpty) WeightedRollBucket(items: base, weight: 0.55), @@ -130,22 +135,24 @@ class _GraffitiBoxOpenScreenState extends State { @override Widget build(BuildContext context) { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( - widget.caseDto.releaseDate, + widget.containerDto.releaseDate, + ); + final color = SourceColorHelper.containerTypeColor( + widget.containerDto.type, ); - final color = SourceColorHelper.containerTypeColor(widget.caseDto.type); return Scaffold( - appBar: AppBar(title: Text(widget.caseDto.name)), + appBar: AppBar(title: Text(widget.containerDto.name)), body: CollectibleOpenBody( future: _graffitiFuture, sliverBuilder: (context, constraints, graffiti, gridCount, aspectRatio) { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.caseDto.caseImage, + assetPath: widget.containerDto.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ - ChipBadge(label: widget.caseDto.typeLabel, color: color), + ChipBadge(label: widget.containerDto.typeLabel, color: color), ], releaseDateText: formattedReleaseDate, description: diff --git a/lib/presentation/screens/graffiti_details_screen.dart b/lib/presentation/screens/graffiti_details_screen.dart index 7da853d3..b7ac5f9b 100644 --- a/lib/presentation/screens/graffiti_details_screen.dart +++ b/lib/presentation/screens/graffiti_details_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/graffiti_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; @@ -28,8 +28,8 @@ class GraffitiDetailsScreen extends StatelessWidget { return Scaffold( appBar: AppBar(title: Text(graffiti.name)), - body: FutureBuilder>( - future: repository.loadCasesForGraffiti(graffiti.id), + body: FutureBuilder>( + future: repository.loadContainersForGraffiti(graffiti.id), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); @@ -47,7 +47,7 @@ class GraffitiDetailsScreen extends StatelessWidget { ); } - final cases = snapshot.data ?? const []; + final cases = snapshot.data ?? const []; return ListView( padding: const EdgeInsets.all(12), @@ -70,25 +70,29 @@ class GraffitiDetailsScreen extends StatelessWidget { value: GraffitiUiHelper.rarityLabel(graffiti), ), if ((graffiti.collection ?? '').isNotEmpty) - DetailInfoRow(title: 'Collection', value: graffiti.collection!), + DetailInfoRow( + title: 'Collection', + value: graffiti.collection!, + ), ], ), const SizedBox(height: 12), - DetailSourceSection( + DetailSourceSection( title: 'Boxes', items: cases, emptyText: 'No graffiti box sources found.', itemBuilder: (item) => DetailSourceTile( - imagePath: item.caseImage, + imagePath: item.containerImage, title: item.name, subtitle: item.typeLabel, trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, AppNavigationHelper.buildContainerOpenScreen( - caseDto: item, + containerDto: item, repository: repository, ), ); diff --git a/lib/presentation/screens/graffiti_glossary_screen.dart b/lib/presentation/screens/graffiti_glossary_screen.dart index 6226035a..d394816b 100644 --- a/lib/presentation/screens/graffiti_glossary_screen.dart +++ b/lib/presentation/screens/graffiti_glossary_screen.dart @@ -21,6 +21,7 @@ class GraffitiGlossaryScreen extends StatefulWidget { class _GraffitiGlossaryScreenState extends State { String _rarityFilter = 'ALL'; + String _collectionFilter = 'ALL'; static const List _rarityOptions = [ GlossaryFilterOption('ALL', 'All rarities'), @@ -30,11 +31,30 @@ class _GraffitiGlossaryScreenState extends State { GlossaryFilterOption('EXOTIC', 'Exotic'), ]; + List _collectionOptions(List items) { + final values = + items + .map((item) => (item.collection ?? '').trim()) + .where((value) => value.isNotEmpty) + .toSet() + .toList() + ..sort(); + + return [ + const GlossaryFilterOption('ALL', 'All collections'), + ...values.map((value) => GlossaryFilterOption(value, value)), + ]; + } + List _filterAndSort(List items, String query) { final filtered = items.where((graffiti) { if (_rarityFilter != 'ALL' && graffiti.rarity != _rarityFilter) { return false; } + if (_collectionFilter != 'ALL' && + (graffiti.collection ?? '') != _collectionFilter) { + return false; + } if (query.isEmpty) return true; final haystack = [ graffiti.name, @@ -78,16 +98,35 @@ class _GraffitiGlossaryScreenState extends State { countLabelBuilder: (count) => '$count graffiti', emptyMessage: 'No graffiti found.', errorPrefix: 'Failed to load graffiti.', - headerControlsBuilder: (_) => [ - GlossaryFilterDropdown( - label: 'Rarity', - value: _rarityFilter, - options: _rarityOptions, - onChanged: (value) { - setState(() { - _rarityFilter = value ?? 'ALL'; - }); - }, + headerControlsBuilder: (_, items) => [ + Row( + children: [ + Expanded( + child: GlossaryFilterDropdown( + label: 'Rarity', + value: _rarityFilter, + options: _rarityOptions, + onChanged: (value) { + setState(() { + _rarityFilter = value ?? 'ALL'; + }); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GlossaryFilterDropdown( + label: 'Collection', + value: _collectionFilter, + options: _collectionOptions(items), + onChanged: (value) { + setState(() { + _collectionFilter = value ?? 'ALL'; + }); + }, + ), + ), + ], ), ], itemBuilder: (context, graffiti) { @@ -98,7 +137,10 @@ class _GraffitiGlossaryScreenState extends State { title: graffiti.name, subtitle: GraffitiUiHelper.secondaryText(graffiti), tags: [ - DetailTag(text: GraffitiUiHelper.rarityLabel(graffiti), color: color), + DetailTag( + text: GraffitiUiHelper.rarityLabel(graffiti), + color: color, + ), if ((graffiti.collection ?? '').isNotEmpty) DetailTag(text: graffiti.collection!), ], diff --git a/lib/presentation/screens/home_screen.dart b/lib/presentation/screens/home_screen.dart index 4a075c56..386c24b1 100644 --- a/lib/presentation/screens/home_screen.dart +++ b/lib/presentation/screens/home_screen.dart @@ -5,7 +5,8 @@ import '../../core/settings/settings_controller.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import 'agent_collection_list_screen.dart'; -import 'case_list_screen.dart'; +import 'container_list_screen.dart'; +import 'charm_collection_list_screen.dart'; import 'glossary_hub_screen.dart'; import 'operation_collection_list_screen.dart'; import 'patch_collection_list_screen.dart'; @@ -30,7 +31,7 @@ class HomeScreen extends StatelessWidget { _HomeMenuItem( icon: Icons.inventory_2, title: 'Open Containers', - buildScreen: () => CaseListScreen( + buildScreen: () => ContainerListScreen( repository: repository, settingsController: settingsController, ), @@ -51,7 +52,8 @@ class HomeScreen extends StatelessWidget { _HomeMenuItem( icon: Icons.collections_bookmark, title: 'Legacy Operation Collections', - buildScreen: () => OperationCollectionListScreen(repository: repository), + buildScreen: () => + OperationCollectionListScreen(repository: repository), ), _HomeMenuItem( icon: Icons.badge, @@ -68,6 +70,11 @@ class HomeScreen extends StatelessWidget { title: 'Patch Collections', buildScreen: () => PatchCollectionListScreen(repository: repository), ), + _HomeMenuItem( + icon: Icons.key, + title: 'Charm Collections', + buildScreen: () => CharmCollectionListScreen(repository: repository), + ), _HomeMenuItem( icon: Icons.swap_horiz, title: 'Trade-Up', @@ -92,39 +99,46 @@ class HomeScreen extends StatelessWidget { ), ], ), - body: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 420), - child: Padding( + body: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( padding: const EdgeInsets.all(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - for (int i = 0; i < menuItems.length; i++) ...[ - _menuButton( - context, - icon: menuItems[i].icon, - title: menuItems[i].title, - onTap: () { - AppNavigationHelper.pushScreen( + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 420, + minHeight: constraints.maxHeight - 32, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (int i = 0; i < menuItems.length; i++) ...[ + _menuButton( context, - menuItems[i].buildScreen(), - ); - }, - ), - if (i != menuItems.length - 1) const SizedBox(height: 16), - ], - const SizedBox(height: 20), - Text( - appVersion, - style: Theme.of(context).textTheme.bodySmall?.copyWith( + icon: menuItems[i].icon, + title: menuItems[i].title, + onTap: () { + AppNavigationHelper.pushScreen( + context, + menuItems[i].buildScreen(), + ); + }, + ), + if (i != menuItems.length - 1) const SizedBox(height: 16), + ], + const SizedBox(height: 20), + Text( + appVersion, + style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).hintColor, ), + ), + ], ), - ], + ), ), - ), - ), + ); + }, ), ); } @@ -145,7 +159,14 @@ class HomeScreen extends StatelessWidget { children: [ Icon(icon), const SizedBox(width: 10), - Text(title, style: const TextStyle(fontSize: 18)), + Flexible( + child: Text( + title, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 18), + ), + ), ], ), ), diff --git a/lib/presentation/screens/music_kit_box_open_screen.dart b/lib/presentation/screens/music_kit_box_open_screen.dart index bd9a47b6..0c2d7f5f 100644 --- a/lib/presentation/screens/music_kit_box_open_screen.dart +++ b/lib/presentation/screens/music_kit_box_open_screen.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/music_kit_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/dropped_music_kit.dart'; @@ -22,12 +22,12 @@ import '../widgets/music_kit_grid_tile.dart'; import '../widgets/opening_roll_item_card.dart'; class MusicKitBoxOpenScreen extends StatefulWidget { - final CaseDto caseDto; + final ContainerDto containerDto; final LocalDataRepository repository; const MusicKitBoxOpenScreen({ super.key, - required this.caseDto, + required this.containerDto, required this.repository, }); @@ -49,8 +49,8 @@ class _MusicKitBoxOpenScreenState extends State { @override void initState() { super.initState(); - _musicKitsFuture = widget.repository.loadMusicKitsForCase( - widget.caseDto.id, + _musicKitsFuture = widget.repository.loadMusicKitsForContainer( + widget.containerDto.id, ); } @@ -116,22 +116,24 @@ class _MusicKitBoxOpenScreenState extends State { @override Widget build(BuildContext context) { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( - widget.caseDto.releaseDate, + widget.containerDto.releaseDate, + ); + final color = SourceColorHelper.containerTypeColor( + widget.containerDto.type, ); - final color = SourceColorHelper.containerTypeColor(widget.caseDto.type); return Scaffold( - appBar: AppBar(title: Text(widget.caseDto.name)), + appBar: AppBar(title: Text(widget.containerDto.name)), body: CollectibleOpenBody( future: _musicKitsFuture, sliverBuilder: (context, constraints, musicKits, gridCount, aspectRatio) { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.caseDto.caseImage, + assetPath: widget.containerDto.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ - ChipBadge(label: widget.caseDto.typeLabel, color: color), + ChipBadge(label: widget.containerDto.typeLabel, color: color), ], releaseDateText: formattedReleaseDate, description: diff --git a/lib/presentation/screens/music_kit_details_screen.dart b/lib/presentation/screens/music_kit_details_screen.dart index 62c0e742..0849617d 100644 --- a/lib/presentation/screens/music_kit_details_screen.dart +++ b/lib/presentation/screens/music_kit_details_screen.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/music_kit_dto.dart'; +import '../../data/models/music_kit_group_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import '../helpers/music_kit_ui_helper.dart'; @@ -47,9 +48,10 @@ class MusicKitDetailsScreen extends StatelessWidget { ); } - final data = snapshot.data ?? - const _MusicKitDetailsData(variants: [], cases: []); - if (data.variants.isEmpty) { + final data = + snapshot.data ?? + const _MusicKitDetailsData(group: null, containers: []); + if (data.group == null) { return const Center( child: Padding( padding: EdgeInsets.all(24), @@ -61,10 +63,9 @@ class MusicKitDetailsScreen extends StatelessWidget { ); } - final primary = data.primary; + final group = data.group!; + final primary = group.primary; final rarityColor = MusicKitUiHelper.rarityColor(primary); - final hasRegular = data.variants.any((item) => !item.isStatTrak); - final hasStatTrak = data.variants.any((item) => item.isStatTrak); return ListView( padding: const EdgeInsets.all(12), @@ -72,22 +73,13 @@ class MusicKitDetailsScreen extends StatelessWidget { CollectibleDetailsCard( imagePath: primary.musicKitImage, title: primary.trackName, - subtitle: _subtitle( - primary, - hasRegular: hasRegular, - hasStatTrak: hasStatTrak, - ), + subtitle: MusicKitUiHelper.groupedSecondaryText(group), tags: [ DetailTag( text: MusicKitUiHelper.rarityLabel(primary), color: rarityColor, ), - DetailTag( - text: _typeLabel( - hasRegular: hasRegular, - hasStatTrak: hasStatTrak, - ), - ), + DetailTag(text: MusicKitUiHelper.groupedTypeLabel(group)), if ((primary.collection ?? '').isNotEmpty) DetailTag(text: primary.collection!), ], @@ -101,16 +93,13 @@ class MusicKitDetailsScreen extends StatelessWidget { ), DetailInfoRow( title: 'Type', - value: _typeLabel( - hasRegular: hasRegular, - hasStatTrak: hasStatTrak, - ), + value: MusicKitUiHelper.groupedTypeLabel(group), ), DetailInfoRow( title: 'StatTrakā„¢ variant', value: _statTrakAvailabilityLabel( - hasRegular: hasRegular, - hasStatTrak: hasStatTrak, + hasRegular: group.hasRegular, + hasStatTrak: group.hasStatTrak, ), ), if ((primary.collection ?? '').isNotEmpty) @@ -118,21 +107,22 @@ class MusicKitDetailsScreen extends StatelessWidget { ], ), const SizedBox(height: 12), - DetailSourceSection( + DetailSourceSection( title: 'Boxes', - items: data.cases, + items: data.containers, emptyText: 'No music kit box sources found.', itemBuilder: (item) => DetailSourceTile( - imagePath: item.caseImage, + imagePath: item.containerImage, title: item.name, subtitle: item.typeLabel, trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, AppNavigationHelper.buildContainerOpenScreen( - caseDto: item, + containerDto: item, repository: repository, ), ); @@ -147,28 +137,13 @@ class MusicKitDetailsScreen extends StatelessWidget { } Future<_MusicKitDetailsData> _loadData() async { - final allMusicKits = await repository.loadMusicKits(); - final variants = allMusicKits.where((musicKit) { - return musicKit.name == musicKitName && - (musicKit.collection ?? '') == (collection ?? ''); - }).toList(); - - final seenCaseIds = {}; - final cases = []; - for (final variant in variants) { - final sources = await repository.loadCasesForMusicKit(variant.id); - for (final item in sources) { - if (seenCaseIds.add(item.id)) { - cases.add(item); - } - } - } - cases.sort( - (a, b) => - (a.releaseDate ?? '9999-99-99').compareTo(b.releaseDate ?? '9999-99-99'), + final group = await repository.loadMusicKitGroup(musicKitName, collection); + final containers = await repository.loadContainersForMusicKitGroup( + musicKitName, + collection, ); - return _MusicKitDetailsData(variants: variants, cases: cases); + return _MusicKitDetailsData(group: group, containers: containers); } String _titleFromName(String fullName) { @@ -178,38 +153,12 @@ class MusicKitDetailsScreen extends StatelessWidget { musicKitImage: '', rarity: '', collection: null, - isStatTrak: false, + hasRegular: true, + hasStatTrak: false, ); return dto.trackName; } - String _subtitle( - MusicKitDto musicKit, { - required bool hasRegular, - required bool hasStatTrak, - }) { - final parts = [ - if ((musicKit.artist ?? '').isNotEmpty) musicKit.artist!, - _typeLabel(hasRegular: hasRegular, hasStatTrak: hasStatTrak), - if (hasRegular && hasStatTrak) 'Both variants', - if ((musicKit.collection ?? '').isNotEmpty) musicKit.collection!, - ]; - return parts.join(' | '); - } - - String _typeLabel({ - required bool hasRegular, - required bool hasStatTrak, - }) { - if (hasRegular && hasStatTrak) { - return 'Music Kit / StatTrakā„¢'; - } - if (hasStatTrak) { - return 'StatTrakā„¢ Music Kit'; - } - return 'Music Kit'; - } - String _statTrakAvailabilityLabel({ required bool hasRegular, required bool hasStatTrak, @@ -225,21 +174,8 @@ class MusicKitDetailsScreen extends StatelessWidget { } class _MusicKitDetailsData { - final List variants; - final List cases; + final MusicKitGroupDto? group; + final List containers; - const _MusicKitDetailsData({ - required this.variants, - required this.cases, - }); - - MusicKitDto get primary { - final sorted = [...variants]..sort((a, b) { - if (a.isStatTrak == b.isStatTrak) { - return a.id.compareTo(b.id); - } - return a.isStatTrak ? 1 : -1; - }); - return sorted.first; - } + const _MusicKitDetailsData({required this.group, required this.containers}); } diff --git a/lib/presentation/screens/music_kit_glossary_screen.dart b/lib/presentation/screens/music_kit_glossary_screen.dart index 0fcff860..969bf531 100644 --- a/lib/presentation/screens/music_kit_glossary_screen.dart +++ b/lib/presentation/screens/music_kit_glossary_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../data/models/music_kit_dto.dart'; +import '../../data/models/music_kit_group_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import '../helpers/music_kit_ui_helper.dart'; @@ -21,6 +21,7 @@ class MusicKitGlossaryScreen extends StatefulWidget { class _MusicKitGlossaryScreenState extends State { String _variantFilter = 'ALL'; + String _seriesFilter = 'ALL'; static const List _variantOptions = [ GlossaryFilterOption('ALL', 'All variants'), @@ -29,21 +30,27 @@ class _MusicKitGlossaryScreenState extends State { GlossaryFilterOption('BOTH', 'Both variants'), ]; - Future> _loadEntries() async { - final items = await widget.repository.loadMusicKits(); - final grouped = >{}; - for (final musicKit in items) { - final key = - '${musicKit.name.trim().toLowerCase()}|${(musicKit.collection ?? '').trim().toLowerCase()}'; - grouped.putIfAbsent(key, () => []).add(musicKit); - } - return grouped.values - .map((variants) => _MusicKitGlossaryEntry.fromVariants(variants)) - .toList(); + List _seriesOptions(List items) { + final values = + items + .map((item) => (item.collection ?? '').trim()) + .where((value) => value.isNotEmpty) + .toSet() + .toList() + ..sort(); + + return [ + const GlossaryFilterOption('ALL', 'All series'), + ...values.map((value) => GlossaryFilterOption(value, value)), + ]; + } + + Future> _loadEntries() { + return widget.repository.loadGroupedMusicKits(); } - List<_MusicKitGlossaryEntry> _filterAndSort( - List<_MusicKitGlossaryEntry> items, + List _filterAndSort( + List items, String query, ) { final filtered = items.where((entry) { @@ -59,6 +66,9 @@ class _MusicKitGlossaryScreenState extends State { !(entry.hasRegular && entry.hasStatTrak)) { return false; } + if (_seriesFilter != 'ALL' && (entry.collection ?? '') != _seriesFilter) { + return false; + } if (query.isEmpty) return true; final haystack = [ @@ -76,8 +86,9 @@ class _MusicKitGlossaryScreenState extends State { filtered.sort((a, b) { final rarityCompare = _rarityOrder(a).compareTo(_rarityOrder(b)); if (rarityCompare != 0) return rarityCompare; - final statTrakCompare = - a.hasStatTrak == b.hasStatTrak ? 0 : (a.hasStatTrak ? 1 : -1); + final statTrakCompare = a.hasStatTrak == b.hasStatTrak + ? 0 + : (a.hasStatTrak ? 1 : -1); if (statTrakCompare != 0) return statTrakCompare; return a.trackName.compareTo(b.trackName); }); @@ -85,7 +96,7 @@ class _MusicKitGlossaryScreenState extends State { return filtered; } - int _rarityOrder(_MusicKitGlossaryEntry musicKit) { + int _rarityOrder(MusicKitGroupDto musicKit) { switch (musicKit.rarity) { case 'HIGH_GRADE': return 0; @@ -96,7 +107,7 @@ class _MusicKitGlossaryScreenState extends State { @override Widget build(BuildContext context) { - return GenericGlossaryScreen<_MusicKitGlossaryEntry>( + return GenericGlossaryScreen( title: 'Music Kit Glossary', searchHint: 'Search by track, artist, or series...', future: _loadEntries(), @@ -104,16 +115,35 @@ class _MusicKitGlossaryScreenState extends State { countLabelBuilder: (count) => '$count music kits', emptyMessage: 'No music kits found.', errorPrefix: 'Failed to load music kits.', - headerControlsBuilder: (_) => [ - GlossaryFilterDropdown( - label: 'Variant', - value: _variantFilter, - options: _variantOptions, - onChanged: (value) { - setState(() { - _variantFilter = value ?? 'ALL'; - }); - }, + headerControlsBuilder: (_, items) => [ + Row( + children: [ + Expanded( + child: GlossaryFilterDropdown( + label: 'Variant', + value: _variantFilter, + options: _variantOptions, + onChanged: (value) { + setState(() { + _variantFilter = value ?? 'ALL'; + }); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GlossaryFilterDropdown( + label: 'Series', + value: _seriesFilter, + options: _seriesOptions(items), + onChanged: (value) { + setState(() { + _seriesFilter = value ?? 'ALL'; + }); + }, + ), + ), + ], ), ], itemBuilder: (context, musicKit) { @@ -122,13 +152,13 @@ class _MusicKitGlossaryScreenState extends State { accentColor: color, imagePath: musicKit.imagePath, title: musicKit.trackName, - subtitle: musicKit.subtitle, + subtitle: MusicKitUiHelper.groupedSecondaryText(musicKit), tags: [ DetailTag( text: MusicKitUiHelper.rarityLabel(musicKit.primary), color: color, ), - DetailTag(text: musicKit.typeLabel), + DetailTag(text: MusicKitUiHelper.groupedTypeLabel(musicKit)), if ((musicKit.collection ?? '').isNotEmpty) DetailTag(text: musicKit.collection!), ], @@ -147,68 +177,3 @@ class _MusicKitGlossaryScreenState extends State { ); } } - -class _MusicKitGlossaryEntry { - final String name; - final String trackName; - final String? artist; - final String? collection; - final String rarity; - final bool hasRegular; - final bool hasStatTrak; - final String imagePath; - final MusicKitDto primary; - - const _MusicKitGlossaryEntry({ - required this.name, - required this.trackName, - required this.artist, - required this.collection, - required this.rarity, - required this.hasRegular, - required this.hasStatTrak, - required this.imagePath, - required this.primary, - }); - - factory _MusicKitGlossaryEntry.fromVariants(List variants) { - final sorted = [...variants]..sort((a, b) { - if (a.isStatTrak == b.isStatTrak) { - return a.id.compareTo(b.id); - } - return a.isStatTrak ? 1 : -1; - }); - final primary = sorted.first; - return _MusicKitGlossaryEntry( - name: primary.name, - trackName: primary.trackName, - artist: primary.artist, - collection: primary.collection, - rarity: primary.rarity, - hasRegular: sorted.any((item) => !item.isStatTrak), - hasStatTrak: sorted.any((item) => item.isStatTrak), - imagePath: primary.musicKitImage, - primary: primary, - ); - } - - String get typeLabel { - if (hasRegular && hasStatTrak) { - return 'Music Kit / StatTrakā„¢'; - } - if (hasStatTrak) { - return 'StatTrakā„¢ Music Kit'; - } - return 'Music Kit'; - } - - String get subtitle { - final parts = [ - if ((artist ?? '').isNotEmpty) artist!, - typeLabel, - if (hasRegular && hasStatTrak) 'Both variants', - if ((collection ?? '').isNotEmpty) collection!, - ]; - return parts.join(' | '); - } -} diff --git a/lib/presentation/screens/operation_collection_list_screen.dart b/lib/presentation/screens/operation_collection_list_screen.dart index 20b45009..e79d4698 100644 --- a/lib/presentation/screens/operation_collection_list_screen.dart +++ b/lib/presentation/screens/operation_collection_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../data/models/operation_collection_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import '../helpers/source_color_helper.dart'; @@ -14,10 +14,7 @@ import 'operation_collection_open_screen.dart'; class OperationCollectionListScreen extends StatefulWidget { final LocalDataRepository repository; - const OperationCollectionListScreen({ - super.key, - required this.repository, - }); + const OperationCollectionListScreen({super.key, required this.repository}); @override State createState() => @@ -26,7 +23,7 @@ class OperationCollectionListScreen extends StatefulWidget { class _OperationCollectionListScreenState extends State { - late Future> _future; + late Future> _future; static const String _filterAll = 'ALL'; String _selectedFilter = _filterAll; @@ -37,11 +34,11 @@ class _OperationCollectionListScreenState _future = widget.repository.loadOperationCollections(); } - List _availableFilters(List all) { + List _availableFilters(List all) { final ids = {_filterAll}; for (final item in all) { - ids.add(item.operationId); + ids.add(item.sourceId ?? ''); } final ordered = [_filterAll]; @@ -69,26 +66,26 @@ class _OperationCollectionListScreenState return ordered; } - String _filterLabel(String id, List all) { + String _filterLabel(String id, List all) { if (id == _filterAll) return 'All'; - final match = all.cast().firstWhere( - (e) => e?.operationId == id, + final match = all.cast().firstWhere( + (e) => e?.sourceId == id, orElse: () => null, ); if (match != null) { - return match.operationName.replaceFirst('Operation ', ''); + return match.sourceLabel.replaceFirst('Operation ', ''); } return id; } - List _applyFilters(List all) { - var items = List.from(all); + List _applyFilters(List all) { + var items = List.from(all); if (_selectedFilter != _filterAll) { - items = items.where((e) => e.operationId == _selectedFilter).toList(); + items = items.where((e) => e.sourceId == _selectedFilter).toList(); } items.sort((a, b) { @@ -97,7 +94,7 @@ class _OperationCollectionListScreenState final byDate = ad.compareTo(bd); if (byDate != 0) return byDate; - final byOperation = a.operationName.compareTo(b.operationName); + final byOperation = a.sourceLabel.compareTo(b.sourceLabel); if (byOperation != 0) return byOperation; return a.name.compareTo(b.name); @@ -106,7 +103,7 @@ class _OperationCollectionListScreenState return items; } - Widget _buildFilterBar(List all) { + Widget _buildFilterBar(List all) { final filters = _availableFilters(all); if (_selectedFilter != _filterAll && !filters.contains(_selectedFilter)) { @@ -125,19 +122,14 @@ class _OperationCollectionListScreenState ); } - Widget _buildCard(BuildContext context, OperationCollectionDto collection) { - final color = SourceColorHelper.operationColor(collection.operationId); + Widget _buildCard(BuildContext context, ContainerDto collection) { + final color = SourceColorHelper.operationColor(collection.sourceId ?? ''); return CollectionListCard( - imagePath: collection.image, + imagePath: collection.containerImage, title: collection.name, releaseDate: collection.releaseDate, - chips: [ - ChipBadge( - label: collection.operationName, - color: color, - ), - ], + chips: [ChipBadge(label: collection.sourceLabel, color: color)], metadata: const [], onTap: () { AppNavigationHelper.pushScreen( @@ -154,15 +146,13 @@ class _OperationCollectionListScreenState @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Operation Collections'), - ), - body: AsyncCollectionLoader( + appBar: AppBar(title: const Text('Operation Collections')), + body: AsyncCollectionLoader( future: _future, builder: (context, all) { final visible = _applyFilters(all); - return ResponsiveCollectionGrid( + return ResponsiveCollectionGrid( items: visible, emptyMessage: 'No operation collections found.', header: _buildFilterBar(all), diff --git a/lib/presentation/screens/operation_collection_open_screen.dart b/lib/presentation/screens/operation_collection_open_screen.dart index f19b58e4..8149feb4 100644 --- a/lib/presentation/screens/operation_collection_open_screen.dart +++ b/lib/presentation/screens/operation_collection_open_screen.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/operation_collection_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/skin_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/dropped_skin.dart'; @@ -20,7 +20,7 @@ import '../widgets/skin_grid_tile.dart'; import '../widgets/source_badge.dart'; class OperationCollectionOpenScreen extends StatefulWidget { - final OperationCollectionDto collection; + final ContainerDto collection; final LocalDataRepository repository; const OperationCollectionOpenScreen({ @@ -53,7 +53,7 @@ class _OperationCollectionOpenScreenState } Color get _operationColor => - SourceColorHelper.operationColor(widget.collection.operationId); + SourceColorHelper.operationColor(widget.collection.sourceId ?? ''); Future _openCollection(List skins) async { await CollectibleOpenFlowHelper.runReveal( @@ -98,11 +98,11 @@ class _OperationCollectionOpenScreenState return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.collection.image, + assetPath: widget.collection.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ SourceBadge( - label: widget.collection.operationName, + label: widget.collection.sourceLabel, color: _operationColor, ), ], diff --git a/lib/presentation/screens/patch_collection_list_screen.dart b/lib/presentation/screens/patch_collection_list_screen.dart index 03994f3f..6df4f1af 100644 --- a/lib/presentation/screens/patch_collection_list_screen.dart +++ b/lib/presentation/screens/patch_collection_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import '../helpers/source_color_helper.dart'; @@ -21,7 +21,7 @@ class PatchCollectionListScreen extends StatefulWidget { } class _PatchCollectionListScreenState extends State { - late Future> _future; + late Future> _future; @override void initState() { @@ -29,7 +29,7 @@ class _PatchCollectionListScreenState extends State { _future = widget.repository.loadPatchCollections(); } - Widget _buildCard(BuildContext context, CaseDto collection) { + Widget _buildCard(BuildContext context, ContainerDto collection) { final typeColor = SourceColorHelper.containerTypeColor(collection.type); final sourceColor = SourceColorHelper.collectibleSourceColor( collection.sourceType, @@ -37,7 +37,7 @@ class _PatchCollectionListScreenState extends State { ); return CollectionListCard( - imagePath: collection.caseImage, + imagePath: collection.containerImage, title: collection.name, releaseDate: collection.releaseDate, chips: [ @@ -74,10 +74,10 @@ class _PatchCollectionListScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Patch Collections')), - body: AsyncCollectionLoader( + body: AsyncCollectionLoader( future: _future, builder: (context, items) { - return ResponsiveCollectionGrid( + return ResponsiveCollectionGrid( items: items, emptyMessage: 'No patch collections found.', itemBuilder: _buildCard, diff --git a/lib/presentation/screens/patch_collection_open_screen.dart b/lib/presentation/screens/patch_collection_open_screen.dart index 1cd9dbd0..034d2354 100644 --- a/lib/presentation/screens/patch_collection_open_screen.dart +++ b/lib/presentation/screens/patch_collection_open_screen.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/patch_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/dropped_patch.dart'; @@ -20,7 +20,7 @@ import '../widgets/patch_drop_card.dart'; import '../widgets/patch_grid_tile.dart'; class PatchCollectionOpenScreen extends StatefulWidget { - final CaseDto collection; + final ContainerDto collection; final LocalDataRepository repository; const PatchCollectionOpenScreen({ @@ -45,7 +45,9 @@ class _PatchCollectionOpenScreenState extends State { @override void initState() { super.initState(); - _patchesFuture = widget.repository.loadPatchesForCase(widget.collection.id); + _patchesFuture = widget.repository.loadPatchesForContainer( + widget.collection.id, + ); } Future _openCollection(List patches) async { @@ -72,7 +74,9 @@ class _PatchCollectionOpenScreenState extends State { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( widget.collection.releaseDate, ); - final typeColor = SourceColorHelper.containerTypeColor(widget.collection.type); + final typeColor = SourceColorHelper.containerTypeColor( + widget.collection.type, + ); final sourceColor = SourceColorHelper.collectibleSourceColor( widget.collection.sourceType, widget.collection.sourceId, @@ -86,10 +90,13 @@ class _PatchCollectionOpenScreenState extends State { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.collection.caseImage, + assetPath: widget.collection.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ - ChipBadge(label: widget.collection.typeLabel, color: typeColor), + ChipBadge( + label: widget.collection.typeLabel, + color: typeColor, + ), if (widget.collection.sourceTypeLabel != null) ChipBadge( label: widget.collection.sourceTypeLabel!, @@ -112,8 +119,9 @@ class _PatchCollectionOpenScreenState extends State { releaseDateText: formattedReleaseDate, description: 'Patch collections open like reward collections: no roulette, just the final reveal.', - buttonLabel: - _isOpening ? 'OPENING...' : 'OPEN PATCH COLLECTION', + buttonLabel: _isOpening + ? 'OPENING...' + : 'OPEN PATCH COLLECTION', onPressed: (_isOpening || patches.isEmpty) ? null : () => _openCollection(patches), diff --git a/lib/presentation/screens/patch_container_open_screen.dart b/lib/presentation/screens/patch_container_open_screen.dart index e489a09d..3829541c 100644 --- a/lib/presentation/screens/patch_container_open_screen.dart +++ b/lib/presentation/screens/patch_container_open_screen.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/patch_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/dropped_patch.dart'; @@ -22,17 +22,18 @@ import '../widgets/patch_drop_card.dart'; import '../widgets/patch_grid_tile.dart'; class PatchContainerOpenScreen extends StatefulWidget { - final CaseDto caseDto; + final ContainerDto containerDto; final LocalDataRepository repository; const PatchContainerOpenScreen({ super.key, - required this.caseDto, + required this.containerDto, required this.repository, }); @override - State createState() => _PatchContainerOpenScreenState(); + State createState() => + _PatchContainerOpenScreenState(); } class _PatchContainerOpenScreenState extends State { @@ -49,7 +50,9 @@ class _PatchContainerOpenScreenState extends State { @override void initState() { super.initState(); - _patchesFuture = widget.repository.loadPatchesForCase(widget.caseDto.id); + _patchesFuture = widget.repository.loadPatchesForContainer( + widget.containerDto.id, + ); } @override @@ -87,7 +90,9 @@ class _PatchContainerOpenScreenState extends State { DroppedPatch drop, ) { final high = allPatches.where((p) => p.rarity == 'HIGH_GRADE').toList(); - final remarkable = allPatches.where((p) => p.rarity == 'REMARKABLE').toList(); + final remarkable = allPatches + .where((p) => p.rarity == 'REMARKABLE') + .toList(); final exotic = allPatches.where((p) => p.rarity == 'EXOTIC').toList(); return OpeningRollSequenceBuilder.build( @@ -97,7 +102,8 @@ class _PatchContainerOpenScreenState extends State { if (high.isNotEmpty) WeightedRollBucket(items: high, weight: 0.7992327), if (remarkable.isNotEmpty) WeightedRollBucket(items: remarkable, weight: 0.1598465), - if (exotic.isNotEmpty) WeightedRollBucket(items: exotic, weight: 0.0409208), + if (exotic.isNotEmpty) + WeightedRollBucket(items: exotic, weight: 0.0409208), ], nearWinnerBuckets: [ if (high.isNotEmpty) WeightedRollBucket(items: high, weight: 0.60), @@ -127,22 +133,24 @@ class _PatchContainerOpenScreenState extends State { @override Widget build(BuildContext context) { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( - widget.caseDto.releaseDate, + widget.containerDto.releaseDate, + ); + final color = SourceColorHelper.containerTypeColor( + widget.containerDto.type, ); - final color = SourceColorHelper.containerTypeColor(widget.caseDto.type); return Scaffold( - appBar: AppBar(title: Text(widget.caseDto.name)), + appBar: AppBar(title: Text(widget.containerDto.name)), body: CollectibleOpenBody( future: _patchesFuture, sliverBuilder: (context, constraints, patches, gridCount, aspectRatio) { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.caseDto.caseImage, + assetPath: widget.containerDto.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ - ChipBadge(label: widget.caseDto.typeLabel, color: color), + ChipBadge(label: widget.containerDto.typeLabel, color: color), ], releaseDateText: formattedReleaseDate, description: diff --git a/lib/presentation/screens/patch_details_screen.dart b/lib/presentation/screens/patch_details_screen.dart index c16e54a8..7b50ccb9 100644 --- a/lib/presentation/screens/patch_details_screen.dart +++ b/lib/presentation/screens/patch_details_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/patch_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; @@ -28,8 +28,8 @@ class PatchDetailsScreen extends StatelessWidget { return Scaffold( appBar: AppBar(title: Text(patch.name)), - body: FutureBuilder>( - future: repository.loadCasesForPatch(patch.id), + body: FutureBuilder>( + future: repository.loadContainersForPatch(patch.id), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); @@ -47,7 +47,7 @@ class PatchDetailsScreen extends StatelessWidget { ); } - final cases = snapshot.data ?? const []; + final cases = snapshot.data ?? const []; return ListView( padding: const EdgeInsets.all(12), @@ -70,25 +70,29 @@ class PatchDetailsScreen extends StatelessWidget { value: PatchUiHelper.rarityLabel(patch), ), if ((patch.collection ?? '').isNotEmpty) - DetailInfoRow(title: 'Collection', value: patch.collection!), + DetailInfoRow( + title: 'Collection', + value: patch.collection!, + ), ], ), const SizedBox(height: 12), - DetailSourceSection( + DetailSourceSection( title: 'Sources', items: cases, emptyText: 'No patch sources found.', itemBuilder: (item) => DetailSourceTile( - imagePath: item.caseImage, + imagePath: item.containerImage, title: item.name, subtitle: item.typeLabel, trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, AppNavigationHelper.buildContainerOpenScreen( - caseDto: item, + containerDto: item, repository: repository, ), ); diff --git a/lib/presentation/screens/patch_glossary_screen.dart b/lib/presentation/screens/patch_glossary_screen.dart index 82d1ecc2..f8fcc4f8 100644 --- a/lib/presentation/screens/patch_glossary_screen.dart +++ b/lib/presentation/screens/patch_glossary_screen.dart @@ -21,6 +21,7 @@ class PatchGlossaryScreen extends StatefulWidget { class _PatchGlossaryScreenState extends State { String _rarityFilter = 'ALL'; + String _collectionFilter = 'ALL'; static const List _rarityOptions = [ GlossaryFilterOption('ALL', 'All rarities'), @@ -29,15 +30,36 @@ class _PatchGlossaryScreenState extends State { GlossaryFilterOption('EXOTIC', 'Exotic'), ]; + List _collectionOptions(List items) { + final values = + items + .map((item) => (item.collection ?? '').trim()) + .where((value) => value.isNotEmpty) + .toSet() + .toList() + ..sort(); + + return [ + const GlossaryFilterOption('ALL', 'All collections'), + ...values.map((value) => GlossaryFilterOption(value, value)), + ]; + } + List _filterAndSort(List items, String query) { final filtered = items.where((patch) { if (_rarityFilter != 'ALL' && patch.rarity != _rarityFilter) { return false; } + if (_collectionFilter != 'ALL' && + (patch.collection ?? '') != _collectionFilter) { + return false; + } if (query.isEmpty) return true; - final haystack = [patch.name, patch.collection ?? '', patch.rarity] - .join(' ') - .toLowerCase(); + final haystack = [ + patch.name, + patch.collection ?? '', + patch.rarity, + ].join(' ').toLowerCase(); return haystack.contains(query); }).toList(); @@ -73,16 +95,35 @@ class _PatchGlossaryScreenState extends State { countLabelBuilder: (count) => '$count patches', emptyMessage: 'No patches found.', errorPrefix: 'Failed to load patches.', - headerControlsBuilder: (_) => [ - GlossaryFilterDropdown( - label: 'Rarity', - value: _rarityFilter, - options: _rarityOptions, - onChanged: (value) { - setState(() { - _rarityFilter = value ?? 'ALL'; - }); - }, + headerControlsBuilder: (_, items) => [ + Row( + children: [ + Expanded( + child: GlossaryFilterDropdown( + label: 'Rarity', + value: _rarityFilter, + options: _rarityOptions, + onChanged: (value) { + setState(() { + _rarityFilter = value ?? 'ALL'; + }); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GlossaryFilterDropdown( + label: 'Collection', + value: _collectionFilter, + options: _collectionOptions(items), + onChanged: (value) { + setState(() { + _collectionFilter = value ?? 'ALL'; + }); + }, + ), + ), + ], ), ], itemBuilder: (context, patch) { diff --git a/lib/presentation/screens/pin_container_open_screen.dart b/lib/presentation/screens/pin_container_open_screen.dart index 3b2a0274..6764c055 100644 --- a/lib/presentation/screens/pin_container_open_screen.dart +++ b/lib/presentation/screens/pin_container_open_screen.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/pin_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/dropped_pin.dart'; @@ -22,12 +22,12 @@ import '../widgets/pin_drop_card.dart'; import '../widgets/pin_grid_tile.dart'; class PinContainerOpenScreen extends StatefulWidget { - final CaseDto caseDto; + final ContainerDto containerDto; final LocalDataRepository repository; const PinContainerOpenScreen({ super.key, - required this.caseDto, + required this.containerDto, required this.repository, }); @@ -49,7 +49,9 @@ class _PinContainerOpenScreenState extends State { @override void initState() { super.initState(); - _pinsFuture = widget.repository.loadPinsForCase(widget.caseDto.id); + _pinsFuture = widget.repository.loadPinsForContainer( + widget.containerDto.id, + ); } @override @@ -138,22 +140,24 @@ class _PinContainerOpenScreenState extends State { @override Widget build(BuildContext context) { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( - widget.caseDto.releaseDate, + widget.containerDto.releaseDate, + ); + final color = SourceColorHelper.containerTypeColor( + widget.containerDto.type, ); - final color = SourceColorHelper.containerTypeColor(widget.caseDto.type); return Scaffold( - appBar: AppBar(title: Text(widget.caseDto.name)), + appBar: AppBar(title: Text(widget.containerDto.name)), body: CollectibleOpenBody( future: _pinsFuture, sliverBuilder: (context, constraints, pins, gridCount, aspectRatio) { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.caseDto.caseImage, + assetPath: widget.containerDto.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ - ChipBadge(label: widget.caseDto.typeLabel, color: color), + ChipBadge(label: widget.containerDto.typeLabel, color: color), ], releaseDateText: formattedReleaseDate, description: 'Pin capsules roll collectible pins only.', @@ -168,11 +172,8 @@ class _PinContainerOpenScreenState extends State { items: _rollSequence, winningIndex: _winningIndex, isRolling: _isOpening, - itemBuilder: (pin, isWinner, itemWidth) => _buildRollItem( - pin, - isWinner: isWinner, - itemWidth: itemWidth, - ), + itemBuilder: (pin, isWinner, itemWidth) => + _buildRollItem(pin, isWinner: isWinner, itemWidth: itemWidth), ), if (_dropped != null) SliverToBoxAdapter(child: PinDropCard(drop: _dropped!)), diff --git a/lib/presentation/screens/pin_details_screen.dart b/lib/presentation/screens/pin_details_screen.dart index 7e3921e3..3c81f57c 100644 --- a/lib/presentation/screens/pin_details_screen.dart +++ b/lib/presentation/screens/pin_details_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/pin_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; @@ -28,8 +28,8 @@ class PinDetailsScreen extends StatelessWidget { return Scaffold( appBar: AppBar(title: Text(pin.name)), - body: FutureBuilder>( - future: repository.loadCasesForPin(pin.id), + body: FutureBuilder>( + future: repository.loadContainersForPin(pin.id), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); @@ -47,7 +47,7 @@ class PinDetailsScreen extends StatelessWidget { ); } - final cases = snapshot.data ?? const []; + final cases = snapshot.data ?? const []; return ListView( padding: const EdgeInsets.all(12), @@ -57,31 +57,39 @@ class PinDetailsScreen extends StatelessWidget { title: pin.name, subtitle: PinUiHelper.secondaryText(pin), tags: [ - DetailTag(text: PinUiHelper.rarityLabel(pin), color: rarityColor), - if ((pin.collection ?? '').isNotEmpty) DetailTag(text: pin.collection!), + DetailTag( + text: PinUiHelper.rarityLabel(pin), + color: rarityColor, + ), + if ((pin.collection ?? '').isNotEmpty) + DetailTag(text: pin.collection!), ], infoRows: [ - DetailInfoRow(title: 'Rarity', value: PinUiHelper.rarityLabel(pin)), + DetailInfoRow( + title: 'Rarity', + value: PinUiHelper.rarityLabel(pin), + ), if ((pin.collection ?? '').isNotEmpty) DetailInfoRow(title: 'Collection', value: pin.collection!), ], ), const SizedBox(height: 12), - DetailSourceSection( + DetailSourceSection( title: 'Containers', items: cases, emptyText: 'No pin capsule sources found.', itemBuilder: (item) => DetailSourceTile( - imagePath: item.caseImage, + imagePath: item.containerImage, title: item.name, subtitle: item.typeLabel, trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, AppNavigationHelper.buildContainerOpenScreen( - caseDto: item, + containerDto: item, repository: repository, ), ); diff --git a/lib/presentation/screens/pin_glossary_screen.dart b/lib/presentation/screens/pin_glossary_screen.dart index 3e5da7b4..3caa8ea8 100644 --- a/lib/presentation/screens/pin_glossary_screen.dart +++ b/lib/presentation/screens/pin_glossary_screen.dart @@ -21,6 +21,7 @@ class PinGlossaryScreen extends StatefulWidget { class _PinGlossaryScreenState extends State { String _rarityFilter = 'ALL'; + String _collectionFilter = 'ALL'; static const List _rarityOptions = [ GlossaryFilterOption('ALL', 'All rarities'), @@ -31,15 +32,36 @@ class _PinGlossaryScreenState extends State { GlossaryFilterOption('EXTRAORDINARY', 'Extraordinary'), ]; + List _collectionOptions(List items) { + final values = + items + .map((item) => (item.collection ?? '').trim()) + .where((value) => value.isNotEmpty) + .toSet() + .toList() + ..sort(); + + return [ + const GlossaryFilterOption('ALL', 'All collections'), + ...values.map((value) => GlossaryFilterOption(value, value)), + ]; + } + List _filterAndSort(List items, String query) { final filtered = items.where((pin) { if (_rarityFilter != 'ALL' && pin.rarity != _rarityFilter) { return false; } + if (_collectionFilter != 'ALL' && + (pin.collection ?? '') != _collectionFilter) { + return false; + } if (query.isEmpty) return true; - final haystack = [pin.name, pin.collection ?? '', pin.rarity] - .join(' ') - .toLowerCase(); + final haystack = [ + pin.name, + pin.collection ?? '', + pin.rarity, + ].join(' ').toLowerCase(); return haystack.contains(query); }).toList(); @@ -79,16 +101,35 @@ class _PinGlossaryScreenState extends State { countLabelBuilder: (count) => '$count pins', emptyMessage: 'No pins found.', errorPrefix: 'Failed to load pins.', - headerControlsBuilder: (_) => [ - GlossaryFilterDropdown( - label: 'Rarity', - value: _rarityFilter, - options: _rarityOptions, - onChanged: (value) { - setState(() { - _rarityFilter = value ?? 'ALL'; - }); - }, + headerControlsBuilder: (_, items) => [ + Row( + children: [ + Expanded( + child: GlossaryFilterDropdown( + label: 'Rarity', + value: _rarityFilter, + options: _rarityOptions, + onChanged: (value) { + setState(() { + _rarityFilter = value ?? 'ALL'; + }); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GlossaryFilterDropdown( + label: 'Collection', + value: _collectionFilter, + options: _collectionOptions(items), + onChanged: (value) { + setState(() { + _collectionFilter = value ?? 'ALL'; + }); + }, + ), + ), + ], ), ], itemBuilder: (context, pin) { diff --git a/lib/presentation/screens/reward_collection_list_screen.dart b/lib/presentation/screens/reward_collection_list_screen.dart index 0c06603b..7556a5e8 100644 --- a/lib/presentation/screens/reward_collection_list_screen.dart +++ b/lib/presentation/screens/reward_collection_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../data/models/reward_collection_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import '../helpers/source_color_helper.dart'; @@ -14,10 +14,7 @@ import 'reward_collection_open_screen.dart'; class RewardCollectionListScreen extends StatefulWidget { final LocalDataRepository repository; - const RewardCollectionListScreen({ - super.key, - required this.repository, - }); + const RewardCollectionListScreen({super.key, required this.repository}); @override State createState() => @@ -26,7 +23,7 @@ class RewardCollectionListScreen extends StatefulWidget { class _RewardCollectionListScreenState extends State { - late Future> _future; + late Future> _future; static const String _filterAll = 'ALL'; static const String _filterOperation = 'OPERATION'; @@ -53,13 +50,13 @@ class _RewardCollectionListScreenState } } - List _applyFilters(List all) { - var items = List.from(all); + List _applyFilters(List all) { + var items = List.from(all); if (_selectedFilter == _filterOperation) { - items = items.where((e) => e.isOperation).toList(); + items = items.where((e) => e.isOperationRewardCollection).toList(); } else if (_selectedFilter == _filterArmory) { - items = items.where((e) => e.isArmory).toList(); + items = items.where((e) => e.isArmoryRewardCollection).toList(); } items.sort((a, b) { @@ -88,18 +85,18 @@ class _RewardCollectionListScreenState ); } - Widget _buildCard(BuildContext context, RewardCollectionDto collection) { + Widget _buildCard(BuildContext context, ContainerDto collection) { final color = SourceColorHelper.rewardSourceColor( - isArmory: collection.isArmory, + isArmory: collection.isArmoryRewardCollection, ); return CollectionListCard( - imagePath: collection.image, + imagePath: collection.containerImage, title: collection.name, releaseDate: collection.releaseDate, chips: [ ChipBadge( - label: collection.isArmory ? 'Armory' : 'Operation', + label: collection.isArmoryRewardCollection ? 'Armory' : 'Operation', color: color, ), ChipBadge( @@ -133,15 +130,13 @@ class _RewardCollectionListScreenState @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Operation / Armory Rewards'), - ), - body: AsyncCollectionLoader( + appBar: AppBar(title: const Text('Operation / Armory Rewards')), + body: AsyncCollectionLoader( future: _future, builder: (context, all) { final visible = _applyFilters(all); - return ResponsiveCollectionGrid( + return ResponsiveCollectionGrid( items: visible, emptyMessage: 'No reward collections found.', header: _buildFilterBar(), diff --git a/lib/presentation/screens/reward_collection_open_screen.dart b/lib/presentation/screens/reward_collection_open_screen.dart index 58a5084d..30243894 100644 --- a/lib/presentation/screens/reward_collection_open_screen.dart +++ b/lib/presentation/screens/reward_collection_open_screen.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/reward_collection_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/skin_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/dropped_skin.dart'; @@ -20,7 +20,7 @@ import '../widgets/skin_grid_tile.dart'; import '../widgets/source_badge.dart'; class RewardCollectionOpenScreen extends StatefulWidget { - final RewardCollectionDto collection; + final ContainerDto collection; final LocalDataRepository repository; const RewardCollectionOpenScreen({ @@ -53,7 +53,7 @@ class _RewardCollectionOpenScreenState } Color get _sourceColor => SourceColorHelper.rewardSourceColor( - isArmory: widget.collection.isArmory, + isArmory: widget.collection.isArmoryRewardCollection, ); Future _openReward(List skins) async { @@ -99,11 +99,11 @@ class _RewardCollectionOpenScreenState return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.collection.image, + assetPath: widget.collection.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ SourceBadge( - label: widget.collection.isArmory + label: widget.collection.isArmoryRewardCollection ? 'Armory Reward' : 'Operation Reward', color: _sourceColor, @@ -137,7 +137,7 @@ class _RewardCollectionOpenScreenState if (_isOpening) SliverToBoxAdapter( child: OpeningLoadingCard( - title: widget.collection.isArmory + title: widget.collection.isArmoryRewardCollection ? 'Opening armory reward...' : 'Opening operation reward...', ), diff --git a/lib/presentation/screens/settings_screen.dart b/lib/presentation/screens/settings_screen.dart index a1dea390..8a6b3208 100644 --- a/lib/presentation/screens/settings_screen.dart +++ b/lib/presentation/screens/settings_screen.dart @@ -7,10 +7,7 @@ import '../../core/settings/settings_controller.dart'; class SettingsScreen extends StatelessWidget { final SettingsController settingsController; - const SettingsScreen({ - super.key, - required this.settingsController, - }); + const SettingsScreen({super.key, required this.settingsController}); @override Widget build(BuildContext context) { @@ -18,9 +15,7 @@ class SettingsScreen extends StatelessWidget { animation: settingsController, builder: (context, _) { return Scaffold( - appBar: AppBar( - title: const Text('Settings'), - ), + appBar: AppBar(title: const Text('Settings')), body: ListView( children: [ SwitchListTile( @@ -76,7 +71,7 @@ class SettingsScreen extends StatelessWidget { height: 52, color: theme.colorScheme.primaryContainer, child: Image.asset( - 'assets/app_icon/latest_case.png', + 'assets/app_icon/latest_container.png', fit: BoxFit.cover, ), ), @@ -184,11 +179,7 @@ class _AboutInfoTile extends StatelessWidget { borderRadius: BorderRadius.circular(10), ), alignment: Alignment.center, - child: Icon( - icon, - size: 20, - color: theme.colorScheme.primary, - ), + child: Icon(icon, size: 20, color: theme.colorScheme.primary), ), const SizedBox(width: 12), Expanded( @@ -202,10 +193,7 @@ class _AboutInfoTile extends StatelessWidget { ), ), const SizedBox(height: 4), - Text( - value, - style: theme.textTheme.bodyLarge, - ), + Text(value, style: theme.textTheme.bodyLarge), if (helperText != null) ...[ const SizedBox(height: 4), Text( diff --git a/lib/presentation/screens/skin_details_screen.dart b/lib/presentation/screens/skin_details_screen.dart index 20d9f877..43e5676e 100644 --- a/lib/presentation/screens/skin_details_screen.dart +++ b/lib/presentation/screens/skin_details_screen.dart @@ -2,9 +2,7 @@ import 'package:flutter/material.dart'; import '../../core/settings/settings_controller.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; -import '../../data/models/operation_collection_dto.dart'; -import '../../data/models/reward_collection_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/skin_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; @@ -34,16 +32,12 @@ class SkinDetailsScreen extends StatelessWidget { final rarityColor = SkinUiHelper.rarityColor(skin); return Scaffold( - appBar: AppBar( - title: Text(skin.itemDisplayName), - ), + appBar: AppBar(title: Text(skin.itemDisplayName)), body: FutureBuilder<_SkinSourcesData>( future: _loadData(), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { - return const Center( - child: CircularProgressIndicator(), - ); + return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { @@ -58,9 +52,10 @@ class SkinDetailsScreen extends StatelessWidget { ); } - final data = snapshot.data ?? + final data = + snapshot.data ?? const _SkinSourcesData( - cases: [], + containers: [], rewardCollections: [], operationCollections: [], ); @@ -87,10 +82,8 @@ class SkinDetailsScreen extends StatelessWidget { isAntiAlias: false, gaplessPlayback: true, cacheWidth: narrow ? 420 : 640, - errorBuilder: (_, _, _) => const Icon( - Icons.image_not_supported, - size: 64, - ), + errorBuilder: (_, _, _) => + const Icon(Icons.image_not_supported, size: 64), ), ); @@ -101,8 +94,9 @@ class SkinDetailsScreen extends StatelessWidget { children: [ Text( skin.itemDisplayName, - textAlign: - narrow ? TextAlign.center : TextAlign.left, + textAlign: narrow + ? TextAlign.center + : TextAlign.left, style: const TextStyle( fontSize: 22, fontWeight: FontWeight.bold, @@ -111,8 +105,9 @@ class SkinDetailsScreen extends StatelessWidget { const SizedBox(height: 6), Text( SkinUiHelper.secondaryText(skin), - textAlign: - narrow ? TextAlign.center : TextAlign.left, + textAlign: narrow + ? TextAlign.center + : TextAlign.left, style: const TextStyle( color: Colors.white70, fontSize: 16, @@ -130,8 +125,8 @@ class SkinDetailsScreen extends StatelessWidget { _tag( skin.itemKind == 'WEAPON' ? SkinUiHelper.weaponTypeLabel( - skin.weaponType, - ) + skin.weaponType, + ) : skin.itemKind == 'KNIFE' ? 'Knife' : 'Gloves', @@ -151,45 +146,26 @@ class SkinDetailsScreen extends StatelessWidget { skin.finishCatalogName!, ), if ((skin.variantName ?? '').isNotEmpty) - _infoRow( - 'Variant', - skin.variantName!, - ), + _infoRow('Variant', skin.variantName!), if ((skin.phase ?? '').isNotEmpty) - _infoRow( - 'Phase', - skin.phase!, - ), + _infoRow('Phase', skin.phase!), if ((skin.apiPaintIndex ?? '').isNotEmpty) - _infoRow( - 'Paint index', - skin.apiPaintIndex!, - ), + _infoRow('Paint index', skin.apiPaintIndex!), ], ); if (narrow) { return Column( - children: [ - image, - const SizedBox(height: 16), - info, - ], + children: [image, const SizedBox(height: 16), info], ); } return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - flex: 4, - child: image, - ), + Expanded(flex: 4, child: image), const SizedBox(width: 16), - Expanded( - flex: 5, - child: info, - ), + Expanded(flex: 5, child: info), ], ); }, @@ -219,31 +195,29 @@ class SkinDetailsScreen extends StatelessWidget { const SizedBox(height: 10), const Text( 'Red segment shows the valid float range for this skin.', - style: TextStyle( - color: Colors.white70, - fontSize: 13, - ), + style: TextStyle(color: Colors.white70, fontSize: 13), ), ], ), ), ), const SizedBox(height: 12), - _sourceSection( + _sourceSection( title: 'Cases / Containers', - items: data.cases, + items: data.containers, emptyText: 'This skin is not present in case/container data.', itemBuilder: (item) => _sourceTile( - imagePath: item.caseImage, + imagePath: item.containerImage, title: item.name, subtitle: item.typeLabel, trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, AppNavigationHelper.buildContainerOpenScreen( - caseDto: item, + containerDto: item, repository: repository, settingsController: settingsController, ), @@ -252,16 +226,17 @@ class SkinDetailsScreen extends StatelessWidget { ), ), const SizedBox(height: 12), - _sourceSection( + _sourceSection( title: 'Reward Collections / Armory', items: data.rewardCollections, emptyText: 'No reward collection sources found.', itemBuilder: (item) => _sourceTile( - imagePath: item.image, + imagePath: item.containerImage, title: item.name, subtitle: _rewardCollectionSubtitle(item), trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, @@ -274,16 +249,17 @@ class SkinDetailsScreen extends StatelessWidget { ), ), const SizedBox(height: 12), - _sourceSection( + _sourceSection( title: 'Legacy Operation Collections', items: data.operationCollections, emptyText: 'No legacy operation collection sources found.', itemBuilder: (item) => _sourceTile( - imagePath: item.image, + imagePath: item.containerImage, title: item.name, subtitle: item.operationLabel, trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, @@ -304,15 +280,15 @@ class SkinDetailsScreen extends StatelessWidget { Future<_SkinSourcesData> _loadData() async { final results = await Future.wait([ - repository.loadCasesForSkin(skin.id), + repository.loadContainersForSkin(skin.id), repository.loadRewardCollectionsForSkin(skin.id), repository.loadOperationCollectionsForSkin(skin.id), ]); return _SkinSourcesData( - cases: results[0] as List, - rewardCollections: results[1] as List, - operationCollections: results[2] as List, + containers: results[0] as List, + rewardCollections: results[1] as List, + operationCollections: results[2] as List, ); } @@ -354,23 +330,20 @@ class SkinDetailsScreen extends StatelessWidget { ); } - String _rewardCollectionSubtitle(RewardCollectionDto item) { - final parts = [ - item.sourceLabel, - item.actionLabel, - ]; + String _rewardCollectionSubtitle(ContainerDto item) { + final parts = [item.sourceLabel, item.actionLabel]; - return parts.join(' • '); + return parts.join(' Š Š†Š ā€šŠ”Ń› '); } } class _SkinSourcesData { - final List cases; - final List rewardCollections; - final List operationCollections; + final List containers; + final List rewardCollections; + final List operationCollections; const _SkinSourcesData({ - required this.cases, + required this.containers, required this.rewardCollections, required this.operationCollections, }); diff --git a/lib/presentation/screens/skin_glossary_screen.dart b/lib/presentation/screens/skin_glossary_screen.dart index 9fa9c47a..e1325d89 100644 --- a/lib/presentation/screens/skin_glossary_screen.dart +++ b/lib/presentation/screens/skin_glossary_screen.dart @@ -95,8 +95,9 @@ class _SkinGlossaryScreenState extends State { }).toList(); filtered.sort((a, b) { - final specialCompare = - (a.isSpecialItem ? 1 : 0).compareTo(b.isSpecialItem ? 1 : 0); + final specialCompare = (a.isSpecialItem ? 1 : 0).compareTo( + b.isSpecialItem ? 1 : 0, + ); if (specialCompare != 0) return specialCompare; final rarityCompare = _rarityOrder(a).compareTo(_rarityOrder(b)); @@ -139,16 +140,12 @@ class _SkinGlossaryScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Skin Glossary'), - ), + appBar: AppBar(title: const Text('Skin Glossary')), body: FutureBuilder>( future: _future, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { - return const Center( - child: CircularProgressIndicator(), - ); + return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { @@ -176,17 +173,17 @@ class _SkinGlossaryScreenState extends State { controller: _searchController, decoration: InputDecoration( hintText: - 'Search by skin, weapon, collection, finish...', + 'Search by skin, weapon, collection, finish...', prefixIcon: const Icon(Icons.search), suffixIcon: _query.isEmpty ? null : IconButton( - tooltip: 'Clear', - onPressed: () { - _searchController.clear(); - }, - icon: const Icon(Icons.clear), - ), + tooltip: 'Clear', + onPressed: () { + _searchController.clear(); + }, + icon: const Icon(Icons.clear), + ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(14), ), @@ -207,10 +204,10 @@ class _SkinGlossaryScreenState extends State { items: _rarityItems .map( (e) => DropdownMenuItem( - value: e.value, - child: Text(e.label), - ), - ) + value: e.value, + child: Text(e.label), + ), + ) .toList(), onChanged: (value) { setState(() { @@ -232,10 +229,10 @@ class _SkinGlossaryScreenState extends State { items: _typeItems .map( (e) => DropdownMenuItem( - value: e.value, - child: Text(e.label), - ), - ) + value: e.value, + child: Text(e.label), + ), + ) .toList(), onChanged: (value) { setState(() { @@ -264,56 +261,58 @@ class _SkinGlossaryScreenState extends State { Expanded( child: filtered.isEmpty ? const Center( - child: Padding( - padding: EdgeInsets.all(24), - child: Text( - 'Nothing found.', - style: TextStyle(fontSize: 16), - ), - ), - ) + child: Padding( + padding: EdgeInsets.all(24), + child: Text( + 'Nothing found.', + style: TextStyle(fontSize: 16), + ), + ), + ) : ListView.separated( - cacheExtent: 1200, - padding: const EdgeInsets.all(12), - itemCount: filtered.length, - separatorBuilder: (_, _) => const SizedBox(height: 10), - itemBuilder: (context, index) { - final skin = filtered[index]; - final rarityColor = SkinUiHelper.rarityColor(skin); + cacheExtent: 1200, + padding: const EdgeInsets.all(12), + itemCount: filtered.length, + separatorBuilder: (_, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final skin = filtered[index]; + final rarityColor = SkinUiHelper.rarityColor(skin); - return GlossaryListItem( - accentColor: rarityColor, - imagePath: skin.skinImage, - title: skin.itemDisplayName, - subtitle: SkinUiHelper.secondaryText(skin), - tags: [ - _pill( - SkinUiHelper.rarityLabel(skin), - color: rarityColor, - ), - _pill( - skin.itemKind == 'WEAPON' - ? SkinUiHelper.weaponTypeLabel(skin.weaponType) - : skin.itemKind == 'KNIFE' - ? 'Knife' - : 'Gloves', - ), - if ((skin.collection ?? '').isNotEmpty) - _pill(skin.collection!), - ], - onTap: () { - AppNavigationHelper.pushScreen( - context, - SkinDetailsScreen( - repository: widget.repository, - settingsController: widget.settingsController, - skin: skin, - ), - ); - }, - ); - }, - ), + return GlossaryListItem( + accentColor: rarityColor, + imagePath: skin.skinImage, + title: skin.itemDisplayName, + subtitle: SkinUiHelper.secondaryText(skin), + tags: [ + _pill( + SkinUiHelper.rarityLabel(skin), + color: rarityColor, + ), + _pill( + skin.itemKind == 'WEAPON' + ? SkinUiHelper.weaponTypeLabel( + skin.weaponType, + ) + : skin.itemKind == 'KNIFE' + ? 'Knife' + : 'Gloves', + ), + if ((skin.collection ?? '').isNotEmpty) + _pill(skin.collection!), + ], + onTap: () { + AppNavigationHelper.pushScreen( + context, + SkinDetailsScreen( + repository: widget.repository, + settingsController: widget.settingsController, + skin: skin, + ), + ); + }, + ); + }, + ), ), ], ); diff --git a/lib/presentation/screens/sticker_collection_list_screen.dart b/lib/presentation/screens/sticker_collection_list_screen.dart index ebac5b60..9f692dfe 100644 --- a/lib/presentation/screens/sticker_collection_list_screen.dart +++ b/lib/presentation/screens/sticker_collection_list_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; import '../helpers/source_color_helper.dart'; @@ -22,7 +22,7 @@ class StickerCollectionListScreen extends StatefulWidget { class _StickerCollectionListScreenState extends State { - late Future> _future; + late Future> _future; static const String _filterAll = 'ALL'; static const String _filterArmory = 'ARMORY_REWARD'; @@ -49,15 +49,15 @@ class _StickerCollectionListScreenState } } - String _sourceGroupLabel(CaseDto collection) { + String _sourceGroupLabel(ContainerDto collection) { if (collection.sourceType == _filterArmory) { return 'Armory'; } return 'Operations'; } - List _applyFilters(List all) { - var items = List.from(all); + List _applyFilters(List all) { + var items = List.from(all); if (_selectedFilter == _filterArmory) { items = items.where((e) => e.sourceType == _filterArmory).toList(); @@ -91,14 +91,14 @@ class _StickerCollectionListScreenState ); } - Widget _buildCard(BuildContext context, CaseDto collection) { + Widget _buildCard(BuildContext context, ContainerDto collection) { final typeColor = SourceColorHelper.containerTypeColor(collection.type); final isArmory = collection.sourceType == _filterArmory; final sourceColor = SourceColorHelper.rewardSourceColor(isArmory: isArmory); final sourceLabel = _sourceGroupLabel(collection); return CollectionListCard( - imagePath: collection.caseImage, + imagePath: collection.containerImage, title: collection.name, releaseDate: collection.releaseDate, chips: [ @@ -122,7 +122,7 @@ class _StickerCollectionListScreenState AppNavigationHelper.pushScreen( context, AppNavigationHelper.buildContainerOpenScreen( - caseDto: collection, + containerDto: collection, repository: widget.repository, ), ); @@ -134,12 +134,12 @@ class _StickerCollectionListScreenState Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Sticker Collections')), - body: AsyncCollectionLoader( + body: AsyncCollectionLoader( future: _future, builder: (context, all) { final visible = _applyFilters(all); - return ResponsiveCollectionGrid( + return ResponsiveCollectionGrid( items: visible, emptyMessage: 'No sticker collections found.', header: _buildFilterBar(), diff --git a/lib/presentation/screens/sticker_container_open_screen.dart b/lib/presentation/screens/sticker_container_open_screen.dart index 90b28d86..f861c523 100644 --- a/lib/presentation/screens/sticker_container_open_screen.dart +++ b/lib/presentation/screens/sticker_container_open_screen.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/sticker_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../../domain/dropped_sticker.dart'; @@ -23,12 +23,12 @@ import '../widgets/sticker_drop_card.dart'; import '../widgets/sticker_grid_tile.dart'; class StickerContainerOpenScreen extends StatefulWidget { - final CaseDto caseDto; + final ContainerDto containerDto; final LocalDataRepository repository; const StickerContainerOpenScreen({ super.key, - required this.caseDto, + required this.containerDto, required this.repository, }); @@ -52,7 +52,9 @@ class _StickerContainerOpenScreenState @override void initState() { super.initState(); - _stickersFuture = widget.repository.loadStickersForCase(widget.caseDto.id); + _stickersFuture = widget.repository.loadStickersForContainer( + widget.containerDto.id, + ); } @override @@ -64,7 +66,7 @@ class _StickerContainerOpenScreenState Future _openContainer(List stickers) async { final drop = _simulator.openContainer(stickers: stickers); - if (widget.caseDto.isStickerCapsule) { + if (widget.containerDto.isStickerCapsule) { final rollData = _buildRollSequence(stickers, drop); await CollectibleOpenFlowHelper.runRoulette( setState: setState, @@ -113,14 +115,14 @@ class _StickerContainerOpenScreenState if (_isOpening) { return 'OPENING...'; } - if (widget.caseDto.isStickerCollection) { + if (widget.containerDto.isStickerCollection) { return 'OPEN STICKER COLLECTION'; } return 'OPEN STICKER CAPSULE'; } String _loadingTitle() { - if (widget.caseDto.isStickerCollection) { + if (widget.containerDto.isStickerCollection) { return 'Opening sticker collection...'; } return 'Opening sticker capsule...'; @@ -193,34 +195,36 @@ class _StickerContainerOpenScreenState @override Widget build(BuildContext context) { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( - widget.caseDto.releaseDate, + widget.containerDto.releaseDate, + ); + final color = SourceColorHelper.containerTypeColor( + widget.containerDto.type, ); - final color = SourceColorHelper.containerTypeColor(widget.caseDto.type); - final sourceTypeLabel = widget.caseDto.sourceTypeLabel; + final sourceTypeLabel = widget.containerDto.sourceTypeLabel; final sourceColor = SourceColorHelper.collectibleSourceColor( - widget.caseDto.sourceType, - widget.caseDto.sourceId, + widget.containerDto.sourceType, + widget.containerDto.sourceId, ); return Scaffold( - appBar: AppBar(title: Text(widget.caseDto.name)), + appBar: AppBar(title: Text(widget.containerDto.name)), body: CollectibleOpenBody( future: _stickersFuture, sliverBuilder: (context, constraints, stickers, gridCount, aspectRatio) { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.caseDto.caseImage, + assetPath: widget.containerDto.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ - ChipBadge(label: widget.caseDto.typeLabel, color: color), - if (widget.caseDto.isStickerCollection && + ChipBadge(label: widget.containerDto.typeLabel, color: color), + if (widget.containerDto.isStickerCollection && sourceTypeLabel != null) ChipBadge(label: sourceTypeLabel, color: sourceColor), - if (widget.caseDto.isStickerCollection && - (widget.caseDto.sourceName ?? '').isNotEmpty) + if (widget.containerDto.isStickerCollection && + (widget.containerDto.sourceName ?? '').isNotEmpty) ChipBadge( - label: widget.caseDto.sourceName!, + label: widget.containerDto.sourceName!, color: sourceColor, ), ], @@ -233,7 +237,7 @@ class _StickerContainerOpenScreenState : () => _openContainer(stickers), ), ), - if (widget.caseDto.isStickerCapsule) + if (widget.containerDto.isStickerCapsule) CollectibleRollerSliver( controller: _rollController, items: _rollSequence, @@ -245,8 +249,10 @@ class _StickerContainerOpenScreenState itemWidth: itemWidth, ), ), - if (_isOpening && !widget.caseDto.isStickerCapsule) - SliverToBoxAdapter(child: OpeningLoadingCard(title: _loadingTitle())), + if (_isOpening && !widget.containerDto.isStickerCapsule) + SliverToBoxAdapter( + child: OpeningLoadingCard(title: _loadingTitle()), + ), if (_dropped != null) SliverToBoxAdapter(child: StickerDropCard(drop: _dropped!)), const SliverToBoxAdapter( diff --git a/lib/presentation/screens/sticker_details_screen.dart b/lib/presentation/screens/sticker_details_screen.dart index ea678c50..ed439047 100644 --- a/lib/presentation/screens/sticker_details_screen.dart +++ b/lib/presentation/screens/sticker_details_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/sticker_dto.dart'; import '../../data/repositories/local_data_repository.dart'; import '../helpers/app_navigation_helper.dart'; @@ -28,8 +28,8 @@ class StickerDetailsScreen extends StatelessWidget { return Scaffold( appBar: AppBar(title: Text(sticker.name)), - body: FutureBuilder>( - future: repository.loadCasesForSticker(sticker.id), + body: FutureBuilder>( + future: repository.loadContainersForSticker(sticker.id), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); @@ -47,7 +47,7 @@ class StickerDetailsScreen extends StatelessWidget { ); } - final cases = snapshot.data ?? const []; + final cases = snapshot.data ?? const []; return ListView( padding: const EdgeInsets.all(12), @@ -63,7 +63,9 @@ class StickerDetailsScreen extends StatelessWidget { ), DetailTag(text: sticker.stickerTypeLabel), if (sticker.effect != 'OTHER') - DetailTag(text: StickerUiHelper.effectLabel(sticker.effect)), + DetailTag( + text: StickerUiHelper.effectLabel(sticker.effect), + ), if ((sticker.collection ?? '').isNotEmpty) DetailTag(text: sticker.collection!), if ((sticker.tournament ?? '').isNotEmpty) @@ -80,13 +82,19 @@ class StickerDetailsScreen extends StatelessWidget { value: StickerUiHelper.effectLabel(sticker.effect), ), if ((sticker.collection ?? '').isNotEmpty) - DetailInfoRow(title: 'Collection', value: sticker.collection!), + DetailInfoRow( + title: 'Collection', + value: sticker.collection!, + ), if ((sticker.tournament ?? '').isNotEmpty) - DetailInfoRow(title: 'Tournament', value: sticker.tournament!), + DetailInfoRow( + title: 'Tournament', + value: sticker.tournament!, + ), ], ), const SizedBox(height: 12), - DetailSourceSection( + DetailSourceSection( title: 'Containers', items: cases, emptyText: 'No sticker container sources found.', @@ -96,16 +104,17 @@ class StickerDetailsScreen extends StatelessWidget { subtitleParts.add(item.sourceTypeLabel!); } return DetailSourceTile( - imagePath: item.caseImage, + imagePath: item.containerImage, title: item.name, - subtitle: subtitleParts.join(' • '), + subtitle: subtitleParts.join(' Š²Š‚Ńž '), trailing: - DateFormatHelper.formatReleaseDate(item.releaseDate) ?? '-', + DateFormatHelper.formatReleaseDate(item.releaseDate) ?? + '-', onTap: () { AppNavigationHelper.pushScreen( context, AppNavigationHelper.buildContainerOpenScreen( - caseDto: item, + containerDto: item, repository: repository, ), ); diff --git a/lib/presentation/screens/sticker_glossary_screen.dart b/lib/presentation/screens/sticker_glossary_screen.dart index 5211b1e0..32d51f3a 100644 --- a/lib/presentation/screens/sticker_glossary_screen.dart +++ b/lib/presentation/screens/sticker_glossary_screen.dart @@ -21,6 +21,7 @@ class StickerGlossaryScreen extends StatefulWidget { class _StickerGlossaryScreenState extends State { String _rarityFilter = 'ALL'; + String _sourceFilter = 'ALL'; static const List _rarityOptions = [ GlossaryFilterOption('ALL', 'All rarities'), @@ -31,11 +32,29 @@ class _StickerGlossaryScreenState extends State { GlossaryFilterOption('CONTRABAND', 'Contraband'), ]; + List _sourceOptions(List items) { + final values = + items + .map((item) => item.sourceLabel.trim()) + .where((value) => value.isNotEmpty) + .toSet() + .toList() + ..sort(); + + return [ + const GlossaryFilterOption('ALL', 'All sources'), + ...values.map((value) => GlossaryFilterOption(value, value)), + ]; + } + List _filterAndSort(List items, String query) { final filtered = items.where((sticker) { if (_rarityFilter != 'ALL' && sticker.rarity != _rarityFilter) { return false; } + if (_sourceFilter != 'ALL' && sticker.sourceLabel != _sourceFilter) { + return false; + } if (query.isEmpty) return true; final haystack = [ sticker.name, @@ -84,16 +103,35 @@ class _StickerGlossaryScreenState extends State { countLabelBuilder: (count) => '$count stickers', emptyMessage: 'No stickers found.', errorPrefix: 'Failed to load stickers.', - headerControlsBuilder: (_) => [ - GlossaryFilterDropdown( - label: 'Rarity', - value: _rarityFilter, - options: _rarityOptions, - onChanged: (value) { - setState(() { - _rarityFilter = value ?? 'ALL'; - }); - }, + headerControlsBuilder: (_, items) => [ + Row( + children: [ + Expanded( + child: GlossaryFilterDropdown( + label: 'Rarity', + value: _rarityFilter, + options: _rarityOptions, + onChanged: (value) { + setState(() { + _rarityFilter = value ?? 'ALL'; + }); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: GlossaryFilterDropdown( + label: 'Source', + value: _sourceFilter, + options: _sourceOptions(items), + onChanged: (value) { + setState(() { + _sourceFilter = value ?? 'ALL'; + }); + }, + ), + ), + ], ), ], itemBuilder: (context, sticker) { @@ -112,7 +150,10 @@ class _StickerGlossaryScreenState extends State { onTap: () { AppNavigationHelper.pushScreen( context, - StickerDetailsScreen(repository: widget.repository, sticker: sticker), + StickerDetailsScreen( + repository: widget.repository, + sticker: sticker, + ), ); }, ); diff --git a/lib/presentation/screens/terminal_open_screen.dart b/lib/presentation/screens/terminal_open_screen.dart index 3958a328..f812b868 100644 --- a/lib/presentation/screens/terminal_open_screen.dart +++ b/lib/presentation/screens/terminal_open_screen.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import '../../core/utils/date_format_helper.dart'; -import '../../data/models/case_dto.dart'; +import '../../data/models/container_dto.dart'; import '../../data/models/skin_dto.dart'; import '../../data/repositories/local_data_repository.dart'; -import '../../domain/case_simulator_service.dart'; +import '../../domain/container_simulator_service.dart'; import '../../domain/dropped_skin.dart'; import '../../domain/terminal_offer.dart'; import '../helpers/source_color_helper.dart'; @@ -19,12 +19,12 @@ import '../widgets/skin_grid_tile.dart'; import '../widgets/terminal_offer_card.dart'; class TerminalOpenScreen extends StatefulWidget { - final CaseDto caseDto; + final ContainerDto containerDto; final LocalDataRepository repository; const TerminalOpenScreen({ super.key, - required this.caseDto, + required this.containerDto, required this.repository, }); @@ -34,7 +34,7 @@ class TerminalOpenScreen extends StatefulWidget { class _TerminalOpenScreenState extends State { late Future> _skinsFuture; - final CaseSimulatorService _simulator = CaseSimulatorService(); + final ContainerSimulatorService _simulator = ContainerSimulatorService(); List _terminalOffers = const []; int _terminalOfferIndex = 0; @@ -65,7 +65,9 @@ class _TerminalOpenScreenState extends State { @override void initState() { super.initState(); - _skinsFuture = widget.repository.loadSkinsForCase(widget.caseDto.id); + _skinsFuture = widget.repository.loadSkinsForContainer( + widget.containerDto.id, + ); } Future _startTerminal(List skins) async { @@ -144,22 +146,27 @@ class _TerminalOpenScreenState extends State { @override Widget build(BuildContext context) { final formattedReleaseDate = DateFormatHelper.formatReleaseDate( - widget.caseDto.releaseDate, + widget.containerDto.releaseDate, + ); + final typeColor = SourceColorHelper.containerTypeColor( + widget.containerDto.type, ); - final typeColor = SourceColorHelper.containerTypeColor(widget.caseDto.type); return Scaffold( - appBar: AppBar(title: Text(widget.caseDto.name)), + appBar: AppBar(title: Text(widget.containerDto.name)), body: CollectibleOpenBody( future: _skinsFuture, sliverBuilder: (context, constraints, skins, gridCount, aspectRatio) { return [ SliverToBoxAdapter( child: CollectibleOpenHeader( - assetPath: widget.caseDto.caseImage, + assetPath: widget.containerDto.containerImage, imageHeight: constraints.maxWidth < 700 ? 90 : 120, badges: [ - ChipBadge(label: widget.caseDto.typeLabel, color: typeColor), + ChipBadge( + label: widget.containerDto.typeLabel, + color: typeColor, + ), ], releaseDateText: formattedReleaseDate, description: diff --git a/lib/presentation/screens/tradeup_screen.dart b/lib/presentation/screens/tradeup_screen.dart index 277ce8e6..6841c371 100644 --- a/lib/presentation/screens/tradeup_screen.dart +++ b/lib/presentation/screens/tradeup_screen.dart @@ -2,30 +2,30 @@ import 'package:flutter/material.dart'; import '../../data/models/skin_dto.dart'; import '../../data/repositories/local_data_repository.dart'; +import '../../domain/dropped_skin.dart'; import '../../domain/tradeup_service.dart'; import '../helpers/responsive_grid_helper.dart'; -import '../helpers/skin_ui_helper.dart'; +import '../helpers/tradeup_controller.dart'; +import '../widgets/tradeup_chance_card.dart'; +import '../widgets/skin_drop_card.dart'; +import '../widgets/tradeup_selected_slot.dart'; +import '../widgets/tradeup_skin_tile.dart'; class TradeUpScreen extends StatefulWidget { final LocalDataRepository repository; - const TradeUpScreen({ - super.key, - required this.repository, - }); + const TradeUpScreen({super.key, required this.repository}); @override State createState() => _TradeUpScreenState(); } class _TradeUpScreenState extends State { - late Future<_TradeUpData> _dataFuture; + static const double _tradeupPanelMaxWidth = 620; - final TradeUpService _service = TradeUpService(); + late Future<_TradeUpData> _dataFuture; - final List _selected = []; - TradeUpResult? _result; - List _chances = []; + late final TradeUpController _controller; String _search = ''; String? _rarity = 'MIL_SPEC'; @@ -33,49 +33,39 @@ class _TradeUpScreenState extends State { @override void initState() { super.initState(); + _controller = TradeUpController(service: TradeUpService()); _dataFuture = _loadData(); } Future<_TradeUpData> _loadData() async { final skins = await widget.repository.loadSkins(); - final cases = await widget.repository.loadCases(); - final caseToSkinIds = await widget.repository.loadCaseToSkinIds(); - - final caseNameById = { - for (final c in cases) c.id: c.name, - }; + final containers = await widget.repository.loadContainers(); + final containerToSkinIds = await widget.repository.loadContainerToSkinIds(); - final regularCases = { - for (final c in cases.where((c) => c.isRegularCase)) c.id: c, + final regularContainers = { + for (final c in containers.where((c) => c.isRegularCase)) c.id: c, }; - final skinIdToCaseNames = >{}; final skinIdToRegularCaseIds = >{}; final regularCaseIdToSkinIds = >{}; - for (final entry in caseToSkinIds.entries) { - final caseId = entry.key; - final caseName = caseNameById[caseId]; - if (caseName == null) continue; - - final isRegularCase = regularCases.containsKey(caseId); + for (final entry in containerToSkinIds.entries) { + final containerId = entry.key; + final isRegularCase = regularContainers.containsKey(containerId); if (isRegularCase) { - regularCaseIdToSkinIds[caseId] = List.from(entry.value); + regularCaseIdToSkinIds[containerId] = List.from(entry.value); } for (final skinId in entry.value) { - skinIdToCaseNames.putIfAbsent(skinId, () => []).add(caseName); - if (isRegularCase) { - skinIdToRegularCaseIds.putIfAbsent(skinId, () => []).add(caseId); + skinIdToRegularCaseIds.putIfAbsent(skinId, () => []).add(containerId); } } } return _TradeUpData( skins: skins, - skinIdToCaseNames: skinIdToCaseNames, skinIdToRegularCaseIds: skinIdToRegularCaseIds, regularCaseIdToSkinIds: regularCaseIdToSkinIds, ); @@ -101,61 +91,157 @@ class _TradeUpScreenState extends State { } int _maxSelectable() { - if (_selected.isEmpty) return 10; - return _selected.first.rarity == 'COVERT' ? 5 : 10; + return _controller.maxSelectable(); } bool _canAddMore() { - return _selected.length < _maxSelectable(); + return _controller.canAddMore; } - bool _tradeReady() { - if (_selected.isEmpty) return false; - final rarity = _selected.first.rarity; - - if (rarity == 'COVERT') { - return _selected.length == 5; - } - return _selected.length == 10; + bool _canExecuteTrade() { + return _controller.canExecuteTrade; } - void _recalculateChances(_TradeUpData data) { - if (_tradeReady()) { - _chances = _service.getTradeUpChances( - input: _selected, + void _add(SkinDto skin, _TradeUpData data) { + try { + _controller.add( + skin, allSkins: data.skins, skinIdToRegularCaseIds: data.skinIdToRegularCaseIds, regularCaseIdToSkinIds: data.regularCaseIdToSkinIds, ); - } else { - _chances = []; + setState(() {}); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))), + ); } } - void _add(SkinDto skin, _TradeUpData data) { - if (!_canAddMore()) return; + Future _editFloatAt(int index, _TradeUpData data) async { + final item = _controller.selected[index]; + final controller = TextEditingController( + text: item.floatValue.toStringAsFixed(5), + ); + var selectedQuality = item.quality; + + final result = await showDialog( + context: context, + builder: (context) { + String? errorText; + + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: const Text('Set Float'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.skin.itemDisplayName), + Text( + item.skin.name, + style: const TextStyle(color: Colors.white70), + ), + const SizedBox(height: 8), + Text( + 'Allowed range: ${item.skin.floatTop.toStringAsFixed(2)} - ${item.skin.floatBottom.toStringAsFixed(2)}', + style: const TextStyle(fontSize: 12, color: Colors.white70), + ), + const SizedBox(height: 12), + TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + decoration: InputDecoration( + labelText: 'Float value', + errorText: errorText, + ), + ), + const SizedBox(height: 12), + const Text( + 'Quality mode', + style: TextStyle(fontSize: 12, color: Colors.white70), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: [ + for (final quality in _editableQualities) + ChoiceChip( + label: Text(_qualityLabel(quality)), + selected: selectedQuality == quality, + onSelected: (_) { + setDialogState(() { + selectedQuality = quality; + }); + }, + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + final parsed = double.tryParse( + controller.text.trim().replaceAll(',', '.'), + ); + + if (parsed == null) { + setDialogState(() { + errorText = 'Enter a valid float value'; + }); + return; + } + + if (parsed < item.skin.floatTop || + parsed > item.skin.floatBottom) { + setDialogState(() { + errorText = 'Float must stay within the allowed range'; + }); + return; + } + + Navigator.of(context).pop(parsed); + }, + child: const Text('Apply'), + ), + ], + ); + }, + ); + }, + ); - if (_selected.isNotEmpty && _selected.first.rarity != skin.rarity) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('All selected skins must have the same rarity')), - ); - return; - } + if (result == null) return; setState(() { - _selected.add(skin); - _result = null; - _recalculateChances(data); + _controller.updateItem( + index, + floatValue: result, + quality: selectedQuality, + allSkins: data.skins, + skinIdToRegularCaseIds: data.skinIdToRegularCaseIds, + regularCaseIdToSkinIds: data.regularCaseIdToSkinIds, + ); }); } void _removeAt(int index, _TradeUpData data) { setState(() { - _selected.removeAt(index); - _result = null; - _recalculateChances(data); - - if (_selected.isEmpty) { + _controller.removeAt( + index, + allSkins: data.skins, + skinIdToRegularCaseIds: data.skinIdToRegularCaseIds, + regularCaseIdToSkinIds: data.regularCaseIdToSkinIds, + ); + if (_controller.selected.isEmpty) { _rarity = 'MIL_SPEC'; } }); @@ -163,23 +249,19 @@ class _TradeUpScreenState extends State { void _clear() { setState(() { - _selected.clear(); - _result = null; - _chances = []; + _controller.clear(); }); } Future _trade(_TradeUpData data) async { try { - final result = _service.tradeUp( - input: _selected, + _controller.executeTrade( allSkins: data.skins, skinIdToRegularCaseIds: data.skinIdToRegularCaseIds, regularCaseIdToSkinIds: data.regularCaseIdToSkinIds, ); - setState(() { - _result = result; + // Controller already updated its state. }); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( @@ -188,268 +270,37 @@ class _TradeUpScreenState extends State { } } - bool _matchesSearch(SkinDto s, _TradeUpData data) { + bool _matchesSearch(SkinDto skin) { final q = _search.trim().toLowerCase(); if (q.isEmpty) return true; - final caseNames = data.skinIdToCaseNames[s.id] ?? const []; - final caseNamesJoined = caseNames.join(' ').toLowerCase(); - final collection = (s.collection ?? '').toLowerCase(); + final collection = (skin.collection ?? '').toLowerCase(); - return s.name.toLowerCase().contains(q) || - s.itemDisplayName.toLowerCase().contains(q) || - caseNamesJoined.contains(q) || + return skin.name.toLowerCase().contains(q) || + skin.itemDisplayName.toLowerCase().contains(q) || collection.contains(q); } - Widget _slot(int i, _TradeUpData data) { - final skin = i < _selected.length ? _selected[i] : null; - - if (skin == null) { - return Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.white24), - borderRadius: BorderRadius.circular(10), - ), - child: const Center(child: Icon(Icons.add)), - ); + String _qualityLabel(TradeUpInputQuality quality) { + switch (quality) { + case TradeUpInputQuality.regular: + return 'Regular'; + case TradeUpInputQuality.statTrak: + return 'StatTrakā„¢'; + case TradeUpInputQuality.souvenir: + return 'Souvenir'; } - - final color = SkinUiHelper.rarityColor(skin); - - return GestureDetector( - onTap: () => _removeAt(i, data), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: color, width: 2), - borderRadius: BorderRadius.circular(10), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.all(4), - child: Image.asset( - skin.skinImage, - fit: BoxFit.contain, - errorBuilder: (_, error, stackTrace) => - const Icon(Icons.image_not_supported), - ), - ), - Positioned( - right: 2, - top: 2, - child: Container( - padding: const EdgeInsets.all(2), - color: Colors.black, - child: const Icon(Icons.close, size: 12), - ), - ), - ], - ), - ), - ); } - Widget _skinTile(SkinDto s, _TradeUpData data) { - final color = SkinUiHelper.rarityColor(s); - final count = _selected.where((e) => e.id == s.id).length; - final blocked = !_canAddMore(); - final caseNames = data.skinIdToCaseNames[s.id] ?? const []; - - return Opacity( - opacity: blocked ? 0.55 : 1, - child: GestureDetector( - onTap: blocked ? null : () => _add(s, data), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.white10), - borderRadius: BorderRadius.circular(10), - ), - child: Stack( - children: [ - Column( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(4), - child: Image.asset( - s.skinImage, - fit: BoxFit.contain, - errorBuilder: (_, error, stackTrace) => - const Icon(Icons.image_not_supported), - ), - ), - ), - Container(height: 3, color: color), - Padding( - padding: const EdgeInsets.all(5), - child: Column( - children: [ - Text( - s.itemDisplayName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 10), - ), - const SizedBox(height: 3), - Text( - s.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 10, color: Colors.white70), - ), - if (caseNames.isNotEmpty) ...[ - const SizedBox(height: 3), - Text( - caseNames.first, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 9, color: Colors.white54), - ), - ], - if (s.collection != null && s.collection!.isNotEmpty) ...[ - const SizedBox(height: 2), - Text( - s.collection!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 9, color: Colors.white38), - ), - ], - ], - ), - ), - ], - ), - if (count > 0) - Positioned( - right: 4, - top: 4, - child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(6), - ), - child: Text('x$count', style: const TextStyle(fontSize: 10)), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _resultCard() { - if (_result == null) return const SizedBox(); - - final s = _result!.skin; - final color = SkinUiHelper.rarityColor(s); - - return Card( - margin: const EdgeInsets.all(12), - shape: RoundedRectangleBorder( - side: BorderSide(color: color, width: 2), - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - children: [ - Image.asset( - s.skinImage, - height: 120, - errorBuilder: (_, error, stackTrace) => - const Icon(Icons.image_not_supported, size: 80), - ), - const SizedBox(height: 8), - Text( - '${s.isSpecialItem ? 'ā˜… ' : ''}${s.itemDisplayName} | ${s.name}', - style: TextStyle( - color: color, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Text('Rarity: ${s.isSpecialItem ? 'Special Item' : _rarityLabel(s.rarity)}'), - Text('Weapon type: ${SkinUiHelper.weaponTypeLabel(s.weaponType)}'), - Text('Float: ${_result!.floatValue.toStringAsFixed(5)}'), - Text(_result!.exterior), - ], - ), - ), - ); - } - - Widget _chanceCard(TradeUpChance chance) { - final skin = chance.skin; - final color = SkinUiHelper.rarityColor(skin); - - return Card( - margin: EdgeInsets.zero, - child: Column( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(4), - child: Image.asset( - skin.skinImage, - fit: BoxFit.contain, - errorBuilder: (_, error, stackTrace) => - const Icon(Icons.image_not_supported), - ), - ), - ), - Container(height: 3, color: color), - Padding( - padding: const EdgeInsets.all(6), - child: Column( - children: [ - Text( - skin.itemDisplayName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 10), - ), - const SizedBox(height: 3), - Text( - skin.name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 10, color: Colors.white70), - ), - const SizedBox(height: 4), - Text( - '${(chance.probability * 100).toStringAsFixed(2)}%', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - color: color, - ), - ), - ], - ), - ), - ], - ), - ); - } + List get _editableQualities => const [ + TradeUpInputQuality.regular, + TradeUpInputQuality.statTrak, + ]; @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Trade-Up Simulator'), - ), + appBar: AppBar(title: const Text('Trade-Up Simulator')), body: FutureBuilder<_TradeUpData>( future: _dataFuture, builder: (_, snap) { @@ -458,10 +309,8 @@ class _TradeUpScreenState extends State { } final data = snap.data!; - final all = data.skins; - - final filtered = all.where((s) { - if (s.isSpecialItem) return false; + final filtered = data.skins.where((skin) { + if (skin.isSpecialItem) return false; const allowedRarities = { 'CONSUMER', @@ -472,21 +321,21 @@ class _TradeUpScreenState extends State { 'COVERT', }; - if (!allowedRarities.contains(s.rarity)) { + if (!allowedRarities.contains(skin.rarity)) { return false; } - if (_rarity != null && s.rarity != _rarity) { + if (_rarity != null && skin.rarity != _rarity) { return false; } - return _matchesSearch(s, data); + return _matchesSearch(skin); }).toList(); filtered.sort((a, b) => int.parse(a.id).compareTo(int.parse(b.id))); return LayoutBuilder( - builder: (context, c) { + builder: (context, constraints) { return CustomScrollView( slivers: [ SliverToBoxAdapter( @@ -497,16 +346,17 @@ class _TradeUpScreenState extends State { children: [ TextField( decoration: const InputDecoration( - hintText: 'Search by skin, case, or collection...', + hintText: 'Search by skin or collection...', prefixIcon: Icon(Icons.search), ), - onChanged: (v) => setState(() => _search = v), + onChanged: (value) => + setState(() => _search = value), ), const SizedBox(height: 8), Wrap( spacing: 8, children: [ - for (final r in [ + for (final rarity in [ 'CONSUMER', 'INDUSTRIAL', 'MIL_SPEC', @@ -515,11 +365,11 @@ class _TradeUpScreenState extends State { 'COVERT', ]) ChoiceChip( - label: Text(_rarityLabel(r)), - selected: _rarity == r, + label: Text(_rarityLabel(rarity)), + selected: _rarity == rarity, onSelected: (_) { setState(() { - _rarity = r; + _rarity = rarity; _clear(); }); }, @@ -528,20 +378,36 @@ class _TradeUpScreenState extends State { ), const SizedBox(height: 12), Text( - _selected.isEmpty + _controller.selected.isEmpty ? 'Select skins' - : 'Selected: ${_selected.length}/${_maxSelectable()}', + : 'Selected: ${_controller.selected.length}/${_maxSelectable()}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), - if (_selected.isNotEmpty && _selected.first.rarity == 'COVERT') + if (_controller.selected.isNotEmpty) + const Padding( + padding: EdgeInsets.only(top: 4), + child: Text( + 'Tap a selected skin to edit its float', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ), + if (_controller.selected.isNotEmpty && + _controller.selected.first.skin.rarity == + 'COVERT') const Padding( padding: EdgeInsets.only(top: 4), child: Text( 'Covert trade-up uses exactly 5 skins', - style: TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), ), ), if (!_canAddMore()) @@ -549,7 +415,21 @@ class _TradeUpScreenState extends State { padding: EdgeInsets.only(top: 4), child: Text( 'Selection is full', - style: TextStyle(color: Colors.amber, fontSize: 12), + style: TextStyle( + color: Colors.amber, + fontSize: 12, + ), + ), + ), + if (_controller.tradeIssue != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + _controller.tradeIssue!, + style: const TextStyle( + color: Colors.orangeAccent, + fontSize: 12, + ), ), ), ], @@ -557,54 +437,106 @@ class _TradeUpScreenState extends State { ), ), SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(12), - child: GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: 10, - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 5, - crossAxisSpacing: 6, - mainAxisSpacing: 6, + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: _tradeupPanelMaxWidth, + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: 10, + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + crossAxisSpacing: 6, + mainAxisSpacing: 6, + ), + itemBuilder: (_, index) => TradeUpSelectedSlot( + item: index < _controller.selected.length + ? _controller.selected[index] + : null, + onTap: index < _controller.selected.length + ? () => _editFloatAt(index, data) + : null, + onRemove: index < _controller.selected.length + ? () => _removeAt(index, data) + : null, + ), + ), ), - itemBuilder: (_, i) => _slot(i, data), ), ), ), SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: _tradeReady() ? () => _trade(data) : null, - child: Text( - _selected.isNotEmpty && - _selected.first.rarity == 'COVERT' - ? 'TRADE → SPECIAL ITEM' - : 'TRADE', + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: _tradeupPanelMaxWidth, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: _canExecuteTrade() + ? () => _trade(data) + : null, + child: Text( + _controller.selected.isNotEmpty && + _controller + .selected + .first + .skin + .rarity == + 'COVERT' + ? 'TRADE SPECIAL ITEM' + : 'TRADE', + ), + ), ), - ), - ), - const SizedBox(width: 8), - OutlinedButton( - onPressed: _selected.isNotEmpty ? _clear : null, - child: const Text('CLEAR'), + const SizedBox(width: 8), + OutlinedButton( + onPressed: _controller.selected.isNotEmpty + ? _clear + : null, + child: const Text('CLEAR'), + ), + ], ), - ], + ), ), ), ), - SliverToBoxAdapter(child: _resultCard()), - if (_chances.isNotEmpty) + SliverToBoxAdapter( + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: _tradeupPanelMaxWidth, + ), + child: _controller.result == null + ? const SizedBox() + : SkinDropCard( + drop: DroppedSkin( + skin: _controller.result!.skin, + isStatTrak: _controller.result!.isStatTrak, + isSouvenir: _controller.result!.isSouvenir, + skinFloat: _controller.result!.floatValue, + exterior: _controller.result!.exterior, + ), + ), + ), + ), + ), + if (_controller.chances.isNotEmpty) const SliverToBoxAdapter( child: Padding( padding: EdgeInsets.fromLTRB(12, 12, 12, 8), child: Text( - 'Possible results', + 'Possible results with projected float', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -612,19 +544,21 @@ class _TradeUpScreenState extends State { ), ), ), - if (_chances.isNotEmpty) + if (_controller.chances.isNotEmpty) SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 12), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( - (_, i) => _chanceCard(_chances[i]), - childCount: _chances.length, + (_, index) => TradeUpChanceCard( + chance: _controller.chances[index], + ), + childCount: _controller.chances.length, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: - ResponsiveGridHelper.tradeGridCrossAxisCount( - c.maxWidth, - ), + ResponsiveGridHelper.tradeGridCrossAxisCount( + constraints.maxWidth, + ), crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 0.72, @@ -647,14 +581,21 @@ class _TradeUpScreenState extends State { padding: const EdgeInsets.all(12), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( - (_, i) => _skinTile(filtered[i], data), + (_, index) => TradeUpSkinTile( + skin: filtered[index], + selectedCount: _controller.selected + .where((e) => e.skin.id == filtered[index].id) + .length, + blocked: !_canAddMore(), + onTap: () => _add(filtered[index], data), + ), childCount: filtered.length, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: - ResponsiveGridHelper.tradeGridCrossAxisCount( - c.maxWidth, - ), + ResponsiveGridHelper.tradeGridCrossAxisCount( + constraints.maxWidth, + ), crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 0.75, @@ -673,13 +614,11 @@ class _TradeUpScreenState extends State { class _TradeUpData { final List skins; - final Map> skinIdToCaseNames; final Map> skinIdToRegularCaseIds; final Map> regularCaseIdToSkinIds; const _TradeUpData({ required this.skins, - required this.skinIdToCaseNames, required this.skinIdToRegularCaseIds, required this.regularCaseIdToSkinIds, }); diff --git a/lib/presentation/widgets/asset_collection_image.dart b/lib/presentation/widgets/asset_collection_image.dart index d66e5d40..623d3c16 100644 --- a/lib/presentation/widgets/asset_collection_image.dart +++ b/lib/presentation/widgets/asset_collection_image.dart @@ -24,9 +24,7 @@ class AssetCollectionImage extends StatelessWidget { fit: fit, placeholderBuilder: (_) => SizedBox( height: height, - child: const Center( - child: CircularProgressIndicator(strokeWidth: 2), - ), + child: const Center(child: CircularProgressIndicator(strokeWidth: 2)), ), ); } diff --git a/lib/presentation/widgets/async_collection_loader.dart b/lib/presentation/widgets/async_collection_loader.dart index 5fadb7f1..981b7b3f 100644 --- a/lib/presentation/widgets/async_collection_loader.dart +++ b/lib/presentation/widgets/async_collection_loader.dart @@ -15,6 +15,18 @@ class AsyncCollectionLoader extends StatelessWidget { return FutureBuilder>( future: future, builder: (context, snapshot) { + if (snapshot.hasError) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + 'Failed to load data:\n${snapshot.error}', + textAlign: TextAlign.center, + ), + ), + ); + } + if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } diff --git a/lib/presentation/widgets/charm_drop_card.dart b/lib/presentation/widgets/charm_drop_card.dart new file mode 100644 index 00000000..ef1493d8 --- /dev/null +++ b/lib/presentation/widgets/charm_drop_card.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +import '../../domain/dropped_charm.dart'; +import '../helpers/charm_ui_helper.dart'; +import 'collectible_drop_card.dart'; + +class CharmDropCard extends StatelessWidget { + final DroppedCharm drop; + + const CharmDropCard({super.key, required this.drop}); + + @override + Widget build(BuildContext context) { + final rarityColor = CharmUiHelper.rarityColor(drop.charm); + + return CollectibleDropCard( + imagePath: drop.charm.charmImage, + title: drop.charm.name, + subtitle: CharmUiHelper.secondaryText(drop.charm), + accentColor: rarityColor, + entries: [ + CollectibleInfoEntry( + title: 'Rarity', + value: CharmUiHelper.rarityLabel(drop.charm), + valueColor: rarityColor, + ), + const CollectibleInfoEntry(title: 'Type', value: 'Charm'), + if ((drop.charm.collection ?? '').isNotEmpty) + CollectibleInfoEntry( + title: 'Collection', + value: drop.charm.collection!, + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/charm_grid_tile.dart b/lib/presentation/widgets/charm_grid_tile.dart new file mode 100644 index 00000000..76915b72 --- /dev/null +++ b/lib/presentation/widgets/charm_grid_tile.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import '../../data/models/charm_dto.dart'; +import '../helpers/charm_ui_helper.dart'; + +class CharmGridTile extends StatelessWidget { + final CharmDto charm; + final bool highlighted; + final int crossAxisCount; + + const CharmGridTile({ + super.key, + required this.charm, + required this.highlighted, + required this.crossAxisCount, + }); + + @override + Widget build(BuildContext context) { + final rarityColor = CharmUiHelper.rarityColor(charm); + final compact = crossAxisCount >= 5; + + return AnimatedContainer( + duration: const Duration(milliseconds: 180), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: highlighted ? rarityColor : Colors.white10, + width: highlighted ? 2.5 : 1, + ), + boxShadow: highlighted + ? [ + BoxShadow( + color: rarityColor.withValues(alpha: 0.45), + blurRadius: 16, + spreadRadius: 2, + ), + ] + : null, + ), + child: Card( + margin: EdgeInsets.zero, + color: highlighted ? rarityColor.withValues(alpha: 0.12) : null, + child: Column( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.all(compact ? 10 : 12), + child: Image.asset( + charm.charmImage, + fit: BoxFit.contain, + errorBuilder: (_, _, _) => + const Icon(Icons.image_not_supported), + ), + ), + ), + Container(height: 5, color: rarityColor), + Padding( + padding: EdgeInsets.all(compact ? 6 : 8), + child: Column( + children: [ + Text( + charm.name, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: compact ? 11 : 14), + ), + const SizedBox(height: 4), + Text( + CharmUiHelper.secondaryText(charm), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white70, + fontSize: compact ? 10 : 12, + ), + ), + const SizedBox(height: 4), + Text( + CharmUiHelper.rarityLabel(charm), + style: TextStyle( + color: rarityColor, + fontSize: compact ? 10 : 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/chip_badge.dart b/lib/presentation/widgets/chip_badge.dart index 5dbe68d4..0eff4ea8 100644 --- a/lib/presentation/widgets/chip_badge.dart +++ b/lib/presentation/widgets/chip_badge.dart @@ -4,11 +4,7 @@ class ChipBadge extends StatelessWidget { final String label; final Color color; - const ChipBadge({ - super.key, - required this.label, - required this.color, - }); + const ChipBadge({super.key, required this.label, required this.color}); @override Widget build(BuildContext context) { diff --git a/lib/presentation/widgets/collectible_details_card.dart b/lib/presentation/widgets/collectible_details_card.dart index 85745991..78bac0df 100644 --- a/lib/presentation/widgets/collectible_details_card.dart +++ b/lib/presentation/widgets/collectible_details_card.dart @@ -57,18 +57,11 @@ class CollectibleDetailsCard extends StatelessWidget { Text( subtitle, textAlign: narrow ? TextAlign.center : TextAlign.left, - style: const TextStyle( - color: Colors.white70, - fontSize: 16, - ), + style: const TextStyle(color: Colors.white70, fontSize: 16), ), if (tags.isNotEmpty) ...[ const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: tags, - ), + Wrap(spacing: 8, runSpacing: 8, children: tags), ], if (infoRows.isNotEmpty) ...[ const SizedBox(height: 14), @@ -79,11 +72,7 @@ class CollectibleDetailsCard extends StatelessWidget { if (narrow) { return Column( - children: [ - image, - const SizedBox(height: 16), - info, - ], + children: [image, const SizedBox(height: 16), info], ); } diff --git a/lib/presentation/widgets/collectible_drop_card.dart b/lib/presentation/widgets/collectible_drop_card.dart index e6a90c60..cbf6dfaf 100644 --- a/lib/presentation/widgets/collectible_drop_card.dart +++ b/lib/presentation/widgets/collectible_drop_card.dart @@ -56,15 +56,14 @@ class CollectibleDropCard extends StatelessWidget { final image = Image.asset( imagePath, height: isNarrow ? 120 : 160, - errorBuilder: (_, _, _) => Icon( - Icons.image_not_supported, - size: isNarrow ? 64 : 80, - ), + errorBuilder: (_, _, _) => + Icon(Icons.image_not_supported, size: isNarrow ? 64 : 80), ); final info = Column( - crossAxisAlignment: - isNarrow ? CrossAxisAlignment.center : CrossAxisAlignment.start, + crossAxisAlignment: isNarrow + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, children: [ Text( title, diff --git a/lib/presentation/widgets/collectible_open_header.dart b/lib/presentation/widgets/collectible_open_header.dart index e0627424..de92387c 100644 --- a/lib/presentation/widgets/collectible_open_header.dart +++ b/lib/presentation/widgets/collectible_open_header.dart @@ -57,7 +57,10 @@ class CollectibleOpenHeader extends StatelessWidget { const SizedBox(height: 12), SizedBox( width: double.infinity, - child: ElevatedButton(onPressed: onPressed, child: Text(buttonLabel)), + child: ElevatedButton( + onPressed: onPressed, + child: Text(buttonLabel), + ), ), ], ), diff --git a/lib/presentation/widgets/collection_list_card.dart b/lib/presentation/widgets/collection_list_card.dart index a889869c..a04a7f99 100644 --- a/lib/presentation/widgets/collection_list_card.dart +++ b/lib/presentation/widgets/collection_list_card.dart @@ -23,8 +23,9 @@ class CollectionListCard extends StatelessWidget { @override Widget build(BuildContext context) { - final formattedReleaseDate = - DateFormatHelper.formatReleaseDate(releaseDate); + final formattedReleaseDate = DateFormatHelper.formatReleaseDate( + releaseDate, + ); return InkWell( borderRadius: BorderRadius.circular(16), @@ -50,11 +51,7 @@ class CollectionListCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ if (chips.isNotEmpty) - Wrap( - spacing: 8, - runSpacing: 8, - children: chips, - ), + Wrap(spacing: 8, runSpacing: 8, children: chips), if (chips.isNotEmpty) const SizedBox(height: 8), Text( title, @@ -113,4 +110,4 @@ class CollectionListCard extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/widgets/detail_info_row.dart b/lib/presentation/widgets/detail_info_row.dart index 88586b9c..85323056 100644 --- a/lib/presentation/widgets/detail_info_row.dart +++ b/lib/presentation/widgets/detail_info_row.dart @@ -23,18 +23,10 @@ class DetailInfoRow extends StatelessWidget { width: titleWidth, child: Text( title, - style: const TextStyle( - color: Colors.white60, - fontSize: 14, - ), - ), - ), - Expanded( - child: Text( - value, - style: const TextStyle(fontSize: 14), + style: const TextStyle(color: Colors.white60, fontSize: 14), ), ), + Expanded(child: Text(value, style: const TextStyle(fontSize: 14))), ], ), ); diff --git a/lib/presentation/widgets/detail_source_section.dart b/lib/presentation/widgets/detail_source_section.dart index ca2d5f35..1acf0ee8 100644 --- a/lib/presentation/widgets/detail_source_section.dart +++ b/lib/presentation/widgets/detail_source_section.dart @@ -24,17 +24,11 @@ class DetailSourceSection extends StatelessWidget { children: [ Text( '$title (${items.length})', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700), ), const SizedBox(height: 12), if (items.isEmpty) - Text( - emptyText, - style: const TextStyle(color: Colors.white70), - ) + Text(emptyText, style: const TextStyle(color: Colors.white70)) else ...items.map(itemBuilder), ], diff --git a/lib/presentation/widgets/detail_tag.dart b/lib/presentation/widgets/detail_tag.dart index ad289264..31ff6e75 100644 --- a/lib/presentation/widgets/detail_tag.dart +++ b/lib/presentation/widgets/detail_tag.dart @@ -4,11 +4,7 @@ class DetailTag extends StatelessWidget { final String text; final Color? color; - const DetailTag({ - super.key, - required this.text, - this.color, - }); + const DetailTag({super.key, required this.text, this.color}); @override Widget build(BuildContext context) { diff --git a/lib/presentation/widgets/float_cap_bar.dart b/lib/presentation/widgets/float_cap_bar.dart index 53e9c7fa..47ac6dd8 100644 --- a/lib/presentation/widgets/float_cap_bar.dart +++ b/lib/presentation/widgets/float_cap_bar.dart @@ -138,9 +138,7 @@ class FloatCapBar extends StatelessWidget { class _FloatTopLabel extends StatelessWidget { final double value; - const _FloatTopLabel({ - required this.value, - }); + const _FloatTopLabel({required this.value}); @override Widget build(BuildContext context) { @@ -165,10 +163,7 @@ class _PointerTriangle extends StatelessWidget { @override Widget build(BuildContext context) { - return CustomPaint( - size: const Size(10, 8), - painter: _TrianglePainter(), - ); + return CustomPaint(size: const Size(10, 8), painter: _TrianglePainter()); } } @@ -199,4 +194,4 @@ class _WearBand { final Color color; const _WearBand(this.label, this.start, this.end, this.color); -} \ No newline at end of file +} diff --git a/lib/presentation/widgets/generic_glossary_screen.dart b/lib/presentation/widgets/generic_glossary_screen.dart index da4314d6..8d02e1d1 100644 --- a/lib/presentation/widgets/generic_glossary_screen.dart +++ b/lib/presentation/widgets/generic_glossary_screen.dart @@ -9,7 +9,8 @@ class GenericGlossaryScreen extends StatefulWidget { final String Function(int count) countLabelBuilder; final String emptyMessage; final String errorPrefix; - final List Function(BuildContext context)? headerControlsBuilder; + final List Function(BuildContext context, List items)? + headerControlsBuilder; const GenericGlossaryScreen({ super.key, @@ -74,7 +75,8 @@ class _GenericGlossaryScreenState extends State> { final items = snapshot.data ?? []; final filtered = widget.filterAndSort(items, _query); - final headerControls = widget.headerControlsBuilder?.call(context) ?? + final headerControls = + widget.headerControlsBuilder?.call(context, items) ?? const []; return Column( diff --git a/lib/presentation/widgets/glossary_list_item.dart b/lib/presentation/widgets/glossary_list_item.dart index c6a11b0d..cbda275e 100644 --- a/lib/presentation/widgets/glossary_list_item.dart +++ b/lib/presentation/widgets/glossary_list_item.dart @@ -27,12 +27,7 @@ class GlossaryListItem extends StatelessWidget { onTap: onTap, child: Container( decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: accentColor, - width: 4, - ), - ), + border: Border(left: BorderSide(color: accentColor, width: 4)), ), child: Padding( padding: const EdgeInsets.all(12), @@ -49,10 +44,8 @@ class GlossaryListItem extends StatelessWidget { isAntiAlias: false, gaplessPlayback: true, cacheWidth: 256, - errorBuilder: (_, _, _) => const Icon( - Icons.image_not_supported, - size: 36, - ), + errorBuilder: (_, _, _) => + const Icon(Icons.image_not_supported, size: 36), ), ), const SizedBox(width: 12), @@ -77,20 +70,13 @@ class GlossaryListItem extends StatelessWidget { ), if (tags.isNotEmpty) ...[ const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: tags, - ), + Wrap(spacing: 8, runSpacing: 8, children: tags), ], ], ), ), const SizedBox(width: 8), - const Icon( - Icons.chevron_right, - color: Colors.white38, - ), + const Icon(Icons.chevron_right, color: Colors.white38), ], ), ), diff --git a/lib/presentation/widgets/info_row.dart b/lib/presentation/widgets/info_row.dart index 6c8e402b..786ccac1 100644 --- a/lib/presentation/widgets/info_row.dart +++ b/lib/presentation/widgets/info_row.dart @@ -27,13 +27,10 @@ class InfoRow extends StatelessWidget { ), ), Expanded( - child: Text( - value, - style: TextStyle(color: valueColor), - ), + child: Text(value, style: TextStyle(color: valueColor)), ), ], ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/widgets/music_kit_drop_card.dart b/lib/presentation/widgets/music_kit_drop_card.dart index fecab46f..214c4ce3 100644 --- a/lib/presentation/widgets/music_kit_drop_card.dart +++ b/lib/presentation/widgets/music_kit_drop_card.dart @@ -29,7 +29,10 @@ class MusicKitDropCard extends StatelessWidget { value: MusicKitUiHelper.typeLabel(drop.musicKit), ), if ((drop.musicKit.collection ?? '').isNotEmpty) - CollectibleInfoEntry(title: 'Series', value: drop.musicKit.collection!), + CollectibleInfoEntry( + title: 'Series', + value: drop.musicKit.collection!, + ), ], ); } diff --git a/lib/presentation/widgets/opening_loading_card.dart b/lib/presentation/widgets/opening_loading_card.dart index 6b4583dc..35b47672 100644 --- a/lib/presentation/widgets/opening_loading_card.dart +++ b/lib/presentation/widgets/opening_loading_card.dart @@ -33,14 +33,11 @@ class OpeningLoadingCard extends StatelessWidget { ), ), const SizedBox(height: 6), - Text( - subtitle, - style: const TextStyle(color: Colors.white70), - ), + Text(subtitle, style: const TextStyle(color: Colors.white70)), ], ), ), ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/widgets/skin_drop_card.dart b/lib/presentation/widgets/skin_drop_card.dart index 8817fcb3..de49513c 100644 --- a/lib/presentation/widgets/skin_drop_card.dart +++ b/lib/presentation/widgets/skin_drop_card.dart @@ -7,10 +7,7 @@ import 'info_row.dart'; class SkinDropCard extends StatelessWidget { final DroppedSkin drop; - const SkinDropCard({ - super.key, - required this.drop, - }); + const SkinDropCard({super.key, required this.drop}); @override Widget build(BuildContext context) { @@ -27,10 +24,7 @@ class SkinDropCard extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), gradient: LinearGradient( - colors: [ - rarityColor.withValues(alpha: 0.18), - Colors.transparent, - ], + colors: [rarityColor.withValues(alpha: 0.18), Colors.transparent], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), @@ -44,15 +38,14 @@ class SkinDropCard extends StatelessWidget { final image = Image.asset( drop.skin.skinImage, height: isNarrow ? 120 : 160, - errorBuilder: (_, error, stackTrace) => Icon( - Icons.image_not_supported, - size: isNarrow ? 64 : 80, - ), + errorBuilder: (_, error, stackTrace) => + Icon(Icons.image_not_supported, size: isNarrow ? 64 : 80), ); final info = Column( - crossAxisAlignment: - isNarrow ? CrossAxisAlignment.center : CrossAxisAlignment.start, + crossAxisAlignment: isNarrow + ? CrossAxisAlignment.center + : CrossAxisAlignment.start, children: [ Text( SkinUiHelper.fullDropDisplayName( @@ -92,41 +85,25 @@ class SkinDropCard extends StatelessWidget { title: 'Float', value: drop.skinFloat?.toStringAsFixed(6) ?? '-', ), - InfoRow( - title: 'Exterior', - value: drop.exterior ?? '-', - ), + InfoRow(title: 'Exterior', value: drop.exterior ?? '-'), if (drop.skin.collection != null && drop.skin.collection!.isNotEmpty) - InfoRow( - title: 'Collection', - value: drop.skin.collection!, - ), + InfoRow(title: 'Collection', value: drop.skin.collection!), ], ); if (isNarrow) { return Column( - children: [ - image, - const SizedBox(height: 12), - info, - ], + children: [image, const SizedBox(height: 12), info], ); } return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - flex: 4, - child: Center(child: image), - ), + Expanded(flex: 4, child: Center(child: image)), const SizedBox(width: 16), - Expanded( - flex: 5, - child: info, - ), + Expanded(flex: 5, child: info), ], ); }, diff --git a/lib/presentation/widgets/skin_grid_tile.dart b/lib/presentation/widgets/skin_grid_tile.dart index db649229..146de344 100644 --- a/lib/presentation/widgets/skin_grid_tile.dart +++ b/lib/presentation/widgets/skin_grid_tile.dart @@ -30,12 +30,12 @@ class SkinGridTile extends StatelessWidget { ), boxShadow: highlighted ? [ - BoxShadow( - color: rarityColor.withValues(alpha: 0.45), - blurRadius: 16, - spreadRadius: 2, - ), - ] + BoxShadow( + color: rarityColor.withValues(alpha: 0.45), + blurRadius: 16, + spreadRadius: 2, + ), + ] : null, ), child: Card( @@ -50,14 +50,11 @@ class SkinGridTile extends StatelessWidget { skin.skinImage, fit: BoxFit.contain, errorBuilder: (_, error, stackTrace) => - const Icon(Icons.image_not_supported), + const Icon(Icons.image_not_supported), ), ), ), - Container( - height: 5, - color: rarityColor, - ), + Container(height: 5, color: rarityColor), Padding( padding: EdgeInsets.all(compact ? 6 : 8), child: Column( diff --git a/lib/presentation/widgets/source_badge.dart b/lib/presentation/widgets/source_badge.dart index d1454832..926dde33 100644 --- a/lib/presentation/widgets/source_badge.dart +++ b/lib/presentation/widgets/source_badge.dart @@ -4,11 +4,7 @@ class SourceBadge extends StatelessWidget { final String label; final Color color; - const SourceBadge({ - super.key, - required this.label, - required this.color, - }); + const SourceBadge({super.key, required this.label, required this.color}); @override Widget build(BuildContext context) { diff --git a/lib/presentation/widgets/sticker_drop_card.dart b/lib/presentation/widgets/sticker_drop_card.dart index 9482c5b2..40fdd085 100644 --- a/lib/presentation/widgets/sticker_drop_card.dart +++ b/lib/presentation/widgets/sticker_drop_card.dart @@ -24,7 +24,10 @@ class StickerDropCard extends StatelessWidget { value: StickerUiHelper.rarityLabel(drop.sticker), valueColor: rarityColor, ), - CollectibleInfoEntry(title: 'Type', value: drop.sticker.stickerTypeLabel), + CollectibleInfoEntry( + title: 'Type', + value: drop.sticker.stickerTypeLabel, + ), CollectibleInfoEntry( title: 'Effect', value: StickerUiHelper.effectLabel(drop.sticker.effect), diff --git a/lib/presentation/widgets/tradeup_chance_card.dart b/lib/presentation/widgets/tradeup_chance_card.dart new file mode 100644 index 00000000..85e52293 --- /dev/null +++ b/lib/presentation/widgets/tradeup_chance_card.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import '../../domain/tradeup_service.dart'; +import '../helpers/skin_ui_helper.dart'; + +class TradeUpChanceCard extends StatelessWidget { + final TradeUpChance chance; + + const TradeUpChanceCard({super.key, required this.chance}); + + @override + Widget build(BuildContext context) { + final skin = chance.skin; + final color = SkinUiHelper.rarityColor(skin); + + return Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(4), + child: Image.asset( + skin.skinImage, + fit: BoxFit.contain, + errorBuilder: (_, _, _) => + const Icon(Icons.image_not_supported), + ), + ), + ), + Container(height: 3, color: color), + Padding( + padding: const EdgeInsets.all(6), + child: Column( + children: [ + Text( + SkinUiHelper.fullDropDisplayName( + skin: skin, + isStatTrak: chance.isStatTrak, + isSouvenir: chance.isSouvenir, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 10), + ), + const SizedBox(height: 3), + Text( + skin.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 10, color: Colors.white70), + ), + const SizedBox(height: 4), + Text( + '${(chance.probability * 100).toStringAsFixed(2)}%', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 2), + Text( + chance.exterior, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 9, color: Colors.white70), + ), + Text( + 'FV ${chance.floatValue.toStringAsFixed(5)}', + style: const TextStyle(fontSize: 9, color: Colors.white54), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tradeup_selected_slot.dart b/lib/presentation/widgets/tradeup_selected_slot.dart new file mode 100644 index 00000000..e9463029 --- /dev/null +++ b/lib/presentation/widgets/tradeup_selected_slot.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +import '../../domain/skin_float_helper.dart'; +import '../../domain/tradeup_service.dart'; +import '../helpers/skin_ui_helper.dart'; + +class TradeUpSelectedSlot extends StatelessWidget { + final TradeUpInputItem? item; + final VoidCallback? onTap; + final VoidCallback? onRemove; + + const TradeUpSelectedSlot({ + super.key, + required this.item, + this.onTap, + this.onRemove, + }); + + @override + Widget build(BuildContext context) { + if (item == null) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white24), + borderRadius: BorderRadius.circular(10), + ), + child: const Center(child: Icon(Icons.add)), + ); + } + + final skin = item!.skin; + final color = SkinUiHelper.rarityColor(skin); + + return Container( + decoration: BoxDecoration( + border: Border.all(color: color, width: 2), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + GestureDetector( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(4), + child: Column( + children: [ + Expanded( + child: Center( + child: Image.asset( + skin.skinImage, + fit: BoxFit.contain, + errorBuilder: (_, _, _) => + const Icon(Icons.image_not_supported), + ), + ), + ), + const SizedBox(height: 2), + Text( + 'FV ${item!.floatValue.toStringAsFixed(5)}', + style: const TextStyle(fontSize: 9, color: Colors.white70), + ), + Text( + _qualityLabel(item!.quality), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 8, color: Colors.white60), + ), + Text( + SkinFloatHelper.exteriorFromFloat(item!.floatValue), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 8, color: Colors.white54), + ), + ], + ), + ), + ), + Positioned( + right: 0, + top: 0, + child: Material( + color: Colors.black87, + borderRadius: BorderRadius.circular(8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: onRemove, + child: const Padding( + padding: EdgeInsets.all(4), + child: Icon(Icons.close, size: 12), + ), + ), + ), + ), + ], + ), + ); + } + + String _qualityLabel(TradeUpInputQuality quality) { + switch (quality) { + case TradeUpInputQuality.regular: + return 'Regular'; + case TradeUpInputQuality.statTrak: + return 'StatTrakā„¢'; + case TradeUpInputQuality.souvenir: + return ''; + } + } +} diff --git a/lib/presentation/widgets/tradeup_skin_tile.dart b/lib/presentation/widgets/tradeup_skin_tile.dart new file mode 100644 index 00000000..71e73255 --- /dev/null +++ b/lib/presentation/widgets/tradeup_skin_tile.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; + +import '../../data/models/skin_dto.dart'; +import '../helpers/skin_ui_helper.dart'; + +class TradeUpSkinTile extends StatelessWidget { + final SkinDto skin; + final int selectedCount; + final bool blocked; + final VoidCallback? onTap; + + const TradeUpSkinTile({ + super.key, + required this.skin, + required this.selectedCount, + required this.blocked, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final color = SkinUiHelper.rarityColor(skin); + + return Opacity( + opacity: blocked ? 0.55 : 1, + child: GestureDetector( + onTap: blocked ? null : onTap, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white10), + borderRadius: BorderRadius.circular(10), + ), + child: Stack( + children: [ + Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(4), + child: Center( + child: Image.asset( + skin.skinImage, + fit: BoxFit.contain, + errorBuilder: (_, _, _) => + const Icon(Icons.image_not_supported), + ), + ), + ), + ), + Container(height: 3, color: color), + Padding( + padding: const EdgeInsets.all(5), + child: Column( + children: [ + Text( + skin.itemDisplayName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 10), + ), + const SizedBox(height: 3), + Text( + skin.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 10, + color: Colors.white70, + ), + ), + if (skin.collection != null && + skin.collection!.isNotEmpty) ...[ + const SizedBox(height: 3), + Text( + skin.collection!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 9, + color: Colors.white38, + ), + ), + ], + ], + ), + ), + ], + ), + if (selectedCount > 0) + Positioned( + right: 4, + top: 4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + 'x$selectedCount', + style: const TextStyle(fontSize: 10), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/xray_reveal_card.dart b/lib/presentation/widgets/xray_reveal_card.dart index acbe6ed8..5336f128 100644 --- a/lib/presentation/widgets/xray_reveal_card.dart +++ b/lib/presentation/widgets/xray_reveal_card.dart @@ -34,10 +34,8 @@ class _XrayRevealCardState extends State @override void initState() { super.initState(); - _controller = AnimationController( - vsync: this, - duration: _scanDuration, - )..addStatusListener((status) { + _controller = AnimationController(vsync: this, duration: _scanDuration) + ..addStatusListener((status) { if (status == AnimationStatus.completed && mounted) { setState(() { _scanCompleted = true; @@ -92,8 +90,10 @@ class _XrayRevealCardState extends State final progress = _controller.value.clamp(0.0, 1.0); final scanY = _scanViewportHeight * progress; final unrevealedTop = scanY.clamp(0.0, _scanViewportHeight); - final unrevealedHeight = - (_scanViewportHeight - unrevealedTop).clamp(0.0, _scanViewportHeight); + final unrevealedHeight = (_scanViewportHeight - unrevealedTop).clamp( + 0.0, + _scanViewportHeight, + ); final lineTop = (scanY - 1).clamp(0.0, _scanViewportHeight - 2); return Container( @@ -115,19 +115,14 @@ class _XrayRevealCardState extends State gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - xrayBg2, - xrayBg, - ], + colors: [xrayBg2, xrayBg], ), ), clipBehavior: Clip.antiAlias, child: Stack( fit: StackFit.expand, children: [ - const Positioned.fill( - child: _XrayGridOverlay(), - ), + const Positioned.fill(child: _XrayGridOverlay()), Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( @@ -144,18 +139,14 @@ class _XrayRevealCardState extends State ), ), ), - Positioned.fill( - child: _buildFixedSkinImage(), - ), + Positioned.fill(child: _buildFixedSkinImage()), if (unrevealedHeight > 0) Positioned( left: 0, right: 0, top: unrevealedTop, height: unrevealedHeight, - child: Container( - color: maskColor, - ), + child: Container(color: maskColor), ), Positioned( left: 0, @@ -220,9 +211,7 @@ class _XrayRevealCardState extends State decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.34), borderRadius: BorderRadius.circular(999), - border: Border.all( - color: xrayGlow.withValues(alpha: 0.35), - ), + border: Border.all(color: xrayGlow.withValues(alpha: 0.35)), ), child: Text( _scanCompleted @@ -261,8 +250,9 @@ class _XrayRevealCardState extends State decoration: BoxDecoration( border: Border( left: BorderSide(color: xrayGlow.withValues(alpha: 0.65)), - bottom: - BorderSide(color: xrayGlow.withValues(alpha: 0.65)), + bottom: BorderSide( + color: xrayGlow.withValues(alpha: 0.65), + ), ), ), ), @@ -275,9 +265,12 @@ class _XrayRevealCardState extends State height: 22, decoration: BoxDecoration( border: Border( - right: BorderSide(color: xrayGlow.withValues(alpha: 0.65)), - bottom: - BorderSide(color: xrayGlow.withValues(alpha: 0.65)), + right: BorderSide( + color: xrayGlow.withValues(alpha: 0.65), + ), + bottom: BorderSide( + color: xrayGlow.withValues(alpha: 0.65), + ), ), ), ), @@ -301,10 +294,7 @@ class _XrayRevealCardState extends State margin: const EdgeInsets.all(12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), - side: const BorderSide( - color: xrayGlow, - width: 1.15, - ), + side: const BorderSide(color: xrayGlow, width: 1.15), ), color: xrayPanel, child: Padding( @@ -329,10 +319,7 @@ class _XrayRevealCardState extends State padding: EdgeInsets.only(top: 16), child: Text( 'Running X-Ray scan...', - style: TextStyle( - color: Colors.white70, - fontSize: 14, - ), + style: TextStyle(color: Colors.white70, fontSize: 14), ), ) : Column( @@ -360,7 +347,8 @@ class _XrayRevealCardState extends State ), InfoRow( title: 'Float', - value: widget.drop.skinFloat?.toStringAsFixed(6) ?? '-', + value: + widget.drop.skinFloat?.toStringAsFixed(6) ?? '-', ), InfoRow( title: 'Exterior', @@ -377,8 +365,9 @@ class _XrayRevealCardState extends State child: OutlinedButton( onPressed: widget.onDestroy, style: OutlinedButton.styleFrom( - padding: - const EdgeInsets.symmetric(vertical: 14), + padding: const EdgeInsets.symmetric( + vertical: 14, + ), ), child: const Text('DESTROY'), ), @@ -388,8 +377,9 @@ class _XrayRevealCardState extends State child: ElevatedButton( onPressed: widget.onClaim, style: ElevatedButton.styleFrom( - padding: - const EdgeInsets.symmetric(vertical: 14), + padding: const EdgeInsets.symmetric( + vertical: 14, + ), ), child: const Text('CLAIM ITEM'), ), @@ -411,9 +401,7 @@ class _XrayGridOverlay extends StatelessWidget { @override Widget build(BuildContext context) { - return CustomPaint( - painter: _XrayGridPainter(), - ); + return CustomPaint(painter: _XrayGridPainter()); } } diff --git a/pubspec.yaml b/pubspec.yaml index 7ddd4641..add6e9b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: cs2_simulator description: "Counter-Strike 2 case opening and Trade-Up contract creation simulator written in Flutter + Dart." publish_to: 'none' -version: 0.9.2 +version: 0.10.0 environment: sdk: ^3.11.3 @@ -23,34 +23,34 @@ dev_dependencies: webp: ^1.0.1 flutter_launcher_icons: - image_path: assets/app_icon/latest_case.png + image_path: assets/app_icon/latest_container.png android: true adaptive_icon_background: assets/app_icon/transparent_bg.png - adaptive_icon_foreground: assets/app_icon/latest_case_foreground.png - adaptive_icon_monochrome: assets/app_icon/latest_case_monochrome.png + adaptive_icon_foreground: assets/app_icon/latest_container_foreground.png + adaptive_icon_monochrome: assets/app_icon/latest_container_monochrome.png adaptive_icon_foreground_inset: 18 ios: true remove_alpha_ios: false - image_path_ios_dark_transparent: assets/app_icon/latest_case_ios_dark.png - image_path_ios_tinted_grayscale: assets/app_icon/latest_case_ios_tinted.png + image_path_ios_dark_transparent: assets/app_icon/latest_container_ios_dark.png + image_path_ios_tinted_grayscale: assets/app_icon/latest_container_ios_tinted.png desaturate_tinted_to_grayscale_ios: false macos: generate: true - image_path: assets/app_icon/latest_case.png + image_path: assets/app_icon/latest_container.png windows: generate: true - image_path: assets/app_icon/latest_case.png + image_path: assets/app_icon/latest_container.png icon_size: 256 linux: true web: generate: true - image_path: assets/app_icon/latest_case.png + image_path: assets/app_icon/latest_container.png background_color: "#00000000" theme_color: "#00000000" @@ -66,8 +66,6 @@ flutter: - assets/agents/ - assets/graffiti/ - assets/patches/ - - assets/cases/ - - assets/agent_collections/ - - assets/reward_collections/ - - assets/operation_collections/ + - assets/charms/ + - assets/containers/ - assets/tournament_logos/ diff --git a/test/widget_test.dart b/test/widget_test.dart index c9b71198..7cd19bc1 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +1,34 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:cs2_simulator/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const Cs2SimulatorApp()); + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + }); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); + tearDown(() { + SharedPreferencesAsyncPlatform.instance = null; + }); + + testWidgets('App starts and shows the home screen', ( + WidgetTester tester, + ) async { + await tester.binding.setSurfaceSize(const Size(1440, 1200)); + + await tester.pumpWidget(const Cs2SimulatorApp()); + await tester.pumpAndSettle(); - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); + expect(find.text('CS2 Simulator'), findsOneWidget); + expect(find.text('Open Containers'), findsOneWidget); + expect(find.text('Trade-Up'), findsOneWidget); - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + await tester.binding.setSurfaceSize(null); }); } diff --git a/tool/generate_app_icon.dart b/tool/generate_app_icon.dart index 4060b2c8..85c2aebc 100644 --- a/tool/generate_app_icon.dart +++ b/tool/generate_app_icon.dart @@ -6,34 +6,21 @@ import 'package:image/image.dart' as img; void main() async { final root = Directory.current; - final casesFile = File('${root.path}/assets/data/cases.json'); - final rewardCollectionsFile = - File('${root.path}/assets/data/reward_collections.json'); - final operationCollectionsFile = - File('${root.path}/assets/data/operation_collections.json'); - - if (!casesFile.existsSync()) { - stderr.writeln('cases.json not found: ${casesFile.path}'); + final containersFile = File('${root.path}/assets/data/containers.json'); + + if (!containersFile.existsSync()) { + stderr.writeln('containers.json not found: ${containersFile.path}'); exit(1); } - final cases = _readJsonList(await casesFile.readAsString(), 'cases.json'); - final rewardCollections = rewardCollectionsFile.existsSync() - ? _readJsonList( - await rewardCollectionsFile.readAsString(), - 'reward_collections.json', - ) - : >[]; - final operationCollections = operationCollectionsFile.existsSync() - ? _readJsonList( - await operationCollectionsFile.readAsString(), - 'operation_collections.json', - ) - : >[]; + final containers = _readJsonList( + await containersFile.readAsString(), + 'containers.json', + ); final candidates = <_IconCandidate>[]; - for (final item in cases) { + for (final item in containers) { final name = (item['name'] as String?)?.trim() ?? ''; if (name.isEmpty) continue; @@ -45,11 +32,11 @@ void main() async { final tournamentLogo = (item['tournamentLogo'] as String?)?.trim(); imageRel = (tournamentLogo != null && tournamentLogo.isNotEmpty) ? tournamentLogo - : (item['caseImage'] as String?)?.trim(); + : (item['containerImage'] as String?)?.trim(); break; default: - imageRel = (item['caseImage'] as String?)?.trim(); + imageRel = (item['containerImage'] as String?)?.trim(); break; } @@ -66,38 +53,6 @@ void main() async { ); } - for (final item in rewardCollections) { - final name = (item['name'] as String?)?.trim() ?? ''; - final imageRel = (item['image'] as String?)?.trim() ?? ''; - if (name.isEmpty || imageRel.isEmpty) continue; - - candidates.add( - _IconCandidate( - name: name, - kind: 'REWARD_COLLECTION', - sourceType: (item['sourceType'] as String?)?.trim() ?? 'REWARD_COLLECTION', - releaseDate: (item['releaseDate'] as String?)?.trim(), - imageRelPath: imageRel, - ), - ); - } - - for (final item in operationCollections) { - final name = (item['name'] as String?)?.trim() ?? ''; - final imageRel = (item['image'] as String?)?.trim() ?? ''; - if (name.isEmpty || imageRel.isEmpty) continue; - - candidates.add( - _IconCandidate( - name: name, - kind: 'OPERATION_COLLECTION', - sourceType: (item['operationId'] as String?)?.trim() ?? 'OPERATION_COLLECTION', - releaseDate: (item['releaseDate'] as String?)?.trim(), - imageRelPath: imageRel, - ), - ); - } - if (candidates.isEmpty) { stderr.writeln('No icon candidates found.'); exit(1); @@ -139,10 +94,7 @@ void main() async { height: canvasSize, numChannels: 4, ); - img.fill( - standardCanvas, - color: img.ColorRgba8(0, 0, 0, 0), - ); + img.fill(standardCanvas, color: img.ColorRgba8(0, 0, 0, 0)); final standardFitted = _resizeToFit( croppedSource, @@ -157,18 +109,16 @@ void main() async { dstY: ((canvasSize - standardFitted.height) / 2).round(), ); - await File('${outDir.path}/latest_case.png') - .writeAsBytes(img.encodePng(standardCanvas)); + await File( + '${outDir.path}/latest_container.png', + ).writeAsBytes(img.encodePng(standardCanvas)); final foregroundCanvas = img.Image( width: canvasSize, height: canvasSize, numChannels: 4, ); - img.fill( - foregroundCanvas, - color: img.ColorRgba8(0, 0, 0, 0), - ); + img.fill(foregroundCanvas, color: img.ColorRgba8(0, 0, 0, 0)); final foregroundFitted = _resizeToFit( croppedSource, @@ -183,8 +133,9 @@ void main() async { dstY: ((canvasSize - foregroundFitted.height) / 2).round(), ); - await File('${outDir.path}/latest_case_foreground.png') - .writeAsBytes(img.encodePng(foregroundCanvas)); + await File( + '${outDir.path}/latest_container_foreground.png', + ).writeAsBytes(img.encodePng(foregroundCanvas)); final monoSource = img.copyResize( croppedSource, @@ -199,10 +150,7 @@ void main() async { height: canvasSize, numChannels: 4, ); - img.fill( - monoCanvas, - color: img.ColorRgba8(0, 0, 0, 0), - ); + img.fill(monoCanvas, color: img.ColorRgba8(0, 0, 0, 0)); img.compositeImage( monoCanvas, monoConverted, @@ -210,18 +158,16 @@ void main() async { dstY: ((canvasSize - monoConverted.height) / 2).round(), ); - await File('${outDir.path}/latest_case_monochrome.png') - .writeAsBytes(img.encodePng(monoCanvas)); + await File( + '${outDir.path}/latest_container_monochrome.png', + ).writeAsBytes(img.encodePng(monoCanvas)); final iosDarkCanvas = img.Image( width: canvasSize, height: canvasSize, numChannels: 4, ); - img.fill( - iosDarkCanvas, - color: img.ColorRgba8(0, 0, 0, 0), - ); + img.fill(iosDarkCanvas, color: img.ColorRgba8(0, 0, 0, 0)); final iosDarkFitted = _resizeToFit( croppedSource, @@ -236,18 +182,16 @@ void main() async { dstY: ((canvasSize - iosDarkFitted.height) / 2).round(), ); - await File('${outDir.path}/latest_case_ios_dark.png') - .writeAsBytes(img.encodePng(iosDarkCanvas)); + await File( + '${outDir.path}/latest_container_ios_dark.png', + ).writeAsBytes(img.encodePng(iosDarkCanvas)); final tintedCanvas = img.Image( width: canvasSize, height: canvasSize, numChannels: 4, ); - img.fill( - tintedCanvas, - color: img.ColorRgba8(0, 0, 0, 0), - ); + img.fill(tintedCanvas, color: img.ColorRgba8(0, 0, 0, 0)); final tintedSource = img.copyResize( croppedSource, @@ -264,31 +208,30 @@ void main() async { dstY: ((canvasSize - tintedGray.height) / 2).round(), ); - await File('${outDir.path}/latest_case_ios_tinted.png') - .writeAsBytes(img.encodePng(tintedCanvas)); + await File( + '${outDir.path}/latest_container_ios_tinted.png', + ).writeAsBytes(img.encodePng(tintedCanvas)); final transparentBg = img.Image( width: canvasSize, height: canvasSize, numChannels: 4, ); - img.fill( - transparentBg, - color: img.ColorRgba8(0, 0, 0, 0), - ); + img.fill(transparentBg, color: img.ColorRgba8(0, 0, 0, 0)); - await File('${outDir.path}/transparent_bg.png') - .writeAsBytes(img.encodePng(transparentBg)); + await File( + '${outDir.path}/transparent_bg.png', + ).writeAsBytes(img.encodePng(transparentBg)); stdout.writeln('Latest item: ${latest.name}'); stdout.writeln('Kind: ${latest.kind}'); stdout.writeln('Source type: ${latest.sourceType}'); stdout.writeln('Selected image: ${latest.imageRelPath}'); - stdout.writeln('Generated: assets/app_icon/latest_case.png'); - stdout.writeln('Generated: assets/app_icon/latest_case_foreground.png'); - stdout.writeln('Generated: assets/app_icon/latest_case_monochrome.png'); - stdout.writeln('Generated: assets/app_icon/latest_case_ios_dark.png'); - stdout.writeln('Generated: assets/app_icon/latest_case_ios_tinted.png'); + stdout.writeln('Generated: assets/app_icon/latest_container.png'); + stdout.writeln('Generated: assets/app_icon/latest_container_foreground.png'); + stdout.writeln('Generated: assets/app_icon/latest_container_monochrome.png'); + stdout.writeln('Generated: assets/app_icon/latest_container_ios_dark.png'); + stdout.writeln('Generated: assets/app_icon/latest_container_ios_tinted.png'); stdout.writeln('Generated: assets/app_icon/transparent_bg.png'); } @@ -322,10 +265,10 @@ List> _readJsonList(String raw, String debugName) { } img.Image _resizeToFit( - img.Image source, { - required int maxWidth, - required int maxHeight, - }) { + img.Image source, { + required int maxWidth, + required int maxHeight, +}) { final widthRatio = maxWidth / source.width; final heightRatio = maxHeight / source.height; final ratio = widthRatio < heightRatio ? widthRatio : heightRatio; @@ -459,14 +402,14 @@ img.Image _trimSolidBackground(img.Image source) { } bool _isCloseColor( - int r1, - int g1, - int b1, - int r2, - int g2, - int b2, - int tolerance, - ) { + int r1, + int g1, + int b1, + int r2, + int g2, + int b2, + int tolerance, +) { return (r1 - r2).abs() <= tolerance && (g1 - g2).abs() <= tolerance && (b1 - b2).abs() <= tolerance; @@ -527,4 +470,4 @@ img.Image _toGrayscalePreserveAlpha(img.Image source) { } return out; -} \ No newline at end of file +} diff --git a/tool/importer/src/config.dart b/tool/importer/src/config.dart index 5d4eb3c7..7cd4a5c7 100644 --- a/tool/importer/src/config.dart +++ b/tool/importer/src/config.dart @@ -10,13 +10,14 @@ const musicKitsUrl = '$baseUrl/music_kits.json'; const agentsUrl = '$baseUrl/agents.json'; const graffitiUrl = '$baseUrl/graffiti.json'; const patchesUrl = '$baseUrl/patches.json'; +const keychainsUrl = '$baseUrl/keychains.json'; const timeoutSeconds = 30; final outRoot = Directory.current; final assetsDir = Directory('${outRoot.path}/assets'); final dataDir = Directory('${assetsDir.path}/data'); -final casesDir = Directory('${assetsDir.path}/cases'); +final containersDir = Directory('${assetsDir.path}/containers'); final skinsDir = Directory('${assetsDir.path}/skins'); final stickersDir = Directory('${assetsDir.path}/stickers'); final pinsDir = Directory('${assetsDir.path}/pins'); @@ -24,11 +25,7 @@ final musicKitsDir = Directory('${assetsDir.path}/music_kits'); final agentsDir = Directory('${assetsDir.path}/agents'); final graffitiDir = Directory('${assetsDir.path}/graffiti'); final patchesDir = Directory('${assetsDir.path}/patches'); -final rewardCollectionsDir = Directory('${assetsDir.path}/reward_collections'); -final operationCollectionsDir = Directory( - '${assetsDir.path}/operation_collections', -); -final agentCollectionsDir = Directory('${assetsDir.path}/agent_collections'); +final charmsDir = Directory('${assetsDir.path}/charms'); final tournamentLogosDir = Directory('${assetsDir.path}/tournament_logos'); final rewardOverridesPath = File( @@ -91,6 +88,13 @@ const patchRarityMap = { 'Exotic': 'EXOTIC', }; +const charmRarityMap = { + 'High Grade': 'HIGH_GRADE', + 'Remarkable': 'REMARKABLE', + 'Extraordinary': 'EXTRAORDINARY', + 'Exotic': 'EXOTIC', +}; + const agentCollectionSourceOverrides = >{ 'Shattered Web Agents': { 'operationId': 'SHATTERED_WEB', @@ -124,6 +128,33 @@ const patchCollectionSourceOverrides = >{ }, }; +const charmCollectionSourceOverrides = >{ + 'Missing Link Charm Collection': { + 'sourceType': 'ARMORY_REWARD', + 'sourceId': 'ARMORY', + 'sourceName': 'The Armory', + 'releaseDate': '2024-10-02', + }, + 'Small Arms Charm Collection': { + 'sourceType': 'ARMORY_REWARD', + 'sourceId': 'ARMORY', + 'sourceName': 'The Armory', + 'releaseDate': '2024-10-02', + }, + 'Missing Link Community Charm Collection': { + 'sourceType': 'ARMORY_REWARD', + 'sourceId': 'ARMORY', + 'sourceName': 'The Armory', + 'releaseDate': '2025-10-02', + }, + 'Dr Boom Charm Collection': { + 'sourceType': 'ARMORY_REWARD', + 'sourceId': 'ARMORY', + 'sourceName': 'The Armory', + 'releaseDate': '2025-10-02', + }, +}; + const stickerCollectionSourceOverrides = >{ 'Shattered Web Sticker Collection': { 'sourceType': 'LEGACY_OPERATION', diff --git a/tool/importer/src/dart_backend.dart b/tool/importer/src/dart_backend.dart index df817276..85bed203 100644 --- a/tool/importer/src/dart_backend.dart +++ b/tool/importer/src/dart_backend.dart @@ -18,9 +18,10 @@ class DartImporterBackend implements ImporterBackend { final rewardSourceOverrides = _loadRewardOverrides(); final operationCollectionOverrides = _loadOperationOverrides(); - final allExistingCases = _io.loadJsonList( - File('${dataDir.path}/cases.json'), - ); + final allExistingCases = _io + .loadJsonList(File('${dataDir.path}/containers.json')) + .map(_normalizeExistingContainerMeta) + .toList(); final existingSkins = _io.loadJsonList(File('${dataDir.path}/skins.json')); final existingStickers = _io.loadJsonList( File('${dataDir.path}/stickers.json'), @@ -29,13 +30,18 @@ class DartImporterBackend implements ImporterBackend { final existingMusicKits = _io.loadJsonList( File('${dataDir.path}/music_kits.json'), ); - final existingAgents = _io.loadJsonList(File('${dataDir.path}/agents.json')); + final existingAgents = _io.loadJsonList( + File('${dataDir.path}/agents.json'), + ); final existingGraffiti = _io.loadJsonList( File('${dataDir.path}/graffiti.json'), ); final existingPatches = _io.loadJsonList( File('${dataDir.path}/patches.json'), ); + final existingCharms = _io.loadJsonList( + File('${dataDir.path}/charms.json'), + ); final existingCases = allExistingCases.where((item) { final type = (item['type'] ?? '').toString().trim().toUpperCase(); return !{ @@ -45,15 +51,61 @@ class DartImporterBackend implements ImporterBackend { 'MUSIC_KIT_BOX', }.contains(type); }).toList(); - final existingRewardCollections = _io.loadJsonList( - File('${dataDir.path}/reward_collections.json'), - ); - final existingOperationCollections = _io.loadJsonList( - File('${dataDir.path}/operation_collections.json'), - ); - final existingAgentCollections = _io.loadJsonList( - File('${dataDir.path}/agent_collections.json'), - ); + final existingRewardCollections = allExistingCases + .where( + (item) => + (item['type'] ?? '').toString().trim().toUpperCase() == + 'REWARD_COLLECTION', + ) + .map( + (item) => { + 'id': item['id'], + 'name': item['name'], + 'image': item['containerImage'], + 'sourceType': (item['sourceType'] ?? '') == 'ARMORY_REWARD' + ? 'ARMORY' + : 'OPERATION', + 'sourceId': item['sourceId'], + 'currency': item['currency'], + 'cost': item['cost'], + 'releaseDate': item['releaseDate'], + }, + ) + .toList(); + final existingOperationCollections = allExistingCases + .where( + (item) => + (item['type'] ?? '').toString().trim().toUpperCase() == + 'OPERATION_COLLECTION', + ) + .map( + (item) => { + 'id': item['id'], + 'name': item['name'], + 'image': item['containerImage'], + 'operationId': item['sourceId'], + 'operationName': item['sourceName'], + 'releaseDate': item['releaseDate'], + }, + ) + .toList(); + final existingAgentCollections = allExistingCases + .where( + (item) => + (item['type'] ?? '').toString().trim().toUpperCase() == + 'AGENT_COLLECTION', + ) + .map( + (item) => { + 'id': item['id'], + 'name': item['name'], + 'image': item['containerImage'], + 'operationId': item['sourceId'], + 'operationName': item['sourceName'], + 'releaseDate': item['releaseDate'], + }, + ) + .toList(); final existingSkinByKey = <(String, String, String, String), Map>{ @@ -69,11 +121,10 @@ class DartImporterBackend implements ImporterBackend { for (final p in existingPins) existingPinKey(p): Map.from(p), }; - final existingMusicKitByKey = - <(String, String, bool), Map>{ - for (final m in existingMusicKits) - existingMusicKitKey(m): Map.from(m), - }; + final existingMusicKitByKey = <(String, String), Map>{ + for (final m in existingMusicKits) + existingMusicKitKey(m): Map.from(m), + }; final existingAgentByKey = <(String, String, String), Map>{ for (final a in existingAgents) existingAgentKey(a): Map.from(a), @@ -86,6 +137,10 @@ class DartImporterBackend implements ImporterBackend { for (final p in existingPatches) existingPatchKey(p): Map.from(p), }; + final existingCharmByKey = <(String, String), Map>{ + for (final c in existingCharms) + existingCharmKey(c): Map.from(c), + }; final existingCaseByName = >{ for (final c in allExistingCases) existingCaseKey(c): Map.from(c), @@ -108,7 +163,9 @@ class DartImporterBackend implements ImporterBackend { operationKey( (c['name'] ?? '').toString(), (c['operationId'] ?? '').toString(), - ): Map.from(c), + ): Map.from( + c, + ), }; final usedSkinIds = _extractUsedIds(existingSkins); @@ -118,6 +175,7 @@ class DartImporterBackend implements ImporterBackend { final usedAgentIds = _extractUsedIds(existingAgents); final usedGraffitiIds = _extractUsedIds(existingGraffiti); final usedPatchIds = _extractUsedIds(existingPatches); + final usedCharmIds = _extractUsedIds(existingCharms); final usedCaseIds = _extractUsedIds(allExistingCases); final usedRewardIds = _extractUsedIds(existingRewardCollections); final usedOperationIds = _extractUsedIds(existingOperationCollections); @@ -130,6 +188,7 @@ class DartImporterBackend implements ImporterBackend { var nextAgentId = _nextId(usedAgentIds, 980000000); var nextGraffitiId = _nextId(usedGraffitiIds, 990000000); var nextPatchId = _nextId(usedPatchIds, 995000000); + var nextCharmId = _nextId(usedCharmIds, 996000000); var nextCaseId = _nextId(usedCaseIds, 0); var nextRewardId = _nextId(usedRewardIds, 10000); var nextOperationId = _nextId(usedOperationIds, 20000); @@ -151,6 +210,8 @@ class DartImporterBackend implements ImporterBackend { final graffitiData = _asJsonList(await _io.fetchJson(graffitiUrl)); _io.printInfo('Fetching patches.json ...'); final patchesData = _asJsonList(await _io.fetchJson(patchesUrl)); + _io.printInfo('Fetching keychains.json ...'); + final keychainsData = _asJsonList(await _io.fetchJson(keychainsUrl)); final collectionImageByName = buildCollectionImageMap(skinsData); buildCollectionMetaMap(collectionsData); @@ -195,7 +256,8 @@ class DartImporterBackend implements ImporterBackend { operationKey( (c['name'] ?? '').toString(), (c['operationId'] ?? '').toString(), - ): (c['id'] ?? '').toString(), + ): (c['id'] ?? '') + .toString(), }; final supportedCrates = crates.where(isSupportedContainer).toList() @@ -240,7 +302,7 @@ class DartImporterBackend implements ImporterBackend { } final existingCase = existingCaseByName[crateName]; - final caseId = existingCase != null + final containerId = existingCase != null ? existingCase['id'].toString() : (nextCaseId++).toString(); var releaseDate = existingCase?['releaseDate']; @@ -284,21 +346,21 @@ class DartImporterBackend implements ImporterBackend { } } - final caseImagePath = await _syncAsset( + final containerImagePath = await _syncAsset( imageUrl: crate['image']?.toString(), - dirPath: casesDir.path, - relativeDir: 'assets/cases', - id: caseId, - existingRelativePath: existingCase?['caseImage']?.toString(), + dirPath: containersDir.path, + relativeDir: 'assets/containers', + id: containerId, + existingRelativePath: existingCase?['containerImage']?.toString(), ); final patchCollectionSource = containerType == 'PATCH_COLLECTION' ? resolvePatchCollectionSource(crateName) : const {}; final caseRecord = { - 'id': caseId, + 'id': containerId, 'name': crateName, - 'caseImage': caseImagePath, + 'containerImage': containerImagePath, 'releaseDate': releaseDate, 'type': containerType, 'tournamentName': tournamentName, @@ -308,8 +370,63 @@ class DartImporterBackend implements ImporterBackend { 'sourceName': patchCollectionSource['sourceName'], }; - newCases[caseId] = caseRecord; - caseNameToId[crateName] = caseId; + newCases[containerId] = caseRecord; + caseNameToId[crateName] = containerId; + } + + final charmCollections = + collectionsData.where((collection) { + final name = (collection['name'] ?? '').toString().trim(); + final contains = collection['contains']; + return name.endsWith('Charm Collection') && + contains is List && + contains.isNotEmpty; + }).toList()..sort( + (a, b) => ((a['name'] ?? '').toString()).compareTo( + (b['name'] ?? '').toString(), + ), + ); + + for (final collection in charmCollections) { + final collectionName = (collection['name'] ?? '').toString().trim(); + if (collectionName.isEmpty) { + continue; + } + + final existingCase = existingCaseByName[collectionName]; + final containerId = existingCase != null + ? existingCase['id'].toString() + : (nextCaseId++).toString(); + final sourceMeta = resolveCharmCollectionSource(collectionName); + final releaseDate = sourceMeta['releaseDate']; + if (releaseDate == null) { + throw StateError( + 'Missing hardcoded release date for charm collection: $collectionName', + ); + } + + final containerImagePath = await _syncAsset( + imageUrl: collection['image']?.toString(), + dirPath: containersDir.path, + relativeDir: 'assets/containers', + id: containerId, + existingRelativePath: existingCase?['containerImage']?.toString(), + compressionModeOverride: CompressionMode.maxCompress, + ); + + newCases[containerId] = { + 'id': containerId, + 'name': collectionName, + 'containerImage': containerImagePath, + 'releaseDate': releaseDate, + 'type': 'CHARM_COLLECTION', + 'tournamentName': null, + 'tournamentLogo': null, + 'sourceType': sourceMeta['sourceType'], + 'sourceId': sourceMeta['sourceId'], + 'sourceName': sourceMeta['sourceName'], + }; + caseNameToId[collectionName] = containerId; } final newSkins = >{}; @@ -319,20 +436,24 @@ class DartImporterBackend implements ImporterBackend { final newAgents = >{}; final newGraffiti = >{}; final newPatches = >{}; + final newCharms = >{}; final skinIdByFullName = {}; - final caseContentsMap = >{ - for (final caseId in newCases.keys) caseId: {}, + final containerContentsMap = >{ + for (final containerId in newCases.keys) containerId: {}, }; final stickerContentsMap = >{}; final pinContentsMap = >{}; - final musicKitContentsMap = >{}; + final musicKitContentsMap = >>{}; + final musicKitVariantPresence = <(String, String), Map>{}; final musicKitCollectionBySourceId = {}; final agentCollectionContentsMap = >{ - for (final collectionId in newAgentCollections.keys) collectionId: {}, + for (final collectionId in newAgentCollections.keys) + collectionId: {}, }; final graffitiContentsMap = >{}; final patchContentsMap = >{}; + final charmContentsMap = >{}; final rewardContentsMap = >{ for (final collectionId in newRewardCollections.keys) collectionId: {}, @@ -349,6 +470,7 @@ class DartImporterBackend implements ImporterBackend { var createdAgentCount = 0; var createdGraffitiCount = 0; var createdPatchCount = 0; + var createdCharmCount = 0; var reusedSkinCount = 0; var reusedStickerCount = 0; var reusedPinCount = 0; @@ -356,6 +478,7 @@ class DartImporterBackend implements ImporterBackend { var reusedAgentCount = 0; var reusedGraffitiCount = 0; var reusedPatchCount = 0; + var reusedCharmCount = 0; var skippedUnknownItems = 0; var containerRefsCreatedFromSkinMeta = 0; var rewardCollectionsCreated = 0; @@ -520,8 +643,8 @@ class DartImporterBackend implements ImporterBackend { final rewardImagePath = await _syncAsset( imageUrl: collectionImageByName[collectionName], - dirPath: rewardCollectionsDir.path, - relativeDir: 'assets/reward_collections', + dirPath: containersDir.path, + relativeDir: 'assets/containers', id: rewardId, existingRelativePath: (newRewardCollections[rewardId] ?? existingReward)?['image'] @@ -543,6 +666,25 @@ class DartImporterBackend implements ImporterBackend { rewardCollectionsCreated += 1; } newRewardCollections[rewardId] = rewardRecord; + newCases[rewardId] = { + 'id': rewardId, + 'name': collectionName, + 'containerImage': rewardImagePath, + 'releaseDate': rewardMeta['releaseDate'], + 'type': 'REWARD_COLLECTION', + 'tournamentName': null, + 'tournamentLogo': null, + 'sourceType': rewardMeta['sourceType'] == 'ARMORY' + ? 'ARMORY_REWARD' + : 'OPERATION_REWARD', + 'sourceId': rewardMeta['sourceId'], + 'sourceName': rewardRecord['sourceType'] == 'ARMORY' + ? 'The Armory' + : resolveOperationNameFromId(rewardMeta['sourceId']?.toString()), + 'currency': rewardMeta['currency'] ?? 'STARS', + 'cost': rewardMeta['cost'] ?? 4, + }; + caseNameToId[collectionName] = rewardId; rewardContentsMap.putIfAbsent(rewardId, () => {}).add(skinId); } @@ -564,8 +706,8 @@ class DartImporterBackend implements ImporterBackend { final operationImagePath = await _syncAsset( imageUrl: collectionImageByName[collectionName ?? ''], - dirPath: operationCollectionsDir.path, - relativeDir: 'assets/operation_collections', + dirPath: containersDir.path, + relativeDir: 'assets/containers', id: opId, existingRelativePath: (newOperationCollections[opId] ?? existingOperation)?['image'] @@ -585,6 +727,21 @@ class DartImporterBackend implements ImporterBackend { operationCollectionsCreated += 1; } newOperationCollections[opId] = operationRecord; + newCases[opId] = { + 'id': opId, + 'name': collectionName, + 'containerImage': operationImagePath, + 'releaseDate': operationMeta['releaseDate'], + 'type': 'OPERATION_COLLECTION', + 'tournamentName': null, + 'tournamentLogo': null, + 'sourceType': 'LEGACY_OPERATION', + 'sourceId': operationMeta['operationId'], + 'sourceName': operationMeta['operationName'], + 'currency': null, + 'cost': null, + }; + caseNameToId[collectionName ?? ''] = opId; operationContentsMap.putIfAbsent(opId, () => {}).add(skinId); } @@ -600,8 +757,8 @@ class DartImporterBackend implements ImporterBackend { continue; } - var caseId = caseNameToId[crateName]; - if (caseId == null) { + var containerId = caseNameToId[crateName]; + if (containerId == null) { final existingCaseMeta = existingCaseByName[crateName]; final releaseDate = resolveContainerReleaseDate( crateName: crateName, @@ -609,20 +766,21 @@ class DartImporterBackend implements ImporterBackend { crateMeta: Map.from(crateRef), existingReleaseDate: existingCaseMeta?['releaseDate'], ); - caseId = existingCaseMeta != null + containerId = existingCaseMeta != null ? existingCaseMeta['id'].toString() : (nextCaseId++).toString(); - final caseImagePath = await _syncAsset( + final containerImagePath = await _syncAsset( imageUrl: crateRef['image']?.toString(), - dirPath: casesDir.path, - relativeDir: 'assets/cases', - id: caseId, - existingRelativePath: existingCaseMeta?['caseImage']?.toString(), + dirPath: containersDir.path, + relativeDir: 'assets/containers', + id: containerId, + existingRelativePath: existingCaseMeta?['containerImage'] + ?.toString(), ); - newCases[caseId] = { - 'id': caseId, + newCases[containerId] = { + 'id': containerId, 'name': crateName, - 'caseImage': caseImagePath, + 'containerImage': containerImagePath, 'releaseDate': releaseDate, 'type': 'STICKER_CAPSULE', 'tournamentName': null, @@ -631,12 +789,14 @@ class DartImporterBackend implements ImporterBackend { 'sourceId': null, 'sourceName': null, }; - caseNameToId[crateName] = caseId; - caseContentsMap.putIfAbsent(caseId, () => {}); + caseNameToId[crateName] = containerId; + containerContentsMap.putIfAbsent(containerId, () => {}); containerRefsCreatedFromSkinMeta += 1; } - caseContentsMap.putIfAbsent(caseId, () => {}).add(skinId); + containerContentsMap + .putIfAbsent(containerId, () => {}) + .add(skinId); } } } @@ -734,10 +894,10 @@ class DartImporterBackend implements ImporterBackend { continue; } - var caseId = caseNameToId[crateName]; + var containerId = caseNameToId[crateName]; final containerType = inferStickerContainerType(crateName); - if (caseId == null) { + if (containerId == null) { final existingCaseMeta = existingCaseByName[crateName]; var releaseDate = resolveContainerReleaseDate( crateName: crateName, @@ -757,20 +917,21 @@ class DartImporterBackend implements ImporterBackend { releaseDate = sourceMeta['releaseDate']!; } - caseId = existingCaseMeta != null + containerId = existingCaseMeta != null ? existingCaseMeta['id'].toString() : (nextCaseId++).toString(); - final caseImagePath = await _syncAsset( + final containerImagePath = await _syncAsset( imageUrl: crateRef['image']?.toString(), - dirPath: casesDir.path, - relativeDir: 'assets/cases', - id: caseId, - existingRelativePath: existingCaseMeta?['caseImage']?.toString(), + dirPath: containersDir.path, + relativeDir: 'assets/containers', + id: containerId, + existingRelativePath: existingCaseMeta?['containerImage'] + ?.toString(), ); - newCases[caseId] = { - 'id': caseId, + newCases[containerId] = { + 'id': containerId, 'name': crateName, - 'caseImage': caseImagePath, + 'containerImage': containerImagePath, 'releaseDate': releaseDate, 'type': containerType, 'tournamentName': null, @@ -779,10 +940,10 @@ class DartImporterBackend implements ImporterBackend { 'sourceId': sourceMeta['sourceId'], 'sourceName': sourceMeta['sourceName'], }; - caseNameToId[crateName] = caseId; + caseNameToId[crateName] = containerId; containerRefsCreatedFromSkinMeta += 1; } else { - final existingCaseRecord = newCases[caseId]; + final existingCaseRecord = newCases[containerId]; if (existingCaseRecord != null) { var releaseDate = resolveContainerReleaseDate( crateName: crateName, @@ -811,7 +972,7 @@ class DartImporterBackend implements ImporterBackend { } stickerContentsMap - .putIfAbsent(caseId, () => {}) + .putIfAbsent(containerId, () => {}) .add(stickerId); } } @@ -832,18 +993,19 @@ class DartImporterBackend implements ImporterBackend { continue; } - var caseId = stickerCollectionNameToId[stickerCollectionName]; - if (caseId == null) { + var containerId = stickerCollectionNameToId[stickerCollectionName]; + if (containerId == null) { final existingCaseMeta = existingCaseByName[stickerCollectionName]; String releaseDate; if (existingCaseMeta != null) { - caseId = existingCaseMeta['id'].toString(); + containerId = existingCaseMeta['id'].toString(); releaseDate = (existingCaseMeta['releaseDate'] ?? '').toString(); } else if (caseNameToId.containsKey(stickerCollectionName)) { - caseId = caseNameToId[stickerCollectionName]!; - releaseDate = (newCases[caseId]?['releaseDate'] ?? '').toString(); + containerId = caseNameToId[stickerCollectionName]!; + releaseDate = (newCases[containerId]?['releaseDate'] ?? '') + .toString(); } else { - caseId = (nextCaseId++).toString(); + containerId = (nextCaseId++).toString(); releaseDate = '2000-01-01'; } @@ -854,17 +1016,17 @@ class DartImporterBackend implements ImporterBackend { releaseDate = sourceMeta['releaseDate']!; } - newCases[caseId] = { - 'id': caseId, + newCases[containerId] = { + 'id': containerId, 'name': stickerCollectionName, - 'caseImage': await _syncAsset( + 'containerImage': await _syncAsset( imageUrl: collectionRef['image']?.toString(), - dirPath: casesDir.path, - relativeDir: 'assets/cases', - id: caseId, + dirPath: containersDir.path, + relativeDir: 'assets/containers', + id: containerId, existingRelativePath: - existingCaseMeta?['caseImage']?.toString() ?? - newCases[caseId]?['caseImage']?.toString(), + existingCaseMeta?['containerImage']?.toString() ?? + newCases[containerId]?['containerImage']?.toString(), ), 'releaseDate': releaseDate, 'type': 'STICKER_COLLECTION', @@ -874,14 +1036,14 @@ class DartImporterBackend implements ImporterBackend { 'sourceId': sourceMeta['sourceId'], 'sourceName': sourceMeta['sourceName'], }; - caseNameToId[stickerCollectionName] = caseId; - stickerCollectionNameToId[stickerCollectionName] = caseId; + caseNameToId[stickerCollectionName] = containerId; + stickerCollectionNameToId[stickerCollectionName] = containerId; } stickerContentsMap - .putIfAbsent(caseId, () => {}) + .putIfAbsent(containerId, () => {}) .add(stickerId); - final existingCaseRecord = newCases[caseId]; + final existingCaseRecord = newCases[containerId]; if (existingCaseRecord != null) { final sourceMeta = resolveStickerCollectionSource( stickerCollectionName, @@ -908,8 +1070,8 @@ class DartImporterBackend implements ImporterBackend { continue; } - final caseId = caseNameToId[crateName]; - if (caseId == null) { + final containerId = caseNameToId[crateName]; + if (containerId == null) { continue; } @@ -983,7 +1145,7 @@ class DartImporterBackend implements ImporterBackend { }; newPins[pinId] = pinRecord; existingPinByKey[key] = pinRecord; - pinContentsMap.putIfAbsent(caseId, () => {}).add(pinId); + pinContentsMap.putIfAbsent(containerId, () => {}).add(pinId); if (!shouldCreateGenuinePin( pinName: pinName, @@ -1044,8 +1206,8 @@ class DartImporterBackend implements ImporterBackend { continue; } - final caseId = caseNameToId[crateName]; - if (caseId == null) { + final containerId = caseNameToId[crateName]; + if (containerId == null) { continue; } @@ -1093,8 +1255,15 @@ class DartImporterBackend implements ImporterBackend { final key = ( canonicalName(musicKitName), canonicalName(musicKitCollection ?? ''), - isStatTrak, ); + final variantPresence = musicKitVariantPresence.putIfAbsent( + key, + () => {'hasRegular': false, 'hasStatTrak': false}, + ); + variantPresence['hasRegular'] = + (variantPresence['hasRegular'] ?? false) || !isStatTrak; + variantPresence['hasStatTrak'] = + (variantPresence['hasStatTrak'] ?? false) || isStatTrak; final existingMusicKit = existingMusicKitByKey[key]; late String musicKitId; if (existingMusicKit != null) { @@ -1134,14 +1303,24 @@ class DartImporterBackend implements ImporterBackend { 'musicKitImage': musicKitImagePath, 'rarity': rarity, 'collection': musicKitCollection, - 'isStatTrak': isStatTrak, + 'hasRegular': variantPresence['hasRegular'] ?? false, + 'hasStatTrak': variantPresence['hasStatTrak'] ?? false, }; newMusicKits[musicKitId] = musicKitRecord; existingMusicKitByKey[key] = musicKitRecord; musicKitCollectionBySourceId[sourceMusicKitId] = musicKitCollection; - musicKitContentsMap - .putIfAbsent(caseId, () => {}) - .add(musicKitId); + final containerMusicKits = musicKitContentsMap.putIfAbsent( + containerId, + () => >{}, + ); + final existingEntry = + containerMusicKits[musicKitId] ?? + {'hasRegular': false, 'hasStatTrak': false}; + existingEntry['hasRegular'] = + (existingEntry['hasRegular'] ?? false) || !isStatTrak; + existingEntry['hasStatTrak'] = + (existingEntry['hasStatTrak'] ?? false) || isStatTrak; + containerMusicKits[musicKitId] = existingEntry; } } @@ -1170,15 +1349,33 @@ class DartImporterBackend implements ImporterBackend { final key = ( canonicalName(musicKitName), canonicalName(musicKitCollection ?? ''), - isStatTrak, ); + final variantPresence = musicKitVariantPresence.putIfAbsent( + key, + () => {'hasRegular': false, 'hasStatTrak': false}, + ); + variantPresence['hasRegular'] = + (variantPresence['hasRegular'] ?? false) || !isStatTrak; + variantPresence['hasStatTrak'] = + (variantPresence['hasStatTrak'] ?? false) || isStatTrak; if (existingMusicKitByKey.containsKey(key)) { + final existingRecord = existingMusicKitByKey[key]; + if (existingRecord != null) { + final existingId = existingRecord['id']?.toString(); + if (existingId != null && newMusicKits.containsKey(existingId)) { + newMusicKits[existingId]!['hasRegular'] = + variantPresence['hasRegular'] ?? false; + newMusicKits[existingId]!['hasStatTrak'] = + variantPresence['hasStatTrak'] ?? false; + } + } continue; } final rarity = - musicKitRarityMap[((meta['rarity'] as Map?)?['name'] ?? '').toString()] ?? + musicKitRarityMap[((meta['rarity'] as Map?)?['name'] ?? '') + .toString()] ?? 'HIGH_GRADE'; final imageUrl = chooseImageUrl(meta); @@ -1213,7 +1410,8 @@ class DartImporterBackend implements ImporterBackend { 'musicKitImage': musicKitImagePath, 'rarity': rarity, 'collection': musicKitCollection, - 'isStatTrak': isStatTrak, + 'hasRegular': variantPresence['hasRegular'] ?? false, + 'hasStatTrak': variantPresence['hasStatTrak'] ?? false, }; newMusicKits[musicKitId] = musicKitRecord; existingMusicKitByKey[key] = musicKitRecord; @@ -1244,7 +1442,8 @@ class DartImporterBackend implements ImporterBackend { } final rarity = - agentRarityMap[((meta['rarity'] as Map?)?['name'] ?? '').toString()] ?? + agentRarityMap[((meta['rarity'] as Map?)?['name'] ?? '') + .toString()] ?? 'DISTINGUISHED'; final team = ((meta['team'] as Map?)?['name'] ?? '') .toString() @@ -1310,7 +1509,8 @@ class DartImporterBackend implements ImporterBackend { final collectionKey = operationKey(collectionName, opId); var agentCollectionId = agentCollectionKeyToId[collectionKey]; - final existingAgentCollection = existingAgentCollectionByKey[collectionKey]; + final existingAgentCollection = + existingAgentCollectionByKey[collectionKey]; if (agentCollectionId == null) { if (existingAgentCollection != null) { agentCollectionId = existingAgentCollection['id'].toString(); @@ -1322,11 +1522,12 @@ class DartImporterBackend implements ImporterBackend { final agentCollectionImagePath = await _syncAsset( imageUrl: collectionRef['image']?.toString(), - dirPath: agentCollectionsDir.path, - relativeDir: 'assets/agent_collections', + dirPath: containersDir.path, + relativeDir: 'assets/containers', id: agentCollectionId, existingRelativePath: - (newAgentCollections[agentCollectionId] ?? existingAgentCollection)?['image'] + (newAgentCollections[agentCollectionId] ?? + existingAgentCollection)?['image'] ?.toString(), ); @@ -1338,11 +1539,90 @@ class DartImporterBackend implements ImporterBackend { 'operationName': opName, 'releaseDate': releaseDate, }; + newCases[agentCollectionId] = { + 'id': agentCollectionId, + 'name': collectionName, + 'containerImage': agentCollectionImagePath, + 'releaseDate': releaseDate, + 'type': 'AGENT_COLLECTION', + 'tournamentName': null, + 'tournamentLogo': null, + 'sourceType': 'LEGACY_OPERATION', + 'sourceId': opId, + 'sourceName': opName, + 'currency': null, + 'cost': null, + }; + caseNameToId[collectionName] = agentCollectionId; agentCollectionContentsMap .putIfAbsent(agentCollectionId, () => {}) .add(agentId); } + for (final meta in keychainsData) { + final charmName = normalizeCharmName( + (meta['name'] ?? '').toString().trim(), + ); + if (charmName.isEmpty) { + continue; + } + + final collectionInfo = chooseCollectionNameAndImage(meta); + final charmCollection = collectionInfo.$1; + final rarity = + charmRarityMap[((meta['rarity'] as Map?)?['name'] ?? '') + .toString()] ?? + 'HIGH_GRADE'; + final imageUrl = chooseImageUrl(meta); + + final key = ( + canonicalName(charmName), + canonicalName(charmCollection ?? ''), + ); + final existingCharm = existingCharmByKey[key]; + late String charmId; + if (existingCharm != null) { + charmId = existingCharm['id'].toString(); + reusedCharmCount += 1; + } else { + final sourceCharmId = (meta['id'] ?? '').toString().trim(); + final candidate = makeStableNumericId(sourceCharmId, 996000000); + if (RegExp(r'^\d+$').hasMatch(candidate) && + !usedCharmIds.contains(int.parse(candidate)) && + !newCharms.containsKey(candidate)) { + charmId = candidate; + usedCharmIds.add(int.parse(candidate)); + } else { + while (usedCharmIds.contains(nextCharmId)) { + nextCharmId += 1; + } + charmId = nextCharmId.toString(); + usedCharmIds.add(nextCharmId); + nextCharmId += 1; + } + createdCharmCount += 1; + } + + final charmImagePath = await _syncAsset( + imageUrl: imageUrl, + dirPath: charmsDir.path, + relativeDir: 'assets/charms', + id: charmId, + existingRelativePath: existingCharm?['charmImage']?.toString(), + compressionModeOverride: CompressionMode.maxCompress, + ); + + final charmRecord = { + 'id': charmId, + 'name': charmName, + 'charmImage': charmImagePath, + 'rarity': rarity, + 'collection': charmCollection, + }; + newCharms[charmId] = charmRecord; + existingCharmByKey[key] = charmRecord; + } + for (final meta in graffitiData) { final graffitiName = normalizeGraffitiName( (meta['name'] ?? '').toString().trim(), @@ -1352,7 +1632,8 @@ class DartImporterBackend implements ImporterBackend { } final rarity = - graffitiRarityMap[((meta['rarity'] as Map?)?['name'] ?? '').toString()] ?? + graffitiRarityMap[((meta['rarity'] as Map?)?['name'] ?? '') + .toString()] ?? 'BASE_GRADE'; final imageUrl = chooseImageUrl(meta); final cratesRefs = meta['crates']; @@ -1416,11 +1697,13 @@ class DartImporterBackend implements ImporterBackend { } final crateRef = crateRefRaw.map((k, v) => MapEntry(k.toString(), v)); final crateName = (crateRef['name'] ?? '').toString().trim(); - final caseId = caseNameToId[crateName]; - if (caseId == null) { + final containerId = caseNameToId[crateName]; + if (containerId == null) { continue; } - graffitiContentsMap.putIfAbsent(caseId, () => {}).add(graffitiId); + graffitiContentsMap + .putIfAbsent(containerId, () => {}) + .add(graffitiId); } } } @@ -1439,8 +1722,8 @@ class DartImporterBackend implements ImporterBackend { continue; } - final caseId = caseNameToId[crateName]; - if (caseId == null) { + final containerId = caseNameToId[crateName]; + if (containerId == null) { continue; } @@ -1465,10 +1748,12 @@ class DartImporterBackend implements ImporterBackend { } final rarity = - patchRarityMap[((collectible['rarity'] as Map?)?['name'] ?? '').toString()] ?? + patchRarityMap[((collectible['rarity'] as Map?)?['name'] ?? '') + .toString()] ?? 'HIGH_GRADE'; final sourcePatchId = (collectible['id'] ?? '').toString(); - final patchMeta = patchMetaById[sourcePatchId] ?? const {}; + final patchMeta = + patchMetaById[sourcePatchId] ?? const {}; final collectibleImage = (collectible['image'] ?? '').toString().trim(); final metaImage = (patchMeta['image'] ?? '').toString().trim(); final imageUrl = collectibleImage.isNotEmpty @@ -1519,11 +1804,13 @@ class DartImporterBackend implements ImporterBackend { }; newPatches[patchId] = patchRecord; existingPatchByKey[key] = patchRecord; - patchContentsMap.putIfAbsent(caseId, () => {}).add(patchId); + patchContentsMap + .putIfAbsent(containerId, () => {}) + .add(patchId); } if (patchContainerType == 'PATCH_COLLECTION') { - final existingCaseRecord = newCases[caseId]; + final existingCaseRecord = newCases[containerId]; if (existingCaseRecord != null) { final sourceMeta = resolvePatchCollectionSource(crateName); existingCaseRecord['sourceType'] = sourceMeta['sourceType']; @@ -1536,6 +1823,83 @@ class DartImporterBackend implements ImporterBackend { } } + for (final collection in charmCollections) { + final collectionName = (collection['name'] ?? '').toString().trim(); + final containerId = caseNameToId[collectionName]; + if (containerId == null) { + continue; + } + + final contains = collection['contains']; + if (contains is! List) { + continue; + } + + for (final collectibleRaw in contains) { + if (collectibleRaw is! Map) { + continue; + } + final collectible = collectibleRaw.map( + (k, v) => MapEntry(k.toString(), v), + ); + final charmName = normalizeCharmName( + (collectible['name'] ?? '').toString().trim(), + ); + if (charmName.isEmpty) { + continue; + } + + final key = (canonicalName(charmName), canonicalName(collectionName)); + + var charmRecord = existingCharmByKey[key]; + if (charmRecord == null) { + final rarity = + charmRarityMap[((collectible['rarity'] as Map?)?['name'] ?? '') + .toString()] ?? + 'HIGH_GRADE'; + final sourceCharmId = (collectible['id'] ?? '').toString().trim(); + late String charmId; + final candidate = makeStableNumericId(sourceCharmId, 996000000); + if (RegExp(r'^\d+$').hasMatch(candidate) && + !usedCharmIds.contains(int.parse(candidate)) && + !newCharms.containsKey(candidate)) { + charmId = candidate; + usedCharmIds.add(int.parse(candidate)); + } else { + while (usedCharmIds.contains(nextCharmId)) { + nextCharmId += 1; + } + charmId = nextCharmId.toString(); + usedCharmIds.add(nextCharmId); + nextCharmId += 1; + } + createdCharmCount += 1; + + final charmImagePath = await _syncAsset( + imageUrl: collectible['image']?.toString(), + dirPath: charmsDir.path, + relativeDir: 'assets/charms', + id: charmId, + compressionModeOverride: CompressionMode.maxCompress, + ); + + charmRecord = { + 'id': charmId, + 'name': charmName, + 'charmImage': charmImagePath, + 'rarity': rarity, + 'collection': collectionName, + }; + newCharms[charmId] = charmRecord; + existingCharmByKey[key] = charmRecord; + } + + charmContentsMap + .putIfAbsent(containerId, () => {}) + .add(charmRecord['id'].toString()); + } + } + for (final legacyCase in legacyCaseOverrides) { final legacyName = (legacyCase['name'] ?? '').toString().trim(); if (legacyName.isEmpty) { @@ -1553,10 +1917,10 @@ class DartImporterBackend implements ImporterBackend { newCases[legacyCaseId] = { 'id': legacyCaseId, 'name': legacyName, - 'caseImage': - existingCase?['caseImage']?.toString() ?? - (baseCase?['caseImage']?.toString()) ?? - 'assets/cases/$legacyCaseId.png', + 'containerImage': + existingCase?['containerImage']?.toString() ?? + (baseCase?['containerImage']?.toString()) ?? + 'assets/containers/$legacyCaseId.png', 'releaseDate': (legacyCase['releaseDate'] ?? '2000-01-01').toString(), 'type': (legacyCase['type'] ?? 'CASE').toString(), 'tournamentName': null, @@ -1566,19 +1930,21 @@ class DartImporterBackend implements ImporterBackend { 'sourceName': null, }; caseNameToId[legacyName] = legacyCaseId; - caseContentsMap.putIfAbsent(legacyCaseId, () => {}); + containerContentsMap.putIfAbsent(legacyCaseId, () => {}); if (legacyCase['copyImageFromBase'] == true && baseCase != null) { final baseImageName = basename( - (baseCase['caseImage'] ?? '').toString(), + (baseCase['containerImage'] ?? '').toString(), ); - final baseImageFile = File('${casesDir.path}/$baseImageName'); + final baseImageFile = File('${containersDir.path}/$baseImageName'); final baseExt = suffixFromPath(baseImageName); - final legacyImageFile = File('${casesDir.path}/$legacyCaseId$baseExt'); + final legacyImageFile = File( + '${containersDir.path}/$legacyCaseId$baseExt', + ); if (baseImageFile.existsSync() && !legacyImageFile.existsSync()) { await legacyImageFile.writeAsBytes(await baseImageFile.readAsBytes()); - newCases[legacyCaseId]!['caseImage'] = - 'assets/cases/$legacyCaseId$baseExt'; + newCases[legacyCaseId]!['containerImage'] = + 'assets/containers/$legacyCaseId$baseExt'; } } @@ -1595,7 +1961,7 @@ class DartImporterBackend implements ImporterBackend { ); continue; } - caseContentsMap + containerContentsMap .putIfAbsent(legacyCaseId, () => {}) .add(skinId); } @@ -1603,7 +1969,8 @@ class DartImporterBackend implements ImporterBackend { if (legacyCase['copySpecialItemsFromBase'] == true && baseCaseId != null) { - for (final skinId in caseContentsMap[baseCaseId] ?? const {}) { + for (final skinId + in containerContentsMap[baseCaseId] ?? const {}) { final skin = newSkins[skinId]; if (skin == null) { continue; @@ -1614,7 +1981,7 @@ class DartImporterBackend implements ImporterBackend { weaponType == 'GLOVES' || itemKind == 'KNIFE' || itemKind == 'GLOVES') { - caseContentsMap + containerContentsMap .putIfAbsent(legacyCaseId, () => {}) .add(skinId); } @@ -1684,19 +2051,29 @@ class DartImporterBackend implements ImporterBackend { a['id'].toString(), ).compareTo(int.parse(b['id'].toString())), ); + final charmsOut = newCharms.values.toList() + ..sort( + (a, b) => int.parse( + a['id'].toString(), + ).compareTo(int.parse(b['id'].toString())), + ); - final caseContentsOut = buildContents(caseContentsMap, 'caseId', 'skinIds'); + final containerContentsOut = buildContents( + containerContentsMap, + 'containerId', + 'skinIds', + ); final stickerContentsOut = buildContents( stickerContentsMap, - 'caseId', + 'containerId', 'stickerIds', ); - final pinContentsOut = buildContents(pinContentsMap, 'caseId', 'pinIds'); - final musicKitContentsOut = buildContents( - musicKitContentsMap, - 'caseId', - 'musicKitIds', + final pinContentsOut = buildContents( + pinContentsMap, + 'containerId', + 'pinIds', ); + final musicKitContentsOut = buildMusicKitContents(musicKitContentsMap); final agentCollectionsOut = newAgentCollections.values.toList() ..sort((a, b) { final dateCompare = (a['releaseDate'] ?? '9999-99-99') @@ -1716,14 +2093,19 @@ class DartImporterBackend implements ImporterBackend { ); final graffitiContentsOut = buildContents( graffitiContentsMap, - 'caseId', + 'containerId', 'graffitiIds', ); final patchContentsOut = buildContents( patchContentsMap, - 'caseId', + 'containerId', 'patchIds', ); + final charmContentsOut = buildContents( + charmContentsMap, + 'containerId', + 'charmIds', + ); final rewardCollectionsOut = newRewardCollections.values.toList() ..sort((a, b) { final sourceCompare = (a['sourceType'] ?? '').toString().compareTo( @@ -1771,7 +2153,7 @@ class DartImporterBackend implements ImporterBackend { 'skinIds', ); - await _io.writeJson(File('${dataDir.path}/cases.json'), casesOut); + await _io.writeJson(File('${dataDir.path}/containers.json'), casesOut); await _io.writeJson(File('${dataDir.path}/skins.json'), skinsOut); await _io.writeJson(File('${dataDir.path}/stickers.json'), stickersOut); await _io.writeJson(File('${dataDir.path}/pins.json'), pinsOut); @@ -1779,9 +2161,10 @@ class DartImporterBackend implements ImporterBackend { await _io.writeJson(File('${dataDir.path}/agents.json'), agentsOut); await _io.writeJson(File('${dataDir.path}/graffiti.json'), graffitiOut); await _io.writeJson(File('${dataDir.path}/patches.json'), patchesOut); + await _io.writeJson(File('${dataDir.path}/charms.json'), charmsOut); await _io.writeJson( - File('${dataDir.path}/case_contents.json'), - caseContentsOut, + File('${dataDir.path}/container_contents.json'), + containerContentsOut, ); await _io.writeJson( File('${dataDir.path}/sticker_contents.json'), @@ -1795,10 +2178,6 @@ class DartImporterBackend implements ImporterBackend { File('${dataDir.path}/music_kit_contents.json'), musicKitContentsOut, ); - await _io.writeJson( - File('${dataDir.path}/agent_collections.json'), - agentCollectionsOut, - ); await _io.writeJson( File('${dataDir.path}/agent_collection_contents.json'), agentCollectionContentsOut, @@ -1812,17 +2191,13 @@ class DartImporterBackend implements ImporterBackend { patchContentsOut, ); await _io.writeJson( - File('${dataDir.path}/reward_collections.json'), - rewardCollectionsOut, + File('${dataDir.path}/charm_contents.json'), + charmContentsOut, ); await _io.writeJson( File('${dataDir.path}/reward_collection_contents.json'), rewardCollectionContentsOut, ); - await _io.writeJson( - File('${dataDir.path}/operation_collections.json'), - operationCollectionsOut, - ); await _io.writeJson( File('${dataDir.path}/operation_collection_contents.json'), operationCollectionContentsOut, @@ -1840,13 +2215,17 @@ class DartImporterBackend implements ImporterBackend { _io.printInfo('Agents: ${agentsOut.length}'); _io.printInfo('Graffiti: ${graffitiOut.length}'); _io.printInfo('Patches: ${patchesOut.length}'); - _io.printInfo('Case contents: ${caseContentsOut.length}'); + _io.printInfo('Charms: ${charmsOut.length}'); + _io.printInfo('Container contents: ${containerContentsOut.length}'); _io.printInfo('Sticker contents: ${stickerContentsOut.length}'); _io.printInfo('Pin contents: ${pinContentsOut.length}'); _io.printInfo('Music kit contents: ${musicKitContentsOut.length}'); - _io.printInfo('Agent collection contents: ${agentCollectionContentsOut.length}'); + _io.printInfo( + 'Agent collection contents: ${agentCollectionContentsOut.length}', + ); _io.printInfo('Graffiti contents: ${graffitiContentsOut.length}'); _io.printInfo('Patch contents: ${patchContentsOut.length}'); + _io.printInfo('Charm contents: ${charmContentsOut.length}'); _io.printInfo( 'Reward collection contents: ${rewardCollectionContentsOut.length}', ); @@ -1860,6 +2239,7 @@ class DartImporterBackend implements ImporterBackend { _io.printInfo('Created agents: $createdAgentCount'); _io.printInfo('Created graffiti: $createdGraffitiCount'); _io.printInfo('Created patches: $createdPatchCount'); + _io.printInfo('Created charms: $createdCharmCount'); _io.printInfo('Reused skins: $reusedSkinCount'); _io.printInfo('Reused stickers: $reusedStickerCount'); _io.printInfo('Reused pins: $reusedPinCount'); @@ -1867,6 +2247,7 @@ class DartImporterBackend implements ImporterBackend { _io.printInfo('Reused agents: $reusedAgentCount'); _io.printInfo('Reused graffiti: $reusedGraffitiCount'); _io.printInfo('Reused patches: $reusedPatchCount'); + _io.printInfo('Reused charms: $reusedCharmCount'); _io.printInfo('Unknown items skipped: $skippedUnknownItems'); _io.printInfo( 'Containers created from skin.crates fallback: $containerRefsCreatedFromSkinMeta', @@ -1878,6 +2259,25 @@ class DartImporterBackend implements ImporterBackend { _io.printInfo('Tournament logos downloaded: $tournamentLogosCreated'); } + Map _normalizeExistingContainerMeta( + Map item, + ) { + final normalized = Map.from(item); + final imagePath = (normalized['containerImage'] ?? normalized['caseImage']) + ?.toString() + .trim(); + + if (imagePath != null && imagePath.isNotEmpty) { + normalized['containerImage'] = imagePath.replaceAll( + 'assets/cases/', + 'assets/containers/', + ); + } + + normalized.remove('caseImage'); + return normalized; + } + Map> _loadRewardOverrides() { final overrides = >{ for (final entry in defaultRewardSourceOverrides.entries) diff --git a/tool/importer/src/io_utils.dart b/tool/importer/src/io_utils.dart index e6a1f212..da90bbfa 100644 --- a/tool/importer/src/io_utils.dart +++ b/tool/importer/src/io_utils.dart @@ -43,7 +43,7 @@ class IoUtils { for (final dir in [ assetsDir, dataDir, - casesDir, + containersDir, skinsDir, stickersDir, pinsDir, @@ -51,9 +51,7 @@ class IoUtils { agentsDir, graffitiDir, patchesDir, - rewardCollectionsDir, - operationCollectionsDir, - agentCollectionsDir, + charmsDir, tournamentLogosDir, ]) { if (!dir.existsSync()) { @@ -71,12 +69,13 @@ class IoUtils { File('${dataDir.path}/music_kits.json'), File('${dataDir.path}/music_kit_contents.json'), File('${dataDir.path}/agents.json'), - File('${dataDir.path}/agent_collections.json'), File('${dataDir.path}/agent_collection_contents.json'), File('${dataDir.path}/graffiti.json'), File('${dataDir.path}/graffiti_contents.json'), File('${dataDir.path}/patches.json'), File('${dataDir.path}/patch_contents.json'), + File('${dataDir.path}/charms.json'), + File('${dataDir.path}/charm_contents.json'), ]) { if (file.existsSync()) { await file.delete(); @@ -210,11 +209,9 @@ class IoUtils { Future downloadOptimizedAsset( String url, - String pathWithoutExt, - { + String pathWithoutExt, { CompressionMode? compressionModeOverride, - } - ) async { + }) async { if (url.isEmpty) { return null; } diff --git a/tool/importer/src/normalization.dart b/tool/importer/src/normalization.dart index 359c85af..1bc4ec92 100644 --- a/tool/importer/src/normalization.dart +++ b/tool/importer/src/normalization.dart @@ -435,10 +435,9 @@ String operationKey(String name, String operationId) => canonicalName((pin['collection'] ?? '').toString()), ); -(String, String, bool) existingMusicKitKey(Map musicKit) => ( +(String, String) existingMusicKitKey(Map musicKit) => ( canonicalName((musicKit['name'] ?? '').toString()), canonicalName((musicKit['collection'] ?? '').toString()), - musicKit['isStatTrak'] == true, ); (String, String, String) existingAgentKey(Map agent) => ( @@ -457,6 +456,11 @@ String operationKey(String name, String operationId) => canonicalName((patch['collection'] ?? '').toString()), ); +(String, String) existingCharmKey(Map charm) => ( + canonicalName((charm['name'] ?? '').toString()), + canonicalName((charm['collection'] ?? '').toString()), +); + (String?, String?) chooseCollectionNameAndImage(Map meta) { final collections = meta['collections']; if (collections is List && @@ -584,6 +588,10 @@ String normalizePatchName(String name) { return name.replaceFirst(RegExp(r'^Patch\s+\|\s+'), '').trim(); } +String normalizeCharmName(String name) { + return name.replaceFirst(RegExp(r'^Charm\s+\|\s+'), '').trim(); +} + Map resolveAgentCollectionSource(String? collectionName) { final normalized = (collectionName ?? '').trim(); final source = agentCollectionSourceOverrides[normalized] ?? const {}; @@ -674,6 +682,49 @@ Map resolvePatchCollectionSource(String? collectionName) { }; } +String? resolveOperationNameFromId(String? operationId) { + switch ((operationId ?? '').trim()) { + case 'PAYBACK': + return 'Operation Payback'; + case 'BRAVO': + return 'Operation Bravo'; + case 'PHOENIX': + return 'Operation Phoenix'; + case 'BREAKOUT': + return 'Operation Breakout'; + case 'BLOODHOUND': + return 'Operation Bloodhound'; + case 'SHATTERED_WEB': + return 'Operation Shattered Web'; + case 'BROKEN_FANG': + return 'Operation Broken Fang'; + case 'RIPTIDE': + return 'Operation Riptide'; + default: + return null; + } +} + +Map resolveCharmCollectionSource(String? collectionName) { + final normalized = (collectionName ?? '').trim(); + final source = charmCollectionSourceOverrides[normalized] ?? const {}; + + String? value(String key) { + final raw = source[key]; + if (raw == null || raw.trim().isEmpty) { + return null; + } + return raw.trim(); + } + + return { + 'sourceType': value('sourceType'), + 'sourceId': value('sourceId'), + 'sourceName': value('sourceName'), + 'releaseDate': value('releaseDate'), + }; +} + String? getExplicitPhase(Map meta) { if (meta['phase'] != null) { return meta['phase'].toString(); @@ -863,6 +914,40 @@ List> buildContents( return out; } +List> buildMusicKitContents( + Map>> source, +) { + final out = source.entries.where((entry) => entry.value.isNotEmpty).map(( + entry, + ) { + final items = + entry.value.entries + .map( + (item) => { + 'musicKitId': item.key, + 'hasRegular': item.value['hasRegular'] ?? false, + 'hasStatTrak': item.value['hasStatTrak'] ?? false, + }, + ) + .toList() + ..sort( + (a, b) => int.parse( + a['musicKitId'].toString(), + ).compareTo(int.parse(b['musicKitId'].toString())), + ); + + return {'containerId': entry.key, 'items': items}; + }).toList(); + + out.sort( + (a, b) => int.parse( + a['containerId'].toString(), + ).compareTo(int.parse(b['containerId'].toString())), + ); + + return out; +} + String suffixFromPath(String path) { final dot = path.lastIndexOf('.'); return dot == -1 ? '.png' : path.substring(dot); diff --git a/tool/prune_generated_assets.dart b/tool/prune_generated_assets.dart index 7eaa0376..8bef6de9 100644 --- a/tool/prune_generated_assets.dart +++ b/tool/prune_generated_assets.dart @@ -25,25 +25,21 @@ Future main() async { } } - collectFromJsonArray('assets/data/cases.json', [ - 'caseImage', + collectFromJsonArray('assets/data/containers.json', [ + 'containerImage', 'tournamentLogo', ]); collectFromJsonArray('assets/data/skins.json', ['skinImage']); collectFromJsonArray('assets/data/stickers.json', ['stickerImage']); collectFromJsonArray('assets/data/pins.json', ['pinImage']); collectFromJsonArray('assets/data/music_kits.json', ['musicKitImage']); - collectFromJsonArray('assets/data/reward_collections.json', ['image']); - collectFromJsonArray('assets/data/operation_collections.json', ['image']); final dirs = [ - 'assets/cases', + 'assets/containers', 'assets/skins', 'assets/stickers', 'assets/pins', 'assets/music_kits', - 'assets/reward_collections', - 'assets/operation_collections', 'assets/tournament_logos', ];