namespace IdleSpectator; /// /// Fact-preserving activity sentence formatting with colored actor/target names. /// supplies templates from the verb map and authored species bags. /// public static class ActivityProse { public const string NameColorHex = "#F0C14A"; public static string ColorName(string name) { if (string.IsNullOrEmpty(name)) { return ""; } return "" + name + ""; } public static void Format( ActivityKind kind, string key, string actorName, string targetName, bool targetIsActor, string placeOrFallback, string rawFact, long subjectId, int lineIndex, out string plain, out string rich) { ActivityContext ctx = ActivityContext.ForHarness( actorName, targetIsActor ? targetName : "", place: placeOrFallback); if (!targetIsActor && !string.IsNullOrEmpty(targetName) && string.IsNullOrEmpty(ctx.PlaceLabel)) { ctx.PlaceLabel = targetName; } if (targetIsActor) { ctx.TargetName = targetName ?? ""; ctx.TargetIsActor = !string.IsNullOrEmpty(targetName); } Format(kind, key, ctx, rawFact, subjectId, lineIndex, out plain, out rich); } public static void Format( ActivityKind kind, string key, ActivityContext ctx, string rawFact, long subjectId, int lineIndex, out string plain, out string rich) { plain = ""; rich = ""; if (ctx == null) { ctx = ActivityContext.Empty; } EnsureDerivedFields(ctx); EnsureChildFromKey(ctx, key); bool hasTarget = ctx.TargetIsActor && !string.IsNullOrEmpty(ctx.TargetName); // Trait salts variety only (dossier shows trait chips) - never names the trait in prose. long varietyId = subjectId ^ ActivityClauseAssembler.TraitVarietySaltPublic(ctx); string template = ActivityClauseAssembler.Assemble( ctx, key, rawFact, hasTarget, varietyId, lineIndex); // Unreachable by composer contract. Preserve empty output so missing authored coverage remains auditable. if (string.IsNullOrEmpty(template)) { return; } plain = Apply(template, ctx, colored: false); rich = Apply(template, ctx, colored: true); _ = kind; } private static void EnsureDerivedFields(ActivityContext ctx) { if (ctx.Voice == null || !ctx.Voice.LiveSpeciesId.Equals(ctx.SpeciesId ?? "", System.StringComparison.Ordinal)) { ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(ctx.SpeciesId); if (asset != null) { ActivityVoiceResolver.ApplyToContext(ctx, asset); } else { ctx.Voice = ActivityVoiceResolver.Resolve(ctx.SpeciesId); ctx.BaseSpeciesId = ctx.Voice.BaseSpeciesId; ctx.Family = ctx.Voice.BaseFamily; ctx.MannerTag = ActivityVoiceResolver.TagName(ctx.Voice.EffectiveActionTag); ctx.ModifierTag = ActivityVoiceResolver.ModifierName(ctx.Voice.Modifier); } } if (string.IsNullOrEmpty(ctx.RoleLabel)) { ctx.RoleLabel = ActivityInterestTable.DeriveRoleLabel(ctx.IsKing, ctx.IsLeader, ctx.IsWarrior); } if (string.IsNullOrEmpty(ctx.TopTraitLabel) && !string.IsNullOrEmpty(ctx.TopTraitId)) { ctx.TopTraitLabel = ActivityInterestTable.TraitLabelFromId(ctx.TopTraitId); } } /// Mark child life-stage from task key when context did not already capture it. private static void EnsureChildFromKey(ActivityContext ctx, string key) { if (ctx == null || ctx.IsChild || string.IsNullOrEmpty(key)) { return; } if (key.StartsWith("child_", System.StringComparison.OrdinalIgnoreCase)) { ctx.IsChild = true; } } public static string HumanizeJobPublic(string jobId) { return HumanizeJob(jobId); } /// Legacy helper used by older call sites / harness. public static string Format( ActivityKind kind, string key, string rawFact, string target, long subjectId, int lineIndex) { Format( kind, key, ActivityContext.ForHarness("", target, place: target), rawFact, subjectId, lineIndex, out string plain, out _); return plain; } private static string Apply(string template, ActivityContext ctx, bool colored) { if (string.IsNullOrEmpty(template)) { return ""; } string actorRaw = string.IsNullOrEmpty(ctx.ActorName) ? "Someone" : ctx.ActorName; string actor = colored ? ColorName(actorRaw) : actorRaw; string targetRaw = ctx.TargetName ?? ""; string target = ""; if (!string.IsNullOrEmpty(targetRaw)) { target = colored && ctx.TargetIsActor ? ColorName(targetRaw) : targetRaw; } string species = !string.IsNullOrEmpty(ctx.SpeciesLabel) ? ctx.SpeciesLabel : (string.IsNullOrEmpty(ctx.SpeciesId) ? "creature" : ctx.SpeciesId.Replace('_', ' ')); string place = ctx.PlaceLabel ?? ""; if (ActivityInterestTable.LooksLikeSpeciesPublic(place, ctx.SpeciesId)) { place = ""; } string job = HumanizeJob(ctx.JobId); string carrying = string.IsNullOrEmpty(ctx.CarryingLabel) ? "goods" : ctx.CarryingLabel; string withClause = string.IsNullOrEmpty(target) ? "" : (" with " + target); string placeAt = string.IsNullOrEmpty(place) ? "" : (" in " + place); string jobAs = string.IsNullOrEmpty(job) ? "" : (" as " + job); string t = template; t = t.Replace("{actor}", actor); t = t.Replace("{species}", species); t = t.Replace("{with}", withClause); t = t.Replace("{place_at}", placeAt); t = t.Replace("{job_as}", jobAs); t = t.Replace("{carrying}", carrying); t = t.Replace("{place}", string.IsNullOrEmpty(place) ? "town" : place); t = t.Replace("{job}", string.IsNullOrEmpty(job) ? "their work" : job); if (string.IsNullOrEmpty(target)) { t = t.Replace(" with {target}", "") .Replace(" on {target}", "") .Replace(" toward {target}", "") .Replace(" after {target}", "") .Replace(" at {target}", "") .Replace(" against {target}", "") .Replace(" before {target}", "") .Replace(" beside {target}", "") .Replace(" near {target}", "") .Replace(" around {target}", "") .Replace(" past {target}", "") .Replace(" behind {target}", "") .Replace(" under {target}", "") .Replace(" over {target}", "") .Replace(" across {target}", "") .Replace(" from {target}", "") .Replace(" into {target}", "") .Replace(" upon {target}", "") .Replace(" {target}", "") .Replace("{target}", "someone"); } else { t = t.Replace("{target}", target); } // Collapse leftover double spaces from empty optional clauses. while (t.IndexOf(" ", System.StringComparison.Ordinal) >= 0) { t = t.Replace(" ", " "); } return t.Trim(); } private static string HumanizeJob(string jobId) { if (string.IsNullOrEmpty(jobId)) { return ""; } string s = jobId.Replace('_', ' ').Trim(); if (s.StartsWith("job ", System.StringComparison.OrdinalIgnoreCase)) { s = s.Substring(4).Trim(); } return LowerFirst(s); } private static string LowerFirst(string s) { if (string.IsNullOrEmpty(s)) { return s; } if (s.Length == 1) { return s.ToLowerInvariant(); } return char.ToLowerInvariant(s[0]) + s.Substring(1); } }