Practice battles work

This commit is contained in:
gamer147
2026-05-23 22:46:11 -04:00
parent 704542786a
commit 21b97269ff
15 changed files with 34968 additions and 82 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SVSim.Database.Migrations
{
/// <inheritdoc />
public partial class PracticeOpponents : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PracticeOpponents",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
PracticeId = table.Column<int>(type: "integer", nullable: false),
TextId = table.Column<string>(type: "text", nullable: false),
ClassId = table.Column<int>(type: "integer", nullable: false),
CharaId = table.Column<int>(type: "integer", nullable: false),
DegreeId = table.Column<int>(type: "integer", nullable: false),
AiDeckLevel = table.Column<int>(type: "integer", nullable: false),
AiLogicLevel = table.Column<int>(type: "integer", nullable: false),
AiMaxLife = table.Column<int>(type: "integer", nullable: false),
Battle3dFieldId = table.Column<string>(type: "text", nullable: false),
IsMaintenance = table.Column<bool>(type: "boolean", nullable: false),
IsCampaignPractice = table.Column<bool>(type: "boolean", nullable: false),
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PracticeOpponents", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PracticeOpponents");
}
}
}

View File

@@ -25486,6 +25486,57 @@ namespace SVSim.Database.Migrations
b.ToTable("PaymentItems");
});
modelBuilder.Entity("SVSim.Database.Models.PracticeOpponentEntry", b =>
{
b.Property<int>("Id")
.HasColumnType("integer");
b.Property<int>("AiDeckLevel")
.HasColumnType("integer");
b.Property<int>("AiLogicLevel")
.HasColumnType("integer");
b.Property<int>("AiMaxLife")
.HasColumnType("integer");
b.Property<string>("Battle3dFieldId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CharaId")
.HasColumnType("integer");
b.Property<int>("ClassId")
.HasColumnType("integer");
b.Property<DateTime>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DateUpdated")
.HasColumnType("timestamp with time zone");
b.Property<int>("DegreeId")
.HasColumnType("integer");
b.Property<bool>("IsCampaignPractice")
.HasColumnType("boolean");
b.Property<bool>("IsMaintenance")
.HasColumnType("boolean");
b.Property<int>("PracticeId")
.HasColumnType("integer");
b.Property<string>("TextId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("PracticeOpponents");
});
modelBuilder.Entity("SVSim.Database.Models.PreReleaseInfo", b =>
{
b.Property<int>("Id")

View File

@@ -0,0 +1,49 @@
using SVSim.Database.Common;
namespace SVSim.Database.Models;
/// <summary>
/// One row per AI opponent shown on the practice (solo-play) opponent select screen.
/// Populated from prod /practice/info captures by SVSim.Bootstrap.GlobalsImporter.
///
/// The (<see cref="ClassId"/>, <see cref="AiDeckLevel"/>) pair MUST exist in the client's
/// baked-in master CSV `ai/practice_ai_setting`; if it doesn't, the client's
/// PracticeAISettingDataSet.GetSettingData throws InvalidOperationException and the
/// difficulty-select dialog crashes BEFORE /practice/start is sent. Prod's catalog is
/// the safe source of truth — we can't see the CSV directly.
/// </summary>
public class PracticeOpponentEntry : BaseEntity<int>
{
/// <summary>Practice slot id (Id = practice_id from the wire; also unique).</summary>
public int PracticeId { get => Id; set => Id = value; }
/// <summary>Text-table key resolved client-side via Data.Master.GetPracticeText.</summary>
public string TextId { get; set; } = string.Empty;
/// <summary>Class (leader) id the AI plays.</summary>
public int ClassId { get; set; }
/// <summary>Portrait / character id (leader art).</summary>
public int CharaId { get; set; }
/// <summary>Title-degree id shown next to the AI name. -1 when unset.</summary>
public int DegreeId { get; set; }
/// <summary>AI deck-strength tier; key into the client's practice_ai_setting CSV.</summary>
public int AiDeckLevel { get; set; }
/// <summary>AI decision-making tier.</summary>
public int AiLogicLevel { get; set; }
/// <summary>Starting HP for the AI side (typically 20; 10 for the easiest "tutorial" rows).</summary>
public int AiMaxLife { get; set; }
/// <summary>3D battlefield asset id (string on the wire; client int.TryParse's it).</summary>
public string Battle3dFieldId { get; set; } = "1";
/// <summary>true => entry shown but disabled with a maintenance suffix.</summary>
public bool IsMaintenance { get; set; }
/// <summary>true => entry is a special event-tied "campaign" practice.</summary>
public bool IsCampaignPractice { get; set; }
}

View File

@@ -111,4 +111,7 @@ public class GlobalsRepository : IGlobalsRepository
public Task<List<ShadowverseCardSetEntry>> GetRotationCardSets() =>
_dbContext.CardSets.AsNoTracking().Where(s => s.IsInRotation).ToListAsync();
public Task<List<PracticeOpponentEntry>> GetPracticeOpponents() =>
_dbContext.PracticeOpponents.AsNoTracking().OrderBy(e => e.ClassId).ThenBy(e => e.Id).ToListAsync();
}

View File

@@ -32,4 +32,5 @@ public interface IGlobalsRepository
Task<List<FeatureMaintenanceEntry>> GetFeatureMaintenances();
Task<PreReleaseInfo?> GetPreReleaseInfo();
Task<List<ShadowverseCardSetEntry>> GetRotationCardSets();
Task<List<PracticeOpponentEntry>> GetPracticeOpponents();
}

View File

@@ -59,6 +59,7 @@ public class SVSimDbContext : DbContext
public DbSet<MaintenanceCardEntry> MaintenanceCards => Set<MaintenanceCardEntry>();
public DbSet<FeatureMaintenanceEntry> FeatureMaintenances => Set<FeatureMaintenanceEntry>();
public DbSet<PreReleaseInfo> PreReleaseInfos => Set<PreReleaseInfo>();
public DbSet<PracticeOpponentEntry> PracticeOpponents => Set<PracticeOpponentEntry>();
#endregion