PlotDirector/PlotLine/Services/CharacterAgeContinuityService.cs

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
};
}