using System; namespace IdleSpectator; /// The only ActorAsset → prose voice classifier. public static class ActivityVoiceResolver { public static ProseVoice Resolve(string speciesId) { ActorAsset asset = ActivityAssetCatalog.TryGetActorAsset(speciesId); return asset != null ? Resolve(asset) : ProseVoice.Unknown(speciesId); } public static ProseVoice Resolve(ActorAsset liveAsset) { if (liveAsset == null) { return ProseVoice.Unknown(); } string liveId = liveAsset.id ?? ""; if (IsVehicle(liveId)) { return Build( liveAsset, liveAsset, ActivityAssetKind.Vehicle, ActivityModifier.None, ActivityBodyTag.Vehicle, ActivityEcologyTag.Water, ActivityVoiceFamilies.Special, false, true); } ActivityModifier modifier = ResolveModifier(liveAsset); bool recoveryExpected = modifier == ActivityModifier.Undead && liveId.StartsWith("zombie_", StringComparison.OrdinalIgnoreCase); ActorAsset baseAsset = ResolveBaseAsset(liveAsset, modifier, out bool recoverySucceeded); if (baseAsset == null) { baseAsset = liveAsset; } ActivityBodyTag body = ResolveBody(baseAsset, liveAsset, modifier); ActivityEcologyTag ecology = ResolveEcology(baseAsset, body, modifier); string family = ResolveFamily(baseAsset, body); ActivityBodyTag effective = EffectiveTag(modifier, body); ActivityAssetKind kind = IsExplicitSpecial(liveId) ? ActivityAssetKind.Special : ActivityAssetKind.Creature; return Build( liveAsset, baseAsset, kind, modifier, effective, ecology, family, recoveryExpected, recoverySucceeded); } public static void ApplyToContext(ActivityContext ctx, ActorAsset liveAsset) { if (ctx == null) { return; } ProseVoice voice = Resolve(liveAsset); ctx.Voice = voice; ctx.SpeciesId = voice.LiveSpeciesId; ctx.BaseSpeciesId = voice.BaseSpeciesId; ctx.Family = voice.BaseFamily; ctx.MannerTag = TagName(voice.EffectiveActionTag); ctx.ModifierTag = ModifierName(voice.Modifier); ctx.IsCiv = voice.BaseIsCiv; ctx.IsHumanoid = voice.BaseIsHumanoid; ctx.IsAnimal = voice.BaseIsAnimal; if (liveAsset != null) { ctx.TaxonomicKingdom = liveAsset.name_taxonomic_kingdom ?? ""; ctx.TaxonomicPhylum = liveAsset.name_taxonomic_phylum ?? ""; ctx.TaxonomicClass = liveAsset.name_taxonomic_class ?? ""; ctx.TaxonomicOrder = liveAsset.name_taxonomic_order ?? ""; ctx.TaxonomicFamily = liveAsset.name_taxonomic_family ?? ""; ctx.TaxonomicGenus = liveAsset.name_taxonomic_genus ?? ""; ctx.TaxonomicSpecies = liveAsset.name_taxonomic_species ?? ""; } } public static string TagName(ActivityBodyTag tag) { return tag.ToString().ToLowerInvariant(); } public static string ModifierName(ActivityModifier modifier) { return modifier == ActivityModifier.None ? "" : modifier.ToString().ToLowerInvariant(); } private static ProseVoice Build( ActorAsset liveAsset, ActorAsset baseAsset, ActivityAssetKind kind, ActivityModifier modifier, ActivityBodyTag effective, ActivityEcologyTag ecology, string family, bool recoveryExpected, bool recoverySucceeded) { string liveId = liveAsset?.id ?? ""; string baseId = baseAsset?.id ?? liveId; ActivityBodyTag baseBody = kind == ActivityAssetKind.Vehicle ? ActivityBodyTag.Vehicle : ResolveBody(baseAsset, liveAsset, modifier); bool authored = ActivitySpeciesVoiceCatalog.HasCompleteVoice(baseId); ActivityVoiceSource source = kind == ActivityAssetKind.Vehicle ? ActivityVoiceSource.Vehicle : authored ? ActivityVoiceSource.AuthoredSpecies : ActivityVoiceSource.TaxonomyFallback; return new ProseVoice( liveId, baseId, family, baseBody, ecology, modifier, effective, kind, baseAsset != null && baseAsset.civ, baseAsset != null && (baseAsset.is_humanoid || baseAsset.civ), baseAsset != null && baseAsset.default_animal, authored ? ActivitySpeciesVoiceCatalog.VoiceKey(baseId) : baseId, source, recoveryExpected, recoverySucceeded); } private static ActivityModifier ResolveModifier(ActorAsset asset) { string id = asset?.id ?? ""; string wild = asset?.kingdom_id_wild ?? ""; if (EqualsIgnore(wild, "undead") || id.StartsWith("zombie_", StringComparison.OrdinalIgnoreCase) || EqualsIgnore(id, "zombie") || EqualsIgnore(id, "skeleton") || EqualsIgnore(id, "ghost")) { return ActivityModifier.Undead; } if (EqualsIgnore(wild, "fire_elemental") || id.StartsWith("fire_elemental", StringComparison.OrdinalIgnoreCase)) { return ActivityModifier.Elemental; } if (EqualsIgnore(wild, "mush") || id.StartsWith("mush_", StringComparison.OrdinalIgnoreCase)) { return ActivityModifier.Mush; } if (EqualsIgnore(wild, "tumor") || id.StartsWith("tumor_", StringComparison.OrdinalIgnoreCase)) { return ActivityModifier.Tumor; } if (EqualsIgnore(wild, "aliens") || EqualsIgnore(id, "alien") || EqualsIgnore(id, "ufo")) { return ActivityModifier.Alien; } if (EqualsIgnore(wild, "living_houses") || EqualsIgnore(id, "living_house") || EqualsIgnore(id, "assimilator") || EqualsIgnore(id, "printer") || EqualsIgnore(id, "god_finger") || EqualsIgnore(id, "crystal_sword")) { return ActivityModifier.Construct; } return ActivityModifier.None; } private static ActorAsset ResolveBaseAsset( ActorAsset liveAsset, ActivityModifier modifier, out bool recoverySucceeded) { recoverySucceeded = true; if (liveAsset == null || modifier != ActivityModifier.Undead) { return liveAsset; } string id = liveAsset.id ?? ""; string candidate = ""; if (id.StartsWith("zombie_civ_", StringComparison.OrdinalIgnoreCase)) { candidate = id.Substring("zombie_".Length); } else if (id.StartsWith("zombie_animal_", StringComparison.OrdinalIgnoreCase)) { candidate = id.Substring("zombie_animal_".Length); } else if (id.StartsWith("zombie_", StringComparison.OrdinalIgnoreCase)) { candidate = id.Substring("zombie_".Length); } if (string.IsNullOrEmpty(candidate)) { return liveAsset; } ActorAsset baseAsset = ActivityAssetCatalog.TryGetActorAsset(candidate); recoverySucceeded = baseAsset != null; return baseAsset ?? liveAsset; } private static ActivityBodyTag ResolveBody( ActorAsset baseAsset, ActorAsset liveAsset, ActivityModifier modifier) { string id = baseAsset?.id ?? liveAsset?.id ?? ""; string liveId = liveAsset?.id ?? id; string taxClass = baseAsset?.name_taxonomic_class ?? ""; string taxOrder = baseAsset?.name_taxonomic_order ?? ""; string taxFamily = baseAsset?.name_taxonomic_family ?? ""; if (IsVehicle(liveId)) return ActivityBodyTag.Vehicle; if (EqualsIgnore(id, "dragon")) return ActivityBodyTag.Dragon; if (EqualsIgnore(id, "crabzilla")) return ActivityBodyTag.Crab; if (EqualsIgnore(id, "crystal_sword")) return ActivityBodyTag.Crystal; if (EqualsIgnore(id, "snowman") || EqualsIgnore(id, "cold_one")) return ActivityBodyTag.Cold; if (EqualsIgnore(id, "living_plants")) return ActivityBodyTag.Plant; if (EqualsIgnore(id, "living_house") || EqualsIgnore(id, "god_finger")) return ActivityBodyTag.Construct; if (EqualsIgnore(id, "assimilator") || EqualsIgnore(id, "printer")) return ActivityBodyTag.Machina; if (EqualsIgnore(id, "sand_spider")) return ActivityBodyTag.Arachnid; if (EqualsIgnore(id, "worm")) return ActivityBodyTag.Crawler; if (id.StartsWith("ant_", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Insect; if (modifier == ActivityModifier.Elemental) { if (liveId.EndsWith("_horse", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Livestock; if (liveId.EndsWith("_snake", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Reptile; if (liveId.EndsWith("_slug", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Crawler; if (liveId.EndsWith("_blob", StringComparison.OrdinalIgnoreCase)) return ActivityBodyTag.Protist; return ActivityBodyTag.Mythic; } if (modifier == ActivityModifier.Mush) return ActivityBodyTag.Fungi; if (modifier == ActivityModifier.Tumor) return ActivityBodyTag.Tumor; if (modifier == ActivityModifier.Alien) return ActivityBodyTag.Alien; if (baseAsset != null && baseAsset.civ) return ActivityBodyTag.Humanoid; if (EqualsIgnore(taxClass, "aves")) return ActivityBodyTag.Bird; if (EqualsIgnore(taxClass, "insecta") || EqualsIgnore(taxOrder, "hymenoptera") || EqualsIgnore(taxOrder, "coleoptera") || EqualsIgnore(taxOrder, "lepidoptera") || EqualsIgnore(taxOrder, "diptera") || EqualsIgnore(taxOrder, "orthoptera")) return ActivityBodyTag.Insect; if (EqualsIgnore(taxClass, "arachnida") || EqualsIgnore(taxOrder, "scorpiones") || EqualsIgnore(taxOrder, "araneae")) return ActivityBodyTag.Arachnid; if (EqualsIgnore(taxClass, "amphibia")) return ActivityBodyTag.Amphibian; if (EqualsIgnore(taxClass, "malacostraca")) return ActivityBodyTag.Crab; if (EqualsIgnore(taxClass, "actinopterygii") || EqualsIgnore(taxClass, "chondrichthyes") || EqualsIgnore(taxClass, "cephalopoda") || EqualsIgnore(taxOrder, "testudines")) return ActivityBodyTag.Aquatic; if (EqualsIgnore(taxClass, "reptilia") || EqualsIgnore(taxOrder, "squamata") || EqualsIgnore(taxOrder, "crocodilia")) return ActivityBodyTag.Reptile; if (EqualsIgnore(taxFamily, "canidae")) return ActivityBodyTag.Canine; if (EqualsIgnore(taxFamily, "felidae")) return ActivityBodyTag.Feline; if (EqualsIgnore(taxFamily, "ursidae")) return ActivityBodyTag.Ursid; if (EqualsIgnore(taxFamily, "phocidae")) return ActivityBodyTag.Seal; if (EqualsIgnore(taxFamily, "procyonidae")) return ActivityBodyTag.Procyonid; if (EqualsIgnore(taxFamily, "hyaenidae")) return ActivityBodyTag.Hyaenid; if (EqualsIgnore(taxOrder, "primates")) return ActivityBodyTag.Primate; if (EqualsIgnore(taxOrder, "rodentia")) return ActivityBodyTag.Rodent; if (EqualsIgnore(taxOrder, "cingulata") || EqualsIgnore(taxFamily, "dasypodidae")) return ActivityBodyTag.Armadillo; if (EqualsIgnore(taxOrder, "artiodactyla") || EqualsIgnore(taxOrder, "perissodactyla") || EqualsIgnore(taxFamily, "bovidae") || EqualsIgnore(taxFamily, "suidae") || EqualsIgnore(taxFamily, "leporidae") || EqualsIgnore(taxFamily, "elephantidae") || EqualsIgnore(taxFamily, "rhinocerotidae") || EqualsIgnore(taxFamily, "camelidae") || EqualsIgnore(taxFamily, "equidae")) return ActivityBodyTag.Livestock; string kingdom = baseAsset?.name_taxonomic_kingdom ?? ""; if (EqualsIgnore(kingdom, "plantae")) return ActivityBodyTag.Plant; if (EqualsIgnore(kingdom, "fungi")) return ActivityBodyTag.Fungi; if (EqualsIgnore(kingdom, "protista")) return ActivityBodyTag.Protist; if (EqualsIgnore(kingdom, "machina")) return ActivityBodyTag.Machina; if (EqualsIgnore(kingdom, "mythoria")) return ActivityBodyTag.Mythic; if (EqualsIgnore(kingdom, "gregalia")) return ActivityBodyTag.Gregalia; // All remaining current animalia assets are terrestrial mammals or explicit specials. if (EqualsIgnore(taxClass, "mammalia")) return ActivityBodyTag.Livestock; if (!string.IsNullOrEmpty(id)) return ActivityBodyTag.Mythic; return ActivityBodyTag.Default; } private static string ResolveFamily(ActorAsset asset, ActivityBodyTag body) { if (asset != null && asset.civ) return ActivityVoiceFamilies.Humanoid; string kingdom = asset?.name_taxonomic_kingdom ?? ""; if (EqualsIgnore(kingdom, "plantae")) return ActivityVoiceFamilies.Plant; if (EqualsIgnore(kingdom, "fungi")) return ActivityVoiceFamilies.Fungi; if (EqualsIgnore(kingdom, "protista")) return ActivityVoiceFamilies.Protist; if (EqualsIgnore(kingdom, "machina")) return ActivityVoiceFamilies.Machina; if (EqualsIgnore(kingdom, "mythoria")) return ActivityVoiceFamilies.Mythic; if (EqualsIgnore(kingdom, "gregalia")) return ActivityVoiceFamilies.Gregalia; return body switch { ActivityBodyTag.Canine => ActivityVoiceFamilies.Canine, ActivityBodyTag.Feline => ActivityVoiceFamilies.Feline, ActivityBodyTag.Bird => ActivityVoiceFamilies.Bird, ActivityBodyTag.Insect => ActivityVoiceFamilies.Insect, ActivityBodyTag.Arachnid => ActivityVoiceFamilies.Arachnid, ActivityBodyTag.Amphibian => ActivityVoiceFamilies.Amphibian, ActivityBodyTag.Aquatic => ActivityVoiceFamilies.Aquatic, ActivityBodyTag.Seal => ActivityVoiceFamilies.Aquatic, ActivityBodyTag.Crab => ActivityVoiceFamilies.Crustacean, ActivityBodyTag.Reptile => ActivityVoiceFamilies.Reptile, ActivityBodyTag.Livestock => ActivityVoiceFamilies.Livestock, ActivityBodyTag.Plant => ActivityVoiceFamilies.Plant, ActivityBodyTag.Fungi => ActivityVoiceFamilies.Fungi, ActivityBodyTag.Protist => ActivityVoiceFamilies.Protist, ActivityBodyTag.Machina => ActivityVoiceFamilies.Machina, ActivityBodyTag.Mythic => ActivityVoiceFamilies.Mythic, ActivityBodyTag.Dragon => ActivityVoiceFamilies.Mythic, ActivityBodyTag.Cold => ActivityVoiceFamilies.Mythic, ActivityBodyTag.Crystal => ActivityVoiceFamilies.Special, ActivityBodyTag.Alien => ActivityVoiceFamilies.Special, ActivityBodyTag.Construct => ActivityVoiceFamilies.Special, ActivityBodyTag.Tumor => ActivityVoiceFamilies.Special, ActivityBodyTag.Gregalia => ActivityVoiceFamilies.Gregalia, ActivityBodyTag.Humanoid => ActivityVoiceFamilies.Humanoid, ActivityBodyTag.Vehicle => ActivityVoiceFamilies.Special, ActivityBodyTag.Default => ActivityVoiceFamilies.Default, _ => ActivityVoiceFamilies.Mammal }; } private static ActivityEcologyTag ResolveEcology( ActorAsset asset, ActivityBodyTag body, ActivityModifier modifier) { if (modifier == ActivityModifier.Undead) return ActivityEcologyTag.Undead; if (modifier == ActivityModifier.Alien) return ActivityEcologyTag.Alien; if (modifier == ActivityModifier.Construct) return ActivityEcologyTag.Machine; if (body == ActivityBodyTag.Vehicle || body == ActivityBodyTag.Aquatic || body == ActivityBodyTag.Seal) return ActivityEcologyTag.Water; if (body == ActivityBodyTag.Bird || body == ActivityBodyTag.Insect) return ActivityEcologyTag.Air; if (body == ActivityBodyTag.Plant) return ActivityEcologyTag.Plant; if (body == ActivityBodyTag.Fungi || modifier == ActivityModifier.Mush) return ActivityEcologyTag.Fungal; if (body == ActivityBodyTag.Machina) return ActivityEcologyTag.Machine; if (body == ActivityBodyTag.Cold) return ActivityEcologyTag.Cold; if (asset != null && asset.civ) return ActivityEcologyTag.Civilized; if (body == ActivityBodyTag.Mythic || body == ActivityBodyTag.Dragon || modifier == ActivityModifier.Elemental) return ActivityEcologyTag.Mythic; return ActivityEcologyTag.Land; } private static ActivityBodyTag EffectiveTag(ActivityModifier modifier, ActivityBodyTag body) { return modifier switch { ActivityModifier.Undead => ActivityBodyTag.Undead, ActivityModifier.Elemental => ActivityBodyTag.Elemental, ActivityModifier.Mush => ActivityBodyTag.Mush, ActivityModifier.Tumor => ActivityBodyTag.Tumor, ActivityModifier.Alien => ActivityBodyTag.Alien, ActivityModifier.Construct => ActivityBodyTag.Construct, _ => body }; } private static bool IsVehicle(string id) { return !string.IsNullOrEmpty(id) && id.StartsWith("boat_", StringComparison.OrdinalIgnoreCase); } private static bool IsExplicitSpecial(string id) { return EqualsIgnore(id, "crabzilla") || EqualsIgnore(id, "god_finger") || EqualsIgnore(id, "living_plants") || EqualsIgnore(id, "living_house") || EqualsIgnore(id, "printer") || EqualsIgnore(id, "assimilator") || EqualsIgnore(id, "crystal_sword"); } private static bool EqualsIgnore(string a, string b) { return !string.IsNullOrEmpty(a) && a.Equals(b, StringComparison.OrdinalIgnoreCase); } }