From fdd9f58b20b297c4ec3d326cc69bef7faea192c4 Mon Sep 17 00:00:00 2001 From: Brandon Nguyen <58405975+Bratah123@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:36:09 -0700 Subject: [PATCH] FEAT: Add better ui for versus season rewards --- spirit/database/json_data/versus_seasons.json | 840 +++++++++++++++++- spirit/server/admin/dashboard.html | 115 ++- 2 files changed, 915 insertions(+), 40 deletions(-) diff --git a/spirit/database/json_data/versus_seasons.json b/spirit/database/json_data/versus_seasons.json index d967b6f..306d8a6 100644 --- a/spirit/database/json_data/versus_seasons.json +++ b/spirit/database/json_data/versus_seasons.json @@ -245,7 +245,7 @@ { "name": "Sword & Shield - Fusion Strike", "rewardType": "Archetype", - "rewardAmount": 10, + "rewardAmount": 1, "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", "rewardCurrency": "", "rewardDescription": { @@ -253,13 +253,121 @@ }, "rewardReason": "", "index": 0 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 1 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 2 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 3 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 4 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 5 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 6 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 7 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 8 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 9 } ], "60": [ { "name": "Sword & Shield - Evolving Skies", "rewardType": "Archetype", - "rewardAmount": 10, + "rewardAmount": 1, "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", "rewardCurrency": "", "rewardDescription": { @@ -267,13 +375,121 @@ }, "rewardReason": "", "index": 0 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 1 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 2 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 3 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 4 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 5 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 6 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 7 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 8 + }, + { + "name": "Sword & Shield - Evolving Skies", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b317777", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Evolving Skies" + }, + "rewardReason": "", + "index": 9 } ], "65": [ { "name": "Sword & Shield - Chilling Reign", "rewardType": "Archetype", - "rewardAmount": 10, + "rewardAmount": 1, "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", "rewardCurrency": "", "rewardDescription": { @@ -281,6 +497,114 @@ }, "rewardReason": "", "index": 0 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 1 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 2 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 3 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 4 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 5 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 6 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 7 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 8 + }, + { + "name": "Sword & Shield - Chilling Reign", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b316666", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Chilling Reign" + }, + "rewardReason": "", + "index": 9 } ], "70": [ @@ -319,7 +643,7 @@ { "name": "Pokemon Go - Pokemon Go", "rewardType": "Archetype", - "rewardAmount": 15, + "rewardAmount": 1, "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", "rewardCurrency": "", "rewardDescription": { @@ -327,13 +651,181 @@ }, "rewardReason": "", "index": 0 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 1 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 2 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 3 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 4 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 5 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 6 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 7 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 8 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 9 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 10 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 11 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 12 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 13 + }, + { + "name": "Pokemon Go - Pokemon Go", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b314545", + "rewardCurrency": "", + "rewardDescription": { + "id": "Pokemon Go - Pokemon Go" + }, + "rewardReason": "", + "index": 14 } ], "110": [ { "name": "Sword & Shield - Rebel Clash", "rewardType": "Archetype", - "rewardAmount": 15, + "rewardAmount": 1, "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", "rewardCurrency": "", "rewardDescription": { @@ -341,13 +833,181 @@ }, "rewardReason": "", "index": 0 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 1 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 2 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 3 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 4 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 5 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 6 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 7 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 8 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 9 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 10 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 11 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 12 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 13 + }, + { + "name": "Sword & Shield - Rebel Clash", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b312222", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Rebel Clash" + }, + "rewardReason": "", + "index": 14 } ], "115": [ { "name": "Sword & Shield - Fusion Strike", "rewardType": "Archetype", - "rewardAmount": 15, + "rewardAmount": 1, "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", "rewardCurrency": "", "rewardDescription": { @@ -355,6 +1015,174 @@ }, "rewardReason": "", "index": 0 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 1 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 2 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 3 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 4 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 5 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 6 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 7 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 8 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 9 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 10 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 11 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 12 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 13 + }, + { + "name": "Sword & Shield - Fusion Strike", + "rewardType": "Archetype", + "rewardAmount": 1, + "rewardProductID": "d017c195-83c5-c74e-0638-25128b3116c4", + "rewardCurrency": "", + "rewardDescription": { + "id": "Sword & Shield - Fusion Strike" + }, + "rewardReason": "", + "index": 14 } ], "120": [ diff --git a/spirit/server/admin/dashboard.html b/spirit/server/admin/dashboard.html index 9663d4a..00f4f8d 100644 --- a/spirit/server/admin/dashboard.html +++ b/spirit/server/admin/dashboard.html @@ -69,7 +69,10 @@ .reward-chip img { height: 58px; width: 42px; object-fit: cover; border-radius: 4px; display: block; background: #0b0e14; } .reward-chip .coin { width: 42px; height: 42px; border-radius: 50%; flex: none; display: flex; align-items: center; justify-content: center; font-size: 21px; background: radial-gradient(circle at 35% 30%, #ffe98a, #d4a017 70%); box-shadow: inset 0 0 0 3px rgba(0,0,0,.18); } .reward-chip .rc-name { font-size: 12px; max-width: 150px; line-height: 1.3; } - .reward-chip .rc-amt { color: var(--accent); font-weight: 700; font-size: 12px; } + .reward-chip .rc-amt { color: var(--accent); font-weight: 700; font-size: 12px; min-width: 26px; text-align: center; } + .rc-step { display: flex; align-items: center; gap: 3px; } + .rc-pm { width: 20px; height: 20px; border-radius: 5px; border: 1px solid var(--line); background: var(--panel); color: var(--accent); font-size: 14px; line-height: 1; cursor: pointer; padding: 0; } + .rc-pm:hover { border-color: var(--accent); } .chip-x { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 14px; padding: 2px 4px; } .chip-x:hover { color: var(--bad); } .add-chip { border: 1px dashed var(--line); color: var(--muted); background: none; border-radius: 9px; padding: 18px 16px; cursor: pointer; font-size: 13px; } @@ -704,7 +707,7 @@ function defaultSeason(n) { function parseTiers(season) { const tiers = (season.tiers || []).map(t => ({ ms: Object.entries(t.rewards || {}) - .map(([pts, rewards]) => ({points: parseInt(pts) || 0, rewards})) + .map(([pts, rewards]) => ({points: parseInt(pts) || 0, rewards: expandRewards(rewards)})) .sort((a, b) => a.points - b.points) })); return tiers.length ? tiers : [{ms: []}]; @@ -739,19 +742,46 @@ function deleteSeason() { SEASONS.splice(VS_SEL, 1); VS_SEL = 0; vsDirty(true); renderVersus(); } -function rewardChipHTML(r, ti, mi, ri) { - let art, label; - const amt = r.rewardAmount || 0; +/* The client shows a reward's quantity by COUNTING duplicate entries in the + milestone array (rewardAmount is ignored for Archetype rewards), so identical + products/cards are stored as N entries. Group them into one stepper chip. */ +function groupRewards(rewards) { + const groups = [], byKey = {}; + (rewards || []).forEach((r, idx) => { + const key = r.rewardType === 'Archetype' + ? 'A:' + String(r.rewardProductID || '').toLowerCase() + : 'T:' + idx; // tokens never merge — they carry their own amount + if (byKey[key] === undefined) { byKey[key] = groups.length; groups.push({reward: r, indices: []}); } + groups[byKey[key]].indices.push(idx); + }); + return groups; +} +function expandRewards(rewards) { + const out = []; + (rewards || []).forEach(r => { + if (r.rewardType === 'Archetype' && (r.rewardAmount || 1) > 1) { + const n = Math.max(1, r.rewardAmount | 0); + for (let k = 0; k < n; k++) out.push({...r, rewardAmount: 1}); + } else { out.push(r); } + }); + return out; +} +function rewardGroupChip(g, stepPrefix, removePrefix) { + const r = g.reward, n = g.indices.length; if (r.rewardType === 'Archetype') { const m = metaFor(r.rewardProductID) || {name: r.name || 'Unknown', image: null}; - art = m.image ? `` : '
🎴
'; - label = `${esc(m.name)}×${Math.max(1, amt)}`; - } else { - art = '
🪙
'; - label = `${esc(r.name || 'Tokens')}${amt}`; + const art = m.image ? `` : '
🎴
'; + return `
${art}${esc(m.name)} + + + ×${n} + + +
`; } - return `
${art}${label} -
`; + return `
🪙
+ ${esc(r.name || 'Tokens')}${r.rewardAmount || 0} +
`; } function milestoneHTML(m, ti, mi) { @@ -765,7 +795,8 @@ function milestoneHTML(m, ti, mi) { pts
- ${m.rewards.map((r, ri) => rewardChipHTML(r, ti, mi, ri)).join('')} + ${groupRewards(m.rewards).map((g, gi) => rewardGroupChip(g, + `vsRewardStep(${ti},${mi},${gi}`, `vsRewardRemoveGroup(${ti},${mi},${gi}`)).join('')} ${warn}
@@ -820,7 +851,19 @@ function addMilestone(ti) { vsDirty(true); renderVersus(); } function delMilestone(ti, mi) { vsSeason()._tiers[ti].ms.splice(mi, 1); vsDirty(true); renderVersus(); } -function removeVsReward(ti, mi, ri) { vsSeason()._tiers[ti].ms[mi].rewards.splice(ri, 1); vsDirty(true); renderVersus(); } +function vsRewardStep(ti, mi, gi, delta) { + const rewards = vsSeason()._tiers[ti].ms[mi].rewards; + const g = groupRewards(rewards)[gi]; if (!g) return; + if (delta > 0) rewards.push({...g.reward, rewardAmount: 1}); + else rewards.splice(g.indices[g.indices.length - 1], 1); + vsDirty(true); renderVersus(); +} +function vsRewardRemoveGroup(ti, mi, gi) { + const rewards = vsSeason()._tiers[ti].ms[mi].rewards; + const g = groupRewards(rewards)[gi]; if (!g) return; + g.indices.slice().sort((a, b) => b - a).forEach(idx => rewards.splice(idx, 1)); + vsDirty(true); renderVersus(); +} async function saveSeasons() { const out = SEASONS.map(s => ({ @@ -874,6 +917,8 @@ async function loadTournaments() { try { const d = await api('tournaments'); TOURS = d.tournaments || []; + TOURS.forEach(t => [t.definition && t.definition.run, t.definition && t.definition.leaderboard] + .forEach(x => (x && x.prizeTable || []).forEach(p => { p.rewards = expandRewards(p.rewards); }))); if (!TOURS.length) TOURS = [defaultTournament(1)]; T_SEL = Math.min(T_SEL, TOURS.length - 1); tDirty(false); @@ -902,25 +947,25 @@ function prizeRowHTML(kind, p, i) {
- ${(p.rewards || []).map((r, ri) => tourRewardChipHTML(kind, r, i, ri)).join('')} + ${groupRewards(p.rewards).map((g, gi) => rewardGroupChip(g, + `tourRewardStep('${kind}',${i},${gi}`, `tourRewardRemoveGroup('${kind}',${i},${gi}`)).join('')}
`; } -function tourRewardChipHTML(kind, r, i, ri) { - let art, label; - const amt = r.rewardAmount || 0; - if (r.rewardType === 'Archetype') { - const m = metaFor(r.rewardProductID) || {name: r.name || 'Unknown', image: null}; - art = m.image ? `` : '
🎴
'; - label = `${esc(m.name)}×${Math.max(1, amt)}`; - } else { - art = '
🪙
'; - label = `${esc(r.name || 'Tokens')}${amt}`; - } - return `
${art}${label} -
`; +function tourRewardStep(kind, i, gi, delta) { + const rewards = prizeTableOf(kind)[i].rewards; + const g = groupRewards(rewards)[gi]; if (!g) return; + if (delta > 0) rewards.push({...g.reward, rewardAmount: 1}); + else rewards.splice(g.indices[g.indices.length - 1], 1); + tDirty(true); renderTournaments(); +} +function tourRewardRemoveGroup(kind, i, gi) { + const rewards = prizeTableOf(kind)[i].rewards; + const g = groupRewards(rewards)[gi]; if (!g) return; + g.indices.slice().sort((a, b) => b - a).forEach(idx => rewards.splice(idx, 1)); + tDirty(true); renderTournaments(); } function prizeTableOf(kind) { return kind === 'run' ? tDef().run.prizeTable : tDef().leaderboard.prizeTable; @@ -933,7 +978,6 @@ function addPrizeRow(kind) { tDirty(true); renderTournaments(); } function delPrizeRow(kind, i) { prizeTableOf(kind).splice(i, 1); tDirty(true); renderTournaments(); } -function removeTourReward(kind, i, ri) { prizeTableOf(kind)[i].rewards.splice(ri, 1); tDirty(true); renderTournaments(); } function renderTournaments() { renderTourPills(); @@ -1132,21 +1176,24 @@ function pickerAdd() { REWARD.products[guid] = (REWARD.products[guid] || 0) + count; updateRewardItems(); } else { - dispatchReward({name, rewardType: 'Archetype', rewardAmount: count, - rewardProductID: guid, rewardDescription: {id: name}, flavor: name}); + // one entry per copy — the client renders the quantity from array length + const base = {name, rewardType: 'Archetype', rewardAmount: 1, + rewardProductID: guid, rewardDescription: {id: name}, flavor: name}; + dispatchRewards(Array.from({length: count}, () => ({...base}))); } } closePicker(); } -function dispatchReward(reward) { +function dispatchRewards(rewards) { if (PICKER.mode === 'versus') { - vsSeason()._tiers[PICKER.tierIndex].ms[PICKER.msIndex].rewards.push(reward); + vsSeason()._tiers[PICKER.tierIndex].ms[PICKER.msIndex].rewards.push(...rewards); vsDirty(true); renderVersus(); } else if (PICKER.mode === 'trun' || PICKER.mode === 'tlb') { - prizeTableOf(PICKER.mode === 'trun' ? 'run' : 'leaderboard')[PICKER.tierIndex].rewards.push(reward); + prizeTableOf(PICKER.mode === 'trun' ? 'run' : 'leaderboard')[PICKER.tierIndex].rewards.push(...rewards); tDirty(true); renderTournaments(); } } +function dispatchReward(reward) { dispatchRewards([reward]); } async function bootData() { await loadProducts();