Story
This commit is contained in:
27
SVSim.Database/Entities/Story/SpecialBattleSetting.cs
Normal file
27
SVSim.Database/Entities/Story/SpecialBattleSetting.cs
Normal 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; }
|
||||
}
|
||||
10
SVSim.Database/Entities/Story/StoryApiType.cs
Normal file
10
SVSim.Database/Entities/Story/StoryApiType.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace SVSim.Database.Entities.Story;
|
||||
|
||||
public enum StoryApiType
|
||||
{
|
||||
None = 0,
|
||||
Main = 1,
|
||||
Limited = 2,
|
||||
Event = 3,
|
||||
AllStory = 4,
|
||||
}
|
||||
51
SVSim.Database/Entities/Story/StoryChapter.cs
Normal file
51
SVSim.Database/Entities/Story/StoryChapter.cs
Normal 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();
|
||||
}
|
||||
13
SVSim.Database/Entities/Story/StoryChapterBattleSetting.cs
Normal file
13
SVSim.Database/Entities/Story/StoryChapterBattleSetting.cs
Normal 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; }
|
||||
}
|
||||
9
SVSim.Database/Entities/Story/StoryChapterReward.cs
Normal file
9
SVSim.Database/Entities/Story/StoryChapterReward.cs
Normal 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; }
|
||||
}
|
||||
26
SVSim.Database/Entities/Story/StorySection.cs
Normal file
26
SVSim.Database/Entities/Story/StorySection.cs
Normal 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; }
|
||||
}
|
||||
9
SVSim.Database/Entities/Story/StorySubChapter.cs
Normal file
9
SVSim.Database/Entities/Story/StorySubChapter.cs
Normal 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; }
|
||||
}
|
||||
15
SVSim.Database/Entities/Story/StoryWorld.cs
Normal file
15
SVSim.Database/Entities/Story/StoryWorld.cs
Normal 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;
|
||||
}
|
||||
9
SVSim.Database/Entities/Story/ViewerStoryBranchUnlock.cs
Normal file
9
SVSim.Database/Entities/Story/ViewerStoryBranchUnlock.cs
Normal 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; }
|
||||
}
|
||||
13
SVSim.Database/Entities/Story/ViewerStoryProgress.cs
Normal file
13
SVSim.Database/Entities/Story/ViewerStoryProgress.cs
Normal 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; }
|
||||
}
|
||||
2570
SVSim.Database/Migrations/20260525163848_Story.Designer.cs
generated
Normal file
2570
SVSim.Database/Migrations/20260525163848_Story.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
283
SVSim.Database/Migrations/20260525163848_Story.cs
Normal file
283
SVSim.Database/Migrations/20260525163848_Story.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
9
SVSim.Database/Models/Config/StoryConfig.cs
Normal file
9
SVSim.Database/Models/Config/StoryConfig.cs
Normal 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();
|
||||
}
|
||||
19
SVSim.Database/Repositories/Story/IStoryMasterRepository.cs
Normal file
19
SVSim.Database/Repositories/Story/IStoryMasterRepository.cs
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
49
SVSim.Database/Repositories/Story/StoryMasterRepository.cs
Normal file
49
SVSim.Database/Repositories/Story/StoryMasterRepository.cs
Normal 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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user