using System; using UnityEngine; namespace IdleSpectator; /// /// Loads WorldBox ui/Icons sprites for HUD chrome. /// public static class HudIcons { public static Sprite FromUiIcon(string iconName) { if (string.IsNullOrEmpty(iconName)) { return null; } try { Sprite sprite = SpriteTextureLoader.getSprite("ui/Icons/" + iconName); if (sprite != null) { return sprite; } // Some paths are lower-case in older content. return SpriteTextureLoader.getSprite("ui/icons/" + iconName); } catch { return null; } } public static Sprite FromAsset(ActorAsset asset) { if (asset == null) { return FromUiIcon("iconQuestionMark"); } try { Sprite sprite = asset.getSpriteIcon(); if (sprite != null) { return sprite; } } catch { // fall through } try { if (!string.IsNullOrEmpty(asset.icon)) { return FromUiIcon(asset.icon); } } catch { // ignore } return FromUiIcon("iconQuestionMark"); } public static Sprite FromActor(Actor actor) { if (actor == null) { return FromUiIcon("iconQuestionMark"); } return FromAsset(actor.asset); } public static Sprite FromSpeciesId(string speciesId) { if (string.IsNullOrEmpty(speciesId)) { return FromUiIcon("iconQuestionMark"); } try { ActorAsset asset = AssetManager.actor_library?.get(speciesId); if (asset != null) { return FromAsset(asset); } } catch { // fall through } return FromUiIcon("iconQuestionMark"); } /// /// Live unit sprite as shown in inspect UI (phenotype/kingdom tint), not the species icon. /// public static Sprite FromActorLive(Actor actor) { if (actor == null || !actor.isAlive()) { return null; } try { Sprite main = actor.calculateMainSprite(); if (main == null) { return FromActor(actor); } Sprite ui = DynamicActorSpriteCreatorUI.getUnitSpriteForUI(actor, main); if (ui != null) { return ui; } if (actor.hasColoredSprite()) { Sprite colored = actor.calculateColoredSprite(main); if (colored != null) { return colored; } } return main; } catch { return FromActor(actor); } } /// /// Actor-specific saga-rail frame only. Returns null for eggs, dead actors, or /// unusable render frames; callers decide whether to display a temporary fallback. /// public static Sprite FromActorLiveRailFace(Actor actor) { if (actor == null || !actor.isAlive()) { return null; } bool egg = false; try { egg = actor.isEgg(); } catch { egg = false; } if (egg) { return null; } try { Sprite main = actor.calculateMainSprite(); if (main == null || LooksLikeEggSprite(main)) { return null; } Sprite speciesIcon = FromActor(actor); bool civilized = actor.asset != null && actor.asset.civ; Sprite colored = null; try { // Some creatures report hasColoredSprite=false even though this returns the // palette-resolved live frame (cat: walk_0 -> gen_walk_0). colored = actor.calculateColoredSprite(main); } catch { colored = null; } Sprite ui = null; if (civilized) { try { ui = DynamicActorSpriteCreatorUI.getUnitSpriteForUI(actor, main); } catch { ui = null; } } // Civ units benefit from the combined inspect frame (body/head/equipment). if (civilized && IsUsableRailFace(ui) && !SameSpriteSource(ui, speciesIcon)) { return ui; } // Creature palette generation is the closest single-frame equivalent to the // live renderer. It must precede the raw atlas frame or palette-key colors leak. if (IsUsableRailFace(colored) && !LooksLikeEggSprite(colored) && !SameSpriteSource(colored, speciesIcon)) { return colored; } if (!civilized && IsUsableRailFace(main) && !SameSpriteSource(main, speciesIcon)) { return main; } // The UI helper is allowed to synthesize a copy of the asset portrait for // creatures. Source-region comparison cannot detect that copy, so never use // it as a non-civ "live" frame. Their main/colored animation is authoritative. if (civilized && IsUsableRailFace(ui) && !SameSpriteSource(ui, speciesIcon)) { return ui; } return civilized && IsUsableRailFace(main) && !SameSpriteSource(main, speciesIcon) ? main : null; } catch { return null; } } /// /// Readable saga-rail face: actor-specific live frame when available, else species icon. /// The combined helper is for non-caching presentation such as Cast cells. /// public static Sprite FromActorRailFace(Actor actor, string speciesFallbackId = null) { Sprite live = FromActorLiveRailFace(actor); if (IsUsableRailFace(live)) { return live; } string species = speciesFallbackId; if (string.IsNullOrEmpty(species) && actor?.asset != null) { species = actor.asset.id; } return FromSpeciesId(species); } public static bool IsUsableRailFace(Sprite sprite) { if (sprite == null) { return false; } try { // Actor animation frames are not square. Foxes and other long-bodied creatures // legitimately use 5-7px-tall frames, so a per-axis 8px floor rejects the live // animation and makes callers fall back to the standing species portrait. float w = sprite.rect.width; float h = sprite.rect.height; if (w < 2f || h < 2f || (w * h) < 4f) { return false; } if (sprite.texture == null) { return false; } return true; } catch { return false; } } /// True when two sprites point at the same atlas region, even as different objects. public static bool SameSpriteSource(Sprite a, Sprite b) { if (a == null || b == null) { return false; } if (ReferenceEquals(a, b)) { return true; } try { Rect ar = a.rect; Rect br = b.rect; return a.texture == b.texture && Mathf.Abs(ar.x - br.x) < 0.1f && Mathf.Abs(ar.y - br.y) < 0.1f && Mathf.Abs(ar.width - br.width) < 0.1f && Mathf.Abs(ar.height - br.height) < 0.1f; } catch { return false; } } /// Harness diagnostic for WorldBox's competing actor-frame sources. public static string DescribeActorRailSources(Actor actor) { if (actor == null || !actor.isAlive()) { return "actor=none"; } try { Sprite species = FromActor(actor); Sprite main = actor.calculateMainSprite(); Sprite colored = null; try { colored = main != null ? actor.calculateColoredSprite(main) : null; } catch { colored = null; } Sprite ui = null; try { ui = main != null ? DynamicActorSpriteCreatorUI.getUnitSpriteForUI(actor, main) : null; } catch { ui = null; } return "main=" + DescribeSprite(main, species) + " colored=" + DescribeSprite(colored, species) + " ui=" + DescribeSprite(ui, species) + " species=" + DescribeSprite(species, species); } catch (Exception ex) { return "sources_error=" + ex.GetType().Name; } } /// Harness: retained frame matches the actor's palette/UI-generated live frame. public static bool MatchesGeneratedActorFrame(Actor actor, Sprite retained) { if (actor == null || retained == null || !actor.isAlive()) { return false; } try { Sprite main = actor.calculateMainSprite(); if (main == null) { return false; } Sprite colored = null; Sprite ui = null; try { colored = actor.calculateColoredSprite(main); } catch { colored = null; } try { ui = DynamicActorSpriteCreatorUI.getUnitSpriteForUI(actor, main); } catch { ui = null; } return (IsUsableRailFace(colored) && SameSpriteSource(retained, colored)) || (IsUsableRailFace(main) && SameSpriteSource(retained, main)) || (IsUsableRailFace(ui) && SameSpriteSource(retained, ui)); } catch { return false; } } private static string DescribeSprite(Sprite sprite, Sprite species) { if (sprite == null) { return "none"; } try { return (sprite.name ?? "?") + ":" + sprite.rect.width.ToString("0") + "x" + sprite.rect.height.ToString("0") + (SameSpriteSource(sprite, species) ? ":species" : ":distinct"); } catch { return "invalid"; } } private static bool LooksLikeEggSprite(Sprite sprite) { if (sprite == null) { return false; } try { string n = sprite.name ?? ""; return n.IndexOf("egg", StringComparison.OrdinalIgnoreCase) >= 0; } catch { return false; } } public static Sprite ForChronicleKind(ChronicleKind kind) { switch (kind) { case ChronicleKind.Death: return FromUiIcon("iconDead") ?? FromUiIcon("iconSkulls"); case ChronicleKind.Kill: return FromUiIcon("iconKills") ?? FromUiIcon("iconAttack"); case ChronicleKind.Lover: return FromUiIcon("iconArrowLover"); case ChronicleKind.Friend: return FromUiIcon("iconFavoriteStar"); case ChronicleKind.World: return FromUiIcon("iconWar") ?? FromUiIcon("iconBooks"); case ChronicleKind.AgeChapter: return FromUiIcon("iconAge") ?? FromUiIcon("iconClock"); default: return FromUiIcon("iconClock"); } } /// /// Activity row icon from a task id, Beh* key, or milestone_* key. /// public static Sprite ForActivityKey(string key) { if (string.IsNullOrEmpty(key)) { return Task() ?? FromUiIcon("iconClock"); } string k = key.ToLowerInvariant(); if (k == "milestone_kill") { return ForChronicleKind(ChronicleKind.Kill); } if (k == "milestone_death") { return ForChronicleKind(ChronicleKind.Death); } if (k == "milestone_lover") { return ForChronicleKind(ChronicleKind.Lover); } if (k == "milestone_friend") { return ForChronicleKind(ChronicleKind.Friend); } if (ActivityStatusProse.TryParseTaskKey(key, out string statusId, out _)) { return FromStatusId(statusId) ?? FromUiIcon("iconConfused") ?? Task() ?? FromUiIcon("iconClock"); } if (ActivityHappinessProse.TryParseTaskKey(key, out string happinessId)) { return FromHappinessId(happinessId) ?? FromUiIcon("iconHappy") ?? Task() ?? FromUiIcon("iconClock"); } string verb = ActivityVerbMap.Resolve(key); if (string.IsNullOrEmpty(verb)) { verb = k; } switch (verb) { case "fight": case "warrior": return FromUiIcon("iconAttack") ?? FromUiIcon("iconKills") ?? Task(); case "hunt": return FromUiIcon("iconKills") ?? FromUiIcon("iconAttack") ?? Task(); case "lover": return FromUiIcon("iconArrowLover") ?? Task(); case "social": case "cry": case "swear": return FromUiIcon("iconTalk") ?? FromUiIcon("iconSpeech") ?? FromUiIcon("iconSocial") ?? FromUiIcon("iconArrowLover") ?? Task(); case "eat": return FromUiIcon("iconFood") ?? FromUiIcon("iconHunger") ?? Task(); case "sleep": case "dream": return FromUiIcon("iconSleep") ?? FromUiIcon("iconRest") ?? FromUiIcon("iconClock") ?? Task(); case "farm": return FromUiIcon("iconFarm") ?? FromUiIcon("iconCrops") ?? FromUiIcon("iconWheat") ?? Task(); case "work": case "haul": return FromUiIcon("iconShowTasks") ?? FromUiIcon("iconHammer") ?? FromUiIcon("iconWalker") ?? Task(); case "fire": case "ignite": case "extinguish": return FromUiIcon("iconFire") ?? FromUiIcon("iconLava") ?? Task(); case "flee": return FromUiIcon("iconFlee") ?? FromUiIcon("iconRun") ?? FromUiIcon("iconDanger") ?? FromUiIcon("iconAttack") ?? Task(); case "play": case "laugh": case "sing": return FromUiIcon("iconHappy") ?? FromUiIcon("iconFun") ?? FromUiIcon("iconMusic") ?? FromUiIcon("iconFavoriteStar") ?? Task(); case "read": return FromUiIcon("iconBooks") ?? FromUiIcon("iconBook") ?? Task(); case "heal": case "recharge": return FromUiIcon("iconHealth") ?? FromUiIcon("iconHeal") ?? FromUiIcon("iconHeart") ?? Task(); case "pollinate": return FromUiIcon("iconFlower") ?? FromUiIcon("iconBee") ?? FromUiIcon("iconNature") ?? Task(); case "fish": return FromUiIcon("iconFish") ?? FromUiIcon("iconFishing") ?? FromUiIcon("iconWater") ?? Task(); case "trade": return FromUiIcon("iconTrade") ?? FromUiIcon("iconCoin") ?? FromUiIcon("iconGold") ?? Task(); case "settle": case "group": return FromUiIcon("iconCity") ?? FromUiIcon("iconHome") ?? FromUiIcon("iconKingdom") ?? Task(); case "relieve": return FromUiIcon("iconPoop") ?? FromUiIcon("iconToilet") ?? Task(); case "confused": case "strange_urge": case "possessed": return FromUiIcon("iconConfused") ?? FromUiIcon("iconMadness") ?? FromUiIcon("iconPossessed") ?? FromUiIcon("iconQuestionMark") ?? Task(); case "steal": case "loot": return FromUiIcon("iconSteal") ?? FromUiIcon("iconLoot") ?? FromUiIcon("iconBag") ?? Task(); case "harvest_life": case "gather_life": return FromUiIcon("iconSoulHarvested") ?? FromUiIcon("iconSoul") ?? FromUiIcon("iconSkulls") ?? FromUiIcon("iconDead") ?? Task(); case "move": return FromUiIcon("iconWalker") ?? FromUiIcon("iconShowTasks") ?? Task(); case "wait": return FromUiIcon("iconClock") ?? Task(); default: return Task() ?? FromUiIcon("iconClock"); } } public static Sprite ForDeathManner(DeathManner manner) { switch (manner) { case DeathManner.Slain: return FromUiIcon("iconKills") ?? FromUiIcon("iconAttack") ?? ForChronicleKind(ChronicleKind.Death); case DeathManner.OldAge: return FromUiIcon("iconAge") ?? FromUiIcon("iconClock") ?? ForChronicleKind(ChronicleKind.Death); case DeathManner.Starvation: return FromUiIcon("iconHunger") ?? FromUiIcon("iconFood") ?? ForChronicleKind(ChronicleKind.Death); case DeathManner.Drowned: return FromUiIcon("iconRain") ?? FromUiIcon("iconWater") ?? ForChronicleKind(ChronicleKind.Death); case DeathManner.Burned: return FromUiIcon("iconFire") ?? FromUiIcon("iconLava") ?? ForChronicleKind(ChronicleKind.Death); case DeathManner.Plague: return FromUiIcon("iconPlague") ?? FromUiIcon("iconDisease") ?? ForChronicleKind(ChronicleKind.Death); case DeathManner.Divine: return FromUiIcon("iconDivineLight") ?? FromUiIcon("iconGodFinger") ?? ForChronicleKind(ChronicleKind.Death); case DeathManner.Accident: return FromUiIcon("iconBomb") ?? FromUiIcon("iconExplosion") ?? ForChronicleKind(ChronicleKind.Death); default: return ForChronicleKind(ChronicleKind.Death); } } public static Sprite Favorite() => FromUiIcon("iconFavorite") ?? FromUiIcon("iconFavoriteStar") ?? FromUiIcon("iconStar"); public static Sprite ExpandHistory() => FromUiIcon("iconBooks") ?? FromUiIcon("iconList") ?? FromUiIcon("iconClock"); public static Sprite Age() => FromUiIcon("iconAge"); public static Sprite Level() => FromUiIcon("iconLevels"); public static Sprite Kills() => FromUiIcon("iconKills") ?? FromUiIcon("iconAttack"); public static Sprite Task() => FromUiIcon("iconShowTasks") ?? FromUiIcon("iconWalker"); public static Sprite SexMale() => FromUiIcon("IconMale"); public static Sprite SexFemale() => FromUiIcon("IconFemale"); public static Sprite FromTrait(ActorTrait trait) { if (trait == null) { return null; } try { Sprite sprite = trait.getSprite(); if (sprite != null) { return sprite; } } catch { // fall through } try { if (!string.IsNullOrEmpty(trait.path_icon)) { return SpriteTextureLoader.getSprite(trait.path_icon); } } catch { // ignore } return null; } public static Sprite FromStatusId(string statusId) { if (string.IsNullOrEmpty(statusId)) { return null; } try { StatusAsset asset = AssetManager.status?.get(statusId.Trim()); if (asset == null) { return null; } return FromStatus(asset); } catch { return null; } } public static Sprite FromStatus(StatusAsset status) { if (status == null) { return null; } try { Sprite sprite = status.getSprite(); if (sprite != null) { return sprite; } } catch { // fall through } try { if (!string.IsNullOrEmpty(status.path_icon)) { return SpriteTextureLoader.getSprite(status.path_icon); } } catch { // ignore } return null; } public static Sprite FromHappinessId(string effectId) { if (string.IsNullOrEmpty(effectId)) { return null; } try { HappinessAsset asset = AssetManager.happiness_library?.get(effectId.Trim()); if (asset == null) { return null; } return FromHappiness(asset); } catch { return null; } } public static Sprite FromHappiness(HappinessAsset asset) { if (asset == null) { return null; } try { Sprite sprite = asset.getSprite(); if (sprite != null) { return sprite; } } catch { // fall through } try { if (!string.IsNullOrEmpty(asset.path_icon)) { return SpriteTextureLoader.getSprite(asset.path_icon); } } catch { // ignore } return null; } public static void Apply(UnityEngine.UI.Image image, Sprite sprite, bool preserveAspect = true) { if (image == null) { return; } image.sprite = sprite; image.enabled = sprite != null; image.preserveAspect = preserveAspect; image.color = Color.white; } }