From 405f49c4901fbc2c395d6b39d5a2ef7490f4254e Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:23:11 -0400 Subject: [PATCH 01/10] feat(seeds): story-deck presentation seed (53 build + 59 trial) Co-Authored-By: Claude Sonnet 4.6 --- SVSim.Bootstrap/Data/seeds/story-decks.json | 1346 +++++++++++++++++++ 1 file changed, 1346 insertions(+) create mode 100644 SVSim.Bootstrap/Data/seeds/story-decks.json diff --git a/SVSim.Bootstrap/Data/seeds/story-decks.json b/SVSim.Bootstrap/Data/seeds/story-decks.json new file mode 100644 index 0000000..8c2a914 --- /dev/null +++ b/SVSim.Bootstrap/Data/seeds/story-decks.json @@ -0,0 +1,1346 @@ +[ + { + "deck_no": 1, + "kind": "build", + "class_id": 1, + "deck_name": "Sterling Archer", + "sleeve_id": 3000011, + "leader_skin_id": 1, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 2, + "kind": "build", + "class_id": 2, + "deck_name": "Hour of the Banquet", + "sleeve_id": 3000011, + "leader_skin_id": 2, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 3, + "kind": "build", + "class_id": 3, + "deck_name": "Quintessence of Conjury", + "sleeve_id": 3000011, + "leader_skin_id": 3, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 4, + "kind": "build", + "class_id": 4, + "deck_name": "Dark Wyrmdriver", + "sleeve_id": 3000011, + "leader_skin_id": 4, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 5, + "kind": "build", + "class_id": 5, + "deck_name": "Roar of the Netherflame", + "sleeve_id": 3000011, + "leader_skin_id": 5, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 6, + "kind": "build", + "class_id": 6, + "deck_name": "Kiss of the Queen", + "sleeve_id": 3000011, + "leader_skin_id": 6, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 7, + "kind": "build", + "class_id": 7, + "deck_name": "Ivory Battlefield", + "sleeve_id": 3000011, + "leader_skin_id": 7, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 201, + "kind": "build", + "class_id": 1, + "deck_name": "Thorned Anomaly", + "sleeve_id": 3000011, + "leader_skin_id": 1, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 202, + "kind": "build", + "class_id": 2, + "deck_name": "Lightning Speed", + "sleeve_id": 3000011, + "leader_skin_id": 2, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 203, + "kind": "build", + "class_id": 3, + "deck_name": "Hulking Rage", + "sleeve_id": 3000011, + "leader_skin_id": 3, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 204, + "kind": "build", + "class_id": 4, + "deck_name": "Aura of the Waterwyrm ", + "sleeve_id": 3000011, + "leader_skin_id": 4, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 205, + "kind": "build", + "class_id": 5, + "deck_name": "Death's Puppeteer", + "sleeve_id": 3000011, + "leader_skin_id": 5, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 206, + "kind": "build", + "class_id": 6, + "deck_name": "Crimson Pact", + "sleeve_id": 3000011, + "leader_skin_id": 6, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 207, + "kind": "build", + "class_id": 7, + "deck_name": "Alabaster Shield", + "sleeve_id": 3000011, + "leader_skin_id": 7, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 301, + "kind": "build", + "class_id": 1, + "deck_name": "Dazzling Stars", + "sleeve_id": 3000011, + "leader_skin_id": 1, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 302, + "kind": "build", + "class_id": 2, + "deck_name": "Stillflame General", + "sleeve_id": 3000011, + "leader_skin_id": 2, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 303, + "kind": "build", + "class_id": 3, + "deck_name": "Enchanted Illusions", + "sleeve_id": 3000011, + "leader_skin_id": 3, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 304, + "kind": "build", + "class_id": 4, + "deck_name": "Primal Flames", + "sleeve_id": 3000011, + "leader_skin_id": 4, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 305, + "kind": "build", + "class_id": 5, + "deck_name": "Giant Obliteration", + "sleeve_id": 3000011, + "leader_skin_id": 5, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 306, + "kind": "build", + "class_id": 6, + "deck_name": "Cruel Abyss", + "sleeve_id": 3000011, + "leader_skin_id": 6, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 307, + "kind": "build", + "class_id": 7, + "deck_name": "Winged Blessings", + "sleeve_id": 3000011, + "leader_skin_id": 7, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 401, + "kind": "build", + "class_id": 1, + "deck_name": "Law of the Forest", + "sleeve_id": 3000011, + "leader_skin_id": 1, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 402, + "kind": "build", + "class_id": 2, + "deck_name": "The Heroes Four", + "sleeve_id": 3000011, + "leader_skin_id": 2, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 403, + "kind": "build", + "class_id": 3, + "deck_name": "Genius Alchemy ", + "sleeve_id": 3000011, + "leader_skin_id": 3, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 404, + "kind": "build", + "class_id": 4, + "deck_name": "Verdict: Armageddon ", + "sleeve_id": 3000011, + "leader_skin_id": 4, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 405, + "kind": "build", + "class_id": 5, + "deck_name": "Love's Last Knell", + "sleeve_id": 3000011, + "leader_skin_id": 5, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 406, + "kind": "build", + "class_id": 6, + "deck_name": "Dazzling Sword Dance", + "sleeve_id": 3000011, + "leader_skin_id": 6, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 407, + "kind": "build", + "class_id": 7, + "deck_name": "Eternal Testament ", + "sleeve_id": 3000011, + "leader_skin_id": 7, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 408, + "kind": "build", + "class_id": 8, + "deck_name": "Warriors of Steel", + "sleeve_id": 3000011, + "leader_skin_id": 8, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 501, + "kind": "build", + "class_id": 1, + "deck_name": "Silent Maiden", + "sleeve_id": 3000011, + "leader_skin_id": 1, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 502, + "kind": "build", + "class_id": 2, + "deck_name": "Unsheathed Fury", + "sleeve_id": 3000011, + "leader_skin_id": 2, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 503, + "kind": "build", + "class_id": 3, + "deck_name": "Sugar-sweet Magic!", + "sleeve_id": 3000011, + "leader_skin_id": 3, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 504, + "kind": "build", + "class_id": 4, + "deck_name": "Wrath of the Empress", + "sleeve_id": 3000011, + "leader_skin_id": 4, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 505, + "kind": "build", + "class_id": 5, + "deck_name": "The Corpsewyrm Beckons", + "sleeve_id": 3000011, + "leader_skin_id": 5, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 506, + "kind": "build", + "class_id": 6, + "deck_name": "Crimson Flowerbed", + "sleeve_id": 3000011, + "leader_skin_id": 6, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 507, + "kind": "build", + "class_id": 7, + "deck_name": "Hallowed Crusade", + "sleeve_id": 3000011, + "leader_skin_id": 7, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 508, + "kind": "build", + "class_id": 8, + "deck_name": "Sins of the Heartless", + "sleeve_id": 3000011, + "leader_skin_id": 8, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 601, + "kind": "build", + "class_id": 1, + "deck_name": "Crystalline Viridian", + "sleeve_id": 3000011, + "leader_skin_id": 1, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 602, + "kind": "build", + "class_id": 2, + "deck_name": "Break of Twilight", + "sleeve_id": 3000011, + "leader_skin_id": 2, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 603, + "kind": "build", + "class_id": 3, + "deck_name": "Sapphire: Insurgence", + "sleeve_id": 3000011, + "leader_skin_id": 3, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 604, + "kind": "build", + "class_id": 4, + "deck_name": "Champions of the Zenith", + "sleeve_id": 3000011, + "leader_skin_id": 4, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 605, + "kind": "build", + "class_id": 5, + "deck_name": "Amethyst: Resistance", + "sleeve_id": 3000011, + "leader_skin_id": 5, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 606, + "kind": "build", + "class_id": 6, + "deck_name": "Garnet: Defiance", + "sleeve_id": 3000011, + "leader_skin_id": 6, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 607, + "kind": "build", + "class_id": 7, + "deck_name": "Guardians of Elysium", + "sleeve_id": 3000011, + "leader_skin_id": 7, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 608, + "kind": "build", + "class_id": 8, + "deck_name": "Steel Autocracy", + "sleeve_id": 3000011, + "leader_skin_id": 8, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 701, + "kind": "build", + "class_id": 1, + "deck_name": "Pure Devotion", + "sleeve_id": 3000011, + "leader_skin_id": 1, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 702, + "kind": "build", + "class_id": 2, + "deck_name": "Gallant Gourmand", + "sleeve_id": 3000011, + "leader_skin_id": 2, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 703, + "kind": "build", + "class_id": 3, + "deck_name": "Let's Get Catty", + "sleeve_id": 3000011, + "leader_skin_id": 3, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 704, + "kind": "build", + "class_id": 4, + "deck_name": "Ready, Set, Brawl!", + "sleeve_id": 3000011, + "leader_skin_id": 4, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 705, + "kind": "build", + "class_id": 5, + "deck_name": "Messages from Beyond", + "sleeve_id": 3000011, + "leader_skin_id": 5, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 706, + "kind": "build", + "class_id": 6, + "deck_name": "Midnight Mistress", + "sleeve_id": 3000011, + "leader_skin_id": 6, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 707, + "kind": "build", + "class_id": 7, + "deck_name": "Highborn Lady", + "sleeve_id": 3000011, + "leader_skin_id": 7, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 708, + "kind": "build", + "class_id": 8, + "deck_name": "All-seeing Tyrant", + "sleeve_id": 3000011, + "leader_skin_id": 8, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": null + }, + { + "deck_no": 10001, + "kind": "trial", + "class_id": 1, + "deck_name": "Control Forestcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 10002, + "kind": "trial", + "class_id": 2, + "deck_name": "Levin Swordcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 10003, + "kind": "trial", + "class_id": 3, + "deck_name": "Natura Runecraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 10004, + "kind": "trial", + "class_id": 4, + "deck_name": "Natura Dragoncraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 10005, + "kind": "trial", + "class_id": 5, + "deck_name": "Natura Shadowcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 10006, + "kind": "trial", + "class_id": 6, + "deck_name": "Midrange Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 10007, + "kind": "trial", + "class_id": 7, + "deck_name": "Machina Elana Havencraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 10008, + "kind": "trial", + "class_id": 8, + "deck_name": "Artifact Portalcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 11901, + "kind": "trial", + "class_id": 1, + "deck_name": "Aggro Forestcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 11902, + "kind": "trial", + "class_id": 2, + "deck_name": "Walfrid Swordcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 11905, + "kind": "trial", + "class_id": 5, + "deck_name": "Aggro Shadowcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 11906, + "kind": "trial", + "class_id": 6, + "deck_name": "Wrath Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12003, + "kind": "trial", + "class_id": 3, + "deck_name": "Mysteria Runecraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12004, + "kind": "trial", + "class_id": 4, + "deck_name": "Ramp Dragoncraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12007, + "kind": "trial", + "class_id": 7, + "deck_name": "Sanctuary Havencraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12008, + "kind": "trial", + "class_id": 8, + "deck_name": "Artifact Portalcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12104, + "kind": "trial", + "class_id": 4, + "deck_name": "Natura Dragoncraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12106, + "kind": "trial", + "class_id": 6, + "deck_name": "Epitaph Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12108, + "kind": "trial", + "class_id": 8, + "deck_name": "Machina Portalcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12201, + "kind": "trial", + "class_id": 1, + "deck_name": "Fairy Forestcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12204, + "kind": "trial", + "class_id": 4, + "deck_name": "Ramp Dragoncraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12206, + "kind": "trial", + "class_id": 6, + "deck_name": "Wrath Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12302, + "kind": "trial", + "class_id": 2, + "deck_name": "Rally Swordcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12307, + "kind": "trial", + "class_id": 7, + "deck_name": "Ward Havencraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12308, + "kind": "trial", + "class_id": 8, + "deck_name": "Artifact Portalcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12403, + "kind": "trial", + "class_id": 3, + "deck_name": "Earth Rite Runecraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12406, + "kind": "trial", + "class_id": 6, + "deck_name": "Festive Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12408, + "kind": "trial", + "class_id": 8, + "deck_name": "Puppet Portalcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12501, + "kind": "trial", + "class_id": 1, + "deck_name": "Combo Forestcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12504, + "kind": "trial", + "class_id": 4, + "deck_name": "Ramp Dragoncraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12506, + "kind": "trial", + "class_id": 6, + "deck_name": "Wrath Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12603, + "kind": "trial", + "class_id": 3, + "deck_name": "Chess Runecraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12604, + "kind": "trial", + "class_id": 4, + "deck_name": "Armed Dragoncraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12607, + "kind": "trial", + "class_id": 7, + "deck_name": "Crystallize Havencraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12702, + "kind": "trial", + "class_id": 2, + "deck_name": "Loot Swordcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12705, + "kind": "trial", + "class_id": 5, + "deck_name": "Last Words Shadowcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12708, + "kind": "trial", + "class_id": 8, + "deck_name": "Condemned Portalcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12801, + "kind": "trial", + "class_id": 1, + "deck_name": "Fairy Forestcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12802, + "kind": "trial", + "class_id": 2, + "deck_name": "Rally Swordcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12806, + "kind": "trial", + "class_id": 6, + "deck_name": "Wrath Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12807, + "kind": "trial", + "class_id": 7, + "deck_name": "Restoration Havencraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12901, + "kind": "trial", + "class_id": 1, + "deck_name": "Fairy Forestcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12902, + "kind": "trial", + "class_id": 2, + "deck_name": "Rally Swordcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12903, + "kind": "trial", + "class_id": 3, + "deck_name": "Earth Rite Runecraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12904, + "kind": "trial", + "class_id": 4, + "deck_name": "Ramp Dragoncraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12905, + "kind": "trial", + "class_id": 5, + "deck_name": "Ghost Shadowcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12906, + "kind": "trial", + "class_id": 6, + "deck_name": "Vengeance Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12907, + "kind": "trial", + "class_id": 7, + "deck_name": "Restoration Havencraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 1, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 12908, + "kind": "trial", + "class_id": 8, + "deck_name": "Artifact Portalcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13001, + "kind": "trial", + "class_id": 1, + "deck_name": "Tempo Forestcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 1, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13002, + "kind": "trial", + "class_id": 2, + "deck_name": "Rally Swordcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 1, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13003, + "kind": "trial", + "class_id": 3, + "deck_name": "Earth Rite Runecraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13006, + "kind": "trial", + "class_id": 6, + "deck_name": "Wrath Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13103, + "kind": "trial", + "class_id": 3, + "deck_name": "Mysteria Runecraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 1, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13105, + "kind": "trial", + "class_id": 5, + "deck_name": "Last Words Shadowcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 0, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13108, + "kind": "trial", + "class_id": 8, + "deck_name": "Artifact Portalcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 1, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13204, + "kind": "trial", + "class_id": 4, + "deck_name": "Natura Dragoncraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 1, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13205, + "kind": "trial", + "class_id": 5, + "deck_name": "Machina Shadowcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 1, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + }, + { + "deck_no": 13206, + "kind": "trial", + "class_id": 6, + "deck_name": "Wrath Bloodcraft", + "sleeve_id": 3000011, + "leader_skin_id": 0, + "is_recommend": 1, + "order_num": 0, + "entry_no": 0, + "deck_format": 1 + } +] From 75a2fca8bb05d92e3dbb1190c29668647ec00af4 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:25:03 -0400 Subject: [PATCH 02/10] feat(db): StoryDeckEntry presentation table + StoryDeckKind enum Co-Authored-By: Claude Sonnet 4.6 --- SVSim.Database/Enums/StoryDeckKind.cs | 11 ++++++++++ SVSim.Database/Models/StoryDeckEntry.cs | 28 +++++++++++++++++++++++++ SVSim.Database/SVSimDbContext.cs | 1 + 3 files changed, 40 insertions(+) create mode 100644 SVSim.Database/Enums/StoryDeckKind.cs create mode 100644 SVSim.Database/Models/StoryDeckEntry.cs diff --git a/SVSim.Database/Enums/StoryDeckKind.cs b/SVSim.Database/Enums/StoryDeckKind.cs new file mode 100644 index 0000000..b5eef1b --- /dev/null +++ b/SVSim.Database/Enums/StoryDeckKind.cs @@ -0,0 +1,11 @@ +namespace SVSim.Database.Enums; + +/// +/// Which story deck-select group a prebuilt deck belongs to. Build = the named story decks +/// (build_deck_list); Trial = archetype trial decks (trial_deck_list). Stored as int. +/// +public enum StoryDeckKind +{ + Build = 0, + Trial = 1, +} diff --git a/SVSim.Database/Models/StoryDeckEntry.cs b/SVSim.Database/Models/StoryDeckEntry.cs new file mode 100644 index 0000000..c37b1dd --- /dev/null +++ b/SVSim.Database/Models/StoryDeckEntry.cs @@ -0,0 +1,28 @@ +using SVSim.Database.Common; +using SVSim.Database.Enums; + +namespace SVSim.Database.Models; + +/// +/// Presentation metadata for a story-mode prebuilt/trial deck, as surfaced under +/// main_story/get_deck_list's build_deck_list / trial_deck_list. PK (DeckNo) equals the deck's +/// wire deck_no, which also equals BuildDeckProductEntry.Id — the 40-card list is read from that +/// product (single source of truth), NOT stored here. Sourced from +/// data_dumps/traffic_prod_trial_decks.ndjson via seeds/story-decks.json. +/// +public class StoryDeckEntry : BaseEntity +{ + public int DeckNo { get => Id; set => Id = value; } // == BuildDeckProductEntry.Id + + public StoryDeckKind Kind { get; set; } + public int ClassId { get; set; } + public string DeckName { get; set; } = string.Empty; + public int SleeveId { get; set; } + public int LeaderSkinId { get; set; } + public int IsRecommend { get; set; } + public int OrderNum { get; set; } + public int EntryNo { get; set; } + + /// Trial decks carry a deck_format on the wire; build decks do not (null). + public int? DeckFormat { get; set; } +} diff --git a/SVSim.Database/SVSimDbContext.cs b/SVSim.Database/SVSimDbContext.cs index b2e5a7b..2e9502e 100644 --- a/SVSim.Database/SVSimDbContext.cs +++ b/SVSim.Database/SVSimDbContext.cs @@ -70,6 +70,7 @@ public class SVSimDbContext : DbContext public DbSet Packs => Set(); public DbSet BuildDeckSeries => Set(); public DbSet BuildDeckProducts => Set(); + public DbSet StoryDecks => Set(); public DbSet SleeveShopSeries => Set(); public DbSet SleeveShopProducts => Set(); public DbSet ItemPurchaseCatalog => Set(); From e0da7f09ca5b8fda9307d7db5ce72fc8a462253e Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:28:50 -0400 Subject: [PATCH 03/10] feat(db): AddStoryDeck migration (DDL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creates StoryDecks table with all required columns (Id PK, DeckNo, Kind, ClassId, DeckName, SleeveId, LeaderSkinId, IsRecommend, OrderNum, EntryNo, DeckFormat nullable, DateCreated, DateUpdated). Pure DDL — no InsertData/HasData. Co-Authored-By: Claude Sonnet 4.6 --- .../20260529142631_AddStoryDeck.Designer.cs | 3823 +++++++++++++++++ .../Migrations/20260529142631_AddStoryDeck.cs | 45 + .../Migrations/SVSimDbContextModelSnapshot.cs | 47 + 3 files changed, 3915 insertions(+) create mode 100644 SVSim.Database/Migrations/20260529142631_AddStoryDeck.Designer.cs create mode 100644 SVSim.Database/Migrations/20260529142631_AddStoryDeck.cs diff --git a/SVSim.Database/Migrations/20260529142631_AddStoryDeck.Designer.cs b/SVSim.Database/Migrations/20260529142631_AddStoryDeck.Designer.cs new file mode 100644 index 0000000..8c85277 --- /dev/null +++ b/SVSim.Database/Migrations/20260529142631_AddStoryDeck.Designer.cs @@ -0,0 +1,3823 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SVSim.Database; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + [DbContext(typeof(SVSimDbContext))] + [Migration("20260529142631_AddStoryDeck")] + partial class AddStoryDeck + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("ShortUdidSequence") + .StartsAt(400000000L); + + modelBuilder.Entity("DegreeEntryViewer", b => + { + b.Property("DegreesId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("DegreesId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("DegreeEntryViewer"); + }); + + modelBuilder.Entity("EmblemEntryViewer", b => + { + b.Property("EmblemsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("EmblemsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("EmblemEntryViewer"); + }); + + modelBuilder.Entity("LeaderSkinEntryViewer", b => + { + b.Property("LeaderSkinsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("LeaderSkinsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("LeaderSkinEntryViewer"); + }); + + modelBuilder.Entity("MyPageBackgroundEntryViewer", b => + { + b.Property("MyPageBackgroundsId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("MyPageBackgroundsId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("MyPageBackgroundEntryViewer"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.SpecialBattleSetting", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BanishEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("ClassDestroyEffectOverride") + .HasColumnType("integer"); + + b.Property("EnemyAttachSkill") + .IsRequired() + .HasColumnType("text"); + + b.Property("EnemyStartLife") + .HasColumnType("integer"); + + b.Property("EnemyStartPp") + .HasColumnType("integer"); + + b.Property("IdOverrideInBattleLog") + .IsRequired() + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("PlayerAttachSkill") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlayerFirstTurn") + .HasColumnType("integer"); + + b.Property("PlayerStartLife") + .HasColumnType("integer"); + + b.Property("PlayerStartPp") + .HasColumnType("integer"); + + b.Property("ResultSkip") + .HasColumnType("integer"); + + b.Property("SpecialTokenDrawEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenDrawEffectOverride") + .IsRequired() + .HasColumnType("text"); + + b.Property("VsEffectOverride") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SpecialBattleSettings"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryChapter", b => + { + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("Battle3dFieldId") + .HasColumnType("integer"); + + b.Property("BattleExists") + .HasColumnType("boolean"); + + b.Property("BgFileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("BgmId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChapterClearTextId") + .HasColumnType("text"); + + b.Property("ChapterEffectPath") + .HasColumnType("text"); + + b.Property("ChapterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("EnemyAiId") + .HasColumnType("integer"); + + b.Property("EnemyCharaId") + .HasColumnType("integer"); + + b.Property("EnemyClass") + .HasColumnType("integer"); + + b.Property("IsCameraMovable") + .HasColumnType("integer"); + + b.Property("IsMaintenanceChapter") + .HasColumnType("boolean"); + + b.Property("IsPlayAnotherEndAppearanceAnimation") + .HasColumnType("boolean"); + + b.Property("IsReleasedAnotherEnd") + .HasColumnType("boolean"); + + b.Property("IsSkipEnabled") + .HasColumnType("boolean"); + + b.Property("NextChapterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReleasePoint") + .HasColumnType("integer"); + + b.Property("RequiredChapterId") + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("integer"); + + b.Property("SelectionDisplayPosition") + .HasColumnType("text"); + + b.Property("SelectionTextId") + .HasColumnType("text"); + + b.Property("ShowCoordinate") + .HasColumnType("integer"); + + b.Property("ShowSubtitles") + .HasColumnType("integer"); + + b.Property("SpecialBattleSettingId") + .HasColumnType("integer"); + + b.Property("UnlockText") + .HasColumnType("text"); + + b.Property("XCoordinate") + .HasColumnType("numeric"); + + b.Property("YCoordinate") + .HasColumnType("numeric"); + + b.HasKey("StoryId"); + + b.HasIndex("NextChapterId"); + + b.HasIndex("SpecialBattleSettingId"); + + b.HasIndex("SectionId", "CharaId", "ChapterId"); + + b.ToTable("StoryChapters"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StorySection", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AllStoryOrderId") + .HasColumnType("integer"); + + b.Property("BackGroundId") + .HasColumnType("integer"); + + b.Property("ChapterSelectType") + .HasColumnType("integer"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsLeaderSelect") + .HasColumnType("boolean"); + + b.Property("IsPlayAnotherEndAppearanceAnimation") + .HasColumnType("boolean"); + + b.Property("IsSpoiler") + .HasColumnType("integer"); + + b.Property("IsUnderMaintenance") + .HasColumnType("boolean"); + + b.Property("NameTextKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("SpoilerMessage") + .IsRequired() + .HasColumnType("text"); + + b.Property("StoryApiType") + .HasColumnType("integer"); + + b.Property("StoryTypeOverwrite") + .HasColumnType("integer"); + + b.Property("WorldId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WorldId"); + + b.ToTable("StorySections"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryWorld", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("PanelImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RibbonText") + .IsRequired() + .HasColumnType("text"); + + b.Property("TitleTextKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("StoryWorlds"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.ViewerStoryBranchUnlock", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("UnlockedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "StoryId"); + + b.ToTable("ViewerStoryBranchUnlocks"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.ViewerStoryProgress", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("StoryId") + .HasColumnType("integer"); + + b.Property("FinishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsFinish") + .HasColumnType("boolean"); + + b.Property("IsSkipped") + .HasColumnType("boolean"); + + b.Property("SkippedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "StoryId"); + + b.ToTable("ViewerStoryProgress"); + }); + + modelBuilder.Entity("SVSim.Database.Models.AchievementCatalogEntry", b => + { + b.Property("AchievementType") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.HasKey("AchievementType", "Level"); + + b.HasIndex("AchievementType"); + + b.HasIndex("EventType", "EventArg"); + + b.ToTable("AchievementCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ArenaSeasonConfig", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Cost") + .HasColumnType("numeric(20,0)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Enable") + .HasColumnType("integer"); + + b.Property("FormatInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsJoin") + .HasColumnType("boolean"); + + b.Property("Mode") + .HasColumnType("integer"); + + b.Property("RupyCost") + .HasColumnType("numeric(20,0)"); + + b.Property("TicketCost") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ArenaSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.AvatarAbilityEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Ability") + .IsRequired() + .HasColumnType("text"); + + b.Property("AbilityCost") + .IsRequired() + .HasColumnType("text"); + + b.Property("AbilityDesc") + .IsRequired() + .HasColumnType("text"); + + b.Property("BattleStartFirstPlayerTurnBp") + .HasColumnType("integer"); + + b.Property("BattleStartMaxLife") + .HasColumnType("integer"); + + b.Property("BattleStartSecondPlayerTurnBp") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("PassiveAbility") + .IsRequired() + .HasColumnType("text"); + + b.Property("PassiveAbilityDesc") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AvatarAbilities"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BannerEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ChangeTime") + .HasColumnType("integer"); + + b.Property("Click") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImagePaths") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Banners"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassLevelEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("RequiredPoint") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("BattlePassLevels"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassMonthlyMissionEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BattlePassPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("Month") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("Year") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Year", "Month"); + + b.HasIndex("Year", "Month", "OrderNum") + .IsUnique(); + + b.ToTable("BattlePassMonthlyMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassRewardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAppealExclusion") + .HasColumnType("boolean"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Track") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId", "Track", "Level") + .IsUnique(); + + b.ToTable("BattlePassRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassSeasonEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CanPurchase") + .HasColumnType("boolean"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxLevel") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceCrystal") + .HasColumnType("integer"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("StartDate", "EndDate"); + + b.ToTable("BattlePassSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlefieldEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsOpen") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Battlefields"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("FeaturedCardId") + .HasColumnType("bigint"); + + b.Property("IntroPriceCrystal") + .HasColumnType("integer"); + + b.Property("IntroPriceRupy") + .HasColumnType("integer"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LeaderId") + .HasColumnType("integer"); + + b.Property("ProductNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("PurchaseNumMax") + .HasColumnType("integer"); + + b.Property("RegularPriceCrystal") + .HasColumnType("integer"); + + b.Property("RegularPriceRupy") + .HasColumnType("integer"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("BuildDeckProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DrumrollPath") + .IsRequired() + .HasColumnType("text"); + + b.Property("IntroKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("NameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderIndex") + .HasColumnType("integer"); + + b.Property("TitlePath") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BuildDeckSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.CardCosmeticReward", b => + { + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("CosmeticId") + .HasColumnType("bigint"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("CardId", "Type", "CosmeticId"); + + b.HasIndex("CardId"); + + b.ToTable("CardCosmeticRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Classes"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassExpEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("NecessaryExp") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ClassExpCurve"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ColosseumConfig", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CardPoolName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColosseumId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColosseumName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAllCardEnabled") + .HasColumnType("integer"); + + b.Property("IsColosseumPeriod") + .HasColumnType("boolean"); + + b.Property("IsDisplayTips") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsNormalTwoPick") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsRoundPeriod") + .HasColumnType("boolean"); + + b.Property("IsSpecialMode") + .IsRequired() + .HasColumnType("text"); + + b.Property("NowRound") + .IsRequired() + .HasColumnType("text"); + + b.Property("SalesPeriodInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("TipsId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Colosseums"); + }); + + modelBuilder.Entity("SVSim.Database.Models.DailyLoginBonusEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BonusData") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("BonusId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("DailyLoginBonuses"); + }); + + modelBuilder.Entity("SVSim.Database.Models.DefaultDeckEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CardIdArray") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("SleeveId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("DefaultDecks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.DegreeEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Degrees"); + }); + + modelBuilder.Entity("SVSim.Database.Models.EmblemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Emblems"); + }); + + modelBuilder.Entity("SVSim.Database.Models.FeatureMaintenanceEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("FeatureMaintenances"); + }); + + modelBuilder.Entity("SVSim.Database.Models.GameConfigSection", b => + { + b.Property("SectionName") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ValueJson") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("SectionName"); + + b.ToTable("GameConfigs"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ItemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ThumbnailPath") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Items"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ItemPurchaseCatalogEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsMonthlyReset") + .HasColumnType("boolean"); + + b.Property("PurchaseItemId") + .HasColumnType("bigint"); + + b.Property("PurchaseItemNum") + .HasColumnType("integer"); + + b.Property("PurchaseItemType") + .HasColumnType("integer"); + + b.Property("PurchaseLimit") + .HasColumnType("integer"); + + b.Property("PurchaseName") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequireItemId") + .HasColumnType("bigint"); + + b.Property("RequireItemNum") + .HasColumnType("integer"); + + b.Property("RequireItemType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ItemPurchaseCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EmoteId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.ToTable("LeaderSkins"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CvNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IntroductionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("ProductNameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.Property("SinglePriceCrystal") + .HasColumnType("integer"); + + b.Property("SinglePriceRupy") + .HasColumnType("integer"); + + b.Property("SinglePriceTicket") + .HasColumnType("integer"); + + b.Property("TicketItemId") + .HasColumnType("bigint"); + + b.Property("TicketNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("LeaderSkinShopProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("SetCompletionRewardStatus") + .HasColumnType("integer"); + + b.Property("SetPriceCrystal") + .HasColumnType("integer"); + + b.Property("SetPriceRupy") + .HasColumnType("integer"); + + b.Property("SetPriceTicket") + .HasColumnType("integer"); + + b.Property("SetPriceTicketId") + .HasColumnType("bigint"); + + b.Property("SetSalesStatus") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("LeaderSkinShopSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LoadingExclusionCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("LoadingExclusionCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MaintenanceCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MaintenanceCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MasterPointRankingPeriodEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BeginTime") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("NecessaryScore") + .HasColumnType("bigint"); + + b.Property("PeriodNum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MasterPointRankingPeriods"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MissionCatalogEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BattlePassPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultFlag") + .HasColumnType("boolean"); + + b.Property("EndTime") + .HasColumnType("bigint"); + + b.Property("EventArg") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("text"); + + b.Property("LotType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("LotType"); + + b.HasIndex("EventType", "EventArg"); + + b.ToTable("MissionCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyPageBackgroundEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MyPageBackgrounds"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyRotationAbilityEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AbilityId") + .HasColumnType("integer"); + + b.Property("Data") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("MyRotationAbilities"); + }); + + modelBuilder.Entity("SVSim.Database.Models.MyRotationSettingEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AbilitiesCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("CardSetIdsCsv") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ReprintedCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RestrictedCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RotationId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MyRotationSettings"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackConfigEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BasePackId") + .HasColumnType("integer"); + + b.Property("CommenceDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CompleteDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("GachaDetail") + .IsRequired() + .HasColumnType("text"); + + b.Property("GachaType") + .HasColumnType("integer"); + + b.Property("IsHide") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.Property("OpenCountLimit") + .HasColumnType("integer"); + + b.Property("OverrideDrawEffectPackId") + .HasColumnType("integer"); + + b.Property("OverrideUiEffectPackId") + .HasColumnType("integer"); + + b.Property("PackCategory") + .HasColumnType("integer"); + + b.Property("PosterType") + .HasColumnType("integer"); + + b.Property("SalesPeriodTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.Property("SpecialSleeveId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Packs"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PaymentItemEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ChargeCrystalNum") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FreeCrystalNum") + .HasColumnType("integer"); + + b.Property("ImageName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsResaleProduct") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("ProductId") + .HasColumnType("integer"); + + b.Property("PurchaseLimit") + .HasColumnType("integer"); + + b.Property("RemainingTime") + .HasColumnType("integer"); + + b.Property("ResaleStartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SpecialShopFlag") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StoreProductId") + .HasColumnType("bigint"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PaymentItems"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PracticeOpponentEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AiDeckLevel") + .HasColumnType("integer"); + + b.Property("AiLogicLevel") + .HasColumnType("integer"); + + b.Property("AiMaxLife") + .HasColumnType("integer"); + + b.Property("Battle3dFieldId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DegreeId") + .HasColumnType("integer"); + + b.Property("IsCampaignPractice") + .HasColumnType("boolean"); + + b.Property("IsMaintenance") + .HasColumnType("boolean"); + + b.Property("PracticeId") + .HasColumnType("integer"); + + b.Property("TextId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PracticeOpponents"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PreReleaseInfo", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CardMasterId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultCardMasterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisplayEndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FreeMatchStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPreRotationFreeMatchTerm") + .HasColumnType("boolean"); + + b.Property("LatestReprintedBaseCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("NextCardSetId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreReleaseCardMasterId") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreReleaseId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReprintedBaseCardIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RotationCardSetIdList") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("PreReleaseInfos"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("GroupId") + .HasColumnType("integer"); + + b.Property("IsAdditional") + .HasColumnType("boolean"); + + b.Property("IsPlayable") + .HasColumnType("boolean"); + + b.Property("PuzzleDifficulty") + .HasColumnType("integer"); + + b.Property("PuzzleId") + .HasColumnType("integer"); + + b.Property("ReleaseConditionTextId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("Puzzles"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleGroupEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("BasicTitleTextId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CharaId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DifficultyNameListJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("PuzzleCharaId") + .HasColumnType("integer"); + + b.Property("PuzzleMasterId") + .HasColumnType("integer"); + + b.Property("SortType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PuzzleGroups"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleMissionEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AchievedMessage") + .IsRequired() + .HasColumnType("text"); + + b.Property("CampaignCommenceTime") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("MissionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("RequireNumber") + .HasColumnType("integer"); + + b.Property("RewardDetailId") + .HasColumnType("bigint"); + + b.Property("RewardNumber") + .HasColumnType("integer"); + + b.Property("RewardType") + .HasColumnType("integer"); + + b.Property("TargetPuzzleGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PuzzleMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.RankInfoEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("AccumulateMasterPoint") + .HasColumnType("integer"); + + b.Property("AccumulatePoint") + .HasColumnType("integer"); + + b.Property("BaseAddBp") + .HasColumnType("integer"); + + b.Property("BaseDropBp") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPromotionWar") + .HasColumnType("integer"); + + b.Property("LoseBonus") + .HasColumnType("double precision"); + + b.Property("LowerLimitPoint") + .HasColumnType("integer"); + + b.Property("MatchCount") + .HasColumnType("integer"); + + b.Property("MaxLoseBonus") + .HasColumnType("integer"); + + b.Property("MaxWinBonus") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NecessaryPoint") + .HasColumnType("integer"); + + b.Property("NecessaryWin") + .HasColumnType("integer"); + + b.Property("ResetLose") + .HasColumnType("integer"); + + b.Property("StreakBonusPt") + .HasColumnType("integer"); + + b.Property("WinBonus") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.ToTable("RankInfo"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ReprintedCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("ReprintedCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SealedConfig", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("CrystalCost") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckUsingNumMin") + .HasColumnType("integer"); + + b.Property("Enable") + .HasColumnType("integer"); + + b.Property("IsDeckCodeMaintenance") + .HasColumnType("boolean"); + + b.Property("IsJoin") + .HasColumnType("boolean"); + + b.Property("PackInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("RupyCost") + .HasColumnType("integer"); + + b.Property("SalesPeriodInfo") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ScheduleId") + .HasColumnType("integer"); + + b.Property("TicketCost") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("SealedSeasons"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("Attack") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Defense") + .HasColumnType("integer"); + + b.Property("IsFoil") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrimaryResourceCost") + .HasColumnType("integer"); + + b.Property("Rarity") + .HasColumnType("integer"); + + b.Property("ShadowverseCardSetEntryId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("ShadowverseCardSetEntryId"); + + b.ToTable("Cards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardSetEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsBasic") + .HasColumnType("boolean"); + + b.Property("IsInRotation") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("CardSets"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Format") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("MyRotationId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("RandomLeaderSkin") + .HasColumnType("boolean"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ClassId"); + + b.HasIndex("LeaderSkinId"); + + b.HasIndex("SleeveId"); + + b.HasIndex("ViewerId"); + + b.ToTable("Decks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Sleeves"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopProductEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("NameKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("PriceCrystal") + .HasColumnType("integer"); + + b.Property("PriceRupy") + .HasColumnType("integer"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("SleeveShopProducts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopSeriesEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsNew") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("SleeveShopSeries"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpecialDeckFormatEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("SpecialDeckFormats"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpotCardEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("Cost") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("SpotCards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SpotCardExchangeEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ExchangePoint") + .HasColumnType("integer"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.Property("TsRotationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("SpotCardExchangeCatalog"); + }); + + modelBuilder.Entity("SVSim.Database.Models.StoryDeckEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .HasColumnType("integer"); + + b.Property("DeckName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("EntryNo") + .HasColumnType("integer"); + + b.Property("IsRecommend") + .HasColumnType("integer"); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("StoryDecks"); + }); + + modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("RestrictionValue") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("UnlimitedRestrictions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("ShortUdid") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValueSql("nextval('\"ShortUdidSequence\"')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("ShortUdid"), "ShortUdidSequence"); + + b.Property("Udid") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ShortUdid"); + + b.HasIndex("Udid") + .IsUnique(); + + b.ToTable("Viewers"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerAchievement", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("AchievementType") + .HasColumnType("integer"); + + b.Property("AchievementStatus") + .HasColumnType("integer"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("NowAchievedLevel") + .HasColumnType("integer"); + + b.Property("ResultAnnounceSawLevel") + .HasColumnType("integer"); + + b.HasKey("ViewerId", "AchievementType"); + + b.ToTable("ViewerAchievements"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerBattlePassClaimEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Level") + .HasColumnType("integer"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("Track") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId", "SeasonId"); + + b.HasIndex("ViewerId", "SeasonId", "Track", "Level") + .IsUnique(); + + b.ToTable("ViewerBattlePassClaims"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerBattlePassProgressEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentPoint") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPremium") + .HasColumnType("boolean"); + + b.Property("SeasonId") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("WeeklyPeriodStart") + .HasColumnType("timestamp with time zone"); + + b.Property("WeeklyPoints") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId", "SeasonId") + .IsUnique(); + + b.ToTable("ViewerBattlePassProgress"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerClaimedTutorialGift", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("PresentId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "PresentId"); + + b.ToTable("ViewerClaimedTutorialGifts"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerEventCounter", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("EventKey") + .HasColumnType("text"); + + b.Property("Period") + .HasColumnType("text"); + + b.Property("Count") + .HasColumnType("integer"); + + b.HasKey("ViewerId", "EventKey", "Period"); + + b.HasIndex("ViewerId", "Period"); + + b.ToTable("ViewerEventCounters"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerLeaderSkinSetClaim", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("SeriesId") + .HasColumnType("integer"); + + b.Property("ClaimedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "SeriesId"); + + b.HasIndex("ViewerId"); + + b.ToTable("ViewerLeaderSkinSetClaims"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AssignedAt") + .HasColumnType("bigint"); + + b.Property("ClaimedAt") + .HasColumnType("bigint"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("MissionCatalogId") + .HasColumnType("integer"); + + b.Property("MissionStatus") + .HasColumnType("integer"); + + b.Property("Slot") + .HasColumnType("integer"); + + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("ViewerId"); + + b.HasIndex("ViewerId", "Slot") + .IsUnique(); + + b.ToTable("ViewerMissions"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerPuzzleClear", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("PuzzleId") + .HasColumnType("integer"); + + b.Property("BestRetryCount") + .HasColumnType("integer"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ViewerId", "PuzzleId"); + + b.ToTable("ViewerPuzzleClears"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerSpotCardExchange", b => + { + b.Property("ViewerId") + .HasColumnType("bigint"); + + b.Property("CardId") + .HasColumnType("bigint"); + + b.Property("ExchangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPreRelease") + .HasColumnType("boolean"); + + b.HasKey("ViewerId", "CardId"); + + b.HasIndex("ViewerId"); + + b.ToTable("ViewerSpotCardExchanges"); + }); + + modelBuilder.Entity("SleeveEntryViewer", b => + { + b.Property("SleevesId") + .HasColumnType("integer"); + + b.Property("ViewersId") + .HasColumnType("bigint"); + + b.HasKey("SleevesId", "ViewersId"); + + b.HasIndex("ViewersId"); + + b.ToTable("SleeveEntryViewer"); + }); + + modelBuilder.Entity("DegreeEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.DegreeEntry", null) + .WithMany() + .HasForeignKey("DegreesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("EmblemEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.EmblemEntry", null) + .WithMany() + .HasForeignKey("EmblemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LeaderSkinEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.LeaderSkinEntry", null) + .WithMany() + .HasForeignKey("LeaderSkinsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MyPageBackgroundEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.MyPageBackgroundEntry", null) + .WithMany() + .HasForeignKey("MyPageBackgroundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StoryChapter", b => + { + b.HasOne("SVSim.Database.Entities.Story.StorySection", "Section") + .WithMany() + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Entities.Story.SpecialBattleSetting", "SpecialBattleSetting") + .WithMany() + .HasForeignKey("SpecialBattleSettingId"); + + b.OwnsMany("SVSim.Database.Entities.Story.StoryChapterBattleSetting", "BattleSettings", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Battle3dFieldIdOverride") + .HasColumnType("integer"); + + b1.Property("BgmIdOverride") + .HasColumnType("integer"); + + b1.Property("DeckClassId") + .HasColumnType("integer"); + + b1.Property("DeckSkinIdOverride") + .HasColumnType("integer"); + + b1.Property("EnemyEmotionOverride") + .HasColumnType("integer"); + + b1.Property("PlayerEmotionOverride") + .HasColumnType("integer"); + + b1.Property("SkinIdOverride") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StoryChapterBattleSetting"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.OwnsMany("SVSim.Database.Entities.Story.StoryChapterReward", "Rewards", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StoryChapterReward"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.OwnsMany("SVSim.Database.Entities.Story.StorySubChapter", "SubChapters", b1 => + { + b1.Property("StoryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("IsMaintenanceChapter") + .HasColumnType("boolean"); + + b1.Property("SubChapterId") + .HasColumnType("integer"); + + b1.Property("SubChapterStoryId") + .HasColumnType("integer"); + + b1.HasKey("StoryId", "Id"); + + b1.ToTable("StorySubChapter"); + + b1.WithOwner() + .HasForeignKey("StoryId"); + }); + + b.Navigation("BattleSettings"); + + b.Navigation("Rewards"); + + b.Navigation("Section"); + + b.Navigation("SpecialBattleSetting"); + + b.Navigation("SubChapters"); + }); + + modelBuilder.Entity("SVSim.Database.Entities.Story.StorySection", b => + { + b.HasOne("SVSim.Database.Entities.Story.StoryWorld", "World") + .WithMany() + .HasForeignKey("WorldId"); + + b.Navigation("World"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassRewardEntry", b => + { + b.HasOne("SVSim.Database.Models.BattlePassSeasonEntry", "Season") + .WithMany("Rewards") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckProductEntry", b => + { + b.HasOne("SVSim.Database.Models.BuildDeckSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.BuildDeckProductCardEntry", "Cards", b1 => + { + b1.Property("BuildDeckProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("IsSpot") + .HasColumnType("boolean"); + + b1.Property("Number") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckProductEntryId", "Id"); + + b1.ToTable("BuildDeckProductCardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckProductEntryId"); + }); + + b.OwnsMany("SVSim.Database.Models.BuildDeckProductRewardEntry", "Rewards", b1 => + { + b1.Property("BuildDeckProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("MessageId") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardIndex") + .HasColumnType("integer"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckProductEntryId", "Id"); + + b1.ToTable("BuildDeckProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckProductEntryId"); + }); + + b.Navigation("Cards"); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.OwnsMany("SVSim.Database.Models.BuildDeckSeriesRewardEntry", "SeriesRewards", b1 => + { + b1.Property("BuildDeckSeriesEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ItemIndex") + .HasColumnType("integer"); + + b1.Property("MessageId") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.Property("TierIndex") + .HasColumnType("integer"); + + b1.HasKey("BuildDeckSeriesEntryId", "Id"); + + b1.ToTable("BuildDeckSeriesRewardEntry"); + + b1.WithOwner() + .HasForeignKey("BuildDeckSeriesEntryId"); + }); + + b.Navigation("SeriesRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.CardCosmeticReward", b => + { + b.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Card"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany("LeaderSkins") + .HasForeignKey("ClassId"); + + b.Navigation("Class"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopProductEntry", b => + { + b.HasOne("SVSim.Database.Models.LeaderSkinShopSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.LeaderSkinShopProductRewardEntry", "Rewards", b1 => + { + b1.Property("LeaderSkinShopProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("LeaderSkinShopProductEntryId", "Id"); + + b1.ToTable("LeaderSkinShopProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("LeaderSkinShopProductEntryId"); + }); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.OwnsMany("SVSim.Database.Models.LeaderSkinShopSeriesRewardEntry", "SetCompletionRewards", b1 => + { + b1.Property("LeaderSkinShopSeriesEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("LeaderSkinShopSeriesEntryId", "Id"); + + b1.ToTable("LeaderSkinShopSeriesRewardEntry"); + + b1.WithOwner() + .HasForeignKey("LeaderSkinShopSeriesEntryId"); + }); + + b.Navigation("SetCompletionRewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PackConfigEntry", b => + { + b.OwnsMany("SVSim.Database.Models.PackBannerEntry", "Banners", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("BannerName") + .IsRequired() + .HasColumnType("text"); + + b1.Property("DialogTitle") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("PackConfigEntryId", "Id"); + + b1.ToTable("PackBannerEntry"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.OwnsMany("SVSim.Database.Models.PackChildGachaEntry", "ChildGachas", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CampaignName") + .HasColumnType("text"); + + b1.Property("CardCount") + .HasColumnType("integer"); + + b1.Property("Cost") + .HasColumnType("integer"); + + b1.Property("FreeGachaCampaignId") + .HasColumnType("integer"); + + b1.Property("GachaId") + .HasColumnType("integer"); + + b1.Property("IsDailySingle") + .HasColumnType("boolean"); + + b1.Property("ItemId") + .HasColumnType("bigint"); + + b1.Property("OverrideIncreaseGachaPoint") + .HasColumnType("integer"); + + b1.Property("PurchaseLimitCount") + .HasColumnType("integer"); + + b1.Property("TypeDetail") + .HasColumnType("integer"); + + b1.HasKey("PackConfigEntryId", "Id"); + + b1.ToTable("PackChildGachaEntry"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.OwnsOne("SVSim.Database.Models.PackGachaPointConfig", "GachaPointConfig", b1 => + { + b1.Property("PackConfigEntryId") + .HasColumnType("integer"); + + b1.Property("ExchangeablePoint") + .HasColumnType("integer"); + + b1.Property("IncreaseGachaPoint") + .HasColumnType("integer"); + + b1.HasKey("PackConfigEntryId"); + + b1.ToTable("Packs"); + + b1.WithOwner() + .HasForeignKey("PackConfigEntryId"); + }); + + b.Navigation("Banners"); + + b.Navigation("ChildGachas"); + + b.Navigation("GachaPointConfig"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleEntry", b => + { + b.HasOne("SVSim.Database.Models.PuzzleGroupEntry", "Group") + .WithMany("Puzzles") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId"); + + b.HasOne("SVSim.Database.Models.ShadowverseCardSetEntry", null) + .WithMany("Cards") + .HasForeignKey("ShadowverseCardSetEntryId"); + + b.OwnsOne("SVSim.Database.Models.CardCollectionInfo", "CollectionInfo", b1 => + { + b1.Property("ShadowverseCardEntryId") + .HasColumnType("bigint"); + + b1.Property("CraftCost") + .HasColumnType("integer"); + + b1.Property("DustReward") + .HasColumnType("integer"); + + b1.HasKey("ShadowverseCardEntryId"); + + b1.ToTable("Cards"); + + b1.WithOwner() + .HasForeignKey("ShadowverseCardEntryId"); + }); + + b.Navigation("Class"); + + b.Navigation("CollectionInfo"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b => + { + b.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.LeaderSkinEntry", "LeaderSkin") + .WithMany() + .HasForeignKey("LeaderSkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.SleeveEntry", "Sleeve") + .WithMany() + .HasForeignKey("SleeveId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Decks") + .HasForeignKey("ViewerId"); + + b.OwnsMany("SVSim.Database.Models.DeckCard", "Cards", b1 => + { + b1.Property("ShadowverseDeckEntryId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.HasKey("ShadowverseDeckEntryId", "Id"); + + b1.HasIndex("CardId"); + + b1.ToTable("DeckCard"); + + b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ShadowverseDeckEntryId"); + + b1.Navigation("Card"); + }); + + b.Navigation("Cards"); + + b.Navigation("Class"); + + b.Navigation("LeaderSkin"); + + b.Navigation("Sleeve"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopProductEntry", b => + { + b.HasOne("SVSim.Database.Models.SleeveShopSeriesEntry", "Series") + .WithMany("Products") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("SVSim.Database.Models.SleeveShopProductRewardEntry", "Rewards", b1 => + { + b1.Property("SleeveShopProductEntryId") + .HasColumnType("integer"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("OrderIndex") + .HasColumnType("integer"); + + b1.Property("RewardDetailId") + .HasColumnType("bigint"); + + b1.Property("RewardNumber") + .HasColumnType("integer"); + + b1.Property("RewardType") + .HasColumnType("integer"); + + b1.HasKey("SleeveShopProductEntryId", "Id"); + + b1.ToTable("SleeveShopProductRewardEntry"); + + b1.WithOwner() + .HasForeignKey("SleeveShopProductEntryId"); + }); + + b.Navigation("Rewards"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.OwnsMany("SVSim.Database.Models.OwnedCardEntry", "Cards", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.Property("IsProtected") + .HasColumnType("boolean"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("CardId"); + + b1.HasIndex("ViewerId", "CardId") + .IsUnique(); + + b1.ToTable("OwnedCardEntry"); + + b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + + b1.Navigation("Card"); + }); + + b.OwnsMany("SVSim.Database.Models.OwnedItemEntry", "Items", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Count") + .HasColumnType("integer"); + + b1.Property("ItemId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ItemId"); + + b1.HasIndex("ViewerId", "ItemId") + .IsUnique(); + + b1.ToTable("OwnedItemEntry"); + + b1.HasOne("SVSim.Database.Models.ItemEntry", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Item"); + + b1.Navigation("Viewer"); + }); + + b.OwnsMany("SVSim.Database.Models.SocialAccountConnection", "SocialAccountConnections", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("AccountId") + .HasColumnType("numeric(20,0)"); + + b1.Property("AccountType") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("AccountType", "AccountId") + .IsUnique(); + + b1.ToTable("SocialAccountConnection"); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Viewer"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerBuildDeckProductPurchase", "BuildDeckPurchases", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ProductId") + .HasColumnType("integer"); + + b1.Property("PurchaseCount") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ViewerId", "ProductId") + .IsUnique(); + + b1.ToTable("ViewerBuildDeckProductPurchase"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerClassData", "Classes", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ClassId") + .HasColumnType("integer"); + + b1.Property("Exp") + .HasColumnType("integer"); + + b1.Property("LeaderSkinId") + .HasColumnType("integer"); + + b1.Property("Level") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ClassId"); + + b1.HasIndex("LeaderSkinId"); + + b1.ToTable("ViewerClassData"); + + b1.HasOne("SVSim.Database.Models.ClassEntry", "Class") + .WithMany() + .HasForeignKey("ClassId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.HasOne("SVSim.Database.Models.LeaderSkinEntry", "LeaderSkin") + .WithMany() + .HasForeignKey("LeaderSkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner("Viewer") + .HasForeignKey("ViewerId"); + + b1.Navigation("Class"); + + b1.Navigation("LeaderSkin"); + + b1.Navigation("Viewer"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerCurrency", "Currency", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("AndroidCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("Crystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("DmmCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("FreeCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("IosCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("LifeTotalCrystals") + .HasColumnType("numeric(20,0)"); + + b1.Property("RedEther") + .HasColumnType("numeric(20,0)"); + + b1.Property("Rupees") + .HasColumnType("numeric(20,0)"); + + b1.Property("SpotPoints") + .HasColumnType("numeric(20,0)"); + + b1.Property("SteamCrystals") + .HasColumnType("numeric(20,0)"); + + b1.HasKey("ViewerId"); + + b1.ToTable("Viewers"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerGachaPointBalance", "GachaPointBalances", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("PackId") + .HasColumnType("integer"); + + b1.Property("Points") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ViewerId", "PackId") + .IsUnique(); + + b1.ToTable("ViewerGachaPointBalance"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerGachaPointReceived", "GachaPointReceived", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CardId") + .HasColumnType("bigint"); + + b1.Property("PackId") + .HasColumnType("integer"); + + b1.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b1.HasKey("ViewerId", "Id"); + + b1.HasIndex("ViewerId", "PackId", "CardId") + .IsUnique(); + + b1.ToTable("ViewerGachaPointReceived"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerInfo", "Info", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b1.Property("CountryCode") + .IsRequired() + .HasColumnType("text"); + + b1.Property("IsOfficial") + .HasColumnType("boolean"); + + b1.Property("IsOfficialMarkDisplayed") + .HasColumnType("boolean"); + + b1.Property("MaxFriends") + .HasColumnType("integer"); + + b1.Property("SelectedDegreeId") + .HasColumnType("integer"); + + b1.Property("SelectedEmblemId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId"); + + b1.HasIndex("SelectedDegreeId"); + + b1.HasIndex("SelectedEmblemId"); + + b1.ToTable("Viewers"); + + b1.HasOne("SVSim.Database.Models.DegreeEntry", "SelectedDegree") + .WithMany() + .HasForeignKey("SelectedDegreeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.HasOne("SVSim.Database.Models.EmblemEntry", "SelectedEmblem") + .WithMany() + .HasForeignKey("SelectedEmblemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + + b1.Navigation("SelectedDegree"); + + b1.Navigation("SelectedEmblem"); + }); + + b.OwnsOne("SVSim.Database.Models.ViewerMissionData", "MissionData", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("HasReceivedPickTwoMission") + .HasColumnType("boolean"); + + b1.Property("MissionChangeTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("MissionReceiveType") + .HasColumnType("integer"); + + b1.Property("TutorialState") + .HasColumnType("integer"); + + b1.HasKey("ViewerId"); + + b1.ToTable("Viewers"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.OwnsMany("SVSim.Database.Models.ViewerPackOpenCount", "PackOpenCounts", b1 => + { + b1.Property("ViewerId") + .HasColumnType("bigint"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("LastDailyFreeAt") + .HasColumnType("timestamp with time zone"); + + b1.Property("OpenCount") + .HasColumnType("integer"); + + b1.Property("PackId") + .HasColumnType("integer"); + + b1.HasKey("ViewerId", "Id"); + + b1.ToTable("ViewerPackOpenCount"); + + b1.WithOwner() + .HasForeignKey("ViewerId"); + }); + + b.Navigation("BuildDeckPurchases"); + + b.Navigation("Cards"); + + b.Navigation("Classes"); + + b.Navigation("Currency") + .IsRequired(); + + b.Navigation("GachaPointBalances"); + + b.Navigation("GachaPointReceived"); + + b.Navigation("Info") + .IsRequired(); + + b.Navigation("Items"); + + b.Navigation("MissionData") + .IsRequired(); + + b.Navigation("PackOpenCounts"); + + b.Navigation("SocialAccountConnections"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerAchievement", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Achievements") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerClaimedTutorialGift", b => + { + b.HasOne("SVSim.Database.Models.Viewer", "Viewer") + .WithMany() + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Viewer"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerEventCounter", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("EventCounters") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.ViewerMission", b => + { + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany("Missions") + .HasForeignKey("ViewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SleeveEntryViewer", b => + { + b.HasOne("SVSim.Database.Models.SleeveEntry", null) + .WithMany() + .HasForeignKey("SleevesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SVSim.Database.Models.Viewer", null) + .WithMany() + .HasForeignKey("ViewersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("SVSim.Database.Models.BattlePassSeasonEntry", b => + { + b.Navigation("Rewards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.BuildDeckSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ClassEntry", b => + { + b.Navigation("LeaderSkins"); + }); + + modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.PuzzleGroupEntry", b => + { + b.Navigation("Puzzles"); + }); + + modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardSetEntry", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("SVSim.Database.Models.SleeveShopSeriesEntry", b => + { + b.Navigation("Products"); + }); + + modelBuilder.Entity("SVSim.Database.Models.Viewer", b => + { + b.Navigation("Achievements"); + + b.Navigation("Decks"); + + b.Navigation("EventCounters"); + + b.Navigation("Missions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SVSim.Database/Migrations/20260529142631_AddStoryDeck.cs b/SVSim.Database/Migrations/20260529142631_AddStoryDeck.cs new file mode 100644 index 0000000..21548ab --- /dev/null +++ b/SVSim.Database/Migrations/20260529142631_AddStoryDeck.cs @@ -0,0 +1,45 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SVSim.Database.Migrations +{ + /// + public partial class AddStoryDeck : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "StoryDecks", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false), + DeckNo = table.Column(type: "integer", nullable: false), + Kind = table.Column(type: "integer", nullable: false), + ClassId = table.Column(type: "integer", nullable: false), + DeckName = table.Column(type: "text", nullable: false), + SleeveId = table.Column(type: "integer", nullable: false), + LeaderSkinId = table.Column(type: "integer", nullable: false), + IsRecommend = table.Column(type: "integer", nullable: false), + OrderNum = table.Column(type: "integer", nullable: false), + EntryNo = table.Column(type: "integer", nullable: false), + DeckFormat = table.Column(type: "integer", nullable: true), + DateCreated = table.Column(type: "timestamp with time zone", nullable: false), + DateUpdated = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_StoryDecks", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StoryDecks"); + } + } +} diff --git a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs index efac356..c91d179 100644 --- a/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs +++ b/SVSim.Database/Migrations/SVSimDbContextModelSnapshot.cs @@ -2240,6 +2240,53 @@ namespace SVSim.Database.Migrations b.ToTable("SpotCardExchangeCatalog"); }); + modelBuilder.Entity("SVSim.Database.Models.StoryDeckEntry", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ClassId") + .HasColumnType("integer"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("DeckFormat") + .HasColumnType("integer"); + + b.Property("DeckName") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeckNo") + .HasColumnType("integer"); + + b.Property("EntryNo") + .HasColumnType("integer"); + + b.Property("IsRecommend") + .HasColumnType("integer"); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("LeaderSkinId") + .HasColumnType("integer"); + + b.Property("OrderNum") + .HasColumnType("integer"); + + b.Property("SleeveId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("StoryDecks"); + }); + modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b => { b.Property("Id") From e792e8d79de0815aeced91787ab098fb8e3d537c Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:32:12 -0400 Subject: [PATCH 04/10] feat(bootstrap): StoryDeckImporter + seed model, wired after BuildDeck Co-Authored-By: Claude Sonnet 4.6 --- .../Importers/StoryDeckImporter.cs | 52 +++++++++++++++++++ SVSim.Bootstrap/Models/Seed/StoryDeckSeed.cs | 17 ++++++ SVSim.Bootstrap/Program.cs | 1 + .../Importers/StoryDeckImporterTests.cs | 49 +++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 SVSim.Bootstrap/Importers/StoryDeckImporter.cs create mode 100644 SVSim.Bootstrap/Models/Seed/StoryDeckSeed.cs create mode 100644 SVSim.UnitTests/Importers/StoryDeckImporterTests.cs diff --git a/SVSim.Bootstrap/Importers/StoryDeckImporter.cs b/SVSim.Bootstrap/Importers/StoryDeckImporter.cs new file mode 100644 index 0000000..42b0025 --- /dev/null +++ b/SVSim.Bootstrap/Importers/StoryDeckImporter.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore; +using SVSim.Bootstrap.Models.Seed; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Models; + +namespace SVSim.Bootstrap.Importers; + +/// +/// Idempotent upsert of story-deck presentation rows from seeds/story-decks.json. +/// Card lists are NOT imported here — they belong to BuildDeckProductEntry (deck_no == product_id), +/// so this importer should run AFTER BuildDeckImporter.ImportPackageAsync. Rows missing from the +/// seed are left intact. +/// +public class StoryDeckImporter +{ + public async Task ImportAsync(SVSimDbContext context, string seedDir) + { + var seed = SeedLoader.LoadList(Path.Combine(seedDir, "story-decks.json")); + if (seed.Count == 0) + { + Console.WriteLine("[StoryDeckImporter] No seed rows; skipping."); + return 0; + } + + var existing = await context.StoryDecks.ToDictionaryAsync(e => e.Id); + int created = 0, updated = 0; + + foreach (var s in seed) + { + if (s.DeckNo == 0) continue; + var entry = existing.TryGetValue(s.DeckNo, out var ex) ? ex : new StoryDeckEntry { DeckNo = s.DeckNo }; + entry.Kind = string.Equals(s.Kind, "trial", StringComparison.OrdinalIgnoreCase) + ? StoryDeckKind.Trial : StoryDeckKind.Build; + entry.ClassId = s.ClassId; + entry.DeckName = s.DeckName; + entry.SleeveId = s.SleeveId; + entry.LeaderSkinId = s.LeaderSkinId; + entry.IsRecommend = s.IsRecommend; + entry.OrderNum = s.OrderNum; + entry.EntryNo = s.EntryNo; + entry.DeckFormat = s.DeckFormat; + + if (ex is null) { context.StoryDecks.Add(entry); existing[s.DeckNo] = entry; created++; } + else updated++; + } + + await context.SaveChangesAsync(); + Console.WriteLine($"[StoryDeckImporter] +{created}/~{updated}"); + return created + updated; + } +} diff --git a/SVSim.Bootstrap/Models/Seed/StoryDeckSeed.cs b/SVSim.Bootstrap/Models/Seed/StoryDeckSeed.cs new file mode 100644 index 0000000..9afe87a --- /dev/null +++ b/SVSim.Bootstrap/Models/Seed/StoryDeckSeed.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace SVSim.Bootstrap.Models.Seed; + +public sealed class StoryDeckSeed +{ + [JsonPropertyName("deck_no")] public int DeckNo { get; set; } + [JsonPropertyName("kind")] public string Kind { get; set; } = "build"; + [JsonPropertyName("class_id")] public int ClassId { get; set; } + [JsonPropertyName("deck_name")] public string DeckName { get; set; } = ""; + [JsonPropertyName("sleeve_id")] public int SleeveId { get; set; } + [JsonPropertyName("leader_skin_id")] public int LeaderSkinId { get; set; } + [JsonPropertyName("is_recommend")] public int IsRecommend { get; set; } + [JsonPropertyName("order_num")] public int OrderNum { get; set; } + [JsonPropertyName("entry_no")] public int EntryNo { get; set; } + [JsonPropertyName("deck_format")] public int? DeckFormat { get; set; } +} diff --git a/SVSim.Bootstrap/Program.cs b/SVSim.Bootstrap/Program.cs index 92e501a..b10ecb2 100644 --- a/SVSim.Bootstrap/Program.cs +++ b/SVSim.Bootstrap/Program.cs @@ -124,6 +124,7 @@ public static class Program await buildDeck.ImportSeriesAsync(context, opts.ReferenceDataDir); await buildDeck.ImportCatalogAsync(context, opts.SeedDir); await buildDeck.ImportPackageAsync(context, opts.ReferenceDataDir); + await new StoryDeckImporter().ImportAsync(context, opts.SeedDir); } else { diff --git a/SVSim.UnitTests/Importers/StoryDeckImporterTests.cs b/SVSim.UnitTests/Importers/StoryDeckImporterTests.cs new file mode 100644 index 0000000..d996686 --- /dev/null +++ b/SVSim.UnitTests/Importers/StoryDeckImporterTests.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Bootstrap.Importers; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Importers; + +public class StoryDeckImporterTests +{ + private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds"); + + [Test] + public async Task Imports_story_decks_from_seed_file() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await new StoryDeckImporter().ImportAsync(db, SeedDir); + + var decks = await db.StoryDecks.OrderBy(d => d.Id).ToListAsync(); + Assert.That(decks.Count, Is.EqualTo(112), "53 build + 59 trial"); + Assert.That(decks.Count(d => d.Kind == StoryDeckKind.Build), Is.EqualTo(53)); + Assert.That(decks.Count(d => d.Kind == StoryDeckKind.Trial), Is.EqualTo(59)); + var pureDevotion = decks.Single(d => d.DeckNo == 701); + Assert.That(pureDevotion.Kind, Is.EqualTo(StoryDeckKind.Build)); + Assert.That(pureDevotion.ClassId, Is.EqualTo(1)); + Assert.That(pureDevotion.DeckName, Is.EqualTo("Pure Devotion")); + Assert.That(pureDevotion.DeckFormat, Is.Null); + Assert.That(decks.Where(d => d.Kind == StoryDeckKind.Trial).All(d => d.DeckFormat != null), Is.True); + } + + [Test] + public async Task Is_idempotent_on_rerun() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + await new StoryDeckImporter().ImportAsync(db, SeedDir); + int before = await db.StoryDecks.CountAsync(); + await new StoryDeckImporter().ImportAsync(db, SeedDir); + int after = await db.StoryDecks.CountAsync(); + + Assert.That(after, Is.EqualTo(before)); + } +} From 68d783192d591855c93907541ffb7f3e146e64fd Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:36:14 -0400 Subject: [PATCH 05/10] feat(repo): GetStoryDecksByClass joins story-deck presentation to product card lists Adds StoryDeckView projection, IBuildDeckRepository.GetStoryDecksByClass interface method, and BuildDeckRepository implementation that loads StoryDeckEntry rows for a class, fetches matching BuildDeckProductEntry card lists, and expands each card by Number into a flat CardIdArray. TDD: 2 tests in StoryDeckRepositoryTests (expand + empty-class). Co-Authored-By: Claude Sonnet 4.6 --- .../BuildDeck/BuildDeckRepository.cs | 34 ++++++++++ .../BuildDeck/IBuildDeckRepository.cs | 7 ++ .../Repositories/BuildDeck/StoryDeckView.cs | 22 +++++++ .../Repositories/StoryDeckRepositoryTests.cs | 66 +++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs create mode 100644 SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.cs diff --git a/SVSim.Database/Repositories/BuildDeck/BuildDeckRepository.cs b/SVSim.Database/Repositories/BuildDeck/BuildDeckRepository.cs index 108f12f..e9abb21 100644 --- a/SVSim.Database/Repositories/BuildDeck/BuildDeckRepository.cs +++ b/SVSim.Database/Repositories/BuildDeck/BuildDeckRepository.cs @@ -64,4 +64,38 @@ public class BuildDeckRepository : IBuildDeckRepository await _db.SaveChangesAsync(); return row.PurchaseCount; } + + public async Task> GetStoryDecksByClass(int classId) + { + var decks = await _db.StoryDecks.Where(d => d.ClassId == classId).ToListAsync(); + if (decks.Count == 0) return new(); + + var ids = decks.Select(d => d.DeckNo).ToList(); + var products = await _db.BuildDeckProducts + .Where(p => ids.Contains(p.Id)) + .Include(p => p.Cards) + .AsSplitQuery() + .ToListAsync(); + + // Expand each product's owned card rows by Number into a flat card_id list (spots included — + // validated against the prod capture, 112/112 match). + var cardsById = products.ToDictionary( + p => p.Id, + p => p.Cards.SelectMany(c => Enumerable.Repeat(c.CardId, c.Number)).ToList()); + + return decks.Select(d => new StoryDeckView + { + DeckNo = d.DeckNo, + Kind = d.Kind, + ClassId = d.ClassId, + DeckName = d.DeckName, + SleeveId = d.SleeveId, + LeaderSkinId = d.LeaderSkinId, + IsRecommend = d.IsRecommend, + OrderNum = d.OrderNum, + EntryNo = d.EntryNo, + DeckFormat = d.DeckFormat, + CardIdArray = cardsById.TryGetValue(d.DeckNo, out var cards) ? cards : new(), + }).ToList(); + } } diff --git a/SVSim.Database/Repositories/BuildDeck/IBuildDeckRepository.cs b/SVSim.Database/Repositories/BuildDeck/IBuildDeckRepository.cs index 06828a3..26e3c32 100644 --- a/SVSim.Database/Repositories/BuildDeck/IBuildDeckRepository.cs +++ b/SVSim.Database/Repositories/BuildDeck/IBuildDeckRepository.cs @@ -26,4 +26,11 @@ public interface IBuildDeckRepository /// Returns the new total. /// Task IncrementPurchaseCount(long viewerId, int productId); + + /// + /// Story deck-select decks for a class: StoryDeckEntry presentation rows joined to the matching + /// BuildDeckProductEntry card lists (deck_no == product_id), expanded to a flat card_id array. + /// Returns build and trial decks together; the caller splits by Kind. + /// + Task> GetStoryDecksByClass(int classId); } diff --git a/SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs b/SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs new file mode 100644 index 0000000..9a7c8d4 --- /dev/null +++ b/SVSim.Database/Repositories/BuildDeck/StoryDeckView.cs @@ -0,0 +1,22 @@ +using SVSim.Database.Enums; + +namespace SVSim.Database.Repositories.BuildDeck; + +/// +/// A story-select deck ready for the wire: presentation metadata from StoryDeckEntry plus the +/// 40-card list expanded from the matching BuildDeckProductEntry. Plain projection, not an entity. +/// +public sealed class StoryDeckView +{ + public int DeckNo { get; init; } + public StoryDeckKind Kind { get; init; } + public int ClassId { get; init; } + public string DeckName { get; init; } = string.Empty; + public int SleeveId { get; init; } + public int LeaderSkinId { get; init; } + public int IsRecommend { get; init; } + public int OrderNum { get; init; } + public int EntryNo { get; init; } + public int? DeckFormat { get; init; } + public List CardIdArray { get; init; } = new(); +} diff --git a/SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.cs b/SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.cs new file mode 100644 index 0000000..28f866f --- /dev/null +++ b/SVSim.UnitTests/Repositories/StoryDeckRepositoryTests.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SVSim.Database; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.Database.Repositories.BuildDeck; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Repositories; + +public class StoryDeckRepositoryTests +{ + [Test] + public async Task GetStoryDecksByClass_returns_decks_with_expanded_card_arrays() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // FK: BuildDeckProducts requires a parent BuildDeckSeries row. + db.BuildDeckSeries.Add(new BuildDeckSeriesEntry { Id = 0 }); + await db.SaveChangesAsync(); + + // Product 701 (class 1 build): 2x card 100, 1x card 200 = 3-card "deck". + db.BuildDeckProducts.Add(new BuildDeckProductEntry + { + Id = 701, SeriesId = 0, LeaderId = 1, DeckCode = "", ProductNameKey = "", IsEnabled = false, + Cards = new() + { + new BuildDeckProductCardEntry { CardId = 100, Number = 2, IsSpot = false }, + new BuildDeckProductCardEntry { CardId = 200, Number = 1, IsSpot = false }, + }, + }); + db.StoryDecks.Add(new StoryDeckEntry + { + DeckNo = 701, Kind = StoryDeckKind.Build, ClassId = 1, DeckName = "Pure Devotion", + SleeveId = 3000011, LeaderSkinId = 1, IsRecommend = 0, OrderNum = 0, EntryNo = 0, DeckFormat = null, + }); + // A class-2 deck that must NOT be returned for class 1. + db.StoryDecks.Add(new StoryDeckEntry { DeckNo = 702, Kind = StoryDeckKind.Build, ClassId = 2, DeckName = "Other" }); + await db.SaveChangesAsync(); + + var repo = new BuildDeckRepository(db); + var result = await repo.GetStoryDecksByClass(1); + + Assert.That(result.Count, Is.EqualTo(1)); + var deck = result[0]; + Assert.That(deck.DeckNo, Is.EqualTo(701)); + Assert.That(deck.DeckName, Is.EqualTo("Pure Devotion")); + Assert.That(deck.Kind, Is.EqualTo(StoryDeckKind.Build)); + Assert.That(deck.CardIdArray.OrderBy(x => x), Is.EqualTo(new long[] { 100, 100, 200 })); + } + + [Test] + public async Task GetStoryDecksByClass_returns_empty_for_class_with_no_decks() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + var repo = new BuildDeckRepository(db); + var result = await repo.GetStoryDecksByClass(8); + + Assert.That(result, Is.Empty); + } +} From 6a507553d1d0a49c123b0ddb22ec2b6fddf0da02 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:38:39 -0400 Subject: [PATCH 06/10] feat(dto): TrialDeck + fleshed BuildDeck + trial/default on GetDeckListResponse --- .../Models/Dtos/Common/TrialDeck.cs | 27 +++++++++++++++++ .../Models/Dtos/Story/GetDeckListDtos.cs | 30 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 SVSim.EmulatedEntrypoint/Models/Dtos/Common/TrialDeck.cs diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Common/TrialDeck.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/TrialDeck.cs new file mode 100644 index 0000000..9e8ddcb --- /dev/null +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Common/TrialDeck.cs @@ -0,0 +1,27 @@ +using MessagePack; +using System.Text.Json.Serialization; + +namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common; + +/// +/// One archetype trial deck under trial_deck_list (DeckAttributeType.TrialDeck). Wire shape +/// from the 2026-05-29 main_story/get_deck_list capture. Distinct from build decks: carries +/// deck_format and no order_num/leader-skin-list. card_id_array only (no numbered card_id_N). +/// +[MessagePackObject] +public class TrialDeck +{ + [JsonPropertyName("deck_no")] [Key("deck_no")] public int DeckNo { get; set; } + [JsonPropertyName("class_id")] [Key("class_id")] public int ClassId { get; set; } + [JsonPropertyName("sleeve_id")] [Key("sleeve_id")] public int SleeveId { get; set; } + [JsonPropertyName("leader_skin_id")] [Key("leader_skin_id")] public int LeaderSkinId { get; set; } + [JsonPropertyName("deck_name")] [Key("deck_name")] public string DeckName { get; set; } = string.Empty; + [JsonPropertyName("card_id_array")] [Key("card_id_array")] public List CardIdArray { get; set; } = new(); + [JsonPropertyName("is_complete_deck")] [Key("is_complete_deck")] public int IsCompleteDeck { get; set; } = 1; + [JsonPropertyName("restricted_card_exists")] [Key("restricted_card_exists")] public bool RestrictedCardExists { get; set; } + [JsonPropertyName("is_available_deck")] [Key("is_available_deck")] public int IsAvailableDeck { get; set; } = 1; + [JsonPropertyName("maintenance_card_ids")] [Key("maintenance_card_ids")] public List MaintenanceCardIds { get; set; } = new(); + [JsonPropertyName("is_include_un_possession_card")] [Key("is_include_un_possession_card")] public bool IsIncludeUnPossessionCard { get; set; } + [JsonPropertyName("deck_format")] [Key("deck_format")] public int DeckFormat { get; set; } + [JsonPropertyName("is_recommend")] [Key("is_recommend")] public int IsRecommend { get; set; } +} diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Story/GetDeckListDtos.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Story/GetDeckListDtos.cs index 37a4b20..1638351 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/Story/GetDeckListDtos.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Story/GetDeckListDtos.cs @@ -1,5 +1,7 @@ using MessagePack; using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; using SVSim.EmulatedEntrypoint.Models.Dtos.Requests; namespace SVSim.EmulatedEntrypoint.Models.Dtos.Story; @@ -30,10 +32,36 @@ public class GetDeckListResponse [JsonPropertyName("build_deck_list")] [Key("build_deck_list")] public List BuildDeckList { get; set; } = new(); + + [JsonPropertyName("trial_deck_list")] + [Key("trial_deck_list")] + public List TrialDeckList { get; set; } = new(); + + /// Global starter decks, keyed by deck_no string (prod ids 91-98, one per class). + [JsonPropertyName("default_deck_list")] + [Key("default_deck_list")] + public Dictionary DefaultDeckList { get; set; } = new(); } +/// +/// One named prebuilt story deck under build_deck_list (DeckAttributeType.BuildDeck). Wire +/// shape from the 2026-05-29 capture. Emits card_id_array only — the numbered card_id_1..40 keys +/// prod also sends are omitted (default/trial entries omit them and parse fine). +/// [MessagePackObject] public class BuildDeck { - // Placeholder — build decks return [] for v1 per spec. + [JsonPropertyName("deck_no")] [Key("deck_no")] public int DeckNo { get; set; } + [JsonPropertyName("order_num")] [Key("order_num")] public int OrderNum { get; set; } + [JsonPropertyName("class_id")] [Key("class_id")] public int ClassId { get; set; } + [JsonPropertyName("sleeve_id")] [Key("sleeve_id")] public int SleeveId { get; set; } + [JsonPropertyName("leader_skin_id")] [Key("leader_skin_id")] public int LeaderSkinId { get; set; } + [JsonPropertyName("entry_no")] [Key("entry_no")] public int EntryNo { get; set; } + [JsonPropertyName("create_deck_time")] [Key("create_deck_time")] public DateTime? CreateDeckTime { get; set; } + [JsonPropertyName("deck_name")] [Key("deck_name")] public string DeckName { get; set; } = string.Empty; + [JsonPropertyName("card_id_array")] [Key("card_id_array")] public List CardIdArray { get; set; } = new(); + [JsonPropertyName("is_complete_deck")] [Key("is_complete_deck")] public int IsCompleteDeck { get; set; } = 1; + [JsonPropertyName("is_available_deck")] [Key("is_available_deck")] public int IsAvailableDeck { get; set; } = 1; + [JsonPropertyName("maintenance_card_ids")] [Key("maintenance_card_ids")] public List MaintenanceCardIds { get; set; } = new(); + [JsonPropertyName("is_recommend")] [Key("is_recommend")] public int IsRecommend { get; set; } } From 66dc0cc657295311bc3668912f09e27b53157724 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:47:20 -0400 Subject: [PATCH 07/10] feat(story): populate build/trial/default deck lists on get_deck_list Wire IBuildDeckRepository into StoryService; GetDeckListAsync now looks up the chapter's CharaId, fetches class-specific prebuilt/trial decks via GetStoryDecksByClass, and loads all DefaultDecks for default_deck_list. Class guard (1-8) leaves build/trial empty for non-class chapters, matching prod behaviour. Co-Authored-By: Claude Sonnet 4.6 --- .../Services/StoryService.cs | 84 ++++++++++++++++++- .../Story/StoryDeckListServiceTests.cs | 74 ++++++++++++++++ SVSim.UnitTests/Story/StoryServiceTests.cs | 3 + 3 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 SVSim.UnitTests/Story/StoryDeckListServiceTests.cs diff --git a/SVSim.EmulatedEntrypoint/Services/StoryService.cs b/SVSim.EmulatedEntrypoint/Services/StoryService.cs index e2c2059..fefad98 100644 --- a/SVSim.EmulatedEntrypoint/Services/StoryService.cs +++ b/SVSim.EmulatedEntrypoint/Services/StoryService.cs @@ -1,12 +1,16 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using System.Text.Json; using SVSim.Database; using SVSim.Database.Entities.Story; using SVSim.Database.Enums; using SVSim.Database.Models.Config; using SVSim.Database.Repositories.Deck; +using SVSim.Database.Repositories.BuildDeck; using SVSim.Database.Services; using SVSim.Database.Repositories.Story; +using SVSim.EmulatedEntrypoint.Models.Dtos; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; using SVSim.EmulatedEntrypoint.Models.Dtos.Story; namespace SVSim.EmulatedEntrypoint.Services; @@ -19,6 +23,7 @@ public class StoryService : IStoryService private readonly SVSimDbContext _db; private readonly IGameConfigService _configService; private readonly IDeckRepository _deckRepository; + private readonly IBuildDeckRepository _buildDecks; private readonly ILogger _logger; public StoryService( @@ -28,6 +33,7 @@ public class StoryService : IStoryService SVSimDbContext db, IGameConfigService configService, IDeckRepository deckRepository, + IBuildDeckRepository buildDecks, ILogger logger) { _master = master; @@ -36,6 +42,7 @@ public class StoryService : IStoryService _db = db; _configService = configService; _deckRepository = deckRepository; + _buildDecks = buildDecks; _logger = logger; } @@ -344,16 +351,85 @@ public class StoryService : IStoryService { var byFormat = await _deckRepository.GetDecksByFormats( viewerId, new[] { SVSim.Database.Enums.Format.Rotation, SVSim.Database.Enums.Format.Unlimited }); - return new GetDeckListResponse + + var resp = new GetDeckListResponse { UserDeckRotation = byFormat[SVSim.Database.Enums.Format.Rotation] - .Select(d => new SVSim.EmulatedEntrypoint.Models.Dtos.UserDeck(d)).ToList(), + .Select(d => new UserDeck(d)).ToList(), UserDeckUnlimited = byFormat[SVSim.Database.Enums.Format.Unlimited] - .Select(d => new SVSim.EmulatedEntrypoint.Models.Dtos.UserDeck(d)).ToList(), - BuildDeckList = new List(), // v1: empty + .Select(d => new UserDeck(d)).ToList(), MaintenanceCardList = new List(), }; + + // The chapter's leader (CharaId == class_id 1-8 for standard classes) drives which + // prebuilt/trial decks the story deck-select shows. Non-class chapters (custom leaders, + // chara_id outside 1-8) get empty build/trial lists, matching prod. + var chapter = await _master.GetChapterByIdAsync(storyId); + int classId = chapter?.CharaId ?? 0; + if (classId is >= 1 and <= 8) + { + var storyDecks = await _buildDecks.GetStoryDecksByClass(classId); + resp.BuildDeckList = storyDecks + .Where(d => d.Kind == StoryDeckKind.Build) + .Select(ToBuildDeck).ToList(); + resp.TrialDeckList = storyDecks + .Where(d => d.Kind == StoryDeckKind.Trial) + .Select(ToTrialDeck).ToList(); + } + + // default_deck_list — all 8 starter decks, keyed by deck_no string (same shape as /deck/info). + var defaults = await _db.DefaultDecks.OrderBy(d => d.Id).ToListAsync(); + resp.DefaultDeckList = defaults.ToDictionary( + d => d.Id.ToString(), + d => new DefaultDeck + { + DeckNo = d.DeckNo, + ClassId = d.ClassId, + SleeveId = d.SleeveId, + LeaderSkinId = d.LeaderSkinId, + DeckName = d.DeckName, + CardIdArray = JsonSerializer.Deserialize>(d.CardIdArray) ?? new(), + IsCompleteDeck = 1, + IsAvailableDeck = 1, + MaintenanceCardIds = new(), + }); + + return resp; } + + private static BuildDeck ToBuildDeck(StoryDeckView d) => new() + { + DeckNo = d.DeckNo, + OrderNum = d.OrderNum, + ClassId = d.ClassId, + SleeveId = d.SleeveId, + LeaderSkinId = d.LeaderSkinId, + EntryNo = d.EntryNo, + CreateDeckTime = null, + DeckName = d.DeckName, + CardIdArray = d.CardIdArray, + IsCompleteDeck = 1, + IsAvailableDeck = 1, + MaintenanceCardIds = new(), + IsRecommend = d.IsRecommend, + }; + + private static TrialDeck ToTrialDeck(StoryDeckView d) => new() + { + DeckNo = d.DeckNo, + ClassId = d.ClassId, + SleeveId = d.SleeveId, + LeaderSkinId = d.LeaderSkinId, + DeckName = d.DeckName, + CardIdArray = d.CardIdArray, + IsCompleteDeck = 1, + RestrictedCardExists = false, + IsAvailableDeck = 1, + MaintenanceCardIds = new(), + IsIncludeUnPossessionCard = false, + DeckFormat = d.DeckFormat ?? 0, + IsRecommend = d.IsRecommend, + }; public async Task StartAsync(StoryApiType apiType, int[] storyIds, long viewerId) { var resp = new StartResponse(); diff --git a/SVSim.UnitTests/Story/StoryDeckListServiceTests.cs b/SVSim.UnitTests/Story/StoryDeckListServiceTests.cs new file mode 100644 index 0000000..09482b0 --- /dev/null +++ b/SVSim.UnitTests/Story/StoryDeckListServiceTests.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using SVSim.Database; +using SVSim.Database.Entities.Story; +using SVSim.Database.Enums; +using SVSim.Database.Models; +using SVSim.EmulatedEntrypoint.Services; +using SVSim.UnitTests.Infrastructure; + +namespace SVSim.UnitTests.Story; + +[TestFixture] +public class StoryDeckListServiceTests +{ + [Test] + public async Task GetDeckList_populates_build_trial_and_default_for_chapter_class() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // StoryChapter.SectionId has an enforced FK to StorySection; seed the parent row first. + db.StorySections.Add(new StorySection { Id = 1, StoryApiType = StoryApiType.Main }); + // Chapter 14 is a class-1 (Forestcraft) chapter. + db.StoryChapters.Add(new StoryChapter { StoryId = 14, SectionId = 1, CharaId = 1, ChapterId = "14" }); + + // One class-1 build deck (701) + one class-1 trial deck (13001), each with a 1-card product. + // BuildDeckProductEntry has an enforced FK SeriesId -> BuildDeckSeries; seed the parent first. + db.BuildDeckSeries.Add(new BuildDeckSeriesEntry { Id = 0 }); + db.BuildDeckProducts.Add(new BuildDeckProductEntry { Id = 701, Cards = new() { new BuildDeckProductCardEntry { CardId = 100, Number = 1 } } }); + db.BuildDeckProducts.Add(new BuildDeckProductEntry { Id = 13001, Cards = new() { new BuildDeckProductCardEntry { CardId = 200, Number = 1 } } }); + db.StoryDecks.Add(new StoryDeckEntry { DeckNo = 701, Kind = StoryDeckKind.Build, ClassId = 1, DeckName = "Pure Devotion", DeckFormat = null }); + db.StoryDecks.Add(new StoryDeckEntry { DeckNo = 13001, Kind = StoryDeckKind.Trial, ClassId = 1, DeckName = "Tempo Forestcraft", DeckFormat = 1 }); + + db.DefaultDecks.Add(new DefaultDeckEntry { Id = 91, ClassId = 1, SleeveId = 3000011, LeaderSkinId = 0, DeckName = "Default", CardIdArray = "[100,100,100]" }); + await db.SaveChangesAsync(); + + var service = scope.ServiceProvider.GetRequiredService(); + var resp = await service.GetDeckListAsync(StoryApiType.Main, storyId: 14, viewerId: 1); + + Assert.That(resp.BuildDeckList.Count, Is.EqualTo(1)); + Assert.That(resp.BuildDeckList[0].DeckNo, Is.EqualTo(701)); + Assert.That(resp.BuildDeckList[0].DeckName, Is.EqualTo("Pure Devotion")); + Assert.That(resp.BuildDeckList[0].CardIdArray, Is.EqualTo(new long[] { 100 })); + + Assert.That(resp.TrialDeckList.Count, Is.EqualTo(1)); + Assert.That(resp.TrialDeckList[0].DeckNo, Is.EqualTo(13001)); + Assert.That(resp.TrialDeckList[0].DeckFormat, Is.EqualTo(1)); + + Assert.That(resp.DefaultDeckList.ContainsKey("91"), Is.True); + Assert.That(resp.DefaultDeckList["91"].CardIdArray, Is.EqualTo(new long[] { 100, 100, 100 })); + } + + [Test] + public async Task GetDeckList_returns_empty_build_trial_for_non_class_chapter() + { + using var factory = new SVSimTestFactory(); + using var scope = factory.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // StoryChapter.SectionId has an enforced FK to StorySection; seed the parent row first. + db.StorySections.Add(new StorySection { Id = 17, StoryApiType = StoryApiType.Main }); + // chara_id 0 -> custom-leader / non-class chapter. + db.StoryChapters.Add(new StoryChapter { StoryId = 500, SectionId = 17, CharaId = 0, ChapterId = "500" }); + await db.SaveChangesAsync(); + + var service = scope.ServiceProvider.GetRequiredService(); + var resp = await service.GetDeckListAsync(StoryApiType.Main, storyId: 500, viewerId: 1); + + Assert.That(resp.BuildDeckList, Is.Empty); + Assert.That(resp.TrialDeckList, Is.Empty); + } +} diff --git a/SVSim.UnitTests/Story/StoryServiceTests.cs b/SVSim.UnitTests/Story/StoryServiceTests.cs index 4bdefc9..1c80aa5 100644 --- a/SVSim.UnitTests/Story/StoryServiceTests.cs +++ b/SVSim.UnitTests/Story/StoryServiceTests.cs @@ -35,6 +35,7 @@ public class StoryServiceTests db: db, configService: StoryServiceTestHelpers.NewConfigService(), deckRepository: new Mock().Object, + buildDecks: new Mock().Object, logger: NullLogger.Instance); } @@ -72,6 +73,7 @@ public class StoryServiceTests db: db, configService: StoryServiceTestHelpers.NewConfigService(), deckRepository: new Mock().Object, + buildDecks: new Mock().Object, logger: NullLogger.Instance); } @@ -404,6 +406,7 @@ public class StoryServiceTests db: db, configService: StoryServiceTestHelpers.NewConfigService(), deckRepository: new Mock().Object, + buildDecks: new Mock().Object, logger: NullLogger.Instance); } From 363213ccf7f7398c3f754dc5832af9968e09a048 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:52:06 -0400 Subject: [PATCH 08/10] test(story): literal-JSON wire-shape guard for get_deck_list deck lists Co-Authored-By: Claude Sonnet 4.6 --- .../Story/GetDeckListWireShapeTests.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 SVSim.UnitTests/Story/GetDeckListWireShapeTests.cs diff --git a/SVSim.UnitTests/Story/GetDeckListWireShapeTests.cs b/SVSim.UnitTests/Story/GetDeckListWireShapeTests.cs new file mode 100644 index 0000000..4ab1e07 --- /dev/null +++ b/SVSim.UnitTests/Story/GetDeckListWireShapeTests.cs @@ -0,0 +1,85 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; +using SVSim.EmulatedEntrypoint.Models.Dtos.Story; + +namespace SVSim.UnitTests.Story; + +public class GetDeckListWireShapeTests +{ + // Mirror Program.cs: keys come from [JsonPropertyName]; null values are dropped. + private static readonly JsonSerializerOptions WireOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + [Test] + public void BuildDeck_serializes_with_prod_wire_keys() + { + var deck = new BuildDeck + { + DeckNo = 701, OrderNum = 0, ClassId = 1, SleeveId = 3000011, LeaderSkinId = 1, + EntryNo = 0, CreateDeckTime = null, DeckName = "Pure Devotion", + CardIdArray = new() { 115141020, 114141020 }, + IsCompleteDeck = 1, IsAvailableDeck = 1, MaintenanceCardIds = new(), IsRecommend = 0, + }; + + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(deck, WireOptions)); + var root = doc.RootElement; + + // Every key the client's BuildDeck branch reads must be present with the right name. + foreach (var key in new[] { "deck_no", "order_num", "class_id", "sleeve_id", "leader_skin_id", + "entry_no", "deck_name", "card_id_array", "is_complete_deck", + "is_available_deck", "maintenance_card_ids", "is_recommend" }) + { + Assert.That(root.TryGetProperty(key, out _), Is.True, $"missing wire key: {key}"); + } + Assert.That(root.GetProperty("deck_name").GetString(), Is.EqualTo("Pure Devotion")); + Assert.That(root.GetProperty("card_id_array").GetArrayLength(), Is.EqualTo(2)); + // Numbered card_id_N keys are intentionally omitted. + Assert.That(root.TryGetProperty("card_id_1", out _), Is.False); + } + + [Test] + public void TrialDeck_serializes_with_prod_wire_keys_including_deck_format() + { + var deck = new TrialDeck + { + DeckNo = 13001, ClassId = 1, SleeveId = 3000011, LeaderSkinId = 0, + DeckName = "Tempo Forestcraft", CardIdArray = new() { 130141020 }, + IsCompleteDeck = 1, RestrictedCardExists = false, IsAvailableDeck = 1, + MaintenanceCardIds = new(), IsIncludeUnPossessionCard = false, DeckFormat = 1, IsRecommend = 1, + }; + + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(deck, WireOptions)); + var root = doc.RootElement; + + foreach (var key in new[] { "deck_no", "class_id", "sleeve_id", "leader_skin_id", "deck_name", + "card_id_array", "is_complete_deck", "restricted_card_exists", + "is_available_deck", "maintenance_card_ids", + "is_include_un_possession_card", "deck_format", "is_recommend" }) + { + Assert.That(root.TryGetProperty(key, out _), Is.True, $"missing wire key: {key}"); + } + Assert.That(root.GetProperty("deck_format").GetInt32(), Is.EqualTo(1)); + } + + [Test] + public void GetDeckListResponse_default_deck_list_is_a_keyed_object() + { + var resp = new GetDeckListResponse(); + resp.DefaultDeckList["91"] = new SVSim.EmulatedEntrypoint.Models.Dtos.DefaultDeck + { + DeckNo = 91, ClassId = 1, SleeveId = 3000011, LeaderSkinId = 0, DeckName = "Default", + CardIdArray = new() { 100111010 }, + }; + + using var doc = JsonDocument.Parse(JsonSerializer.Serialize(resp, WireOptions)); + var root = doc.RootElement; + + Assert.That(root.GetProperty("default_deck_list").ValueKind, Is.EqualTo(JsonValueKind.Object)); + Assert.That(root.GetProperty("default_deck_list").GetProperty("91").GetProperty("class_id").GetInt32(), Is.EqualTo(1)); + Assert.That(root.GetProperty("build_deck_list").ValueKind, Is.EqualTo(JsonValueKind.Array)); + Assert.That(root.GetProperty("trial_deck_list").ValueKind, Is.EqualTo(JsonValueKind.Array)); + } +} From bd2eaa9e97e3d143d4f1d2d74c367f4166b56b5a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 10:54:53 -0400 Subject: [PATCH 09/10] refactor(deck): re-type /deck/info trial_deck_list to List? Co-Authored-By: Claude Sonnet 4.6 --- .../Models/Dtos/Responses/Deck/DeckListResponse.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Deck/DeckListResponse.cs b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Deck/DeckListResponse.cs index 2e77a33..a7f1b2c 100644 --- a/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Deck/DeckListResponse.cs +++ b/SVSim.EmulatedEntrypoint/Models/Dtos/Responses/Deck/DeckListResponse.cs @@ -1,4 +1,5 @@ using MessagePack; +using SVSim.EmulatedEntrypoint.Models.Dtos.Common; using System.Text.Json.Serialization; namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Deck; @@ -53,11 +54,11 @@ public class DeckListResponse [Key("user_leader_skin_setting_list")] public Dictionary UserLeaderSkinSettingList { get; set; } = new(); /// - /// Trial / tutorial-specific decks. Prod emits this on /deck/info (All format) but - /// OMITS the key entirely on /deck/my_list (specific-format) — controller mirrors that - /// asymmetry by leaving this null on specific-format responses. Empty array in the - /// 2026-05-23 prod capture; entry shape TBD. + /// Trial / archetype decks. Prod emits this on /deck/info (All format) but OMITS the key + /// entirely on /deck/my_list (specific-format) — controller mirrors that asymmetry by + /// leaving this null on specific-format responses. Emitted EMPTY on /deck/info (matches the + /// 2026-05-23 prod capture); story/get_deck_list is where trial decks are actually populated. /// [JsonPropertyName("trial_deck_list")] - [Key("trial_deck_list")] public List? TrialDeckList { get; set; } + [Key("trial_deck_list")] public List? TrialDeckList { get; set; } } From 6f9976ebad2f58bd867744e052b955e7d9103844 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Fri, 29 May 2026 11:11:40 -0400 Subject: [PATCH 10/10] style(story): blank line before StartAsync --- SVSim.EmulatedEntrypoint/Services/StoryService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/SVSim.EmulatedEntrypoint/Services/StoryService.cs b/SVSim.EmulatedEntrypoint/Services/StoryService.cs index fefad98..b6b62f7 100644 --- a/SVSim.EmulatedEntrypoint/Services/StoryService.cs +++ b/SVSim.EmulatedEntrypoint/Services/StoryService.cs @@ -430,6 +430,7 @@ public class StoryService : IStoryService DeckFormat = d.DeckFormat ?? 0, IsRecommend = d.IsRecommend, }; + public async Task StartAsync(StoryApiType apiType, int[] storyIds, long viewerId) { var resp = new StartResponse();