This commit is contained in:
gamer147
2026-05-25 14:36:12 -04:00
parent 558e8288eb
commit 5e7a65fe5a
54 changed files with 39633 additions and 29 deletions

View File

@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace SVSim.Database.Entities.Story;
public class SpecialBattleSetting
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public int PlayerFirstTurn { get; set; }
public int PlayerStartPp { get; set; }
public int EnemyStartPp { get; set; }
public int PlayerStartLife { get; set; }
public int EnemyStartLife { get; set; }
public string PlayerAttachSkill { get; set; } = string.Empty;
public string EnemyAttachSkill { get; set; } = string.Empty;
public string IdOverrideInBattleLog { get; set; } = string.Empty;
public string BanishEffectOverride { get; set; } = string.Empty;
public string TokenDrawEffectOverride { get; set; } = string.Empty;
public string SpecialTokenDrawEffectOverride { get; set; } = string.Empty;
public int ResultSkip { get; set; }
public int VsEffectOverride { get; set; }
public int ClassDestroyEffectOverride { get; set; }
public string? Note { get; set; }
}

View File

@@ -0,0 +1,10 @@
namespace SVSim.Database.Entities.Story;
public enum StoryApiType
{
None = 0,
Main = 1,
Limited = 2,
Event = 3,
AllStory = 4,
}

View File

@@ -0,0 +1,51 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace SVSim.Database.Entities.Story;
public class StoryChapter
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int StoryId { get; set; }
public int SectionId { get; set; }
public StorySection? Section { get; set; }
public int CharaId { get; set; }
public string ChapterId { get; set; } = string.Empty;
public string NextChapterId { get; set; } = string.Empty;
public string? RequiredChapterId { get; set; }
public string? SelectionDisplayPosition { get; set; }
public string? SelectionTextId { get; set; }
public decimal XCoordinate { get; set; }
public decimal YCoordinate { get; set; }
public int ShowCoordinate { get; set; }
public int IsCameraMovable { get; set; }
public int ShowSubtitles { get; set; }
public bool BattleExists { get; set; }
public int EnemyCharaId { get; set; }
public int EnemyClass { get; set; }
public int EnemyAiId { get; set; }
public string BgFileName { get; set; } = string.Empty;
public string? ChapterEffectPath { get; set; }
public string? ChapterClearTextId { get; set; }
public int Battle3dFieldId { get; set; }
public string BgmId { get; set; } = string.Empty;
public int? SpecialBattleSettingId { get; set; }
public SpecialBattleSetting? SpecialBattleSetting { get; set; }
public int ReleasePoint { get; set; }
public bool IsMaintenanceChapter { get; set; }
public bool IsPlayAnotherEndAppearanceAnimation { get; set; }
public bool IsReleasedAnotherEnd { get; set; }
public bool IsSkipEnabled { get; set; }
// Owned collections — populated via .OwnsMany() in DbContext.
public List<StoryChapterBattleSetting> BattleSettings { get; set; } = new();
public List<StoryChapterReward> Rewards { get; set; } = new();
public List<StorySubChapter> SubChapters { get; set; } = new();
}

View File

@@ -0,0 +1,13 @@
namespace SVSim.Database.Entities.Story;
[Microsoft.EntityFrameworkCore.Owned]
public class StoryChapterBattleSetting
{
public int DeckClassId { get; set; }
public int PlayerEmotionOverride { get; set; }
public int EnemyEmotionOverride { get; set; }
public int SkinIdOverride { get; set; }
public int Battle3dFieldIdOverride { get; set; }
public int BgmIdOverride { get; set; }
public int DeckSkinIdOverride { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace SVSim.Database.Entities.Story;
[Microsoft.EntityFrameworkCore.Owned]
public class StoryChapterReward
{
public int RewardType { get; set; }
public long RewardDetailId { get; set; }
public int RewardNumber { get; set; }
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace SVSim.Database.Entities.Story;
public class StorySection
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public int? WorldId { get; set; }
public StoryWorld? World { get; set; }
public StoryApiType StoryApiType { get; set; }
public int OrderId { get; set; }
public int AllStoryOrderId { get; set; }
public string NameTextKey { get; set; } = string.Empty;
public string ImageName { get; set; } = string.Empty;
public bool IsLeaderSelect { get; set; }
public int BackGroundId { get; set; }
public int ChapterSelectType { get; set; }
public int StoryTypeOverwrite { get; set; }
public bool IsUnderMaintenance { get; set; }
public bool IsPlayAnotherEndAppearanceAnimation { get; set; }
}

View File

@@ -0,0 +1,9 @@
namespace SVSim.Database.Entities.Story;
[Microsoft.EntityFrameworkCore.Owned]
public class StorySubChapter
{
public int SubChapterId { get; set; }
public int SubChapterStoryId { get; set; }
public bool IsMaintenanceChapter { get; set; }
}

View File

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace SVSim.Database.Entities.Story;
public class StoryWorld
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string TitleTextKey { get; set; } = string.Empty;
public string PanelImageName { get; set; } = string.Empty;
public string RibbonText { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,9 @@
namespace SVSim.Database.Entities.Story;
// Composite PK (ViewerId, StoryId) — StoryId here is the BRANCH CHILD that was unlocked.
public class ViewerStoryBranchUnlock
{
public long ViewerId { get; set; }
public int StoryId { get; set; }
public DateTime UnlockedAt { get; set; }
}

View File

@@ -0,0 +1,13 @@
namespace SVSim.Database.Entities.Story;
// Composite PK (ViewerId, StoryId) configured via fluent API in SVSimDbContext.
public class ViewerStoryProgress
{
public long ViewerId { get; set; }
public int StoryId { get; set; }
public bool IsFinish { get; set; }
public bool IsSkipped { get; set; }
public DateTime? FinishedAt { get; set; }
public DateTime? SkippedAt { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace SVSim.Database.Migrations
{
/// <inheritdoc />
public partial class Story : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SpecialBattleSettings",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
PlayerFirstTurn = table.Column<int>(type: "integer", nullable: false),
PlayerStartPp = table.Column<int>(type: "integer", nullable: false),
EnemyStartPp = table.Column<int>(type: "integer", nullable: false),
PlayerStartLife = table.Column<int>(type: "integer", nullable: false),
EnemyStartLife = table.Column<int>(type: "integer", nullable: false),
PlayerAttachSkill = table.Column<string>(type: "text", nullable: false),
EnemyAttachSkill = table.Column<string>(type: "text", nullable: false),
IdOverrideInBattleLog = table.Column<string>(type: "text", nullable: false),
BanishEffectOverride = table.Column<string>(type: "text", nullable: false),
TokenDrawEffectOverride = table.Column<string>(type: "text", nullable: false),
SpecialTokenDrawEffectOverride = table.Column<string>(type: "text", nullable: false),
ResultSkip = table.Column<int>(type: "integer", nullable: false),
VsEffectOverride = table.Column<int>(type: "integer", nullable: false),
ClassDestroyEffectOverride = table.Column<int>(type: "integer", nullable: false),
Note = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SpecialBattleSettings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "StoryWorlds",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
TitleTextKey = table.Column<string>(type: "text", nullable: false),
PanelImageName = table.Column<string>(type: "text", nullable: false),
RibbonText = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StoryWorlds", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ViewerStoryBranchUnlocks",
columns: table => new
{
ViewerId = table.Column<long>(type: "bigint", nullable: false),
StoryId = table.Column<int>(type: "integer", nullable: false),
UnlockedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ViewerStoryBranchUnlocks", x => new { x.ViewerId, x.StoryId });
});
migrationBuilder.CreateTable(
name: "ViewerStoryProgress",
columns: table => new
{
ViewerId = table.Column<long>(type: "bigint", nullable: false),
StoryId = table.Column<int>(type: "integer", nullable: false),
IsFinish = table.Column<bool>(type: "boolean", nullable: false),
IsSkipped = table.Column<bool>(type: "boolean", nullable: false),
FinishedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
SkippedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ViewerStoryProgress", x => new { x.ViewerId, x.StoryId });
});
migrationBuilder.CreateTable(
name: "StorySections",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false),
WorldId = table.Column<int>(type: "integer", nullable: true),
StoryApiType = table.Column<int>(type: "integer", nullable: false),
OrderId = table.Column<int>(type: "integer", nullable: false),
AllStoryOrderId = table.Column<int>(type: "integer", nullable: false),
NameTextKey = table.Column<string>(type: "text", nullable: false),
ImageName = table.Column<string>(type: "text", nullable: false),
IsLeaderSelect = table.Column<bool>(type: "boolean", nullable: false),
BackGroundId = table.Column<int>(type: "integer", nullable: false),
ChapterSelectType = table.Column<int>(type: "integer", nullable: false),
StoryTypeOverwrite = table.Column<int>(type: "integer", nullable: false),
IsUnderMaintenance = table.Column<bool>(type: "boolean", nullable: false),
IsPlayAnotherEndAppearanceAnimation = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StorySections", x => x.Id);
table.ForeignKey(
name: "FK_StorySections_StoryWorlds_WorldId",
column: x => x.WorldId,
principalTable: "StoryWorlds",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "StoryChapters",
columns: table => new
{
StoryId = table.Column<int>(type: "integer", nullable: false),
SectionId = table.Column<int>(type: "integer", nullable: false),
CharaId = table.Column<int>(type: "integer", nullable: false),
ChapterId = table.Column<string>(type: "text", nullable: false),
NextChapterId = table.Column<string>(type: "text", nullable: false),
RequiredChapterId = table.Column<string>(type: "text", nullable: true),
SelectionDisplayPosition = table.Column<string>(type: "text", nullable: true),
SelectionTextId = table.Column<string>(type: "text", nullable: true),
XCoordinate = table.Column<decimal>(type: "numeric", nullable: false),
YCoordinate = table.Column<decimal>(type: "numeric", nullable: false),
ShowCoordinate = table.Column<int>(type: "integer", nullable: false),
IsCameraMovable = table.Column<int>(type: "integer", nullable: false),
ShowSubtitles = table.Column<int>(type: "integer", nullable: false),
BattleExists = table.Column<bool>(type: "boolean", nullable: false),
EnemyCharaId = table.Column<int>(type: "integer", nullable: false),
EnemyClass = table.Column<int>(type: "integer", nullable: false),
EnemyAiId = table.Column<int>(type: "integer", nullable: false),
BgFileName = table.Column<string>(type: "text", nullable: false),
ChapterEffectPath = table.Column<string>(type: "text", nullable: true),
ChapterClearTextId = table.Column<string>(type: "text", nullable: true),
Battle3dFieldId = table.Column<int>(type: "integer", nullable: false),
BgmId = table.Column<string>(type: "text", nullable: false),
SpecialBattleSettingId = table.Column<int>(type: "integer", nullable: true),
ReleasePoint = table.Column<int>(type: "integer", nullable: false),
IsMaintenanceChapter = table.Column<bool>(type: "boolean", nullable: false),
IsPlayAnotherEndAppearanceAnimation = table.Column<bool>(type: "boolean", nullable: false),
IsReleasedAnotherEnd = table.Column<bool>(type: "boolean", nullable: false),
IsSkipEnabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StoryChapters", x => x.StoryId);
table.ForeignKey(
name: "FK_StoryChapters_SpecialBattleSettings_SpecialBattleSettingId",
column: x => x.SpecialBattleSettingId,
principalTable: "SpecialBattleSettings",
principalColumn: "Id");
table.ForeignKey(
name: "FK_StoryChapters_StorySections_SectionId",
column: x => x.SectionId,
principalTable: "StorySections",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "StoryChapterBattleSetting",
columns: table => new
{
StoryId = table.Column<int>(type: "integer", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
DeckClassId = table.Column<int>(type: "integer", nullable: false),
PlayerEmotionOverride = table.Column<int>(type: "integer", nullable: false),
EnemyEmotionOverride = table.Column<int>(type: "integer", nullable: false),
SkinIdOverride = table.Column<int>(type: "integer", nullable: false),
Battle3dFieldIdOverride = table.Column<int>(type: "integer", nullable: false),
BgmIdOverride = table.Column<int>(type: "integer", nullable: false),
DeckSkinIdOverride = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StoryChapterBattleSetting", x => new { x.StoryId, x.Id });
table.ForeignKey(
name: "FK_StoryChapterBattleSetting_StoryChapters_StoryId",
column: x => x.StoryId,
principalTable: "StoryChapters",
principalColumn: "StoryId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "StoryChapterReward",
columns: table => new
{
StoryId = table.Column<int>(type: "integer", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
RewardType = table.Column<int>(type: "integer", nullable: false),
RewardDetailId = table.Column<long>(type: "bigint", nullable: false),
RewardNumber = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StoryChapterReward", x => new { x.StoryId, x.Id });
table.ForeignKey(
name: "FK_StoryChapterReward_StoryChapters_StoryId",
column: x => x.StoryId,
principalTable: "StoryChapters",
principalColumn: "StoryId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "StorySubChapter",
columns: table => new
{
StoryId = table.Column<int>(type: "integer", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
SubChapterId = table.Column<int>(type: "integer", nullable: false),
SubChapterStoryId = table.Column<int>(type: "integer", nullable: false),
IsMaintenanceChapter = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StorySubChapter", x => new { x.StoryId, x.Id });
table.ForeignKey(
name: "FK_StorySubChapter_StoryChapters_StoryId",
column: x => x.StoryId,
principalTable: "StoryChapters",
principalColumn: "StoryId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_StoryChapters_NextChapterId",
table: "StoryChapters",
column: "NextChapterId");
migrationBuilder.CreateIndex(
name: "IX_StoryChapters_SectionId_CharaId_ChapterId",
table: "StoryChapters",
columns: new[] { "SectionId", "CharaId", "ChapterId" });
migrationBuilder.CreateIndex(
name: "IX_StoryChapters_SpecialBattleSettingId",
table: "StoryChapters",
column: "SpecialBattleSettingId");
migrationBuilder.CreateIndex(
name: "IX_StorySections_WorldId",
table: "StorySections",
column: "WorldId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StoryChapterBattleSetting");
migrationBuilder.DropTable(
name: "StoryChapterReward");
migrationBuilder.DropTable(
name: "StorySubChapter");
migrationBuilder.DropTable(
name: "ViewerStoryBranchUnlocks");
migrationBuilder.DropTable(
name: "ViewerStoryProgress");
migrationBuilder.DropTable(
name: "StoryChapters");
migrationBuilder.DropTable(
name: "SpecialBattleSettings");
migrationBuilder.DropTable(
name: "StorySections");
migrationBuilder.DropTable(
name: "StoryWorlds");
}
}
}

View File

@@ -85,6 +85,281 @@ namespace SVSim.Database.Migrations
b.ToTable("MyPageBackgroundEntryViewer");
});
modelBuilder.Entity("SVSim.Database.Entities.Story.SpecialBattleSetting", b =>
{
b.Property<int>("Id")
.HasColumnType("integer");
b.Property<string>("BanishEffectOverride")
.IsRequired()
.HasColumnType("text");
b.Property<int>("ClassDestroyEffectOverride")
.HasColumnType("integer");
b.Property<string>("EnemyAttachSkill")
.IsRequired()
.HasColumnType("text");
b.Property<int>("EnemyStartLife")
.HasColumnType("integer");
b.Property<int>("EnemyStartPp")
.HasColumnType("integer");
b.Property<string>("IdOverrideInBattleLog")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Note")
.HasColumnType("text");
b.Property<string>("PlayerAttachSkill")
.IsRequired()
.HasColumnType("text");
b.Property<int>("PlayerFirstTurn")
.HasColumnType("integer");
b.Property<int>("PlayerStartLife")
.HasColumnType("integer");
b.Property<int>("PlayerStartPp")
.HasColumnType("integer");
b.Property<int>("ResultSkip")
.HasColumnType("integer");
b.Property<string>("SpecialTokenDrawEffectOverride")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TokenDrawEffectOverride")
.IsRequired()
.HasColumnType("text");
b.Property<int>("VsEffectOverride")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("SpecialBattleSettings");
});
modelBuilder.Entity("SVSim.Database.Entities.Story.StoryChapter", b =>
{
b.Property<int>("StoryId")
.HasColumnType("integer");
b.Property<int>("Battle3dFieldId")
.HasColumnType("integer");
b.Property<bool>("BattleExists")
.HasColumnType("boolean");
b.Property<string>("BgFileName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("BgmId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ChapterClearTextId")
.HasColumnType("text");
b.Property<string>("ChapterEffectPath")
.HasColumnType("text");
b.Property<string>("ChapterId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CharaId")
.HasColumnType("integer");
b.Property<int>("EnemyAiId")
.HasColumnType("integer");
b.Property<int>("EnemyCharaId")
.HasColumnType("integer");
b.Property<int>("EnemyClass")
.HasColumnType("integer");
b.Property<int>("IsCameraMovable")
.HasColumnType("integer");
b.Property<bool>("IsMaintenanceChapter")
.HasColumnType("boolean");
b.Property<bool>("IsPlayAnotherEndAppearanceAnimation")
.HasColumnType("boolean");
b.Property<bool>("IsReleasedAnotherEnd")
.HasColumnType("boolean");
b.Property<bool>("IsSkipEnabled")
.HasColumnType("boolean");
b.Property<string>("NextChapterId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("ReleasePoint")
.HasColumnType("integer");
b.Property<string>("RequiredChapterId")
.HasColumnType("text");
b.Property<int>("SectionId")
.HasColumnType("integer");
b.Property<string>("SelectionDisplayPosition")
.HasColumnType("text");
b.Property<string>("SelectionTextId")
.HasColumnType("text");
b.Property<int>("ShowCoordinate")
.HasColumnType("integer");
b.Property<int>("ShowSubtitles")
.HasColumnType("integer");
b.Property<int?>("SpecialBattleSettingId")
.HasColumnType("integer");
b.Property<decimal>("XCoordinate")
.HasColumnType("numeric");
b.Property<decimal>("YCoordinate")
.HasColumnType("numeric");
b.HasKey("StoryId");
b.HasIndex("NextChapterId");
b.HasIndex("SpecialBattleSettingId");
b.HasIndex("SectionId", "CharaId", "ChapterId");
b.ToTable("StoryChapters");
});
modelBuilder.Entity("SVSim.Database.Entities.Story.StorySection", b =>
{
b.Property<int>("Id")
.HasColumnType("integer");
b.Property<int>("AllStoryOrderId")
.HasColumnType("integer");
b.Property<int>("BackGroundId")
.HasColumnType("integer");
b.Property<int>("ChapterSelectType")
.HasColumnType("integer");
b.Property<string>("ImageName")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsLeaderSelect")
.HasColumnType("boolean");
b.Property<bool>("IsPlayAnotherEndAppearanceAnimation")
.HasColumnType("boolean");
b.Property<bool>("IsUnderMaintenance")
.HasColumnType("boolean");
b.Property<string>("NameTextKey")
.IsRequired()
.HasColumnType("text");
b.Property<int>("OrderId")
.HasColumnType("integer");
b.Property<int>("StoryApiType")
.HasColumnType("integer");
b.Property<int>("StoryTypeOverwrite")
.HasColumnType("integer");
b.Property<int?>("WorldId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("WorldId");
b.ToTable("StorySections");
});
modelBuilder.Entity("SVSim.Database.Entities.Story.StoryWorld", b =>
{
b.Property<int>("Id")
.HasColumnType("integer");
b.Property<string>("PanelImageName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RibbonText")
.IsRequired()
.HasColumnType("text");
b.Property<string>("TitleTextKey")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("StoryWorlds");
});
modelBuilder.Entity("SVSim.Database.Entities.Story.ViewerStoryBranchUnlock", b =>
{
b.Property<long>("ViewerId")
.HasColumnType("bigint");
b.Property<int>("StoryId")
.HasColumnType("integer");
b.Property<DateTime>("UnlockedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("ViewerId", "StoryId");
b.ToTable("ViewerStoryBranchUnlocks");
});
modelBuilder.Entity("SVSim.Database.Entities.Story.ViewerStoryProgress", b =>
{
b.Property<long>("ViewerId")
.HasColumnType("bigint");
b.Property<int>("StoryId")
.HasColumnType("integer");
b.Property<DateTime?>("FinishedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsFinish")
.HasColumnType("boolean");
b.Property<bool>("IsSkipped")
.HasColumnType("boolean");
b.Property<DateTime?>("SkippedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("ViewerId", "StoryId");
b.ToTable("ViewerStoryProgress");
});
modelBuilder.Entity("SVSim.Database.Models.ArenaSeasonConfig", b =>
{
b.Property<int>("Id")
@@ -1562,6 +1837,134 @@ namespace SVSim.Database.Migrations
.IsRequired();
});
modelBuilder.Entity("SVSim.Database.Entities.Story.StoryChapter", b =>
{
b.HasOne("SVSim.Database.Entities.Story.StorySection", "Section")
.WithMany()
.HasForeignKey("SectionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SVSim.Database.Entities.Story.SpecialBattleSetting", "SpecialBattleSetting")
.WithMany()
.HasForeignKey("SpecialBattleSettingId");
b.OwnsMany("SVSim.Database.Entities.Story.StoryChapterBattleSetting", "BattleSettings", b1 =>
{
b1.Property<int>("StoryId")
.HasColumnType("integer");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<int>("Battle3dFieldIdOverride")
.HasColumnType("integer");
b1.Property<int>("BgmIdOverride")
.HasColumnType("integer");
b1.Property<int>("DeckClassId")
.HasColumnType("integer");
b1.Property<int>("DeckSkinIdOverride")
.HasColumnType("integer");
b1.Property<int>("EnemyEmotionOverride")
.HasColumnType("integer");
b1.Property<int>("PlayerEmotionOverride")
.HasColumnType("integer");
b1.Property<int>("SkinIdOverride")
.HasColumnType("integer");
b1.HasKey("StoryId", "Id");
b1.ToTable("StoryChapterBattleSetting");
b1.WithOwner()
.HasForeignKey("StoryId");
});
b.OwnsMany("SVSim.Database.Entities.Story.StoryChapterReward", "Rewards", b1 =>
{
b1.Property<int>("StoryId")
.HasColumnType("integer");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<long>("RewardDetailId")
.HasColumnType("bigint");
b1.Property<int>("RewardNumber")
.HasColumnType("integer");
b1.Property<int>("RewardType")
.HasColumnType("integer");
b1.HasKey("StoryId", "Id");
b1.ToTable("StoryChapterReward");
b1.WithOwner()
.HasForeignKey("StoryId");
});
b.OwnsMany("SVSim.Database.Entities.Story.StorySubChapter", "SubChapters", b1 =>
{
b1.Property<int>("StoryId")
.HasColumnType("integer");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<bool>("IsMaintenanceChapter")
.HasColumnType("boolean");
b1.Property<int>("SubChapterId")
.HasColumnType("integer");
b1.Property<int>("SubChapterStoryId")
.HasColumnType("integer");
b1.HasKey("StoryId", "Id");
b1.ToTable("StorySubChapter");
b1.WithOwner()
.HasForeignKey("StoryId");
});
b.Navigation("BattleSettings");
b.Navigation("Rewards");
b.Navigation("Section");
b.Navigation("SpecialBattleSetting");
b.Navigation("SubChapters");
});
modelBuilder.Entity("SVSim.Database.Entities.Story.StorySection", b =>
{
b.HasOne("SVSim.Database.Entities.Story.StoryWorld", "World")
.WithMany()
.HasForeignKey("WorldId");
b.Navigation("World");
});
modelBuilder.Entity("SVSim.Database.Models.CardCosmeticReward", b =>
{
b.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card")

View File

@@ -0,0 +1,9 @@
namespace SVSim.Database.Models.Config;
[ConfigSection("Story")]
public class StoryConfig
{
public int ClassXpPerClear { get; set; } = 200;
public static StoryConfig ShippedDefaults() => new();
}

View File

@@ -0,0 +1,19 @@
using SVSim.Database.Entities.Story;
namespace SVSim.Database.Repositories.Story;
public interface IStoryMasterRepository
{
Task<List<StorySection>> GetSectionsByFamilyAsync(StoryApiType apiType);
Task<List<StoryWorld>> GetWorldsForSectionsAsync(IEnumerable<int> worldIds);
Task<List<StoryChapter>> GetChaptersBySectionCharaAsync(int sectionId, int charaId);
/// <summary>
/// Bulk-load chapter scalars (no owned collections) across multiple sections in one round-trip.
/// Used by the section rollup to avoid N+1 per (section, chara) lookups.
/// </summary>
Task<List<StoryChapter>> GetChaptersBySectionsAsync(IEnumerable<int> sectionIds);
Task<StoryChapter?> GetChapterByIdAsync(int storyId);
Task<SpecialBattleSetting?> GetSbsByIdAsync(int sbsId);
}

View File

@@ -0,0 +1,12 @@
using SVSim.Database.Entities.Story;
namespace SVSim.Database.Repositories.Story;
public interface IViewerStoryProgressRepository
{
Task<Dictionary<int, ViewerStoryProgress>> GetProgressForChaptersAsync(long viewerId, IEnumerable<int> storyIds);
Task<HashSet<int>> GetBranchUnlockedStoryIdsAsync(long viewerId, IEnumerable<int> storyIds);
Task UpsertProgressAsync(long viewerId, int storyId, bool? isFinish, bool? isSkipped);
Task UpsertBranchUnlockAsync(long viewerId, int storyId);
}

View File

@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Entities.Story;
namespace SVSim.Database.Repositories.Story;
public class StoryMasterRepository : IStoryMasterRepository
{
private readonly SVSimDbContext _db;
public StoryMasterRepository(SVSimDbContext db) { _db = db; }
public Task<List<StorySection>> GetSectionsByFamilyAsync(StoryApiType apiType)
{
var families = apiType == StoryApiType.AllStory
? new[] { StoryApiType.Main } // AllStory effectively returns Main per spec
: new[] { apiType };
return _db.StorySections.Where(s => families.Contains(s.StoryApiType))
.OrderBy(s => s.AllStoryOrderId)
.ToListAsync();
}
public Task<List<StoryWorld>> GetWorldsForSectionsAsync(IEnumerable<int> worldIds)
=> _db.StoryWorlds.Where(w => worldIds.Contains(w.Id)).ToListAsync();
public Task<List<StoryChapter>> GetChaptersBySectionCharaAsync(int sectionId, int charaId)
=> _db.StoryChapters
.Include(c => c.BattleSettings).Include(c => c.Rewards).Include(c => c.SubChapters)
.Where(c => c.SectionId == sectionId && c.CharaId == charaId)
.ToListAsync();
// No Includes — the rollup only reads SectionId/CharaId/StoryId. Including the three owned
// collections here would cartesian-explode across ~677 chapters and turn a single query into
// a multi-MB result set.
public Task<List<StoryChapter>> GetChaptersBySectionsAsync(IEnumerable<int> sectionIds)
{
var ids = sectionIds.ToList();
return _db.StoryChapters
.AsNoTracking()
.Where(c => ids.Contains(c.SectionId))
.ToListAsync();
}
public Task<StoryChapter?> GetChapterByIdAsync(int storyId)
=> _db.StoryChapters
.Include(c => c.BattleSettings).Include(c => c.Rewards).Include(c => c.SubChapters)
.FirstOrDefaultAsync(c => c.StoryId == storyId);
public Task<SpecialBattleSetting?> GetSbsByIdAsync(int sbsId)
=> _db.SpecialBattleSettings.FirstOrDefaultAsync(s => s.Id == sbsId);
}

View File

@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Entities.Story;
namespace SVSim.Database.Repositories.Story;
public class ViewerStoryProgressRepository : IViewerStoryProgressRepository
{
private readonly SVSimDbContext _db;
public ViewerStoryProgressRepository(SVSimDbContext db) { _db = db; }
public async Task<Dictionary<int, ViewerStoryProgress>> GetProgressForChaptersAsync(
long viewerId, IEnumerable<int> storyIds)
{
var ids = storyIds.ToList();
var rows = await _db.ViewerStoryProgress
.Where(p => p.ViewerId == viewerId && ids.Contains(p.StoryId))
.ToListAsync();
return rows.ToDictionary(r => r.StoryId);
}
public async Task<HashSet<int>> GetBranchUnlockedStoryIdsAsync(long viewerId, IEnumerable<int> storyIds)
{
var ids = storyIds.ToList();
var rows = await _db.ViewerStoryBranchUnlocks
.Where(u => u.ViewerId == viewerId && ids.Contains(u.StoryId))
.Select(u => u.StoryId)
.ToListAsync();
return new HashSet<int>(rows);
}
public async Task UpsertProgressAsync(long viewerId, int storyId, bool? isFinish, bool? isSkipped)
{
var row = await _db.ViewerStoryProgress.FirstOrDefaultAsync(
p => p.ViewerId == viewerId && p.StoryId == storyId);
if (row is null)
{
row = new ViewerStoryProgress { ViewerId = viewerId, StoryId = storyId };
_db.ViewerStoryProgress.Add(row);
}
if (isFinish.HasValue) { row.IsFinish = isFinish.Value; if (isFinish.Value) row.FinishedAt = DateTime.UtcNow; }
if (isSkipped.HasValue) { row.IsSkipped = isSkipped.Value; if (isSkipped.Value) row.SkippedAt = DateTime.UtcNow; }
await _db.SaveChangesAsync();
}
public async Task UpsertBranchUnlockAsync(long viewerId, int storyId)
{
bool exists = await _db.ViewerStoryBranchUnlocks
.AnyAsync(u => u.ViewerId == viewerId && u.StoryId == storyId);
if (!exists)
{
_db.ViewerStoryBranchUnlocks.Add(new ViewerStoryBranchUnlock
{ ViewerId = viewerId, StoryId = storyId, UnlockedAt = DateTime.UtcNow });
await _db.SaveChangesAsync();
}
}
}

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SVSim.Database.Common;
using SVSim.Database.Entities.Story;
using SVSim.Database.Models;
using SVSim.Database.Models.Config;
@@ -67,6 +68,14 @@ public class SVSimDbContext : DbContext
public DbSet<PuzzleMissionEntry> PuzzleMissions => Set<PuzzleMissionEntry>();
public DbSet<ViewerPuzzleClear> ViewerPuzzleClears => Set<ViewerPuzzleClear>();
// Story reference data + viewer progress
public DbSet<StoryWorld> StoryWorlds => Set<StoryWorld>();
public DbSet<StorySection> StorySections => Set<StorySection>();
public DbSet<StoryChapter> StoryChapters => Set<StoryChapter>();
public DbSet<SpecialBattleSetting> SpecialBattleSettings => Set<SpecialBattleSetting>();
public DbSet<ViewerStoryProgress> ViewerStoryProgress => Set<ViewerStoryProgress>();
public DbSet<ViewerStoryBranchUnlock> ViewerStoryBranchUnlocks => Set<ViewerStoryBranchUnlock>();
#endregion
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
@@ -142,6 +151,29 @@ public class SVSimDbContext : DbContext
.HasColumnType("jsonb");
}
// --- Story entities ---
// Composite PKs for viewer-state tables
modelBuilder.Entity<ViewerStoryProgress>().HasKey(x => new { x.ViewerId, x.StoryId });
modelBuilder.Entity<ViewerStoryBranchUnlock>().HasKey(x => new { x.ViewerId, x.StoryId });
// StoryChapter owned collections (shadow-PK per row)
modelBuilder.Entity<StoryChapter>(c =>
{
c.OwnsMany(x => x.BattleSettings, b => { b.WithOwner().HasForeignKey("StoryId"); b.Property<int>("Id"); b.HasKey("StoryId", "Id"); });
c.OwnsMany(x => x.Rewards, b => { b.WithOwner().HasForeignKey("StoryId"); b.Property<int>("Id"); b.HasKey("StoryId", "Id"); });
c.OwnsMany(x => x.SubChapters, b => { b.WithOwner().HasForeignKey("StoryId"); b.Property<int>("Id"); b.HasKey("StoryId", "Id"); });
});
// FK relationships
modelBuilder.Entity<StorySection>().HasOne(s => s.World).WithMany().HasForeignKey(s => s.WorldId);
modelBuilder.Entity<StoryChapter>().HasOne(c => c.Section).WithMany().HasForeignKey(c => c.SectionId);
modelBuilder.Entity<StoryChapter>().HasOne(c => c.SpecialBattleSetting).WithMany().HasForeignKey(c => c.SpecialBattleSettingId);
// Indexes
modelBuilder.Entity<StoryChapter>().HasIndex(c => new { c.SectionId, c.CharaId, c.ChapterId });
modelBuilder.Entity<StoryChapter>().HasIndex(c => c.NextChapterId);
base.OnModelCreating(modelBuilder);
}