namespace SVSim.Database.Services; /// /// Registry of every string that can appear as ViewerEventCounter.EventKey or /// mission/achievement EventType. Two goals: /// /// /// Give emitters (controllers) a single named entry point per event family so no /// controller inlines the string form. /// Give the catalog seed importers a validation set — every event_type in a /// seed row must start with a prefix registered here, else the importer throws at bootstrap. /// Prevents silent drift between catalog data and emitter code (e.g. a mission that /// references practice_wln and never advances). /// /// /// Format convention: colon-hierarchical, most-general first. Callers emit multiple levels /// so a single event increments every level of counter the catalog might reference. /// public static class MissionEventKeys { // ---- Top-level catalog prefixes (12) — 1:1 with seed JSON event_type strings ---- public const string PracticeWin = "practice_win"; public const string RankedWin = "ranked_win"; public const string RankedOrArenaWin = "ranked_or_arena_win"; public const string DailyMatchWin = "daily_match_win"; public const string StoryChapterFinish = "story_chapter_finish"; public const string ClassLevelUp = "class_level_up"; public const string RankAchieved = "rank_achieved"; public const string ChallengePlay = "challenge_play"; public const string ChallengeWin = "challenge_win"; public const string ChallengeFullClear = "challenge_full_clear"; public const string PlayFollowers = "play_followers"; public const string PrivateMatchDistinctOpponent = "private_match_distinct_opponent"; // ---- Item purchase (per-catalog-entry) ---- public const string ItemPurchasePrefix = "item_purchase"; public static string ItemPurchase(int catalogId) => $"{ItemPurchasePrefix}:{catalogId}"; // ---- Practice hierarchical builders ---- public static class Practice { /// /// Wire difficulty → tier name used in catalog rows. Values 4/6/7 correspond to /// Elite / Elite 2 / Elite 3 respectively (verified via /// practicetext.json + practice-opponents.json + practice_ai_setting.csv cross-reference). /// Other CSV difficulty values (0, 2, 3, 5, 101-109) have no achievement catalog rows — /// null return means "emit only the top-level counter." /// public static string? TierName(int wireDifficulty) => wireDifficulty switch { 4 => "elite", 6 => "elite2", 7 => "elite3", _ => null, }; /// /// Wire enemy_class_id → leader name used in catalog rows. Class ordering /// matches the CardClass enum (1=Forestcraft/Arisa, ..., 8=Portalcraft/Yuwan). /// public static string? LeaderName(int enemyClassId) => enemyClassId switch { 1 => "arisa", 2 => "erika", 3 => "isabelle", 4 => "rowen", 5 => "luna", 6 => "urias", 7 => "eris", 8 => "yuwan", _ => null, }; /// /// Emits practice_win plus any hierarchical variants whose parts resolve. If the /// wire values don't resolve to a known tier or leader, that level is skipped — the /// lifetime counter still advances. /// public static IReadOnlyList WinAll(int wireDifficulty, int enemyClassId) { var tier = TierName(wireDifficulty); var leader = LeaderName(enemyClassId); var list = new List(3) { PracticeWin }; if (tier is not null) list.Add($"{PracticeWin}:{tier}"); if (tier is not null && leader is not null) list.Add($"{PracticeWin}:{tier}:{leader}"); return list; } } // ---- Story hierarchical builders ---- public static class Story { /// /// Emits story_chapter_finish plus :{family} plus :{family}:{storyId}. /// is the low-cardinality family label (main, /// limited, event, ...) resolved from StoryApiType upstream. /// public static IReadOnlyList ChapterFinishAll(string family, long storyId) => new[] { StoryChapterFinish, $"{StoryChapterFinish}:{family}", $"{StoryChapterFinish}:{family}:{storyId}", }; } // ---- Seed-import validation ---- private static readonly IReadOnlySet _registeredPrefixes = new HashSet { PracticeWin, RankedWin, RankedOrArenaWin, DailyMatchWin, StoryChapterFinish, ClassLevelUp, RankAchieved, ChallengePlay, ChallengeWin, ChallengeFullClear, PlayFollowers, PrivateMatchDistinctOpponent, ItemPurchasePrefix, }; /// /// True iff is a registered top-level prefix or a hierarchical /// extension of one (prefix alone, or prefix:qualifier). Called by the /// achievement/mission catalog importers to catch drift between seed data and code. /// public static bool IsRegistered(string eventType) { foreach (var prefix in _registeredPrefixes) { if (eventType == prefix) return true; if (eventType.Length > prefix.Length + 1 && eventType[prefix.Length] == ':' && eventType.AsSpan(0, prefix.Length).SequenceEqual(prefix)) return true; } return false; } /// Exposed for test assertions; do NOT mutate. public static IReadOnlySet RegisteredPrefixes => _registeredPrefixes; }