From f581d03becc0bbf047a114367f9d61f1239b0f47 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Wed, 3 Jun 2026 01:07:42 +0300 Subject: [PATCH] Add weekly challenge configuration management: implement types and functions for building and parsing weekly challenge configurations; enhance UI for challenge type and amount selection --- .../Services/ServerSettingsService.cs | 17 ++- .../admin-ui/src/pages/Settings.tsx | 139 ++++++++++++++++-- 2 files changed, 146 insertions(+), 10 deletions(-) diff --git a/DorkNet.Server/Services/ServerSettingsService.cs b/DorkNet.Server/Services/ServerSettingsService.cs index 9ca76cf..2a3dd6d 100644 --- a/DorkNet.Server/Services/ServerSettingsService.cs +++ b/DorkNet.Server/Services/ServerSettingsService.cs @@ -329,7 +329,7 @@ public class ServerSettingsService(DorkNetDbContext db) if (!element.TryGetProperty("ct", out var typeElement) || !typeElement.TryGetInt32(out var type)) return false; - if (type == 0) + if (type == 0 || IsKnownClientChallengeType(type)) return true; if (type != 1) @@ -352,6 +352,21 @@ public class ServerSettingsService(DorkNetDbContext db) return children.EnumerateArray().All(IsClientWeeklyChallengeConfigElement); } + private static bool IsKnownClientChallengeType(int type) => type is + 2 // TimedBufferChallenge + or 3 // DynamicFloatArithmeticChallenge + or 4 // DynamicIntArithmeticChallenge + or 6 // RequiredEventTypeChallenge + or 7 // RequiredRoomSceneLocationChallenge + or 8 // RequiredEnemyTypeChallenge + or 9 // BoolVarEqualsChallenge + or 11 // DiscGolfFinishUnderParChallenge + or 12 // RequiredGameModeActivityChallenge + or 13 // CompleteGameWithoutChallenge + or 14 // RequiredGestureChallenge + or 15 // HitstreakChallenge + or 16; // HitstreakCountChallenge + private static string CountedAnyChallengeConfig(int target) => "{\"ct\":1,\"ctc\":[{\"ct\":0}],\"t\":" + Math.Clamp(target, 1, 1000) + ",\"cc\":0}"; diff --git a/DorkNet.Server/admin-ui/src/pages/Settings.tsx b/DorkNet.Server/admin-ui/src/pages/Settings.tsx index aec305d..013b1ec 100644 --- a/DorkNet.Server/admin-ui/src/pages/Settings.tsx +++ b/DorkNet.Server/admin-ui/src/pages/Settings.tsx @@ -54,6 +54,69 @@ interface RewardOptions { equipment: RewardOption[]; } +interface WeeklyChallengeTypeOption { + value: number; + label: string; + note: string; +} + +const WEEKLY_CHALLENGE_TYPES: WeeklyChallengeTypeOption[] = [ + { value: 0, label: 'Any challenge event', note: 'Base Challenge' }, + { value: 2, label: 'Timed buffer', note: 'TimedBufferChallenge' }, + { value: 3, label: 'Dynamic float arithmetic', note: 'DynamicFloatArithmeticChallenge' }, + { value: 4, label: 'Dynamic int arithmetic', note: 'DynamicIntArithmeticChallenge' }, + { value: 6, label: 'Required event type', note: 'RequiredEventTypeChallenge' }, + { value: 7, label: 'Required room scene location', note: 'RequiredRoomSceneLocationChallenge' }, + { value: 8, label: 'Required enemy type', note: 'RequiredEnemyTypeChallenge' }, + { value: 9, label: 'Bool var equals', note: 'BoolVarEqualsChallenge' }, + { value: 11, label: 'Disc golf under par', note: 'DiscGolfFinishUnderParChallenge' }, + { value: 12, label: 'Required game mode activity', note: 'RequiredGameModeActivityChallenge' }, + { value: 13, label: 'Complete game without', note: 'CompleteGameWithoutChallenge' }, + { value: 14, label: 'Required gesture', note: 'RequiredGestureChallenge' }, + { value: 15, label: 'Hitstreak', note: 'HitstreakChallenge' }, + { value: 16, label: 'Hitstreak count', note: 'HitstreakCountChallenge' }, +]; + +const WEEKLY_TYPE_BY_VALUE = new Map(WEEKLY_CHALLENGE_TYPES.map((type) => [type.value, type])); +const DEFAULT_WEEKLY_CHALLENGE_TYPE = 0; +const MAX_WEEKLY_CHALLENGE_AMOUNT = 1000; + +function clampWeeklyAmount(value: number): number { + if (!Number.isFinite(value)) return 1; + return Math.min(MAX_WEEKLY_CHALLENGE_AMOUNT, Math.max(1, Math.trunc(value))); +} + +function buildWeeklyChallengeConfig(type: number, amount: number): string { + const safeType = WEEKLY_TYPE_BY_VALUE.has(type) ? type : DEFAULT_WEEKLY_CHALLENGE_TYPE; + const target = clampWeeklyAmount(amount); + const child = { ct: safeType }; + if (target <= 1) return JSON.stringify(child); + return JSON.stringify({ ct: 1, ctc: [child], t: target, cc: 0 }); +} + +function parseWeeklyChallengeConfig(config: string): { type: number; amount: number } { + try { + const parsed = JSON.parse(config) as unknown; + if (!parsed || typeof parsed !== 'object') throw new Error('not an object'); + const root = parsed as { ct?: unknown; ctc?: unknown; t?: unknown }; + const rootType = typeof root.ct === 'number' ? root.ct : DEFAULT_WEEKLY_CHALLENGE_TYPE; + if (rootType === 1 && Array.isArray(root.ctc) && root.ctc.length > 0) { + const child = root.ctc[0] as { ct?: unknown }; + const childType = typeof child?.ct === 'number' && WEEKLY_TYPE_BY_VALUE.has(child.ct) + ? child.ct + : DEFAULT_WEEKLY_CHALLENGE_TYPE; + const amount = typeof root.t === 'number' ? root.t : 1; + return { type: childType, amount: clampWeeklyAmount(amount) }; + } + return { + type: WEEKLY_TYPE_BY_VALUE.has(rootType) ? rootType : DEFAULT_WEEKLY_CHALLENGE_TYPE, + amount: 1, + }; + } catch { + return { type: DEFAULT_WEEKLY_CHALLENGE_TYPE, amount: 1 }; + } +} + interface DiscoveredGameConfigSettings { friendsPostGamePromptUnderFriendCount: number; friendsSuggestFriendCodeOnFriendsScreenCount: number; @@ -148,7 +211,7 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) { { index: current.challenges.length, name: 'New weekly challenge', - config: '{}', + config: buildWeeklyChallengeConfig(DEFAULT_WEEKLY_CHALLENGE_TYPE, 1), description: '', tooltip: '', }, @@ -165,6 +228,19 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) { }); }; + const updateWeeklyChallengeConfig = ( + index: number, + patch: Partial<{ type: number; amount: number }>, + ) => { + const challenge = weekly?.challenges[index]; + if (!challenge) return; + const current = parseWeeklyChallengeConfig(challenge.config); + const next = { ...current, ...patch }; + updateWeeklyChallenge(index, { + config: buildWeeklyChallengeConfig(next.type, next.amount), + }); + }; + const updateWeeklyReward = (patch: Partial) => { setWeekly((current) => current ? { ...current, @@ -544,14 +620,10 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) { onChange={(e) => updateWeeklyChallenge(index, { description: e.target.value })} /> -