using System; using System.Collections.Generic; using System.Globalization; using System.Text; using UnityEngine; namespace IdleSpectator; /// /// Single voice owner for saga/caption English. /// Evidence drives copy; enum/taxonomy names never appear in player-facing strings. /// public static class SagaProse { public static readonly string[] BanList = { "earned rival", "earned a rival", "rivalry with", "saga hero", "fellow saga hero", "war foe", "plot ally", "likely successor", "a life in motion", "carries a kingdom", "holds the pack together", "a scheme is still unfolding", "lineage hangs by a thread", "what they built still needs defending" }; private static readonly HashSet TitleSmallWords = new HashSet(StringComparer.OrdinalIgnoreCase) { "of", "the", "a", "an", "in", "on", "and", "for", "to" }; private static int _memoryRevision; private static long _cacheFocusId; private static long _cacheOtherId; private static int _cacheRevision = -1; private static long _cacheLoverId; private static long _cacheFriendId; private static RelationEvidence _cacheEvidence; public static void BumpMemoryRevision() { _memoryRevision++; _cacheRevision = -1; } public static int MemoryRevision => _memoryRevision; public static bool ContainsBannedPhrase(string text) { if (string.IsNullOrEmpty(text)) { return false; } string lower = text.ToLowerInvariant(); for (int i = 0; i < BanList.Length; i++) { if (lower.IndexOf(BanList[i], StringComparison.Ordinal) >= 0) { return true; } } return false; } public static string TitleLabel(string raw) { if (string.IsNullOrEmpty(raw)) { return ""; } string titled = ActivityAssetCatalog.TitleCaseWords(raw.Trim()); if (string.IsNullOrEmpty(titled)) { return ""; } string[] parts = titled.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length <= 1) { return titled; } var sb = new StringBuilder(titled.Length); for (int i = 0; i < parts.Length; i++) { if (i > 0) { sb.Append(' '); } string p = parts[i]; bool small = i > 0 && i < parts.Length - 1 && TitleSmallWords.Contains(p); if (small) { sb.Append(p.ToLowerInvariant()); } else { sb.Append(p); } } return sb.ToString(); } public static string Sentence(string raw) { if (string.IsNullOrEmpty(raw)) { return ""; } string s = raw.Trim(); if (s.Length == 0) { return ""; } char first = s[0]; if (char.IsLetter(first) && char.IsLower(first)) { return char.ToUpperInvariant(first) + s.Substring(1); } return s; } /// Identity title for MCs. Empty when no real role (caller falls back to species/job). public static string Title(LifeSagaSlot slot, Actor actor, LifeSagaLifeMemory memory) { if (slot != null) { if (slot.IsKing) { string kingdom = FirstNonEmpty(slot.KingdomKey, KingdomName(actor)); return string.IsNullOrEmpty(kingdom) ? TitleLabel("King") : TitleLabel("King of " + kingdom); } if (slot.IsClanChief) { return TitleLabel("Clan Chief"); } if (slot.IsLeader) { string city = FirstNonEmpty(slot.CityKey, CityName(actor)); return string.IsNullOrEmpty(city) ? TitleLabel("City Leader") : TitleLabel("Leader of " + city); } if (slot.IsAlpha) { return TitleLabel("Pack Alpha"); } if (slot.IsArmyCaptain) { return TitleLabel("Army Captain"); } if (slot.IsFounder) { return TitleLabel("Founder"); } if (slot.IsPlotAuthor) { return TitleLabel("Plotter"); } if (slot.IsSingleton) { return TitleLabel("Last of Their Kind"); } } if (memory != null) { for (int i = 0; i < memory.Facts.Count; i++) { LifeSagaFact f = memory.Facts[i]; if (f == null) { continue; } if (f.Kind == LifeSagaFactKind.Founding) { return TitleLabel("Founder"); } if (f.Kind == LifeSagaFactKind.RoleChange) { string note = f.Note ?? ""; if (note.IndexOf("king", StringComparison.OrdinalIgnoreCase) >= 0) { string kingdom = FirstNonEmpty(slot?.KingdomKey, KingdomName(actor)); return string.IsNullOrEmpty(kingdom) ? TitleLabel("King") : TitleLabel("King of " + kingdom); } if (note.IndexOf("alpha", StringComparison.OrdinalIgnoreCase) >= 0) { if (LifeSagaRoster.UsesPackLanguage(actor)) { return TitleLabel("Pack Alpha"); } continue; } if (note.IndexOf("chief", StringComparison.OrdinalIgnoreCase) >= 0 || note.IndexOf("clan", StringComparison.OrdinalIgnoreCase) >= 0) { return TitleLabel("Clan Chief"); } if (note.IndexOf("leader", StringComparison.OrdinalIgnoreCase) >= 0) { string city = FirstNonEmpty(slot?.CityKey, CityName(actor)); return string.IsNullOrEmpty(city) ? TitleLabel("City Leader") : TitleLabel("Leader of " + city); } if (note.IndexOf("captain", StringComparison.OrdinalIgnoreCase) >= 0 || note.IndexOf("army", StringComparison.OrdinalIgnoreCase) >= 0) { return TitleLabel("Army Captain"); } } } } return ""; } public static string FormatFactLine(LifeSagaFact fact) { if (fact == null) { return ""; } bool unresolved = !fact.Resolved; string line = FormatFactBody(fact, unresolved); return Sentence(line); } public static string FormatLegacyLine(LifeSagaFact fact) { string body = FormatFactLine(fact); if (string.IsNullOrEmpty(body)) { return ""; } if (!string.IsNullOrEmpty(fact?.DateLabel)) { return fact.DateLabel + " ยท " + body; } return body; } public static string StakeLine(LifeSagaFact fact) { return FormatFactLine(fact); } public static string CastLabelFromEvidence(RelationEvidence e) { if (e == null || e.Class == RelationClass.None) { return ""; } switch (e.Class) { case RelationClass.Bond: return e.BondBroken ? TitleLabel("Once Loved") : TitleLabel("Lover"); case RelationClass.Friend: return e.BestFriend ? TitleLabel("Best Friend") : TitleLabel("Friend"); case RelationClass.Kin: if (e.KinKind == KinKind.Parent) return TitleLabel("Parent"); if (e.KinKind == KinKind.Child) return TitleLabel("Child"); if (e.KinKind == KinKind.Heir) return TitleLabel("Heir"); if (e.KinKind == KinKind.Packmate) return TitleLabel("Packmate"); return TitleLabel("Kin"); case RelationClass.Foe: if (e.FoeKind == FoeKind.War) return TitleLabel("War Enemy"); if (e.FoeKind == FoeKind.Plot) return TitleLabel("Plot Enemy"); if (e.FoeKind == FoeKind.Blood) return TitleLabel("Blood Foe"); return TitleLabel("Old Foe"); case RelationClass.PlotPartner: return TitleLabel("Plot Partner"); case RelationClass.Grief: return TitleLabel("Once Loved"); default: return ""; } } public static string CastLabel(LifeSagaFact fact, Actor other) { if (fact == null) { return ""; } RelationEvidence e = EvidenceFromFact(fact, other); return CastLabelFromEvidence(e); } public static string CastLabelLive(string kind) { switch ((kind ?? "").ToLowerInvariant()) { case "lover": return TitleLabel("Lover"); case "bestfriend": case "best friend": return TitleLabel("Best Friend"); case "friend": return TitleLabel("Friend"); case "parent": return TitleLabel("Parent"); case "child": return TitleLabel("Child"); case "heir": return TitleLabel("Heir"); case "packmate": return TitleLabel("Packmate"); case "kin": return TitleLabel("Kin"); case "onceloved": case "once loved": case "lost lover": return TitleLabel("Once Loved"); default: return TitleLabel(kind ?? ""); } } public static RelationEvidence ResolveRelation(long focusId, long otherId, Actor focus, Actor other) { if (focusId == 0 || otherId == 0 || focusId == otherId) { return RelationEvidence.None; } long loverId = focus != null ? EventFeedUtil.SafeId(ActorRelation.GetLover(focus)) : 0; long friendId = focus != null ? EventFeedUtil.SafeId(ActorRelation.GetBestFriend(focus)) : 0; if (_cacheFocusId == focusId && _cacheOtherId == otherId && _cacheRevision == _memoryRevision && _cacheLoverId == loverId && _cacheFriendId == friendId && _cacheEvidence != null) { return _cacheEvidence; } RelationEvidence best = RelationEvidence.None; int bestPri = 99; // Live bond if (loverId == otherId) { Consider(ref best, ref bestPri, new RelationEvidence { Class = RelationClass.Bond, OtherId = otherId, Priority = 1 }); } // Live friend if (friendId == otherId) { Consider(ref best, ref bestPri, new RelationEvidence { Class = RelationClass.Friend, BestFriend = true, OtherId = otherId, Priority = 5 }); } // Live kin if (focus != null && other != null) { foreach (Actor p in ActorRelation.EnumerateParents(focus, 4)) { if (EventFeedUtil.SafeId(p) == otherId) { Consider(ref best, ref bestPri, new RelationEvidence { Class = RelationClass.Kin, KinKind = KinKind.Parent, OtherId = otherId, Priority = 4 }); break; } } foreach (Actor c in ActorRelation.EnumerateChildren(focus, 4)) { if (EventFeedUtil.SafeId(c) == otherId) { Consider(ref best, ref bestPri, new RelationEvidence { Class = RelationClass.Kin, KinKind = KinKind.Child, OtherId = otherId, Priority = 4 }); break; } } if (LifeSagaPresentation.TryGetLikelySuccessor(focus, out Actor heir, out _) && EventFeedUtil.SafeId(heir) == otherId) { Consider(ref best, ref bestPri, new RelationEvidence { Class = RelationClass.Kin, KinKind = KinKind.Heir, OtherId = otherId, Priority = 4 }); } } LifeSagaLifeMemory mem = LifeSagaMemory.Get(focusId); bool alsoBond = loverId == otherId; bool alsoFoe = false; RelationEvidence foeEv = null; if (mem != null) { for (int i = 0; i < mem.Facts.Count; i++) { LifeSagaFact f = mem.Facts[i]; if (f == null || f.OtherId != otherId) { continue; } RelationEvidence fromFact = EvidenceFromFact(f, other); if (fromFact.Class == RelationClass.Bond && fromFact.BondBroken) { Consider(ref best, ref bestPri, fromFact); } else if (fromFact.Class == RelationClass.Foe) { alsoFoe = true; foeEv = fromFact; Consider(ref best, ref bestPri, fromFact); } else if (fromFact.Class != RelationClass.None) { Consider(ref best, ref bestPri, fromFact); } } } if (LifeSagaMemory.TryGetEarnedRival(focusId, out LifeSagaFact rival) && rival != null && rival.OtherId == otherId) { RelationEvidence r = EvidenceFromFact(rival, other); alsoFoe = true; foeEv = r; Consider(ref best, ref bestPri, r); } if (best != null && best.Class != RelationClass.None) { best.AlsoBond = alsoBond || best.Class == RelationClass.Bond; best.AlsoFoe = alsoFoe || best.Class == RelationClass.Foe; if (foeEv != null && best.Class == RelationClass.Bond) { best.FoeKind = foeEv.FoeKind; best.RematchCount = foeEv.RematchCount; best.RealmOrPlot = foeEv.RealmOrPlot; } } _cacheFocusId = focusId; _cacheOtherId = otherId; _cacheRevision = _memoryRevision; _cacheLoverId = loverId; _cacheFriendId = friendId; _cacheEvidence = best ?? RelationEvidence.None; return _cacheEvidence; } public static string ClassifyTipVerb(string tipLabel) { string t = (tipLabel ?? "").ToLowerInvariant(); if (t.IndexOf("fight", StringComparison.Ordinal) >= 0 || t.IndexOf("duel", StringComparison.Ordinal) >= 0 || t.IndexOf("battle", StringComparison.Ordinal) >= 0 || t.IndexOf("skirmish", StringComparison.Ordinal) >= 0 || t.IndexOf("mass -", StringComparison.Ordinal) >= 0 || t.IndexOf("war -", StringComparison.Ordinal) >= 0 || t.IndexOf("attack", StringComparison.Ordinal) >= 0 || t.IndexOf("slay", StringComparison.Ordinal) >= 0 || t.IndexOf("killed", StringComparison.Ordinal) >= 0) { return "combat"; } if (t.IndexOf("lover", StringComparison.Ordinal) >= 0 || t.IndexOf("love", StringComparison.Ordinal) >= 0 || t.IndexOf("smitten", StringComparison.Ordinal) >= 0 || t.IndexOf("kiss", StringComparison.Ordinal) >= 0 || t.IndexOf("intimacy", StringComparison.Ordinal) >= 0 || t.IndexOf("bound", StringComparison.Ordinal) >= 0 || t.IndexOf("mate", StringComparison.Ordinal) >= 0 || t.IndexOf("pregnant", StringComparison.Ordinal) >= 0) { return "love"; } if (t.IndexOf("mourn", StringComparison.Ordinal) >= 0 || t.IndexOf("grief", StringComparison.Ordinal) >= 0 || t.IndexOf("fallen", StringComparison.Ordinal) >= 0 || t.IndexOf("stands over", StringComparison.Ordinal) >= 0) { return "grief"; } if (t.IndexOf("plot", StringComparison.Ordinal) >= 0) { return "plot"; } // Sleep / dream / rest tips hitch poorly against closed biography stakes. if (t.IndexOf("sleep", StringComparison.Ordinal) >= 0 || t.IndexOf("dream", StringComparison.Ordinal) >= 0 || t.IndexOf("asleep", StringComparison.Ordinal) >= 0 || t.IndexOf("wakes", StringComparison.Ordinal) >= 0 || t.IndexOf("rests", StringComparison.Ordinal) >= 0 || t.IndexOf("naps", StringComparison.Ordinal) >= 0) { return "rest"; } return "other"; } public static string RoleFallbackStake(LifeSagaAdmissionReason reason, LifeSagaSlot slot, Actor actor) { switch (reason) { case LifeSagaAdmissionReason.King: { string k = FirstNonEmpty(slot?.KingdomKey, KingdomName(actor)); return string.IsNullOrEmpty(k) ? Sentence("Rules a kingdom") : Sentence("Rules " + k); } case LifeSagaAdmissionReason.Alpha: return LifeSagaRoster.UsesPackLanguage(actor) ? Sentence("Leads the pack") : Sentence("Leads a family"); case LifeSagaAdmissionReason.PlotAuthor: return Sentence("Leads a plot"); case LifeSagaAdmissionReason.Singleton: return Sentence("Last of their kind"); case LifeSagaAdmissionReason.RecurringAntagonist: return Sentence("A recurring threat"); case LifeSagaAdmissionReason.RareCreature: return Sentence("One of few of their kind"); case LifeSagaAdmissionReason.SpecialCreature: return Sentence("A rare power in the world"); case LifeSagaAdmissionReason.Founder: return Sentence("Defends what they founded"); case LifeSagaAdmissionReason.CityLeader: { string c = FirstNonEmpty(slot?.CityKey, CityName(actor)); return string.IsNullOrEmpty(c) ? Sentence("Leads a city") : Sentence("Leads " + c); } case LifeSagaAdmissionReason.EmergingStory: return Sentence("A life in motion"); default: return ""; } } /// True when a remembered role change merely restates the current Identity title. public static bool RoleChangeEchoesTitle(LifeSagaFact fact, string title) { if (fact == null || fact.Kind != LifeSagaFactKind.RoleChange || string.IsNullOrEmpty(title)) { return false; } string body = FormatFactLine(fact).ToLowerInvariant(); string current = title.ToLowerInvariant(); string[] roles = { "king", "queen", "leader", "alpha", "chief", "captain", "founder", "plotter" }; for (int i = 0; i < roles.Length; i++) { string role = roles[i]; if (body.IndexOf(role, StringComparison.Ordinal) >= 0 && current.IndexOf(role, StringComparison.Ordinal) >= 0) { return true; } } return false; } public static bool IsEnsembleTip(string tip) { if (string.IsNullOrEmpty(tip)) { return false; } string t = tip.TrimStart(); return t.StartsWith("Duel", StringComparison.OrdinalIgnoreCase) || t.StartsWith("Skirmish", StringComparison.OrdinalIgnoreCase) || t.StartsWith("Battle", StringComparison.OrdinalIgnoreCase) || t.StartsWith("Mass", StringComparison.OrdinalIgnoreCase) || t.StartsWith("War", StringComparison.OrdinalIgnoreCase) || t.StartsWith("Plot", StringComparison.OrdinalIgnoreCase) || t.StartsWith("Pack", StringComparison.OrdinalIgnoreCase) || t.StartsWith("Outbreak", StringComparison.OrdinalIgnoreCase); } private static void Consider(ref RelationEvidence best, ref int bestPri, RelationEvidence cand) { if (cand == null || cand.Class == RelationClass.None) { return; } int pri = cand.Priority > 0 ? cand.Priority : ClassPriority(cand.Class); if (pri < bestPri) { bestPri = pri; best = cand; best.Priority = pri; } } private static int ClassPriority(RelationClass c) { switch (c) { case RelationClass.Bond: return 1; case RelationClass.Foe: return 2; case RelationClass.PlotPartner: return 3; case RelationClass.Kin: return 4; case RelationClass.Friend: return 5; case RelationClass.Grief: return 1; default: return 9; } } private static RelationEvidence EvidenceFromFact(LifeSagaFact fact, Actor other) { if (fact == null) { return RelationEvidence.None; } var e = new RelationEvidence { OtherId = fact.OtherId }; switch (fact.Kind) { case LifeSagaFactKind.BondFormed: e.Class = RelationClass.Bond; e.Priority = 1; return e; case LifeSagaFactKind.BondBroken: e.Class = RelationClass.Bond; e.BondBroken = true; e.Priority = 1; return e; case LifeSagaFactKind.FriendFormed: e.Class = RelationClass.Friend; e.Priority = 5; return e; case LifeSagaFactKind.ParentChild: e.Class = RelationClass.Kin; e.KinKind = LifeSagaMemory.IsParentPerspective(fact) ? KinKind.Child : KinKind.Parent; e.Priority = 4; return e; case LifeSagaFactKind.RivalEarned: e.Class = RelationClass.Foe; e.Priority = 2; FillFoeFromNote(e, fact.Note); return e; case LifeSagaFactKind.WarJoin: e.Class = RelationClass.Foe; e.FoeKind = FoeKind.War; e.RealmOrPlot = FirstNonEmpty(fact.Note, fact.KingdomKey); e.Priority = 2; return e; case LifeSagaFactKind.PlotJoin: // Same-side plot join = partner; opposition notes use RivalEarned. e.Class = RelationClass.PlotPartner; e.RealmOrPlot = fact.Note ?? ""; e.Priority = 3; return e; case LifeSagaFactKind.Death: case LifeSagaFactKind.HardArcGrief: e.Class = RelationClass.Grief; e.Priority = 1; return e; case LifeSagaFactKind.Kill: case LifeSagaFactKind.HardArcCombat: case LifeSagaFactKind.CombatEncounter: // Not automatically a cast foe unless RivalEarned. return RelationEvidence.None; default: return RelationEvidence.None; } } private static void FillFoeFromNote(RelationEvidence e, string note) { if (string.IsNullOrEmpty(note)) { e.FoeKind = FoeKind.Generic; return; } if (note.StartsWith("opposed_in_war:", StringComparison.Ordinal)) { e.FoeKind = FoeKind.War; e.RealmOrPlot = note.Substring("opposed_in_war:".Length).Trim(); return; } if (note.StartsWith("opposed_in_plot:", StringComparison.Ordinal)) { e.FoeKind = FoeKind.Plot; e.RealmOrPlot = note.Substring("opposed_in_plot:".Length).Trim(); return; } if (note.StartsWith("killed_kin", StringComparison.Ordinal) || note.StartsWith("kin_slain", StringComparison.Ordinal)) { e.FoeKind = FoeKind.Blood; return; } if (note.StartsWith("mc_rematch:", StringComparison.Ordinal) || note.StartsWith("repeated_encounters:", StringComparison.Ordinal)) { e.FoeKind = FoeKind.Rematch; int colon = note.IndexOf(':'); if (colon >= 0 && colon + 1 < note.Length && int.TryParse(note.Substring(colon + 1).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int n)) { e.RematchCount = n; } return; } e.FoeKind = FoeKind.Generic; } private static string FormatFactBody(LifeSagaFact fact, bool unresolved) { string other = !string.IsNullOrEmpty(fact.Other.Name) ? fact.Other.Name : ""; switch (fact.Kind) { case LifeSagaFactKind.BondFormed: return string.IsNullOrEmpty(other) ? "Found love" : "Bound with " + other; case LifeSagaFactKind.BondBroken: return string.IsNullOrEmpty(other) ? "Lost a lover" : "Lost " + other; case LifeSagaFactKind.FriendFormed: return string.IsNullOrEmpty(other) ? "Made a true friend" : "Befriended " + other; case LifeSagaFactKind.ParentChild: if (!LifeSagaMemory.IsParentPerspective(fact)) { return string.IsNullOrEmpty(other) ? "Was born" : "Born to " + other; } return string.IsNullOrEmpty(other) ? "Had a child" : "Had child " + other; case LifeSagaFactKind.Kill: return string.IsNullOrEmpty(other) ? "Took a life" : "Slew " + other; case LifeSagaFactKind.Death: return string.IsNullOrEmpty(other) ? "Died" : "Slain by " + other; case LifeSagaFactKind.RivalEarned: return FormatRivalBody(fact, other); case LifeSagaFactKind.WarJoin: { string realm = FirstNonEmpty(fact.Note, fact.KingdomKey); if (unresolved) { if (!string.IsNullOrEmpty(other) && !string.IsNullOrEmpty(realm)) { return "Still at war with " + other + " of " + realm; } if (!string.IsNullOrEmpty(other)) { return "Still at war with " + other; } return string.IsNullOrEmpty(realm) ? "Still at war" : "Still at war over " + realm; } if (!string.IsNullOrEmpty(other) && !string.IsNullOrEmpty(realm)) { return "Went to war against " + other + " (" + realm + ")"; } if (!string.IsNullOrEmpty(other)) { return "Went to war against " + other; } return string.IsNullOrEmpty(realm) ? "Entered a war" : "Went to war (" + realm + ")"; } case LifeSagaFactKind.WarEnd: { string realm = FirstNonEmpty(fact.Note, fact.KingdomKey); return string.IsNullOrEmpty(realm) ? "War ended" : "War with " + realm + " ended"; } case LifeSagaFactKind.PlotJoin: { string plot = fact.Note ?? ""; if (unresolved) { if (!string.IsNullOrEmpty(other) && !string.IsNullOrEmpty(plot)) { return "Still tangled in " + plot + " with " + other; } if (!string.IsNullOrEmpty(other)) { return "Still plotting with " + other; } return string.IsNullOrEmpty(plot) ? "Still tangled in a plot" : "Still tangled in " + plot; } if (!string.IsNullOrEmpty(other) && !string.IsNullOrEmpty(plot)) { return "Plotted with " + other + " (" + plot + ")"; } if (!string.IsNullOrEmpty(other)) { return "Plotted with " + other; } return string.IsNullOrEmpty(plot) ? "Joined a plot" : "Joined " + plot; } case LifeSagaFactKind.PlotEnd: { string plot = fact.Note ?? ""; return string.IsNullOrEmpty(plot) ? "Plot resolved" : "Plot resolved (" + plot + ")"; } case LifeSagaFactKind.Founding: return !string.IsNullOrEmpty(fact.Note) ? "Founded " + fact.Note : "Founded a new home"; case LifeSagaFactKind.RoleChange: return FormatRoleNote(fact.Note); case LifeSagaFactKind.HomeGained: return "Found a home"; case LifeSagaFactKind.HomeLost: return "Lost a home"; case LifeSagaFactKind.HardArcLove: return FirstNonEmpty(StripBannedNote(fact.Note), "Bound with someone close"); case LifeSagaFactKind.HardArcGrief: return FirstNonEmpty(StripBannedNote(fact.Note), "Grief took hold"); case LifeSagaFactKind.HardArcCombat: return FirstNonEmpty(StripBannedNote(fact.Note), "Survived a hard fight"); case LifeSagaFactKind.HardArcWar: return FirstNonEmpty(StripBannedNote(fact.Note), "Marked by war"); case LifeSagaFactKind.HardArcPlot: return FirstNonEmpty(StripBannedNote(fact.Note), "Entangled in a plot"); case LifeSagaFactKind.CombatEncounter: return string.IsNullOrEmpty(other) ? "Fought again" : "Clashed with " + other; default: return FirstNonEmpty(StripBannedNote(fact.Note), "A turning point"); } } private static string FormatRivalBody(LifeSagaFact fact, string other) { string note = fact.Note ?? ""; if (note.StartsWith("opposed_in_war:", StringComparison.Ordinal)) { string realm = note.Substring("opposed_in_war:".Length).Trim(); if (!fact.Resolved) { if (!string.IsNullOrEmpty(other) && !string.IsNullOrEmpty(realm)) { return "Still at war with " + other + " of " + realm; } return string.IsNullOrEmpty(other) ? "Still at war" : "Still at war with " + other; } return string.IsNullOrEmpty(other) ? "Went to war" : "Went to war against " + other; } if (note.StartsWith("opposed_in_plot:", StringComparison.Ordinal)) { string plot = note.Substring("opposed_in_plot:".Length).Trim(); if (!fact.Resolved) { if (!string.IsNullOrEmpty(other) && !string.IsNullOrEmpty(plot)) { return "Locked against " + other + " in " + plot; } return string.IsNullOrEmpty(other) ? "Locked in a plot" : "Locked against " + other; } return string.IsNullOrEmpty(other) ? "Opposed in a plot" : "Opposed " + other + " in a plot"; } if (note.StartsWith("killed_kin", StringComparison.Ordinal) || note.StartsWith("kin_slain", StringComparison.Ordinal)) { return string.IsNullOrEmpty(other) ? "Blood feud over kin" : "Blood feud with " + other; } if (note.StartsWith("mc_rematch:", StringComparison.Ordinal) || note.StartsWith("repeated_encounters:", StringComparison.Ordinal)) { int colon = note.IndexOf(':'); int n = 0; if (colon >= 0) { int.TryParse(note.Substring(colon + 1).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out n); } if (n >= 2 && !string.IsNullOrEmpty(other)) { return "Has clashed " + n + " times with " + other; } return string.IsNullOrEmpty(other) ? "Fought again and again" : "Fought " + other + " again and again"; } return string.IsNullOrEmpty(other) ? "Feud continues" : "Feud with " + other; } private static string FormatRoleNote(string roleId) { if (string.IsNullOrEmpty(roleId)) { return "Rose in standing"; } if (roleId.IndexOf("king", StringComparison.OrdinalIgnoreCase) >= 0) { return "Became " + TitleLabel("King"); } if (roleId.IndexOf("leader", StringComparison.OrdinalIgnoreCase) >= 0) { return "Became a " + TitleLabel("Leader"); } if (roleId.IndexOf("alpha", StringComparison.OrdinalIgnoreCase) >= 0) { return "Became " + TitleLabel("Pack Alpha"); } if (roleId.IndexOf("chief", StringComparison.OrdinalIgnoreCase) >= 0 || roleId.IndexOf("clan", StringComparison.OrdinalIgnoreCase) >= 0) { return "Became " + TitleLabel("Clan Chief"); } if (roleId.IndexOf("captain", StringComparison.OrdinalIgnoreCase) >= 0 || roleId.IndexOf("army", StringComparison.OrdinalIgnoreCase) >= 0) { return "Became " + TitleLabel("Army Captain"); } // Unknown game/mod role ids are evidence, not player-facing copy. return "Rose in standing"; } private static string StripBannedNote(string note) { if (string.IsNullOrEmpty(note) || ContainsBannedPhrase(note)) { return ""; } return note; } private static string KingdomName(Actor actor) { try { return actor?.kingdom != null ? actor.kingdom.name ?? "" : ""; } catch { return ""; } } private static string CityName(Actor actor) { try { return actor?.city != null ? actor.city.name ?? "" : ""; } catch { return ""; } } private static string FirstNonEmpty(params string[] values) { if (values == null) { return ""; } for (int i = 0; i < values.Length; i++) { if (!string.IsNullOrEmpty(values[i])) { return values[i]; } } return ""; } } public enum RelationClass { None = 0, Bond, Friend, Kin, Foe, PlotPartner, Grief } public enum KinKind { None = 0, Parent, Child, Heir, Packmate, Kin } public enum FoeKind { None = 0, Rematch, War, Plot, Blood, Generic } public sealed class RelationEvidence { public static readonly RelationEvidence None = new RelationEvidence(); public RelationClass Class; public KinKind KinKind; public FoeKind FoeKind; public bool BondBroken; public bool BestFriend; public bool AlsoBond; public bool AlsoFoe; public int RematchCount; public string RealmOrPlot = ""; public long OtherId; public int Priority; }