From f2a5f2b128af5dc96e98ab45e6fc2144a24bf07d Mon Sep 17 00:00:00 2001 From: Ada Vale <104856138+AdaInTheLab@users.noreply.github.com> Date: Fri, 29 May 2026 04:30:52 -0400 Subject: [PATCH 1/2] @ feat(vip): VIP tiers + first-login pack + recurring tier gifts (#233, #234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a spawn-driven VipPerks feature covering two board items: #233 — first-time-login pack: the first time a player ever spawns, a configured VIP-gift template is delivered straight into their inventory (items dropped at feet, linked commands run). Once-ever, guarded by the new first_login_grants table. #234 — recurring tier gifts: players carry an admin-assigned VIP tier (new player_metadata.vip_tier, admin-editable tier names in settings). On spawn, each tier-gift rule for that tier is materialized once as a repeatable vip_gifts row; the existing /vip claim flow then drives the daily/weekly cadence. No tier ("pleb") = nothing. Spawn-driven rather than timer-driven: a player must be online to claim or receive items anyway, so doing the work on connect stays idempotent across reconnects without a background sweep. Backend: - migration 012: player_metadata.vip_tier + first_login_grants - VipPerksFeature + VipPerksSettings (persisted JSON, VoteRewards pattern) - FirstLoginGrantRepository; VipGiftRepository.HasGiftByNameAndPeriod; PlayerMetadataRepository.SetTier/GetPlayerIdsByTier (SetTier writes only vip_tier so it never clobbers name colour / tag / notes) - VipPerksController (settings + tiers); PlayerMetadataController tier route Frontend: - VIP tier dropdown in PlayerEditDialog - VIP Perks settings tab (tiers, first-login pack, tier-gift rules) - api/types/i18n (en translated; other locales English placeholders) Tests: PlayerMetadata (tier set/clear, no-clobber both ways, by-tier query), FirstLoginGrant (once-ever), VipGift (has-by-name-and-period). 10 new, green. Co-Authored-By: Claude Opus 4.8 @ --- frontend/src/api/players.ts | 5 + frontend/src/api/vipperks.ts | 16 + frontend/src/components/PlayerEditDialog.vue | 40 ++- frontend/src/i18n/locales/de.ts | 30 ++ frontend/src/i18n/locales/en.ts | 32 ++ frontend/src/i18n/locales/es.ts | 30 ++ frontend/src/i18n/locales/fr.ts | 30 ++ frontend/src/i18n/locales/ja.ts | 30 ++ frontend/src/i18n/locales/ko.ts | 30 ++ frontend/src/i18n/locales/zh-CN.ts | 30 ++ frontend/src/i18n/locales/zh-TW.ts | 30 ++ frontend/src/types/index.ts | 17 + frontend/src/views/SettingsView.vue | 185 ++++++++++- .../FirstLoginGrantRepositoryTests.cs | 54 ++++ .../PlayerMetadataRepositoryTests.cs | 102 ++++++ .../Repositories/VipGiftRepositoryTests.cs | 44 +++ src/KitsuneCommand.Tests/TestDbFixture.cs | 25 ++ .../Config/Migrations/012_vip_tiers.sql | 15 + .../Data/Entities/FirstLoginGrant.cs | 13 + .../Data/Entities/PlayerMetadata.cs | 7 + .../Repositories/FirstLoginGrantRepository.cs | 47 +++ .../Repositories/PlayerMetadataRepository.cs | 39 +++ .../Data/Repositories/VipGiftRepository.cs | 17 + .../Features/VipPerksFeature.cs | 305 ++++++++++++++++++ .../Features/VipPerksSettings.cs | 70 ++++ .../Controllers/PlayerMetadataController.cs | 16 + .../Web/Controllers/VipPerksController.cs | 77 +++++ src/KitsuneCommand/Web/Models/PlayerModels.cs | 6 + 28 files changed, 1338 insertions(+), 4 deletions(-) create mode 100644 frontend/src/api/vipperks.ts create mode 100644 src/KitsuneCommand.Tests/Repositories/FirstLoginGrantRepositoryTests.cs create mode 100644 src/KitsuneCommand.Tests/Repositories/PlayerMetadataRepositoryTests.cs create mode 100644 src/KitsuneCommand.Tests/Repositories/VipGiftRepositoryTests.cs create mode 100644 src/KitsuneCommand/Config/Migrations/012_vip_tiers.sql create mode 100644 src/KitsuneCommand/Data/Entities/FirstLoginGrant.cs create mode 100644 src/KitsuneCommand/Data/Repositories/FirstLoginGrantRepository.cs create mode 100644 src/KitsuneCommand/Features/VipPerksFeature.cs create mode 100644 src/KitsuneCommand/Features/VipPerksSettings.cs create mode 100644 src/KitsuneCommand/Web/Controllers/VipPerksController.cs diff --git a/frontend/src/api/players.ts b/frontend/src/api/players.ts index 6482edb..2c15ad1 100644 --- a/frontend/src/api/players.ts +++ b/frontend/src/api/players.ts @@ -76,3 +76,8 @@ export async function updatePlayerMetadata( export async function setAdminLevel(entityId: number, level: number): Promise { await apiClient.post(`/api/players/${entityId}/admin-level`, { level }) } + +export async function setPlayerTier(playerId: string, tier: string | null): Promise { + const response = await apiClient.put(`/api/players/metadata/${encodeURIComponent(playerId)}/tier`, { tier }) + return response.data.data +} diff --git a/frontend/src/api/vipperks.ts b/frontend/src/api/vipperks.ts new file mode 100644 index 0000000..69b056d --- /dev/null +++ b/frontend/src/api/vipperks.ts @@ -0,0 +1,16 @@ +import apiClient from './client' +import type { VipPerksSettings } from '@/types' + +export async function getVipPerksSettings(): Promise { + const response = await apiClient.get('/api/vipperks/settings') + return response.data.data +} + +export async function updateVipPerksSettings(settings: VipPerksSettings): Promise { + await apiClient.put('/api/vipperks/settings', settings) +} + +export async function getVipTiers(): Promise { + const response = await apiClient.get('/api/vipperks/tiers') + return response.data.data ?? [] +} diff --git a/frontend/src/components/PlayerEditDialog.vue b/frontend/src/components/PlayerEditDialog.vue index cefd611..572d3d7 100644 --- a/frontend/src/components/PlayerEditDialog.vue +++ b/frontend/src/components/PlayerEditDialog.vue @@ -1,8 +1,9 @@