Phase 11A PlotDirector Character Age and Story Date Management feature.
This commit is contained in:
parent
cfe95a510f
commit
b5decb0dc6
109
PlotLine/Services/CharacterAgeDisplayService.cs
Normal file
109
PlotLine/Services/CharacterAgeDisplayService.cs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
using PlotLine.Models;
|
||||||
|
|
||||||
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
|
public sealed class CharacterAgeDisplayResult
|
||||||
|
{
|
||||||
|
public bool HasExactAge { get; init; }
|
||||||
|
public string? Label { get; init; }
|
||||||
|
public string? DisplayText { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CharacterAgeDisplayService
|
||||||
|
{
|
||||||
|
public static CharacterAgeDisplayResult GetAgeForScene(SceneCharacter character, string? sceneTimeModeName, DateTime? sceneStartDateTime)
|
||||||
|
{
|
||||||
|
var effectiveDate = ResolveEffectiveSceneDate(sceneTimeModeName, sceneStartDateTime);
|
||||||
|
if (character.BirthDate.HasValue && effectiveDate.HasValue)
|
||||||
|
{
|
||||||
|
return new CharacterAgeDisplayResult
|
||||||
|
{
|
||||||
|
HasExactAge = true,
|
||||||
|
Label = "Age in scene",
|
||||||
|
DisplayText = FormatExactAge(character.BirthDate.Value.Date, effectiveDate.Value.Date)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character.AgeAtSeriesStart.HasValue)
|
||||||
|
{
|
||||||
|
return new CharacterAgeDisplayResult
|
||||||
|
{
|
||||||
|
Label = "Approximate age",
|
||||||
|
DisplayText = $"About {character.AgeAtSeriesStart.Value}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CharacterAgeDisplayResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CharacterAgeDisplayResult GetAgeForScene(Character character, string? sceneTimeModeName, DateTime? sceneStartDateTime)
|
||||||
|
{
|
||||||
|
var effectiveDate = ResolveEffectiveSceneDate(sceneTimeModeName, sceneStartDateTime);
|
||||||
|
if (character.BirthDate.HasValue && effectiveDate.HasValue)
|
||||||
|
{
|
||||||
|
return new CharacterAgeDisplayResult
|
||||||
|
{
|
||||||
|
HasExactAge = true,
|
||||||
|
Label = "Age in scene",
|
||||||
|
DisplayText = FormatExactAge(character.BirthDate.Value.Date, effectiveDate.Value.Date)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character.AgeAtSeriesStart.HasValue)
|
||||||
|
{
|
||||||
|
return new CharacterAgeDisplayResult
|
||||||
|
{
|
||||||
|
Label = "Approximate age",
|
||||||
|
DisplayText = $"About {character.AgeAtSeriesStart.Value}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CharacterAgeDisplayResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DateTime? ResolveEffectiveSceneDate(string? timeModeName, DateTime? startDateTime)
|
||||||
|
{
|
||||||
|
if (!startDateTime.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(timeModeName, "Exact Date", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(timeModeName, "Exact DateTime", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(timeModeName, "Exact Date and Time", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? startDateTime.Value.Date
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatExactAge(DateTime birthDate, DateTime sceneDate)
|
||||||
|
{
|
||||||
|
var years = sceneDate.Year - birthDate.Year;
|
||||||
|
if (sceneDate < birthDate.AddYears(years))
|
||||||
|
{
|
||||||
|
years--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (years < 0)
|
||||||
|
{
|
||||||
|
years = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastBirthday = birthDate.AddYears(years);
|
||||||
|
var months = ((sceneDate.Year - lastBirthday.Year) * 12) + sceneDate.Month - lastBirthday.Month;
|
||||||
|
if (sceneDate.Day < lastBirthday.Day)
|
||||||
|
{
|
||||||
|
months--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (months < 0)
|
||||||
|
{
|
||||||
|
months = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return months == 0
|
||||||
|
? $"{years} year{Pluralise(years)}"
|
||||||
|
: $"{years} year{Pluralise(years)}, {months} month{Pluralise(months)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Pluralise(int value) => value == 1 ? string.Empty : "s";
|
||||||
|
}
|
||||||
@ -1001,6 +1001,7 @@ public sealed class SceneService(
|
|||||||
SceneNumber = existing.Count + 1,
|
SceneNumber = existing.Count + 1,
|
||||||
RevisionStatusID = ChapterService.DefaultRevisionStatusId(lookupData),
|
RevisionStatusID = ChapterService.DefaultRevisionStatusId(lookupData),
|
||||||
TimeModeID = lookupData.TimeModes.FirstOrDefault(x => x.TimeModeName == "Unknown")?.TimeModeID ?? lookupData.TimeModes.First().TimeModeID,
|
TimeModeID = lookupData.TimeModes.FirstOrDefault(x => x.TimeModeName == "Unknown")?.TimeModeID ?? lookupData.TimeModes.First().TimeModeID,
|
||||||
|
TimeModeName = lookupData.TimeModes.FirstOrDefault(x => x.TimeModeName == "Unknown")?.TimeModeName ?? lookupData.TimeModes.First().TimeModeName,
|
||||||
TimeConfidenceID = lookupData.TimeConfidences.FirstOrDefault(x => x.TimeConfidenceName == "Unknown")?.TimeConfidenceID ?? lookupData.TimeConfidences.First().TimeConfidenceID,
|
TimeConfidenceID = lookupData.TimeConfidences.FirstOrDefault(x => x.TimeConfidenceName == "Unknown")?.TimeConfidenceID ?? lookupData.TimeConfidences.First().TimeConfidenceID,
|
||||||
Chapter = context.Value.Chapter,
|
Chapter = context.Value.Chapter,
|
||||||
Book = context.Value.Book,
|
Book = context.Value.Book,
|
||||||
@ -1031,6 +1032,7 @@ public sealed class SceneService(
|
|||||||
SceneTitle = scene.SceneTitle,
|
SceneTitle = scene.SceneTitle,
|
||||||
Summary = scene.Summary,
|
Summary = scene.Summary,
|
||||||
TimeModeID = scene.TimeModeID,
|
TimeModeID = scene.TimeModeID,
|
||||||
|
TimeModeName = scene.TimeModeName,
|
||||||
StartDateTime = scene.StartDateTime,
|
StartDateTime = scene.StartDateTime,
|
||||||
EndDateTime = scene.EndDateTime,
|
EndDateTime = scene.EndDateTime,
|
||||||
DurationAmount = scene.DurationAmount,
|
DurationAmount = scene.DurationAmount,
|
||||||
@ -5614,20 +5616,9 @@ public sealed class CharacterService(
|
|||||||
|
|
||||||
public string DisplayAge(Character character, DateTime? sceneStartDateTime = null)
|
public string DisplayAge(Character character, DateTime? sceneStartDateTime = null)
|
||||||
{
|
{
|
||||||
if (character.BirthDate.HasValue && sceneStartDateTime.HasValue)
|
var age = CharacterAgeDisplayService.GetAgeForScene(character, sceneStartDateTime.HasValue ? "Exact DateTime" : null, sceneStartDateTime);
|
||||||
{
|
|
||||||
var age = sceneStartDateTime.Value.Year - character.BirthDate.Value.Year;
|
|
||||||
if (sceneStartDateTime.Value.Date < character.BirthDate.Value.Date.AddYears(age))
|
|
||||||
{
|
|
||||||
age--;
|
|
||||||
}
|
|
||||||
|
|
||||||
return age.ToString();
|
return age.DisplayText ?? "Unknown";
|
||||||
}
|
|
||||||
|
|
||||||
return character.AgeAtSeriesStart.HasValue
|
|
||||||
? $"About {character.AgeAtSeriesStart.Value}"
|
|
||||||
: "Unknown";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<SelectListItem> ToRelationshipTypeSelectList(IEnumerable<RelationshipType> relationshipTypes)
|
private static IReadOnlyList<SelectListItem> ToRelationshipTypeSelectList(IEnumerable<RelationshipType> relationshipTypes)
|
||||||
|
|||||||
@ -126,6 +126,8 @@ public sealed class SceneEditViewModel
|
|||||||
[Display(Name = "Time mode")]
|
[Display(Name = "Time mode")]
|
||||||
public int TimeModeID { get; set; }
|
public int TimeModeID { get; set; }
|
||||||
|
|
||||||
|
public string TimeModeName { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Display(Name = "Start date/time")]
|
[Display(Name = "Start date/time")]
|
||||||
public DateTime? StartDateTime { get; set; }
|
public DateTime? StartDateTime { get; set; }
|
||||||
|
|
||||||
|
|||||||
@ -354,10 +354,15 @@
|
|||||||
@foreach (var character in Model.SceneCharacters)
|
@foreach (var character in Model.SceneCharacters)
|
||||||
{
|
{
|
||||||
var editId = $"scene-character-edit-{character.SceneCharacterID}";
|
var editId = $"scene-character-edit-{character.SceneCharacterID}";
|
||||||
|
var ageDisplay = CharacterAgeDisplayService.GetAgeForScene(character, Model.TimeModeName, Model.StartDateTime);
|
||||||
<article class="character-appearance-card">
|
<article class="character-appearance-card">
|
||||||
<div class="character-appearance-summary">
|
<div class="character-appearance-summary">
|
||||||
<div>
|
<div>
|
||||||
<strong>@character.CharacterName</strong>
|
<strong>@character.CharacterName</strong>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(ageDisplay.DisplayText))
|
||||||
|
{
|
||||||
|
<span class="muted">@ageDisplay.Label: @ageDisplay.DisplayText</span>
|
||||||
|
}
|
||||||
<span class="muted">@((character.RoleInSceneTypeName ?? "Role not set")) • @((character.PresenceTypeName ?? "Presence not set"))</span>
|
<span class="muted">@((character.RoleInSceneTypeName ?? "Role not set")) • @((character.PresenceTypeName ?? "Presence not set"))</span>
|
||||||
<span class="muted" title="@character.LocationPath">Location: @(character.LocationName ?? "Not set")</span>
|
<span class="muted" title="@character.LocationPath">Location: @(character.LocationName ?? "Not set")</span>
|
||||||
<span class="muted">Entry: @(character.EntryLocationName ?? "Not set")</span>
|
<span class="muted">Entry: @(character.EntryLocationName ?? "Not set")</span>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
@using PlotLine
|
@using PlotLine
|
||||||
@using PlotLine.Models
|
@using PlotLine.Models
|
||||||
|
@using PlotLine.Services
|
||||||
@using PlotLine.ViewModels
|
@using PlotLine.ViewModels
|
||||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
@addTagHelper *, PlotLine
|
@addTagHelper *, PlotLine
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user