diff --git a/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-card-weights.json b/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-card-weights.json
new file mode 100644
index 0000000..5ce386a
--- /dev/null
+++ b/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-card-weights.json
@@ -0,0 +1,7 @@
+[
+ { "pack_id": 10000, "slot": "general", "tier": "bronze", "card_id": 100, "rate_pct": 50.0, "is_leader": false, "is_alt_art": false },
+ { "pack_id": 10000, "slot": "general", "tier": "bronze", "card_id": 101, "rate_pct": 26.5, "is_leader": false, "is_alt_art": false },
+ { "pack_id": 10000, "slot": "general", "tier": "legendary", "card_id": 200, "rate_pct": 1.5, "is_leader": false, "is_alt_art": false },
+ { "pack_id": 98001, "slot": "bonus", "tier": "special", "card_id": 300, "rate_pct": null, "is_leader": true, "is_alt_art": false },
+ { "pack_id": 98001, "slot": "bonus", "tier": "special", "card_id": 301, "rate_pct": null, "is_leader": true, "is_alt_art": false }
+]
diff --git a/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-config.json b/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-config.json
new file mode 100644
index 0000000..a256566
--- /dev/null
+++ b/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-config.json
@@ -0,0 +1,4 @@
+[
+ { "pack_id": 10000, "short_code": "Basic", "animation_rate_pct": 8.0, "has_bonus_slot": false, "special_kind": null },
+ { "pack_id": 98001, "short_code": "98ANV", "animation_rate_pct": 8.0, "has_bonus_slot": true, "special_kind": "leader_card" }
+]
diff --git a/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-slot-rates.json b/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-slot-rates.json
new file mode 100644
index 0000000..42b2a4a
--- /dev/null
+++ b/SVSim.Bootstrap/Data/test-fixtures/seeds/pack-draw-slot-rates.json
@@ -0,0 +1,10 @@
+[
+ { "pack_id": 10000, "slot": "general", "tier": "bronze", "rate_pct": 76.5 },
+ { "pack_id": 10000, "slot": "general", "tier": "silver", "rate_pct": 16.0 },
+ { "pack_id": 10000, "slot": "general", "tier": "gold", "rate_pct": 6.0 },
+ { "pack_id": 10000, "slot": "general", "tier": "legendary", "rate_pct": 1.5 },
+ { "pack_id": 10000, "slot": "eighth", "tier": "silver", "rate_pct": 92.5 },
+ { "pack_id": 10000, "slot": "eighth", "tier": "gold", "rate_pct": 6.0 },
+ { "pack_id": 10000, "slot": "eighth", "tier": "legendary", "rate_pct": 1.5 },
+ { "pack_id": 98001, "slot": "bonus", "tier": "special", "rate_pct": 100.0 }
+]
diff --git a/SVSim.Bootstrap/Importers/PackDrawTableImporter.cs b/SVSim.Bootstrap/Importers/PackDrawTableImporter.cs
new file mode 100644
index 0000000..7ff2a21
--- /dev/null
+++ b/SVSim.Bootstrap/Importers/PackDrawTableImporter.cs
@@ -0,0 +1,102 @@
+using Microsoft.EntityFrameworkCore;
+using SVSim.Bootstrap.Models.Seed;
+using SVSim.Database;
+using SVSim.Database.Enums;
+using SVSim.Database.Models;
+
+namespace SVSim.Bootstrap.Importers;
+
+///
+/// Idempotent upsert of the per-pack draw table from
+/// seeds/pack-draw-config.json, pack-draw-slot-rates.json, and
+/// pack-draw-card-weights.json. Replaces wholesale per pack (config keyed on
+/// pack_id; slot rates / card weights wiped and reinserted) — the upstream data is
+/// post-shutdown closed, so we do not preserve hand-edits on these tables.
+///
+public class PackDrawTableImporter
+{
+ public async Task ImportAsync(SVSimDbContext context, string seedDir)
+ {
+ var configs = SeedLoader.LoadList(Path.Combine(seedDir, "pack-draw-config.json"));
+ var slotRates = SeedLoader.LoadList(Path.Combine(seedDir, "pack-draw-slot-rates.json"));
+ var cardWeights = SeedLoader.LoadList(Path.Combine(seedDir, "pack-draw-card-weights.json"));
+
+ if (configs.Count == 0)
+ {
+ Console.WriteLine("[PackDrawTableImporter] No seed rows; skipping.");
+ return 0;
+ }
+
+ var seedPackIds = configs.Select(c => c.PackId).ToHashSet();
+
+ // Full-replace strategy: wipe rows for any pack in the seed, then reinsert.
+ await context.PackDrawCardWeights
+ .Where(w => seedPackIds.Contains(w.PackId))
+ .ExecuteDeleteAsync();
+ await context.PackDrawSlotRates
+ .Where(s => seedPackIds.Contains(s.PackId))
+ .ExecuteDeleteAsync();
+
+ var existingConfigs = await context.PackDrawConfigs
+ .Where(c => seedPackIds.Contains(c.Id))
+ .ToDictionaryAsync(c => c.Id);
+
+ foreach (var s in configs)
+ {
+ var row = existingConfigs.TryGetValue(s.PackId, out var ex)
+ ? ex : new PackDrawConfigEntry { Id = s.PackId };
+ row.AnimationRatePct = s.AnimationRatePct;
+ row.HasBonusSlot = s.HasBonusSlot;
+ row.SpecialKind = s.SpecialKind;
+ row.ShortCode = s.ShortCode;
+ if (ex is null) context.PackDrawConfigs.Add(row);
+ }
+
+ foreach (var s in slotRates)
+ {
+ context.PackDrawSlotRates.Add(new PackDrawSlotRateEntry
+ {
+ PackId = s.PackId,
+ Slot = ParseSlot(s.Slot),
+ Tier = ParseTier(s.Tier),
+ RatePct = s.RatePct,
+ });
+ }
+
+ foreach (var s in cardWeights)
+ {
+ context.PackDrawCardWeights.Add(new PackDrawCardWeightEntry
+ {
+ PackId = s.PackId,
+ Slot = ParseSlot(s.Slot),
+ Tier = ParseTier(s.Tier),
+ CardId = s.CardId,
+ RatePct = s.RatePct,
+ IsLeader = s.IsLeader,
+ IsAltArt = s.IsAltArt,
+ });
+ }
+
+ await context.SaveChangesAsync();
+ Console.WriteLine($"[PackDrawTableImporter] {configs.Count} configs / {slotRates.Count} slot rates / {cardWeights.Count} card weights");
+ return configs.Count;
+ }
+
+ private static DrawSlot ParseSlot(string s) => s switch
+ {
+ "general" => DrawSlot.General,
+ "eighth" => DrawSlot.Eighth,
+ "bonus" => DrawSlot.Bonus,
+ _ => throw new InvalidDataException($"PackDrawTableImporter: unknown slot \"{s}\""),
+ };
+
+ private static DrawTier ParseTier(string s) => s switch
+ {
+ "bronze" => DrawTier.Bronze,
+ "silver" => DrawTier.Silver,
+ "gold" => DrawTier.Gold,
+ "legendary" => DrawTier.Legendary,
+ "special" => DrawTier.Special,
+ _ => throw new InvalidDataException($"PackDrawTableImporter: unknown tier \"{s}\""),
+ };
+}
diff --git a/SVSim.UnitTests/Importers/PackDrawTableImporterTests.cs b/SVSim.UnitTests/Importers/PackDrawTableImporterTests.cs
new file mode 100644
index 0000000..01709e6
--- /dev/null
+++ b/SVSim.UnitTests/Importers/PackDrawTableImporterTests.cs
@@ -0,0 +1,48 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using SVSim.Bootstrap.Importers;
+using SVSim.Database;
+using SVSim.Database.Enums;
+using SVSim.UnitTests.Infrastructure;
+
+namespace SVSim.UnitTests.Importers;
+
+public class PackDrawTableImporterTests
+{
+ private static string SeedDir => Path.Combine(AppContext.BaseDirectory, "Data", "seeds");
+
+ [Test]
+ public async Task Imports_config_slot_rates_and_card_weights()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new PackDrawTableImporter().ImportAsync(db, SeedDir);
+
+ Assert.That(await db.PackDrawConfigs.CountAsync(), Is.EqualTo(2));
+ Assert.That(await db.PackDrawSlotRates.CountAsync(), Is.EqualTo(8));
+ Assert.That(await db.PackDrawCardWeights.CountAsync(), Is.EqualTo(5));
+
+ var bonus = await db.PackDrawCardWeights
+ .Where(w => w.PackId == 98001 && w.Slot == DrawSlot.Bonus)
+ .ToListAsync();
+ Assert.That(bonus.Count, Is.EqualTo(2));
+ Assert.That(bonus.All(w => w.RatePct == null && w.IsLeader && w.Tier == DrawTier.Special), Is.True);
+ }
+
+ [Test]
+ public async Task Is_idempotent_on_rerun()
+ {
+ using var factory = new SVSimTestFactory();
+ using var scope = factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ await new PackDrawTableImporter().ImportAsync(db, SeedDir);
+ int before = await db.PackDrawCardWeights.CountAsync();
+ await new PackDrawTableImporter().ImportAsync(db, SeedDir);
+ int after = await db.PackDrawCardWeights.CountAsync();
+
+ Assert.That(after, Is.EqualTo(before));
+ }
+}