Phase 21T - Story Intelligence Visual Clarity and Semantics
@ -59,6 +59,7 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Phase 21R replay uses persisted data only", Phase21RReplayUsesPersistedDataOnly),
|
("Phase 21R replay uses persisted data only", Phase21RReplayUsesPersistedDataOnly),
|
||||||
("Phase 21R scene browser and panels are replay friendly", Phase21RSceneBrowserAndPanelsAreReplayFriendly),
|
("Phase 21R scene browser and panels are replay friendly", Phase21RSceneBrowserAndPanelsAreReplayFriendly),
|
||||||
("Phase 21S uses durable Story Memory assignments", Phase21SUsesDurableStoryMemoryAssignments),
|
("Phase 21S uses durable Story Memory assignments", Phase21SUsesDurableStoryMemoryAssignments),
|
||||||
|
("Story Intelligence visual polish keeps labels fixed and semantics specific", StoryIntelligenceVisualPolishKeepsLabelsFixedAndSemanticsSpecific),
|
||||||
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
|
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
|
||||||
("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel),
|
("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel),
|
||||||
("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics),
|
("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics),
|
||||||
@ -559,7 +560,7 @@ static void StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage(
|
|||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("money") == "Money", "Money must not classify as Car.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("money") == "Money", "Money must not classify as Car.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("bottle") == "Bottle", "Bottle must not classify as LockedBox.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("bottle") == "Bottle", "Bottle must not classify as LockedBox.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("scarves") == "Clothing", "Scarves must not classify as Car.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("scarves") == "Clothing", "Scarves must not classify as Car.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("driving licence") == "Document", "Driving licence should classify as Document.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("driving licence") == "DrivingLicence", "Driving licence should classify as DrivingLicence.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("ring") == "Jewellery", "Ring should classify as Jewellery.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("ring") == "Jewellery", "Ring should classify as Jewellery.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("gun") == "Weapon", "Gun should classify as Weapon.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("gun") == "Weapon", "Gun should classify as Weapon.");
|
||||||
}
|
}
|
||||||
@ -609,8 +610,8 @@ static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictS
|
|||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("supercharger") == "CarPart", "Supercharger should not classify as full car.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("supercharger") == "CarPart", "Supercharger should not classify as full car.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("biscuit tin") == "Tin", "Biscuit tin should not classify as Photograph.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("biscuit tin") == "Tin", "Biscuit tin should not classify as Photograph.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("parcel") == "Parcel", "Parcel should not classify as Letter.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("parcel") == "Parcel", "Parcel should not classify as Letter.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("pass certificate") == "Document", "Pass certificate should classify as Document.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("pass certificate") == "Certificate", "Pass certificate should classify as Certificate.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("driving licence") == "Document", "Driving licence should classify as Document.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("driving licence") == "DrivingLicence", "Driving licence should classify as DrivingLicence.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("thin cotton blouse") == "Clothing", "Blouse should classify as Clothing.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("thin cotton blouse") == "Clothing", "Blouse should classify as Clothing.");
|
||||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("two pairs of sunglasses") == "Clothing", "Sunglasses should classify as Clothing.");
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("two pairs of sunglasses") == "Clothing", "Sunglasses should classify as Clothing.");
|
||||||
|
|
||||||
@ -990,6 +991,40 @@ static void Phase21RSceneBrowserAndPanelsAreReplayFriendly()
|
|||||||
Assert(progressViewModel.Contains("public int? ImportSessionID", StringComparison.Ordinal), "Import progress page should carry the import session for replay links.");
|
Assert(progressViewModel.Contains("public int? ImportSessionID", StringComparison.Ordinal), "Import progress page should carry the import session for replay links.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void StoryIntelligenceVisualPolishKeepsLabelsFixedAndSemanticsSpecific()
|
||||||
|
{
|
||||||
|
var script = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/wwwroot/js/story-intelligence-experience-prototype.js"));
|
||||||
|
var css = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/wwwroot/css/story-intelligence-experience-prototype.css"));
|
||||||
|
var snapshot = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Services/StoryIntelligenceVisualisationSnapshotService.cs"));
|
||||||
|
|
||||||
|
Assert(!script.Contains("node.dataset.labelSide", StringComparison.Ordinal), "Character labels must not use dynamic side positioning.");
|
||||||
|
Assert(!css.Contains(".story-character[data-label-side=\"left\"]", StringComparison.Ordinal), "Left-floating character labels must be removed.");
|
||||||
|
Assert(!css.Contains(".story-character[data-layout-role=\"partner\"] .story-character__text", StringComparison.Ordinal), "Partner labels must not float beside portraits.");
|
||||||
|
Assert(css.Contains("top: calc(var(--node-size, 150px) + 8px)", StringComparison.Ordinal), "Character captions should be fixed below portraits.");
|
||||||
|
Assert(css.Contains("background: rgba(4, 10, 20, 0.91)", StringComparison.Ordinal), "Character caption opacity should be increased for readability.");
|
||||||
|
Assert(css.Contains(".story-exp-insight span {\n display: none;", StringComparison.Ordinal), "Redundant insight tone headings should be hidden.");
|
||||||
|
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Cock Hill Lane") == "Road", "Cock Hill Lane should classify as Road.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Mount Pleasant Road") == "Road", "Mount Pleasant Road should classify as Road.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Queens Court") == "Road", "Court should classify as Road.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Station Gardens") == "Road", "Gardens should classify as Road.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("driver seat inside the car") == "VehicleInterior", "Driver seat should classify as VehicleInterior.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Driving Test Centre waiting room") == "DrivingTestCentre", "Driving test centre should have its own location family.");
|
||||||
|
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("VehicleInterior", "HouseExterior"), "Vehicle interiors must not resolve to houses or buildings.");
|
||||||
|
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("passport") == "Passport", "Passport should not be Generic Object.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("sealed envelope") == "Envelope", "Envelope should not be Generic Object.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Highway Code book") == "DrivingManual", "Highway Code should classify as a driving manual.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("traffic sign") == "TrafficSign", "Traffic sign should classify as TrafficSign.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("wall clock") == "Clock", "Clock should classify as Clock.");
|
||||||
|
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("park bench") == "Bench", "Bench should classify as Bench.");
|
||||||
|
|
||||||
|
Assert(snapshot.Contains("\"loc-generic-road\" => $\"{FallbackRoot}/location-road.svg\"", StringComparison.Ordinal), "Roads need a generic road fallback illustration.");
|
||||||
|
Assert(snapshot.Contains("\"loc-vehicle-interior\" => $\"{FallbackRoot}/location-vehicle-interior.svg\"", StringComparison.Ordinal), "Vehicle interiors need a non-building fallback illustration.");
|
||||||
|
Assert(snapshot.Contains("\"asset-document\" => $\"{FallbackRoot}/asset-document.svg\"", StringComparison.Ordinal), "Documents need a recognisable document fallback.");
|
||||||
|
Assert(snapshot.Contains("\"asset-traffic-sign\" => $\"{FallbackRoot}/asset-traffic-sign.svg\"", StringComparison.Ordinal), "Traffic signs need a recognisable fallback.");
|
||||||
|
}
|
||||||
|
|
||||||
static JsonSerializerOptions JsonOptions()
|
static JsonSerializerOptions JsonOptions()
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using PlotLine.Models;
|
using PlotLine.Models;
|
||||||
|
|
||||||
namespace PlotLine.Services;
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
public static class StoryIntelligenceIllustrationCompatibility
|
public static class StoryIntelligenceIllustrationCompatibility
|
||||||
{
|
{
|
||||||
|
private static readonly Regex RoadSuffixRegex = new(@"\b[A-Z0-9][\w' -]*\s(road|street|lane|drive|close|court|way|avenue|terrace|gardens|highway|motorway|bridge|roundabout|junction|crossroads|carriageway|track|footpath|path|cycleway|tunnel|flyover|underpass)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
public static class AgeBands
|
public static class AgeBands
|
||||||
{
|
{
|
||||||
public const string Child = "Child";
|
public const string Child = "Child";
|
||||||
@ -247,6 +250,10 @@ public static class StoryIntelligenceIllustrationCompatibility
|
|||||||
public static string LocationType(string? value)
|
public static string LocationType(string? value)
|
||||||
{
|
{
|
||||||
var text = (value ?? string.Empty).ToLowerInvariant();
|
var text = (value ?? string.Empty).ToLowerInvariant();
|
||||||
|
var padded = $" {text} ";
|
||||||
|
if (ContainsAny(text, "driving test centre", "driving test center", "test centre", "test center")) return "DrivingTestCentre";
|
||||||
|
if (ContainsAny(text, "police station")) return "PoliceStation";
|
||||||
|
if (ContainsAny(text, "car interior", "vehicle interior", "inside the car", "inside car", "driver seat", "driver's seat", "passenger seat", "rear seat", "back seat", "boot", "cabin", "dashboard")) return "VehicleInterior";
|
||||||
if (ContainsAny(text, "bathroom", "toilet", "washroom")) return "Bathroom";
|
if (ContainsAny(text, "bathroom", "toilet", "washroom")) return "Bathroom";
|
||||||
if (text.Contains("kitchen", StringComparison.OrdinalIgnoreCase)) return "Kitchen";
|
if (text.Contains("kitchen", StringComparison.OrdinalIgnoreCase)) return "Kitchen";
|
||||||
if (text.Contains("bedroom", StringComparison.OrdinalIgnoreCase)) return "Bedroom";
|
if (text.Contains("bedroom", StringComparison.OrdinalIgnoreCase)) return "Bedroom";
|
||||||
@ -254,17 +261,26 @@ public static class StoryIntelligenceIllustrationCompatibility
|
|||||||
if (ContainsAny(text, "hall", "hallway", "corridor")) return "Hallway";
|
if (ContainsAny(text, "hall", "hallway", "corridor")) return "Hallway";
|
||||||
if (text.Contains("stair", StringComparison.OrdinalIgnoreCase)) return "Stairwell";
|
if (text.Contains("stair", StringComparison.OrdinalIgnoreCase)) return "Stairwell";
|
||||||
if (ContainsAny(text, "hospital", "derelict hospital")) return "Hospital";
|
if (ContainsAny(text, "hospital", "derelict hospital")) return "Hospital";
|
||||||
|
if (ContainsAny(text, "church", "chapel")) return "Church";
|
||||||
|
if (ContainsAny(text, "restaurant", "cafe", "café", "diner")) return "Restaurant";
|
||||||
if (ContainsAny(text, "pub", "public house")) return "Pub";
|
if (ContainsAny(text, "pub", "public house")) return "Pub";
|
||||||
if (ContainsAny(text, "beach", "seafront")) return "Beach";
|
if (ContainsAny(text, "beach", "seafront")) return "Beach";
|
||||||
if (ContainsAny(text, "railway station", "train station", "station platform")) return "RailwayStation";
|
if (ContainsAny(text, "railway station", "train station", "station platform")) return "RailwayStation";
|
||||||
if (ContainsAny(text, "canal", "towpath")) return "Canal";
|
if (ContainsAny(text, "canal", "towpath")) return "Canal";
|
||||||
if (ContainsAny(text, "garage")) return "Garage";
|
if (ContainsAny(text, "garage", "workshop")) return "Garage";
|
||||||
if (ContainsAny(text, "front door", "porch", "entrance", "house exterior", "outside the house", "exterior")) return "HouseExterior";
|
if (ContainsAny(text, "front door", "porch", "entrance", "house exterior", "outside the house", "exterior")) return "HouseExterior";
|
||||||
if (ContainsAny(text, "office", "desk", "study", "library")) return "Office";
|
if (ContainsAny(text, "office", "desk", "study", "library")) return "Office";
|
||||||
if (text.Contains("school", StringComparison.OrdinalIgnoreCase)) return "School";
|
if (text.Contains("school", StringComparison.OrdinalIgnoreCase)) return "School";
|
||||||
if (text.Contains("waiting room", StringComparison.OrdinalIgnoreCase)) return "WaitingRoom";
|
if (text.Contains("waiting room", StringComparison.OrdinalIgnoreCase)) return "WaitingRoom";
|
||||||
if (ContainsAny(text, "road", "street", "lane", "drive", "close", "avenue", "boulevard", "motorway", "bridge", "roundabout", "footpath", "path", "track")) return "Road";
|
if (ContainsAny(text, "car park", "parking", "forecourt")) return "CarPark";
|
||||||
if (ContainsAny(text, "car park", "parking")) return "CarPark";
|
if (ContainsAny(text,
|
||||||
|
"road", "street", "lane", "drive", " close ", "court", "way", "avenue", "terrace", "gardens",
|
||||||
|
"highway", "motorway", "bridge", "roundabout", "junction", "crossroads", "dual carriageway",
|
||||||
|
"track", "footpath", " path ", "cycleway", "tunnel", "flyover", "underpass", "boulevard")
|
||||||
|
|| RoadSuffixRegex.IsMatch(padded))
|
||||||
|
{
|
||||||
|
return "Road";
|
||||||
|
}
|
||||||
if (text.Contains("shop", StringComparison.OrdinalIgnoreCase)) return "Shop";
|
if (text.Contains("shop", StringComparison.OrdinalIgnoreCase)) return "Shop";
|
||||||
if (ContainsAny(text, "bench", "bus stop")) return "StreetFurniture";
|
if (ContainsAny(text, "bench", "bus stop")) return "StreetFurniture";
|
||||||
if (ContainsAny(text, "flat", "apartment", "room")) return "FlatInterior";
|
if (ContainsAny(text, "flat", "apartment", "room")) return "FlatInterior";
|
||||||
@ -275,13 +291,32 @@ public static class StoryIntelligenceIllustrationCompatibility
|
|||||||
{
|
{
|
||||||
var text = (value ?? string.Empty).ToLowerInvariant();
|
var text = (value ?? string.Empty).ToLowerInvariant();
|
||||||
var padded = $" {text} ";
|
var padded = $" {text} ";
|
||||||
|
if (ContainsAny(text, "driving licence", "driver licence", "provisional licence", "license", "licence")) return "DrivingLicence";
|
||||||
|
if (ContainsAny(text, "highway code")) return "DrivingManual";
|
||||||
|
if (ContainsAny(text, "passport")) return "Passport";
|
||||||
|
if (ContainsAny(text, "certificate", "pass certificate")) return "Certificate";
|
||||||
|
if (ContainsAny(text, "envelope")) return "Envelope";
|
||||||
|
if (ContainsAny(text, "traffic sign", "road sign", "street sign")) return "TrafficSign";
|
||||||
|
if (ContainsAny(text, "clock", "watch")) return "Clock";
|
||||||
|
if (ContainsAny(text, "wallet", "purse")) return "Wallet";
|
||||||
|
if (ContainsAny(text, "handbag", "bag")) return "Handbag";
|
||||||
|
if (ContainsAny(text, "newspaper")) return "Newspaper";
|
||||||
|
if (ContainsAny(text, "magazine")) return "Magazine";
|
||||||
|
if (ContainsAny(text, "painting", "picture frame")) return "Painting";
|
||||||
|
if (ContainsAny(text, "bench")) return "Bench";
|
||||||
|
if (ContainsAny(text, "tree")) return "Tree";
|
||||||
|
if (ContainsAny(text, "lamp", "light")) return "Lamp";
|
||||||
|
if (ContainsAny(text, "window")) return "Window";
|
||||||
|
if (ContainsAny(text, "door")) return "Door";
|
||||||
|
if (ContainsAny(text, "fence", "gate")) return "Fence";
|
||||||
|
if (ContainsAny(text, "gift", "present")) return "Gift";
|
||||||
if (text.Contains("ambulance", StringComparison.OrdinalIgnoreCase)) return "Ambulance";
|
if (text.Contains("ambulance", StringComparison.OrdinalIgnoreCase)) return "Ambulance";
|
||||||
if (text.Contains("stretcher", StringComparison.OrdinalIgnoreCase)) return "Stretcher";
|
if (text.Contains("stretcher", StringComparison.OrdinalIgnoreCase)) return "Stretcher";
|
||||||
if (text.Contains("body bag", StringComparison.OrdinalIgnoreCase)) return "BodyBag";
|
if (text.Contains("body bag", StringComparison.OrdinalIgnoreCase)) return "BodyBag";
|
||||||
if (text.Contains("bottle", StringComparison.OrdinalIgnoreCase)) return "Bottle";
|
if (text.Contains("bottle", StringComparison.OrdinalIgnoreCase)) return "Bottle";
|
||||||
if (ContainsAny(text, "money", "cash", "coins", "banknote")) return "Money";
|
if (ContainsAny(text, "money", "cash", "coins", "banknote")) return "Money";
|
||||||
if (ContainsAny(text, "scarf", "scarves")) return "Clothing";
|
if (ContainsAny(text, "scarf", "scarves")) return "Clothing";
|
||||||
if (ContainsAny(text, "driving licence", "driver licence", "provisional licence", "license", "licence", "pass certificate", "certificate", "paperwork")) return "Document";
|
if (ContainsAny(text, "paperwork")) return "Document";
|
||||||
if (ContainsAny(text, "ring", "necklace", "jewellery", "jewelry")) return "Jewellery";
|
if (ContainsAny(text, "ring", "necklace", "jewellery", "jewelry")) return "Jewellery";
|
||||||
if (ContainsAny(text, "gun", "pistol", "rifle", "weapon")) return "Weapon";
|
if (ContainsAny(text, "gun", "pistol", "rifle", "weapon")) return "Weapon";
|
||||||
if (ContainsAny(padded, " hatchback ", " vw golf ", " volkswagen golf ", " golf gti ", " ford fiesta ", " fiesta ")) return "Hatchback";
|
if (ContainsAny(padded, " hatchback ", " vw golf ", " volkswagen golf ", " golf gti ", " ford fiesta ", " fiesta ")) return "Hatchback";
|
||||||
|
|||||||
@ -2187,7 +2187,14 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
{
|
{
|
||||||
var value = $"{name} {location?.LocationType} {location?.GenericRoomType} {setting?.LocationType}".ToLowerInvariant();
|
var value = $"{name} {location?.LocationType} {location?.GenericRoomType} {setting?.LocationType}".ToLowerInvariant();
|
||||||
var type = StoryIntelligenceIllustrationCompatibility.LocationType(value);
|
var type = StoryIntelligenceIllustrationCompatibility.LocationType(value);
|
||||||
if (type is "Bathroom" or "WaitingRoom" or "Road") return string.Empty;
|
if (type == "Bathroom") return "loc-generic-bathroom";
|
||||||
|
if (type == "Kitchen") return "loc-generic-kitchen";
|
||||||
|
if (type == "WaitingRoom") return "loc-generic-waiting-room";
|
||||||
|
if (type == "DrivingTestCentre") return "loc-driving-test-centre";
|
||||||
|
if (type == "VehicleInterior") return "loc-vehicle-interior";
|
||||||
|
if (type == "Road") return "loc-generic-road";
|
||||||
|
if (type == "Garage") return "loc-garage-workshop";
|
||||||
|
if (type is "Hospital" or "PoliceStation" or "School" or "Church" or "Restaurant" or "Pub") return $"loc-generic-{StableKey(type)}";
|
||||||
if (type is "Kitchen" or "Bedroom" or "LivingRoom" or "FlatInterior" or "Hallway") return "loc-small-flat-interior";
|
if (type is "Kitchen" or "Bedroom" or "LivingRoom" or "FlatInterior" or "Hallway") return "loc-small-flat-interior";
|
||||||
if (type == "Office") return "loc-domestic-desk";
|
if (type == "Office") return "loc-domestic-desk";
|
||||||
if (type == "Stairwell") return "loc-narrow-stairwell";
|
if (type == "Stairwell") return "loc-narrow-stairwell";
|
||||||
@ -2200,12 +2207,22 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
{
|
{
|
||||||
var value = $"{name} {asset.AssetType} {asset.Status}".ToLowerInvariant();
|
var value = $"{name} {asset.AssetType} {asset.Status}".ToLowerInvariant();
|
||||||
var type = StoryIntelligenceIllustrationCompatibility.AssetType(value);
|
var type = StoryIntelligenceIllustrationCompatibility.AssetType(value);
|
||||||
if (type is "Letter" or "Note" or "Document") return "asset-folded-letter";
|
if (type is "Letter" or "Note" or "Document" or "Certificate" or "Passport" or "DrivingLicence") return "asset-document";
|
||||||
if (type is "Notebook" or "Book") return "asset-worn-notebook";
|
if (type is "Envelope") return "asset-envelope";
|
||||||
|
if (type is "Notebook") return "asset-worn-notebook";
|
||||||
|
if (type is "Book" or "DrivingManual" or "Newspaper" or "Magazine") return "asset-book";
|
||||||
if (type == "Car") return "asset-red-classic-car";
|
if (type == "Car") return "asset-red-classic-car";
|
||||||
if (type == "Keys") return "asset-old-keys";
|
if (type == "Keys") return "asset-old-keys";
|
||||||
if (type == "Telephone") return "asset-phone-recorder";
|
if (type == "Telephone") return "asset-phone-recorder";
|
||||||
if (type == "Photograph") return "asset-photo-print";
|
if (type == "Photograph") return "asset-photo-print";
|
||||||
|
if (type is "Painting") return "asset-photo-print";
|
||||||
|
if (type == "Bottle") return "asset-bottle";
|
||||||
|
if (type == "Clock") return "asset-clock";
|
||||||
|
if (type == "TrafficSign") return "asset-traffic-sign";
|
||||||
|
if (type is "Wallet" or "Handbag" or "Suitcase") return "asset-bag-wallet";
|
||||||
|
if (type is "Parcel" or "Gift") return "asset-parcel";
|
||||||
|
if (type is "Bench") return "asset-bench";
|
||||||
|
if (type is "Tree" or "Lamp" or "Window" or "Door" or "Fence") return "asset-environment-object";
|
||||||
if (type == "Rucksack") return "asset-rucksack";
|
if (type == "Rucksack") return "asset-rucksack";
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
@ -2234,6 +2251,19 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
"loc-narrow-stairwell" => $"{FallbackRoot}/location-stairwell.svg",
|
"loc-narrow-stairwell" => $"{FallbackRoot}/location-stairwell.svg",
|
||||||
"loc-wet-car-park" => $"{FallbackRoot}/location-car-park.svg",
|
"loc-wet-car-park" => $"{FallbackRoot}/location-car-park.svg",
|
||||||
"loc-estate-house-exterior" => $"{FallbackRoot}/location-doweries.svg",
|
"loc-estate-house-exterior" => $"{FallbackRoot}/location-doweries.svg",
|
||||||
|
"loc-generic-road" => $"{FallbackRoot}/location-road.svg",
|
||||||
|
"loc-vehicle-interior" => $"{FallbackRoot}/location-vehicle-interior.svg",
|
||||||
|
"loc-generic-bathroom" => $"{FallbackRoot}/location-bathroom.svg",
|
||||||
|
"loc-generic-kitchen" => $"{FallbackRoot}/location-kitchen.svg",
|
||||||
|
"loc-generic-waiting-room" => $"{FallbackRoot}/location-waiting-room.svg",
|
||||||
|
"loc-driving-test-centre" => $"{FallbackRoot}/location-driving-test-centre.svg",
|
||||||
|
"loc-garage-workshop" => $"{FallbackRoot}/location-garage-workshop.svg",
|
||||||
|
"loc-generic-hospital" => $"{FallbackRoot}/location-building.svg",
|
||||||
|
"loc-generic-policestation" => $"{FallbackRoot}/location-building.svg",
|
||||||
|
"loc-generic-school" => $"{FallbackRoot}/location-building.svg",
|
||||||
|
"loc-generic-church" => $"{FallbackRoot}/location-building.svg",
|
||||||
|
"loc-generic-restaurant" => $"{FallbackRoot}/location-building.svg",
|
||||||
|
"loc-generic-pub" => $"{FallbackRoot}/location-building.svg",
|
||||||
_ => $"{FallbackRoot}/location-flat.svg"
|
_ => $"{FallbackRoot}/location-flat.svg"
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2241,10 +2271,21 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
=> AssetLibraryCode(name, new SceneIntelligenceAsset()) switch
|
=> AssetLibraryCode(name, new SceneIntelligenceAsset()) switch
|
||||||
{
|
{
|
||||||
"asset-folded-letter" => $"{FallbackRoot}/asset-letter.svg",
|
"asset-folded-letter" => $"{FallbackRoot}/asset-letter.svg",
|
||||||
|
"asset-document" => $"{FallbackRoot}/asset-document.svg",
|
||||||
|
"asset-envelope" => $"{FallbackRoot}/asset-envelope.svg",
|
||||||
"asset-worn-notebook" => $"{FallbackRoot}/asset-notebook.svg",
|
"asset-worn-notebook" => $"{FallbackRoot}/asset-notebook.svg",
|
||||||
|
"asset-book" => $"{FallbackRoot}/asset-book.svg",
|
||||||
"asset-red-classic-car" => $"{FallbackRoot}/asset-red-tr6.svg",
|
"asset-red-classic-car" => $"{FallbackRoot}/asset-red-tr6.svg",
|
||||||
"asset-old-keys" => $"{FallbackRoot}/asset-keys.svg",
|
"asset-old-keys" => $"{FallbackRoot}/asset-keys.svg",
|
||||||
"asset-phone-recorder" => $"{FallbackRoot}/asset-phone.svg",
|
"asset-phone-recorder" => $"{FallbackRoot}/asset-phone.svg",
|
||||||
|
"asset-photo-print" => $"{FallbackRoot}/asset-photo.svg",
|
||||||
|
"asset-bottle" => $"{FallbackRoot}/asset-bottle.svg",
|
||||||
|
"asset-clock" => $"{FallbackRoot}/asset-clock.svg",
|
||||||
|
"asset-traffic-sign" => $"{FallbackRoot}/asset-traffic-sign.svg",
|
||||||
|
"asset-bag-wallet" => $"{FallbackRoot}/asset-bag-wallet.svg",
|
||||||
|
"asset-parcel" => $"{FallbackRoot}/asset-parcel.svg",
|
||||||
|
"asset-bench" => $"{FallbackRoot}/asset-bench.svg",
|
||||||
|
"asset-environment-object" => $"{FallbackRoot}/asset-environment.svg",
|
||||||
_ => $"{FallbackRoot}/asset-generic.svg"
|
_ => $"{FallbackRoot}/asset-generic.svg"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -286,6 +286,48 @@ input:focus-visible {
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-exp-rebuild {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 9px;
|
||||||
|
margin: 0 18px 10px;
|
||||||
|
border: 1px solid rgba(151, 185, 218, 0.12);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: rgba(4, 12, 24, 0.74);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-rebuild label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-rebuild select,
|
||||||
|
.story-exp-rebuild button {
|
||||||
|
border: 1px solid rgba(151, 185, 218, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
color: var(--story-soft);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-rebuild select {
|
||||||
|
max-width: min(280px, 40vw);
|
||||||
|
padding: 6px 22px 6px 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-rebuild button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.story-exp-shell {
|
.story-exp-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(180px, 0.46fr) minmax(820px, 2.72fr) minmax(188px, 0.48fr);
|
grid-template-columns: minmax(180px, 0.46fr) minmax(820px, 2.72fr) minmax(188px, 0.48fr);
|
||||||
@ -301,7 +343,15 @@ input:focus-visible {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
max-height: calc(100vh - 126px);
|
max-height: calc(100vh - 126px);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-left::-webkit-scrollbar,
|
||||||
|
.story-exp-right::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-left {
|
.story-exp-left {
|
||||||
@ -331,18 +381,18 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-progress,
|
.story-exp-progress,
|
||||||
.story-exp-panel {
|
.story-exp-panel {
|
||||||
padding: 16px;
|
padding: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-panel {
|
.story-exp-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-panel--compact {
|
.story-exp-panel--compact {
|
||||||
padding: 12px;
|
padding: 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-progress-ring {
|
.story-exp-progress-ring {
|
||||||
@ -454,38 +504,39 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-panel h2 {
|
.story-exp-panel h2 {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 7px;
|
||||||
font-size: 1rem;
|
font-size: 0.88rem;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-list,
|
.story-exp-list,
|
||||||
.story-exp-feed {
|
.story-exp-feed {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: visible;
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-list li,
|
.story-exp-list li,
|
||||||
.story-exp-feed li,
|
.story-exp-feed li,
|
||||||
.story-exp-insight {
|
.story-exp-insight {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.065);
|
border: 1px solid rgba(255, 255, 255, 0.065);
|
||||||
border-radius: 14px;
|
border-radius: 9px;
|
||||||
padding: 10px 11px;
|
padding: 7px 8px;
|
||||||
background: rgba(255, 255, 255, 0.035);
|
background: rgba(255, 255, 255, 0.035);
|
||||||
color: var(--story-soft);
|
color: var(--story-soft);
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-list li {
|
.story-exp-list li {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 18px 1fr;
|
grid-template-columns: 14px 1fr;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-list li span {
|
.story-exp-list li span {
|
||||||
@ -701,7 +752,7 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-characters {
|
.story-exp-characters {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 90;
|
z-index: 120;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -713,13 +764,13 @@ input:focus-visible {
|
|||||||
height: var(--node-size, 150px);
|
height: var(--node-size, 150px);
|
||||||
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
|
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
|
||||||
opacity: var(--opacity, 1);
|
opacity: var(--opacity, 1);
|
||||||
z-index: calc(var(--z, 3) + 90);
|
z-index: calc(var(--z, 3) + 120);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transition: transform 1050ms cubic-bezier(.16, .84, .28, 1), opacity 780ms ease;
|
transition: transform 1050ms cubic-bezier(.16, .84, .28, 1), opacity 780ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-character.is-pov {
|
.story-character.is-pov {
|
||||||
z-index: 48;
|
z-index: 190;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-character.is-context .story-character__image {
|
.story-character.is-context .story-character__image {
|
||||||
@ -760,58 +811,28 @@ input:focus-visible {
|
|||||||
.story-character__text {
|
.story-character__text {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 180;
|
z-index: 180;
|
||||||
top: calc(var(--node-size, 150px) + 9px);
|
top: calc(var(--node-size, 150px) + 8px);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 2px;
|
gap: 1px;
|
||||||
width: min(188px, calc(var(--node-size, 150px) + 58px));
|
width: min(188px, calc(var(--node-size, 150px) + 58px));
|
||||||
max-width: calc(100vw - 32px);
|
max-width: calc(100vw - 32px);
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
border: 1px solid rgba(151, 185, 218, 0.1);
|
border: 1px solid rgba(151, 185, 218, 0.16);
|
||||||
border-radius: 10px;
|
border-radius: 9px;
|
||||||
padding: 5px 7px 6px;
|
padding: 5px 7px 6px;
|
||||||
background: rgba(4, 10, 20, 0.82);
|
background: rgba(4, 10, 20, 0.91);
|
||||||
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.28), 0 0 0 1px rgba(4, 10, 20, 0.22);
|
box-shadow: 0 12px 26px rgba(0, 0, 0, 0.28), 0 0 0 1px rgba(4, 10, 20, 0.22);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-character.is-pov .story-character__text {
|
|
||||||
top: auto;
|
|
||||||
left: calc(50% + 34px);
|
|
||||||
bottom: calc(100% + 9px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-character[data-layout-role="partner"] .story-character__text {
|
|
||||||
top: 50%;
|
|
||||||
left: calc(100% + 12px);
|
|
||||||
justify-items: start;
|
|
||||||
text-align: left;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-character[data-layout-role="support"] .story-character__text {
|
|
||||||
left: calc(100% + 10px);
|
|
||||||
justify-items: start;
|
|
||||||
text-align: left;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.story-character[data-layout-role="context"] .story-character__text {
|
.story-character[data-layout-role="context"] .story-character__text {
|
||||||
width: min(132px, calc(var(--node-size, 150px) + 34px));
|
width: min(132px, calc(var(--node-size, 150px) + 34px));
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: calc(100% + 7px);
|
top: calc(var(--node-size, 150px) + 7px);
|
||||||
}
|
|
||||||
|
|
||||||
.story-character[data-label-side="left"] .story-character__text {
|
|
||||||
top: 50%;
|
|
||||||
left: auto;
|
|
||||||
right: calc(100% + 10px);
|
|
||||||
justify-items: end;
|
|
||||||
text-align: right;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-character__text strong {
|
.story-character__text strong {
|
||||||
@ -972,7 +993,7 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-ribbon-wrap {
|
.story-exp-ribbon-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: minmax(104px, auto) 56px;
|
grid-template-rows: auto minmax(104px, auto) 56px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border: 1px solid rgba(151, 185, 218, 0.1);
|
border: 1px solid rgba(151, 185, 218, 0.1);
|
||||||
@ -983,6 +1004,47 @@ input:focus-visible {
|
|||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.story-exp-replay-browser {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-replay-browser button,
|
||||||
|
.story-exp-replay-browser select,
|
||||||
|
.story-exp-controls select {
|
||||||
|
border: 1px solid rgba(151, 185, 218, 0.18);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(4, 12, 24, 0.84);
|
||||||
|
color: var(--story-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-replay-browser button {
|
||||||
|
padding: 7px 12px;
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-replay-browser label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-replay-browser select {
|
||||||
|
max-width: min(300px, 36vw);
|
||||||
|
padding: 7px 24px 7px 10px;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
|
||||||
.story-exp-ribbon {
|
.story-exp-ribbon {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -1117,43 +1179,44 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-insight-list {
|
.story-exp-insight-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 9px;
|
gap: 5px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: visible;
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-insight {
|
.story-exp-insight {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
border-color: rgba(255, 255, 255, 0.055);
|
border-color: rgba(255, 255, 255, 0.055);
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.038), rgba(255, 255, 255, 0.018));
|
linear-gradient(180deg, rgba(255, 255, 255, 0.038), rgba(255, 255, 255, 0.018));
|
||||||
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.16);
|
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.16);
|
||||||
transition: opacity 560ms ease, transform 700ms cubic-bezier(.16, .84, .28, 1);
|
transition: opacity 560ms ease, transform 700ms cubic-bezier(.16, .84, .28, 1);
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-panel--compact .story-exp-insight {
|
.story-exp-panel--compact .story-exp-insight {
|
||||||
padding: 8px 9px;
|
padding: 6px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-panel--compact .story-exp-insight p {
|
.story-exp-panel--compact .story-exp-insight p {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.76rem;
|
font-size: 0.72rem;
|
||||||
line-height: 1.25;
|
line-height: 1.16;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-more {
|
.story-exp-more {
|
||||||
border: 1px solid rgba(151, 185, 218, 0.12);
|
border: 1px solid rgba(151, 185, 218, 0.12);
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
padding: 7px 9px;
|
padding: 5px 7px;
|
||||||
background: rgba(255, 255, 255, 0.035);
|
background: rgba(255, 255, 255, 0.035);
|
||||||
color: var(--story-soft);
|
color: var(--story-soft);
|
||||||
font-size: 0.78rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@ -1165,10 +1228,12 @@ input:focus-visible {
|
|||||||
|
|
||||||
.story-exp-insight strong {
|
.story-exp-insight strong {
|
||||||
color: var(--story-text);
|
color: var(--story-text);
|
||||||
font-size: 0.92rem;
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-insight span {
|
.story-exp-insight span {
|
||||||
|
display: none;
|
||||||
color: var(--story-muted);
|
color: var(--story-muted);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@ -1211,8 +1276,56 @@ input:focus-visible {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-controls input {
|
.story-exp-controls select {
|
||||||
width: 104px;
|
padding: 5px 20px 5px 9px;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-detail {
|
||||||
|
position: fixed;
|
||||||
|
right: 24px;
|
||||||
|
top: 96px;
|
||||||
|
z-index: 12;
|
||||||
|
width: min(360px, calc(100vw - 48px));
|
||||||
|
border: 1px solid rgba(151, 185, 218, 0.16);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(4, 10, 20, 0.9);
|
||||||
|
box-shadow: var(--story-shadow);
|
||||||
|
color: var(--story-soft);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-detail button {
|
||||||
|
float: right;
|
||||||
|
border: 1px solid rgba(151, 185, 218, 0.16);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.045);
|
||||||
|
color: var(--story-soft);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-detail span {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
color: var(--story-muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-detail h2 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: var(--story-text);
|
||||||
|
font-size: 1.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-exp-detail p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-exp-diagnostics {
|
.story-exp-diagnostics {
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Bag or wallet">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#081524"/>
|
||||||
|
<rect x="38" y="68" width="104" height="70" rx="12" fill="#473326" stroke="#d7a85d" stroke-width="5"/>
|
||||||
|
<path d="M65 70c0-24 50-24 50 0" fill="none" stroke="#d9edf5" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<path d="M100 102h28" stroke="#f2c46b" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 460 B |
@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Bench">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#081524"/>
|
||||||
|
<path d="M36 82h108M46 108h88" stroke="#d7a85d" stroke-width="13" stroke-linecap="round"/>
|
||||||
|
<path d="M52 108v36M128 108v36M40 144h104" stroke="#7dc1ff" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 355 B |
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Book">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#071321"/>
|
||||||
|
<path d="M46 28h78c8 0 14 6 14 14v110H58c-7 0-12-5-12-12z" fill="#23384c"/>
|
||||||
|
<path d="M58 28v124" stroke="#d7a85d" stroke-width="8"/>
|
||||||
|
<path d="M76 56h42M76 76h32" stroke="#d9edf5" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
<path d="M58 152h80" stroke="#6fa8cc" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 466 B |
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Bottle">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#081524"/>
|
||||||
|
<path d="M76 30h28v36c15 8 23 21 23 40v36c0 8-7 14-15 14H68c-8 0-15-6-15-14v-36c0-19 8-32 23-40z" fill="#2c6f78" stroke="#7dc1ff" stroke-width="5"/>
|
||||||
|
<path d="M74 100h32" stroke="#d7a85d" stroke-width="13" stroke-linecap="round"/>
|
||||||
|
<path d="M74 31h32" stroke="#d9edf5" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 474 B |
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Clock">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#081524"/>
|
||||||
|
<circle cx="90" cy="90" r="55" fill="#d9edf5" stroke="#d7a85d" stroke-width="7"/>
|
||||||
|
<path d="M90 56v36l24 16" stroke="#20384d" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<circle cx="90" cy="90" r="5" fill="#20384d"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 378 B |
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Document">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#091625"/>
|
||||||
|
<rect x="44" y="28" width="92" height="124" rx="8" fill="#d9edf5"/>
|
||||||
|
<path d="M112 28v30h24" fill="#b9ced8"/>
|
||||||
|
<path d="M58 74h64M58 94h52M58 114h60" stroke="#20384d" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
<rect x="58" y="42" width="32" height="20" rx="4" fill="#d7a85d"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 443 B |
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Envelope">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#081524"/>
|
||||||
|
<rect x="28" y="50" width="124" height="82" rx="8" fill="#d9edf5"/>
|
||||||
|
<path d="M31 56 90 99l59-43M31 128l42-40M149 128l-42-40" fill="none" stroke="#20384d" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
<path d="M112 72h22" stroke="#d7a85d" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 443 B |
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Environmental object">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#071321"/>
|
||||||
|
<path d="M42 134h96" stroke="#d7a85d" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<path d="M69 134V74h42v60" fill="#142b3f" stroke="#7dc1ff" stroke-width="6"/>
|
||||||
|
<path d="M54 76c10-34 62-34 72 0" fill="#2f6f53"/>
|
||||||
|
<path d="M90 46v34" stroke="#8a6841" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 469 B |
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Parcel">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#071321"/>
|
||||||
|
<rect x="38" y="50" width="104" height="92" rx="8" fill="#8a6841" stroke="#d7a85d" stroke-width="5"/>
|
||||||
|
<path d="M90 51v90M39 84h102" stroke="#f2d28a" stroke-width="7"/>
|
||||||
|
<path d="M58 114h32" stroke="#20384d" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 413 B |
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Photograph">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#071321"/>
|
||||||
|
<rect x="34" y="36" width="112" height="108" rx="8" fill="#d9edf5"/>
|
||||||
|
<rect x="46" y="50" width="88" height="68" rx="4" fill="#18344e"/>
|
||||||
|
<circle cx="112" cy="68" r="10" fill="#d7a85d"/>
|
||||||
|
<path d="M50 116 80 88l20 18 14-13 20 23" fill="#6fa8cc"/>
|
||||||
|
<path d="M58 132h64" stroke="#20384d" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 497 B |
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="Traffic sign">
|
||||||
|
<rect width="180" height="180" rx="28" fill="#071321"/>
|
||||||
|
<path d="M90 28 144 82l-54 54-54-54z" fill="#d7a85d" stroke="#f2d28a" stroke-width="5"/>
|
||||||
|
<path d="M90 65v36M72 83h36" stroke="#20384d" stroke-width="9" stroke-linecap="round"/>
|
||||||
|
<path d="M90 136v26" stroke="#d9edf5" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 428 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Bathroom">
|
||||||
|
<rect width="240" height="240" fill="#0b1d2d"/>
|
||||||
|
<path d="M36 72h168v124H36z" fill="#142d42"/>
|
||||||
|
<path d="M58 124h90v34c0 22-18 40-40 40H98c-22 0-40-18-40-40z" fill="#d9edf5" opacity=".92"/>
|
||||||
|
<path d="M52 116h102" stroke="#f2c46b" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<path d="M164 96h32v86h-32z" fill="#c4d8df"/>
|
||||||
|
<path d="M170 91h20" stroke="#7dc1ff" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
<path d="M58 57h55v40H58z" fill="#203d55" stroke="#7dc1ff" stroke-width="3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 590 B |
@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Building">
|
||||||
|
<rect width="240" height="240" fill="#081524"/>
|
||||||
|
<path d="M48 70h144v126H48z" fill="#172b3f" stroke="#6fa8cc" stroke-width="3"/>
|
||||||
|
<path d="M72 92h28v28H72zm70 0h28v28h-28zM72 140h28v28H72zm70 0h28v28h-28z" fill="#d7a85d" opacity=".68"/>
|
||||||
|
<path d="M38 70h164L120 36z" fill="#233c52"/>
|
||||||
|
<path d="M108 196v-42h24v42" fill="#0b1726"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 440 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Driving test centre">
|
||||||
|
<rect width="240" height="240" fill="#071321"/>
|
||||||
|
<path d="M38 70h164v92H38z" fill="#183149" stroke="#6fa8cc" stroke-width="3"/>
|
||||||
|
<path d="M62 98h45v32H62zm70 0h45v32h-45z" fill="#0d1d2c"/>
|
||||||
|
<path d="M48 162h144v28H48z" fill="#26384a"/>
|
||||||
|
<path d="M70 190c14-20 86-20 100 0" stroke="#d7a85d" stroke-width="6" fill="none" stroke-linecap="round"/>
|
||||||
|
<path d="M82 55h76" stroke="#f2c46b" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<text x="120" y="153" text-anchor="middle" fill="#d9edf5" font-family="Arial, sans-serif" font-size="18" font-weight="700">TEST</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 682 B |
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Garage workshop">
|
||||||
|
<rect width="240" height="240" fill="#0a1420"/>
|
||||||
|
<path d="M34 72h172v116H34z" fill="#172738"/>
|
||||||
|
<path d="M54 104h132v84H54z" fill="#101b28" stroke="#6fa8cc" stroke-width="3"/>
|
||||||
|
<path d="M54 126h132M54 148h132M54 170h132" stroke="#294863" stroke-width="4"/>
|
||||||
|
<path d="M66 92h58" stroke="#d7a85d" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<path d="M144 88l26 26M170 88l-26 26" stroke="#d9edf5" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 552 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Kitchen">
|
||||||
|
<rect width="240" height="240" fill="#0b1726"/>
|
||||||
|
<path d="M30 66h180v126H30z" fill="#172b3b"/>
|
||||||
|
<path d="M46 86h54v90H46zm116 0h32v90h-32z" fill="#d7a85d" opacity=".65"/>
|
||||||
|
<path d="M103 86h56v90h-56z" fill="#20384c"/>
|
||||||
|
<path d="M112 98h38v30h-38z" fill="#0d1c2a" stroke="#7dc1ff" stroke-width="3"/>
|
||||||
|
<path d="M42 176h156M56 63v-20h75v20" stroke="#d9edf5" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<circle cx="177" cy="116" r="5" fill="#f2c46b"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Road">
|
||||||
|
<defs><linearGradient id="sky" x1="0" x2="0" y1="0" y2="1"><stop stop-color="#153454"/><stop offset="1" stop-color="#071323"/></linearGradient></defs>
|
||||||
|
<rect width="240" height="240" fill="url(#sky)"/>
|
||||||
|
<path d="M0 155h240v85H0z" fill="#203142"/>
|
||||||
|
<path d="M92 240 114 112h12l24 128z" fill="#303946"/>
|
||||||
|
<path d="M120 126v26M120 174v30M120 225v15" stroke="#f2c46b" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M0 155c42-12 75-14 116-16 52-2 83-9 124-24v40c-50 14-92 18-137 19-40 1-69 5-103 16z" fill="#2f4f4f" opacity=".55"/>
|
||||||
|
<path d="M24 119h42v30H24zm145-20h39v34h-39z" fill="#101e30" stroke="#6fa8cc" stroke-width="2"/>
|
||||||
|
<path d="M31 149h171" stroke="#d7a85d" stroke-width="2" opacity=".55"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 811 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Vehicle interior">
|
||||||
|
<rect width="240" height="240" fill="#071321"/>
|
||||||
|
<path d="M24 76c18-24 174-24 192 0l-18 65H42z" fill="#12263b" stroke="#6fa8cc" stroke-width="3"/>
|
||||||
|
<path d="M52 88h136l-13 39H65z" fill="#203a52"/>
|
||||||
|
<path d="M28 141h184v54H28z" fill="#0e1d2c"/>
|
||||||
|
<circle cx="82" cy="165" r="25" fill="none" stroke="#d7a85d" stroke-width="7"/>
|
||||||
|
<path d="M132 155h52M132 174h38M55 113l-24 17M185 113l24 17" stroke="#7dc1ff" stroke-width="5" stroke-linecap="round"/>
|
||||||
|
<path d="M70 207h100" stroke="#304d68" stroke-width="10" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 648 B |
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Waiting room">
|
||||||
|
<rect width="240" height="240" fill="#081524"/>
|
||||||
|
<path d="M32 64h176v130H32z" fill="#14283a"/>
|
||||||
|
<path d="M54 122h132v42H54z" fill="#233b50"/>
|
||||||
|
<path d="M62 96h38v68M110 96h38v68M158 96h20v68" stroke="#7dc1ff" stroke-width="7" stroke-linecap="round"/>
|
||||||
|
<circle cx="178" cy="67" r="24" fill="#101e30" stroke="#d7a85d" stroke-width="4"/>
|
||||||
|
<path d="M178 52v17l12 8" stroke="#d9edf5" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M48 194h144" stroke="#d7a85d" stroke-width="4" opacity=".7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 608 B |
@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
const realMode = root.dataset.mode === "real";
|
const realMode = root.dataset.mode === "real";
|
||||||
|
const replayMode = root.dataset.mode === "replay";
|
||||||
const simulationMode = root.dataset.mode === "simulation";
|
const simulationMode = root.dataset.mode === "simulation";
|
||||||
|
const controllableMode = replayMode || simulationMode;
|
||||||
const snapshotUrl = root.dataset.snapshotUrl || "";
|
const snapshotUrl = root.dataset.snapshotUrl || "";
|
||||||
const failedImageUrls = new Set();
|
const failedImageUrls = new Set();
|
||||||
const preloadedImageUrls = new Set();
|
const preloadedImageUrls = new Set();
|
||||||
@ -73,7 +75,14 @@
|
|||||||
diagnosticsRefresh: root.querySelector("[data-diagnostics-refresh]"),
|
diagnosticsRefresh: root.querySelector("[data-diagnostics-refresh]"),
|
||||||
diagnosticsBuild: root.querySelector("[data-diagnostics-build]"),
|
diagnosticsBuild: root.querySelector("[data-diagnostics-build]"),
|
||||||
diagnosticsOperations: root.querySelector("[data-diagnostics-operations]"),
|
diagnosticsOperations: root.querySelector("[data-diagnostics-operations]"),
|
||||||
diagnosticValues: root.querySelectorAll("[data-diagnostic-key]")
|
diagnosticValues: root.querySelectorAll("[data-diagnostic-key]"),
|
||||||
|
replayChapter: root.querySelector("[data-replay-chapter]"),
|
||||||
|
replayScene: root.querySelector("[data-replay-scene]"),
|
||||||
|
detailPanel: root.querySelector("[data-detail-panel]"),
|
||||||
|
detailClose: root.querySelector("[data-detail-close]"),
|
||||||
|
detailTone: root.querySelector("[data-detail-tone]"),
|
||||||
|
detailTitle: root.querySelector("[data-detail-title]"),
|
||||||
|
detailBody: root.querySelector("[data-detail-body]")
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!scenes.length) {
|
if (!scenes.length) {
|
||||||
@ -97,6 +106,7 @@
|
|||||||
|
|
||||||
updateProgress(current);
|
updateProgress(current);
|
||||||
updateSceneCopy(current);
|
updateSceneCopy(current);
|
||||||
|
updateReplayBrowser(current);
|
||||||
const layout = sceneLayout(current);
|
const layout = sceneLayout(current);
|
||||||
reconcileCharacters(current, previous, layout);
|
reconcileCharacters(current, previous, layout);
|
||||||
reconcileLocation(current, previous);
|
reconcileLocation(current, previous);
|
||||||
@ -149,21 +159,75 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProgress(scene) {
|
function updateProgress(scene) {
|
||||||
text(dom.stageLabel, scene.stage);
|
const replayText = replayMode ? replayStatusText(scene) : scene.stage;
|
||||||
|
text(dom.stageLabel, replayText);
|
||||||
text(dom.currentScene, scene.sceneNumber);
|
text(dom.currentScene, scene.sceneNumber);
|
||||||
animateNumber(dom.progressPercent, scene.progressPercent, "%");
|
animateNumber(dom.progressPercent, scene.progressPercent, "%");
|
||||||
text(dom.elapsedTime, scene.elapsedTime || "Calculating");
|
text(dom.elapsedTime, scene.elapsedTime || "Calculating");
|
||||||
text(dom.estimatedRemaining, scene.estimatedRemaining);
|
text(dom.estimatedRemaining, scene.estimatedRemaining);
|
||||||
text(dom.chapterCount, `${scene.chaptersAnalysed} of ${scene.chapterTotal}`);
|
text(dom.chapterCount, `${scene.chaptersAnalysed} of ${scene.chapterTotal}`);
|
||||||
text(dom.sceneCount, `${scene.scenesAnalysed} of ${scene.sceneTotal}`);
|
text(dom.sceneCount, `${scene.scenesAnalysed} of ${scene.sceneTotal}`);
|
||||||
text(dom.currentStage, scene.stage);
|
text(dom.currentStage, replayText);
|
||||||
if (dom.progressRing) dom.progressRing.style.setProperty("--progress-value", `${scene.progressPercent * 3.6}deg`);
|
if (dom.progressRing) dom.progressRing.style.setProperty("--progress-value", `${scene.progressPercent * 3.6}deg`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replayStatusText(scene) {
|
||||||
|
if (!scene) return "Replay";
|
||||||
|
const stateText = state.playing ? "Replaying" : "Paused at";
|
||||||
|
return `${stateText} ${scene.chapterLabel} Scene ${scene.sceneNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
function updateSceneCopy(scene) {
|
function updateSceneCopy(scene) {
|
||||||
text(dom.sceneSummary, scene.summary);
|
text(dom.sceneSummary, scene.summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupReplayBrowser() {
|
||||||
|
if (!replayMode || !dom.replayChapter || !dom.replayScene) return;
|
||||||
|
const chapters = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const failedChapters = (root.dataset.failedChapters || "")
|
||||||
|
.split(";")
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
scenes.forEach((scene) => {
|
||||||
|
const key = replayChapterKey(scene);
|
||||||
|
if (seen.has(key)) return;
|
||||||
|
seen.add(key);
|
||||||
|
chapters.push({ key, label: scene.chapterLabel || `Chapter ${scene.chapterNumber || ""}` });
|
||||||
|
});
|
||||||
|
|
||||||
|
const replayOptions = chapters
|
||||||
|
.map((chapter) => `<option value="${escapeHtml(chapter.key)}">${escapeHtml(chapter.label)}</option>`);
|
||||||
|
const gapOptions = failedChapters
|
||||||
|
.map((chapter, index) => `<option value="failed-${index}" disabled>${escapeHtml(chapter)}</option>`);
|
||||||
|
dom.replayChapter.innerHTML = replayOptions.concat(gapOptions).join("");
|
||||||
|
populateReplayScenes(chapters[0]?.key || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateReplayScenes(chapterKey) {
|
||||||
|
if (!dom.replayScene) return;
|
||||||
|
const options = scenes
|
||||||
|
.map((scene, index) => ({ scene, index }))
|
||||||
|
.filter((item) => replayChapterKey(item.scene) === chapterKey);
|
||||||
|
dom.replayScene.innerHTML = options
|
||||||
|
.map((item) => `<option value="${item.index}">Scene ${item.scene.sceneNumber} - ${escapeHtml(compactPhrase(item.scene.title || item.scene.summary || "Saved scene", 58))}</option>`)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateReplayBrowser(scene) {
|
||||||
|
if (!replayMode || !scene || !dom.replayChapter || !dom.replayScene) return;
|
||||||
|
const key = replayChapterKey(scene);
|
||||||
|
if (dom.replayChapter.value !== key) {
|
||||||
|
dom.replayChapter.value = key;
|
||||||
|
populateReplayScenes(key);
|
||||||
|
}
|
||||||
|
dom.replayScene.value = String(state.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replayChapterKey(scene) {
|
||||||
|
return String(scene?.chapterNumber || scene?.chapterLabel || "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
function setEntityImage(image, url, resolution, altText) {
|
function setEntityImage(image, url, resolution, altText) {
|
||||||
if (!image) return;
|
if (!image) return;
|
||||||
image.alt = altText;
|
image.alt = altText;
|
||||||
@ -255,7 +319,6 @@
|
|||||||
node.style.setProperty("--opacity", String(position.opacity));
|
node.style.setProperty("--opacity", String(position.opacity));
|
||||||
node.style.setProperty("--z", String(position.z));
|
node.style.setProperty("--z", String(position.z));
|
||||||
node.dataset.layoutRole = position.layoutRole;
|
node.dataset.layoutRole = position.layoutRole;
|
||||||
node.dataset.labelSide = position.labelSide || "";
|
|
||||||
setEntityImage(node.querySelector("img"), character.imagePath, character.imageResolution, `${character.name} prototype portrait`);
|
setEntityImage(node.querySelector("img"), character.imagePath, character.imageResolution, `${character.name} prototype portrait`);
|
||||||
text(node.querySelector("[data-character-name]"), character.name);
|
text(node.querySelector("[data-character-name]"), character.name);
|
||||||
const label = conciseCharacterLabel(character, character.id === scene.povCharacterId);
|
const label = conciseCharacterLabel(character, character.id === scene.povCharacterId);
|
||||||
@ -356,8 +419,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateKnowledgeThreads(scene) {
|
function updateKnowledgeThreads(scene) {
|
||||||
renderInsights(dom.knowledge, scene.knowledgeThreads);
|
renderInsights(dom.knowledge, (scene.knowledgeThreads || []).slice(0, 3));
|
||||||
renderInsights(dom.observations, scene.observations);
|
renderOverflowSummary(dom.knowledge, (scene.knowledgeThreads || []).length - 3, "thread");
|
||||||
|
renderInsights(dom.observations, (scene.observations || []).slice(0, 2));
|
||||||
|
renderOverflowSummary(dom.observations, (scene.observations || []).length - 2, "observation");
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSceneRibbon(scene) {
|
function updateSceneRibbon(scene) {
|
||||||
@ -427,8 +492,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateFeeds(scene) {
|
function updateFeeds(scene) {
|
||||||
renderList(dom.discoveries, scene.latestDiscoveries);
|
renderList(dom.discoveries, (scene.latestDiscoveries || []).slice(0, 4));
|
||||||
renderList(dom.activityFeed, scene.activityFeed);
|
renderList(dom.activityFeed, (scene.activityFeed || []).slice(0, 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInsights(container, items) {
|
function renderInsights(container, items) {
|
||||||
@ -439,15 +504,23 @@
|
|||||||
node = document.createElement("article");
|
node = document.createElement("article");
|
||||||
node.className = "story-exp-insight is-entering";
|
node.className = "story-exp-insight is-entering";
|
||||||
node.dataset.insightId = item.id;
|
node.dataset.insightId = item.id;
|
||||||
node.innerHTML = `<span></span><strong></strong><p></p>`;
|
node.innerHTML = `<strong></strong><p></p>`;
|
||||||
container.append(node);
|
container.append(node);
|
||||||
settleEntering(node);
|
settleEntering(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
node.dataset.tone = item.tone || "";
|
node.dataset.tone = item.tone || "";
|
||||||
text(node.querySelector("span"), item.tone || "Signal");
|
|
||||||
text(node.querySelector("strong"), item.title);
|
text(node.querySelector("strong"), item.title);
|
||||||
text(node.querySelector("p"), item.detail);
|
text(node.querySelector("p"), item.detail);
|
||||||
|
node.title = `${item.title || ""}\n${item.detail || ""}`.trim();
|
||||||
|
node.tabIndex = 0;
|
||||||
|
node.onclick = () => openDetail(item);
|
||||||
|
node.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
openDetail(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
container.querySelectorAll("[data-insight-id]").forEach((node) => {
|
container.querySelectorAll("[data-insight-id]").forEach((node) => {
|
||||||
@ -459,6 +532,18 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDetail(item) {
|
||||||
|
if (!dom.detailPanel) return;
|
||||||
|
text(dom.detailTone, item.tone || "Story detail");
|
||||||
|
text(dom.detailTitle, item.title || "Story detail");
|
||||||
|
text(dom.detailBody, item.detail || "");
|
||||||
|
dom.detailPanel.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetail() {
|
||||||
|
if (dom.detailPanel) dom.detailPanel.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderOverflowSummary(container, hiddenCount, noun) {
|
function renderOverflowSummary(container, hiddenCount, noun) {
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
let node = container.querySelector("[data-insight-overflow]");
|
let node = container.querySelector("[data-insight-overflow]");
|
||||||
@ -690,7 +775,7 @@
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
z: 7,
|
z: 7,
|
||||||
layoutRole: "pov",
|
layoutRole: "pov",
|
||||||
labelSide: "pov"
|
labelSide: "below"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -721,7 +806,7 @@
|
|||||||
opacity: contextual ? 0.76 : 0.94,
|
opacity: contextual ? 0.76 : 0.94,
|
||||||
z: contextual ? 3 : 5,
|
z: contextual ? 3 : 5,
|
||||||
layoutRole: contextual ? "context" : slot.role,
|
layoutRole: contextual ? "context" : slot.role,
|
||||||
labelSide: contextual && pov && resolved.x < positions.get(pov.id).x ? "left" : slot.role === "partner" || slot.role === "support" ? "right" : "below"
|
labelSide: "below"
|
||||||
};
|
};
|
||||||
placed.push(position);
|
placed.push(position);
|
||||||
positions.set(character.id, position);
|
positions.set(character.id, position);
|
||||||
@ -1096,10 +1181,15 @@
|
|||||||
function startTimer() {
|
function startTimer() {
|
||||||
if (!scenes.length) return;
|
if (!scenes.length) return;
|
||||||
stopTimer();
|
stopTimer();
|
||||||
if (!state.playing || !simulationMode) return;
|
if (!state.playing || !controllableMode) return;
|
||||||
state.timer = window.setInterval(() => {
|
state.timer = window.setInterval(() => {
|
||||||
const next = state.index >= scenes.length - 1 ? 0 : state.index + 1;
|
const next = state.index >= scenes.length - 1 ? (replayMode ? scenes.length - 1 : 0) : state.index + 1;
|
||||||
transitionToNextState(next);
|
transitionToNextState(next);
|
||||||
|
if (replayMode && next >= scenes.length - 1) {
|
||||||
|
state.playing = false;
|
||||||
|
text(dom.play, "Play");
|
||||||
|
stopTimer();
|
||||||
|
}
|
||||||
}, state.intervalMs);
|
}, state.intervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1307,6 +1397,7 @@
|
|||||||
if (key === "pollingActive") return state.pollTimer ? "active" : "stopped";
|
if (key === "pollingActive") return state.pollTimer ? "active" : "stopped";
|
||||||
if (key === "pollingStoppedReason") return state.pollingStoppedReason || "none";
|
if (key === "pollingStoppedReason") return state.pollingStoppedReason || "none";
|
||||||
if (key === "currentPollingInterval") return realMode ? `${document.hidden ? 15000 : 7000}ms` : `${state.intervalMs}ms`;
|
if (key === "currentPollingInterval") return realMode ? `${document.hidden ? 15000 : 7000}ms` : `${state.intervalMs}ms`;
|
||||||
|
if (key === "replayIndex") return replayMode ? state.index + 1 : diagnostics[key];
|
||||||
if (key === "latestSnapshotAge") return latestSnapshotAge();
|
if (key === "latestSnapshotAge") return latestSnapshotAge();
|
||||||
const value = diagnostics[key];
|
const value = diagnostics[key];
|
||||||
return value === null || value === undefined || value === "" ? "none" : value;
|
return value === null || value === undefined || value === "" ? "none" : value;
|
||||||
@ -1394,37 +1485,67 @@
|
|||||||
return window.CSS?.escape ? window.CSS.escape(value) : String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
return window.CSS?.escape ? window.CSS.escape(value) : String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
root.querySelector("[data-control='restart']")?.addEventListener("click", () => {
|
function goToReplayIndex(index, restartPlayback) {
|
||||||
if (!simulationMode) return;
|
if (!scenes.length) return;
|
||||||
|
transitionToNextState(Math.max(0, Math.min(scenes.length - 1, index)));
|
||||||
|
if (restartPlayback) startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelectorAll("[data-control='restart']").forEach((button) => button.addEventListener("click", () => {
|
||||||
|
if (!controllableMode) return;
|
||||||
transitionToNextState(0);
|
transitionToNextState(0);
|
||||||
startTimer();
|
startTimer();
|
||||||
});
|
}));
|
||||||
|
|
||||||
root.querySelector("[data-control='previous']")?.addEventListener("click", () => {
|
root.querySelectorAll("[data-control='previous']").forEach((button) => button.addEventListener("click", () => {
|
||||||
if (!simulationMode) return;
|
if (!controllableMode) return;
|
||||||
transitionToNextState(state.index <= 0 ? scenes.length - 1 : state.index - 1);
|
transitionToNextState(state.index <= 0 ? (replayMode ? 0 : scenes.length - 1) : state.index - 1);
|
||||||
startTimer();
|
startTimer();
|
||||||
});
|
}));
|
||||||
|
|
||||||
root.querySelector("[data-control='next']")?.addEventListener("click", () => {
|
root.querySelectorAll("[data-control='next']").forEach((button) => button.addEventListener("click", () => {
|
||||||
if (!simulationMode) return;
|
if (!controllableMode) return;
|
||||||
transitionToNextState(state.index >= scenes.length - 1 ? 0 : state.index + 1);
|
transitionToNextState(state.index >= scenes.length - 1 ? (replayMode ? scenes.length - 1 : 0) : state.index + 1);
|
||||||
startTimer();
|
startTimer();
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
root.querySelectorAll("[data-control='first']").forEach((button) => button.addEventListener("click", () => {
|
||||||
|
if (!replayMode) return;
|
||||||
|
goToReplayIndex(0, false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
root.querySelectorAll("[data-control='last']").forEach((button) => button.addEventListener("click", () => {
|
||||||
|
if (!replayMode) return;
|
||||||
|
goToReplayIndex(scenes.length - 1, false);
|
||||||
|
}));
|
||||||
|
|
||||||
dom.play?.addEventListener("click", () => {
|
dom.play?.addEventListener("click", () => {
|
||||||
if (!simulationMode) return;
|
if (!controllableMode) return;
|
||||||
state.playing = !state.playing;
|
state.playing = !state.playing;
|
||||||
text(dom.play, state.playing ? "Pause" : "Play");
|
text(dom.play, state.playing ? "Pause" : "Play");
|
||||||
if (state.playing) startTimer(); else stopTimer();
|
if (state.playing) startTimer(); else stopTimer();
|
||||||
|
if (replayMode) updateProgress(loadSceneState(state.index));
|
||||||
});
|
});
|
||||||
|
|
||||||
dom.speed?.addEventListener("input", () => {
|
dom.speed?.addEventListener("input", () => {
|
||||||
if (!simulationMode) return;
|
if (!controllableMode) return;
|
||||||
state.intervalMs = Number.parseInt(dom.speed.value, 10) || 6200;
|
state.intervalMs = Number.parseInt(dom.speed.value, 10) || 6200;
|
||||||
startTimer();
|
startTimer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
dom.detailClose?.addEventListener("click", closeDetail);
|
||||||
|
|
||||||
|
dom.replayChapter?.addEventListener("change", () => {
|
||||||
|
if (!replayMode) return;
|
||||||
|
populateReplayScenes(dom.replayChapter.value);
|
||||||
|
goToReplayIndex(Number.parseInt(dom.replayScene.value, 10) || 0, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.replayScene?.addEventListener("change", () => {
|
||||||
|
if (!replayMode) return;
|
||||||
|
goToReplayIndex(Number.parseInt(dom.replayScene.value, 10) || 0, false);
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (!realMode || state.terminal) return;
|
if (!realMode || state.terminal) return;
|
||||||
startPolling();
|
startPolling();
|
||||||
@ -1454,6 +1575,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scenes.length) {
|
if (scenes.length) {
|
||||||
|
setupReplayBrowser();
|
||||||
transitionToNextState(realMode ? authoritativeSceneIndex(scenes) : 0);
|
transitionToNextState(realMode ? authoritativeSceneIndex(scenes) : 0);
|
||||||
startTimer();
|
startTimer();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||