159 lines
6.4 KiB
C#
159 lines
6.4 KiB
C#
using PlotLine.Models;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public sealed class CharacterAgeContinuityScene
|
|
{
|
|
public int SceneID { get; init; }
|
|
public int ProjectID { get; init; }
|
|
public int? BookID { get; init; }
|
|
public int? ChapterID { get; init; }
|
|
public decimal? ChapterNumber { get; init; }
|
|
public string? ChapterTitle { get; init; }
|
|
public decimal? SceneNumber { get; init; }
|
|
public string SceneTitle { get; init; } = string.Empty;
|
|
public int ChapterSortOrder { get; init; }
|
|
public int SceneSortOrder { get; init; }
|
|
public string TimeModeName { get; init; } = string.Empty;
|
|
public DateTime? StartDateTime { get; init; }
|
|
}
|
|
|
|
public static class CharacterAgeContinuityService
|
|
{
|
|
public static IReadOnlyList<ContinuityWarning> GetWarningsForScene(
|
|
CharacterAgeContinuityScene currentScene,
|
|
IReadOnlyList<CharacterAgeContinuityScene> scenes,
|
|
IReadOnlyList<SceneCharacter> appearances,
|
|
IReadOnlyList<SceneCharacter> currentSceneCharacters)
|
|
{
|
|
var warnings = new List<ContinuityWarning>();
|
|
warnings.AddRange(GetApproximateAgeMismatchWarnings(currentScene, currentSceneCharacters));
|
|
warnings.AddRange(GetBirthdayTransitionWarnings(currentScene, scenes, appearances));
|
|
return warnings;
|
|
}
|
|
|
|
public static IReadOnlyList<ContinuityWarning> GetApproximateAgeMismatchWarnings(
|
|
CharacterAgeContinuityScene scene,
|
|
IReadOnlyList<SceneCharacter> sceneCharacters)
|
|
{
|
|
var sceneDate = CharacterAgeDisplayService.ResolveEffectiveSceneDate(scene.TimeModeName, scene.StartDateTime);
|
|
if (!sceneDate.HasValue)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return sceneCharacters
|
|
.Where(character => character.BirthDate.HasValue && character.AgeAtSeriesStart.HasValue)
|
|
.Select(character => new
|
|
{
|
|
Character = character,
|
|
ExactAge = CharacterAgeDisplayService.CalculateExactAge(character.BirthDate!.Value.Date, sceneDate.Value.Date)
|
|
})
|
|
.Where(item => Math.Abs(item.Character.AgeAtSeriesStart!.Value - item.ExactAge.Years) > 1)
|
|
.Select(item => CreateWarning(
|
|
scene,
|
|
item.Character.CharacterID,
|
|
"Warning",
|
|
"Character Age",
|
|
"Character profile age differs significantly from calculated age.",
|
|
$"Profile states approximately {item.Character.AgeAtSeriesStart} years old. Calculated age in this scene is {CharacterAgeDisplayService.FormatExactAge(item.Character.BirthDate!.Value.Date, sceneDate.Value.Date)}."))
|
|
.ToList();
|
|
}
|
|
|
|
public static IReadOnlyList<ContinuityWarning> GetBirthdayTransitionWarnings(
|
|
CharacterAgeContinuityScene currentScene,
|
|
IReadOnlyList<CharacterAgeContinuityScene> scenes,
|
|
IReadOnlyList<SceneCharacter> appearances)
|
|
{
|
|
var orderedScenes = scenes
|
|
.Select(scene => new
|
|
{
|
|
Scene = scene,
|
|
EffectiveDate = CharacterAgeDisplayService.ResolveEffectiveSceneDate(scene.TimeModeName, scene.StartDateTime)
|
|
})
|
|
.Where(scene => scene.EffectiveDate.HasValue)
|
|
.OrderBy(scene => scene.EffectiveDate!.Value)
|
|
.ThenBy(scene => scene.Scene.ChapterSortOrder)
|
|
.ThenBy(scene => scene.Scene.SceneSortOrder)
|
|
.ThenBy(scene => scene.Scene.SceneID)
|
|
.ToList();
|
|
|
|
var currentIndex = orderedScenes.FindIndex(scene => scene.Scene.SceneID == currentScene.SceneID);
|
|
if (currentIndex < 0 || currentIndex == orderedScenes.Count - 1)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var current = orderedScenes[currentIndex];
|
|
var next = orderedScenes[currentIndex + 1];
|
|
if (next.EffectiveDate!.Value <= current.EffectiveDate!.Value)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var nextSceneCharacterIds = appearances
|
|
.Where(appearance => appearance.SceneID == next.Scene.SceneID)
|
|
.Select(appearance => appearance.CharacterID)
|
|
.ToHashSet();
|
|
|
|
return appearances
|
|
.Where(appearance => appearance.SceneID == current.Scene.SceneID
|
|
&& appearance.BirthDate.HasValue
|
|
&& nextSceneCharacterIds.Contains(appearance.CharacterID))
|
|
.GroupBy(appearance => appearance.CharacterID)
|
|
.Select(group => group.First())
|
|
.Select(appearance => new
|
|
{
|
|
Appearance = appearance,
|
|
Birthday = GetNextBirthdayAfter(appearance.BirthDate!.Value.Date, current.EffectiveDate!.Value.Date)
|
|
})
|
|
.Where(item => item.Birthday <= next.EffectiveDate!.Value.Date)
|
|
.Select(item =>
|
|
{
|
|
var age = CharacterAgeDisplayService.CalculateExactAge(item.Appearance.BirthDate!.Value.Date, item.Birthday).Years;
|
|
return CreateWarning(
|
|
current.Scene,
|
|
item.Appearance.CharacterID,
|
|
"Info",
|
|
"Character Age",
|
|
$"{item.Appearance.CharacterName} turns {age} between this scene and the next.",
|
|
$"Birthday falls on {item.Birthday:dd MMM yyyy}. Next scene: {next.Scene.SceneTitle}.");
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
private static DateTime GetNextBirthdayAfter(DateTime birthDate, DateTime sceneDate)
|
|
{
|
|
var birthday = new DateTime(sceneDate.Year, birthDate.Month, birthDate.Day);
|
|
return birthday > sceneDate ? birthday : birthday.AddYears(1);
|
|
}
|
|
|
|
private static ContinuityWarning CreateWarning(
|
|
CharacterAgeContinuityScene scene,
|
|
int? characterId,
|
|
string severityName,
|
|
string warningTypeName,
|
|
string message,
|
|
string details) =>
|
|
new()
|
|
{
|
|
ContinuityWarningID = 0,
|
|
ProjectID = scene.ProjectID,
|
|
BookID = scene.BookID,
|
|
ChapterID = scene.ChapterID,
|
|
ChapterNumber = scene.ChapterNumber,
|
|
ChapterTitle = scene.ChapterTitle,
|
|
SceneID = scene.SceneID,
|
|
SceneNumber = scene.SceneNumber,
|
|
SceneTitle = scene.SceneTitle,
|
|
EntityType = "Character",
|
|
EntityID = characterId,
|
|
WarningTypeName = warningTypeName,
|
|
SeverityName = severityName,
|
|
Message = message,
|
|
Details = details,
|
|
CreatedDate = DateTime.UtcNow,
|
|
LastDetectedDate = DateTime.UtcNow
|
|
};
|
|
}
|