feat(mission): add MissionEventKeys registry with Practice numeric→named mapping

Central registry of every string that can appear as ViewerEventCounter.EventKey
or catalog EventType. All 12 top-level prefixes from the achievement/mission
seed JSON are constants; Practice.WinAll and Story.ChapterFinishAll are
hierarchical builders that emit multi-level counter keys per event.

Practice.WinAll translates the wire (difficulty:int, enemy_class_id:int) tuple
into the catalog-facing named form (elite/elite2/elite3 : arisa/erika/...).
Wire difficulty 4/6/7 map to elite/elite2/elite3 (verified via
practicetext.json + practice-opponents.json + practice_ai_setting.csv). Class
1-8 map to the leader names in CardClass ordering. Wire values outside the
known set skip that hierarchical level but still advance the top-level counter.

MissionEventKeys.IsRegistered validates that a string starts with a registered
top-level prefix — for seed-importer drift checks in the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-04 12:59:15 -04:00
parent 6e6856db4f
commit e1b38d5649
2 changed files with 274 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
using SVSim.Database.Services;
namespace SVSim.UnitTests.Services;
public class MissionEventKeysTests
{
// ---- Practice.WinAll ----
[Test]
public void Practice_WinAll_emits_all_three_levels_for_known_tier_and_leader()
{
// Wire difficulty 4 = "elite", enemy_class_id 1 = "arisa" (Forestcraft).
var keys = MissionEventKeys.Practice.WinAll(wireDifficulty: 4, enemyClassId: 1);
Assert.That(keys, Is.EqualTo(new[]
{
"practice_win",
"practice_win:elite",
"practice_win:elite:arisa",
}));
}
[Test]
public void Practice_WinAll_maps_all_three_elite_tiers()
{
Assert.That(MissionEventKeys.Practice.WinAll(4, 2), Contains.Item("practice_win:elite:erika"));
Assert.That(MissionEventKeys.Practice.WinAll(6, 2), Contains.Item("practice_win:elite2:erika"));
Assert.That(MissionEventKeys.Practice.WinAll(7, 2), Contains.Item("practice_win:elite3:erika"));
}
[Test]
public void Practice_WinAll_covers_all_eight_leaders()
{
Assert.Multiple(() =>
{
Assert.That(MissionEventKeys.Practice.WinAll(4, 1), Contains.Item("practice_win:elite:arisa"));
Assert.That(MissionEventKeys.Practice.WinAll(4, 2), Contains.Item("practice_win:elite:erika"));
Assert.That(MissionEventKeys.Practice.WinAll(4, 3), Contains.Item("practice_win:elite:isabelle"));
Assert.That(MissionEventKeys.Practice.WinAll(4, 4), Contains.Item("practice_win:elite:rowen"));
Assert.That(MissionEventKeys.Practice.WinAll(4, 5), Contains.Item("practice_win:elite:luna"));
Assert.That(MissionEventKeys.Practice.WinAll(4, 6), Contains.Item("practice_win:elite:urias"));
Assert.That(MissionEventKeys.Practice.WinAll(4, 7), Contains.Item("practice_win:elite:eris"));
Assert.That(MissionEventKeys.Practice.WinAll(4, 8), Contains.Item("practice_win:elite:yuwan"));
});
}
[Test]
public void Practice_WinAll_falls_back_to_top_level_for_non_elite_difficulty()
{
// Wire difficulty 2 = "Advanced" (or similar) — not in the elite tier registry.
// Emit only the top-level counter; hierarchical levels drop off.
var keys = MissionEventKeys.Practice.WinAll(wireDifficulty: 2, enemyClassId: 1);
Assert.That(keys, Is.EqualTo(new[] { "practice_win" }));
}
[Test]
public void Practice_WinAll_drops_leader_level_for_unknown_class()
{
var keys = MissionEventKeys.Practice.WinAll(wireDifficulty: 4, enemyClassId: 99);
Assert.That(keys, Is.EqualTo(new[] { "practice_win", "practice_win:elite" }));
}
// ---- Story.ChapterFinishAll ----
[Test]
public void Story_ChapterFinishAll_emits_all_three_levels()
{
var keys = MissionEventKeys.Story.ChapterFinishAll("main", 42);
Assert.That(keys, Is.EqualTo(new[]
{
"story_chapter_finish",
"story_chapter_finish:main",
"story_chapter_finish:main:42",
}));
}
// ---- ItemPurchase ----
[Test]
public void ItemPurchase_returns_prefixed_id_string()
{
Assert.That(MissionEventKeys.ItemPurchase(501), Is.EqualTo("item_purchase:501"));
}
// ---- IsRegistered ----
[Test]
public void IsRegistered_accepts_bare_top_level_prefix()
{
Assert.That(MissionEventKeys.IsRegistered("practice_win"), Is.True);
Assert.That(MissionEventKeys.IsRegistered("ranked_or_arena_win"), Is.True);
Assert.That(MissionEventKeys.IsRegistered("challenge_full_clear"), Is.True);
}
[Test]
public void IsRegistered_accepts_hierarchical_extensions()
{
Assert.That(MissionEventKeys.IsRegistered("practice_win:elite:arisa"), Is.True);
Assert.That(MissionEventKeys.IsRegistered("class_level_up:forestcraft"), Is.True);
Assert.That(MissionEventKeys.IsRegistered("item_purchase:501"), Is.True);
Assert.That(MissionEventKeys.IsRegistered("rank_achieved:master"), Is.True);
}
[Test]
public void IsRegistered_rejects_typos_and_unknown_prefixes()
{
Assert.That(MissionEventKeys.IsRegistered("practice_wln"), Is.False); // typo
Assert.That(MissionEventKeys.IsRegistered("practice_win_extra"), Is.False); // suffix without colon
Assert.That(MissionEventKeys.IsRegistered("battle_win_total"), Is.False); // test-only fixture
Assert.That(MissionEventKeys.IsRegistered("orphan_event"), Is.False);
Assert.That(MissionEventKeys.IsRegistered(""), Is.False);
}
// ---- Seed drift sanity check ----
[Test]
public void All_seed_prefixes_are_registered()
{
// Every prefix that could appear in a seed row must be in the registry. Enumerating
// explicitly rather than reading the seed JSON — the point is to catch someone removing
// a prefix from the code without noticing the seed still references it.
string[] knownSeedPrefixes = {
"practice_win", "ranked_win", "ranked_or_arena_win", "daily_match_win",
"story_chapter_finish", "class_level_up", "rank_achieved",
"challenge_play", "challenge_win", "challenge_full_clear",
"play_followers", "private_match_distinct_opponent",
};
foreach (var prefix in knownSeedPrefixes)
{
Assert.That(MissionEventKeys.IsRegistered(prefix), Is.True, $"prefix '{prefix}' not registered");
}
}
}