diff --git a/readme.md b/readme.md index 75f4d1c..7b414b9 100644 --- a/readme.md +++ b/readme.md @@ -63,7 +63,8 @@ To Be Played is split into three repositories: snapshots, once per user, kind and game (`ref`). Rules and numbers: `RewardRules`; a Steam finish needs playtime and one proof of fresh play (ending achievement or playtime around the finish, or half of the IGDB time played since the finish; old history pays nothing). The month bonus needs a finished game in that month. Games added by hand pay 15 for a finish, at most 3 per ISO week. The settle takes a per-user advisory lock. V26 also pays a one-time starter grant - for finishes from before the status log. Shop: a whitelist of 8 looks (`ShopItem`); a `BUY` row proves ownership, + for finishes from before the status log. Shop: a whitelist of 28 looks (`ShopItem`) in seven slots, including animated completed effects, page backgrounds and banner + backgrounds (V27 widens `reward_equipped.slot`); a `BUY` row proves ownership, `reward_equipped` holds what is worn. A platform admin owns every item (the role of the login token, or the `role` claim of the profile ticket for visitors). - **Wishlist**: synced from Steam with store price and discount. diff --git a/src/main/java/core_service/modules/rewards/domain/LookSlot.java b/src/main/java/core_service/modules/rewards/domain/LookSlot.java index 7d23539..5e8e71d 100644 --- a/src/main/java/core_service/modules/rewards/domain/LookSlot.java +++ b/src/main/java/core_service/modules/rewards/domain/LookSlot.java @@ -1,5 +1,5 @@ package core_service.modules.rewards.domain; public enum LookSlot { - AVATAR_BORDER, BADGE, ACCENT, BANNER_FRAME + AVATAR_BORDER, BADGE, ACCENT, BANNER_FRAME, COMPLETED_EFFECT, PAGE_BACKGROUND, BANNER_BACKGROUND } diff --git a/src/main/java/core_service/modules/rewards/domain/ShopItem.java b/src/main/java/core_service/modules/rewards/domain/ShopItem.java index 880b94c..74cfac0 100644 --- a/src/main/java/core_service/modules/rewards/domain/ShopItem.java +++ b/src/main/java/core_service/modules/rewards/domain/ShopItem.java @@ -6,6 +6,8 @@ import java.util.stream.Collectors; /** * The whitelist of looks. The frontend maps a code to its visuals; nothing here is ever uploaded CSS. + * {@code FX_} effects show on the cover of a COMPLETED game and on its details page, {@code BG_} on the page + * background, {@code BN_} on the dashboard and profile banner. */ public enum ShopItem { BORDER_GOLD(LookSlot.AVATAR_BORDER, 150), @@ -15,7 +17,27 @@ public enum ShopItem { ACCENT_GOLD(LookSlot.ACCENT, 200), ACCENT_TEAL(LookSlot.ACCENT, 200), FRAME_GOLD(LookSlot.BANNER_FRAME, 150), - FRAME_NEON(LookSlot.BANNER_FRAME, 200); + FRAME_NEON(LookSlot.BANNER_FRAME, 200), + FX_GOLD_SHINE(LookSlot.COMPLETED_EFFECT, 300), + FX_NEON_PULSE(LookSlot.COMPLETED_EFFECT, 300), + FX_HOLO(LookSlot.COMPLETED_EFFECT, 350), + FX_EMBER(LookSlot.COMPLETED_EFFECT, 300), + BG_TRIANGLES(LookSlot.PAGE_BACKGROUND, 400), + BG_STARFIELD(LookSlot.PAGE_BACKGROUND, 400), + BG_PIXELS(LookSlot.PAGE_BACKGROUND, 400), + BG_AURORA(LookSlot.PAGE_BACKGROUND, 450), + BG_MESH(LookSlot.PAGE_BACKGROUND, 450), + BG_GRID(LookSlot.PAGE_BACKGROUND, 450), + BG_WAVES(LookSlot.PAGE_BACKGROUND, 400), + BG_BOKEH(LookSlot.PAGE_BACKGROUND, 400), + BN_TRIANGLES(LookSlot.BANNER_BACKGROUND, 300), + BN_STARFIELD(LookSlot.BANNER_BACKGROUND, 300), + BN_PIXELS(LookSlot.BANNER_BACKGROUND, 300), + BN_AURORA(LookSlot.BANNER_BACKGROUND, 300), + BN_MESH(LookSlot.BANNER_BACKGROUND, 300), + BN_GRID(LookSlot.BANNER_BACKGROUND, 300), + BN_WAVES(LookSlot.BANNER_BACKGROUND, 300), + BN_BOKEH(LookSlot.BANNER_BACKGROUND, 300); private final LookSlot slot; private final int price; diff --git a/src/main/resources/db/migration/V27__Reward_Equipped_Slot_Width.sql b/src/main/resources/db/migration/V27__Reward_Equipped_Slot_Width.sql new file mode 100644 index 0000000..178a31a --- /dev/null +++ b/src/main/resources/db/migration/V27__Reward_Equipped_Slot_Width.sql @@ -0,0 +1,2 @@ +-- BANNER_BACKGROUND is 17 characters. +ALTER TABLE reward_equipped ALTER COLUMN slot TYPE VARCHAR(32); diff --git a/src/test/java/core_service/modules/rewards/api/RewardControllerTest.java b/src/test/java/core_service/modules/rewards/api/RewardControllerTest.java index bc2ef3a..ee7e9d6 100644 --- a/src/test/java/core_service/modules/rewards/api/RewardControllerTest.java +++ b/src/test/java/core_service/modules/rewards/api/RewardControllerTest.java @@ -1,6 +1,7 @@ package core_service.modules.rewards.api; import core_service.configuration.security.ClientHeaderFilter; +import core_service.modules.rewards.domain.ShopItem; import core_service.modules.rewards.service.RewardService; import core_service.shared.security.Role; import core_service.shared.test.AbstractIntegrationTest; @@ -81,7 +82,7 @@ class RewardControllerTest extends AbstractIntegrationTest { .andExpect(jsonPath("$.balance").value(30)) .andExpect(jsonPath("$.history[0].kind").value("FINISHED")) .andExpect(jsonPath("$.history[0].gameName").value("Game " + GAME)) - .andExpect(jsonPath("$.catalog.length()").value(8)) + .andExpect(jsonPath("$.catalog.length()").value(ShopItem.values().length)) .andExpect(jsonPath("$.owned").isEmpty()); setStatus(GAME, "BACKLOG"); @@ -212,7 +213,7 @@ class RewardControllerTest extends AbstractIntegrationTest { @Test void admin_OwnsEveryItem_NeverBuysAndFallsBackToDefaultAsAUser() throws Exception { mockMvc.perform(asAdmin(get("/api/rewards/wallet"))) - .andExpect(jsonPath("$.owned.length()").value(8)); + .andExpect(jsonPath("$.owned.length()").value(ShopItem.values().length)); mockMvc.perform(asAdmin(post("/api/rewards/purchases")).header(ClientHeaderFilter.CLIENT_HEADER, "test") .contentType(MediaType.APPLICATION_JSON).content("{\"item\":\"FRAME_GOLD\"}")) .andExpect(status().isConflict()).andExpect(jsonPath("$.code").value("ALREADY_OWNED")); diff --git a/src/test/java/core_service/modules/rewards/domain/LooksTest.java b/src/test/java/core_service/modules/rewards/domain/LooksTest.java index 7c2be0e..6fd712b 100644 --- a/src/test/java/core_service/modules/rewards/domain/LooksTest.java +++ b/src/test/java/core_service/modules/rewards/domain/LooksTest.java @@ -25,9 +25,10 @@ class LooksTest extends AbstractUnitTest { @Test void wearable_DropsLooksThatAreNotOwnedOrNotInTheCatalog() { Map equipped = Map.of(LookSlot.AVATAR_BORDER, "BORDER_GOLD", LookSlot.BADGE, "BADGE_PILE_SLAYER", - LookSlot.ACCENT, "ACCENT_NEON_PINK"); + LookSlot.ACCENT, "ACCENT_NEON_PINK", LookSlot.BANNER_BACKGROUND, "BN_GRID"); - assertThat(Looks.wearable(equipped, Set.of("BORDER_GOLD", "ACCENT_NEON_PINK"))) - .containsOnly(Map.entry(LookSlot.AVATAR_BORDER, "BORDER_GOLD")); + assertThat(Looks.wearable(equipped, Set.of("BORDER_GOLD", "ACCENT_NEON_PINK", "BN_GRID"))) + .containsOnly(Map.entry(LookSlot.AVATAR_BORDER, "BORDER_GOLD"), + Map.entry(LookSlot.BANNER_BACKGROUND, "BN_GRID")); } } diff --git a/src/test/java/core_service/modules/rewards/repo/RewardLedgerRepositoryTest.java b/src/test/java/core_service/modules/rewards/repo/RewardLedgerRepositoryTest.java index 3e51686..533b780 100644 --- a/src/test/java/core_service/modules/rewards/repo/RewardLedgerRepositoryTest.java +++ b/src/test/java/core_service/modules/rewards/repo/RewardLedgerRepositoryTest.java @@ -99,4 +99,11 @@ class RewardLedgerRepositoryTest extends AbstractPersistenceTest { assertThat(repository.equipped(USER)).containsOnlyKeys(LookSlot.ACCENT); } + + @Test + void equip_TheLongestSlotNameFits() { + repository.equip(USER, LookSlot.BANNER_BACKGROUND, "BN_GRID"); + + assertThat(repository.equipped(USER)).containsOnly(Map.entry(LookSlot.BANNER_BACKGROUND, "BN_GRID")); + } }