@model SceneEditViewModel
@{
var revisionStatus = Model.RevisionStatuses.FirstOrDefault(x => x.Value == Model.RevisionStatusID.ToString())?.Text ?? "Not set";
var primaryLocation = Model.LocationOptions.FirstOrDefault(x => x.Value == Model.PrimaryLocationID?.ToString())?.Text ?? "No primary location";
var povCharacter = Model.SceneCharacters.FirstOrDefault(x => x.RoleInSceneTypeName?.Contains("POV", StringComparison.OrdinalIgnoreCase) == true)?.CharacterName
?? Model.SceneCharacters.FirstOrDefault()?.CharacterName
?? "No POV";
var timeLabel = !string.IsNullOrWhiteSpace(Model.RelativeTimeText)
? Model.RelativeTimeText
: Model.StartDateTime?.ToString("dd MMM yyyy HH:mm")
?? Model.StartDateTime?.ToString("dd MMM yyyy")
?? "Time not set";
var selectedPurposeLabels = Model.ScenePurposeTypes
.Where(x => Model.SelectedPurposeTypeIds.Contains(x.ScenePurposeTypeID))
.Select(x => x.PurposeName)
.ToList();
var dependencyCount = Model.DependenciesThisSceneNeeds.Count + Model.DependenciesNeedingThisScene.Count;
var totalWarningCount = Model.Warnings.Count + Model.AgeContinuityWarnings.Count;
var exactDateTimeModeId = Model.TimeModes.FirstOrDefault(x => string.Equals(x.Text, "Exact DateTime", StringComparison.OrdinalIgnoreCase))?.Value ?? string.Empty;
var exactDateModeId = Model.TimeModes.FirstOrDefault(x => string.Equals(x.Text, "Exact Date", StringComparison.OrdinalIgnoreCase))?.Value ?? string.Empty;
}
Working context
@(string.IsNullOrWhiteSpace(Model.SceneTitle) ? "Untitled scene" : Model.SceneTitle)
@if (Model.Chapter is not null)
{
Chapter @Model.Chapter.ChapterNumber
}
Scene @Model.SceneNumber
@povCharacter
@primaryLocation
@revisionStatus
@timeLabel
@totalWarningCount warning@(totalWarningCount == 1 ? "" : "s")
@Model.SceneCharacters.Count character@(Model.SceneCharacters.Count == 1 ? "" : "s")
@Model.AssetEvents.Count asset event@(Model.AssetEvents.Count == 1 ? "" : "s")
@Model.ThreadEvents.Count thread event@(Model.ThreadEvents.Count == 1 ? "" : "s")
@dependencyCount dependenc@(dependencyCount == 1 ? "y" : "ies")
@if (selectedPurposeLabels.Any())
{
@foreach (var purpose in selectedPurposeLabels)
{
@purpose
}
}
@if (!string.IsNullOrWhiteSpace(Model.SceneOutcomeNotes))
{
@Model.SceneOutcomeNotes
}
@if (Model.SceneID > 0 && Model.ReturnToTimeline)
{
Archive Scene
}
@if (TempData["WriterWorkspaceError"] is string writerWorkspaceError)
{
@writerWorkspaceError
}
Characters
@if (!Model.SceneCharacters.Any())
{
No characters attached to this scene yet. Add the people who appear, are mentioned, or meaningfully affect the scene.
}
else
{
@foreach (var character in Model.SceneCharacters)
{
var editId = $"scene-character-edit-{character.SceneCharacterID}";
var ageDisplay = CharacterAgeDisplayService.GetAgeForScene(character, Model.TimeModeName, Model.StartDateTime);
@character.CharacterName
@if (!string.IsNullOrWhiteSpace(ageDisplay.DisplayText))
{
@ageDisplay.Label: @ageDisplay.DisplayText
}
@((character.RoleInSceneTypeName ?? "Role not set")) • @((character.PresenceTypeName ?? "Presence not set"))
Location: @(character.LocationName ?? "Not set")
Entry: @(character.EntryLocationName ?? "Not set")
Exit: @(character.ExitLocationName ?? "Not set")
@if (!string.IsNullOrWhiteSpace(character.OutfitDescription) || !string.IsNullOrWhiteSpace(character.PhysicalCondition) || !string.IsNullOrWhiteSpace(character.EmotionalState))
{
@character.OutfitDescription @character.PhysicalCondition @character.EmotionalState
}
Edit
Remove
Role
Not set
@foreach (var option in Model.CharacterRoleOptions)
{
@option.Text
}
Presence
Not set
@foreach (var option in Model.PresenceTypeOptions)
{
@option.Text
}
Current location
Not set
@foreach (var option in Model.LocationOptions)
{
@option.Text
}
Enters from
Not set
@foreach (var option in Model.LocationOptions)
{
@option.Text
}
Exits to
Not set
@foreach (var option in Model.LocationOptions)
{
@option.Text
}
Appearance notes
@character.AppearanceNotes
Outfit description
Physical condition
Emotional state
Knowledge notes
@character.KnowledgeNotes
Save character
}
}
Add character to scene
@if (TempData["SceneCharacterAddError"] is string addCharacterError)
{
@addCharacterError
}
Add character to scene
Character Attributes
@if (Model.CharacterAttributeEvents.Any())
{
@foreach (var item in Model.CharacterAttributeEvents)
{
@item.CharacterName: @item.AttributeName
@item.AttributeValue
@item.Description
}
}
else
{
Character attribute history for this scene will appear here when available.
}
Add attribute
Character Knowledge
@if (Model.CharacterKnowledge.Any())
{
@foreach (var item in Model.CharacterKnowledge)
{
@item.CharacterName @item.KnowledgeStateName
@(item.AssetName ?? item.ThreadTitle)
@item.Description
Remove
}
}
Add knowledge
Relationship Events
@if (Model.RelationshipEvents.Any())
{
@foreach (var item in Model.RelationshipEvents)
{
@item.CharacterAName -> @item.CharacterBName
@item.RelationshipStateName / intensity @item.Intensity
@item.Description
Remove
}
}
@if (Model.RelationshipOptions.Count == 1)
{
@Model.RelationshipOptions[0].Text
}
else
{
Create or choose relationship
}
Character A
Character B
Relationship type
Intensity 5
Other character knows
Add relationship event
Plot Thread Events
@if (!Model.ThreadEvents.Any())
{
No plot thread events attached to this scene yet. Add clues, reveals, promises, payoffs, or reversals when the scene touches a thread.
}
else
{
@foreach (var threadEvent in Model.ThreadEvents)
{
@threadEvent.MarkerText
@threadEvent.EventTitle
@threadEvent.PlotEventTypeName / @threadEvent.PlotLineName / @threadEvent.ThreadTitle
Remove
Edit
@if (TempData[$"ThreadEventEditError:{threadEvent.ThreadEventID}"] is string editThreadEventError)
{
@editThreadEventError
}
Event type
Choose event type
@foreach (var eventType in Model.PlotEventTypeOptions)
{
if (eventType.Value == threadEvent.PlotEventTypeID.ToString())
{
@eventType.Text
}
else
{
@eventType.Text
}
}
Branch starts another plot line while this one continues. Split ends this plot line and starts two or more others. Merge ends this plot line by feeding into another.
Target plot line
Choose target plot line
@foreach (var plotLine in Model.PlotLineEventOptions)
{
if (threadEvent.TargetPlotLineID == plotLine.PlotLineID)
{
@plotLine.PlotLineName
}
else
{
@plotLine.PlotLineName
}
}
Merge into plot line
Choose merge target
@foreach (var plotLine in Model.PlotLineEventOptions)
{
if (threadEvent.TargetPlotLineID == plotLine.PlotLineID)
{
@plotLine.PlotLineName
}
else
{
@plotLine.PlotLineName
}
}
Target plot lines
@foreach (var plotLine in Model.PlotLineEventOptions)
{
if (threadEvent.TargetPlotLineIDs.Contains(plotLine.PlotLineID))
{
@plotLine.PlotLineName
}
else
{
@plotLine.PlotLineName
}
}
Marker type
@foreach (var markerType in Model.ThreadEventTypeOptions)
{
if (markerType.Value == threadEvent.EventTypeID.ToString())
{
@markerType.Text
}
else
{
@markerType.Text
}
}
Event title
Description
@threadEvent.EventDescription
Save event
}
}
Choose an existing thread, or choose a plot line to create one from the event title.
@if (TempData["ThreadEventAddError"] is string addThreadEventError)
{
@addThreadEventError
}
Plot line
Choose plot line
@foreach (var plotLine in Model.PlotLineEventOptions)
{
@plotLine.PlotLineName
}
Existing plot thread
Create or choose a thread
@foreach (var plotThread in Model.PlotThreadEventOptions)
{
@plotThread.PlotLineName: @plotThread.ThreadTitle
}
New thread title
Thread type
Default
Event type
Choose event type
@foreach (var eventType in Model.PlotEventTypeOptions)
{
if (eventType.Value == Model.NewThreadEvent.PlotEventTypeID.ToString())
{
@eventType.Text
}
else
{
@eventType.Text
}
}
Branch starts another plot line while this one continues. Split ends this plot line and starts two or more others. Merge ends this plot line by feeding into another.
Target plot line
Choose target plot line
@foreach (var plotLine in Model.PlotLineEventOptions)
{
@plotLine.PlotLineName
}
Merge into plot line
Choose merge target
@foreach (var plotLine in Model.PlotLineEventOptions)
{
@plotLine.PlotLineName
}
Target plot lines
@foreach (var plotLine in Model.PlotLineEventOptions)
{
@plotLine.PlotLineName
}
Marker type
Event title
Description
Add thread event
Story Assets
@if (!Model.AssetEvents.Any())
{
No asset events attached to this scene yet. Add one when an object, clue, secret, or important item changes here.
}
else
{
@foreach (var assetEvent in Model.AssetEvents)
{
@assetEvent.MarkerText
@assetEvent.EventTitle
@assetEvent.AssetName / @assetEvent.AssetEventTypeName
@if (!string.IsNullOrWhiteSpace(assetEvent.ToStateName))
{
State: @(assetEvent.FromStateName ?? "Any") -> @assetEvent.ToStateName
}
Remove
}
}
Choose an existing asset, or enter a new asset name to create it from this scene.
Asset
Create or choose an asset
New asset name
Kind
Event type
From state
Any
To state
No change
Event title
Description
Add asset event
Asset Locations
@if (!Model.SceneAssetLocations.Any())
{
No assets have been placed in a location for this scene yet.
}
else
{
@foreach (var assetLocation in Model.SceneAssetLocations)
{
@assetLocation.AssetName
@assetLocation.LocationName
@assetLocation.Description
Remove
}
}
Place asset
Asset Custody
@if (!Model.AssetCustodyEvents.Any())
{
No custody entries attached to this scene yet.
}
else
{
@foreach (var custody in Model.AssetCustodyEvents)
{
@custody.CustodyEventTypeName
@custody.AssetName
@custody.CustodianSummary
Remove
}
}
Asset
Custody event
Role
Existing custodians
Custodian names
Description
Add custody
Attachments / Research
@if (Model.StorageUsage is not null)
{
Storage used: @Model.StorageUsage.DisplayLabel
}
@if (!Model.Attachments.Any())
{
No scene references yet. Add images, links, floorplans, research notes, or files that help you write this scene.
}
else
{
@foreach (var attachment in Model.Attachments)
{
var hasFile = !string.IsNullOrWhiteSpace(attachment.FilePath);
var fileHref = hasFile && attachment.FilePath!.StartsWith("http", StringComparison.OrdinalIgnoreCase)
? attachment.FilePath
: hasFile && attachment.FilePath!.StartsWith("/")
? attachment.FilePath
: hasFile && !System.IO.Path.IsPathRooted(attachment.FilePath!)
? "/" + attachment.FilePath!.TrimStart('/', '\\')
: null;
var fileExtension = hasFile ? System.IO.Path.GetExtension(attachment.FilePath!) : string.Empty;
var isImageAttachment = !string.IsNullOrWhiteSpace(fileHref)
&& new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp" }.Contains(fileExtension, StringComparer.OrdinalIgnoreCase);
@(hasFile ? "File" : "Link")
@attachment.TypeName
@attachment.Title
@if (isImageAttachment)
{
}
else
{
@(hasFile ? "File" : "Link")
}
@if (!string.IsNullOrWhiteSpace(attachment.Notes))
{
@attachment.Notes
}
@if (hasFile)
{
@System.IO.Path.GetFileName(attachment.FilePath)
}
@foreach (var option in Model.SceneAttachmentTypeOptions)
{
@option.Text
}
Replace file
@attachment.Notes
Save attachment
Remove attachment
}
}
+ Add Attachment
Attachment type
Title
Choose file
Or external URL
Add attachment
Continuity Warnings
@if (Model.SceneID > 0 && Model.ReturnProjectID.HasValue)
{
Validate scene
}
@if (!Model.Warnings.Any() && !Model.AgeContinuityWarnings.Any())
{
No active warnings for this scene. Run validation again after major story changes.
}
else
{
@foreach (var warning in Model.Warnings)
{
@warning.SeverityName
@warning.WarningTypeName
@warning.Message
Dismiss
Intentional
}
@foreach (var warning in Model.AgeContinuityWarnings)
{
@warning.SeverityName
@warning.WarningTypeName
@warning.Message
@if (!string.IsNullOrWhiteSpace(warning.Details))
{
@warning.Details
}
}
}
Scene Dependencies
This scene depends on
@if (!Model.DependenciesThisSceneNeeds.Any())
{
No dependencies yet. Add one when this scene needs another moment to happen first.
}
else
{
@foreach (var dependency in Model.DependenciesThisSceneNeeds)
{
Ch @dependency.SourceChapterNumber, Scene @dependency.SourceSceneNumber
@dependency.SourceSceneTitle
@dependency.SceneDependencyTypeName @(dependency.IsHardDependency ? "/ hard" : "/ soft")
Remove
}
}
Scenes depending on this
@if (!Model.DependenciesNeedingThisScene.Any())
{
No dependent scenes yet. Later scenes that rely on this one will appear here.
}
else
{
@foreach (var dependency in Model.DependenciesNeedingThisScene)
{
Ch @dependency.TargetChapterNumber, Scene @dependency.TargetSceneNumber
@dependency.TargetSceneTitle
@dependency.SceneDependencyTypeName @(dependency.IsHardDependency ? "/ hard" : "/ soft")
Remove
}
}
Plain English
This scene depends on another scene
Another scene depends on this scene
Related scene
Dependency type
Hard
Add dependency
Move / Reorder
Move to chapter
Position
End of chapter
Start of chapter
Renumber scenes
Preview move
Renumber this chapter